Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ad8fda94 | ||
|
|
e602d88695 | ||
|
|
5d6627af50 | ||
|
|
a26fd32eba | ||
|
|
67d12929fc | ||
|
|
5fd2e8302c |
No files matched your search
@@ -22,9 +22,7 @@ frontend-web/node_modules/
|
||||
frontend-web/.pnpm-store/
|
||||
frontend-web/tsconfig.tsbuildinfo
|
||||
frontend-web/vite-dev.log
|
||||
frontend-web/vite-dev.err.log
|
||||
frontend-web/.rspack-cache/
|
||||
frontend-web/dist-firefox52/
|
||||
frontend-web/vite-dev.err.log
|
||||
|
||||
# Frontend local packages/archives/docs (non-source)
|
||||
frontend-web/pnpm-lock.yaml
|
||||
@@ -71,10 +69,10 @@ offline-backend-20260512/wheelhouse/
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db-*
|
||||
offline-backend-20260512/backend/.deer-flow/*
|
||||
offline-backend-20260512/backend/.env
|
||||
offline-backend-20260512/backend/.env.local
|
||||
offline-backend-20260512/backend/.extensions_config.json.lock
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/backend/.env
|
||||
offline-backend-20260512/backend/.env.local
|
||||
offline-backend-20260512/backend/.extensions_config.json.lock
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/stop_backend.sh
|
||||
offline-backend-20260512/check_backend.sh
|
||||
|
||||
@@ -90,36 +88,18 @@ offline-backend-20260512/backend/skill_evolution.json
|
||||
skills/
|
||||
hermes-agent-main/
|
||||
.gitignore
|
||||
offline-backend-20260512/backend/tests/*
|
||||
!offline-backend-20260512/backend/tests/test_large_file_safety.py
|
||||
# Seeder-owned Agent Skills are product source; keep them out of the broad
|
||||
# runtime `skills/` ignore above.
|
||||
!offline-backend-20260512/backend/app/gateway/routers/_*_seed_assets/skills/
|
||||
!offline-backend-20260512/backend/app/gateway/routers/_*_seed_assets/skills/**
|
||||
# Keep startup seed completeness and task-report conversion regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_seed_assets.py
|
||||
!offline-backend-20260512/backend/tests/test_latency_diagnosis_seed.py
|
||||
!offline-backend-20260512/backend/tests/test_query_slowdown_diagnosis_seed.py
|
||||
!offline-backend-20260512/backend/tests/test_task_report_convert.py
|
||||
!offline-backend-20260512/backend/tests/test_migration_compatibility.py
|
||||
# Keep project-scoped cross-conversation memory regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_integration.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_store.py
|
||||
# Keep answer-level feedback persistence, scoring, resolver, and migration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_answer_feedback.py
|
||||
# Keep wx common-agent category configuration regressions versioned.
|
||||
offline-backend-20260512/backend/tests/*
|
||||
!offline-backend-20260512/backend/tests/test_large_file_safety.py
|
||||
# Keep answer-level feedback persistence, scoring, resolver, and migration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_answer_feedback.py
|
||||
# Keep wx common-agent category configuration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_wx_common_agents.py
|
||||
# Keep the built-in public-document seed regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_gongwen_agent_seed.py
|
||||
!offline-backend-20260512/backend/tests/test_gongwen_documents.py
|
||||
# Keep the openGauss URL/dialect/schema compatibility regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_gauss_database.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
|
||||
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
|
||||
# Keep formal Word OOXML and embedded-font regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_word_export.py
|
||||
@@ -141,28 +121,28 @@ offline-backend-20260512/backend/tests/*
|
||||
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||
# Keep assistant-source snapshot publication atomic across re-import jobs.
|
||||
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||
# Keep assistant-source snapshot publication atomic across re-import jobs.
|
||||
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
|
||||
# Deep-research reliability regression coverage.
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
|
||||
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
|
||||
!offline-backend-20260512/backend/tests/test_menu_overrides.py
|
||||
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_upload_policy.py
|
||||
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
|
||||
!offline-backend-20260512/backend/tests/test_artifact_preview.py
|
||||
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
|
||||
!offline-backend-20260512/backend/tests/test_menu_overrides.py
|
||||
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_upload_policy.py
|
||||
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
|
||||
!offline-backend-20260512/backend/tests/test_artifact_preview.py
|
||||
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
|
||||
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
|
||||
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
|
||||
@@ -219,22 +199,22 @@ codegraph.mdc
|
||||
|
||||
docs/aidev-docs
|
||||
docs/backups
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
|
||||
!offline-backend-20260512/backend/skills/
|
||||
offline-backend-20260512/backend/skills/*
|
||||
!offline-backend-20260512/backend/skills/public/
|
||||
offline-backend-20260512/backend/skills/public/*
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
|
||||
!offline-backend-20260512/backend/skills/
|
||||
offline-backend-20260512/backend/skills/*
|
||||
!offline-backend-20260512/backend/skills/public/
|
||||
offline-backend-20260512/backend/skills/public/*
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
|
||||
|
||||
# uv lockfile (not tracked per project preference)
|
||||
offline-backend-20260512/backend/uv.lock
|
||||
|
||||
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
|
||||
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
|
||||
|
||||
# Local-only scratch directory (not committed)
|
||||
0622/
|
||||
@@ -242,8 +222,5 @@ offline-backend-20260512/backend/uv.lock
|
||||
# Python bytecode cache
|
||||
__pycache__/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# Local Firefox 52 test profiles
|
||||
.firefox52-profile*/
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -1,40 +1,5 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Retrieved files in chat (2026-10-07)
|
||||
|
||||
The shared `MessageGroup` renders `RetrievedFileList` below its reasoning/tool
|
||||
timeline, so retrieved originals remain visible when old tool steps collapse and
|
||||
do not depend on a model calling `present_files` or enabling reference mode.
|
||||
The file list starts collapsed, keeps the latest retrieved file visible with the
|
||||
total count, and expands the remaining files in reverse retrieval order.
|
||||
`core/messages/retrieved-files.ts` derives these cards only from completed skill
|
||||
invocations (a known skill tool name or a skill script path in an execution tool).
|
||||
Generic reads, listings, report generation tools, uploads and outputs are excluded;
|
||||
workspace paths require a structured retrieval/file container and exclude paths
|
||||
written in the same message group. It accepts structured file links and WeKnora identifiers such as `knowledge_id` and
|
||||
`document_file_name`. It handles JSON wrappers, MCP text blocks and bounded JSON
|
||||
following console logs, deduplicates by file identity and caps one group at 100
|
||||
files / 2,000 visited payload nodes. Do not extract file cards from assistant prose,
|
||||
failed tool calls, arbitrary document text or `/mnt/skills` / host filesystem paths.
|
||||
|
||||
The admin-only Settings → Appearance switch「技能检索文件展示」persists globally as
|
||||
`citation_display.retrieved_files_enabled` (default true). The existing citation-display
|
||||
PUT endpoint enforces administrator authorization. The UI waits for the setting
|
||||
before collecting files, shares its query cache, and refreshes active clients every
|
||||
60 seconds/on stale window focus. Disabling it hides retrieved cards, not normal artifacts.
|
||||
|
||||
Thread files reuse the authorized artifact endpoints and uploaded-file reader;
|
||||
knowledge files reuse the authorized LLMWiki original-file API and
|
||||
`WeKnoraSourceDrawer`. `/api/llmwiki/sources/resolve` maps provider knowledge-base
|
||||
ids to local mapping ids only after checking the viewer's existing read access.
|
||||
External links use HTTP(S) only, credential-free fetches for
|
||||
inline preview, and a native original-file link for download/fallback; never send
|
||||
the DeerFlow bearer token to them. External download behavior depends on the
|
||||
source's response headers and browser cross-origin rules. Unknown formats or
|
||||
unavailable Office sidecars fall back to downloading the original. Public shares
|
||||
use their existing file route and do not query private knowledge document APIs.
|
||||
Closing the shared preview dialog aborts in-flight requests and releases blob URLs.
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
@@ -117,7 +82,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **Routing**: React Router v7; top-level routes in `src/pages/PageRoutes.tsx`, workspace sub-routes in `src/pages/WorkspaceRoutes.tsx`
|
||||
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
|
||||
- **普通问答地图可视化**: shared `MarkdownContent` recognizes only dedicated `map-json` fences and otherwise preserves the legacy Markdown/Streamdown path byte-for-byte. `components/workspace/visualizations/` validates the bounded map contract with Zod, lazy-loads MapLibre, and provides loading/error/text fallbacks so malformed or unreachable maps never blank the message. The lead-agent prompt emits `map-json` only when the user explicitly asks for a map; flowcharts and other graphics continue through the existing Markdown/Mermaid path. The raster tile template comes from the single post-build setting `runtime-config.js` → `VITE_MAP_TILE_URL` (Google by default); replace it with the intranet `{z}/{x}/{y}` endpoint for disconnected deployments.
|
||||
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是地图网页模式读取的瓦片模板。设置 → 外观的管理员选项「HTML 地图」可选 `echarts`(本地 GeoJSON,`data-df-geo`,默认)或 `leaflet`(地图网页,`data-df-map`);运行时同时保留两种初始化,已生成页面不因切换失效。用户上传的栅格图若要作为页面背景,使用同一 Gateway 源下的签名地址 `/api/public/html-images/{token}` 写入 `background-image`。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
|
||||
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是初始化脚本读取的瓦片模板。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`,地图使用 Leaflet `data-df-map`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
|
||||
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
|
||||
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
|
||||
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
|
||||
@@ -128,9 +93,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
|
||||
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, openMode, loginParams, appendAuth, authTokenParam, authNameParam, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route (e.g. `/task/item-aiceh/step-analysis-audience`), `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). Independent-tab clicks toast and do nothing. **`url` buttons support two independent, coexisting login-append mechanisms**: (1) **`loginParams` (追加登录参数, like 应用管理/Light App Center)** — a list of `{key, source, customValue}` resolved at click time: `runTaskButton` reads `userInfo` (via `readUserInfo` from `strategy-components/lib/light-app`, embed-isolated through `authStorage`) and appends `?{key}={value}` for each (`source` = a userInfo field such as `accessToken`/`yUserId`, or `other` → literal `customValue`); empty list = nothing appended. Reuses `LOGIN_SOURCE_OPTIONS` from light-app; stored as JSON column `task_buttons.login_params_json` (`PortableJSON`, migration `20260625_01`). (2) **`appendAuth` (追加登录态)** — appends `?{authTokenParam}={token1}&{authNameParam}={username}` (default param names `token`/`username`) so the跳转目标外部系统 can identify the current user — e.g. `…/action/sentiment?token=<JWT>&username=lizhaoxing`. **Gated on token login**: `token1` = the `?authToken=` used by token-exchange login; `username` = the raw login name the backend resolved from `getTokenInfo`. Both are stashed at login by `persistLoginCredentials` into `login.token1`/`login.username` (`core/auth/login-credentials.ts`, scoped via [[scoped-storage]] so嵌入/独立会话隔离 + 随独立会话迁移). Only token login writes them → a non-token-login session has no `token1`, so `runTaskButton` silently skips the auth append (the jump still works, just without those two params). The frontend reads `username` straight from the new `UsernameLoginResponse.username` field (the stored email local-part is lossy — lowercased/normalized — so not used), **not** by decoding the JWT. Stored as columns `task_buttons.{append_auth, auth_token_param, auth_name_param}` (migration `20260625_02`). The admin page renders both editors for `url` buttons (loginParams 虚线框 + 追加登录态 switch). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. **Bottom 快捷跳转 bar groups by prev/next**: `TaskJumpBar` renders only「上一步/下一步」-type buttons in the bottom bar — a `prev` group (id 含 `prev` 或文案「上一步」, 描边次要样式 + ←图标) rendered **左侧**, then a `next` group (id 含 `next` 或文案「下一步」, 实心主色) on the **右侧** (container `justify-end`); everything else goes to the right-edge floating menu. The default 上一步 button (`rwfx-prev`, `url`, target = `DASHBOARD_AGENT_ROUTE` `/page/strategy/dashboard-agent`, `appendTaskId` + `idParam=taskId` → `?taskId=任务id`, `openMode=self`) 默认指向**应用内**「大屏绘制」Line truncated
|
||||
- **实时并发监控 (Concurrency / System-Pressure Monitor, admin-only)**: a live panel mounted at the **top of the admin 排行榜 page** (`components/workspace/admin/concurrency-monitor.tsx` `ConcurrencyMonitorPanel`, rendered in `pages/AdminUserLeaderboardPage.tsx`) showing **当前并发的大模型调用数 + 列表(用户/智能体/类型/已运行时长)**, each row with a 「停止」button (and a 「全部停止」) that **truly aborts** that run's in-flight model stream via `RunManager.cancel` (`POST /api/admin/active-runs/{run_id}/cancel`) — to relieve pressure when 使用人数过多. Two line charts (minute/hour toggle): **并发量折线图** (peak/avg, `GET /api/admin/active-runs/concurrency`, fed by a lightweight background sampler into `concurrency_samples`) and **大模型出字速度折线图** (tokens/sec, `GET /api/admin/active-runs/token-speed`, aggregated from `llm_call_metrics`) so a dip pinpoints where the bottleneck is. A **「后台采样」Switch** (`system_settings.json → concurrency_monitor.enabled`, **default off / opt-in**, `GET|PUT /api/admin/active-runs/settings`) turns the background sampling on/off **at runtime, no restart** (the live concurrency count + 「停止」 work even while sampling is off — only the 并发量 history chart needs it on); everything is designed to **never impact production** (one tiny INSERT/15s, retention-purged, best-effort). Client/hooks: `core/admin/active-runs.ts` + `core/admin/hooks.ts` (`useActiveRuns`/`useConcurrencySeries`/`useTokenSpeedSeries`/`useCancel*`/`useMonitorSettings`). See the backend CLAUDE.md「Admin Concurrency Monitor」.
|
||||
- **会话冲突与模型工具流恢复**: when a user submits a new question while the previous turn is still streaming, the backend's `RunManager.create_or_reject` rejects with HTTP 409 (`services.py` returns the Chinese detail「当前对话还在生成回答…」). `core/threads/hooks.ts` detects the conflict and routes it through `confirmOrRejoin`. Run-creation SSE responses expose `Content-Location` plus the SDK `Location` reconnect endpoint; `core/api/api-client.ts` records the last consumed event id, so an in-page rejoin resumes after that cursor instead of replaying tool arguments from `-1`. `write_file` argument updates carried by `messages-tuple` count as business activity. A known active run is probed only after 90 seconds without a business frame; the watchdog hides `lg:stream`, stops only the old browser reader, and rejoins without cancelling the backend run. Clean EOF, transport errors, 409 and watchdog recovery share one single-flight Promise to prevent duplicate joins/finalization. The backend `PatchedChatOpenAI` separately normalizes confirmed cumulative argument snapshots and bounded compatible response shapes while preserving standard deltas; its content-free diagnostics never log prompts, arguments or file bodies.
|
||||
- **React #185 与流式刷新保护**: every direct LangGraph `useStream` surface (main/agent chat, AI writing, Canvas and Studio notebook) uses shared `STREAM_RENDER_BATCHING = true`. The installed SDK implements numeric `throttle: 16` as a trailing debounce, which can indefinitely postpone UI updates during a continuous fast stream; `true` retains asynchronous next-task batching without requiring 16ms silence. `stream-error-classification.ts` detects `Maximum update depth exceeded` / production `#185` through bounded, cycle-safe wrappers. Main chat filters the SDK's stored error and duplicate submit rejection; `react-ui-error.ts` gives each send at most one automatic UI recovery, then pauses rejoin without cancelling the server run. Token activity must not reset that budget. The top-level `ReactUiErrorBoundary` catches only this render/commit error, shows a static friendly reload panel, and never automatically remounts the failing tree. Other errors are not swallowed. Shared toasts are deduplicated for 30 seconds; diagnostics retain error type, recovery phase, thread/run identifiers and component stack but never message/tool content. Real SDK SSE replay covers partial reasoning and write_file Markdown before generation ends; the browser boundary check triggers a real production React layout-effect loop and requires manual reload to recover. These safeguards do not prove the unknown production component loop is fixed.
|
||||
- **流式游标作用域**: resumable cursors belong to the current SDK message accumulator, not a module-global map. `createStreamAPIClient` isolates same-reader recovery; newly mounted chats and real thread switches must replay the retained prefix instead of borrowing another reader's consumed position. Clearing replaces the cursor map so late old-reader events cannot pollute the new scope. The shared `getAPIClient` does not rewrite replay-from-start cursors. `stream-cursor-replay.test.ts` uses the real SDK to protect against both duplicate tool arguments on reconnect and missing prefixes on reopening.
|
||||
- **会话冲突 409 中文化**: when a user submits a new question while the previous turn is still streaming, the backend's `RunManager.create_or_reject` rejects with HTTP 409 (`services.py` now returns a **Chinese** detail「当前对话还在生成回答…」). `core/threads/hooks.ts` `onError` detects the conflict (`isActiveRunConflictError`: 409 / "already has an active run" / the Chinese text). A premature SSE close (SDK treats it as success and restores the send button while the backend run is still occupying the thread) is recovered by `confirmOrRejoin`: hold the stop button, `joinStream` the live run, and on 409 auto-reconnect instead of a fake AI bubble. `sse_consumer` no longer ends a resumable (`on_disconnect=continue`) stream on a false-positive `is_disconnected()`.
|
||||
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
|
||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
@@ -141,10 +104,6 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
|
||||
### Configuration
|
||||
|
||||
普通问答与智能体问答的 `write_file` 流式展示:`namedStreamingWriteFilePath` 统一判断路径是否可用;缺少路径、路径尚未完整或只有占位名时,不生成虚拟文件来自动打开沙箱,而由 `message-group.tsx` 在步骤内用绿色小字展示正文。步骤参数合并 `tool_calls` 与 `tool_call_chunks`,后续收到真实路径时复用原工具调用 ID 恢复正常文件预览;`outputs` 下真实文件的自动打开和沙箱流式预览保持原流程。
|
||||
|
||||
沙箱文件正在生成时,ArtifactsProvider 的 `select` / `openStreamedArtifact` 阻止切到其他文件;手动操作提示「当前文件正在生成,请稍等片刻,完成后就可以切换查看其他文件。」,自动选择静默跳过。ChatBox 按选中文件的未完成 write_file 和运行状态注册锁,预览组件同时覆盖流式写入及全文改写;完成、停止或卸载时移除所属锁。同一个文件的虚拟预览和真实路径视为同一目标。
|
||||
|
||||
Backend config lives in `offline-backend-20260512/backend/config.yaml`. Values starting with `$` resolve as environment variables (e.g., `$OPENAI_API_KEY`). MCP servers and skills are configured in `extensions_config.json` in the same directory.
|
||||
|
||||
## Detailed Backend Reference
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
# 本地问答、资料库与技能并发验收
|
||||
|
||||
测试日期:2026-09-28 至 2026-09-29 凌晨。环境:Windows、约 8GB 物理内存、Python 3.12、Node 22、Gateway 单进程、SQLite、默认 MiniMax-M2.5。前端 Vite 5174,Gateway 8001。模型并发限流与 Redis 分布式门控未启用。
|
||||
|
||||
本次保留并验收了工作区原有的资料库索引、问答收尾、沙箱超时和流式恢复修改,另外修复了压测复现的技能刷新重复校验,以及真实地图问答暴露的内部待办提醒显示问题。测试结果不能等同于 Linux 多进程生产环境容量。
|
||||
|
||||
## 版本范围
|
||||
|
||||
开始测试时本地为 `3a07c476`,没有 `cognitive-situation-map`。随后拉取远程,发现 `origin/master` 已前进至 `3107f2cc`,包含该技能的初始化及 v2.6.0 精简运行时。保留本地修改后合入远程,合并提交为 `c9effe83`;最终绘图与多用户测试以合入后的代码为准。
|
||||
|
||||
旧本地版本里 `page-report-generation` 的地图仅支持中、美、日、台,因此其“不支持菲律宾”的反馈不能用于评价最新的 `cognitive-situation-map`。
|
||||
|
||||
## 真实问答
|
||||
|
||||
使用独立普通测试账户,阶梯间顺序运行,逐条检查预期测试关键词与结果、SSE `end`、持久化 `success`,结束后删除本轮生成的测试线程。1/3/5/10 路检查正文包含“测试”和数字 4;20 路增强为检查“测试正常”及明确的“2加2等于4”。这属于简单问答验收,不是复杂内容质量评测。
|
||||
|
||||
| 同时问答用户数 | 成功 | 首段答案中位耗时 | 流结束中位耗时 | 流结束最慢 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 1 | 1/1 | 5.792s | 6.439s | 6.439s |
|
||||
| 3 | 3/3 | 2.214s | 3.245s | 3.287s |
|
||||
| 5 | 5/5 | 2.885s | 3.702s | 4.373s |
|
||||
| 10 | 10/10 | 2.920s | 4.796s | 5.416s |
|
||||
| 20 | 20/20 | 4.364s | 10.622s | 11.135s |
|
||||
|
||||
累计 39/39 通过。20 用户整批耗时 11.829s,同期 health 12/12 成功、中位 13ms、最大 371ms。这里是一次阶梯短时突发,并未证明百用户持续长任务容量。
|
||||
|
||||
取消接口返回 204,58ms 后状态为 `interrupted`,测试线程删除返回 200。同期健康探测 20 次均成功,中位 9ms,最大 3.044s;说明仍存在数秒调度尖峰,不能仅凭平均值宣布无阻塞。
|
||||
|
||||
早期多个磁盘基准同时运行时,单路首段答案为 33.853s,日志显示同步 agent 构建 28.344s,health 也有 5s 超时。这批明确有本机资源争用,不纳入稳定容量结论。开发脚本启动的服务进程还曾消失,没有拿到退出或 reload 日志,原因未确认;期间的连接失败剔除。后续使用无热重载的固定进程复测。
|
||||
|
||||
## 资料库和知识库
|
||||
|
||||
- 资料库/知识库相关回归 186 项通过;资料库新存储 Memory/SQLite 专项另有 8 项通过,覆盖用户隔离、分页、原子换代、并发租约和旧任务围栏。
|
||||
- 真实测试账户看到 2 个共享知识库;选中的库包含 668 份文档、168 个 Wiki 页面。首次 hybrid 查询 3.58s,返回 5 条。
|
||||
- 排除合成磁盘负载后,空资料库、知识库目录和 hybrid 查询分别测试 1/4/8 并发,每档 8 次,总计 72/72 HTTP 200,零异常。
|
||||
|
||||
| 接口 | 8 并发中位耗时 | 8 并发最大耗时 |
|
||||
|---|---:|---:|
|
||||
| 资料库列表(空测试账户) | 165ms | 224ms |
|
||||
| 知识库目录 | 517ms | 557ms |
|
||||
| hybrid 检索(相同问题、5条结果) | 426ms | 441ms |
|
||||
|
||||
这组重复使用同账号和查询,包含热缓存效果;空账户列表不代表有大量文件的资料库。独立测试 SQLite 另生成了 101 个合成用户、10 万条资料元数据,480 次查询全部成功;其中单用户 5 万条资料关键词查询 P95 为 1.08s、首页 P95 为 130ms,但此轮同时有其他 IO 测试,只作为规模观察值。
|
||||
|
||||
资料库索引按 5 分钟过期刷新;进程异常退出时正在同步的租约最多等待 30 分钟恢复。大库关键词查询仍使用多字段包含匹配,不能据当前测试证明生产规模检索始终快速。
|
||||
|
||||
## 大技能与并发刷新
|
||||
|
||||
文件体积大和小文件数量多分别测试,使用隔离临时目录,不改真实技能内容。
|
||||
|
||||
| 场景 | 结果 |
|
||||
|---|---|
|
||||
| 真实技能副本,269 文件 / 2.75MiB,稳定缓存 64 并发 | 全成功,约 1.05s;早期混合负载观察值 |
|
||||
| 同副本改 1 个资产,32 并发,修复前混合负载 | 27 成功、5 个 30s 锁等待超时,复现问题 |
|
||||
| 独占配对:同副本改 1 个资产,32 并发,旧逻辑 | 32/32 成功,P95 13.700s,最大 14.842s |
|
||||
| 独占配对:同场景,新逻辑 | 32/32 成功,P95 / 最大 3.963s,P95 降低约 71% |
|
||||
| 128MiB 资产、34 文件,新逻辑 | 冷初始化 5.54s;变更后 32 并发 P95 0.584s,全成功 |
|
||||
| 约 160 技能、5120 个小文件(修复前规模观察) | 冷加载执行者约 107s,31/32 等待者超时;热校验约 18s |
|
||||
|
||||
修复让同一进程内、相同权限配置的一批重叠请求共享一次刷新及最终校验。等待者仍检查当前缓存身份、权限哈希和能力标记;失败或技能下线时不返回旧视图。新增 8 项回归连同原有 62 项,共 70 项通过。
|
||||
|
||||
**剩余风险:** 数千个小文件的单次 Windows 元数据扫描仍昂贵,本次修复没有消除这项成本,也未证明跨进程刷新能合并。不能把 32/64 个技能调用线程等同于 32/64 个真实模型用户。
|
||||
|
||||
实际调用 `gongwen-assistant` 读取技能并输出短通知,10.672s 内正常结束,无工具卡住。但答案把要求的 10月8日改成10月15日:通信/技能读取通过,内容日期遵循失败,不能记为整体语义验收通过。
|
||||
|
||||
## 绘图与界面
|
||||
|
||||
普通问答已实际生成菲律宾地图底图及马尼拉、吕宋北部、巴拉望、棉兰老四个点位,点位弹窗可打开。使用明确标注的虚构数据,未进行时事真实性评估。初次模型额外要求确认消息内地图,回复确认后完成,属于可见的额外交互成本。
|
||||
|
||||
这次真实对话发现 `todo_completion_reminder` 被当作用户问题展示。修复后,后端待办提醒带 `hide_from_ui`,前端也按两个历史内部名字过滤聊天和问题导航;真实用户引用 `<system_reminder>` 的文本仍保留。后端 3 项、前端新及关联分组 12 项通过。
|
||||
|
||||
最新 `cognitive-situation-map v2.6.0` 已经通过真实问答完整调用:读取技能和菲律宾参考 JSON、写入场景、调用技能自带 `node render.mjs`、检查文件、`present_files` 交付,42.481s 完成,SSE 正常结束并保存 `success`。产物标题为“菲律宾冲突认知态势图(功能验收示意)”,包含 4 个事件、4 个标注、3 个行为体和 2 条连线,明确标注所有事件与等级均为测试数据。输出 `status=ok`、无恢复和警告。模型曾发起两次过宽的目录 glob,被技能访问隔离拒绝后自行改用指定技能目录完成,没有循环卡住。
|
||||
|
||||
同一最新技能的直接渲染为 624ms;8 个并发独立渲染进程全部成功,范围 650–949ms。围栏/截断/缺失字段/约 600KiB 场景输入的恢复、非空目录拒绝覆盖、脚本结束标签转义均完成专项检查。真实 Windows LocalSandbox 虚拟路径经 PowerShell 调用 Node 成功(含冷 shell 7.85s)。CSS、JS、菲律宾底图和旗帜资源均返回 200;2.93MB 底图一次请求耗时 208ms。8 个渲染进程不等于 8 个同时生成长内容的模型用户。
|
||||
|
||||
**最新 HTML 的视觉验收未通过:** 测试账户能在会话中看到生成文件卡片,文件接口返回 200,源码模式能显示 HTML、正确资源地址和页面结构,但 Codex 内嵌浏览器的预览面板空白;DOM 中 iframe 的 `src` 为 `blob:http://127.0.0.1:5174/...`,可访问树仍为 `about:blank`。普通地图组件的可见性不能替代这个 HTML 的验收,渲染器返回成功也不能证明最终视觉正确。已保留空白预览截图,未将此项计为通过。
|
||||
|
||||
进一步在内存中执行真实 `prepareGeneratedHtml`:11,269 字节输入变为 14,597 字节,原有 3 个脚本逐字保留、DOM id 与外链 URL 均不变,新增兼容脚本语法有效。暂未发现生成器或预处理破坏 HTML 的证据;故障停在预览加载/Blob 导航阶段,具体原因未确认,不将其归为已知浏览器限制,也不宣称已修复。
|
||||
|
||||
界面还复现了 TDesign 新手引导闲置时的 `reading 'element'` 异常:`current=-1` 时其 resize 监听仍读取不存在的步骤。简洁模式与智能体两处包装组件已改为只有有效步骤才挂载 Guide。浏览器复测了文件面板开关、侧栏切换、手动启动、下一步、跳过,未再出现新的同类异常;完成/跳过的持久化语义保持不变。
|
||||
|
||||
## 合并后回归
|
||||
|
||||
- 完整 harness:274/274 通过,53.07s;包含前述技能及 Todo 子集,不重复累加。
|
||||
- 模型并发配置专项:10 项通过;种子技能资源:4 项通过。
|
||||
- 前端流式恢复专项:9 组共 44 项通过;待办可见性及相关消息分组:12 项通过。
|
||||
- 全量前端构建和类型检查最初并行运行时,系统空闲内存降到约 17MB,因此主动停止该轮类型检查,改为串行执行;该轮中断不作为代码失败。
|
||||
- `pnpm build` 通过(10m25s,包含前期资源争用,有大 chunk 警告)。其后新增的两个 Guide 挂载条件已通过单独语法检查和真实浏览器回归;另补齐问题导航抽取后的类型导入,不改变生成代码行为。
|
||||
- 最终 `pnpm typecheck` 仍未通过,共 73 项原有诊断;与测试前本地 `hang-typecheck.log` 的 75 项逐条比较,减少了消息分组测试的 2 项,没有新增诊断。本轮发现的 `QuestionItem` 类型导入遗漏已经修正,复跑后消失。剩余问题包括 `nextra` 依赖、旧 JSX 类型和既有组件/测试类型,未扩大范围处理,不能标记全量类型检查通过。
|
||||
|
||||
## 本地证据和复测入口
|
||||
|
||||
详细证据仅保留在 `.runtime/test-results/20260928-local-qa/`,不提交模型凭证、问答日志、测试数据库、构建 ZIP 或技能临时大目录:
|
||||
|
||||
- `qa-concurrency-results.json`:逐轮时间、结束事件、答案检查和清理结果。
|
||||
- `qa-concurrency-20-results.json`、`harness-full.xml`:20 个独立用户问答和完整 harness 回归。
|
||||
- `knowledge-stable-recheck.json`、`knowledge-library-report.md`:知识库与资料库稳定复测及限制。
|
||||
- `skills-benchmark-paired.json`、`skills-findings.md`:同机旧/新刷新配对;“旧逻辑”在隔离进程禁用新合并函数回放,不改服务源码。
|
||||
- `philippines-html.json`、`large-skill.json`:旧页面技能能力缺口与公文技能真实调用。
|
||||
- `philippines-map-ui.png`:普通地图真实浏览器渲染。
|
||||
- `cognitive-philippines.json`、`cognitive-skill-probe.json`:最新技能真实完整问答及渲染器专项。
|
||||
- `cognitive-preview-ui.png`:最新 HTML 在本轮内嵌浏览器出现空白的实际截图。
|
||||
- `frontend-build.log`、`frontend-typecheck-final.log`:全量构建与最终类型检查。
|
||||
|
||||
提交的回归入口包括 `packages/harness/tests/test_skill_view_refresh_coalescing.py`、`test_skill_view_waits.py`、`test_sandbox_command_liveness.py`、`test_run_completion_timeout.py`、`test_todo_reminder_visibility.py`,以及前端 `control-timeout`、`stream-terminal-recovery` 和 `todo-reminder-visibility` 测试。
|
||||
@@ -1,358 +0,0 @@
|
||||
# 技能并发与流式修复:配置、前端包与验收说明
|
||||
|
||||
日期:2026-09-27(2026-09-28 补充高并发运行配置)。
|
||||
|
||||
本说明对应本次提交的修改,包含模型流式兼容与恢复、React #185 错误处理和已完成的最小技能示例,
|
||||
以及可选的技能视图复用、版本缓存、模型计时日志队列和 Gateway 线程池扩容。
|
||||
部署前通过 `git log -1` 核对代码提交;仅修改配置或替换旧包不能确认已部署本次修复。
|
||||
|
||||
## 1. 先确认需要更新什么
|
||||
|
||||
- **后台代码也要更新**:四个新开关需要新版后台读取。只换前端 dist 或只把字段写进旧版 config.yaml 不会实现后台优化。
|
||||
- **前端**:使用本说明末尾的 ZIP,里面是完整 `dist/`,不是源码或 node_modules。
|
||||
- **仓库运行配置已开启优化**:两份 config.yaml 的四个开关均已显式设为 `true`。但已有服务器的持久化/外挂配置不会被仓库自动覆盖,仍需手工同步。
|
||||
- 开启线程池开关后可将上限调整为 100,但不提高模型服务商配额、不启用多 worker。
|
||||
|
||||
## 2. .env 需要修改哪些项
|
||||
|
||||
后台启动所使用的 `.env` 保持或修改为:
|
||||
|
||||
```dotenv
|
||||
# Gateway 进程数,不是线程池大小;当前内存流/运行管理器必须单进程。
|
||||
GATEWAY_WORKERS=1
|
||||
|
||||
# 默认就是 0;显式写 0 可防止误用不安全的多进程覆盖开关。
|
||||
DEERFLOW_ALLOW_UNSAFE_MULTIWORKER=0
|
||||
|
||||
# Gateway asyncio 线程池上限;还必须打开下一节的 asyncio_thread_pool_enabled。
|
||||
DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100
|
||||
```
|
||||
|
||||
适用于已更新的 `scripts/start_backend-online.sh` 和 `offline-backend-20260512/start_backend.sh`。
|
||||
若通过自定义服务直接运行 Uvicorn,也应确认实际命令是单 worker;仅修改 .env 不会改变硬编码的启动命令。
|
||||
启动环境中已导出的同名变量可能覆盖 .env,需一并核对。
|
||||
|
||||
前面三个技能/日志优化没有独立环境变量,它们配置在下一节的 `config.yaml`。
|
||||
线程池上限变量现已真正接入;修改 .env 后必须重新启动后台,Docker 应通过现有启动脚本重新创建容器,
|
||||
单纯 `docker restart` 不会更新容器创建时注入的环境变量。
|
||||
其余模型地址、密钥、数据库连接等保留服务器原值,不用本机 .env 覆盖生产文件。
|
||||
|
||||
### 100 个线程与环境变量优先级
|
||||
|
||||
`GATEWAY_WORKERS=100` **不是**把工具线程池调成 100,而是启动 100 个进程,当前部署不要这样设置。
|
||||
|
||||
`DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100` **已经实现**,兼容别名 `DEERFLOW_IO_THREADS=100`,
|
||||
两者任选其一即可。开启线程池开关后,按以下顺序取第一个非空值:
|
||||
|
||||
1. `DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE`(正式变量)
|
||||
2. `DEERFLOW_IO_THREADS`(兼容别名)
|
||||
3. `config.yaml` 的 `runtime_performance.asyncio_thread_pool_size`(默认 100)
|
||||
|
||||
有效范围为整数 **1..1024**。选中环境变量的值非法时启动失败并提示,不会静默退回默认小线程池。
|
||||
开关关闭时不读取环境变量来改变线程池,保留 Python 原默认。已有正式变量时,修改别名不会覆盖它。
|
||||
线程按需创建,100 是上限,不会预先常驻启动 100 个线程。
|
||||
|
||||
## 3. config.yaml 新增配置
|
||||
|
||||
在实际加载的 `config.yaml` **顶层**配置这一段,不能缩进到 `models`、`sandbox` 或 `skills` 内,
|
||||
也不要重复定义两个 `runtime_performance`。下面是程序在字段缺失时的兼容性默认值;
|
||||
当前仓库的两份可运行配置已使用后文的高并发开启值:
|
||||
|
||||
```yaml
|
||||
runtime_performance:
|
||||
# 一次工具调用内只获取一次已验证技能视图,默认 false。
|
||||
skill_view_reuse_enabled: false
|
||||
|
||||
# 版本未变时复用技能视图,默认 false。
|
||||
skill_view_cache_enabled: false
|
||||
|
||||
# 模型计时日志使用独立队列,不争抢耗时工具线程,默认 false。
|
||||
model_timing_nonblocking_enabled: false
|
||||
|
||||
# 是否覆盖 Gateway asyncio 默认线程池,默认 false。
|
||||
asyncio_thread_pool_enabled: false
|
||||
asyncio_thread_pool_size: 100
|
||||
```
|
||||
|
||||
如决定启用本次四项优化并将线程池上限设为 100,改为:
|
||||
|
||||
```yaml
|
||||
runtime_performance:
|
||||
skill_view_reuse_enabled: true
|
||||
skill_view_cache_enabled: true
|
||||
model_timing_nonblocking_enabled: true
|
||||
asyncio_thread_pool_enabled: true
|
||||
asyncio_thread_pool_size: 100
|
||||
```
|
||||
|
||||
四个开关独立生效,可以单独开关。关闭/省略对应项时,对应的性能优化走原实现。
|
||||
这四项不控制模型协议兼容修复或前端错误处理;全部关闭不等于回退所有代码。
|
||||
为了让已有模型实例和所有后台进程配置一致,修改后重启后台;回退也是改回 `false` 后重启。
|
||||
|
||||
### 确认没有改错配置文件
|
||||
|
||||
- 已设置 `DEER_FLOW_CONFIG_PATH` 时,以它指向的文件为准;Docker 内该值必须是容器内可访问路径。
|
||||
- 本仓库包含 `offline-backend-20260512/config.yaml` 和 `offline-backend-20260512/backend/config.yaml`;
|
||||
两份已同步为高并发开启值,但服务器要修改的仍是启动时实际加载/挂载的那一份。
|
||||
- 启动日志 `Loaded AppConfig from ...` 可确认路径,`RUNTIME_PERFORMANCE` 可确认实际开关值。
|
||||
|
||||
## 4. 开启后做了什么,没做什么
|
||||
|
||||
### 单次调用复用
|
||||
|
||||
同一次 `read_file`、`ls`、`glob`、`grep`、`bash` 等工具执行期间,技能检查、沙箱初始化、
|
||||
路径替换和输出脱敏共用一份已验证视图;退出调用后清理作用域,下一次调用重新验证。
|
||||
不会把一个问答最初的技能启停状态一直沿用到结束。
|
||||
|
||||
### 技能版本缓存
|
||||
|
||||
每次核对启停 JSON 内容、已知源/目标文件和目录的元数据及标记。
|
||||
版本一致时不重新发现/解析全部技能、不复制文件、不持有全局刷新锁。
|
||||
新增、编辑、删除、启停及支持文件变化触发重建,并保留跨进程锁合并并发刷新。
|
||||
|
||||
没有采用会延迟下线的 TTL 缓存;下一次工具调用能发现下线或无效配置并拒绝访问。
|
||||
已经开始执行的工具不因此被强行中断。
|
||||
缓存仅用于标准 `LocalSkillStorage`,自定义存储实现/子类保守回到原刷新路径。
|
||||
同一 Gateway 进程内,仅在多个热命中调用真正重叠时合并为一次元数据校验;
|
||||
该批调用结束后结果立即丢弃,新调用仍重新校验。启停文件哈希不同的调用不会加入旧校验,
|
||||
校验异常会对该批调用统一 fail-closed,且不会阻止后续调用重试。
|
||||
|
||||
热路径仍有 O(文件数) 元数据校验,慢磁盘/网络挂载仍可能耗时;这不是“所有文件操作零开销”,
|
||||
也没有把受锁保护的重建变成无锁写入。
|
||||
|
||||
### 模型计时日志
|
||||
|
||||
仅轻量计时/入队在回调中执行,日志 I/O 由独立线程消费,不占工具共享线程池或事件循环。
|
||||
队列上限 4096 条;日志端长期阻塞时会丢弃诊断并在恢复后汇总告警,不无限积压或阻塞模型输出。
|
||||
退出时最多等待 0.5 秒尽力刷新,异常退出可能丢失尾部日志。
|
||||
|
||||
这是 `ModelTimingCallback` 的优化,不会改变其他回调(如 RunJournal、可选追踪插件)、
|
||||
子智能体调度池、数据库池或模型上游的连接/速率限制。不能据此保证 100 个会话同时生成。
|
||||
|
||||
### Gateway 工具线程池
|
||||
|
||||
开启 `asyncio_thread_pool_enabled` 后,在后台启动时、LangGraph 初始化之前设置当前
|
||||
Gateway 事件循环的默认执行器。同步工具、该循环的 `asyncio.to_thread` 等可使用最多 100 个槽位。
|
||||
关闭时完全不替换默认执行器;退出时由拥有该事件循环的服务运行器清理。
|
||||
|
||||
这不是 AnyIO 的同步 HTTP 路由线程池,也不改变子智能体专用池或独立事件循环的默认池。
|
||||
线程池扩大只能减少可并行 I/O 的排队,不能让同一把技能锁同时被多人持有,也不会增加模型端的生成配额。
|
||||
|
||||
## 5. 日志检查
|
||||
|
||||
确认加载日志类似:
|
||||
|
||||
```text
|
||||
RUNTIME_PERFORMANCE skill_view_reuse_enabled=True skill_view_cache_enabled=True model_timing_nonblocking_enabled=True asyncio_thread_pool_enabled=True asyncio_thread_pool_size=100
|
||||
```
|
||||
|
||||
线程池最终是否生效,以另一条启动日志为准(进程 id 随实际启动变化):
|
||||
|
||||
```text
|
||||
GATEWAY_ASYNCIO_EXECUTOR enabled=true max_workers=100 source=DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE pid=12345
|
||||
```
|
||||
|
||||
如果仍显示 `enabled=false max_workers=python_default`,说明线程池开关没打开或改错了配置文件,
|
||||
不是已经调整到 100。改环境变量后若日志仍取旧值,检查是否重建了 Docker 容器及实际 env-file。
|
||||
|
||||
查看以下日志:
|
||||
|
||||
| 日志 | 用途 |
|
||||
| --- | --- |
|
||||
| `GATEWAY_ASYNCIO_EXECUTOR` | 实际线程池开关、最终上限、配置来源和进程 id;关闭时显示使用 Python 默认,不虚报 100 |
|
||||
| `SKILL_VIEW_CACHE status=refreshed` | 发生重建;包含 `lock_wait_ms`、`refresh_ms`、`total_ms` 和条目数 |
|
||||
| `SKILL_VIEW_CACHE status=joined` | 等待期间其他线程/进程已完成重建,本次直接复用 |
|
||||
| `SKILL_VIEW_CACHE status=hit` | 缓存命中;正常命中为 DEBUG,超过 500ms 的慢命中为 INFO |
|
||||
| `SKILL_VIEW_CACHE status=failed` | 刷新失败,旧能力不可继续使用,需要排查配置/磁盘 |
|
||||
| `LLM_CONNECT_START/OK`、`LLM_REQUEST_DONE/FAILED` | 模型调用生命周期、首块和总耗时 |
|
||||
| `LLM_CALL_RETRY/FAILED`、`OPENAI_TOOL_STREAM_SUMMARY` | 模型重试/兼容处理诊断 |
|
||||
| `LLM_TIMING_LOG_DROPPED` | 日志队列曾满;检查日志磁盘/采集端速度 |
|
||||
|
||||
如需查看正常缓存命中,可短时设置顶层 `log_level: debug`;排查后恢复 `info`。
|
||||
首块耗时不等于纯网络连接等待,仍包括上游排队、模型处理等阶段。单次 curl 成功也不能证明并发容量足够。
|
||||
本次日志不记录技能正文、模型正文或 API 密钥。
|
||||
|
||||
## 6. 前端替换与回退
|
||||
|
||||
1. 先备份服务器当前前端目录和 `runtime-config.js`,并备份实际后台 .env/config.yaml。
|
||||
2. 校验下载 ZIP 的 SHA-256 与本说明记录一致,再解压到一个新的发布目录。
|
||||
3. ZIP 包含 `dist/`;Web 根目录应指向其内部,确保根下直接能找到 `index.html`,不要误放成 `dist/dist/`。
|
||||
4. **保留/恢复服务器自己的 `runtime-config.js`**。核对生产后台地址、认证和嵌入参数;不要把构建机默认值当生产配置。
|
||||
也不要覆盖生产 .env 的密钥与数据库配置。恢复 runtime-config 后其单文件哈希自然与原包不同。
|
||||
5. 更新后台代码并按需设置四个开关,使用现有部署方式重启后台,再切换前端发布目录。
|
||||
6. 刷新浏览器缓存后检查普通问答、智能体问答、技能示例、读取技能、生成文件以及离开后重新打开正在生成的会话。
|
||||
7. 如需回退性能优化,后台关闭相应开关并重启,无需删除技能文件或技能缓存目录。
|
||||
如需回退模型协议或前端修复,须恢复对应的已备份代码/前端发布目录,单独关闭性能开关不够。
|
||||
|
||||
正式 Linux AMD64 离线镜像/发版仍通过独立的 `knowledge-base-bj/deerflow-offline-deployment`
|
||||
仓库流程执行;本次前端 ZIP 不是完整后台离线镜像。
|
||||
|
||||
## 7. 本地验证结果与限制
|
||||
|
||||
### 后台
|
||||
|
||||
- 提交前最终专项:技能/线程池/日志/启动配置及技能老回归 **166 项通过**;模型适配、错误分类、
|
||||
诊断失败保护、SSE 响应头、会话计时与真实 transport **123 项通过**(两组不重叠,共 289 项)。
|
||||
对应修改模块与测试 Ruff 通过,harness 未增加对 app 的反向依赖。后续没有执行生产模型并发压测。
|
||||
- 本次技能复用/缓存/配置、日志回调与相关技能目录、大文件安全专项:**79 项通过**;
|
||||
又补充 1 项开关关闭/重新开启的回退测试,通过缓存文件全部 13 项回归,合计 **80 项**。
|
||||
- 模型工具参数流/推理流、错误恢复、SSE 传输和启动 worker 校验另 **76 项通过**;相关专项共 **156 项通过**。
|
||||
- 后续新增线程池接入与配置专项 **45 项通过**(与前述配置测试有重叠,不相加充当独立总数):
|
||||
覆盖默认关闭、两个环境变量优先级/别名、非法值拒启、启动顺序,以及实际 **24 / 100 个本地阻塞任务同时执行**、
|
||||
退出后线程全部清理;新模块和相关测试 Ruff 检查通过。另一次联合技能/日志/启动配置回归 **88 项通过**。
|
||||
该测试只验证本地线程池,不是 100 路模型压测。
|
||||
- 线程池接入后再次尝试后台全量测试(首次失败即停止):**604 通过、2 跳过、1 失败**,
|
||||
仍停在修改前已复现的 `test_llm_metrics.py::test_middleware_records_success_with_speed`,不是全量通过。
|
||||
- 冷启动 8 线程并发只构建一次;真实第二进程复用持久化清单、不再发现技能。
|
||||
- 下线/无效配置、源文件变更、同长保留 mtime 编辑、目标篡改、Windows junction 保护均有回归覆盖。
|
||||
- 真实 LangChain 异步模型测试证明:默认工具线程池占满且日志处理器阻塞时,启用计时日志优化仍可开始、输出 token 并结束;未调用真实模型服务。
|
||||
- 修改后尝试全量测试,设置失败 5 项停止:**871 通过、2 跳过、5 失败**,并非全量通过。
|
||||
失败包括 `test_llm_metrics.py` 的 3 项记录时序断言、既有 Wiki `auto_sync` 配置与测试预期不一致、
|
||||
`test_model_management.py` 的 thinking 参数预期不一致。首项在本轮修改前即已复现;
|
||||
本轮没有修改指标中间件、模型工厂或 Wiki auto_sync 设置,不为过测试擅改这些业务配置。
|
||||
- 本次针对 #185/MD 的追加回归:模型真实 SDK 回放、工具参数、思考字段、日志、配置、线程池、
|
||||
错误中间件和传输联合 **145 项通过**。其中新增 SDK 回放 19 项,覆盖 8 种写文件形态,
|
||||
确认 JSON 尚未闭合时部分 MD 已传出、最终工具名称和参数正确;没有连接线上模型。
|
||||
|
||||
### 合成性能对比
|
||||
|
||||
Windows 11 / Python 3.12.13,本机临时目录:150 个技能、450 个文件,各模式预热一次后串行 10 次。
|
||||
旧刷新平均约 **3133ms**,缓存命中平均约 **806ms**(本次约 3.89 倍);
|
||||
`load_skills` 从 10 次降为 0 次。临时数据已清理。
|
||||
这不是生产 Linux 性能结论,也不是多人访问模型的承载压测。
|
||||
|
||||
### 前端与交付包
|
||||
|
||||
- 提交前最终前端专项 **60 项通过**(原 57 项,加同 reader 续接/新页面重播与迟到事件隔离 2 项、
|
||||
最小技能示例 1 项);真实生产 React 浏览器错误边界测试再次通过。
|
||||
- 最终全量类型检查仍有既有 **75 个诊断**,本次变更文件无新增诊断;记录在
|
||||
`.runtime/frontend-final-typecheck-20260927.log`。不要把构建成功等同全量类型检查通过。
|
||||
- 前端本次流式恢复、React #185 分类、技能示例、流式预览及相关权限等 **32 项专项通过**;
|
||||
修正新增测试文件一处 `.ts` 导入导致的 TS5097 后,相关 16 项再次通过。
|
||||
- 最新追加前端专项 **57 项通过**,含真实 SDK SSE → 文件参数 → MD 预览;旧 16ms 设置在连续流中
|
||||
没有通知刷新,修复后在 JSON 尚未闭合时持续更新。真实 Chrome + 生产 React 中用循环 layoutEffect
|
||||
触发 #185,验证静态友好兜底、无自动重挂、点击重新载入后恢复。
|
||||
- 最新 `pnpm build` **通过**:Vite 13,853 个模块,8 分 49 秒;存在大于 500KB 的 chunk 提醒,不是构建失败。
|
||||
构建日志 `.runtime/frontend-final-build-20260927.log`;本次修复重新打开会话的游标边界后已重新打包。
|
||||
- 前一版临时 localhost 静态 HTTP 烟测 **通过**:入口 HTML 及引用的主 JS/CSS/runtime 等 5 个资源均 200,
|
||||
响应字节 SHA-256 与磁盘一致,没有缺失资源;烟测服务已关闭。
|
||||
- 最新 ZIP 中 **886 个文件**均逐文件比对大小和 SHA-256,与本次 dist 一致;入口/CSS 的 99 个本地资源引用全部存在。包内无 `.env`,
|
||||
入口为 `dist/index.html`,包含 `dist/runtime-config.js`。
|
||||
- 全量 `pnpm typecheck` **未通过**:复测还有 **75 个诊断、涉及 29 个本次未修改文件**,
|
||||
包括现有 nextra / `@codemirror/state` 依赖、旧测试及 legacy QA 类型问题;
|
||||
本次新增测试文件的 TS5097 已修正并复测,本次前端变更文件无类型诊断。
|
||||
完整本地日志为 `.runtime/qa/frontend-delivery-typecheck-20260927.log`;
|
||||
不能把生产构建通过表述成整个项目类型检查通过。
|
||||
追加修复后本次产品变更文件无全量类型诊断,新增测试及依赖专项 TypeScript 检查为 0 诊断。
|
||||
- 附加 legacy Chrome CSS 检查 **未通过**,报三个本轮未修改文件
|
||||
`legacy-html-preview.test.ts`、`legacy-html-preview.ts`、`SolutionProjectLanding.tsx` 中的现代颜色函数字样;
|
||||
没有报本次 dist CSS 缺失回退,也不能据此宣称完成旧浏览器端到端兼容验收。
|
||||
|
||||
交付 ZIP(仓库根目录):
|
||||
|
||||
```text
|
||||
deerflow-frontend-dist-20260927-master-final.zip
|
||||
```
|
||||
|
||||
- ZIP 大小:35,869,210 字节(约 34.2 MiB)。
|
||||
- 此包替代 `deerflow-frontend-dist-20260927-191207-185-stream-fix.zip` 及更早版本;旧包保留,但不包含最终的游标作用域修复。
|
||||
- 解压后文件总大小:66,347,693 字节。
|
||||
- ZIP SHA-256:
|
||||
|
||||
```text
|
||||
7E4C1525BDEB686161FFF5AED12D36087407BBF4F472B87E78CA04DADD1DF371
|
||||
```
|
||||
|
||||
Linux 可用 `sha256sum deerflow-frontend-dist-20260927-master-final.zip`;
|
||||
Windows 可用 `Get-FileHash -Algorithm SHA256 .\deerflow-frontend-dist-20260927-master-final.zip`。
|
||||
|
||||
未连接生产环境进行真实多人模型问答端到端验收,不能宣称所有线上卡顿已被实测消除。
|
||||
|
||||
## 8. 关于 20 人同时使用
|
||||
|
||||
当前不能承诺 20 人同时问答一定流畅。本次验证了本地代码路径和构建产物,并没有验证你的两台服务器、
|
||||
上游模型地址/API key 配额、数据库/磁盘及实际技能耗时在 20 路负载下的容量。
|
||||
|
||||
部署新版后台、明确开启所需开关后,先在测试环境按 5 → 10 → 20 个独立会话逐级测试:
|
||||
|
||||
1. 纯聊天和包含技能读取/写文件的问答分开测试,保持提示长度和输出规模接近真实业务。
|
||||
2. 每级记录成功/失败/超时数、首字和总耗时的分位数,以及技能 `lock_wait_ms/total_ms`。
|
||||
3. 同步查看模型端 429/503、排队数量/首 token 延迟,以及应用 CPU、内存、磁盘和数据库等待。
|
||||
4. 若上游只能并行处理少量请求,应用扩线程无法增加上游容量;应控制并发或扩容模型服务。
|
||||
|
||||
20 个在线用户、20 条运行中的问答和 20 个正在模型端生成的请求不是同一指标。
|
||||
|
||||
## 9. #185 与 MD 流式追加修复
|
||||
|
||||
用户已确认思考模式正常;本节针对仍出现的 #185 和 MD 预览卡住。
|
||||
|
||||
- 四个直接流入口统一使用 `STREAM_RENDER_BATCHING=true`,避免安装版本 SDK 的数字 16ms
|
||||
尾部防抖不断推迟 UI 刷新;保留异步批处理,不改成每个片段同步强制渲染。
|
||||
- #185 流错误不会再按模型异常直接显示原始错误串;同一轮最多一次自动恢复,重复失败暂停重连,
|
||||
不主动取消后台任务,不循环重新发送问题。友好提示 30 秒去重。
|
||||
- 真正渲染/commit 阶段的 #185 由页面错误边界接住,进入静态提示,只能手动重新载入;
|
||||
不吞其他错误。控制台 `react_ui_render_error` 保留组件堆栈,帮助定位未知的线上循环组件。
|
||||
- 恢复游标与当前页面的消息累积器绑定:同一页面断线续接不重复拼接工具参数,离开后重新打开或
|
||||
切换会话则完整重播,不能复用全局旧游标跳过正文前缀;旧 reader 的迟到事件也不污染新页面。
|
||||
- 后端兼容重复工具名称,避免 `write_filewrite_file` 使工具路由和预览识别失效;
|
||||
`OPENAI_TOOL_STREAM_SUMMARY repeated_tool_names` 可确认是否命中。也修复了空 `error:{}` 被误判失败。
|
||||
- 诊断日志写入失败不会覆盖真实模型异常、取消或成功结果。错误脱敏后仍在内存中识别上下文超限,
|
||||
并识别 `invalid_api_key` 等结构化认证错误,防止本应修剪上下文或停止重试的请求反复排队。
|
||||
- **实时预览和物理落盘不同**:模型还在生成参数时展示虚拟 MD 内容;参数完整、工具校验通过后,
|
||||
`write_file` 才正式写沙箱文件。此次不把未完成的 JSON 直接写进最终文件。
|
||||
|
||||
本地已经复现并修复上述具体缺陷,但没有线上原始流/组件堆栈,不能保证它们解释全部生产故障。
|
||||
|
||||
## 10. `threads` 创建慢:数据库路由与阶段耗时
|
||||
|
||||
`POST /api/threads` 会先查/写应用数据库的 `threads_meta`,再调用检查点存储写入空 checkpoint;
|
||||
它不需要请求模型,也不会在创建时遍历所有旧问答。
|
||||
|
||||
当前仓库部署模板是 `database.backend: mysql`,同时 `checkpointer.type: postgres`、
|
||||
`checkpointer.use_postgres: false`。如果服务器保持这些值且没有 `DEER_FLOW_USE_PG_CHECKPOINTER`
|
||||
覆盖,实际是 **MySQL 元数据 + SQLite 检查点**,清理 PostgreSQL 不会加速这条链路。
|
||||
本地开发配置则是 SQLite。线上以实际加载的配置和环境覆盖为准,不凭数据库连接串是否存在判断。
|
||||
|
||||
新建/搜索接口新增 `GATEWAY_OPERATION_TIMING` 日志和 `Server-Timing` 响应头,
|
||||
浏览器 Network 的响应头也能看到,不必先拿服务器日志:
|
||||
|
||||
- `metadata_lookup` / `metadata_create`:应用元数据查询/写入。
|
||||
- `checkpoint_create`:检查点创建,包含 saver 锁/连接池等待和数据库写入,不能等同为纯 SQL 时间。
|
||||
- `metadata_search`:会话列表的元数据查询。
|
||||
- `handler_total`:进入处理函数后的耗时;**不含进入函数前的事件循环/鉴权/代理排队**。
|
||||
|
||||
模型图构建仍保持原来的任务上下文。新增 `AGENT_BUILD_TIMING`:超过 500ms 以 WARNING 记录,
|
||||
正常为 DEBUG。已在本地复现同步慢工厂阻塞同事件循环的其他请求,但不能直接裸搬到线程:
|
||||
延迟工具注册上下文需回传,MCP 初始化存在跨事件循环约束。此处目前是定位日志,不是已经消除阻塞。
|
||||
|
||||
本机安装的 SQLite saver 和 PostgreSQL saver 都存在实例级串行锁;后者使用连接池也不代表
|
||||
同一个 saver 自动并行处理所有检查点。扩大 asyncio 线程池不解除此锁。
|
||||
尚未取得线上分阶段耗时,因此不能承诺新建 10 秒已经根治,也不能承诺归档即可解决。
|
||||
新增计时与真实 SQLite 锁测试、已有分析项目/浏览器内部创建入口联合 **43 项通过**,
|
||||
其中计时专项 14 项;这是本地回归,不是线上延迟验收。
|
||||
|
||||
## 11. 30 天已结束会话的 PostgreSQL 归档 SQL
|
||||
|
||||
归档脚本单独交付,不进入本源码仓库,也不在启动时执行。使用 `pg_checkpoint_archive.sql` 搬移,
|
||||
`pg_checkpoint_restore.sql` 按提交批次恢复;两者需与 `pg_checkpoint_common.sql` 放在同一目录。
|
||||
应用数据库在 MySQL / PostgreSQL 的候选只读导出分别为
|
||||
`mysql_checkpoint_candidates.sql` / `pg_checkpoint_candidates.sql`。
|
||||
|
||||
按用户确认,候选仅包括 30 天未更新、所有运行已结束的会话,排除正在运行、待处理和等待用户输入的会话。
|
||||
PG 检查点没有应用会话终态字段,因此不能只按 `checkpoint.ts` 猜测,必须先从应用元数据导出候选 ID。
|
||||
脚本仅在确认实际启用 PG、全部写入服务已停、备份可恢复及双读回退关闭后允许提交。
|
||||
默认是预览/回滚;提交时三张原始表的全部 namespace 一起搬入新归档 schema,逐行校验后才删源行,
|
||||
恢复时不覆盖同 ID 的新数据,归档副本保留。不删除沙箱文件。
|
||||
|
||||
**范围与风险**:这是 PG 检查点冷归档,不是完整应用归档。它不搬 MySQL `threads_meta/runs`,
|
||||
因此旧会话列表入口可能仍在,但归档正文需要恢复后才能读取/续聊。归档到同一库的新表也不保证减少总磁盘占用。
|
||||
SQL 的验证记录与具体执行命令见单独交付包的 README;部署前仍须在生产备份恢复的隔离副本演练。
|
||||
不要跳过预检直接在生产提交。如果线上仍为 SQLite,这组 PG SQL 不适用。
|
||||
|
||||
独立包 `deerflow-db-archive-20260927.zip` 含 SQL、README 和复验测试,不含数据库数据、连接凭据或数据库二进制。
|
||||
已在 PostgreSQL 16.15 + 安装的 LangGraph checkpoint 3.1.2 原始建表结构上通过 13 项真实执行和 14 项静态检查;
|
||||
未执行真实 MySQL 候选导出,详见独立包 README。ZIP 共 9 个文件、20,983 字节,SHA-256:
|
||||
|
||||
```text
|
||||
E90A189DA7D7032527EED7CEC9B51DE1A30FB6D48B4382CE06AF1C287B670F59
|
||||
```
|
||||
@@ -1,73 +0,0 @@
|
||||
# 问答停在执行命令或读取技能:排查与修复
|
||||
|
||||
日期:2026-09-28。
|
||||
|
||||
本次检查针对普通问答和智能体问答:页面停在工具步骤,上游模型没有收到下一轮请求。
|
||||
以下是代码审查和受控回归确认的触发路径;尚未收到具体生产对话 ID 和现场日志,不能据此认定某次现场故障只有一个根因。
|
||||
|
||||
## 已修复的等待路径
|
||||
|
||||
| 位置 | 原来的触发条件 | 当前行为 |
|
||||
| --- | --- | --- |
|
||||
| LocalSandbox 命令执行 | `subprocess.run` 超时仅终止外层 shell,后代仍持有输出管道;Windows 超时收尾继续无期限 `communicate` | 独立进程组、关闭交互输入、临时文件采集输出;超时清理进程树并限制回收等待 |
|
||||
| 技能视图刷新 | 进程锁、跨进程文件锁和等待并发校验结果均没有统一等待上限 | 等待最多 30 秒后抛出明确工具错误;不删除正在执行的刷新,不使用未经校验的旧视图 |
|
||||
| AIO 沙箱 | 上次命令未返回时,后续命令或文件操作无限等待同一把锁 | 锁等待 30 秒后失败;不抢占旧请求的锁,不自动重复执行命令 |
|
||||
| 文件分段读取 | 超长单行先读完整行才裁剪;FIFO/设备文件可能一直等数据 | 达到当前行输出预算即返回;本地读取用非阻塞打开并检查普通文件,远端分段读取同步限制 |
|
||||
| 前端结束恢复 | 后端已 success/error,但旧 SSE 仍开着,SDK `isLoading` 一直为真;整段 SSE 期间的 `sendInFlight` 又把同一轮结束快照丢掉 | 核对轮次及运行 ID 后清理重连标记、关闭本地 reader、应用结束快照;不因此取消后端或覆盖新一轮 |
|
||||
| 前端恢复查询 | `/runs`、检查点或历史请求悬挂,占着 single-flight 恢复槽 | 普通 SDK 请求设置 30 秒超时;恢复失败仍保留后续检查机会,SSE 不受此总时长限制 |
|
||||
| 后端结束收尾 | 图已返回,但检查点检查、状态写入、日志刷新或标题同步永远不返回,导致 `end` 发不出去 | 终态检查、终态状态写入分别有 10 秒预算;后续元数据收尾共享 10 秒预算,独立 finally 发出 `end` |
|
||||
|
||||
技能锁超时走原有工具错误处理链,模型可收到失败结果并决定后续操作;不会把“超时”伪装成成功。
|
||||
元数据收尾超时只发生在图已经结束后,不给整个研究/子智能体执行增加统一短时限。
|
||||
终态检查点超时会报告运行失败,不能直接当成模型成功。
|
||||
|
||||
## 配置和上线
|
||||
|
||||
本次需同时更新后端代码和前端构建产物,重启后端后生效。只刷新页面或调线程数不能替代修复。
|
||||
本次未操作生产服务、未发布前端包;正式离线发版仍使用独立的 `deerflow-offline-deployment` 仓库。
|
||||
|
||||
本地命令保留原来的 600 秒默认执行时限,新版允许在实际加载的配置文件中调整:
|
||||
|
||||
```yaml
|
||||
sandbox:
|
||||
# 合并到已有 sandbox 段中,保留 use、mounts 等既有字段。
|
||||
command_timeout_seconds: 600
|
||||
```
|
||||
|
||||
该字段只控制 LocalSandbox 命令执行,必须为有限正数。未配置仍为 600 秒;不要把它误认为模型请求超时。
|
||||
命令输出每个流最多读入 1 MiB 头尾内容,再经过既有工具输出裁剪。
|
||||
运行中的命令约每 100ms 检查累计捕获输出,超过 16 MiB 请求终止;这是采样护栏,不是磁盘硬配额。
|
||||
正常退出后的显式后台进程需要自行重定向输出和管理生命周期。
|
||||
|
||||
前端已有的无业务事件检查窗口仍为 90 秒;这是启动后台状态核对的时机,不是宣判任务失败的时限。
|
||||
30 秒控制请求超时不影响合法的长时间 SSE 和工具执行。
|
||||
|
||||
## 仍需现场证据区分的情况
|
||||
|
||||
- 磁盘/NFS 本身阻塞的刷新执行者仍可能卡在操作系统 I/O;本次限制的是其他调用的等待,不宣称能终止任意内核 I/O。
|
||||
- 自定义技能脚本自身请求外部服务不返回,仍会等到本地命令时限;建议技能内各 HTTP 请求也设置连接/读取超时。
|
||||
- 整个图中的第三方异步工具、模型前中间件和检查点操作没有统一的“无事件即取消”规则,避免误杀合法研究任务。
|
||||
- 检查点回滚是实际数据修改,保持原有完成/取消语义,不把仍在执行的回滚丢到后台后开放下一轮。
|
||||
- 已提交给数据库的单条写入未必能被驱动撤销;取消和新轮次检查可阻止旧清理继续发起后续写入,不能保证撤回已经执行的 SQL。
|
||||
|
||||
再次出现时,记录对话 ID、运行 ID、最后一步和时间,并对照上游请求日志、运行状态及
|
||||
`RUN_COMPLETION_TIMEOUT run_id=... phase=terminal_checkpoint|terminal_status|metadata_cleanup`。
|
||||
技能锁超时会包含 `Timed out waiting for skill view ...`;沙箱命令超时会包含 `Command timed out`。
|
||||
不要仅凭页面最后显示“读取技能”就断定仍在读文件,它也可能是结束消息没有传到浏览器。
|
||||
|
||||
## 回归入口
|
||||
|
||||
- 后端 `packages/harness/tests/test_skill_view_waits.py`:锁竞争和并发校验超时、失败后恢复。
|
||||
- 后端 `packages/harness/tests/test_skill_view_cache.py`、`test_skill_view_tool_reuse.py`:既有启停与缓存安全性。
|
||||
- 后端 `packages/harness/tests/test_sandbox_command_liveness.py`:真实父子进程、输出、非交互输入及特殊文件/长行。
|
||||
- 后端 `packages/harness/tests/test_run_completion_timeout.py`:挂起的结束收尾、取消、新一轮隔离及正常长图执行。
|
||||
- 前端 `src/core/api/control-timeout.test.ts`、`src/core/threads/stream-terminal-recovery.test.ts`:控制请求超时及真实 SDK reader 恢复。
|
||||
- 前端既有 `stream-recovery`、`stream-display`、`stream-reasoning-replay`、`stream-cursor` 测试继续验证断流重连和消息显示。
|
||||
|
||||
## 本机验证结果
|
||||
|
||||
- 后端 `python -m pytest packages/harness/tests -q`:**263 passed**。旧同步日志断言补了显式测试配置,避免当前启用异步日志时与后台队列竞速;异步日志仍由独立测试覆盖,未改实际运行配置。
|
||||
- 前端八个相关测试文件打包后用 Node test runner 执行:**31 passed**;新增测试类型修正后其中三个新增用例再次通过。
|
||||
- 修改的后端代码和测试通过 Ruff;`git diff --check` 通过。
|
||||
- 完整前端类型检查未通过:**75 个 TypeScript 错误**,均在本次未修改的文件,涉及 `nextra` 缺失、旧 JSX 类型、既有测试类型等。本次修改文件及新增测试无诊断。详细输出保留在 `frontend-web/.runtime/hang-typecheck.log`(本机文件,不作为发版资产)。
|
||||
- 命令进程树测试在 Windows 使用真实子进程;Linux 进程组分支已审查,但本机未做 Linux 实跑,AIO 未连接实际容器。未用生产模型调用或真实业务技能执行验证。
|
||||
@@ -1,52 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" suppressHydrationWarning data-browser-compat="firefox52">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var root = document.documentElement;
|
||||
root.setAttribute("data-browser-compat", "firefox52");
|
||||
var theme = localStorage.getItem("strategy-theme");
|
||||
root.classList.remove("dark", "light");
|
||||
if (theme !== "light") root.classList.add("dark");
|
||||
} catch (error) {}
|
||||
})();
|
||||
(function () {
|
||||
try {
|
||||
if (typeof AbortSignal === "undefined" || typeof AbortSignal.timeout === "function") {
|
||||
return;
|
||||
}
|
||||
AbortSignal.timeout = function (milliseconds) {
|
||||
var controller = new AbortController();
|
||||
var timer = setTimeout(function () {
|
||||
var reason;
|
||||
try {
|
||||
reason = new DOMException("signal timed out", "TimeoutError");
|
||||
} catch (error) {
|
||||
reason = new Error("signal timed out");
|
||||
reason.name = "TimeoutError";
|
||||
}
|
||||
try {
|
||||
controller.abort(reason);
|
||||
} catch (error) {
|
||||
controller.abort();
|
||||
}
|
||||
}, milliseconds);
|
||||
controller.signal.addEventListener("abort", function () {
|
||||
clearTimeout(timer);
|
||||
});
|
||||
return controller.signal;
|
||||
};
|
||||
} catch (error) {}
|
||||
})();
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>CM 助手</title>
|
||||
<script src="/runtime-config.js" data-runtime-config></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,72 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const { transformSync } = require("@babel/core");
|
||||
const presetEnv = require("@babel/preset-env");
|
||||
|
||||
// Firefox 52 implements async functions, but async iteration arrived later.
|
||||
// Babel must therefore lower both async-generator declarations and `for await`
|
||||
// consumers. Leaving the latter to SWC turns them into a synchronous
|
||||
// `value[Symbol.iterator]()` loop and breaks LangGraph streams at runtime.
|
||||
const ASYNC_ITERATION_RE =
|
||||
/async\s+(?:function\s*)?\*|\bfor\s+await\s*\(/;
|
||||
|
||||
module.exports = function firefox52LegacyGeneratorLoader(source, inputMap) {
|
||||
if (!ASYNC_ITERATION_RE.test(source)) {
|
||||
this.callback(null, source, inputMap);
|
||||
return;
|
||||
}
|
||||
|
||||
this.cacheable?.(true);
|
||||
const common = {
|
||||
filename: this.resourcePath,
|
||||
babelrc: false,
|
||||
configFile: false,
|
||||
sourceType: "unambiguous",
|
||||
sourceMaps: false,
|
||||
compact: false,
|
||||
};
|
||||
|
||||
// Babel's regenerator transform cannot consume several modern expressions
|
||||
// inside async generators in one pass. Lower those expressions first while
|
||||
// deliberately preserving async/generator syntax, then run the Firefox 52
|
||||
// pass. Only 24 LangChain/LangGraph modules currently enter this slow path.
|
||||
const normalized = transformSync(source, {
|
||||
...common,
|
||||
presets: [
|
||||
[
|
||||
presetEnv,
|
||||
{
|
||||
targets: { ie: "11" },
|
||||
bugfixes: true,
|
||||
modules: false,
|
||||
useBuiltIns: false,
|
||||
exclude: [
|
||||
"@babel/plugin-transform-async-generator-functions",
|
||||
"@babel/plugin-transform-async-to-generator",
|
||||
"@babel/plugin-transform-regenerator",
|
||||
// Preserve `for await` for the second pass. Transforming it as a
|
||||
// plain `for of` here loses async-iterator semantics permanently.
|
||||
"@babel/plugin-transform-for-of",
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const result = transformSync(normalized?.code ?? source, {
|
||||
...common,
|
||||
presets: [
|
||||
[
|
||||
presetEnv,
|
||||
{
|
||||
targets: { firefox: "52" },
|
||||
bugfixes: true,
|
||||
modules: false,
|
||||
useBuiltIns: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
this.callback(null, result?.code ?? normalized?.code ?? source, inputMap);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
|
||||
const loader = require("./legacy-generator-loader.cjs");
|
||||
|
||||
function compile(source) {
|
||||
let output;
|
||||
let thrown;
|
||||
loader.call(
|
||||
{
|
||||
resourcePath: "langgraph-sdk/dist/ui/manager.js",
|
||||
cacheable() {},
|
||||
callback(error, code) {
|
||||
thrown = error;
|
||||
output = code;
|
||||
},
|
||||
},
|
||||
source,
|
||||
undefined,
|
||||
);
|
||||
if (thrown) throw thrown;
|
||||
return output;
|
||||
}
|
||||
|
||||
test("keeps for-await asynchronous while lowering it for Firefox 52", () => {
|
||||
const output = compile(`
|
||||
export async function consume(run) {
|
||||
for await (const value of run) {
|
||||
consumeValue(value);
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
assert.match(output, /_asyncIterator\(run\)/);
|
||||
assert.doesNotMatch(output, /run\[Symbol\.iterator\]/);
|
||||
assert.doesNotMatch(output, /for\s+await/);
|
||||
});
|
||||
|
||||
test("passes ordinary modules through without invoking Babel", () => {
|
||||
const source = "export const value = 42;";
|
||||
assert.equal(compile(source), source);
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import autoprefixer from "autoprefixer";
|
||||
import firefox52RgbVars from "../../scripts/postcss-firefox52-rgb-vars.mjs";
|
||||
import firefox52Where from "../../scripts/postcss-firefox52-where.mjs";
|
||||
|
||||
export default {
|
||||
plugins: [firefox52RgbVars, firefox52Where, autoprefixer],
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import tailwindcss from "tailwindcss";
|
||||
import darkDescendant from "../../scripts/postcss-dark-descendant.mjs";
|
||||
import firefox52RgbVars from "../../scripts/postcss-firefox52-rgb-vars.mjs";
|
||||
import firefox52Where from "../../scripts/postcss-firefox52-where.mjs";
|
||||
import autoprefixer from "autoprefixer";
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
tailwindcss,
|
||||
darkDescendant,
|
||||
firefox52RgbVars,
|
||||
firefox52Where,
|
||||
autoprefixer,
|
||||
],
|
||||
};
|
||||
@@ -1,229 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
function replaceRequired(source, search, replacement, label) {
|
||||
const result = source.split(search).join(replacement);
|
||||
if (result === source) {
|
||||
throw new Error(`Firefox 52 compatibility patch no longer matches ${label}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = function firefox52SourceCompatLoader(source) {
|
||||
this.cacheable?.(true);
|
||||
const id = this.resourcePath.replaceAll("\\", "/");
|
||||
|
||||
if (/\/p-queue\/dist\/index\.js$/.test(id) && source.includes("#idAssigner = 1n;")) {
|
||||
return replaceRequired(source, "#idAssigner = 1n;", "#idAssigner = 1;", "p-queue");
|
||||
}
|
||||
|
||||
if (/\/mdast-util-gfm-autolink-literal\/lib\/index\.js$/.test(id)) {
|
||||
return replaceRequired(
|
||||
source,
|
||||
"(?<=^|\\s|\\p{P}|\\p{S})",
|
||||
"",
|
||||
"mdast-util-gfm-autolink-literal",
|
||||
);
|
||||
}
|
||||
|
||||
if (id.includes("/oniguruma-parser/")) {
|
||||
return source
|
||||
.split("\\p{Alpha}\\p{Pc}").join("A-Za-z_")
|
||||
.split("\\p{AHex}").join("[A-Fa-f0-9]")
|
||||
.split("\\p{Alpha}").join("[A-Za-z]")
|
||||
.split('"gsu"').join('"gu"');
|
||||
}
|
||||
|
||||
if (/\/node_modules\/mermaid\/dist\/chunks\/.*\.mjs$/.test(id)) {
|
||||
// Mermaid's generated Jison lexers put `break` immediately after `return`.
|
||||
// Firefox 52 reports every one as an unreachable-code warning. Removing
|
||||
// those dead statements is semantics-preserving and keeps DevTools usable.
|
||||
return source.replace(
|
||||
/(\breturn(?:\s+[^;\r\n]+)?;)(\r?\n[ \t]*)break;/g,
|
||||
"$1$2",
|
||||
);
|
||||
}
|
||||
|
||||
if (/\/web-streams-polyfill\/dist\/ponyfill\.es5\.mjs$/.test(id)) {
|
||||
// The distributed ES5 ponyfill declares a local variable after an
|
||||
// unconditional return. Move the declaration before the return so Firefox
|
||||
// does not report it as unreachable while preserving its function scope.
|
||||
let result = replaceRequired(
|
||||
source,
|
||||
"function xt(e){return",
|
||||
"function xt(e){var o;return",
|
||||
"web-streams ponyfill local declaration",
|
||||
);
|
||||
return replaceRequired(
|
||||
result,
|
||||
";var o}function Yt",
|
||||
";}function Yt",
|
||||
"web-streams ponyfill unreachable declaration",
|
||||
);
|
||||
}
|
||||
|
||||
if (/\/node_modules\/@xyflow\/react\/dist\/esm\/index\.(?:js|mjs)$/.test(id)) {
|
||||
// React Flow 12.12 uses BigInt only as a monotonically increasing render
|
||||
// serial. Firefox 52 has no BigInt; Number preserves the queue semantics
|
||||
// because this counter cannot approach MAX_SAFE_INTEGER in one session.
|
||||
let result = replaceRequired(
|
||||
source,
|
||||
"useState(BigInt(0))",
|
||||
"useState(0)",
|
||||
"@xyflow/react queue serial initializer",
|
||||
);
|
||||
return replaceRequired(
|
||||
result,
|
||||
"setSerial(n => n + BigInt(1))",
|
||||
"setSerial(n => n + 1)",
|
||||
"@xyflow/react queue serial increment",
|
||||
);
|
||||
}
|
||||
|
||||
if (id.includes("/node_modules/react-router/")) {
|
||||
let result = source;
|
||||
if (result.includes("async function loadRouteModule(")) {
|
||||
const start = result.indexOf("async function loadRouteModule(");
|
||||
const endMarker = "\n\n// lib/dom/ssr/links.ts";
|
||||
const end = result.indexOf(endMarker, start);
|
||||
if (end < 0) {
|
||||
throw new Error("Firefox 52 compatibility patch no longer matches React Router route modules");
|
||||
}
|
||||
const compatible = `async function loadRouteModule(route, routeModulesCache) {
|
||||
if (route.id in routeModulesCache) {
|
||||
return routeModulesCache[route.id];
|
||||
}
|
||||
throw new Error("External React Router route modules require native dynamic import and are unavailable in Firefox 52");
|
||||
}`;
|
||||
result = result.slice(0, start) + compatible + result.slice(end);
|
||||
}
|
||||
if (result.includes("function prefetchModule(modulePath)")) {
|
||||
result = replaceRequired(
|
||||
result,
|
||||
`function prefetchModule(modulePath) {
|
||||
import(
|
||||
/* @vite-ignore */
|
||||
/* webpackIgnore: true */
|
||||
modulePath
|
||||
);
|
||||
}`,
|
||||
`function prefetchModule(modulePath) {
|
||||
return Promise.resolve(modulePath);
|
||||
}`,
|
||||
"React Router module prefetch",
|
||||
);
|
||||
}
|
||||
const dynamicImportPattern = /import\(\s*\/\* @vite-ignore \*\/\s*\/\* webpackIgnore: true \*\/\s*/g;
|
||||
const withoutIgnoredImports = result.replace(
|
||||
dynamicImportPattern,
|
||||
"firefox52UnsupportedDynamicImport(",
|
||||
);
|
||||
if (withoutIgnoredImports !== result) {
|
||||
result = `${withoutIgnoredImports}
|
||||
|
||||
function firefox52UnsupportedDynamicImport(modulePath) {
|
||||
return Promise.reject(new Error(
|
||||
"External React Router module " + modulePath + " requires native dynamic import",
|
||||
));
|
||||
}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (/\/node_modules\/zod\/v4\/core\/util\.js$/.test(id)) {
|
||||
return source
|
||||
.split('int64: [/* @__PURE__*/ BigInt("-9223372036854775808"), /* @__PURE__*/ BigInt("9223372036854775807")],')
|
||||
.join("int64: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],")
|
||||
.split('uint64: [/* @__PURE__*/ BigInt(0), /* @__PURE__*/ BigInt("18446744073709551615")],')
|
||||
.join("uint64: [0, Number.MAX_SAFE_INTEGER],");
|
||||
}
|
||||
|
||||
if (/\/langsmith\/dist\/_openapi_client\/internal\/utils\/path\.(?:js|cjs)$/.test(id)) {
|
||||
const original = `const invalidSegmentPattern = /(?<=^|\\/)(?:\\.|%2e){1,2}(?=\\/|$)/gi;
|
||||
let match;
|
||||
// Find all invalid segments
|
||||
while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
|
||||
invalidSegments.push({
|
||||
start: match.index,
|
||||
length: match[0].length,
|
||||
error: \`Value "\${match[0]}" can\\'t be safely passed as a path parameter\`,
|
||||
});
|
||||
}`;
|
||||
const compatible = `const invalidSegmentPattern = /(?:^|\\/)((?:\\.|%2e){1,2})(?=\\/|$)/gi;
|
||||
let match;
|
||||
// Firefox 52 has no RegExp lookbehind. Keep the separator in the full
|
||||
// match and report only the captured path segment.
|
||||
while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
|
||||
const segment = match[1];
|
||||
invalidSegments.push({
|
||||
start: match.index + match[0].length - segment.length,
|
||||
length: segment.length,
|
||||
error: \`Value "\${segment}" can\\'t be safely passed as a path parameter\`,
|
||||
});
|
||||
}`;
|
||||
return replaceRequired(source, original, compatible, "langsmith path validation");
|
||||
}
|
||||
|
||||
if (/\/langsmith\/dist\/utils\/sampling\.js$/.test(id)) {
|
||||
let result = replaceRequired(
|
||||
source,
|
||||
"const SAMPLING_HASH_MODULUS = 1000000n;",
|
||||
'const SAMPLING_HASH_MODULUS = typeof BigInt === "function" ? BigInt(1000000) : 1000000;',
|
||||
"langsmith sampling modulus",
|
||||
);
|
||||
return replaceRequired(
|
||||
result,
|
||||
"const bucket = XXH3_128(encoder.encode(identifier.toLowerCase())) % SAMPLING_HASH_MODULUS;\n return Number(bucket) / Number(SAMPLING_HASH_MODULUS) < samplingRate;",
|
||||
`const hash = XXH3_128(encoder.encode(identifier.toLowerCase()));
|
||||
const bucket = hash instanceof Uint8Array
|
||||
? new DataView(hash.buffer, hash.byteOffset, hash.byteLength).getUint32(0, false) % 1000000
|
||||
: hash % SAMPLING_HASH_MODULUS;
|
||||
return Number(bucket) / Number(SAMPLING_HASH_MODULUS) < samplingRate;`,
|
||||
"langsmith sampling fallback",
|
||||
);
|
||||
}
|
||||
|
||||
if (/\/langsmith\/dist\/utils\/xxhash\/xxhash\.js$/.test(id)) {
|
||||
const fallback = `
|
||||
function hash128WithoutBigInt(data) {
|
||||
const states = [2166136261, 2246822507, 3266489909, 668265263];
|
||||
const primes = [16777619, 3266489917, 668265263, 374761393];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const value = data[i] + i;
|
||||
for (let j = 0; j < states.length; j++) {
|
||||
states[j] = Math.imul(states[j] ^ (value + j * 97), primes[j]) >>> 0;
|
||||
states[j] = (states[j] ^ (states[j] >>> 13)) >>> 0;
|
||||
}
|
||||
}
|
||||
const result = new Uint8Array(16);
|
||||
const view = new DataView(result.buffer);
|
||||
for (let i = 0; i < states.length; i++) {
|
||||
let value = states[i] ^ data.length;
|
||||
value = Math.imul(value ^ (value >>> 16), 2246822507) >>> 0;
|
||||
value = Math.imul(value ^ (value >>> 13), 3266489909) >>> 0;
|
||||
view.setUint32(i * 4, (value ^ (value >>> 16)) >>> 0, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
`;
|
||||
let result = replaceRequired(
|
||||
source,
|
||||
"const n = (n) => BigInt(n);",
|
||||
'const n = (value) => typeof BigInt === "function" ? BigInt(value) : 0;',
|
||||
"langsmith BigInt initializer",
|
||||
);
|
||||
result = replaceRequired(
|
||||
result,
|
||||
"export function XXH3_128(data, seed = n(0)) {",
|
||||
`${fallback}\nexport function XXH3_128(data, seed = n(0)) {\n if (typeof BigInt !== "function" || typeof BigUint64Array !== "function" || typeof DataView.prototype.getBigUint64 !== "function") {\n return hash128WithoutBigInt(data);\n }`,
|
||||
"langsmith XXH3 fallback",
|
||||
);
|
||||
return replaceRequired(
|
||||
result,
|
||||
"export function xxh128ToBytes(hash128) {",
|
||||
"export function xxh128ToBytes(hash128) {\n if (hash128 instanceof Uint8Array) return hash128;",
|
||||
"langsmith byte conversion",
|
||||
);
|
||||
}
|
||||
|
||||
return source;
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
|
||||
const loader = require("./source-compat-loader.cjs");
|
||||
|
||||
function compile(source, resourcePath) {
|
||||
return loader.call({ resourcePath, cacheable() {} }, source);
|
||||
}
|
||||
|
||||
test("replaces only XYFlow's render serial BigInt operations", () => {
|
||||
const output = compile(
|
||||
"const [serial, setSerial] = useState(BigInt(0)); setSerial(n => n + BigInt(1));",
|
||||
"C:/repo/node_modules/@xyflow/react/dist/esm/index.js",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
output,
|
||||
"const [serial, setSerial] = useState(0); setSerial(n => n + 1);",
|
||||
);
|
||||
});
|
||||
|
||||
test("fails when the pinned XYFlow source no longer matches", () => {
|
||||
assert.throws(
|
||||
() => compile("const serial = 0;", "C:/repo/node_modules/@xyflow/react/dist/esm/index.js"),
|
||||
/compatibility patch no longer matches @xyflow\/react queue serial initializer/,
|
||||
);
|
||||
});
|
||||
|
||||
test("removes Mermaid Jison breaks that Firefox reports as unreachable", () => {
|
||||
const output = compile(
|
||||
'switch (rule) {\n case 1:\n return "TEXT";\n break;\n case 2:\n return 2;\n break;\n}',
|
||||
"C:/repo/node_modules/mermaid/dist/chunks/mermaid.core/flow.mjs",
|
||||
);
|
||||
|
||||
assert.doesNotMatch(output, /break;/);
|
||||
assert.match(output, /return "TEXT";/);
|
||||
assert.match(output, /return 2;/);
|
||||
});
|
||||
|
||||
test("moves the web streams ponyfill declaration before its return", () => {
|
||||
const output = compile(
|
||||
"function xt(e){return make(e);var o}function Yt(e){return e}",
|
||||
"C:/repo/node_modules/web-streams-polyfill/dist/ponyfill.es5.mjs",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
output,
|
||||
"function xt(e){var o;return make(e);}function Yt(e){return e}",
|
||||
);
|
||||
});
|
||||
@@ -13,22 +13,12 @@
|
||||
|
||||
模式在创建后不可修改,前后端都不提供修改入口。项目、对话归属和智能体访问权限在服务端校验;方案结构及每条对话所选智能体也以服务端配置为准。
|
||||
|
||||
## 普通问答跨会话记忆
|
||||
|
||||
- 仅普通问答项目显示「跨会话项目记忆」。它只受项目自身开关控制,与系统「问答记忆」总开关相互独立;成功完成的会话会在后台整理,状态包括等待沉淀、整理中、已就绪和异常。
|
||||
- 新会话提问时会召回同一账号、同一项目中其他会话的事实、决策、约束、偏好、术语与未决事项。不会读取其他项目或其他账号,也不会混用普通聊天/智能体的通用记忆。
|
||||
- 项目设置和问答页头部均可打开「项目记忆」面板,查看精简快照、条目可信度/重要度和来源对话;支持手工新增、编辑、固定、删除、清空以及从当前项目历史对话重建。项目暂停后仍可清空已有数据,但不能重建。
|
||||
- 编辑、删除或清空原对话时,服务端先让对应记忆失效,再修改消息;重建期间不注入旧快照。固定或手工条目在重建时保留,自动条目按来源 checkpoint 时间判断新旧,较早会话不会因后台任务晚完成而覆盖新的纠正。
|
||||
- 交互问答在首包前使用有界关键词召回,避免等待远程向量编码;后台整理仍可写入向量供显式或离线检索使用,编码不可用不影响跨会话记忆。输入提示块有条数与 token 上限,历史内容只作为非指令资料,当前消息优先。
|
||||
|
||||
## 代码与运行
|
||||
|
||||
- `src/pages/AnalysisProjectsPage.tsx`:普通问答与方案项目的新建、设置、历史和工作台布局。
|
||||
- `src/strategy-selection/StrategyProjectsPage.tsx`:「我的项目」三类项目列表及创建入口。
|
||||
- `src/pages/analysis-projects/ProjectConversation.tsx`:现有聊天、研究进度、方案画布的项目级组合。
|
||||
- `src/core/analysis-projects/api.ts`:Gateway 项目 API 与按账号隔离的缓存。
|
||||
- `src/pages/analysis-projects/ProjectMemoryPanel.tsx`:项目记忆状态、快照、来源和管理操作。
|
||||
- `src/core/analysis-projects/memory-status.ts`:总开关/项目开关、状态和按钮可用性映射。
|
||||
- 后端:`/api/analysis-projects`,对话仍通过现有 `/api/threads/{thread_id}/runs/*` 流式接口发送;服务端从不可变归属读取项目配置。
|
||||
|
||||
启动新版 Gateway 后自动创建项目表。生产持久化依赖已配置的数据库;memory 模式中的项目随服务重启丢失。前端沿用仓库的 Vite 开发与构建命令。
|
||||
@@ -43,14 +33,6 @@ python -m pytest tests/test_analysis_projects.py tests/test_deep_research_report
|
||||
|
||||
涵盖内存与 SQLite 存储、跨用户隔离、禁止修改模式、运行时覆盖伪造模式与知识库、更新配置、对话归属与失败清理、固定方案模板及知识库资料进入研究管线。
|
||||
|
||||
跨会话记忆专项:
|
||||
|
||||
```shell
|
||||
python -m pytest tests/test_analysis_project_memory.py tests/test_analysis_project_memory_store.py tests/test_analysis_project_memory_runtime.py tests/test_analysis_project_memory_integration.py tests/test_analysis_projects.py -q
|
||||
```
|
||||
|
||||
前端专项:`node --experimental-strip-types --test src/core/analysis-projects/memory-status.test.ts`,并执行 `pnpm build`。
|
||||
|
||||
部署前还需使用实际知识库和模型验证三类问答、流式断线续接及方案后续修改。接口模拟与单元测试不能替代真实模型联调。
|
||||
|
||||
## 2026-09-14 我的项目与知识库验收修复
|
||||
@@ -61,7 +43,7 @@ python -m pytest tests/test_analysis_project_memory.py tests/test_analysis_proje
|
||||
- 普通问答先创建具名项目,再在项目内创建多个对话。列表读取 `/api/analysis-projects` 的 `qa` 项目,不再读取、标记或自动归入全局普通聊天。
|
||||
- 项目设置更新保留既有知识库绑定。旧「项目空间」入口已移除并跳转「我的项目」;普通问答路由为 `qa/projects/groups/*`,方案生成路由为 `qa/projects/solutions/*`。
|
||||
- 策略生成卡片统一先显示思考/执行过程,再显示正文;评判、方案及展开视图沿用相同顺序。
|
||||
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步不重复展示问答记录,突出当前任务并扩大策略回答、评判正文;第三步将生成过程默认收起并为方案正文保留固定可视空间,集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载。保存后的项目卡也提供 Markdown/Word 下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
|
||||
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步只保留候选策略和评判选择,第三步集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
|
||||
- 资料库把已完成的策略优选综合方案作为独立「策略生成」来源展示,并标注具体策略项目名称;条目可回到策略项目第三步,也可直接下载 Markdown/Word。普通策略项目线程生成的文件沿用原文件下载链路并显示同类来源标识。
|
||||
- 方案生成使用独立项目和固定报告结构,通过 `deep_research_report` 直接生成并打开方案画布;策略优选项目不再出现在方案生成分类中。
|
||||
- 知识库管理页读取上游最新目录,避免远端删除后命中旧缓存;检索调用仍共享短期目录缓存。
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# Firefox 52 第三方依赖兼容清单
|
||||
|
||||
本文记录 `fix/firefox52-compat` 分支中会影响 Firefox 52 的依赖、处理方式和回归入口。版本以 2026-10-09 的 `pnpm-lock.yaml` 实际解析结果为准;升级表内依赖后必须重新执行文末检查。
|
||||
|
||||
## 分级
|
||||
|
||||
| 等级 | 含义 | 当前处理 |
|
||||
|---|---|---|
|
||||
| A | 原生可运行,或由项目统一 SWC 转换即可运行 | 使用 Rspack 的 Firefox 52 单目标转换 |
|
||||
| B | 语法可转换,但缺少浏览器 API | 在 `src/core/compat/firefox52.ts` 做能力检测后安装 polyfill |
|
||||
| C | 依赖源码含 async iterator 等需要额外 Babel 转换的语法 | 精确 allowlist 后交给专用 loader |
|
||||
| D | 含 BigInt、危险正则或平台实现差异,普通转译不能解决 | 固定版本并做带断言的精确源码替换或替代实现 |
|
||||
| E | Firefox 52 网络层不提供现代流式 fetch 等能力 | 提供保持业务协议的运行时回退 |
|
||||
|
||||
## 已验证依赖
|
||||
|
||||
| 依赖/能力 | 实际版本 | 等级 | 处理与验证 |
|
||||
|---|---:|:---:|---|
|
||||
| React / React DOM | 18.3.1 | A | 兼容分支从 React 19 降为 18.3;所有页面统一使用这一运行时。118 条路由矩阵用于发现 Provider/Hook 运行时问题。 |
|
||||
| React Router DOM | 7.18.4 | D | `source-compat-loader.cjs` 处理动态路由模块加载差异;Hash 路由、重定向、刷新、公开页和 iframe 均进入完整路由冒烟。 |
|
||||
| Tailwind CSS | 3.4.19 | A | 采用 Tailwind 3 生成静态 utility;PostCSS 展平 `@layer`,转换 `:where()`、暗色后代规则和 CSS 变量透明色。 |
|
||||
| Rspack | 2.2.8 | A | `target: ["web", "es5"]`,SWC/Lightning CSS 的目标均为 Firefox 52;使用普通 script 与 Rspack chunk loader。 |
|
||||
| `@langchain/core` | 1.2.17 | C | LangChain 包进入 async-iteration loader;真实问答验证 thread 创建、XHR 流、导航和最终回复。 |
|
||||
| `@langchain/langgraph-sdk` | 1.12.3 | C | 对 async generator 和普通 async 方法中的 `for await` 做两阶段 Babel 转换;回归测试防止退化为同步 `Symbol.iterator`。 |
|
||||
| LangSmith | 0.10.8 | D | 进入源码兼容 loader,替换 Firefox 52 不支持的 BigInt/哈希实现路径;产物静态扫描兜底。 |
|
||||
| `p-queue` | 6.6.2 / 9.3.3 | D | 精确处理 BigInt 并发计数;loader 找不到预期源码时直接让构建失败,避免静默输出危险代码。 |
|
||||
| React Flow (`@xyflow/react`) | 12.12.0 | D | 仅把内部渲染序号的 `BigInt(0)` 和递增改为 Number;流程图相关路由已在 Firefox 52 中加载。 |
|
||||
| Shiki | 3.15.0 | D | 重型模块按需分块;其 Oniguruma JS 引擎进入下面的正则兼容处理。 |
|
||||
| Oniguruma parser | 0.12.2 | D | 移除 Firefox 52 不支持且不改变当前匹配语义的正则 flag;代码高亮页面与产物语法检查共同验证。 |
|
||||
| Mermaid | 11.17.2 | C | 由 Rspack/SWC 转换并按需加载;Markdown/研究/会商入口纳入路由矩阵。 |
|
||||
| Zod | 3.24.x(直接依赖) | D | 依赖源码进入兼容 loader,处理 BigInt 上下界;扫描最终 chunks 是否残留危险语法。 |
|
||||
| MapLibre GL | 4.7.1 | D | 固定在可转换版本;保持 Leaflet/静态信息回退,避免升级后引入 Firefox 52 无法执行的 Worker/语法。 |
|
||||
| CodeMirror / Tiptap / KaTeX / Marked | 锁文件版本 | A/C | 由 SWC 统一转换并路由级加载;Markdown 编辑器、预览和 Word 导出均有真实浏览器冒烟。 |
|
||||
| Radix UI / TDesign / cmdk | 锁文件版本 | A/B | Pointer Events、ResizeObserver、Portal 点击和滚动锁由兼容入口及分层 CSS 处理;菜单、选择器、弹窗、Tabs 有点击测试。 |
|
||||
| `docx` / `file-saver` | 9.5.1 / 2.0.5 | B | 正式 Word 由后端生成;Firefox 52 冒烟验证非空 DOCX Blob、MIME 和 `.docx` 下载文件名。 |
|
||||
| xlsx 解压 | fflate 0.8.3 | D | 浏览器端解压路径改为 fflate,避开原实现的现代能力依赖。 |
|
||||
|
||||
## 浏览器 API 与平台回退
|
||||
|
||||
`src/core/compat/firefox52.ts` 在应用入口最先加载,按能力补充 AbortController、AbortSignal、Observers、Web Streams、Pointer Events、`requestSubmit`、`scrollIntoView`、Clipboard、Blob/File、UUID 等能力。所有补丁先检查原生实现,现代浏览器不会被无条件覆盖。
|
||||
|
||||
Firefox 52 的 `fetch()` 没有可供业务消费的流式 `Response.body`。`src/core/compat/firefox52-fetch.ts` 用 `XMLHttpRequest.onprogress` 构造兼容的增量响应,保留请求头、鉴权、取消、错误状态和 SSE/NDJSON 分片语义。该路径属于 E 级平台回退,不能删除后只依赖 Web Streams polyfill。
|
||||
|
||||
## 升级规则
|
||||
|
||||
1. 修改表内版本后先运行 `pnpm test:firefox52-build`。精确源码片段不匹配会直接失败,此时需重新审计依赖实现,不能放宽成无条件替换。
|
||||
2. 执行 `pnpm build:firefox52 && pnpm check:firefox52`,确认最终 HTML、CSS、JS 与所有动态 chunks 都能被 Firefox 52 解析。
|
||||
3. 启动 Firefox 52 后执行 `pnpm smoke:firefox52:themes`、`pnpm smoke:firefox52:live` 和 `pnpm smoke:firefox52:full`。
|
||||
4. LangChain、LangGraph、LangSmith、p-queue、React Router、XYFlow、Oniguruma、Shiki、Mermaid、MapLibre 任一升级,都必须重新跑真实流式问答和相关懒加载路由,不能只看构建成功。
|
||||
|
||||
## 当前限制
|
||||
|
||||
- WebGL 的最终可用性还受显卡、远程桌面和驱动影响;无 WebGL 环境应走现有的二维或文本信息回退。
|
||||
- `1366×768`、`1920×1080`、`2560×1440` 的精确视觉截图需要对应大小的真实显示环境。当前自动化会读取真实 DOM 尺寸和样式,但不会伪造操作系统无法提供的物理视口。
|
||||
- 创建、删除、发布、回滚等会修改业务数据的管理操作不在自动点击矩阵中;这些页面会验证加载、可见 Tabs 与未捕获异常,数据写入流程由专门测试数据验收。
|
||||
@@ -1,56 +0,0 @@
|
||||
# Firefox 52 路由与交互验收说明
|
||||
|
||||
## 自动化范围
|
||||
|
||||
`pnpm smoke:firefox52:full` 直接连接 Firefox 52 的远程调试端口,并以管理员会话逐一访问前端路由。矩阵当前包含 118 个入口,覆盖:
|
||||
|
||||
- 登录、落地页、公开页和独立会商页;
|
||||
- 工作区、智能体、技能、AI 写作、深度研究、公文、企业研究和 Notebook;
|
||||
- 多智能体会商、业务链、报告协作和知识空间;
|
||||
- 定时任务、管理员页面、设置页面和模型相关页面;
|
||||
- 任务管理、任务分析、行动分析、任务会商、资源管理和轻应用;
|
||||
- canvas、notebook、strategy、iframe 与兼容重定向入口。
|
||||
|
||||
每条路由都会检查 URL/重定向、页面完整加载、根节点尺寸、可见内容、错误边界、未捕获 `PageError`、控制台错误和 chunk 加载错误。页面内所有可见且未禁用的 `[role="tab"]` 会依次点击,以触发非破坏性的懒加载面板。脚本不会点击保存、删除、发布、回滚等会修改业务数据的按钮。
|
||||
|
||||
默认使用应用正常的 SPA 导航。`FIREFOX_FULL_RELOAD=1` 可切换为逐路由整页刷新,建议配合 `FIREFOX_ROUTE_FILTER` 分批运行。Firefox 52 在远程调试器、开发 source map 和上百次连续整页刷新同时存在时会积累调试内存;默认模式可以覆盖全部入口,同时避免把调试器内存压力误判为某个页面故障。
|
||||
|
||||
## 核心交互与专项
|
||||
|
||||
`pnpm smoke:firefox52` 当前包含 20 项真实浏览器检查:
|
||||
|
||||
- 7 个核心页面加载;
|
||||
- 左下角“设置和更多”、换肤色板;
|
||||
- Agent、模型和常用提示词弹窗;
|
||||
- 输入框 Enter 提交;
|
||||
- 对话类型筛选、项目创建模式;
|
||||
- AI 写作模型、会商业务链、定时任务新建弹窗;
|
||||
- Markdown 编辑器重置;
|
||||
- 调用正式后端接口生成非空 DOCX Blob,并校验 MIME 与 `.docx` 文件名。
|
||||
|
||||
`pnpm smoke:firefox52:live` 在上述基础上执行一轮真实问答。它验证表单提交、线程创建、路由跳转、XHR 增量流式读取和包含唯一校验标记的助手回复。`pnpm smoke:firefox52:themes` 验证六套主题乘以全量/简洁布局,共 12 种组合。
|
||||
|
||||
## 运行方式
|
||||
|
||||
```powershell
|
||||
$env:FIREFOX_DEBUG_PORT = "6001"
|
||||
pnpm smoke:firefox52
|
||||
pnpm smoke:firefox52:live
|
||||
pnpm smoke:firefox52:themes
|
||||
pnpm smoke:firefox52:full
|
||||
```
|
||||
|
||||
可用 `FIREFOX_ROUTE_FILTER` 只回归匹配的路由;冷编译较重的页面可通过 `FIREFOX_ROUTE_TIMEOUT_MS` 调整单页等待时间,默认 30 秒。测试账号默认 `admin`,可用 `FIREFOX_TEST_USERNAME` 和 `FIREFOX_TEST_PASSWORD` 覆盖。
|
||||
|
||||
## 2026-10-09 结果
|
||||
|
||||
| 项目 | 结果 |
|
||||
|---|---|
|
||||
| 核心页面与交互 | 20/20 通过 |
|
||||
| 真实流式问答 | 1/1 通过 |
|
||||
| 真实智能体流式问答 | 1/1 通过 |
|
||||
| 主题 × 布局 | 12/12 通过 |
|
||||
| 开发站点完整路由 | 118/118 通过,并点击 71 个可见非破坏性 Tabs |
|
||||
| 生产预览完整路由 | 118/118 通过,并点击 74 个可见非破坏性 Tabs |
|
||||
|
||||
验收结论仅覆盖本次账号和现有测试数据。需要特定外部系统、任务深链参数、硬件 WebGL 或实际业务数据写入的流程,仍应在对应部署环境补充人工验收。
|
||||
@@ -1,579 +0,0 @@
|
||||
# Firefox 52 全功能兼容与 Rspack 高性能构建开发计划
|
||||
|
||||
## 1. 文档信息
|
||||
|
||||
| 项目 | 内容 |
|
||||
|---|---|
|
||||
| 目标分支 | `fix/firefox52-compat` |
|
||||
| 独立工作区 | `F:\react01\firefox-web` |
|
||||
| 前端目录 | `F:\react01\firefox-web\frontend-web` |
|
||||
| 目标浏览器 | Firefox 52(Windows x64) |
|
||||
| 技术方向 | React 18.3 + Tailwind CSS 3.4 + Rspack 2.x + 内置 SWC |
|
||||
| 核心目标 | 尽可能保持现有全部功能,提高冷构建和增量构建速度,保证主要页面与主题样式 |
|
||||
| 当前状态 | 兼容构建与自动回归已接通:核心交互 20/20、主题布局 12/12、完整路由 118/118,真实普通问答与智能体问答均通过 |
|
||||
|
||||
### 1.1 2026-10-09 实施进度
|
||||
|
||||
已完成:
|
||||
|
||||
- 建立 Firefox 52 专用 Rspack/SWC 单目标构建,开发服务器、快速构建、正式构建和预览命令均已接通;持久化缓存生效。当前机器实测冷生产构建 16.865 秒,首轮后的四次稳态构建中位数 1.584 秒、P95 1.641 秒。
|
||||
- 运行时降级到 React 18.3;Tailwind CSS 采用 `feat/tailwind-v3-legacy-chrome` 已验证方案降级到 3.4,保留现有主题 token 和组件类名。
|
||||
- 建立统一兼容入口,补齐 Pointer Events、`requestSubmit`、`scrollIntoView({block:"nearest"})`、Blob、UUID、Clipboard、ResizeObserver、Web Streams 等能力。
|
||||
- 流式请求使用 XHR `onprogress` 适配 SSE/NDJSON/Range 等接口;单元测试 4/4 通过。
|
||||
- 修复 Firefox 52 下的 SPA 路由白屏、`display: contents` 根布局失效、侧边栏分隔线颜色、首页宽度 token、欢迎标题透明、常用提示词换行、Radix 菜单无法点击、cmdk 下拉框崩溃和 Enter 无法发送。
|
||||
- CSS 兼容样式拆到 `src/styles/firefox52/`;PostCSS 自动转换 `rgb(var(...) / alpha)`、`:where()` 和暗色后代选择器,并为 flex gap 提供回退。
|
||||
- MapLibre 固定到 4.7.1,xlsx 解压改用 fflate;对 LangChain/LangGraph/LangSmith、Oniguruma、React Router、Zod 等依赖启用精确转换或源码兼容 loader。
|
||||
- 修复 LangGraph 普通 async 方法中 `for await` 被错误降为同步迭代的问题,并增加 loader 回归测试;修复 XYFlow 12.12 的 BigInt 渲染序号导致流程页面白屏的问题。
|
||||
- 增加 `check:firefox52-css`、`check:firefox52-js`、核心交互、真实流式、主题布局和 118 条完整路由脚本,用于阻止现代 CSS/JS 语法、路由和页面交互回归。
|
||||
- 建立 `docs/firefox52-dependency-compatibility.md` 和 `docs/firefox52-route-compatibility.md`,记录版本、补丁原因、验证入口与平台限制。
|
||||
|
||||
本阶段验证结果:
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|---|---|
|
||||
| Firefox 52 核心页面与交互冒烟 | 20/20 通过 |
|
||||
| Firefox 52 真实普通问答 | 线程创建、路由跳转、XHR 增量流与最终回复均通过 |
|
||||
| Firefox 52 真实智能体问答 | 自动选择可用智能体,线程创建、XHR 增量流与最终回复均通过 |
|
||||
| 主题与布局 | 6 套主题 × 全量/简洁布局,12/12 通过 |
|
||||
| Word 导出 | 后端实际生成非空 DOCX,MIME 与下载文件名通过 |
|
||||
| 完整路由 | 开发与生产预览均为 118/118;分别点击 71/74 个可见非破坏性 Tabs;强制刷新模式支持按过滤器分批运行 |
|
||||
| 关键交互 | 设置和更多、换肤、Agent/模型选择、提示词管理、Enter 提交、项目创建、业务链条、定时任务新建、Markdown 编辑器与 Word 下载 |
|
||||
| 正式产物 CSS 检查 | 通过 |
|
||||
| 正式产物 Acorn ES2017 解析 | 464/464,通过 |
|
||||
| Firefox 52 浏览器自身 `new Function` 解析 | 464/464,0 错误 |
|
||||
| 正式产物 JS 体积 | 25.2 MiB(464 个按需 chunk) |
|
||||
| Rspack 生产构建 | 冷构建 16.865 秒;稳态中位数 1.584 秒、P95 1.641 秒 |
|
||||
|
||||
管理员、任务、iframe、公开页、canvas、会商和设置等静态入口已纳入完整路由矩阵。需要特定任务参数、外部系统、业务数据写入、硬件 WebGL 或精确物理分辨率的流程,仍需在对应部署环境补充人工验收,不能用静态入口通过替代。
|
||||
|
||||
## 2. 建设目标
|
||||
|
||||
本项目在独立兼容分支中建设 Firefox 52 专用前端构建链。现代浏览器版本继续在主开发目录使用现有 Vite 构建,兼容分支只生成 Firefox 52 单目标产物,不生成现代与旧版两套 bundle。
|
||||
|
||||
开发遵循以下目标:
|
||||
|
||||
1. **功能覆盖优先**:现有路由和主要业务流程都必须进入兼容验收矩阵,不能以“首页能够打开”作为完成标准。
|
||||
2. **构建性能优先**:使用 Rspack、SWC、增量编译和持久化缓存,移除 legacy bundle 的二次 Babel/SystemJS 转换。
|
||||
3. **样式一致性优先**:保留 Tailwind、TDesign、Radix 和自定义主题,系统处理 Firefox 52 不支持的 CSS 语法与布局能力。
|
||||
4. **兼容逻辑可维护**:浏览器差异集中到构建配置、兼容入口、传输适配器和依赖补丁,不在业务组件中散落版本判断。
|
||||
5. **持续跟进主分支**:兼容分支能够定期合并 `master-zg` 的新功能,并通过自动检查发现新增的不兼容依赖和语法。
|
||||
|
||||
## 3. 完成标准
|
||||
|
||||
### 3.1 功能标准
|
||||
|
||||
- Firefox 52 打开登录页、首页和工作区时没有白屏。
|
||||
- 首屏与路由切换没有未捕获 JavaScript 异常、语法错误和 chunk 加载错误。
|
||||
- 功能矩阵中的 P0、P1 场景全部通过;P2 场景逐项验证并记录结果。
|
||||
- 无法由旧浏览器实现的原生能力必须提供等价回退,页面和主流程不能直接不可用。
|
||||
- 文件上传、文件下载、流式问答、Markdown、公式、代码块、流程图和 Word 导出必须作为专项验收项。
|
||||
- iframe、任务深链、公开分享页面和运行时配置必须保持现有 URL 与接口行为。
|
||||
|
||||
### 3.2 性能标准
|
||||
|
||||
先在阶段 0 获取当前 Vite legacy 的基线,后续使用相同机器、相同源码、相同依赖和相同环境重复测试。最终门槛如下:
|
||||
|
||||
| 指标 | 目标 |
|
||||
|---|---|
|
||||
| 开发服务器首次可访问时间 | 不高于基线的 50%,期望不超过 10 秒 |
|
||||
| 修改单个 TSX 后的增量构建 | P95 不超过 1.5 秒 |
|
||||
| 修改全局 CSS 后的增量构建 | P95 不超过 2 秒 |
|
||||
| 无缓存生产构建 | 不高于基线的 50% |
|
||||
| 有缓存生产构建 | 不高于基线的 30% |
|
||||
| 连续构建稳定性 | 连续 10 次无随机失败、无失效 chunk、无内存溢出 |
|
||||
| 产物浏览器检查 | 不含 Firefox 52 无法解析的模块入口和已知危险语法 |
|
||||
|
||||
绝对时间与相对提升发生冲突时,以“功能正确、样式正确、相对基线明显提升”为判断顺序。未达到性能门槛前不移除旧构建链。
|
||||
|
||||
### 3.3 样式标准
|
||||
|
||||
- 全量模式、简洁模式、嵌入模式均无整体布局塌陷。
|
||||
- 浅色、深色、浅蓝、浅绿、冰蓝、浅黄等现有主题逐一验证。
|
||||
- 重点分辨率至少覆盖 `1366×768`、`1920×1080`、`2560×1440`。
|
||||
- 导航栏、侧边栏、表单、表格、标签页、弹窗、下拉框、Tooltip、抽屉和浮层没有明显错位或遮挡。
|
||||
- 页面不得出现因 `@layer`、现代颜色语法或不支持的选择器导致的整片无样式。
|
||||
- 主流程截图与现代浏览器参考图保持结构、间距、颜色和层级一致;字体抗锯齿造成的细微像素差异不作为失败。
|
||||
|
||||
## 4. 当前兼容基线
|
||||
|
||||
兼容分支已经存在以下措施,Rspack 迁移必须先等价承接,再进行优化:
|
||||
|
||||
1. `@vitejs/plugin-legacy` 生成 Firefox 52 的 SystemJS legacy bundle。
|
||||
2. Mermaid legacy chunk 额外经过 Babel 转换。
|
||||
3. 对 `p-queue`、GFM autolink、Oniguruma parser、LangSmith XXHash 进行构建期源码补丁。
|
||||
4. 入口安装 AbortController、IntersectionObserver、ResizeObserver 和 Web Streams polyfill。
|
||||
5. PostCSS 展平 Tailwind `@layer`。
|
||||
6. 自定义颜色回退和 Lightning CSS 目标转换。
|
||||
7. 全局样式中已经维护主题对应的 legacy 色值。
|
||||
8. `index.html` 在应用入口前安装 `AbortSignal.timeout` 回退。
|
||||
|
||||
当前方案的主要问题:
|
||||
|
||||
- Vite 开发服务器依赖现代 ESM,Firefox 52 无法直接用于日常开发调试。
|
||||
- legacy 构建对最终 chunks 再执行 Babel 和 SystemJS 转换,模块量越大耗时越明显。
|
||||
- 依赖源码在构建过程中做字符串替换,缓存命中率、升级稳定性和错误定位都较差。
|
||||
- Web Streams polyfill只能补充 JavaScript API,不能自动让 Firefox 52 的 `fetch` 响应具备网络流式读取能力。
|
||||
- CSS 已有回退主要围绕旧版 Chrome 建设,还需要按 Firefox 52 实际渲染能力重新审计。
|
||||
|
||||
## 5. 总体技术方案
|
||||
|
||||
### 5.1 构建链
|
||||
|
||||
Firefox 52 专用构建使用以下组件:
|
||||
|
||||
- `@rspack/core`
|
||||
- `@rspack/cli`
|
||||
- `@rspack/dev-server`
|
||||
- `builtin:swc-loader`
|
||||
- Rspack SWC JavaScript minimizer
|
||||
- Rspack Lightning CSS minimizer或现有 PostCSS + Lightning CSS 流程
|
||||
- HTML 生成插件、资源复制插件和 React Refresh 集成(确认 Firefox 52 可运行后启用)
|
||||
|
||||
构建目标分成两层:
|
||||
|
||||
1. Rspack runtime 目标指定 Firefox 52,保证 chunk loader、bootstrap 和 HMR runtime 不输出危险语法。
|
||||
2. SWC `env.targets` 指定 Firefox 52,负责 TS、TSX、JSX 和业务 JavaScript 的语法降级及 core-js 注入。
|
||||
|
||||
生产构建仅输出一套 Firefox 52 产物,使用普通 script/JSONP chunk loading,不保留 Vite legacy 的 SystemJS 双阶段处理。
|
||||
|
||||
### 5.2 配置与资源保持
|
||||
|
||||
迁移时保持以下外部行为:
|
||||
|
||||
- `runtime-config.js` 继续在应用 bundle 之前加载。
|
||||
- 保持现有 `VITE_*` 配置键,不要求部署环境一次性改名。
|
||||
- `@/env`、`@/*`、`@vite/*`、`next/navigation`、`next/link` 等 alias 保持一致。
|
||||
- `base`、静态资源目录、favicon、字体和图片路径保持可配置。
|
||||
- Hash 路由、BrowserRouter 深层路由、iframe 嵌入路由和公开页面刷新都能正确加载资源。
|
||||
- 开发端口与启动脚本可以通过环境变量覆盖,默认不与主开发工作区冲突。
|
||||
|
||||
### 5.3 构建模式
|
||||
|
||||
计划提供以下命令:
|
||||
|
||||
| 命令 | 用途 |
|
||||
|---|---|
|
||||
| `npm run dev` / `pnpm dev` | 默认启动 Firefox 52 Rspack Dev Server,端口 5174 |
|
||||
| `pnpm dev:firefox52` | Rspack Dev Server,供 Firefox 52 直接调试 |
|
||||
| `npm run dev:vite` / `pnpm dev:vite` | 仅在现代浏览器调试时启动原 Vite Dev Server |
|
||||
| `pnpm build:firefox52` | Firefox 52 正式单目标构建 |
|
||||
| `pnpm build:firefox52:fast` | 不压缩、保留调试信息的快速验证构建 |
|
||||
| `pnpm preview:firefox52` | 使用静态服务器验证正式产物 |
|
||||
| `pnpm check:firefox52` | 扫描危险语法、资源路径和兼容清单 |
|
||||
| `pnpm benchmark:firefox52` | 执行冷构建、热构建和增量构建基准 |
|
||||
| `pnpm smoke:firefox52` | 运行 Firefox 52 核心页面与交互冒烟 |
|
||||
| `pnpm smoke:firefox52:live` | 运行真实普通问答流式冒烟 |
|
||||
| `pnpm smoke:firefox52:agent-live` | 选择当前可用智能体并运行真实流式冒烟 |
|
||||
| `pnpm smoke:firefox52:themes` | 验证 6 套主题 × 2 种布局 |
|
||||
| `pnpm smoke:firefox52:full` | 验证 118 个路由及可见非破坏性 Tabs |
|
||||
|
||||
类型检查继续单独运行 `pnpm typecheck`,不阻塞开发构建的模块转换。
|
||||
|
||||
## 6. JavaScript 与浏览器 API 兼容方案
|
||||
|
||||
### 6.1 兼容入口
|
||||
|
||||
建立唯一的 Firefox 52 兼容入口,并保证它早于 React、路由、请求客户端和业务模块执行。兼容入口统一处理:
|
||||
|
||||
- `Promise`、`Map`、`Set`、`Symbol`、迭代器及必要的 core-js 能力。
|
||||
- AbortController、AbortSignal.timeout 和 abort reason 差异。
|
||||
- ResizeObserver、IntersectionObserver、matchMedia 监听接口。
|
||||
- ReadableStream、WritableStream、TransformStream。
|
||||
- `queueMicrotask`、`globalThis`、`Object.fromEntries`、`Promise.finally` 等运行时能力。
|
||||
- `crypto.randomUUID`、Clipboard、下载、全屏等能力的回退实现。
|
||||
- `requestIdleCallback`、`structuredClone` 等组件可能间接使用的 API。
|
||||
|
||||
所有 polyfill 必须具备能力检测,只在缺失时安装,避免重复覆盖浏览器实现。
|
||||
|
||||
### 6.2 流式请求专项
|
||||
|
||||
流式问答是兼容工作的 P0 项。Firefox 52 的网络层不能依赖现代 `fetch().body.getReader()`,需建立统一传输适配器:
|
||||
|
||||
1. 现代浏览器继续使用现有 fetch/SSE/NDJSON 路径。
|
||||
2. Firefox 52 使用 XHR `onprogress` 增量读取 `responseText`。
|
||||
3. 适配器统一处理 SSE、NDJSON、取消、超时、断线、服务端错误和 UTF-8 分片。
|
||||
4. LangGraph、普通问答、智能体问答、AI 写作、深度研究、会商、技能地址扫描等流式接口全部复用同一兼容层。
|
||||
5. 增加分片边界测试,覆盖半个 JSON、半个 UTF-8 字符、连续空行、最后一行无换行和中途取消。
|
||||
|
||||
### 6.3 不可直接转换的语法
|
||||
|
||||
以下内容不能只依赖 SWC 或 core-js,必须通过升级、降级、替代或固定补丁解决:
|
||||
|
||||
- BigInt 字面量及 BigInt 专用算法。
|
||||
- 正则向后断言、Unicode property escapes 和不支持的正则 flag。
|
||||
- Module Worker 和依赖原生 ESM 的 Worker。
|
||||
- 运行时拼接后才生成的现代语法。
|
||||
- 强依赖新浏览器 Web API 的第三方组件。
|
||||
|
||||
处理顺序:优先选择兼容版本,其次维护专用 fork,再使用 `patch-package` 或安装后补丁。构建期间的逐文件字符串替换只作为临时迁移手段。
|
||||
|
||||
### 6.4 产物静态扫描
|
||||
|
||||
`check:firefox52` 至少扫描:
|
||||
|
||||
- 未被打包的 `import`、`export`、`import.meta`。
|
||||
- BigInt 字面量和 `BigInt64Array`/`BigUint64Array` 直接依赖。
|
||||
- 正则向后断言、Unicode property escapes、dotAll 等危险正则。
|
||||
- 未处理的 async generator、module worker 和 module script。
|
||||
- 绝对开发机路径、丢失的 chunk、错误 public path。
|
||||
- HTML 中会被 Firefox 52 忽略且没有回退的入口脚本。
|
||||
|
||||
扫描结果必须支持白名单,白名单需要注明依赖、原因、验证方式和负责人,不能静默忽略。
|
||||
|
||||
## 7. 第三方依赖治理
|
||||
|
||||
### 7.1 依赖分级
|
||||
|
||||
建立 `docs/firefox52-dependency-compatibility.md`,按以下类别维护:
|
||||
|
||||
| 等级 | 含义 | 处理方式 |
|
||||
|---|---|---|
|
||||
| A | 原生兼容或经 SWC 即可兼容 | 正常打包 |
|
||||
| B | 缺少 API polyfill | 加入统一兼容入口 |
|
||||
| C | 包含可转换的新语法 | 加入 SWC 依赖 allowlist |
|
||||
| D | 包含不可转换语法或新 API | 固定版本、fork、patch 或替换 |
|
||||
| E | Firefox 52 平台能力无法完整支持 | 提供业务等价回退并记录差异 |
|
||||
|
||||
### 7.2 首批专项依赖
|
||||
|
||||
优先审计以下依赖:
|
||||
|
||||
- React 19、React DOM、React Router。
|
||||
- LangGraph SDK、LangChain、AI SDK、LangSmith。
|
||||
- Mermaid、Shiki、react-syntax-highlighter、Oniguruma。
|
||||
- CodeMirror、Tiptap/ProseMirror、KaTeX、Marked、react-markdown。
|
||||
- MapLibre GL、Leaflet、XYFlow、OGL、GSAP、Motion。
|
||||
- Radix UI、TDesign React、cmdk、Embla。
|
||||
- TanStack Query、Axios、Zod、date-fns、uuid。
|
||||
- docx、file-saver 和文件预览相关模块。
|
||||
|
||||
重型模块保持路由级或组件级动态加载。Rspack 必须把动态 import 转为 Firefox 52 可用的普通 chunk loader。
|
||||
|
||||
## 8. CSS 与视觉一致性方案
|
||||
|
||||
### 8.1 CSS 构建顺序
|
||||
|
||||
CSS 建议按以下顺序处理:
|
||||
|
||||
1. Tailwind 4 生成样式。
|
||||
2. 展平 `@layer`,防止 Firefox 52 忽略整组规则。
|
||||
3. 生成颜色和透明度回退。
|
||||
4. 处理嵌套、逻辑属性、选择器和可安全转换的现代语法。
|
||||
5. Lightning CSS 按 Firefox 52 目标转换。
|
||||
6. 生产模式压缩,开发模式保持未压缩和快速 source map。
|
||||
|
||||
需重点验证当前 `rgb(var(--legacy-*-rgb) / alpha)` 在 Firefox 52 中的实际表现;若浏览器不接受空格分隔和斜杠透明度语法,应在构建期生成逗号分隔的 `rgba()` 或直接颜色值。
|
||||
|
||||
### 8.2 手写兼容 CSS 与文件拆分
|
||||
|
||||
构建工具无法可靠转换,或者转换后仍存在布局和交互差异的样式,直接编写 Firefox 52 专用 CSS。兼容样式不得继续集中追加到 `globals.css`,统一放在独立目录中按职责拆分:
|
||||
|
||||
```text
|
||||
src/styles/firefox52/
|
||||
├── index.css # 只负责按固定顺序汇总核心兼容样式
|
||||
├── tokens.css # 颜色、透明度、阴影和主题变量回退
|
||||
├── base.css # 浏览器基础差异和通用元素修正
|
||||
├── layout.css # 页面框架、侧边栏、滚动区和响应式布局
|
||||
├── overlays.css # Dialog、Popover、Select、Tooltip、Guide 等浮层
|
||||
├── markdown.css # Markdown、代码块、公式、表格和引用
|
||||
├── editors.css # CodeMirror、Tiptap、ProseMirror 等编辑器
|
||||
├── visualizations.css # Mermaid、XYFlow、图表、地图和 Canvas 容器
|
||||
├── chat.css # 普通问答、智能体问答和消息时间线
|
||||
├── writing-research.css # AI 写作、深度研究和公文工作台
|
||||
├── roundtable.css # 会商、流程图和报告协作
|
||||
├── task-workspaces.css # 任务深链、目的树和快捷跳转栏
|
||||
└── admin.css # 管理端表格、表单和配置页
|
||||
```
|
||||
|
||||
拆分规则:
|
||||
|
||||
1. 全局首屏必需的 `tokens/base/layout/overlays` 由 `index.css` 统一引入。
|
||||
2. Markdown、编辑器、可视化、会商、任务工作区和管理端样式随对应路由或组件导入,让 Rspack 生成独立 CSS chunk,避免所有用户加载完整兼容样式。
|
||||
3. 单个兼容 CSS 文件建议控制在 500 行或未压缩 30 KB 以内;超过任一门槛时继续按组件或业务域拆分。
|
||||
4. HTML 根节点由兼容入口设置稳定标记,例如 `data-browser-compat="firefox52"`。兼容规则应挂在该标记下,防止影响现代浏览器和公共组件的默认样式。
|
||||
5. 优先复用语义 token 和现有组件 class,不复制整份第三方组件样式。只覆盖 Firefox 52 实际不兼容的声明。
|
||||
6. `!important` 只用于第三方组件无法通过正常层叠覆盖的场景,并在规则旁注明原样式来源和原因。
|
||||
7. 文件加载顺序必须固定:主题 token → 基础样式 → 布局 → 通用浮层 → 业务页面。禁止依赖偶然的 chunk 到达顺序解决优先级。
|
||||
8. 每个文件明确对应验收页面,删除页面或修复上游兼容性时同步清理失效规则。
|
||||
|
||||
如果某个功能仅需要少量兼容规则,样式文件放在功能模块旁并使用 `.firefox52.css` 后缀;跨页面共用的兼容规则才放入 `src/styles/firefox52/`。构建检查需要统计各文件行数、体积、重复选择器和 `!important` 数量,超过门槛时提示拆分。
|
||||
|
||||
### 8.3 CSS 风险清单
|
||||
|
||||
建立自动扫描和人工验证清单,覆盖:
|
||||
|
||||
- `color-mix()`、OKLCH、现代 `rgb()`/`hsl()`。
|
||||
- `@layer`、CSS nesting、container query。
|
||||
- `:has()`、`:focus-visible`、复杂 `:is()`/`:where()`。
|
||||
- Flexbox `gap`、Grid 差异和 `subgrid`。
|
||||
- `aspect-ratio`、`inset`、`min()`、`max()`、`clamp()`。
|
||||
- `dvh`/`svh`/`lvh`、逻辑方向属性。
|
||||
- `backdrop-filter`、mask、混合模式和复杂滤镜。
|
||||
- sticky、滚动容器、overscroll 和自定义滚动条。
|
||||
- Portal 浮层、z-index、transform containing block 和 `pointer-events`。
|
||||
|
||||
无法可靠转换的布局功能通过兼容 utility 或组件级基础样式回退解决,不能只隐藏出错区域。
|
||||
|
||||
### 8.4 视觉回归范围
|
||||
|
||||
每种主题至少截取以下代表页面:
|
||||
|
||||
1. 登录页和新对话落地页。
|
||||
2. 普通问答流式消息页。
|
||||
3. 智能体列表、创建页和智能体聊天页。
|
||||
4. AI 写作工作台。
|
||||
5. 多智能体会商三步工作台和流程图。
|
||||
6. 知识库列表、编辑器和知识图谱。
|
||||
7. 任务管理、目的树弹窗和快捷跳转栏。
|
||||
8. 管理端表格、Tabs、表单和确认弹窗。
|
||||
9. iframe 嵌入页和公开分享页。
|
||||
10. 站点预览、地图、图表和全屏场景。
|
||||
|
||||
视觉验收重点比较布局边界、间距、颜色、滚动区域、遮罩和弹层位置。截图自动化优先使用与 Firefox 52 匹配的 Selenium/geckodriver;无法稳定自动化的页面纳入人工截图清单。
|
||||
|
||||
## 9. 全功能验收矩阵
|
||||
|
||||
### 9.1 P0:必须首先打通
|
||||
|
||||
| 功能域 | 核心场景 |
|
||||
|---|---|
|
||||
| 登录与鉴权 | 密码登录、URL 参数登录、退出、刷新恢复、鉴权失效 |
|
||||
| 页面框架 | 全量/简洁模式、侧边栏、菜单、主题、路由刷新 |
|
||||
| 普通问答 | 新建会话、历史会话、流式回答、停止、重试、错误显示 |
|
||||
| 消息展示 | Markdown、GFM、代码高亮、公式、链接、引用、流程图 |
|
||||
| 文件 | 上传、附件卡片、产物预览、下载、Word 导出 |
|
||||
| 智能体问答 | 智能体列表、进入聊天、新建和历史会话 |
|
||||
| iframe | `embed=1`、`hideJump=1`、主题、独立鉴权、postMessage |
|
||||
| 错误恢复 | 断网、接口 401/403/500、chunk 加载失败、刷新恢复 |
|
||||
|
||||
### 9.2 P1:主要业务能力
|
||||
|
||||
| 功能域 | 核心场景 |
|
||||
|---|---|
|
||||
| 智能体管理 | 创建、编辑、模型、知识空间、SOUL、技能、保存、引导 |
|
||||
| 舆情分析智能体 | 外部流式接口、会话、错误信息 |
|
||||
| AI 写作 | 经典版、对话版、阶段推进、干预、资料、Markdown 与 Word 下载 |
|
||||
| 深度研究/公文 | 发起任务、过程状态、恢复、结果展示和下载 |
|
||||
| 多智能体会商 | 普通、规划、8BF、岗位会商、链条配置、后台续跑 |
|
||||
| 报告协作 | 会话列表、协作过程、结果编辑和导出 |
|
||||
| 知识空间 | 列表、详情、上传、Markdown 编辑、Wiki、图谱、检索调试 |
|
||||
| Notebook | 列表、详情、资料和生成流程 |
|
||||
| 定时任务 | 任务列表、创建、运行详情、公开运行页 |
|
||||
| 任务深链 | rwfx、3qfx、xdfx、agentfx、单智能体、会商、行动分区 |
|
||||
| 任务报告 | 生成、文件卡片、入库确认、宿主通知 |
|
||||
| 轻应用 | 应用列表、窗口打开、iframe 宿主、动态菜单 |
|
||||
|
||||
### 9.3 P2:管理、配置和扩展能力
|
||||
|
||||
- 用户、活动、记忆、模型、模型交换与并发配置。
|
||||
- 智能体、技能、技能上下线、技能地址扫描/预览/替换/回滚。
|
||||
- 菜单、简洁菜单、按钮、标签、固定问题、推荐问题、提示词前缀。
|
||||
- 敏感词、排行榜、插件、岗位和岗位角色。
|
||||
- 业务链、发布广场、私有广场、技能压缩、LLM 指标和 checkpoint 迁移。
|
||||
- 公开分享、排行榜、会商报告、会商看板和定时任务全屏页面。
|
||||
- Web App/Site Studio、地图、流程图、图表和沙箱 HTML 预览。
|
||||
- 微信样式入口、无界嵌入和其他特殊页面壳。
|
||||
|
||||
### 9.4 单项验收规则
|
||||
|
||||
每个功能项必须记录:
|
||||
|
||||
- 入口 URL、角色和前置数据。
|
||||
- 页面是否正常加载及首个可交互时间。
|
||||
- 主操作、保存/提交、返回和刷新恢复结果。
|
||||
- 是否涉及流式、上传、下载、Worker、Canvas、SVG、WebGL 或 iframe。
|
||||
- Firefox 52 控制台错误和网络失败。
|
||||
- 样式截图、已知差异和回退行为。
|
||||
- 验收人、日期、构建 commit 和结果。
|
||||
|
||||
路由清单应由路由源码自动提取后与人工场景表合并,新增路由若未进入矩阵则 CI 提示失败。
|
||||
|
||||
## 10. 开发阶段
|
||||
|
||||
### 阶段 0:基线和可观测性
|
||||
|
||||
工作项:
|
||||
|
||||
- 固定 Node、pnpm、Firefox 52、Rspack/SWC 候选版本和测试机器信息。
|
||||
- 记录 Vite legacy 的冷构建、热构建、增量构建、内存峰值和产物体积。
|
||||
- 保存当前主要页面在现代浏览器和 Firefox 52 的截图、控制台错误及网络日志。
|
||||
- 自动提取路由清单和动态 import 清单。
|
||||
- 输出依赖语法扫描报告和浏览器 API 使用清单。
|
||||
- 建立可重复执行的 benchmark 脚本,结果保存为 JSON 和 Markdown。
|
||||
|
||||
退出条件:任何人都能按文档复现当前构建时间和 Firefox 52 白屏/错误现状。
|
||||
|
||||
### 阶段 1:Rspack 并行构建骨架
|
||||
|
||||
工作项:
|
||||
|
||||
- 增加 Rspack 配置、HTML、静态资源、alias、环境变量和 public path。
|
||||
- 接入内置 SWC,先处理项目源码,再逐步扩展到依赖 allowlist。
|
||||
- 接入 React、CSS、字体、SVG、图片和动态 import。
|
||||
- 建立 `dev:firefox52`、`build:firefox52:fast` 和 preview 命令。
|
||||
- 暂时保留 Vite legacy 命令作为对照和回退。
|
||||
|
||||
退出条件:Firefox 52 能通过 Rspack dev server 打开登录页和空工作区,没有语法错误与 chunk 加载错误。
|
||||
|
||||
### 阶段 2:运行时与流式链路
|
||||
|
||||
工作项:
|
||||
|
||||
- 重整统一兼容入口和 polyfill 顺序。
|
||||
- 完成 XHR 流式适配器并接入核心请求路径。
|
||||
- 验证取消、超时、错误解析、鉴权头和跨域行为。
|
||||
- 处理 Web Worker、WebSocket、EventSource 和动态 chunk 加载。
|
||||
- 完成普通问答端到端测试。
|
||||
|
||||
退出条件:普通问答可以连续完成发送、流式接收、停止、重试和刷新恢复。
|
||||
|
||||
### 阶段 3:依赖清理和固定补丁
|
||||
|
||||
工作项:
|
||||
|
||||
- 对首批专项依赖分级。
|
||||
- 把现有 Vite 字符串补丁迁移为版本固定、fork、alias 或安装后补丁。
|
||||
- 对 Mermaid、Shiki、Oniguruma、LangSmith、p-queue 等建立单独验收用例。
|
||||
- 配置最终产物危险语法扫描。
|
||||
- 为新增依赖增加 CI 扫描。
|
||||
|
||||
退出条件:生产产物通过静态扫描,补丁均有版本校验,依赖升级导致补丁失效时构建直接失败并给出清晰错误。
|
||||
|
||||
### 阶段 4:CSS 和组件样式兼容
|
||||
|
||||
工作项:
|
||||
|
||||
- 把现有 PostCSS/Tailwind/Lightning CSS 流程接入 Rspack。
|
||||
- 建立 `src/styles/firefox52/`,按主题、布局、浮层和业务域拆分手写兼容 CSS,避免继续扩大 `globals.css`。
|
||||
- 配置核心兼容 CSS 与路由级兼容 CSS 的分包策略,并增加单文件行数、体积和重复规则检查。
|
||||
- 修复主题颜色、透明度、cascade layer、flex gap 和现代选择器问题。
|
||||
- 验证 TDesign、Radix、Tailwind 组件和 Portal 浮层。
|
||||
- 建立 CSS 危险语法扫描与代表页面截图回归。
|
||||
- 验证各主题和三种目标分辨率。
|
||||
|
||||
退出条件:P0 页面和代表组件没有布局塌陷、透明色失效、弹层错位或滚动锁死。
|
||||
|
||||
### 阶段 5:全功能逐批清零
|
||||
|
||||
按以下批次执行,每批都完成“功能、控制台、网络、样式、刷新恢复”五项检查:
|
||||
|
||||
1. 登录、框架、普通问答和文件。
|
||||
2. 智能体管理与智能体问答。
|
||||
3. AI 写作、深度研究、公文。
|
||||
4. 多智能体会商、业务链和报告协作。
|
||||
5. 知识空间、Notebook、Markdown 编辑器和图谱。
|
||||
6. 任务深链、任务报告、轻应用和 iframe。
|
||||
7. 定时任务、管理端和设置页。
|
||||
8. 公开页面、Site Studio、地图、图表和其他扩展能力。
|
||||
|
||||
退出条件:P0、P1 全部通过,P2 完成验证;所有遗留项都有可执行的修复或等价回退方案。
|
||||
|
||||
### 阶段 6:构建性能专项优化
|
||||
|
||||
工作项:
|
||||
|
||||
- 开启并调优 Rspack 持久化缓存和增量构建。
|
||||
- 开发模式关闭压缩,使用低成本 source map。
|
||||
- 生产模式使用 SWC minimizer,CSS 使用 Lightning CSS minimizer。
|
||||
- 缩小 SWC 转换范围,对不兼容依赖使用精确 allowlist。
|
||||
- 对 Mermaid、Shiki、MapLibre、编辑器、会商等重型模块做路由级拆分。
|
||||
- 检查重复依赖、barrel export、无效 locale 和大资源。
|
||||
- 评估 lazy compilation;仅在不破坏 Firefox 52 chunk 加载时启用。
|
||||
- 将耗时阶段输出到构建 profile,逐项消除长任务。
|
||||
|
||||
退出条件:达到第 3.2 节性能门槛,同时功能和样式回归无新增失败。
|
||||
|
||||
### 阶段 7:持续集成和离线部署衔接
|
||||
|
||||
工作项:
|
||||
|
||||
- CI 执行 typecheck、Rspack 构建、危险语法扫描、核心单测和冒烟测试。
|
||||
- 保存构建时间趋势,超过阈值时提示回归。
|
||||
- 固定包版本和校验值,保证离线环境可重复安装。
|
||||
- 离线依赖与发布资产交由 `knowledge-base-bj/deerflow-offline-deployment` 仓库维护,本业务仓库只保留源码和依赖清单。
|
||||
- 更新启动脚本、部署文档、缓存策略和静态资源响应头。
|
||||
|
||||
退出条件:干净环境可以离线安装、构建、启动并通过 Firefox 52 冒烟测试。
|
||||
|
||||
### 阶段 8:切换与旧链路清理
|
||||
|
||||
工作项:
|
||||
|
||||
- 完成最终功能矩阵、视觉报告和性能报告评审。
|
||||
- 将 Firefox 52 开发和构建命令切换到 Rspack。
|
||||
- 删除已经被固定补丁替代的 Vite legacy 临时代码。
|
||||
- 保留一段时间的 Vite 基准脚本,确认稳定后再彻底清理兼容分支中的旧依赖。
|
||||
|
||||
退出条件:连续多个提交未出现功能、样式和性能回归,Rspack 成为兼容分支唯一正式构建链。
|
||||
|
||||
## 11. 性能测试方法
|
||||
|
||||
每组测试至少执行 5 次,取中位数并记录 P95:
|
||||
|
||||
1. **冷启动**:删除构建缓存后启动开发服务器,测量到首页可请求时间。
|
||||
2. **热启动**:保留缓存重启开发服务器。
|
||||
3. **TSX 增量**:修改一个叶子组件和一个高层布局组件。
|
||||
4. **CSS 增量**:修改组件样式和全局主题样式。
|
||||
5. **冷生产构建**:删除 Rspack 缓存和 dist。
|
||||
6. **热生产构建**:保留缓存、源码不变再次构建。
|
||||
7. **单文件生产变更**:修改一个组件后重新构建。
|
||||
|
||||
记录字段:commit、构建模式、CPU、内存、磁盘、Node/pnpm 版本、总耗时、各阶段耗时、峰值内存、模块数量、chunk 数量、JS/CSS 体积和缓存命中率。
|
||||
|
||||
性能比较必须在相同机器和相同后台负载下进行。正式报告同时保留原始日志,避免只记录一次最佳结果。
|
||||
|
||||
## 12. 主分支同步策略
|
||||
|
||||
兼容分支需要持续接收正常业务开发,采用以下约束:
|
||||
|
||||
- 构建和兼容代码集中在独立目录与入口,减少对业务组件的侵入。
|
||||
- 能通过适配器解决的问题不直接修改业务协议。
|
||||
- 定期从 `master-zg` 合并新功能,每次合并后自动更新路由和依赖扫描结果。
|
||||
- 新增路由必须进入功能矩阵;新增依赖必须进入依赖兼容清单。
|
||||
- 新增流式接口必须使用统一传输层,不能绕过 Firefox 52 XHR 回退。
|
||||
- 新增主题和全局组件必须进入样式截图集。
|
||||
- 冲突解决优先保留主分支业务逻辑,再在兼容层补充旧浏览器处理。
|
||||
|
||||
## 13. 风险与处理原则
|
||||
|
||||
| 风险 | 处理原则 |
|
||||
|---|---|
|
||||
| 第三方依赖停止支持旧浏览器 | 固定版本或维护兼容 fork |
|
||||
| fetch 流式响应不可用 | XHR onprogress 统一回退 |
|
||||
| Map/WebGL 能力差异 | 降级依赖或提供保持业务信息的二维/静态回退 |
|
||||
| 编辑器、语法高亮和 Worker 依赖现代能力 | classic worker、主线程回退或兼容版本 |
|
||||
| CSS 转换后仍存在布局差异 | 兼容 utility、组件基础样式和截图回归 |
|
||||
| Polyfill 体积过大 | 单目标按需注入,重型功能路由级加载 |
|
||||
| 兼容补丁随依赖升级失效 | 精确版本校验,失败时终止构建 |
|
||||
| Rspack 插件兼容不足 | 优先使用内置能力,必要时编写小型 loader/plugin |
|
||||
| 主分支持续新增功能 | 自动路由、依赖、API 和 CSS 扫描 |
|
||||
| 旧浏览器自动化不稳定 | 自动冒烟与人工矩阵结合,保留截图和日志证据 |
|
||||
|
||||
## 14. 交付物
|
||||
|
||||
实施完成后应包含:
|
||||
|
||||
- Rspack 开发、快速构建和生产构建配置。
|
||||
- Firefox 52 统一 polyfill 入口和流式传输适配器。
|
||||
- 第三方依赖兼容清单及固定补丁。
|
||||
- CSS 兼容流水线、扫描脚本和视觉基准图。
|
||||
- 按职责拆分的 Firefox 52 手写兼容 CSS,以及对应的体积和重复规则检查。
|
||||
- 路由级功能验收矩阵。
|
||||
- 自动冒烟、危险语法扫描和构建性能脚本。
|
||||
- 基线报告、最终性能对比报告和已知差异说明。
|
||||
- 开发运行、离线构建、部署和故障排查文档。
|
||||
|
||||
## 15. 最终验收清单
|
||||
|
||||
- [x] Firefox 52 开发服务器可直接打开并调试。
|
||||
- [x] 登录、导航、主题、路由刷新无白屏。
|
||||
- [x] 普通问答和智能体问答可以稳定流式输出。
|
||||
- [ ] 上传、下载、Markdown、公式、代码、流程图和 Word 导出通过。
|
||||
- [ ] AI 写作、研究、会商、知识库、任务深链等 P1 功能全部通过。
|
||||
- [ ] 管理端、设置、公开页和扩展能力完成 P2 验证。
|
||||
- [ ] 所有现有主题和目标分辨率完成视觉回归。
|
||||
- [x] 产物通过危险语法和资源路径扫描。
|
||||
- [ ] 冷构建、热构建和增量构建达到性能门槛(冷/热生产构建已记录,增量构建仍需单独量化)。
|
||||
- [ ] 连续构建与长时间运行无 chunk 丢失和内存异常。
|
||||
- [ ] 新增功能与依赖具备持续兼容检查。
|
||||
- [ ] 离线环境可以重复安装、构建和部署。
|
||||
@@ -93,8 +93,7 @@ frontend-web/
|
||||
|
||||
| 脚本 | 命令 | 说明 |
|
||||
|---|---|---|
|
||||
| `pnpm dev` / `npm run dev` | `rspack serve --config rspack.firefox52.config.mjs --mode development` | 兼容分支默认启动 Firefox 52 开发服务器,端口 5174 |
|
||||
| `pnpm dev:vite` / `npm run dev:vite` | `vite` | 仅在现代浏览器调试时启动原 Vite 开发服务器 |
|
||||
| `pnpm dev` | `vite` | 启动开发服务器(默认端口 5173;项目实际通过仓库根脚本统一为 5174) |
|
||||
| `pnpm build` | `node --max-old-space-size=8192 vite build` | 生产构建(提升 Node 内存阈值,避免大型依赖 OOM) |
|
||||
| `pnpm preview` | `vite preview` | 本地预览 dist 构建产物 |
|
||||
| `pnpm typecheck` | `tsc --noEmit` | 仅做 TS 类型检查,不输出文件 |
|
||||
|
||||
@@ -1,977 +0,0 @@
|
||||
# 多智能体会商功能与接口梳理
|
||||
|
||||
## 1. 文档范围与入口
|
||||
|
||||
本文梳理的是常规「多智能体会商 / 圆桌规划」页面 `RoundtablePlanningPage` 的完整三步流程:
|
||||
|
||||
1. 任务意图理解
|
||||
2. 多智能体研讨
|
||||
3. 方案结果展示
|
||||
|
||||
主入口为 `/page/workspace/qa/multi-agent-roundtable`。它也可由任务深链 `3qfx-A`、`rwfx-A`、`xdfx-A` 打开,分别挂载在任务管理下的三情、任务、行动业务路由中;另有 `#/embed/roundtable` 嵌入入口。
|
||||
|
||||
任务深链会预填任务名称、任务目标、业务编码和任务 ID,并自动选择对应业务链:
|
||||
|
||||
| 深链 | 业务编码 | 嵌入路由 |
|
||||
| --- | --- | --- |
|
||||
| `3qfx-A` | `3Q` | `/page/strategy/task-management/plan-task-sentiment/roundtable` |
|
||||
| `rwfx-A` | `6BF` | `/page/strategy/task-management/plan-task/plan-task-fx/roundtable` |
|
||||
| `xdfx-A` | `7BF` | `/page/strategy/task-management/plan-task/plan-task-action/roundtable` |
|
||||
|
||||
> 本文不展开独立的阵位会商,以及仅展示报告的 8BF 变体。8BF 复用了部分组件,但其第三步为 `report-only`,不属于常规三步会商主路径。
|
||||
|
||||
## 2. 公共机制
|
||||
|
||||
### 2.1 会商记录与保存
|
||||
|
||||
功能:
|
||||
|
||||
- 保存任务意图、研讨过程、会商结论、第三步报告及当前已完成步骤。
|
||||
- 支持新建会商、历史记录切换、重命名、删除和从已有记录继续。
|
||||
- 普通入口按当前用户隔离;任务深链按任务共享,进入同一任务的用户看到同一批会商和后台任务。
|
||||
- 更新使用乐观锁;版本不一致时后端返回 `409`,前端应以服务端最新记录为准后再处理。
|
||||
|
||||
接口:
|
||||
|
||||
| 场景 | 接口 | 调用方式与参数 |
|
||||
| --- | --- | --- |
|
||||
| 普通会商列表 | `GET /api/roundtable-drafts` | 无请求体,按当前用户返回。 |
|
||||
| 新建普通会商 | `POST /api/roundtable-drafts` | JSON:`{ id?, title, furthest_step, step1, step2, step3 }`。 |
|
||||
| 读取/更新/删除普通会商 | `GET/PUT/DELETE /api/roundtable-drafts/{draftId}` | `PUT` 传需更新字段及 `expected_version`。 |
|
||||
| 任务会商列表 | `GET /api/roundtable-task-drafts/by-task/{taskId}` | `taskId` 为任务 ID。 |
|
||||
| 读取最新任务会商 | `GET /api/roundtable-task-drafts/by-task/{taskId}/latest` | 用于任务进入后恢复最近一次会商。 |
|
||||
| 新建任务会商 | `POST /api/roundtable-task-drafts` | JSON:`{ id?, task_id, title, furthest_step, step1, step2, step3 }`。 |
|
||||
| 读取/更新/删除任务会商 | `GET/PUT/DELETE /api/roundtable-task-drafts/{draftId}` | `PUT` 为局部更新并携带 `expected_version`。 |
|
||||
|
||||
参数说明:
|
||||
|
||||
- `step1`、`step2`、`step3` 保存的是各步骤可恢复的完整页面状态,而不是单一文本。
|
||||
- `furthest_step` 记录用户历史上到达的最远步骤,用于恢复导航可用性。
|
||||
- `xdfx-A` 的页面记录和后台任务分区使用 URL 原始行动 ID;任务正文及任务详情仍使用解析后的实际任务 ID。
|
||||
|
||||
### 2.2 流式调用与附件
|
||||
|
||||
功能:
|
||||
|
||||
- 大模型回复、意图澄清、推荐、研讨、报告和流程图均通过 SSE 增量返回。
|
||||
- 用户可停止当前流;前端按事件逐段追加正文、思考、工具调用、澄清卡片和错误信息。
|
||||
- 有文件时先上传到对应线程,再将后端返回的虚拟路径传给后续流式接口。
|
||||
|
||||
附件接口:
|
||||
|
||||
```http
|
||||
POST /api/threads/{threadId}/uploads
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
files: <file> # 可重复,支持多文件
|
||||
```
|
||||
|
||||
响应中的 `{ filename, size, virtual_path }` 应转换后传入流式请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"filename": "任务材料.docx",
|
||||
"size": 10240,
|
||||
"path": "/mnt/user-data/uploads/任务材料.docx",
|
||||
"status": "uploaded"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所有 JSON 接口经前端 `apiFetch` 调用,自动携带当前登录态;流式接口使用 `ReadableStream` 解析 SSE。
|
||||
|
||||
---
|
||||
|
||||
# 第一步:任务意图理解
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第一步将用户的自然语言任务转成可供多个智能体研讨的结构化意图,并确定后续参会智能体来源。
|
||||
|
||||
- 创建内置「任务意图理解」智能体会话,接收任务描述、补充材料和多轮追问。
|
||||
- 流式展示智能体回复、思考过程和工具型澄清问题。
|
||||
- 澄清卡支持单选、多选、自定义填写、跳过和继续追问;同一轮可返回多张澄清卡。
|
||||
- 意图完成后生成可编辑的结构化摘要:目标(`objective`)、约束(`constraints`)、可选假设(`assumptions`)。
|
||||
- 支持手工修改意图摘要后再进入下一步。
|
||||
- 智能体选择有两条路径:
|
||||
- **智能体推荐**:依据意图,在可用智能体候选池中推荐参会人及理由。
|
||||
- **业务链条**:选择已配置的业务链条,带入席位、阶段、阶段目标、协调人提示词和席位模型策略。
|
||||
- 任务深链模式自动加载任务卡片、预填任务上下文并自动选择业务链;普通模式在“选择参会智能体”弹窗中手工选择“我的”或“公共”业务链。
|
||||
|
||||
步骤流转:
|
||||
|
||||
```text
|
||||
输入任务 / 上传材料
|
||||
↓
|
||||
意图智能体澄清(可多轮)
|
||||
↓
|
||||
结构化意图完成并可编辑
|
||||
↓
|
||||
智能体推荐 或 选择业务链
|
||||
↓
|
||||
进入第二步
|
||||
```
|
||||
|
||||
### 1.1 业务链功能与两个弹窗
|
||||
|
||||
业务链是可命名、可复用、可保存的“智能体编排模板”,不是单纯的智能体名单。一条链定义了谁参会、谁先后执行、哪些席位并行、每阶段应产出什么,以及协调人如何派发和汇总。
|
||||
|
||||
第一步中有两个职责不同的业务链入口:
|
||||
|
||||
| 入口 | 弹窗/页面 | 作用 | 是否直接开始研讨 |
|
||||
| --- | --- | --- | --- |
|
||||
| 页面顶栏“业务链条” | BusinessChainListDialog | 浏览、筛选、启停、发布、进入配置页编辑业务链。 | 否,它是管理入口。 |
|
||||
| 意图完成后点击“选择参会智能体 / 进入研讨” | RecommendAgentsDialog 的“业务链条”页签,内部为 BusinessChainPicker | 从可用链中选择一条,校验席位后把编排结果带入第二步。 | 是,确认“进入研讨”后开始第二步。 |
|
||||
|
||||
#### A. 选择参会智能体弹窗中的“业务链条”页签
|
||||
|
||||
功能:
|
||||
|
||||
- 弹窗有“智能体推荐”和“业务链条”两个页签;切换到业务链条后不会调用推荐模型,而是读取已保存的链条。
|
||||
- 可在“我的 / 公共”之间筛选:
|
||||
- **我的**:当前用户创建的业务链。
|
||||
- **公共**:已发布、所有账号都可查看和选用的业务链。
|
||||
- 每张链条卡展示链条名称、描述、席位数和席位编排流:
|
||||
- 线性链用“智能体 A → 智能体 B → 智能体 C”表示顺序。
|
||||
- 同一阶段有多个席位时显示“并行 N”分组;阶段之间仍用箭头表示串行依赖。
|
||||
- 实时智能体名称优先于链条保存时的名称快照;已删除智能体会置灰并提示。
|
||||
- 仅启用的链条可进入选择列表;选中链条后,至少保留 **2 个仍存在的可用席位** 才能点击“进入研讨”。
|
||||
- 如果链条配置了默认执行模式,弹窗顶部的执行模式会自动带入该值,用户仍可临时改为:
|
||||
- flash:快速,不开启思考。
|
||||
- thinking:思考,低推理强度。
|
||||
- pro:专业问答,思考 + 中等推理强度。
|
||||
- ultra:多智能体问答,思考 + 高推理强度,并允许席位调用子代理。
|
||||
- 如果链条含有有效分层、阶段目标或协调人提示词,选择后按 dag 编排;否则按 chain 线性编排。
|
||||
- DAG 链在弹窗底部额外选择执行方式:
|
||||
- **快速 · 双列禁写**:同阶段席位并行,主区双列展示,并行席位不写文件。
|
||||
- **沙箱 · 单列可写**:同阶段仍并行,但主区单列展示,允许文件产出,右侧沙箱随当前席位切换。
|
||||
- “配置链条”按钮跳转到业务链编辑页;没有链条时也会显示“去配置业务链条”引导。
|
||||
|
||||
确认后传给第二步的运行结果可概括为:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势专家",
|
||||
"model": "该席位模型覆盖,可为空",
|
||||
"seat_skill_directive": true,
|
||||
"human_validation": false,
|
||||
"seatMode": "该席位推理深度覆盖,可为空"
|
||||
}
|
||||
],
|
||||
"mode": "chain | dag",
|
||||
"chain": {
|
||||
"id": "chain-id",
|
||||
"title": "任务综合研判链"
|
||||
},
|
||||
"plan": {
|
||||
"stages": "仅 dag 时携带的分层执行计划"
|
||||
},
|
||||
"dagExecMode": "fast | sandbox",
|
||||
"seatMode": "本次会商的链条级执行模式",
|
||||
"gatherFirst": true
|
||||
}
|
||||
~~~
|
||||
|
||||
其中,席位数组的顺序就是线性链执行顺序;DAG 时按阶段展平。单席位的 seatMode、模型覆盖、技能识别强化和人工校验会一并继承到第二步。
|
||||
|
||||
#### B. 顶部“业务链条”管理弹窗
|
||||
|
||||
功能:
|
||||
|
||||
- 顶栏点击“业务链条”打开,定位为“浏览和管理”,不直接选择参会人。
|
||||
- 支持按 **我的 / 公共 / 全部(仅管理员)** 筛选,再按 **全部状态 / 启用 / 停用** 过滤。
|
||||
- 每张卡片展示:
|
||||
- 名称、描述、席位数量;
|
||||
- 公共、待审核、已停用等状态;
|
||||
- 配置人和最后修改时间;
|
||||
- 智能体编排流:同 Stage 的席位用“并行 N”分组,Stage 之间以箭头连接。
|
||||
- 本人链条或管理员可在卡片上:
|
||||
- 启用 / 停用;
|
||||
- 发布为公共、取消发布;
|
||||
- 草稿链提交审核,审核中可撤回;
|
||||
- 点击“设置”跳转到该链条的编辑页。
|
||||
- 新建和编辑页面包含:
|
||||
- **基本信息**:标题、描述、默认执行模式、研讨前并行取数开关、总控提示词。
|
||||
- **链条编排**:从候选智能体池拖拽到 Stage;可新建、删除、上下移动 Stage,移动/拆分/合并席位。
|
||||
- **阶段目标**:每个 Stage 可填写目标,并可使用 AI 优化;阶段目标会指导协调人该阶段产出什么、如何承接前一阶段结果。
|
||||
- **总控提示词**:说明协调人如何派发、汇总、收口,支持 Markdown 和 AI 优化。
|
||||
- **席位级配置**:模型覆盖、推理深度覆盖、会商技能识别强化、人工校验;岗位协同专用入口还可配置岗位归属和产物位置。
|
||||
- **编排预览**:以流程图检查“同层并行、层间串行”的最终结构。
|
||||
|
||||
两个业务链弹窗的页面结构:
|
||||
|
||||
~~~text
|
||||
顶部“业务链条”管理弹窗
|
||||
┌ 标题和说明 ───────────────────────────────────────────────────────────┐
|
||||
│ 筛选:我的 / 公共 / 全部(管理员) | 全部状态 / 启用 / 停用 │
|
||||
├ 业务链卡片列表 ───────────────────────────────────────────────────────┤
|
||||
│ 名称、席位数、公共/待审核/停用状态、配置人、更新时间、说明 │
|
||||
│ 智能体 A → [并行:智能体 B、智能体 C] → 智能体 D │
|
||||
│ 操作:启停 / 发布或撤回 / 设置 │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ 底部:配置(新建或跳转到业务链编辑页) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
“选择参会智能体”弹窗的业务链页签
|
||||
┌ 顶部:智能体推荐 | 业务链条 | 本次执行模式(快速/思考/专业/多智能体)┐
|
||||
│ 筛选:我的 / 公共 │
|
||||
├ 链条选择卡片 ─────────────────────────────────────────────────────────┤
|
||||
│ 名称、描述、席位数、线性/分层编排、已删除席位告警 │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ DAG 链:快速·双列禁写 | 沙箱·单列可写 │
|
||||
│ 底部:配置链条 | 取消 | 进入研讨 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
~~~
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化意图会话
|
||||
|
||||
```http
|
||||
POST /api/intent/init
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "模型标识,可选,使用页面当前模型",
|
||||
"task_context": {
|
||||
"task_id": "任务深链时的任务 ID",
|
||||
"task_name": "任务名称",
|
||||
"task_goal": "任务目标",
|
||||
"business_code": "3Q | 6BF | 7BF"
|
||||
},
|
||||
"intent_agent_id": "roundtable-intent"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 普通会商一般省略 `task_context`,`intent_agent_id` 也可省略并由后端使用内置 `roundtable-intent`。
|
||||
- 返回 `{ status, agent_id, thread_id }`;后续上传与流式对话都使用此 `thread_id`。
|
||||
|
||||
### 2.2 发送任务或回答澄清问题
|
||||
|
||||
```http
|
||||
POST /api/intent/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"thread_id": "意图会话线程 ID",
|
||||
"message": "用户任务描述,或对澄清问题的答案",
|
||||
"model": "页面当前模型,可选",
|
||||
"intent_agent_id": "roundtable-intent",
|
||||
"task_context": {
|
||||
"task_id": "任务 ID",
|
||||
"task_name": "任务名称",
|
||||
"task_goal": "任务目标",
|
||||
"business_code": "业务编码"
|
||||
},
|
||||
"client_turn_id": "前端生成的幂等 ID",
|
||||
"files": [
|
||||
{
|
||||
"filename": "材料.pdf",
|
||||
"size": 10240,
|
||||
"path": "/mnt/user-data/uploads/材料.pdf",
|
||||
"status": "uploaded"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
主要 SSE 完成帧字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "asking | clarification | done | error",
|
||||
"content": "本轮可见回答",
|
||||
"summary": {
|
||||
"objective": "任务目标",
|
||||
"constraints": ["约束 1"],
|
||||
"assumptions": ["可选假设"]
|
||||
},
|
||||
"question": "单个澄清问题",
|
||||
"clarifications": ["多个澄清问题"],
|
||||
"clarification_type": "single | multiple | text",
|
||||
"clarification_context": {},
|
||||
"options": ["选项 A", "选项 B"],
|
||||
"allow_custom": true,
|
||||
"allow_multiple": false,
|
||||
"error_detail": "失败原因"
|
||||
}
|
||||
```
|
||||
|
||||
调用要点:
|
||||
|
||||
- `client_turn_id` 用于避免重试或网络抖动造成同一用户轮次重复执行。
|
||||
- 用户选项、填写的自定义答案、点击“继续/跳过”后,仍通过本接口的 `message` 继续同一 `thread_id`。
|
||||
- 上传接口必须先于本接口调用;流式接口只传上传成功后的虚拟路径。
|
||||
|
||||
### 2.3 获取候选智能体和执行推荐
|
||||
|
||||
```http
|
||||
GET /api/agents
|
||||
```
|
||||
|
||||
前端从返回列表中排除内置工具型智能体,将可参会的候选人整理成:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势研判专家",
|
||||
"description": "负责态势信息研判"
|
||||
}
|
||||
```
|
||||
|
||||
```http
|
||||
POST /api/recommend/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"intent": {
|
||||
"objective": "形成处置建议",
|
||||
"constraints": ["时限 24 小时"],
|
||||
"assumptions": ["数据可访问"]
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势研判专家",
|
||||
"description": "负责态势信息研判"
|
||||
}
|
||||
],
|
||||
"model": "页面当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
推荐完成帧可得到:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"content": "推荐过程说明",
|
||||
"picks": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"reason": "与任务目标的匹配理由"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
普通会商会额外保存推荐记录,便于恢复和审计:
|
||||
|
||||
| 接口 | 参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-drafts/{draftId}/recommendations` | 无请求体,读取该草稿的推荐历史。 |
|
||||
| `POST /api/roundtable-drafts/{draftId}/recommendations` | `{ objective, status, model?, rationale?, picks, candidates }`。 |
|
||||
|
||||
### 2.4 读取、选择和管理业务链
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-chains?scope=public\|mine\|pending\|all` | 获取业务链列表;入口页通常读取公开链。 |
|
||||
| `GET /api/roundtable-chains/{id}` | 获取某条业务链完整定义。 |
|
||||
| `POST /api/roundtable-chains` | 新建业务链。 |
|
||||
| `PUT /api/roundtable-chains/{id}` | 更新业务链。 |
|
||||
| `DELETE /api/roundtable-chains/{id}` | 删除业务链。 |
|
||||
| `POST /api/roundtable-chains/optimize-prompt` | 使用模型优化总控提示词或阶段目标。 |
|
||||
|
||||
读取与弹窗行为:
|
||||
|
||||
- “选择参会智能体”弹窗调用 GET /api/roundtable-chains(我的)或 GET /api/roundtable-chains?scope=public(公共),并同时调用 GET /api/agents 校验每个席位是否仍存在。
|
||||
- 顶部管理弹窗在管理员身份下还可调用 scope=all;scope=pending 用于待审核链的管理场景。非管理员请求 all/pending 时后端会退化为当前用户范围。
|
||||
- 启用状态为 false 的链条仍可在管理弹窗查看,但不会出现在“选择参会智能体”弹窗中。
|
||||
|
||||
创建或更新业务链的核心参数示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "任务综合研判链",
|
||||
"description": "先收集信息,再由专家研讨",
|
||||
"seats": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势专家",
|
||||
"description": "分析外部态势",
|
||||
"model": "模型标识",
|
||||
"seat_skill_directive": true,
|
||||
"human_validation": true,
|
||||
"seat_mode": "flash"
|
||||
}
|
||||
],
|
||||
"stages": [["agent-001", "agent-002"], ["agent-003"]],
|
||||
"stage_goals": ["平行收集信息", "汇总形成判断"],
|
||||
"coordinator_prompt": "协调人需要按风险、机会、建议汇总",
|
||||
"seat_mode": "flash",
|
||||
"gather_first": true
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- 链条创建时前端使用驼峰字段 stageGoals、coordinatorPrompt、seatMode、gatherFirst;请求发送前映射为接口字段 stage_goals、coordinator_prompt、seat_mode、gather_first。更新时传 null 可清除分层、阶段目标、总控提示词或默认执行模式。
|
||||
- `stages` 表示 DAG 分层:同层可并行,不同层按顺序执行。
|
||||
- `seat_mode` / 席位 `seat_mode` 可取 `flash`、`thinking`、`pro`、`ultra`。
|
||||
- model 为空时,该席位跟随第二步页面的全局模型;有值时仅覆盖该席位。
|
||||
- seat_skill_directive: true 会在该席位运行时强化其技能识别和使用指令;它与智能体自身同名开关任一开启即可生效。
|
||||
- `human_validation: true` 表示该席位输出后需要人工确认才可继续。
|
||||
- `gather_first: true` 表示先让全部席位平行收集信息,再进入正式研讨。
|
||||
|
||||
发布、审核和启停均通过更新接口发送局部字段:
|
||||
|
||||
| 操作 | PUT /api/roundtable-chains/{id} 请求体 |
|
||||
| --- | --- |
|
||||
| 启用 / 停用 | { "enabled": true } / { "enabled": false } |
|
||||
| 直接发布 / 取消发布 | { "is_public": true } / { "is_public": false } |
|
||||
| 提交审核 / 撤回审核 | { "pending_review": true } / { "pending_review": false } |
|
||||
| 管理员审核发布 | { "is_public": true, "pending_review": false } |
|
||||
|
||||
提示词优化接口的请求示例:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"prompt_text": "当前总控提示词或阶段目标原文",
|
||||
"kind": "coordinator | stage_goal",
|
||||
"context": "链条标题、各阶段席位及已有阶段目标组成的上下文",
|
||||
"model_name": "可选模型标识"
|
||||
}
|
||||
~~~
|
||||
|
||||
返回:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"optimized": "可直接回填到总控提示词或阶段目标编辑框的文本"
|
||||
}
|
||||
~~~
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 三步导航:①任务意图理解 ②多智能体研讨 ③方案结果展示 │
|
||||
│ 业务链 / 新建或重新分析 / 保存草稿 / 历史记录 / 模型选择 / 设置 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 左侧入口栏 ───────────┬──────────── 中间工作区 ────────────┬──── 右侧摘要栏 ────┐
|
||||
│ 任务卡(仅任务深链) │ Step1 对话面板 │ 当前模型选择 │
|
||||
│ 业务链列表与展开关系 │ - 意图智能体消息 │ 结构化意图摘要 │
|
||||
│ 会商/分析历史记录 │ - 思考、工具、澄清卡 │ - 目标 │
|
||||
│ │ - 输入框、附件、发送/停止/继续 │ - 约束 │
|
||||
│ │ │ - 假设,均可编辑 │
|
||||
└───────────────────────────────────┴────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
任务深链还会显示任务目的入口和底部 `TaskJumpBar` 快捷跳转栏;任务模式隐藏手工选择业务链的入口,以深链业务编码自动选择链条。
|
||||
|
||||
---
|
||||
|
||||
# 第二步:多智能体研讨
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第二步以第一步意图和选定智能体/业务链为输入,创建协调人与多个席位智能体,进行可观察、可干预、可恢复的会商。
|
||||
|
||||
- 为每位参会智能体和协调人创建独立线程。
|
||||
- 支持三种编排模式:
|
||||
- **推荐模式(recommend)**:协调人自由决定调度哪些推荐智能体。
|
||||
- **链条模式(chain)**:按业务链指定的席位顺序执行。
|
||||
- **DAG 模式(dag)**:同阶段并行、跨阶段串行;可传入阶段目标和协调人提示词。
|
||||
- DAG 支持 `fast` 与 `sandbox` 执行视图。快速模式会限制并行席位写文件;沙箱模式可产出和预览文件。
|
||||
- 可选“先收集后研讨”:全部席位先并行收集资料,再将有效交付共享给正式研讨。
|
||||
- 协调人可继续派发、发起人工澄清,或在不再派发时形成共识。
|
||||
- 用户可暂停/继续本地编排,回答协调人的澄清问题,向协调人追加文字或附件。
|
||||
- 席位可配置人工验证点;达到验证点后需人工确认。
|
||||
- 支持打开角色画像抽屉,在下一次运行前调整单席位模型和运行策略。
|
||||
- 实时展示角色状态、对话、思考、工具调用、阶段流图、并行席位、文件产物和人工参与记录。
|
||||
- 每次重新推荐或选择链条都会创建独立“研讨运行快照”;历史运行互不覆盖,可在左侧切换查看。
|
||||
- 支持将长时间会商转入后台任务。后台任务可被同一任务的其他用户看到、跟踪、继续回答或接管。
|
||||
|
||||
步骤门槛:
|
||||
|
||||
- 只有第一步的 `intentReady` 为真时,第二步可进入。
|
||||
- 协调人形成共识(`hasConsensus`)后,第三步才可进入。
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化所有参会线程
|
||||
|
||||
```http
|
||||
POST /api/multi-agent/init
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_names": ["agent-001", "agent-002"],
|
||||
"main_agent_name": "coordinator",
|
||||
"model": "页面当前模型,可选"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `agent_names` 为选中的席位智能体标识。
|
||||
- `main_agent_name` 为协调人标识。
|
||||
- SSE 依次返回席位就绪、协调人就绪和初始化完成事件,最终得到线程映射:
|
||||
|
||||
```json
|
||||
{
|
||||
"thread_ids": {
|
||||
"agent-001": "席位线程 ID",
|
||||
"agent-002": "席位线程 ID",
|
||||
"coordinator": "协调人线程 ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 驱动协调人和席位研讨
|
||||
|
||||
```http
|
||||
POST /api/multi-agent/run/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
基础请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "leader | special",
|
||||
"agent_name": "coordinator 或席位 agent ID",
|
||||
"display_name": "页面展示名称",
|
||||
"d_agent_thread_id": {
|
||||
"coordinator": "协调人线程 ID",
|
||||
"agent-001": "席位线程 ID"
|
||||
},
|
||||
"new_message": "给协调人或席位的本轮提示词",
|
||||
"model": "模型标识",
|
||||
"business_code": "3Q | 6BF | 7BF",
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
关键参数:
|
||||
|
||||
| 参数 | 适用对象 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `agent_type: "leader"` | 协调人 | 由协调人分析现状、派发席位或形成共识。 |
|
||||
| `agent_type: "special"` | 席位、总结/报告等专用智能体 | 执行专业分析、总结或文件生成。 |
|
||||
| `d_agent_thread_id` | 全部 | 是“智能体 ID → 线程 ID”的映射,不是单一线程 ID。 |
|
||||
| `thinking_enabled?` | 席位 | 是否启用思考。 |
|
||||
| `reasoning_effort?` | 席位 | 推理强度。 |
|
||||
| `subagent_enabled?` | 席位 | 是否允许席位再调用子智能体。 |
|
||||
| `seat_skill_directive?` | 席位 | 席位特定的技能使用指令。 |
|
||||
| `parallel_no_file?` | 并行席位 | 快速并行时限制文件写入。 |
|
||||
| `peer_deliveries?` | 后续席位/协调人 | 传递前序席位可见交付物。 |
|
||||
| `synthesis_mode?` | 协调人 | 指示汇总类执行。 |
|
||||
| `suppress_pre_broadcast?` | 协调人 | 控制汇总前广播。 |
|
||||
| `skill_stop_names?` | 全部 | 指定需要拦截/停止的技能名称。 |
|
||||
|
||||
流式完成后前端重点读取:
|
||||
|
||||
```json
|
||||
{
|
||||
"dispatched": ["agent-001"],
|
||||
"content": "可见回答或结论",
|
||||
"agentName": "本次完成者",
|
||||
"modelUsed": "实际使用模型",
|
||||
"clarification": {
|
||||
"question": "需要用户回答的问题",
|
||||
"options": ["选项 A"]
|
||||
},
|
||||
"invalidDelivery": false
|
||||
}
|
||||
```
|
||||
|
||||
编排规则:
|
||||
|
||||
- 协调人先运行并决定需调度的席位。
|
||||
- 每个席位完成后,将其“可见回答”作为交付物回传,思考内容不作为席位交付。
|
||||
- 链条模式按链定义的顺序运行;DAG 模式同层并行、阶段间串行。
|
||||
- 协调人返回不再派发且无待答澄清时,前端将结果标记为共识。
|
||||
|
||||
### 2.3 产物查看与下载
|
||||
|
||||
| 接口 | 调用方式 |
|
||||
| --- | --- |
|
||||
| `GET /api/threads/{threadId}/artifacts` | 查询指定协调人或席位线程的文件产物。 |
|
||||
| `GET /api/threads/{threadId}/artifacts/{path}` | 预览或下载文件;下载时前端附加 `?download=true`。 |
|
||||
|
||||
### 2.4 后台会商任务
|
||||
|
||||
开始后台任务:
|
||||
|
||||
```http
|
||||
POST /api/roundtable-jobs
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "前端请求 ID",
|
||||
"draftId": "会商草稿 ID",
|
||||
"taskId": "任务深链时的任务 ID",
|
||||
"intent": {
|
||||
"objective": "任务目标",
|
||||
"constraints": ["约束"],
|
||||
"assumptions": ["假设"]
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "agent-001",
|
||||
"name": "态势专家"
|
||||
}
|
||||
],
|
||||
"model": "当前模型",
|
||||
"orchestrationMode": "recommend | chain | dag",
|
||||
"chain": {
|
||||
"id": "chain-id",
|
||||
"title": "业务链名称"
|
||||
},
|
||||
"orchestrationPlan": {},
|
||||
"seedMessage": "首次给协调人的提示",
|
||||
"threadIds": {
|
||||
"coordinator": "线程 ID",
|
||||
"agent-001": "线程 ID"
|
||||
},
|
||||
"coordinatorName": "协调人名称",
|
||||
"seat_skill_directives": {
|
||||
"agent-001": true
|
||||
},
|
||||
"seat_modes": {
|
||||
"agent-001": "thinking"
|
||||
},
|
||||
"gather_first": true,
|
||||
"business_code": "3Q",
|
||||
"thinking_enabled": true,
|
||||
"reasoning_effort": "medium",
|
||||
"subagent_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
后台任务接口:
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-jobs?draft_id={draftId}&task_id={taskId}` | 获取草稿/任务关联的后台任务列表。任务模式需带 `task_id`。 |
|
||||
| `GET /api/roundtable-jobs/{jobId}?task_id={taskId}` | 获取任务快照。 |
|
||||
| `GET /api/roundtable-jobs/{jobId}/stream?task_id={taskId}` | SSE 跟踪后台任务完整进度。 |
|
||||
| `POST /api/roundtable-jobs/{jobId}/resume?task_id={taskId}` | JSON:`{ answer }`,用于回答后台任务等待的人工问题。 |
|
||||
| `POST /api/roundtable-jobs/{jobId}/cancel?task_id={taskId}` | 取消后台任务;前台接管时也会先结束后台执行。 |
|
||||
|
||||
状态和阶段:
|
||||
|
||||
| 分类 | 枚举 |
|
||||
| --- | --- |
|
||||
| 任务状态 | `queued`、`running`、`awaiting_input`、`done`、`error`、`cancelled` |
|
||||
| 执行阶段 | `initializing`、`leader_thinking`、`dispatching`、`consensus`、`summary_writing`、`report_drawing`、`report_done` |
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 三步导航 / 会商记录 / 模型选择 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 可收起左栏 ───────────┬──────────── 中间研讨区 ─────────────┬──── 右侧沙箱区 ────┐
|
||||
│ 意图摘要 │ 顶部:模型、暂停/继续、转后台 │ Step2SandboxLayout │
|
||||
│ 角色列表与席位筛选 │ DAG 流程缩略图、并行席位切换 │ 文件/网页预览 │
|
||||
│ 人工参与/验证状态 │ 对话时间线:协调人、席位、工具、思考 │ 产物查看和下载 │
|
||||
│ 多次研讨运行快照切换 │ 澄清问题卡、人工确认卡 │ 打开时左栏自动收起 │
|
||||
│ 任务概览(任务深链) │ 底部:追加消息、附件、发送 │ │
|
||||
└───────────────────────────────────┴─────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
页面展示规则:
|
||||
|
||||
- 角色卡显示席位状态、当前轮次、模型和是否等待人工确认。
|
||||
- 对话区可按角色筛选,思考内容默认折叠,工具调用和交付物以步骤卡展示。
|
||||
- DAG 研讨可打开流程面板观察阶段、依赖和并行关系。
|
||||
- 打开沙箱/产物预览后,优先保证产物可读区域,左侧栏自动收起。
|
||||
|
||||
---
|
||||
|
||||
# 第三步:方案结果展示
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第三步基于第一步意图、第二步席位交付和协调人共识,生成可阅读、可修改、可下载、可继续加工的最终方案。
|
||||
|
||||
- 首次进入常规会商第三步时,启动三个相互独立的产出流:
|
||||
- **方案总结报告**:生成 Markdown 方案,是默认主界面。
|
||||
- **经典可视化报告**:生成 HTML 风格报告/仪表盘。
|
||||
- **结构化仪表盘**:生成 `report-json`,由前端 `DashboardApp` 渲染。
|
||||
- 三条流以约 0 / 300 / 600 ms 错峰启动,减少瞬间请求峰值,但互不等待。
|
||||
- 已保存草稿再次打开时优先恢复既有结果,不重复生成;切换到新会商运行时会终止旧运行的活跃流。
|
||||
- Markdown 报告完成后,常规模式自动调用流程图提取,生成 `flow-json` 结构化流程图。
|
||||
- 用户可对报告提问、要求重写、修改流程图、重新生成流程图或报告。
|
||||
- 任务深链可根据业务链层次和业务编码生成任务 → 目的 → 行为体等结构;行动业务会使用行动名称作为根节点。
|
||||
- 若任务和技能条件满足,可选择“现在导入 / 稍后导入 / 不再导入”,把报告和流程结构交给 `roundtable-structure` 进行任务结构化入库。
|
||||
- 左侧汇集协调人共识、各席位交付和文件;右侧可预览 Markdown、流程图及沙箱产物。
|
||||
- 支持下载会商文档和智能体产物。任务模式还可将选定产物提交到任务交付接口。
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化第三步专用智能体
|
||||
|
||||
| 目标 | 接口 | 返回 |
|
||||
| --- | --- | --- |
|
||||
| Markdown 方案总结 | `POST /api/multi-agent/summary/init` | `{ agent_name, thread_id }` |
|
||||
| 经典可视化报告 | `POST /api/multi-agent/report/init` | `{ agent_name, thread_id }` |
|
||||
| 结构化仪表盘 | `POST /api/multi-agent/dashboard/init` | `{ agent_name, thread_id }` |
|
||||
| 任务结构化入库 | `POST /api/multi-agent/structure/init` | `{ agent_name, thread_id }` |
|
||||
|
||||
初始化请求体均为:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
各专用智能体初始化后,均继续通过统一接口 `POST /api/multi-agent/run/stream` 运行。
|
||||
|
||||
### 2.2 生成或修改 Markdown 方案总结
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-summary",
|
||||
"display_name": "方案总结报告",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-summary": "总结线程 ID"
|
||||
},
|
||||
"new_message": "包含结构化意图、席位交付、协调人共识、链条提示词的生成或修改指令",
|
||||
"model": "当前模型",
|
||||
"business_code": "3Q",
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
功能覆盖:
|
||||
|
||||
- 首次生成 Markdown 报告。
|
||||
- 在报告下方继续问答或下达修改指令。
|
||||
- 支持暂停、继续、重新生成和生成后下载 Docx。
|
||||
- 上述问答与修改沿用同一总结线程,保证上下文连续。
|
||||
|
||||
### 2.3 生成经典报告和仪表盘
|
||||
|
||||
经典报告请求使用 `roundtable-report`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-report",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-report": "报告线程 ID"
|
||||
},
|
||||
"new_message": "包含意图、席位交付、共识、主题模式、修订要求和已有 HTML 的提示词",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
结构化仪表盘请求使用 `roundtable-dashboard`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-dashboard",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-dashboard": "仪表盘线程 ID"
|
||||
},
|
||||
"new_message": "包含意图、席位交付、共识,或对已有仪表盘的修订要求",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
前端依据返回的 HTML 或 `report-json` 分别渲染经典报告和 `DashboardApp`,并提供全屏、在新窗口打开、继续优化以及切回经典报告的操作。
|
||||
|
||||
### 2.4 自动提取和修改流程图
|
||||
|
||||
```http
|
||||
POST /api/flow-extract/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"report_md": "Markdown 完整报告",
|
||||
"seat_deliveries": [
|
||||
{
|
||||
"sender": "态势专家",
|
||||
"content": "席位可见交付"
|
||||
}
|
||||
],
|
||||
"chain_levels": [["信息收集"], ["研判决策"]],
|
||||
"coordinator_prompt": "协调人对结构层次的要求",
|
||||
"business_code": "3Q",
|
||||
"root_label": "任务根节点名称",
|
||||
"model": "当前模型",
|
||||
"current_graph": {
|
||||
"nodes": [],
|
||||
"edges": []
|
||||
},
|
||||
"instruction": "用户对当前流程图的修改要求"
|
||||
}
|
||||
```
|
||||
|
||||
参数使用规则:
|
||||
|
||||
- 首次自动提取可不传 `current_graph` 和 `instruction`。
|
||||
- 修改现有图时传当前图和用户要求。
|
||||
- 任务模式的 `chain_levels` 由业务链阶段目标推导;若 `coordinator_prompt` 明确规定层次,则优先使用该要求。
|
||||
|
||||
流中包含提取增量、校验过程、模型切换和最终图数据,最终形成前端 `FlowGraphPanel` 使用的 `flow-json`。
|
||||
|
||||
### 2.5 判断用户在第三步的意图
|
||||
|
||||
| 接口 | 请求体 | 返回用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/flow-extract/intent` | `{ message, has_report, has_flow, can_ingest, model? }` | 分类为 `regenerate_report`、`modify_flow`、`regenerate_flow`、`ingest`、`qa`、`unclear`,决定继续调用哪一条流。 |
|
||||
| `POST /api/flow-extract/ingest-intent` | `{ message, model? }` | 在已提示导入或稍后导入时,判断 `start_ingest`、`add_data`、`other`、`unclear`。 |
|
||||
|
||||
### 2.6 任务结构化入库
|
||||
|
||||
先检查是否可入库:
|
||||
|
||||
```http
|
||||
POST /api/flow-extract/ingest-eligibility
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "任务 ID",
|
||||
"business_code": "3Q",
|
||||
"chain_name": "业务链名称",
|
||||
"coordinator_prompt": "协调人提示词",
|
||||
"report_title": "报告标题",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"can_ingest": true,
|
||||
"skill": "命中的入库技能",
|
||||
"reason": "是否可入库的原因"
|
||||
}
|
||||
```
|
||||
|
||||
符合条件且用户确认后,初始化 `roundtable-structure` 线程,并调用统一研讨流:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-structure",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-structure": "结构化入库线程 ID"
|
||||
},
|
||||
"new_message": "包含 taskId、业务编码/链条/技能提示、最新 Markdown 报告、flow-json 和用户补充说明的指令",
|
||||
"model": "当前模型",
|
||||
"business_code": "3Q"
|
||||
}
|
||||
```
|
||||
|
||||
注意:前端不会直接调用某个入库技能,而是交给 `roundtable-structure` 智能体在具备完整报告和流程图上下文后执行。
|
||||
|
||||
### 2.7 任务交付物提交
|
||||
|
||||
任务深链启用交付配置时,第三步可以查询和更新任务产物提交状态。
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable/tasks/{taskId}/artifacts?submitted=true\|false` | 获取任务已提交或待提交产物。 |
|
||||
| `PUT /api/roundtable/tasks/{taskId}/artifacts/submission` | 更新某个产物的提交状态。 |
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "任务 ID",
|
||||
"submitted": true,
|
||||
"registerOnly": false,
|
||||
"submissionId": "已有提交记录 ID,可选",
|
||||
"taskDeliveryId": "外部交付 ID,可选",
|
||||
"expectedVersion": 3,
|
||||
"artifact": {
|
||||
"sessionId": "会商会话 ID",
|
||||
"threadId": "产物所属线程 ID",
|
||||
"nodeKey": "流程节点标识,可选",
|
||||
"sourceKey": "来源标识",
|
||||
"path": "/mnt/user-data/outputs/report.md",
|
||||
"name": "会商报告.md",
|
||||
"mimeType": "text/markdown",
|
||||
"revision": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
默认的“方案总结”视图:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 步骤导航 / 报告标题 / 模型 / 总结报告、流程图、最终方案等视图切换 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 可收起左栏 ───────────┬──────────── 中间主内容 ─────────────┬──── 右侧结果区 ────┐
|
||||
│ 协调人共识 │ Markdown 总结报告对话流 │ Markdown 预览 │
|
||||
│ 各席位交付 │ 报告问答、修改、重生成入口 │ 或 FlowGraphPanel │
|
||||
│ 各线程文件/产物 │ 输入框与附件 │ 文件/沙箱预览 │
|
||||
│ │ │ │
|
||||
└───────────────────────────────────┴─────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
其他结果视图:
|
||||
|
||||
| 视图 | 中央区域 | 右侧/扩展区域 |
|
||||
| --- | --- | --- |
|
||||
| 最终方案 | 汇总后的静态方案和文件清单 | 可查看来源与下载产物。 |
|
||||
| 经典可视化报告 | 报告生成对话、修订指令 | HTML/沙箱预览,支持全屏和下载。 |
|
||||
| 结构化仪表盘 | 仪表盘优化对话 | `DashboardApp` 或 HTML 视图,可新窗口打开。 |
|
||||
| 流程图 | 报告问答和流程图修改指令 | `FlowGraphPanel` 显示节点、关系和层级。 |
|
||||
|
||||
常规第三步会隐藏外层研讨左导航,以扩大结果阅读区域;任务深链保留任务环境相关的交付与跳转能力。
|
||||
|
||||
## 4. 三步数据衔接
|
||||
|
||||
```text
|
||||
第一步
|
||||
结构化意图 + 已选智能体/业务链 + 意图线程
|
||||
│
|
||||
▼
|
||||
第二步
|
||||
席位线程 + 协调人线程 + 对话/交付 + 共识 + 研讨运行快照
|
||||
│
|
||||
▼
|
||||
第三步
|
||||
Markdown 报告 + HTML 报告 + report-json 仪表盘 + flow-json 流程图 + 任务入库/交付
|
||||
```
|
||||
|
||||
关键约束:
|
||||
|
||||
- 第一步未形成有效意图,不能开始第二步。
|
||||
- 第二步未形成协调人共识,不能进入第三步。
|
||||
- 第三步的所有产出都应绑定对应的第二步研讨运行快照,避免新一轮会商覆盖历史结果。
|
||||
- 后端发生模型降级/切换时,前端应保留已收到的流内容并显示实际使用模型或切换提示。
|
||||
|
||||
## 5. 主要前端模块索引
|
||||
|
||||
| 范围 | 主要文件/目录 |
|
||||
| --- | --- |
|
||||
| 页面编排 | `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` |
|
||||
| 第一步 | `components/Step1Panel.tsx`、`hooks/useStep1Intent.ts`、`api/intent.ts` |
|
||||
| 推荐与业务链 | `api/recommend.ts`、`components/RecommendAgentsDialog.tsx`、`api/chains.ts` |
|
||||
| 第二步 | `components/Step2Panel.tsx`、`components/Step2SandboxLayout.tsx`、`hooks/useStep2Orchestration.ts` |
|
||||
| 多智能体与后台任务 API | `api/multi-agent.ts`、`api/roundtable-jobs.ts` |
|
||||
| 第三步 | `components/Step3Panel.tsx`、`components/HighFidelityReport.tsx`、`hooks/useStep3Summary.ts`、`hooks/useStep3Report.ts`、`hooks/useStep3Dashboard.ts` |
|
||||
| 流程图与任务入库 | `api/flow-extract.ts`、`api/ingest.ts` |
|
||||
| 草稿持久化 | `api/drafts.ts`、`api/task-drafts.ts`、相关草稿 hooks |
|
||||
@@ -12,46 +12,12 @@
|
||||
root.classList.add("dark");
|
||||
} catch (e) {}
|
||||
})();
|
||||
// Chrome 102 has no AbortSignal.timeout (Chrome 103+). Install before
|
||||
// any module so LangGraph SDK control requests can set a deadline.
|
||||
(function () {
|
||||
try {
|
||||
if (typeof AbortSignal === "undefined" || typeof AbortSignal.timeout === "function") {
|
||||
return;
|
||||
}
|
||||
AbortSignal.timeout = function (milliseconds) {
|
||||
var controller = new AbortController();
|
||||
var timer = setTimeout(function () {
|
||||
var reason;
|
||||
try {
|
||||
reason = new DOMException("signal timed out", "TimeoutError");
|
||||
} catch (err) {
|
||||
reason = new Error("signal timed out");
|
||||
reason.name = "TimeoutError";
|
||||
}
|
||||
try {
|
||||
controller.abort(reason);
|
||||
} catch (err) {
|
||||
controller.abort();
|
||||
}
|
||||
}, milliseconds);
|
||||
controller.signal.addEventListener(
|
||||
"abort",
|
||||
function () {
|
||||
clearTimeout(timer);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
return controller.signal;
|
||||
};
|
||||
} catch (err) {}
|
||||
})();
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>CM 助手</title>
|
||||
<script src="/runtime-config.js" data-runtime-config></script>
|
||||
</head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>CM 助手</title>
|
||||
<script src="/runtime-config.js" data-runtime-config></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
@@ -1,178 +1,135 @@
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "rspack serve --config rspack.firefox52.config.mjs --mode development",
|
||||
"dev:vite": "vite",
|
||||
"dev:firefox52": "rspack serve --config rspack.firefox52.config.mjs --mode development",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:firefox52": "rspack build --config rspack.firefox52.config.mjs --mode production",
|
||||
"build:firefox52:fast": "rspack build --config rspack.firefox52.config.mjs --mode production --env fast=true",
|
||||
"build:firefox52:vite": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode firefox52 --minify false",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"preview:firefox52": "rspack serve --config rspack.firefox52.config.mjs --mode production --env preview=true",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
|
||||
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
|
||||
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
|
||||
"test:abort-signal": "node --experimental-strip-types --test src/core/compat/abort-signal.test.ts",
|
||||
"test:firefox52-fetch": "node --experimental-strip-types --test src/core/compat/firefox52-fetch.test.ts",
|
||||
"test:firefox52-markdown": "node --experimental-strip-types --test src/core/rehype/index.test.ts",
|
||||
"smoke:firefox52": "node scripts/smoke-firefox52.mjs",
|
||||
"smoke:firefox52:live": "node scripts/smoke-firefox52.mjs --live",
|
||||
"smoke:firefox52:agent-live": "node scripts/smoke-firefox52-agent-live.mjs",
|
||||
"smoke:firefox52:full": "node scripts/smoke-firefox52-full.mjs",
|
||||
"smoke:firefox52:themes": "node scripts/smoke-firefox52-themes.mjs",
|
||||
"benchmark:firefox52": "node scripts/benchmark-firefox52.mjs",
|
||||
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
|
||||
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
|
||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:firefox52-css": "node scripts/check-firefox52-css.mjs",
|
||||
"check:firefox52-js": "node scripts/check-firefox52-js.mjs",
|
||||
"test:firefox52-build": "node --test build/firefox52/legacy-generator-loader.test.cjs build/firefox52/source-compat-loader.test.cjs",
|
||||
"check:firefox52": "pnpm test:firefox52-build && pnpm test:firefox52-markdown && pnpm check:firefox52-css && pnpm check:firefox52-js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"core-js": "3.50.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"fflate": "^0.8.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "4.7.1",
|
||||
"marked": "16.4.2",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"pepjs": "0.5.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@rspack/cli": "2.2.8",
|
||||
"@rspack/core": "2.2.8",
|
||||
"@rspack/dev-server": "2.2.1",
|
||||
"@swc/core": "1.16.13",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-legacy": "6.1.1",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"abortcontroller-polyfill": "^1.7.8",
|
||||
"acorn": "8.19.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-loader": "10.0.0",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-loader": "8.2.1",
|
||||
"postcss-selector-parser": "^6.1.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"terser": "^5.51.2",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2",
|
||||
"web-streams-polyfill": "4.3.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2",
|
||||
"browserslist": [
|
||||
"Firefox >= 52"
|
||||
]
|
||||
}
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
|
||||
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
|
||||
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
|
||||
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
|
||||
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
|
||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^6.9.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import tailwindcss from "tailwindcss";
|
||||
import darkDescendant from "./scripts/postcss-dark-descendant.mjs";
|
||||
import autoprefixer from "autoprefixer";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs";
|
||||
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs";
|
||||
|
||||
export default {
|
||||
plugins: [tailwindcss, darkDescendant, autoprefixer],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
legacyColorFallback(),
|
||||
lightningCssLegacyChrome(),
|
||||
],
|
||||
};
|
||||
@@ -98,10 +98,10 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope",
|
||||
|
||||
// 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。
|
||||
VITE_AIPI_BACKENP_URL: "https://knowledgetoolserver.b.uat.4.cn",
|
||||
VITE_AIPI_BACKENP_URL: "http://47.88.25.99:8000",
|
||||
|
||||
// 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。
|
||||
VITE_NOTEBOOK_API_BASE_URL: "https://knowledgetoolserver.b.uat.4.cn",
|
||||
VITE_NOTEBOOK_API_BASE_URL: "http://47.88.25.99:8000",
|
||||
|
||||
// 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。
|
||||
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
CopyRspackPlugin,
|
||||
DefinePlugin,
|
||||
HtmlRspackPlugin,
|
||||
LightningCssMinimizerRspackPlugin,
|
||||
SwcJsMinimizerRspackPlugin,
|
||||
} from "@rspack/core";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const firefox52Target = { firefox: "52" };
|
||||
|
||||
function loadViteEnvironment(mode) {
|
||||
const values = {};
|
||||
const files = [
|
||||
".env",
|
||||
`.env.${mode}`,
|
||||
".env.local",
|
||||
`.env.${mode}.local`,
|
||||
];
|
||||
|
||||
for (const filename of files) {
|
||||
const fullPath = path.join(rootDir, filename);
|
||||
if (!fs.existsSync(fullPath)) continue;
|
||||
Object.assign(values, dotenv.parse(fs.readFileSync(fullPath)));
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (key.startsWith("VITE_") && typeof value === "string") {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...Object.fromEntries(
|
||||
Object.entries(values).filter(([key]) => key.startsWith("VITE_")),
|
||||
),
|
||||
BASE_URL: process.env.VITE_ASSET_BASE ?? "./",
|
||||
MODE: mode,
|
||||
DEV: mode === "development",
|
||||
PROD: mode === "production",
|
||||
SSR: false,
|
||||
};
|
||||
}
|
||||
|
||||
function cssLoaders(postcssConfig) {
|
||||
const loaders = [
|
||||
{
|
||||
loader: "builtin:lightningcss-loader",
|
||||
options: {
|
||||
targets: firefox52Target,
|
||||
include: {
|
||||
logicalProperties: true,
|
||||
},
|
||||
minify: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (postcssConfig) {
|
||||
loaders.push({
|
||||
loader: "postcss-loader",
|
||||
options: {
|
||||
implementation: "postcss",
|
||||
postcssOptions: {
|
||||
config: path.join(rootDir, "build", "firefox52", postcssConfig),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return loaders;
|
||||
}
|
||||
|
||||
function swcLoader(isDevelopment) {
|
||||
return {
|
||||
loader: "builtin:swc-loader",
|
||||
options: {
|
||||
detectSyntax: "auto",
|
||||
isModule: "unknown",
|
||||
env: {
|
||||
targets: firefox52Target,
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
jsc: {
|
||||
externalHelpers: false,
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
development: isDevelopment,
|
||||
refresh: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default (_env = {}, argv = {}) => {
|
||||
const mode = argv.mode === "development" ? "development" : "production";
|
||||
const isDevelopment = mode === "development";
|
||||
const isFastBuild = _env.fast === true || _env.fast === "true";
|
||||
const isPreview = _env.preview === true || _env.preview === "true";
|
||||
const assetBase = isDevelopment || isPreview
|
||||
? "/"
|
||||
: process.env.VITE_ASSET_BASE ?? "./";
|
||||
const buildEnv = loadViteEnvironment(mode);
|
||||
|
||||
return {
|
||||
name: "firefox52",
|
||||
mode,
|
||||
context: rootDir,
|
||||
target: ["web", "es5"],
|
||||
entry: {
|
||||
app: path.join(rootDir, "src", "main.tsx"),
|
||||
},
|
||||
output: {
|
||||
path: path.join(rootDir, "dist-firefox52"),
|
||||
publicPath: assetBase,
|
||||
filename: isDevelopment ? "assets/[name].js" : "assets/[name].[contenthash:8].js",
|
||||
chunkFilename: isDevelopment
|
||||
? "assets/[name].chunk.js"
|
||||
: "assets/[name].[contenthash:8].chunk.js",
|
||||
cssFilename: isDevelopment
|
||||
? "assets/[name].css"
|
||||
: "assets/[name].[contenthash:8].css",
|
||||
cssChunkFilename: isDevelopment
|
||||
? "assets/[name].chunk.css"
|
||||
: "assets/[name].[contenthash:8].chunk.css",
|
||||
clean: true,
|
||||
module: false,
|
||||
environment: {
|
||||
arrowFunction: false,
|
||||
asyncFunction: false,
|
||||
bigIntLiteral: false,
|
||||
const: false,
|
||||
destructuring: false,
|
||||
dynamicImport: false,
|
||||
module: false,
|
||||
optionalChaining: false,
|
||||
templateLiteral: false,
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
type: "persistent",
|
||||
name: `firefox52-${mode}`,
|
||||
version: "2-tailwind3",
|
||||
buildDependencies: [
|
||||
fileURLToPath(import.meta.url),
|
||||
path.join(rootDir, "package.json"),
|
||||
path.join(rootDir, "postcss.config.js"),
|
||||
path.join(rootDir, "tailwind.config.ts"),
|
||||
path.join(rootDir, "scripts", "postcss-dark-descendant.mjs"),
|
||||
path.join(rootDir, "scripts", "postcss-firefox52-rgb-vars.mjs"),
|
||||
path.join(rootDir, "scripts", "postcss-firefox52-where.mjs"),
|
||||
path.join(rootDir, "build", "firefox52", "source-compat-loader.cjs"),
|
||||
path.join(rootDir, "build", "firefox52", "legacy-generator-loader.cjs"),
|
||||
path.join(rootDir, "build", "firefox52", "postcss-tailwind.config.mjs"),
|
||||
path.join(rootDir, "build", "firefox52", "postcss-compat.config.mjs"),
|
||||
],
|
||||
storage: {
|
||||
type: "filesystem",
|
||||
directory: path.join(rootDir, ".rspack-cache"),
|
||||
},
|
||||
},
|
||||
experiments: {
|
||||
css: true,
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
devtool: isDevelopment ? "cheap-module-source-map" : false,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
enforce: "pre",
|
||||
test(resourcePath) {
|
||||
const id = resourcePath.replaceAll("\\", "/");
|
||||
return (
|
||||
/\/p-queue\/dist\/index\.js$/.test(id) ||
|
||||
/\/mdast-util-gfm-autolink-literal\/lib\/index\.js$/.test(id) ||
|
||||
id.includes("/oniguruma-parser/") ||
|
||||
id.includes("/node_modules/mermaid/dist/chunks/") ||
|
||||
id.includes("/node_modules/web-streams-polyfill/dist/") ||
|
||||
id.includes("/node_modules/@xyflow/react/") ||
|
||||
id.includes("/node_modules/react-router/") ||
|
||||
id.includes("/node_modules/zod/") ||
|
||||
id.includes("/node_modules/langsmith/")
|
||||
);
|
||||
},
|
||||
use: path.join(rootDir, "build", "firefox52", "source-compat-loader.cjs"),
|
||||
},
|
||||
{
|
||||
oneOf: [
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: "asset/source",
|
||||
},
|
||||
{
|
||||
test(resourcePath) {
|
||||
const id = resourcePath.replaceAll("\\", "/");
|
||||
return (
|
||||
id.includes("/node_modules/@langchain/core/") ||
|
||||
id.includes("/node_modules/@langchain/langgraph-sdk/") ||
|
||||
id.includes("/node_modules/langsmith/")
|
||||
);
|
||||
},
|
||||
// Loaders run right-to-left: normalize modules that declare or
|
||||
// consume async iterators first, then let the fast built-in SWC
|
||||
// pass handle the complete package consistently.
|
||||
use: [
|
||||
swcLoader(isDevelopment),
|
||||
path.join(rootDir, "build", "firefox52", "legacy-generator-loader.cjs"),
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(?:js|mjs|cjs|jsx|ts|tsx)$/,
|
||||
exclude: [
|
||||
/node_modules[\\/]core-js[\\/]/,
|
||||
/node_modules[\\/]regenerator-runtime[\\/]/,
|
||||
/node_modules[\\/]web-streams-polyfill[\\/]/,
|
||||
],
|
||||
use: swcLoader(isDevelopment),
|
||||
},
|
||||
{
|
||||
test: /[\\/]src[\\/]styles[\\/]globals\.css$/,
|
||||
type: "css",
|
||||
use: cssLoaders("postcss-tailwind.config.mjs"),
|
||||
},
|
||||
{
|
||||
test: /[\\/]src[\\/].*\.css$/,
|
||||
type: "css",
|
||||
use: cssLoaders("postcss-compat.config.mjs"),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
type: "css",
|
||||
use: cssLoaders(null),
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
type: "webassembly/async",
|
||||
},
|
||||
{
|
||||
test: /\.(?:png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot)$/i,
|
||||
type: "asset/resource",
|
||||
generator: {
|
||||
filename: "assets/[name].[contenthash:8][ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs", ".json"],
|
||||
alias: {
|
||||
"highlight.js/lib/languages/sql_more$": "highlight.js/lib/languages/sql",
|
||||
"@/env$": path.join(rootDir, "src", "env.ts"),
|
||||
"next/navigation$": path.join(rootDir, "src", "shims", "next-navigation.ts"),
|
||||
"next/link$": path.join(rootDir, "src", "shims", "next-link.tsx"),
|
||||
"@vite": path.join(rootDir, "src"),
|
||||
"@": path.join(rootDir, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
"import.meta.env": JSON.stringify(buildEnv),
|
||||
__FIREFOX52_BUILD__: "true",
|
||||
}),
|
||||
new HtmlRspackPlugin({
|
||||
template: path.join(rootDir, "build", "firefox52", "index.html"),
|
||||
filename: "index.html",
|
||||
inject: "body",
|
||||
scriptLoading: "blocking",
|
||||
minify: false,
|
||||
}),
|
||||
new CopyRspackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.join(rootDir, "public"),
|
||||
to: ".",
|
||||
noErrorOnMissing: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
optimization: {
|
||||
minimize: !isDevelopment && !isFastBuild,
|
||||
minimizer: [
|
||||
new SwcJsMinimizerRspackPlugin({
|
||||
minimizerOptions: {
|
||||
ecma: 5,
|
||||
},
|
||||
}),
|
||||
new LightningCssMinimizerRspackPlugin({
|
||||
minimizerOptions: {
|
||||
targets: firefox52Target,
|
||||
include: {
|
||||
logicalProperties: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
host: "0.0.0.0",
|
||||
port: Number(process.env.FIREFOX52_PORT || 5174),
|
||||
hot: false,
|
||||
liveReload: true,
|
||||
historyApiFallback: true,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
static: {
|
||||
directory: isPreview
|
||||
? path.join(rootDir, "dist-firefox52")
|
||||
: path.join(rootDir, "public"),
|
||||
watch: !isPreview,
|
||||
},
|
||||
devMiddleware: {
|
||||
publicPath: "/",
|
||||
writeToDisk: isPreview,
|
||||
},
|
||||
client: {
|
||||
overlay: true,
|
||||
},
|
||||
},
|
||||
stats: "errors-warnings",
|
||||
performance: false,
|
||||
};
|
||||
};
|
||||
@@ -1,122 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rounds = Math.max(1, Number(process.env.FIREFOX_BENCHMARK_ROUNDS || 5));
|
||||
const cold = process.argv.includes("--cold");
|
||||
const fast = process.argv.includes("--fast");
|
||||
const command = fast ? "build:firefox52:fast" : "build:firefox52";
|
||||
const reportDir = path.join(rootDir, ".runtime", "firefox52-benchmark");
|
||||
|
||||
function removeWorkspaceDirectory(relativePath) {
|
||||
const target = path.resolve(rootDir, relativePath);
|
||||
if (path.dirname(target) !== rootDir) throw new Error(`拒绝清理工作区之外的目录:${target}`);
|
||||
fs.rmSync(target, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function runBuild() {
|
||||
return new Promise((resolve) => {
|
||||
const startedAt = performance.now();
|
||||
const executable = process.platform === "win32" ? (process.env.ComSpec || "cmd.exe") : "pnpm";
|
||||
const args = process.platform === "win32" ? ["/d", "/s", "/c", `pnpm ${command}`] : [command];
|
||||
const child = spawn(executable, args, {
|
||||
cwd: rootDir,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
shell: false,
|
||||
});
|
||||
child.on("exit", (code, signal) => {
|
||||
resolve({
|
||||
durationMs: Math.round(performance.now() - startedAt),
|
||||
exitCode: code,
|
||||
signal,
|
||||
});
|
||||
});
|
||||
child.on("error", (error) => resolve({ durationMs: Math.round(performance.now() - startedAt), exitCode: -1, error: String(error) }));
|
||||
});
|
||||
}
|
||||
|
||||
function percentile(values, ratio) {
|
||||
if (values.length === 0) return null;
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.ceil(ratio * sorted.length) - 1];
|
||||
}
|
||||
|
||||
function artifactStats() {
|
||||
const assetsDir = path.join(rootDir, "dist-firefox52", "assets");
|
||||
const stats = { jsFiles: 0, jsBytes: 0, cssFiles: 0, cssBytes: 0 };
|
||||
if (!fs.existsSync(assetsDir)) return stats;
|
||||
for (const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue;
|
||||
const size = fs.statSync(path.join(assetsDir, entry.name)).size;
|
||||
if (/\.js$/i.test(entry.name)) {
|
||||
stats.jsFiles += 1;
|
||||
stats.jsBytes += size;
|
||||
} else if (/\.css$/i.test(entry.name)) {
|
||||
stats.cssFiles += 1;
|
||||
stats.cssBytes += size;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
if (cold) {
|
||||
const productionCache = path.resolve(rootDir, ".rspack-cache", "firefox52-production");
|
||||
if (!productionCache.startsWith(`${path.resolve(rootDir, ".rspack-cache")}${path.sep}`)) {
|
||||
throw new Error(`拒绝清理预期缓存目录之外的路径:${productionCache}`);
|
||||
}
|
||||
fs.rmSync(productionCache, { recursive: true, force: true });
|
||||
removeWorkspaceDirectory("dist-firefox52");
|
||||
}
|
||||
|
||||
const samples = [];
|
||||
for (let index = 0; index < rounds; index += 1) {
|
||||
console.log(`\n[Firefox 52 benchmark] ${index + 1}/${rounds} ${command}`);
|
||||
const sample = await runBuild();
|
||||
samples.push(sample);
|
||||
if (sample.exitCode !== 0) break;
|
||||
}
|
||||
|
||||
const successfulDurations = samples.filter((item) => item.exitCode === 0).map((item) => item.durationMs);
|
||||
const steadyDurations = successfulDurations.slice(1);
|
||||
const now = new Date();
|
||||
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
||||
const report = {
|
||||
generatedAt: now.toISOString(),
|
||||
command: `pnpm ${command}`,
|
||||
coldCacheBeforeFirstRun: cold,
|
||||
requestedRounds: rounds,
|
||||
environment: {
|
||||
platform: `${os.platform()} ${os.release()} ${os.arch()}`,
|
||||
cpu: os.cpus()[0]?.model || "unknown",
|
||||
logicalCpus: os.cpus().length,
|
||||
totalMemoryBytes: os.totalmem(),
|
||||
node: process.version,
|
||||
pnpm: process.env.npm_config_user_agent || "run via pnpm.cmd",
|
||||
},
|
||||
samples,
|
||||
summary: {
|
||||
successfulRuns: successfulDurations.length,
|
||||
medianMs: percentile(successfulDurations, 0.5),
|
||||
p95Ms: percentile(successfulDurations, 0.95),
|
||||
minMs: successfulDurations.length ? Math.min(...successfulDurations) : null,
|
||||
maxMs: successfulDurations.length ? Math.max(...successfulDurations) : null,
|
||||
steadyStateMedianMs: percentile(steadyDurations, 0.5),
|
||||
steadyStateP95Ms: percentile(steadyDurations, 0.95),
|
||||
},
|
||||
artifacts: artifactStats(),
|
||||
};
|
||||
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
const jsonPath = path.join(reportDir, `${stamp}.json`);
|
||||
const markdownPath = path.join(reportDir, `${stamp}.md`);
|
||||
fs.writeFileSync(jsonPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
fs.writeFileSync(markdownPath, `# Firefox 52 Rspack 构建基准\n\n- 时间:${report.generatedAt}\n- 命令:\`${report.command}\`\n- 轮次:${samples.length}\n- 全部中位数:${report.summary.medianMs ?? "-"} ms\n- 全部 P95:${report.summary.p95Ms ?? "-"} ms\n- 首轮后的稳态中位数:${report.summary.steadyStateMedianMs ?? "-"} ms\n- 首轮后的稳态 P95:${report.summary.steadyStateP95Ms ?? "-"} ms\n- JS:${report.artifacts.jsFiles} 个 / ${report.artifacts.jsBytes} bytes\n- CSS:${report.artifacts.cssFiles} 个 / ${report.artifacts.cssBytes} bytes\n\n| 轮次 | 耗时(ms) | 退出码 |\n|---:|---:|---:|\n${samples.map((sample, index) => `| ${index + 1} | ${sample.durationMs} | ${sample.exitCode} |`).join("\n")}\n`);
|
||||
|
||||
console.log(`\nFirefox 52 benchmark median=${report.summary.medianMs}ms p95=${report.summary.p95Ms}ms steadyMedian=${report.summary.steadyStateMedianMs}ms`);
|
||||
console.log(`JSON: ${jsonPath}`);
|
||||
console.log(`Markdown: ${markdownPath}`);
|
||||
if (successfulDurations.length !== rounds) process.exitCode = 1;
|
||||
@@ -28,8 +28,8 @@ const requiredLegacyFallbacks = [
|
||||
},
|
||||
{
|
||||
file: "src/components/ai-elements/prompt-input.tsx",
|
||||
pattern: /<InputGroup className="h-auto min-h-16 flex-col items-stretch">/,
|
||||
description: "Prompt textarea must not rely on :has() for its layout",
|
||||
pattern: /<InputGroup className="h-auto min-h-16">/,
|
||||
description: "Prompt textarea must not rely on :has() for its parent height",
|
||||
},
|
||||
{
|
||||
file: "src/core/utils/mermaid.ts",
|
||||
@@ -41,68 +41,6 @@ const requiredLegacyFallbacks = [
|
||||
pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/,
|
||||
description: "Generated HTML charts must downlevel modern CSS colors for Chrome 109",
|
||||
},
|
||||
{
|
||||
file: "src/pages/ChatPage.tsx",
|
||||
pattern: /chat-landing-composer-offset[\s\S]*chat-landing-content-lift/,
|
||||
description: "Chat landing layout must not rely on Tailwind's individual translate property",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern: /\.chat-landing-composer-offset\s*\{\s*position:\s*relative;\s*top:\s*calc\(96px\s*-\s*50vh\);\s*\}/,
|
||||
description: "Chat landing composer must offset with top, not Tailwind translate",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern:
|
||||
/\.legacy-dialog-viewport\s*\{[\s\S]*?display:\s*flex;[\s\S]*?align-items:\s*center;[\s\S]*?justify-content:\s*center;/,
|
||||
description: "Dialog content must be flex-centered without Tailwind translate",
|
||||
},
|
||||
{
|
||||
file: "src/components/ui/dialog.tsx",
|
||||
pattern: /legacy-dialog-viewport[\s\S]*relative grid/,
|
||||
description: "Dialog panel must not use fixed + translate centering",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern:
|
||||
/\.legacy-dialog-viewport\s*>\s*\[data-slot="dialog-content"\]\s*\{[\s\S]*?min-height:\s*0;[\s\S]*?max-height:\s*100%;/,
|
||||
description: "Flex-centered dialogs must keep min-height:0 so inner scroll works",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern: /\.legacy-floating-layer\s*\{\s*z-index:\s*300;/,
|
||||
description: "Portaled select/dropdown layers must sit above dialog overlay z-index 200",
|
||||
},
|
||||
{
|
||||
file: "src/components/workspace/skills/skill-file-preview-dialog.tsx",
|
||||
pattern: /minHeight:[\s\S]*100vh[\s\S]*maxHeight:[\s\S]*100vh/,
|
||||
description: "Skill file preview must size with vh, not dvh, for Chrome 102",
|
||||
},
|
||||
{
|
||||
file: "src/core/compat/abort-signal.ts",
|
||||
pattern: /export function installAbortSignalTimeout[\s\S]*AbortSignal/,
|
||||
description: "LangGraph SDK timeoutMs needs AbortSignal.timeout for Chrome 102",
|
||||
},
|
||||
{
|
||||
file: "src/core/api/api-client.ts",
|
||||
pattern: /function installLegacyControlTimeout[\s\S]*AbortSignal\.timeout/,
|
||||
description: "Chat send must skip AbortSignal.timeout when Chrome 102 lacks it",
|
||||
},
|
||||
{
|
||||
file: "index.html",
|
||||
pattern: /AbortSignal\.timeout = function/,
|
||||
description: "Chrome 102 needs AbortSignal.timeout before any module loads",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern: /\.workspace-chat-bg,\s*\n\s*\.canvas-writing-bg\s*\{\s*background-color:\s*var\(--background\);/,
|
||||
description: "Agent chat canvas needs an explicit token background for Chrome 102",
|
||||
},
|
||||
{
|
||||
file: "src/styles/globals.css",
|
||||
pattern: /@supports not \(translate: 0\)[\s\S]*\.dialog-content-center\s*\{\s*transform:\s*translate\(-50%,\s*-50%\);/,
|
||||
description: "Dialog centering needs a transform fallback for Chrome 102",
|
||||
},
|
||||
];
|
||||
|
||||
for (const file of cssFiles) {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import postcss from "postcss";
|
||||
|
||||
/**
|
||||
* Firefox 52 CSS 产物守卫:构建后运行,防止回归。
|
||||
* 迁移后的结构性保证(见 tailwind.config.ts / postcss.config.js):
|
||||
* - Tailwind v3 输出无 @layer(Chrome 99+)与 @property(Chrome 85+);
|
||||
* - dark: 变体经 postcss-dark-descendant 改写为 .dark 后代形式;
|
||||
* - :where() 由 Firefox 专用 PostCSS 插件展开;
|
||||
* - 媒体查询 range 语法(Chrome 104+)由 lightningcss 按 target 转译;
|
||||
* - 手写 CSS 不使用 :is()(复杂参数无法安全降级,源头展开为逗号选择器);
|
||||
* - color-mix()/oklch() 仅允许出现在 @supports 守护内或带前置回退声明。
|
||||
* :has() 按渐进增强放行,关键输入框布局由显式 class 和 :focus-within 兜底。
|
||||
*/
|
||||
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
|
||||
const assetsDir = path.resolve("dist-firefox52/assets");
|
||||
|
||||
if (!fs.existsSync(assetsDir)) {
|
||||
throw new Error("dist-firefox52/assets 不存在,请先运行 pnpm build:firefox52:fast");
|
||||
}
|
||||
|
||||
const cssFiles = fs.readdirSync(assetsDir).filter((f) => f.endsWith(".css"));
|
||||
if (cssFiles.length === 0) throw new Error("dist-firefox52/assets 中没有 CSS 构建产物");
|
||||
|
||||
const failures = [];
|
||||
const hardTokens = [
|
||||
[/@layer\b/, "@layer(Chrome 99+)"],
|
||||
[/@property\b/, "@property(Chrome 85+)"],
|
||||
[/:is\(/, ":is()(Chrome 88+)"],
|
||||
[/:where\(/, ":where()(Firefox 78+)"],
|
||||
[/\(width\s*>=|\(width\s*<=|\(height\s*>=/, "媒体查询 range 语法(Chrome 104+)"],
|
||||
[/rgb\(\s*var\(/i, "rgb(var(...)) 现代通道语法"],
|
||||
[/rgba\([^;{}]*,\s*calc\(/i, "rgba() 的 calc alpha(Firefox 52 不解析)"],
|
||||
[/rgba\([^;{}]*,\s*[\d.]+%\s*\)/i, "rgba() 百分比 alpha(Firefox 52 不解析)"],
|
||||
];
|
||||
let guardedColors = 0;
|
||||
let progressiveHas = 0;
|
||||
const requiredComposerWidths = new Map([
|
||||
[".max-w-\\[var\\(--container-width-md\\)\\]", "816px"],
|
||||
[".max-w-\\[calc\\(var\\(--container-width-md\\)\\+150px\\)\\]", "966px"],
|
||||
[".max-w-\\[var\\(--container-width-lg\\)\\]", "1024px"],
|
||||
]);
|
||||
const foundComposerWidths = new Map();
|
||||
|
||||
for (const file of cssFiles) {
|
||||
const raw = fs.readFileSync(path.join(assetsDir, file), "utf8");
|
||||
for (const [re, label] of hardTokens) {
|
||||
const hits = raw.match(new RegExp(re.source, "g")) || [];
|
||||
if (hits.length > 0) failures.push(`${file}: ${hits.length} 处 ${label}`);
|
||||
}
|
||||
progressiveHas += (raw.match(/:has\(/g) || []).length;
|
||||
|
||||
const root = postcss.parse(raw, { from: file });
|
||||
root.walkRules((rule) => {
|
||||
for (const [selector, expectedWidth] of requiredComposerWidths) {
|
||||
if (!rule.selector?.includes(selector)) continue;
|
||||
const width = rule.nodes?.find((node) => node.type === "decl" && node.prop === "max-width");
|
||||
if (width?.value === expectedWidth) foundComposerWidths.set(selector, true);
|
||||
}
|
||||
});
|
||||
root.walkDecls((decl) => {
|
||||
if (!unsupportedColor.test(decl.value)) return;
|
||||
guardedColors += 1;
|
||||
let anc = decl.parent;
|
||||
while (anc) {
|
||||
if (anc.type === "atrule" && anc.name === "supports" && unsupportedColor.test(anc.params)) return;
|
||||
anc = anc.parent;
|
||||
}
|
||||
let prev = decl.prev();
|
||||
while (prev?.type === "comment") prev = prev.prev();
|
||||
const hasFallback =
|
||||
!decl.prop.startsWith("--") &&
|
||||
prev?.type === "decl" &&
|
||||
prev.prop === decl.prop &&
|
||||
!unsupportedColor.test(prev.value);
|
||||
if (!hasFallback) {
|
||||
failures.push(`${file}: 未守护的现代颜色 ${decl.prop}: ${decl.value.slice(0, 60)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const selector of requiredComposerWidths.keys()) {
|
||||
if (!foundComposerWidths.has(selector)) {
|
||||
failures.push(`缺少 Firefox 52 输入区宽度回退:${selector}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("Firefox 52 CSS 兼容检查失败:\n");
|
||||
console.error(failures.slice(0, 50).join("\n"));
|
||||
if (failures.length > 50) console.error(`... 及另外 ${failures.length - 50} 处`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Firefox 52 CSS 兼容检查通过(${guardedColors} 处守护的现代颜色声明,${progressiveHas} 处渐进式 :has())。`,
|
||||
);
|
||||
@@ -1,48 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { parse } from "acorn";
|
||||
|
||||
const assetsDir = path.resolve("dist-firefox52/assets");
|
||||
if (!fs.existsSync(assetsDir)) {
|
||||
throw new Error("dist-firefox52/assets 不存在,请先运行 pnpm build:firefox52");
|
||||
}
|
||||
|
||||
const jsFiles = fs.readdirSync(assetsDir).filter((file) => file.endsWith(".js"));
|
||||
if (jsFiles.length === 0) {
|
||||
throw new Error("dist-firefox52/assets 中没有 JavaScript 构建产物");
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
let totalBytes = 0;
|
||||
for (const file of jsFiles) {
|
||||
const source = fs.readFileSync(path.join(assetsDir, file), "utf8");
|
||||
totalBytes += Buffer.byteLength(source);
|
||||
try {
|
||||
// Firefox 52 implements ES2017 parsing, but not later syntax such as
|
||||
// optional chaining, BigInt literals, object rest, or dynamic import.
|
||||
parse(source, {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: "script",
|
||||
allowHashBang: false,
|
||||
});
|
||||
} catch (error) {
|
||||
const location = error.loc ? `${error.loc.line}:${error.loc.column}` : "unknown";
|
||||
failures.push(`${file}:${location} ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("Firefox 52 JavaScript 语法检查失败:\n");
|
||||
console.error(failures.slice(0, 50).join("\n"));
|
||||
if (failures.length > 50) console.error(`... 及另外 ${failures.length - 50} 处`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Firefox 52 JavaScript 语法检查通过(${jsFiles.length} 个文件,${(
|
||||
totalBytes /
|
||||
1024 /
|
||||
1024
|
||||
).toFixed(1)} MiB)。`,
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const { build } = await import(pathToFileURL(require.resolve("esbuild", {
|
||||
paths: [dirname(require.resolve("vite"))],
|
||||
})).href);
|
||||
const output = join(root, ".runtime", "ui-boundary-check");
|
||||
mkdirSync(output, { recursive: true });
|
||||
await build({
|
||||
absWorkingDir: root,
|
||||
entryPoints: ["src/components/react-ui-error-boundary.browser-check.tsx"],
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
format: "iife",
|
||||
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
||||
outfile: join(output, "check.js"),
|
||||
});
|
||||
const browser = process.env.CHROME_PATH || (process.platform === "win32"
|
||||
? "C:/Program Files/Google/Chrome/Application/chrome.exe"
|
||||
: "/usr/bin/chromium");
|
||||
const response = spawnSync(browser, [
|
||||
"--headless=new", "--disable-gpu", "--disable-background-networking", "--no-first-run",
|
||||
`--user-data-dir=${mkdtempSync(join(output, "browser-profile-"))}`,
|
||||
"--virtual-time-budget=3000", "--dump-dom",
|
||||
pathToFileURL(join(root, "scripts", "react-ui-error-boundary-check.html")).href,
|
||||
], { encoding: "utf8", windowsHide: true, timeout: 30_000 });
|
||||
if (response.error) throw response.error;
|
||||
const payload = response.stdout?.match(/<pre id="boundary-check-result">([^<]+)<\/pre>/)?.[1];
|
||||
if (!payload || response.status !== 0) {
|
||||
throw new Error(`Browser check did not finish: exit=${response.status}; ${response.stderr?.slice(-500)}`);
|
||||
}
|
||||
const result = JSON.parse(payload);
|
||||
if (result.passed !== true) throw new Error(`Browser check failed: ${payload}`);
|
||||
console.log("React production boundary browser check:", result);
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 把 Tailwind v3.4 class 模式暗色变体输出的 `X:is(.dark *)` 机械改写为
|
||||
* `.dark X` 后代选择器,供麒麟 Chromium ~80 使用(无 :is())。
|
||||
*
|
||||
* - 特异性不变:`:is(.dark *)` 取最高参数 = (0,1,0),剥离后前置 `.dark `
|
||||
* 同样 +(0,1,0),两种形式均为 (0,2,0),级联行为一致。
|
||||
* - `.dark` 挂在 <html> 上、是全局祖先,前置到组合器最左侧语义等价
|
||||
* (group-hover:dark:* 等组合下同样成立)。
|
||||
* - 堆叠变体(dark:marker:* 等)生成的 `X *:is(.dark *)::marker` 会被
|
||||
* 改写为 `.dark X *::marker`,保持合法。
|
||||
*/
|
||||
const TOKEN = ":is(.dark *)";
|
||||
|
||||
export default function darkDescendant() {
|
||||
return {
|
||||
postcssPlugin: "postcss-dark-descendant",
|
||||
Rule(rule) {
|
||||
if (!rule.selector.includes(TOKEN)) return;
|
||||
rule.selector = rule.selector
|
||||
.split(",")
|
||||
.map((sel) => {
|
||||
const trimmed = sel.trim();
|
||||
if (!trimmed.includes(TOKEN)) return trimmed;
|
||||
return ".dark " + trimmed.split(TOKEN).join("");
|
||||
})
|
||||
.join(", ");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
darkDescendant.postcss = true;
|
||||
@@ -1,164 +0,0 @@
|
||||
const LEGACY_RGB_NAME = /^--legacy-[a-z0-9-]+-rgb$/i;
|
||||
const RGB_NAME = /^--[a-z0-9-]+-rgb$/i;
|
||||
const RGB_CHANNELS = /^\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s*$/;
|
||||
const RGB_CHANNEL_REFERENCE = /^\s*var\(\s*(--[a-z0-9-]+-rgb)(?:\s*,\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?))?\s*\)\s*$/i;
|
||||
const INHERENT_ALPHA_TOKENS = new Set([
|
||||
"border",
|
||||
"input",
|
||||
"ring",
|
||||
"sidebar-border",
|
||||
]);
|
||||
const ALPHA_MULTIPLIERS = [0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1];
|
||||
|
||||
function alphaVariableSuffix(multiplier) {
|
||||
return String(Math.round(multiplier * 1000));
|
||||
}
|
||||
|
||||
function formatAlpha(value) {
|
||||
return String(Math.round(value * 1_000_000) / 1_000_000);
|
||||
}
|
||||
|
||||
function legacyAlphaToken(variableName) {
|
||||
const match = variableName.match(/^--legacy-(.+)-rgb$/);
|
||||
return match && INHERENT_ALPHA_TOKENS.has(match[1]) ? match[1] : null;
|
||||
}
|
||||
|
||||
function normalizeAlpha(variableName, alpha) {
|
||||
const percentage = alpha.match(/^(\d+(?:\.\d+)?)%$/);
|
||||
if (percentage) return String(Number(percentage[1]) / 100);
|
||||
|
||||
const token = legacyAlphaToken(variableName);
|
||||
if (!token) return alpha;
|
||||
const composite = alpha.match(
|
||||
new RegExp(
|
||||
`^calc\\(var\\(--${token}-alpha,\\s*1\\)\\s*\\*\\s*(.+)\\)$`,
|
||||
),
|
||||
);
|
||||
if (!composite) return alpha;
|
||||
|
||||
const multiplier = composite[1].trim();
|
||||
const numeric = Number(multiplier);
|
||||
if (Number.isFinite(numeric)) {
|
||||
return `var(--ff52-${token}-alpha-${alphaVariableSuffix(numeric)})`;
|
||||
}
|
||||
if (/^var\(--tw-[a-z-]+-opacity,\s*1\)$/.test(multiplier)) {
|
||||
return `var(--ff52-${token}-alpha-${alphaVariableSuffix(1)})`;
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
function findClosingParenthesis(value, openingIndex) {
|
||||
let depth = 0;
|
||||
for (let index = openingIndex; index < value.length; index += 1) {
|
||||
if (value[index] === "(") depth += 1;
|
||||
if (value[index] !== ")") continue;
|
||||
depth -= 1;
|
||||
if (depth === 0) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function commaChannelValue(value) {
|
||||
const channels = value.match(RGB_CHANNELS);
|
||||
if (channels) return `${channels[1]}, ${channels[2]}, ${channels[3]}`;
|
||||
|
||||
const reference = value.match(RGB_CHANNEL_REFERENCE);
|
||||
if (!reference) return null;
|
||||
const fallback = reference[2]
|
||||
? `, ${reference[2]}, ${reference[3]}, ${reference[4]}`
|
||||
: "";
|
||||
return `var(${reference[1]}-comma${fallback})`;
|
||||
}
|
||||
|
||||
function rewriteLegacyRgbFunctions(
|
||||
value,
|
||||
convertibleNames = { has: (name) => LEGACY_RGB_NAME.test(name) },
|
||||
) {
|
||||
let output = "";
|
||||
let cursor = 0;
|
||||
|
||||
while (cursor < value.length) {
|
||||
const start = value.indexOf("rgb(var(--", cursor);
|
||||
if (start < 0) return output + value.slice(cursor);
|
||||
output += value.slice(cursor, start);
|
||||
|
||||
const varStart = start + 4;
|
||||
const varEnd = findClosingParenthesis(value, varStart + 3);
|
||||
const rgbEnd = findClosingParenthesis(value, start + 3);
|
||||
if (varEnd < 0 || rgbEnd < 0 || varEnd >= rgbEnd) {
|
||||
output += value.slice(start, start + 4);
|
||||
cursor = start + 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
const variableName = value.slice(varStart + 4, varEnd).trim();
|
||||
const separator = value.slice(varEnd + 1, rgbEnd);
|
||||
const slash = separator.indexOf("/");
|
||||
if (!RGB_NAME.test(variableName) || !convertibleNames.has(variableName) || slash < 0) {
|
||||
output += value.slice(start, rgbEnd + 1);
|
||||
cursor = rgbEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const alpha = normalizeAlpha(
|
||||
variableName,
|
||||
separator.slice(slash + 1).trim(),
|
||||
);
|
||||
output += `rgba(var(${variableName}-comma), ${alpha})`;
|
||||
cursor = rgbEnd + 1;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Firefox 52 cannot parse CSS Color 4's `rgb(var(--channels) / alpha)` form.
|
||||
* Keep the space-separated live channels for modern builds, add an equivalent
|
||||
* comma-separated variable, and rewrite only the Firefox artifact to rgba().
|
||||
*/
|
||||
export default function firefox52RgbVars() {
|
||||
const convertibleNames = new Set();
|
||||
return {
|
||||
postcssPlugin: "postcss-firefox52-rgb-vars",
|
||||
Once(root) {
|
||||
root.walkDecls((declaration) => {
|
||||
if (RGB_NAME.test(declaration.prop)) {
|
||||
const commaValue = commaChannelValue(declaration.value);
|
||||
if (commaValue) {
|
||||
convertibleNames.add(declaration.prop);
|
||||
declaration.cloneAfter({
|
||||
prop: `${declaration.prop}-comma`,
|
||||
value: commaValue,
|
||||
});
|
||||
|
||||
const token = legacyAlphaToken(declaration.prop);
|
||||
if (token) {
|
||||
const alphaDeclaration = declaration.parent?.nodes?.find(
|
||||
(node) =>
|
||||
node.type === "decl" && node.prop === `--${token}-alpha`,
|
||||
);
|
||||
const inherentAlpha = Number(alphaDeclaration?.value ?? 1);
|
||||
for (const multiplier of ALPHA_MULTIPLIERS) {
|
||||
declaration.cloneAfter({
|
||||
prop: `--ff52-${token}-alpha-${alphaVariableSuffix(multiplier)}`,
|
||||
value: formatAlpha(inherentAlpha * multiplier),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
Declaration(declaration) {
|
||||
if (!declaration.value.includes("rgb(var(--")) return;
|
||||
declaration.value = rewriteLegacyRgbFunctions(
|
||||
declaration.value,
|
||||
convertibleNames,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
firefox52RgbVars.postcss = true;
|
||||
|
||||
export { rewriteLegacyRgbFunctions };
|
||||
@@ -1,47 +0,0 @@
|
||||
import selectorParser from "postcss-selector-parser";
|
||||
|
||||
function firstWhere(selector) {
|
||||
let found;
|
||||
selector.walkPseudos((pseudo) => {
|
||||
if (!found && pseudo.value.toLowerCase() === ":where") found = pseudo;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
function expandWhere(selector) {
|
||||
const pseudo = firstWhere(selector);
|
||||
if (!pseudo) return [selector];
|
||||
const alternatives = pseudo.nodes ?? [];
|
||||
if (alternatives.length === 0) {
|
||||
pseudo.remove();
|
||||
return expandWhere(selector);
|
||||
}
|
||||
|
||||
return alternatives.flatMap((alternative) => {
|
||||
const clone = selector.clone();
|
||||
const clonePseudo = firstWhere(clone);
|
||||
if (!clonePseudo) return [clone];
|
||||
clonePseudo.replaceWith(...alternative.nodes.map((node) => node.clone()));
|
||||
return expandWhere(clone);
|
||||
});
|
||||
}
|
||||
|
||||
/** Expand :where() only in the Firefox 52 build, where the selector is invalid. */
|
||||
export default function firefox52Where() {
|
||||
return {
|
||||
postcssPlugin: "postcss-firefox52-where",
|
||||
Rule(rule) {
|
||||
if (!rule.selector.includes(":where(")) return;
|
||||
rule.selector = selectorParser((root) => {
|
||||
const selectors = [...root.nodes];
|
||||
root.removeAll();
|
||||
for (const selector of selectors) {
|
||||
for (const expanded of expandWhere(selector)) root.append(expanded);
|
||||
}
|
||||
}).processSync(rule.selector);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
firefox52Where.postcss = true;
|
||||
|
||||
@@ -2,12 +2,10 @@ import postcss from "postcss";
|
||||
import { transform } from "lightningcss";
|
||||
|
||||
const LEGACY_CHROME = 100 << 16;
|
||||
const LEGACY_FIREFOX = 52 << 16;
|
||||
|
||||
/**
|
||||
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
|
||||
* resulting CSS to the supported legacy browser syntax for both dev and
|
||||
* production. Cascade layers are flattened by the preceding PostCSS plugin.
|
||||
* resulting CSS to Chrome 100-compatible syntax for both dev and production.
|
||||
*/
|
||||
export default function lightningCssLegacyChrome() {
|
||||
return {
|
||||
@@ -17,7 +15,7 @@ export default function lightningCssLegacyChrome() {
|
||||
const output = transform({
|
||||
filename,
|
||||
code: Buffer.from(root.toString()),
|
||||
targets: { chrome: LEGACY_CHROME, firefox: LEGACY_FIREFOX },
|
||||
targets: { chrome: LEGACY_CHROME },
|
||||
minify: false,
|
||||
});
|
||||
const compiled = postcss.parse(output.code.toString(), { from: filename });
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><title>Offline React UI boundary check</title></head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../.runtime/ui-boundary-check/check.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,48 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const backendUrl = (process.env.FIREFOX_BACKEND_URL || "http://localhost:8001").replace(/\/$/, "");
|
||||
const username = process.env.FIREFOX_TEST_USERNAME || "admin";
|
||||
const password = process.env.FIREFOX_TEST_PASSWORD || "";
|
||||
|
||||
async function selectAgentId() {
|
||||
if (process.env.FIREFOX_TEST_AGENT_ID) return process.env.FIREFOX_TEST_AGENT_ID;
|
||||
const loginUrl = new URL(`${backendUrl}/api/v1/auth/login/username`);
|
||||
if (password) loginUrl.searchParams.set("password", password);
|
||||
const loginResponse = await fetch(loginUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
if (!loginResponse.ok) throw new Error(`测试账号登录失败:HTTP ${loginResponse.status}`);
|
||||
const auth = await loginResponse.json();
|
||||
const selectorResponse = await fetch(`${backendUrl}/api/agents?admin=true`, {
|
||||
headers: { Authorization: `${auth.token_type || "Bearer"} ${auth.access_token}` },
|
||||
});
|
||||
if (!selectorResponse.ok) throw new Error(`读取智能体列表失败:HTTP ${selectorResponse.status}`);
|
||||
const payload = await selectorResponse.json();
|
||||
const agents = Array.isArray(payload.agents) ? payload.agents : [];
|
||||
const agent = agents.find((item) => item?.id && item?.model) || agents.find((item) => item?.id);
|
||||
if (!agent) throw new Error("智能体列表中没有可用于流式验收的智能体");
|
||||
return String(agent.id);
|
||||
}
|
||||
|
||||
const agentId = await selectAgentId();
|
||||
const executable = process.execPath;
|
||||
const child = spawn(executable, [path.join(rootDir, "scripts", "smoke-firefox52.mjs"), "--live-only"], {
|
||||
cwd: rootDir,
|
||||
env: {
|
||||
...process.env,
|
||||
FIREFOX_LIVE_CHAT_HASH: `#/page/workspace/agents/${encodeURIComponent(agentId)}/chats/new`,
|
||||
FIREFOX_LIVE_CHAT_NAME: `真实智能体流式问答(${agentId})`,
|
||||
},
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
const exitCode = await new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("exit", (code) => resolve(code ?? 1));
|
||||
});
|
||||
process.exitCode = exitCode;
|
||||
@@ -1,377 +0,0 @@
|
||||
import net from "node:net";
|
||||
|
||||
const debugPort = Number(process.env.FIREFOX_DEBUG_PORT || 6001);
|
||||
const baseUrl = process.env.FIREFOX_BASE_URL || "http://localhost:5174/";
|
||||
const backendUrl = (process.env.FIREFOX_BACKEND_URL || "http://localhost:8001").replace(/\/$/, "");
|
||||
const username = process.env.FIREFOX_TEST_USERNAME || "admin";
|
||||
const password = process.env.FIREFOX_TEST_PASSWORD || "";
|
||||
const routeTimeoutMs = Number(process.env.FIREFOX_ROUTE_TIMEOUT_MS || 30_000);
|
||||
const fullReload = process.env.FIREFOX_FULL_RELOAD === "1";
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Static routes that can be verified without creating or mutating business
|
||||
* records. Parameterised workflows need their own test data; this list
|
||||
* intentionally rejects unexpected auth redirects.
|
||||
*/
|
||||
const routes = [
|
||||
// Public and standalone shells.
|
||||
["登录页鉴权恢复", `#/login/${encodeURIComponent(username)}`, { redirect: "#/page/workspace/chats/new" }],
|
||||
["落地页", "#/landing"],
|
||||
["公开排行榜", "#/public/leaderboard"],
|
||||
["会商大屏", "#/roundtable-dashboard"],
|
||||
["Official DeerFlow", "#/official"],
|
||||
|
||||
// Workspace routes.
|
||||
["工作区首页", "#/page/workspace/chats/new"],
|
||||
["对话列表", "#/page/workspace/chats"],
|
||||
["项目入口", "#/page/workspace/projects", { redirect: "#/page/strategy/qa/projects" }],
|
||||
["资料库入口", "#/page/workspace/library"],
|
||||
["微信新对话", "#/page/workspace/wx/chats/new"],
|
||||
["微信智能体", "#/page/workspace/wx/agents"],
|
||||
["微信技能", "#/page/workspace/wx/skills"],
|
||||
["智能体列表", "#/page/workspace/agents"],
|
||||
["新建智能体", "#/page/workspace/agents/new"],
|
||||
["智能体协作网络", "#/page/workspace/agents/orbit"],
|
||||
["智能体广场", "#/page/workspace/agents/plaza"],
|
||||
["技能列表", "#/page/workspace/skills"],
|
||||
["技能进化", "#/page/workspace/skills/evolution"],
|
||||
["AI 写作", "#/page/workspace/ai-writing"],
|
||||
["深度研究", "#/page/workspace/deep-research"],
|
||||
["公文助手", "#/page/workspace/gongwen"],
|
||||
["新建报告结构", "#/page/workspace/deep-research/structures/new"],
|
||||
["企业研究", "#/page/workspace/enterprise-research"],
|
||||
["Notebook 列表", "#/page/workspace/studio/notebooks"],
|
||||
["多智能体规划", "#/page/workspace/roundtable/planning"],
|
||||
["8BF 会商", "#/page/workspace/roundtable/8bf"],
|
||||
["岗位会商", "#/page/workspace/roundtable/position"],
|
||||
["业务链编辑", "#/page/workspace/roundtable/chains"],
|
||||
["业务链列表", "#/page/workspace/business-chains"],
|
||||
["报告协作", "#/page/workspace/report-collaboration"],
|
||||
["派活链路预览", "#/page/workspace/roundtable/chain-preview"],
|
||||
["知识空间", "#/page/workspace/knowledge"],
|
||||
["知识图谱配置回退", "#/page/workspace/knowledge/graph", { redirect: "#/page/workspace/knowledge" }],
|
||||
["助手知识库配置回退", "#/page/workspace/knowledge/assistant", { redirect: "#/page/workspace/knowledge" }],
|
||||
["知识检索调试配置回退", "#/page/workspace/knowledge/retrieval-debug", { redirect: "#/page/workspace/knowledge" }],
|
||||
["Markdown 转 Word", "#/page/workspace/knowledge/markdown-editor"],
|
||||
["定时任务", "#/page/workspace/scheduled/tasks"],
|
||||
|
||||
// Workspace admin/settings surfaces.
|
||||
["用户管理中心", "#/page/workspace/admin"],
|
||||
["管理员定时任务", "#/page/workspace/admin/scheduled-tasks"],
|
||||
["管理员智能体", "#/page/workspace/admin/agents"],
|
||||
["管理员技能", "#/page/workspace/admin/skills"],
|
||||
["技能定时", "#/page/workspace/admin/skill-timing"],
|
||||
["模型交换", "#/page/workspace/admin/model-exchanges"],
|
||||
["用户排行榜管理", "#/page/workspace/admin/user-leaderboard"],
|
||||
["用户排行榜设置", "#/page/workspace/admin/user-leaderboard/settings"],
|
||||
["菜单管理", "#/page/workspace/admin/menu-management"],
|
||||
["固定问题", "#/page/workspace/admin/fixed-questions"],
|
||||
["标签管理", "#/page/workspace/admin/tags"],
|
||||
["插件管理", "#/page/workspace/admin/plugins"],
|
||||
["岗位管理", "#/page/workspace/admin/positions"],
|
||||
["岗位角色", "#/page/workspace/admin/position-roles"],
|
||||
["推荐问题", "#/page/workspace/settings/recommended-questions"],
|
||||
["提示词前缀", "#/page/workspace/settings/prompt-prefix"],
|
||||
["私有广场", "#/page/workspace/settings/private-square"],
|
||||
["模型管理", "#/page/workspace/admin/models"],
|
||||
["模型并发", "#/page/workspace/settings/model-concurrency"],
|
||||
["广场发布", "#/page/workspace/settings/publish-squares"],
|
||||
["技能压缩", "#/page/workspace/settings/skill-compression"],
|
||||
["LLM 指标", "#/page/workspace/settings/llm-metrics"],
|
||||
["Checkpoint 迁移", "#/page/workspace/settings/checkpoint-migration"],
|
||||
|
||||
// Strategy routes and legacy/full-layout entry points.
|
||||
["通用问答", "#/page/strategy/qa/general"],
|
||||
["分析项目", "#/page/strategy/qa/projects"],
|
||||
["方案项目", "#/page/strategy/qa/projects/solutions", { redirect: "#/page/strategy/qa/projects?type=solution" }],
|
||||
["问答项目", "#/page/strategy/qa/projects/groups", { redirect: "#/page/strategy/qa/projects?type=qa" }],
|
||||
["策略选择配置回退", "#/page/strategy/qa/strategy-selection", { redirect: "#/page/strategy/qa/projects" }],
|
||||
["专家问答", "#/page/strategy/qa/expert-qa"],
|
||||
["策略多智能体会商", "#/page/strategy/qa/multi-agent-roundtable"],
|
||||
["策略 8BF 会商", "#/page/strategy/qa/8bf-roundtable"],
|
||||
["策略岗位会商", "#/page/strategy/qa/position-roundtable"],
|
||||
["策略深度研究", "#/page/strategy/qa/deep-research"],
|
||||
["策略公文助手", "#/page/strategy/qa/gongwen"],
|
||||
["策略新建报告结构", "#/page/strategy/qa/deep-research/structures/new"],
|
||||
["策略企业研究", "#/page/strategy/qa/enterprise-research"],
|
||||
["智能体看板", "#/page/strategy/dashboard-agent"],
|
||||
["任务管理", "#/page/strategy/task-management"],
|
||||
["三情分析任务", "#/page/strategy/task-management/plan-task-sentiment"],
|
||||
["任务分析", "#/page/strategy/task-management/plan-task/plan-task-fx"],
|
||||
["行动分析", "#/page/strategy/task-management/plan-task/plan-task-action"],
|
||||
["任务三情会商", "#/page/strategy/task-management/plan-task-sentiment/roundtable"],
|
||||
["任务分析会商", "#/page/strategy/task-management/plan-task/plan-task-fx/roundtable"],
|
||||
["行动分析会商", "#/page/strategy/task-management/plan-task/plan-task-action/roundtable"],
|
||||
["网络行为体资源", "#/page/strategy/resource-management/resource-network-actor"],
|
||||
["知识库资源", "#/page/strategy/resource-management/resource-knowledge-base"],
|
||||
["评价指标资源", "#/page/strategy/resource-management/resource-evaluation-index"],
|
||||
["轻应用管理", "#/page/strategy/light-app/management"],
|
||||
["轻应用列表", "#/page/strategy/light-app/apps"],
|
||||
["轻应用新建笔记", "#/page/strategy/light-app/new-note"],
|
||||
["轻应用笔记", "#/page/strategy/light-app/notes"],
|
||||
["轻应用笔记问答", "#/page/strategy/light-app/note-qa"],
|
||||
["策略对话", "#/page/strategy/chats"],
|
||||
["策略智能体", "#/page/strategy/agents"],
|
||||
["策略智能体广场", "#/page/strategy/agents/plaza"],
|
||||
["策略新建智能体", "#/page/strategy/agents/new"],
|
||||
["策略技能", "#/page/strategy/skills"],
|
||||
["策略技能进化", "#/page/strategy/skills/evolution"],
|
||||
["业务链映射", "#/page/strategy/business-chain-mapping"],
|
||||
["策略业务链列表", "#/page/strategy/business-chains"],
|
||||
["策略业务链编辑", "#/page/strategy/roundtable/chains"],
|
||||
["策略定时任务", "#/page/strategy/scheduled/tasks"],
|
||||
["登录白名单", "#/page/strategy/admin/login-whitelist"],
|
||||
["会商诊断", "#/page/strategy/admin/roundtable-diagnostics"],
|
||||
["技能地址", "#/page/strategy/admin/skill-addresses"],
|
||||
["敏感词权限回退", "#/page/strategy/admin/sensitive-words", { redirect: "#/page/workspace/chats/new" }],
|
||||
["任务按钮", "#/page/strategy/admin/task-buttons"],
|
||||
["策略管理员定时任务", "#/page/strategy/admin/scheduled-tasks"],
|
||||
["策略排行榜", "#/page/strategy/user-leaderboard"],
|
||||
["策略用户活动", "#/page/strategy/admin/user-activity"],
|
||||
["策略标签", "#/page/strategy/admin/tags"],
|
||||
["策略岗位", "#/page/strategy/admin/positions"],
|
||||
|
||||
// Parallel canvas/notebook/roundtable route trees.
|
||||
["画布首页", "#/page/canvas"],
|
||||
["画布 AI 写作", "#/page/canvas/ai-writing"],
|
||||
["对话版 AI 写作", "#/page/canvas/ai-writing-chat"],
|
||||
["文章类型", "#/page/canvas/article-types"],
|
||||
["研究模板", "#/page/canvas/research-templates"],
|
||||
["新建研究模板", "#/page/canvas/research-templates/new"],
|
||||
["Notebook AI 写作", "#/page/notebook/ai-writing"],
|
||||
["独立会商", "#/page/roundtable/planning"],
|
||||
// Keep isolated/embed routes last because their sessionStorage contract is
|
||||
// intentionally sticky across in-app navigation.
|
||||
["iframe 新对话", "#/page/workspace/chats_iframe/new?embed=1&hideJump=1&theme=light", { redirect: "#/page/workspace/chats/new?embed=1&hideJump=1&theme=light&chatMode=full" }],
|
||||
];
|
||||
|
||||
const results = [];
|
||||
const browserErrors = [];
|
||||
const socket = net.createConnection({ host: "127.0.0.1", port: debugPort });
|
||||
let buffer = Buffer.alloc(0);
|
||||
let rootActor;
|
||||
let consoleActor;
|
||||
let pendingEvaluation;
|
||||
|
||||
function send(packet) {
|
||||
const body = Buffer.from(JSON.stringify(packet));
|
||||
socket.write(Buffer.concat([Buffer.from(`${body.length}:`), body]));
|
||||
}
|
||||
|
||||
function evaluate(expression) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingEvaluation = { expression, resolve, reject };
|
||||
send({ to: consoleActor, type: "evaluateJS", text: expression });
|
||||
});
|
||||
}
|
||||
|
||||
function shouldIgnoreConsoleError(message) {
|
||||
return (
|
||||
message.includes("validateDOMNesting") ||
|
||||
message.includes("init intent failed") ||
|
||||
message.includes("ResizeObserver loop") ||
|
||||
message === "[object Object]"
|
||||
);
|
||||
}
|
||||
|
||||
function formatConsoleArgument(argument) {
|
||||
if (argument == null || typeof argument !== "object") return String(argument);
|
||||
const preview = argument.preview;
|
||||
if (preview?.message) {
|
||||
return `${preview.name || argument.class || "Error"}: ${preview.message}${preview.stack ? `\n${preview.stack}` : ""}`;
|
||||
}
|
||||
if (argument.class === "Error" && argument.ownPropertyLength === 0) return "Error";
|
||||
if (typeof argument.description === "string") return argument.description;
|
||||
try {
|
||||
return JSON.stringify(argument);
|
||||
} catch {
|
||||
return String(argument);
|
||||
}
|
||||
}
|
||||
|
||||
function handle(packet) {
|
||||
if (packet.applicationType && !rootActor) {
|
||||
rootActor = packet.from;
|
||||
send({ to: rootActor, type: "listTabs" });
|
||||
return;
|
||||
}
|
||||
if (packet.tabs && !consoleActor) {
|
||||
consoleActor = packet.tabs[packet.selected].consoleActor;
|
||||
send({
|
||||
to: consoleActor,
|
||||
type: "startListeners",
|
||||
listeners: ["PageError", "ConsoleAPI"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
packet.type === "pageError" &&
|
||||
packet.pageError &&
|
||||
packet.pageError.category !== "CSS Parser" &&
|
||||
packet.pageError.warning !== true
|
||||
) {
|
||||
const pageError = packet.pageError;
|
||||
const errorText = String(pageError.errorMessage || pageError.message || "").trim();
|
||||
const usefulErrorText = /^Error:\s*$/.test(errorText) ? "" : errorText;
|
||||
// Firefox 52 emits a duplicate empty promise-rejection packet when an XHR
|
||||
// owned by the previous document is cancelled during reload. It has no
|
||||
// stack and no message; meaningful runtime failures carry one or both.
|
||||
if (!usefulErrorText && !pageError.stacktrace) return;
|
||||
browserErrors.push(
|
||||
`PAGE: ${usefulErrorText || JSON.stringify(pageError)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (packet.type === "consoleAPICall" && packet.message?.level === "error") {
|
||||
const message = (packet.message.arguments || [])
|
||||
.map(formatConsoleArgument)
|
||||
.join(" ") || "error";
|
||||
if (!shouldIgnoreConsoleError(message)) browserErrors.push(`CONSOLE: ${message}`);
|
||||
return;
|
||||
}
|
||||
if (pendingEvaluation && packet.input === pendingEvaluation.expression) {
|
||||
const pending = pendingEvaluation;
|
||||
pendingEvaluation = null;
|
||||
if (packet.exception) pending.reject(new Error(String(packet.exception)));
|
||||
else pending.resolve(packet.result);
|
||||
}
|
||||
}
|
||||
|
||||
function consume() {
|
||||
while (true) {
|
||||
const colon = buffer.indexOf(58);
|
||||
if (colon < 0) return;
|
||||
const length = Number(buffer.subarray(0, colon).toString());
|
||||
if (buffer.length < colon + 1 + length) return;
|
||||
const packet = JSON.parse(buffer.subarray(colon + 1, colon + 1 + length).toString());
|
||||
buffer = buffer.subarray(colon + 1 + length);
|
||||
handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
consume();
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.once("connect", resolve);
|
||||
socket.once("error", reject);
|
||||
});
|
||||
while (!consoleActor) await sleep(50);
|
||||
|
||||
async function waitFor(expression, timeoutMs = routeTimeoutMs) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
if (await evaluate(expression)) return true;
|
||||
} catch {
|
||||
// A full reload can invalidate the previous Firefox console actor for a
|
||||
// few milliseconds. Retry against the actor once it resumes.
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function installAdminSession() {
|
||||
const url = new URL(`${backendUrl}/api/v1/auth/login/username`);
|
||||
if (password) url.searchParams.set("password", password);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`测试账号登录失败:HTTP ${response.status}`);
|
||||
}
|
||||
const auth = await response.json();
|
||||
if (auth.system_role !== "admin") {
|
||||
throw new Error(`测试账号 ${username} 不是管理员,无法验证管理路由`);
|
||||
}
|
||||
await evaluate(`(function(){var auth=${JSON.stringify(JSON.stringify(auth))},info=${JSON.stringify(JSON.stringify({ userName: username }))},settings={};try{settings=JSON.parse(localStorage.getItem('deerflow.local-settings')||'{}')}catch(e){}settings.display=settings.display||{};settings.display.layoutMode='full';localStorage.setItem('deerflow.local-settings',JSON.stringify(settings));localStorage.setItem('deerflow.auth',auth);localStorage.setItem('userInfo',info);sessionStorage.setItem('deerflow.auth',auth);sessionStorage.setItem('userInfo',info);return true})()`);
|
||||
}
|
||||
|
||||
async function exerciseVisibleTabs() {
|
||||
const count = Number(await evaluate(`(function(){return [].slice.call(document.querySelectorAll('[role="tab"]')).filter(function(item){var rect=item.getBoundingClientRect(),style=getComputedStyle(item);return !item.disabled&&item.getAttribute('aria-disabled')!=='true'&&rect.width>0&&rect.height>0&&style.display!=='none'&&style.visibility!=='hidden'}).length})()`));
|
||||
let clicks = 0;
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const clicked = await evaluate(`(function(){var items=[].slice.call(document.querySelectorAll('[role="tab"]')).filter(function(item){var rect=item.getBoundingClientRect(),style=getComputedStyle(item);return !item.disabled&&item.getAttribute('aria-disabled')!=='true'&&rect.width>0&&rect.height>0&&style.display!=='none'&&style.visibility!=='hidden'}),item=items[${index}];if(!item)return false;item.click();return true})()`);
|
||||
if (!clicked) continue;
|
||||
clicks += 1;
|
||||
await sleep(120);
|
||||
const healthy = await evaluate(`(function(){var root=document.getElementById('root'),rect=root&&root.getBoundingClientRect(),text=document.body.innerText||'';return !!root&&rect.width>0&&rect.height>0&&!/something went wrong|ChunkLoadError|Loading chunk [^ ]+ failed|SyntaxError:|ReferenceError:|TypeError:/i.test(text)})()`);
|
||||
if (!healthy) return { pass: false, clicks, detail: `点击第 ${index + 1} 个标签页后进入错误边界` };
|
||||
}
|
||||
return { pass: true, clicks };
|
||||
}
|
||||
|
||||
async function navigate(hash, options = {}) {
|
||||
const expectedHash = options.redirect || hash;
|
||||
const url = new URL(hash, baseUrl).href;
|
||||
try {
|
||||
await evaluate(`(function(){sessionStorage.removeItem('embed.session');sessionStorage.removeItem('embed.session.nested');try{delete window.__deerflowTopChromeEmbed}catch(e){}var url=${JSON.stringify(url)},reload=${JSON.stringify(fullReload)};if(reload){if(location.href!==url)location.href=url;location.reload()}else if(location.href!==url){location.href=url}return true})()`);
|
||||
} catch {
|
||||
// Firefox destroys the old console evaluation context during a successful
|
||||
// full-page reload and occasionally reports that as an opaque exception.
|
||||
// The route/readiness assertion below is the source of truth.
|
||||
}
|
||||
await sleep(700);
|
||||
const ready = await waitFor(`(function(){var root=document.getElementById('root'),rect=root&&root.getBoundingClientRect(),hasSurface=document.body.innerText.trim().length>0||!!document.querySelector('iframe,canvas,svg,.cm-editor');return location.hash.indexOf(${JSON.stringify(expectedHash)})===0&&document.readyState==='complete'&&!!root&&rect.width>0&&rect.height>0&&root.innerHTML.length>100&&hasSurface})()`);
|
||||
if (!ready) {
|
||||
const state = await evaluate(`(function(){function role(store){try{var value=store.getItem('deerflow.auth');return value?JSON.parse(value).system_role:'none'}catch(e){return 'invalid'}}return location.hash+';local='+role(localStorage)+';session='+role(sessionStorage)+';embed='+sessionStorage.getItem('embed.session')})()`);
|
||||
return `页面未在 ${Math.round(routeTimeoutMs / 1000)} 秒内完成渲染(${state})`;
|
||||
}
|
||||
const healthy = await evaluate(`(function(){var text=document.body.innerText||'';return !/something went wrong|ChunkLoadError|Loading chunk [^ ]+ failed|SyntaxError:|ReferenceError:|TypeError:/i.test(text)})()`);
|
||||
return healthy === true ? true : "页面进入错误边界";
|
||||
}
|
||||
|
||||
try {
|
||||
await installAdminSession();
|
||||
const routeFilter = process.env.FIREFOX_ROUTE_FILTER
|
||||
? new RegExp(process.env.FIREFOX_ROUTE_FILTER, "i")
|
||||
: null;
|
||||
const selectedRoutes = routeFilter
|
||||
? routes.filter(([name, hash]) => routeFilter.test(`${name} ${hash}`))
|
||||
: routes;
|
||||
if (selectedRoutes.length === 0) {
|
||||
throw new Error("FIREFOX_ROUTE_FILTER 未匹配到路由");
|
||||
}
|
||||
for (const [name, hash, options = {}] of selectedRoutes) {
|
||||
// Let late callbacks from the previous page settle before assigning
|
||||
// errors to the next full-page navigation.
|
||||
await sleep(350);
|
||||
const errorStart = browserErrors.length;
|
||||
let detail;
|
||||
try {
|
||||
detail = await navigate(hash, options);
|
||||
if (detail === true) {
|
||||
const tabResult = await exerciseVisibleTabs();
|
||||
if (!tabResult.pass) detail = tabResult.detail;
|
||||
else results.tabClicks = (results.tabClicks || 0) + tabResult.clicks;
|
||||
}
|
||||
} catch (error) {
|
||||
detail = String(error);
|
||||
}
|
||||
// Allow asynchronous render errors to arrive before assigning them to the
|
||||
// next route. Network/API failures are displayed by the page and do not
|
||||
// become PageError events; uncaught runtime errors still fail the route.
|
||||
await sleep(150);
|
||||
const errors = browserErrors
|
||||
.slice(errorStart)
|
||||
.filter((message) => !message.startsWith("CONSOLE: Warning:"));
|
||||
const pass = detail === true && errors.length === 0;
|
||||
results.push({ name, hash, pass, detail, errors });
|
||||
console.log(`${pass ? "PASS" : "FAIL"} ${name}${pass ? "" : ` — ${detail}`}`);
|
||||
for (const error of errors) console.log(` ${error}`);
|
||||
}
|
||||
} finally {
|
||||
socket.end();
|
||||
}
|
||||
|
||||
const failed = results.filter((result) => !result.pass);
|
||||
console.log(`\nFirefox 52 full route smoke: ${results.length - failed.length}/${results.length} passed`);
|
||||
console.log(`Non-mutating visible tab interactions: ${results.tabClicks || 0}`);
|
||||
console.log(`Navigation mode: ${fullReload ? "full reload" : "SPA"}`);
|
||||
if (failed.length > 0) process.exitCode = 1;
|
||||
@@ -1,163 +0,0 @@
|
||||
import net from "node:net";
|
||||
|
||||
const debugPort = Number(process.env.FIREFOX_DEBUG_PORT || 6001);
|
||||
const baseUrl = process.env.FIREFOX_BASE_URL || "http://localhost:5174/";
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const themes = ["light", "dark", "lightblue", "lightgreen", "iceblue", "lightyellow"];
|
||||
const layouts = ["full", "compact"];
|
||||
const results = [];
|
||||
const browserErrors = [];
|
||||
|
||||
const socket = net.createConnection({ host: "127.0.0.1", port: debugPort });
|
||||
let buffer = Buffer.alloc(0);
|
||||
let rootActor;
|
||||
let consoleActor;
|
||||
let pendingEvaluation;
|
||||
|
||||
function send(packet) {
|
||||
const body = Buffer.from(JSON.stringify(packet));
|
||||
socket.write(Buffer.concat([Buffer.from(`${body.length}:`), body]));
|
||||
}
|
||||
|
||||
function evaluate(expression) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingEvaluation = { expression, resolve, reject };
|
||||
send({ to: consoleActor, type: "evaluateJS", text: expression });
|
||||
});
|
||||
}
|
||||
|
||||
function formatArgument(argument) {
|
||||
if (argument == null || typeof argument !== "object") return String(argument);
|
||||
if (argument.preview?.message) return `${argument.preview.name || argument.class || "Error"}: ${argument.preview.message}`;
|
||||
return argument.description || argument.class || JSON.stringify(argument);
|
||||
}
|
||||
|
||||
function handle(packet) {
|
||||
if (packet.applicationType && !rootActor) {
|
||||
rootActor = packet.from;
|
||||
send({ to: rootActor, type: "listTabs" });
|
||||
return;
|
||||
}
|
||||
if (packet.tabs && !consoleActor) {
|
||||
consoleActor = packet.tabs[packet.selected].consoleActor;
|
||||
send({ to: consoleActor, type: "startListeners", listeners: ["PageError", "ConsoleAPI"] });
|
||||
return;
|
||||
}
|
||||
if (packet.type === "pageError" && packet.pageError && packet.pageError.warning !== true && packet.pageError.category !== "CSS Parser") {
|
||||
const message = String(packet.pageError.errorMessage || packet.pageError.message || "").trim();
|
||||
if (message || packet.pageError.stacktrace) browserErrors.push(`PAGE: ${message || "unknown page error"}`);
|
||||
return;
|
||||
}
|
||||
if (packet.type === "consoleAPICall" && packet.message?.level === "error") {
|
||||
const message = (packet.message.arguments || []).map(formatArgument).join(" ") || "error";
|
||||
if (!message.includes("validateDOMNesting") && !message.includes("init intent failed") && !message.includes("ResizeObserver loop") && message !== "[object Object]") {
|
||||
browserErrors.push(`CONSOLE: ${message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pendingEvaluation && packet.input === pendingEvaluation.expression) {
|
||||
const pending = pendingEvaluation;
|
||||
pendingEvaluation = null;
|
||||
if (packet.exception) pending.reject(new Error(packet.exceptionMessage || packet.exception?.description || String(packet.exception)));
|
||||
else pending.resolve(packet.result);
|
||||
}
|
||||
}
|
||||
|
||||
function consume() {
|
||||
while (true) {
|
||||
const colon = buffer.indexOf(58);
|
||||
if (colon < 0) return;
|
||||
const length = Number(buffer.subarray(0, colon).toString());
|
||||
if (buffer.length < colon + 1 + length) return;
|
||||
const packet = JSON.parse(buffer.subarray(colon + 1, colon + 1 + length).toString());
|
||||
buffer = buffer.subarray(colon + 1 + length);
|
||||
handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
consume();
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.once("connect", resolve);
|
||||
socket.once("error", reject);
|
||||
});
|
||||
while (!consoleActor) await sleep(50);
|
||||
|
||||
async function waitFor(expression, timeoutMs = 30_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
if (await evaluate(expression)) return true;
|
||||
} catch {
|
||||
// A reload briefly invalidates the old evaluation context.
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const layout of layouts) {
|
||||
for (const theme of themes) {
|
||||
const errorStart = browserErrors.length;
|
||||
let detail = true;
|
||||
try {
|
||||
const settings = JSON.stringify({
|
||||
notification: { enabled: true },
|
||||
display: {
|
||||
streamSpeedMeterEnabled: true,
|
||||
ragModeEnabled: false,
|
||||
artifactDirectoryModeEnabled: false,
|
||||
contextCompressionEnabled: false,
|
||||
citationCleanupWords: [],
|
||||
fontScale: 1,
|
||||
layoutMode: layout,
|
||||
},
|
||||
context: {},
|
||||
});
|
||||
const url = new URL("#/page/workspace/chats/new", baseUrl).href;
|
||||
try {
|
||||
await evaluate(`(function(){localStorage.setItem('strategy-theme',${JSON.stringify(theme)});localStorage.setItem('strategy-theme-user-set','1');localStorage.setItem('deerflow.local-settings',${JSON.stringify(settings)});sessionStorage.removeItem('embed.session');location.href=${JSON.stringify(url)};location.reload();return true})()`);
|
||||
} catch {
|
||||
// A successful full reload can destroy the old console request.
|
||||
}
|
||||
// The hash route is identical for every matrix item. Give Firefox time
|
||||
// to tear down the previous document before accepting a ready DOM.
|
||||
await sleep(700);
|
||||
const ready = await waitFor(`(function(){var root=document.getElementById('root'),textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]');return location.hash.indexOf('#/page/workspace/chats/new')===0&&document.readyState==='complete'&&!!root&&root.getBoundingClientRect().width>0&&!!textarea&&textarea.getBoundingClientRect().height>0})()`);
|
||||
if (!ready) {
|
||||
detail = "页面未在 30 秒内完成渲染";
|
||||
} else {
|
||||
const state = await evaluate(`(function(){var html=document.documentElement,root=document.getElementById('root'),textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]'),style=getComputedStyle(html),sidebar=document.querySelector('[data-sidebar="sidebar"],[data-compact-guide="sidebar-nav"]');return JSON.stringify({classes:html.className,dataTheme:html.getAttribute('data-theme')||'',themeMode:html.getAttribute('theme-mode')||'',storedTheme:localStorage.getItem('strategy-theme'),layout:JSON.parse(localStorage.getItem('deerflow.local-settings')||'{}').display.layoutMode,rootWidth:Math.round(root.getBoundingClientRect().width),inputWidth:Math.round(textarea.getBoundingClientRect().width),sidebarVisible:!!sidebar&&sidebar.getBoundingClientRect().width>0,background:style.getPropertyValue('--background').trim(),foreground:style.getPropertyValue('--foreground').trim(),primary:style.getPropertyValue('--primary').trim()})})()`);
|
||||
const parsed = JSON.parse(state);
|
||||
const themeApplied = parsed.storedTheme === theme && parsed.classes.split(/\s+/).includes(theme);
|
||||
const modeApplied = theme === "dark"
|
||||
? parsed.themeMode === "dark" && parsed.classes.split(/\s+/).includes("dark")
|
||||
: theme === "light"
|
||||
? parsed.dataTheme === "" && parsed.themeMode === ""
|
||||
: parsed.dataTheme === theme && parsed.themeMode === "";
|
||||
const tokensApplied = parsed.background && parsed.foreground && parsed.primary;
|
||||
const layoutApplied = parsed.layout === layout;
|
||||
const dimensionsHealthy = parsed.rootWidth > 0 && parsed.inputWidth > 200;
|
||||
if (!themeApplied || !modeApplied || !tokensApplied || !layoutApplied || !dimensionsHealthy) detail = state;
|
||||
}
|
||||
} catch (error) {
|
||||
detail = String(error);
|
||||
}
|
||||
await sleep(150);
|
||||
const errors = browserErrors.slice(errorStart);
|
||||
const pass = detail === true && errors.length === 0;
|
||||
results.push({ theme, layout, pass, detail, errors });
|
||||
console.log(`${pass ? "PASS" : "FAIL"} ${layout}/${theme}${pass ? "" : ` — ${detail}`}`);
|
||||
for (const error of errors) console.log(` ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const viewport = await evaluate(`JSON.stringify({innerWidth:window.innerWidth,innerHeight:window.innerHeight,devicePixelRatio:window.devicePixelRatio,screenWidth:screen.width,screenHeight:screen.height})`);
|
||||
socket.end();
|
||||
const failed = results.filter((result) => !result.pass);
|
||||
console.log(`\nFirefox 52 theme/layout smoke: ${results.length - failed.length}/${results.length} passed`);
|
||||
console.log(`Viewport: ${viewport}`);
|
||||
if (failed.length > 0) process.exitCode = 1;
|
||||
@@ -1,294 +0,0 @@
|
||||
import net from "node:net";
|
||||
|
||||
const debugPort = Number(process.env.FIREFOX_DEBUG_PORT || 6001);
|
||||
const baseUrl = process.env.FIREFOX_BASE_URL || "http://localhost:5174/";
|
||||
const liveOnly = process.argv.includes("--live-only");
|
||||
const liveMode = liveOnly || process.argv.includes("--live");
|
||||
const liveChatHash = process.env.FIREFOX_LIVE_CHAT_HASH || "#/page/workspace/chats/new";
|
||||
const liveChatName = process.env.FIREFOX_LIVE_CHAT_NAME || "真实流式问答";
|
||||
const liveThreadPrefix = liveChatHash.replace(/\/new(?:\?.*)?$/, "/");
|
||||
const smokeFilter = process.env.FIREFOX_SMOKE_FILTER
|
||||
? new RegExp(process.env.FIREFOX_SMOKE_FILTER, "i")
|
||||
: null;
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const routes = [
|
||||
["新对话", "#/page/workspace/chats/new", `!!document.querySelector('textarea[placeholder="今天我能为你做些什么?"]')`],
|
||||
["对话", "#/page/workspace/chats", `!!document.querySelector('input[type=search]')`],
|
||||
["我的项目", "#/page/strategy/qa/projects", `[].slice.call(document.querySelectorAll('button')).some(function(e){return e.innerText.trim()==='新建项目'})`],
|
||||
["AI 写作", "#/page/workspace/ai-writing", `location.hash.indexOf('#/page/workspace/ai-writing')===0&&!!document.querySelector('textarea[placeholder="今天我能为你做些什么?"]')`],
|
||||
["多智能体规划", "#/page/workspace/roundtable/planning", `[].slice.call(document.querySelectorAll('button')).some(function(e){return e.innerText.trim()==='业务链条'})`],
|
||||
["定时任务", "#/page/workspace/scheduled/tasks", `[].slice.call(document.querySelectorAll('button')).some(function(e){return e.innerText.trim()==='新建'})`],
|
||||
["md 转 Word", "#/page/workspace/knowledge/markdown-editor", `!!document.querySelector('.cm-editor')`],
|
||||
];
|
||||
|
||||
const results = [];
|
||||
const browserErrors = [];
|
||||
const socket = net.createConnection({ host: "127.0.0.1", port: debugPort });
|
||||
let buffer = Buffer.alloc(0);
|
||||
let rootActor;
|
||||
let consoleActor;
|
||||
let pendingEvaluation;
|
||||
|
||||
function send(packet) {
|
||||
const body = Buffer.from(JSON.stringify(packet));
|
||||
socket.write(Buffer.concat([Buffer.from(`${body.length}:`), body]));
|
||||
}
|
||||
|
||||
function evaluate(expression) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingEvaluation = { expression, resolve, reject };
|
||||
send({ to: consoleActor, type: "evaluateJS", text: expression });
|
||||
});
|
||||
}
|
||||
|
||||
function handle(packet) {
|
||||
if (packet.applicationType && !rootActor) {
|
||||
rootActor = packet.from;
|
||||
send({ to: rootActor, type: "listTabs" });
|
||||
return;
|
||||
}
|
||||
if (packet.tabs && !consoleActor) {
|
||||
consoleActor = packet.tabs[packet.selected].consoleActor;
|
||||
send({
|
||||
to: consoleActor,
|
||||
type: "startListeners",
|
||||
listeners: ["PageError", "ConsoleAPI"],
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
packet.type === "pageError" &&
|
||||
packet.pageError?.error &&
|
||||
packet.pageError.category !== "CSS Parser"
|
||||
) {
|
||||
browserErrors.push(`PAGE: ${packet.pageError.errorMessage}`);
|
||||
return;
|
||||
}
|
||||
if (packet.type === "consoleAPICall" && packet.message?.level === "error") {
|
||||
const message = String(packet.message.arguments?.[0] ?? "error");
|
||||
// Existing scheduled-task cards contain a nested action button. React's
|
||||
// development-only warning is unrelated to Firefox execution.
|
||||
if (
|
||||
!message.includes("validateDOMNesting") &&
|
||||
!message.includes("init intent failed") &&
|
||||
message !== "Unhandled promise rejection" &&
|
||||
message !== "[object Object]"
|
||||
) {
|
||||
browserErrors.push(`CONSOLE: ${message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pendingEvaluation && packet.input === pendingEvaluation.expression) {
|
||||
const pending = pendingEvaluation;
|
||||
pendingEvaluation = null;
|
||||
if (packet.exception) {
|
||||
const detail =
|
||||
packet.exceptionMessage ||
|
||||
packet.exception?.preview?.message ||
|
||||
packet.exception?.description ||
|
||||
packet.exception?.class ||
|
||||
String(packet.exception);
|
||||
pending.reject(new Error(String(detail)));
|
||||
}
|
||||
else pending.resolve(packet.result);
|
||||
}
|
||||
}
|
||||
|
||||
function consume() {
|
||||
while (true) {
|
||||
const colon = buffer.indexOf(58);
|
||||
if (colon < 0) return;
|
||||
const length = Number(buffer.subarray(0, colon).toString());
|
||||
if (buffer.length < colon + 1 + length) return;
|
||||
const packet = JSON.parse(buffer.subarray(colon + 1, colon + 1 + length).toString());
|
||||
buffer = buffer.subarray(colon + 1 + length);
|
||||
handle(packet);
|
||||
}
|
||||
}
|
||||
|
||||
socket.on("data", (chunk) => {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
consume();
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
socket.once("connect", resolve);
|
||||
socket.once("error", reject);
|
||||
});
|
||||
while (!consoleActor) await sleep(50);
|
||||
|
||||
async function waitFor(expression, timeoutMs = 20_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (await evaluate(expression)) return true;
|
||||
await sleep(250);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function navigate(hash, readyExpression = "true") {
|
||||
const url = new URL(hash, baseUrl).href;
|
||||
await evaluate(`(function(){sessionStorage.removeItem('embed.session');sessionStorage.removeItem('embed.session.nested');try{delete window.__deerflowTopChromeEmbed}catch(e){}var url=${JSON.stringify(url)};if(location.href===url)location.reload();else location.href=url;return 'navigate'})()`);
|
||||
// HashRouter keeps the previous route mounted for a short time after the URL
|
||||
// changes. Do not mistake that stale DOM for the destination being ready.
|
||||
await sleep(700);
|
||||
return waitFor(`(function(){var root=document.getElementById('root');return location.hash.indexOf(${JSON.stringify(hash)})===0&&document.readyState==='complete'&&!!root&&root.innerHTML.length>100&&document.body.innerText.length>20&&(${readyExpression})})()`);
|
||||
}
|
||||
|
||||
const mouseClick = `(function(element){if(!element)return false;['mousemove','mousedown','mouseup','click'].forEach(function(type){element.dispatchEvent(new MouseEvent(type,{bubbles:true,cancelable:true,view:window,button:0,buttons:type==='mousedown'?1:0}))});return true})`;
|
||||
const clickText = (text, prefix = false) =>
|
||||
`(function(){var text=${JSON.stringify(text)},items=[].slice.call(document.querySelectorAll('button,[role=menuitem],[role=tab]'));var element=items.filter(function(item){var value=(item.innerText||'').trim(),rect=item.getBoundingClientRect(),style=getComputedStyle(item);return rect.width>0&&rect.height>0&&style.display!=='none'&&style.visibility!=='hidden'&&${prefix ? "value.indexOf(text)===0" : "value===text"}})[0];return ${mouseClick}(element)})()`;
|
||||
const clickTitle = (title) =>
|
||||
`(function(){var title=${JSON.stringify(title)},element=[].slice.call(document.querySelectorAll('[title]')).filter(function(item){return item.getAttribute('title')===title})[0];return ${mouseClick}(element)})()`;
|
||||
const clickModelSelector = `(${mouseClick})([].slice.call(document.querySelectorAll('[data-firefox52-smoke="model-selector"]')).filter(function(item){var rect=item.getBoundingClientRect();return rect.width>0&&rect.height>0})[0])`;
|
||||
const visibleText = (selector, text) =>
|
||||
`(function(){var text=${JSON.stringify(text)};return [].slice.call(document.querySelectorAll(${JSON.stringify(selector)})).some(function(e){var r=e.getBoundingClientRect(),s=getComputedStyle(e);return r.width>0&&r.height>0&&s.display!=='none'&&s.visibility!=='hidden'&&(e.innerText||'').indexOf(text)>=0})})()`;
|
||||
|
||||
async function check(name, operation) {
|
||||
if (smokeFilter && !smokeFilter.test(name)) return;
|
||||
const errorStart = browserErrors.length;
|
||||
try {
|
||||
const detail = await operation();
|
||||
const errors = browserErrors.slice(errorStart);
|
||||
results.push({ name, pass: detail === true && errors.length === 0, detail, errors });
|
||||
} catch (error) {
|
||||
results.push({ name, pass: false, detail: String(error), errors: browserErrors.slice(errorStart) });
|
||||
}
|
||||
}
|
||||
|
||||
if (!liveOnly) {
|
||||
for (const [name, hash, readyExpression] of routes) {
|
||||
await check(`页面:${name}`, async () => {
|
||||
if (!(await navigate(hash, readyExpression))) return "页面未在 10 秒内完成渲染";
|
||||
const healthy = await evaluate(`(function(){var root=document.getElementById('root');return !!root&&root.getBoundingClientRect().width>0&&root.getBoundingClientRect().height>0&&!/something went wrong|页面加载失败|发生错误/i.test(document.body.innerText)})()`);
|
||||
return healthy || "页面根节点尺寸或错误边界异常";
|
||||
});
|
||||
}
|
||||
|
||||
await check("设置和更多菜单", async () => {
|
||||
await navigate("#/page/workspace/chats/new", routes[0][2]);
|
||||
const settingsTrigger = `((${clickText("设置和更多")}))||((${clickTitle("设置和更多")}))`;
|
||||
if (!(await waitFor(settingsTrigger, 15_000))) return "未找到入口";
|
||||
return (await waitFor(visibleText("[role=menu],[data-slot=dropdown-menu-content]", "通知"))) || "菜单未打开";
|
||||
});
|
||||
|
||||
await check("换肤色板", async () => {
|
||||
await navigate("#/page/workspace/chats/new", routes[0][2]);
|
||||
const hovered = await evaluate(`(function(){var e=[].slice.call(document.querySelectorAll('[title]')).filter(function(x){return x.getAttribute('title')==='换肤'})[0];if(!e)return false;e.dispatchEvent(new MouseEvent('mouseover',{bubbles:true,view:window}));return true})()`);
|
||||
if (!hovered) return "未找到换肤入口";
|
||||
return (await waitFor(`[].slice.call(document.querySelectorAll('button[aria-label]')).filter(function(x){return ['红色','绿色','蓝色','冰川蓝','浅黄色','深蓝色'].indexOf(x.getAttribute('aria-label'))>=0&&x.getBoundingClientRect().width>0}).length>=6`)) || "色板未显示";
|
||||
});
|
||||
|
||||
for (const [name, button, expected] of [
|
||||
["Agent 选择器", "选择 Agent", "选择 Agent"],
|
||||
["模型选择器", "__model_selector__", "Model Selector"],
|
||||
["常用提示词管理", null, "管理常用提示词"],
|
||||
]) {
|
||||
await check(name, async () => {
|
||||
await navigate("#/page/workspace/chats/new", routes[0][2]);
|
||||
const clicked = await evaluate(
|
||||
button === "__model_selector__"
|
||||
? clickModelSelector
|
||||
: button
|
||||
? clickText(button)
|
||||
: clickTitle("管理常用提示词"),
|
||||
);
|
||||
if (!clicked) return "未找到入口";
|
||||
return (await waitFor(visibleText("[role=dialog]", expected))) || "弹窗未打开";
|
||||
});
|
||||
}
|
||||
|
||||
await check("输入框 Enter 提交", async () => {
|
||||
await navigate("#/page/workspace/chats/new", routes[0][2]);
|
||||
const submitted = await evaluate(`(function(){var textarea=document.querySelector('textarea'),form=textarea&&textarea.closest('form');if(!textarea||!form)return false;window.__firefox52SubmitCount=0;form.addEventListener('submit',function(event){event.preventDefault();window.__firefox52SubmitCount+=1},{capture:true,once:true});var setter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set;setter.call(textarea,'Firefox 52 Enter smoke test');textarea.dispatchEvent(new Event('input',{bubbles:true}));textarea.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true,cancelable:true}));return true})()`);
|
||||
if (!submitted) return "未找到输入表单";
|
||||
return (await waitFor(`window.__firefox52SubmitCount===1`)) || "Enter 未触发表单提交";
|
||||
});
|
||||
|
||||
await check("对话类型筛选", async () => {
|
||||
await navigate("#/page/workspace/chats", routes[1][2]);
|
||||
if (!(await evaluate(clickText("多智能体会商", true)))) return "未找到筛选按钮";
|
||||
await sleep(300);
|
||||
return (await evaluate(`(function(){var root=document.getElementById('root'),rect=root&&root.getBoundingClientRect();return !!root&&rect.width>0&&rect.height>0&&!/something went wrong|页面加载失败|发生错误/i.test(document.body.innerText)})()`)) || "筛选后页面异常";
|
||||
});
|
||||
|
||||
await check("项目创建模式", async () => {
|
||||
await navigate("#/page/strategy/qa/projects", routes[2][2]);
|
||||
if (!(await evaluate(clickText("新建项目")))) return "未找到新建项目";
|
||||
return (await waitFor(visibleText("[role=dialog]", "选择创建模式"))) || "创建模式弹窗未打开";
|
||||
});
|
||||
|
||||
await check("AI 写作模型选择", async () => {
|
||||
await navigate("#/page/workspace/ai-writing", routes[3][2]);
|
||||
if (!(await evaluate(clickModelSelector))) return "未找到模型入口";
|
||||
return (await waitFor(visibleText("[role=dialog]", "Model Selector"))) || "模型弹窗未打开";
|
||||
});
|
||||
|
||||
await check("多智能体业务链条", async () => {
|
||||
await navigate("#/page/workspace/roundtable/planning", routes[4][2]);
|
||||
if (!(await evaluate(clickText("业务链条")))) return "未找到业务链条入口";
|
||||
return (await waitFor(visibleText("[role=dialog]", "业务链条"))) || "业务链条弹窗未打开";
|
||||
});
|
||||
|
||||
await check("定时任务新建", async () => {
|
||||
await navigate("#/page/workspace/scheduled/tasks", routes[5][2]);
|
||||
if (!(await evaluate(clickText("新建")))) return "未找到新建入口";
|
||||
return (await waitFor(visibleText("[role=dialog]", "新建定时任务"))) || "新建定时任务弹窗未打开";
|
||||
});
|
||||
|
||||
await check("md 转 Word 编辑器", async () => {
|
||||
await navigate("#/page/workspace/knowledge/markdown-editor", routes[6][2]);
|
||||
const ready = await evaluate(`!!document.querySelector('.cm-editor')&&!!document.querySelector('input[placeholder="请输入导出的 Word 标题"]')`);
|
||||
if (!ready) return "编辑器或标题输入框缺失";
|
||||
if (!(await evaluate(clickText("重置")))) return "重置按钮不可用";
|
||||
return true;
|
||||
});
|
||||
|
||||
await check("md 转 Word 实际生成", async () => {
|
||||
await navigate("#/page/workspace/knowledge/markdown-editor", routes[6][2]);
|
||||
const installed = await evaluate(`(function(){window.__firefox52Download=null;window.__firefox52DownloadNetwork=[];window.__firefox52DownloadNotices=[];new MutationObserver(function(){[].slice.call(document.querySelectorAll('[data-sonner-toast],[role="alert"],[role="status"]')).forEach(function(item){var value=(item.innerText||item.textContent||'').trim();if(value&&window.__firefox52DownloadNotices.indexOf(value)<0)window.__firefox52DownloadNotices.push(value)})}).observe(document.body,{childList:true,subtree:true});var originalFetch=window.fetch.bind(window);window.fetch=function(){var args=arguments,item={url:String(args[0]&&args[0].url||args[0]||''),status:'pending'};window.__firefox52DownloadNetwork.push(item);return originalFetch.apply(window,args).then(function(response){item.status=response.status;return response},function(error){item.status='error';item.error=String(error);throw error})};var originalCreate=URL.createObjectURL.bind(URL);URL.createObjectURL=function(blob){window.__firefox52Download={blobSize:blob.size,blobType:blob.type,download:''};return originalCreate(blob)};HTMLAnchorElement.prototype.click=function(){var state=window.__firefox52Download||{};state.download=this.download||'';state.href=this.href||'';window.__firefox52Download=state};return true})()`);
|
||||
if (!installed) return "无法安装下载捕获器";
|
||||
if (!(await evaluate(clickText("下载 Word")))) return "下载 Word 按钮不可用";
|
||||
const generated = await waitFor(`(function(){var item=window.__firefox52Download;return !!item&&item.blobSize>0&&/\\.docx$/i.test(item.download)&&item.blobType==='application/vnd.openxmlformats-officedocument.wordprocessingml.document'})()`, 60_000);
|
||||
if (generated) return true;
|
||||
return await evaluate(`JSON.stringify({download:window.__firefox52Download||null,network:window.__firefox52DownloadNetwork||[],notices:window.__firefox52DownloadNotices||[]})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (liveMode) {
|
||||
await check(liveChatName, async () => {
|
||||
await navigate(liveChatHash, routes[0][2]);
|
||||
const marker = `FIREFOX52_STREAM_OK_${Date.now()}`;
|
||||
const prompt = `请只回复这一行,不要添加其他内容:${marker}`;
|
||||
const tracing = await evaluate(`(function(){window.__firefox52Network=[];window.__firefox52RuntimeErrors=[];window.__firefox52Notices=[];window.addEventListener('error',function(event){window.__firefox52RuntimeErrors.push(String(event.message||event.error||'error'))});window.addEventListener('unhandledrejection',function(event){window.__firefox52RuntimeErrors.push(String(event.reason||'unhandled rejection'))});new MutationObserver(function(){[].slice.call(document.querySelectorAll('[data-sonner-toast],[role="alert"],[role="status"]')).forEach(function(item){var value=(item.innerText||item.textContent||'').trim();if(value&&window.__firefox52Notices.indexOf(value)<0)window.__firefox52Notices.push(value)})}).observe(document.body,{childList:true,subtree:true});if(!window.__firefox52OriginalFetch){window.__firefox52OriginalFetch=window.fetch;window.fetch=function(){var args=arguments,url=String(args[0]&&args[0].url||args[0]||''),init=args[1]||{};window.__firefox52Network.push({kind:'fetch',method:String(init.method||args[0]&&args[0].method||'GET'),url:url,status:'pending'});var item=window.__firefox52Network[window.__firefox52Network.length-1];return window.__firefox52OriginalFetch.apply(this,args).then(function(response){item.status=response.status;if(item.method==='POST'&&item.url.slice(-12)==='/api/threads'){try{response.clone().text().then(function(body){item.body=body.slice(0,500)},function(error){item.cloneError=String(error)})}catch(error){item.cloneError=String(error)}}return response},function(error){item.status='error';item.error=String(error);throw error})}}if(!window.__firefox52OriginalXhrOpen){window.__firefox52OriginalXhrOpen=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url){this.__firefox52Trace={kind:'xhr',method:String(method),url:String(url),status:'pending'};window.__firefox52Network.push(this.__firefox52Trace);this.addEventListener('loadend',function(){this.__firefox52Trace.status=this.status});return window.__firefox52OriginalXhrOpen.apply(this,arguments)}}return true})()`);
|
||||
if (!tracing) return "无法安装请求追踪器";
|
||||
const filled = await evaluate(`(function(){var textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]');if(!textarea)return false;var setter=Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype,'value').set;setter.call(textarea,${JSON.stringify(prompt)});textarea.dispatchEvent(new Event('input',{bubbles:true}));return textarea.value===${JSON.stringify(prompt)}})()`);
|
||||
if (!filled) return "未找到问答输入框";
|
||||
// Keep Enter compatibility as its own assertion above. The live check
|
||||
// clicks the real submit control so failures are attributable to the
|
||||
// backend stream rather than to synthetic keyboard-event differences.
|
||||
await sleep(300);
|
||||
const sent = await evaluate(`(function(){var buttons=[].slice.call(document.querySelectorAll('button[aria-label="Submit"]')),button=buttons.filter(function(item){var rect=item.getBoundingClientRect();return rect.width>0&&rect.height>0})[0];if(!button||button.disabled)return false;var form=button.form;if(!form)return false;window.__firefox52SubmitCount=0;form.addEventListener('submit',function(){window.__firefox52SubmitCount+=1},{capture:true});button.click();return true})()`);
|
||||
if (!sent) return "发送按钮不存在或仍处于禁用状态";
|
||||
const enteredThread = await waitFor(`location.hash.indexOf(${JSON.stringify(liveThreadPrefix)})===0&&location.hash.indexOf('/new')<0`, 45_000);
|
||||
if (!enteredThread) {
|
||||
const diagnostic = await evaluate(`(function(){var textarea=document.querySelector('textarea[placeholder="今天我能为你做些什么?"]');return JSON.stringify({hash:location.hash,submitCount:window.__firefox52SubmitCount||0,textarea:textarea&&textarea.value,markerVisible:(document.body.innerText||'').indexOf(${JSON.stringify(marker)})>=0,lastAgent:localStorage.getItem('deerflow.last-agent-id'),pendingAgentMessage:sessionStorage.getItem('pendingAgentMessage'),notices:window.__firefox52Notices||[],runtimeErrors:window.__firefox52RuntimeErrors||[],network:(window.__firefox52Network||[]).slice(-12),text:(document.body.innerText||'').slice(-800)})})()`);
|
||||
return `发送后未进入会话路由:${diagnostic}`;
|
||||
}
|
||||
const streamed = await waitFor(`(function(){var marker=${JSON.stringify(marker)},text=document.body.innerText||'',count=text.split(marker).length-1;return count>=2&&!!document.querySelector('[data-reference-batch-id]')})()`, 180_000);
|
||||
return streamed || "180 秒内未收到包含校验标记的流式回答";
|
||||
});
|
||||
}
|
||||
|
||||
socket.end();
|
||||
|
||||
for (const result of results) {
|
||||
const mark = result.pass ? "PASS" : "FAIL";
|
||||
console.log(`${mark} ${result.name}${result.pass ? "" : ` — ${result.detail}`}`);
|
||||
for (const error of result.errors) console.log(` ${error}`);
|
||||
}
|
||||
|
||||
const failed = results.filter((result) => !result.pass);
|
||||
console.log(`\nFirefox 52 smoke: ${results.length - failed.length}/${results.length} passed`);
|
||||
if (failed.length > 0) process.exitCode = 1;
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:#fddadc;stroke:#f86c73;stroke-dasharray:4;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0.5 -109.5)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 110)"/><g transform="translate(6.5 116.5)"><rect class="b" width="39" height="39"/><path class="c" d="M20.72,28.846a8.938,8.938,0,0,1-7.044-14.437c.01-.014.02-.025.029-.037s.026-.035.04-.051l.02-.025.017-.02.012-.014c.01-.012.021-.023.03-.037l.015-.018a.179.179,0,0,0,.021-.025l.011-.012a.139.139,0,0,1,.011-.012l.064-.078.026-.031A.221.221,0,0,0,14,14.021a8.936,8.936,0,0,1,12.315-1.082c.093.072.181.146.279.234,0,0,0,0,.008.006.023.021.048.041.07.063a.393.393,0,0,1,.037.033c.015.012.028.025.041.037l.016.016a.055.055,0,0,1,.013.01l.014.012.014.012a.043.043,0,0,0,.008.01l0,0a8.964,8.964,0,0,1,2.719,5.055l0,.014c0,.031.01.061.015.09l0,.016,0,.016c0,.021.007.045.011.066l0,.018v0a.04.04,0,0,1,0,.01l0,.016,0,.018,0,.016c0,.018,0,.035.006.053a.171.171,0,0,1,0,.031c.011.078.02.158.026.238l.008.07v.018c0,.016,0,.029,0,.045a.106.106,0,0,0,0,.018.108.108,0,0,1,0,.018.28.28,0,0,0,0,.035l0,.051v.006c0,.021,0,.045,0,.064s0,.02,0,.029a.064.064,0,0,1,0,.014v.014c0,.021,0,.041,0,.063v.014c0,.01,0,.02,0,.029v.018c0,.025,0,.049,0,.074s0,.033,0,.049a.131.131,0,0,0,0,.02V19.7c0,.016,0,.027,0,.043v.041c0,.041,0,.082,0,.123A8.947,8.947,0,0,1,20.72,28.846Zm-3.574-9.953a1.016,1.016,0,0,0,0,2.031H19.7v2.563a1.016,1.016,0,0,0,2.031,0V20.924h2.563a1.016,1.016,0,0,0,0-2.031H21.73V16.334a1.016,1.016,0,0,0-2.031,0v2.559Zm-6.872,9.547H2.726A2.731,2.731,0,0,1,0,25.709V18.166a2.728,2.728,0,0,1,2.726-2.725h7.548a2.736,2.736,0,0,1,1.336.35A10.015,10.015,0,0,0,12.954,26.2,2.727,2.727,0,0,1,10.273,28.439ZM2.726,17.877a.292.292,0,0,0-.289.289v7.543A.293.293,0,0,0,2.726,26h7.548a.292.292,0,0,0,.289-.295V18.166a.292.292,0,0,0-.289-.289ZM10.273,13H2.726A2.731,2.731,0,0,1,0,10.273V2.73A2.731,2.731,0,0,1,2.726,0h7.548A2.731,2.731,0,0,1,13,2.73v7.543A2.731,2.731,0,0,1,10.273,13Zm17.09-.559v0a9.929,9.929,0,0,0-3.091-1.881H25.71A.288.288,0,0,0,26,10.273V2.73a.288.288,0,0,0-.288-.289H18.166a.286.286,0,0,0-.289.289v7.543c0,.014,0,.027,0,.049a9.841,9.841,0,0,0-2.246.979,2.737,2.737,0,0,1-.2-1.027V2.73A2.734,2.734,0,0,1,18.166,0H25.71a2.733,2.733,0,0,1,2.729,2.73v7.543a2.709,2.709,0,0,1-1.075,2.17Z" transform="translate(5.281 5.278)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:#d4e9ff;stroke:#55a6fe;stroke-dasharray:4;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0.5 -109.5)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 110)"/><g transform="translate(6.5 116.5)"><rect class="b" width="39" height="39"/><path class="c" d="M20.72,28.846a8.938,8.938,0,0,1-7.044-14.437c.01-.014.02-.025.029-.037s.026-.035.04-.051l.02-.025.017-.02.012-.014c.01-.012.021-.023.03-.037l.015-.018a.179.179,0,0,0,.021-.025l.011-.012a.139.139,0,0,1,.011-.012l.064-.078.026-.031A.221.221,0,0,0,14,14.021a8.936,8.936,0,0,1,12.315-1.082c.093.072.181.146.279.234,0,0,0,0,.008.006.023.021.048.041.07.063a.393.393,0,0,1,.037.033c.015.012.028.025.041.037l.016.016a.055.055,0,0,1,.013.01l.014.012.014.012a.043.043,0,0,0,.008.01l0,0a8.964,8.964,0,0,1,2.719,5.055l0,.014c0,.031.01.061.015.09l0,.016,0,.016c0,.021.007.045.011.066l0,.018v0a.04.04,0,0,1,0,.01l0,.016,0,.018,0,.016c0,.018,0,.035.006.053a.171.171,0,0,1,0,.031c.011.078.02.158.026.238l.008.07v.018c0,.016,0,.029,0,.045a.106.106,0,0,0,0,.018.108.108,0,0,1,0,.018.28.28,0,0,0,0,.035l0,.051v.006c0,.021,0,.045,0,.064s0,.02,0,.029a.064.064,0,0,1,0,.014v.014c0,.021,0,.041,0,.063v.014c0,.01,0,.02,0,.029v.018c0,.025,0,.049,0,.074s0,.033,0,.049a.131.131,0,0,0,0,.02V19.7c0,.016,0,.027,0,.043v.041c0,.041,0,.082,0,.123A8.947,8.947,0,0,1,20.72,28.846Zm-3.574-9.953a1.016,1.016,0,0,0,0,2.031H19.7v2.563a1.016,1.016,0,0,0,2.031,0V20.924h2.563a1.016,1.016,0,0,0,0-2.031H21.73V16.334a1.016,1.016,0,0,0-2.031,0v2.559Zm-6.872,9.547H2.726A2.731,2.731,0,0,1,0,25.709V18.166a2.728,2.728,0,0,1,2.726-2.725h7.548a2.736,2.736,0,0,1,1.336.35A10.015,10.015,0,0,0,12.954,26.2,2.727,2.727,0,0,1,10.273,28.439ZM2.726,17.877a.292.292,0,0,0-.289.289v7.543A.293.293,0,0,0,2.726,26h7.548a.292.292,0,0,0,.289-.295V18.166a.292.292,0,0,0-.289-.289ZM10.273,13H2.726A2.731,2.731,0,0,1,0,10.273V2.73A2.731,2.731,0,0,1,2.726,0h7.548A2.731,2.731,0,0,1,13,2.73v7.543A2.731,2.731,0,0,1,10.273,13Zm17.09-.559v0a9.929,9.929,0,0,0-3.091-1.881H25.71A.288.288,0,0,0,26,10.273V2.73a.288.288,0,0,0-.288-.289H18.166a.286.286,0,0,0-.289.289v7.543c0,.014,0,.027,0,.049a9.841,9.841,0,0,0-2.246.979,2.737,2.737,0,0,1-.2-1.027V2.73A2.734,2.734,0,0,1,18.166,0H25.71a2.733,2.733,0,0,1,2.729,2.73v7.543a2.709,2.709,0,0,1-1.075,2.17Z" transform="translate(5.281 5.278)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(2.236 118.236)"><path class="b" d="M9204.336-22218.762a6.4,6.4,0,0,1-2.489-.691,6.242,6.242,0,0,1-2-1.615,6.363,6.363,0,0,1-1.206-2.283,6.357,6.357,0,0,1-.246-1.863c-.047-.033-.094-.066-.139-.1a6.4,6.4,0,0,1-2.272-3.4,6.418,6.418,0,0,1,.268-4.076,6.336,6.336,0,0,1,1.233-1.912,6.383,6.383,0,0,1-.4-1.973,6.41,6.41,0,0,1,.53-2.809,6.441,6.441,0,0,1,1.7-2.289,6.424,6.424,0,0,1,1.751-1.057,4.969,4.969,0,0,1,.343-1.289,4.965,4.965,0,0,1,1.334-1.859,5.038,5.038,0,0,1,2.016-1.082,5.037,5.037,0,0,1,2.289-.074,5.024,5.024,0,0,1,2.089.943,4.947,4.947,0,0,1,.871.861,5.147,5.147,0,0,1,.876-.861,5,5,0,0,1,2.083-.943,5.035,5.035,0,0,1,2.289.074,5.06,5.06,0,0,1,2.021,1.082,5.031,5.031,0,0,1,1.334,1.859,5.008,5.008,0,0,1,.343,1.289,6.424,6.424,0,0,1,1.751,1.057,6.441,6.441,0,0,1,1.7,2.289,6.405,6.405,0,0,1,.524,2.809,6.434,6.434,0,0,1-.4,1.973,6.315,6.315,0,0,1,1.234,1.912,6.418,6.418,0,0,1,.268,4.076,6.4,6.4,0,0,1-2.272,3.4l-.143.105a6.214,6.214,0,0,1-.248,1.859,6.26,6.26,0,0,1-1.2,2.283,6.271,6.271,0,0,1-2.01,1.615,6.354,6.354,0,0,1-2.485.691c-.14.006-.278.012-.418.012a6.213,6.213,0,0,1-2.139-.375,6.394,6.394,0,0,1-2.2-1.34,6.4,6.4,0,0,1-.915-1.066,6.415,6.415,0,0,1-.91,1.066,6.427,6.427,0,0,1-2.206,1.34,6.283,6.283,0,0,1-2.143.377C9204.609-22218.748,9204.473-22218.752,9204.336-22218.762Zm6.768-6.342a4.232,4.232,0,0,0,.346,1.652,4.123,4.123,0,0,0,.972,1.379,4.116,4.116,0,0,0,1.44.877,4.062,4.062,0,0,0,1.67.234,4.159,4.159,0,0,0,1.63-.447,4.159,4.159,0,0,0,1.313-1.061,4.227,4.227,0,0,0,.787-1.49c.02-.07.037-.139.054-.209a6.32,6.32,0,0,1-1.422.164,1.1,1.1,0,0,1-1.1-1.1,1.1,1.1,0,0,1,1.1-1.1,4.178,4.178,0,0,0,2.529-.859,4.138,4.138,0,0,0,1.485-2.221,4.216,4.216,0,0,0-.173-2.67,4.2,4.2,0,0,0-1.765-2.01,1.1,1.1,0,0,1-.4-1.5,1.1,1.1,0,0,1,1.063-.543,4.16,4.16,0,0,0,.1-.758,4.249,4.249,0,0,0-.346-1.838,4.156,4.156,0,0,0-1.117-1.5,4.078,4.078,0,0,0-.569-.408,4.928,4.928,0,0,1-.385.838,1.1,1.1,0,0,1-.955.553,1.08,1.08,0,0,1-.547-.15,1.093,1.093,0,0,1-.4-1.5,2.906,2.906,0,0,0,.374-1.229,1.022,1.022,0,0,1,.006-.145,2.841,2.841,0,0,0-.218-1.133,2.836,2.836,0,0,0-.754-1.051,2.794,2.794,0,0,0-1.139-.607,2.8,2.8,0,0,0-1.29-.045,2.782,2.782,0,0,0-1.173.535,2.84,2.84,0,0,0-.82.994,2.912,2.912,0,0,0-.3,1.262v5.254a4.426,4.426,0,0,0,.877,2.625,4.4,4.4,0,0,0,2.277,1.58,1.1,1.1,0,0,1,.748,1.361,1.1,1.1,0,0,1-1.367.748,6.6,6.6,0,0,1-2.535-1.41v6.928Zm-10.352,1.145a4.119,4.119,0,0,0,.787,1.49,4.148,4.148,0,0,0,1.313,1.061,4.105,4.105,0,0,0,1.624.447,4.076,4.076,0,0,0,1.676-.234,4.125,4.125,0,0,0,1.44-.877,4.208,4.208,0,0,0,.972-1.379,4.226,4.226,0,0,0,.34-1.652v-6.924a6.628,6.628,0,0,1-2.534,1.406,1.117,1.117,0,0,1-.308.045,1.1,1.1,0,0,1-1.055-.793,1.093,1.093,0,0,1,.748-1.361,4.393,4.393,0,0,0,2.272-1.58,4.354,4.354,0,0,0,.876-2.625v-5.254a2.906,2.906,0,0,0-.29-1.262,2.84,2.84,0,0,0-.82-.994,2.778,2.778,0,0,0-1.179-.535,2.8,2.8,0,0,0-1.29.045,2.832,2.832,0,0,0-1.139.607,2.821,2.821,0,0,0-.748,1.051,2.819,2.819,0,0,0-.212,1.271,2.821,2.821,0,0,0,.374,1.234,1.1,1.1,0,0,1-.4,1.5,1.1,1.1,0,0,1-1.5-.4,5.009,5.009,0,0,1-.386-.838,4.127,4.127,0,0,0-.568.408,4.156,4.156,0,0,0-1.117,1.5,4.249,4.249,0,0,0-.346,1.838,4.246,4.246,0,0,0,.1.758,1.106,1.106,0,0,1,1.064.543,1.1,1.1,0,0,1-.4,1.5,4.2,4.2,0,0,0-1.765,2.01,4.216,4.216,0,0,0-.173,2.67,4.138,4.138,0,0,0,1.485,2.221,4.178,4.178,0,0,0,2.529.859,1.1,1.1,0,0,1,1.1,1.1,1.1,1.1,0,0,1-1.1,1.1,6.32,6.32,0,0,1-1.422-.164C9200.714-22224.1,9200.732-22224.029,9200.752-22223.959Z" transform="translate(-9186.242 22256.754)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{opacity:0;}.b,.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18559 4825)"><rect class="b" width="42" height="42" transform="translate(-18554 -4704)"/><path class="c" d="M-9736.837-26950.139a1.008,1.008,0,0,1-.542-.889v-3.371h-.752a4.506,4.506,0,0,1-4.5-4.5v-2.629c-1.89-.08-3.366-2-3.366-4.367s1.477-4.287,3.366-4.369v-2.633a4.5,4.5,0,0,1,4.5-4.5h15.658v.129a3.676,3.676,0,0,0,.494,1.869h-16.152a2.5,2.5,0,0,0-2.5,2.5v14a2.5,2.5,0,0,0,2.5,2.5h1.741a1.006,1.006,0,0,1,1.009,1v2.432l4.544-3.244a.97.97,0,0,1,.576-.189h13.124a2.5,2.5,0,0,0,2.506-2.5v-9.441a3.524,3.524,0,0,0,1.118.189h.033a3.634,3.634,0,0,0,2.764-1.293,4.793,4.793,0,0,1,1.456,3.549,5.014,5.014,0,0,1-.982,3.041,3.23,3.23,0,0,1-2.391,1.334v2.621a4.507,4.507,0,0,1-4.5,4.5h-12.806l-5.858,4.184a1.021,1.021,0,0,1-.582.191A1.007,1.007,0,0,1-9736.837-26950.139Zm1.334-10.389a1.876,1.876,0,0,1-1.876-1.877v-7.869a1.876,1.876,0,0,1,1.876-1.877h15.739a1.876,1.876,0,0,1,1.876,1.877v7.869a1.876,1.876,0,0,1-1.876,1.877Zm10.931-5.811a1.752,1.752,0,0,0,1.754,1.746,1.751,1.751,0,0,0,1.747-1.746,1.754,1.754,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9724.572-26966.338Zm-9.617,0a1.752,1.752,0,0,0,1.748,1.746,1.751,1.751,0,0,0,1.747-1.746,1.755,1.755,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9734.189-26966.338Zm20.1-5.277-.453-1.551a4.433,4.433,0,0,0-2.946-3.006l-1.537-.467a.649.649,0,0,1-.447-.611.524.524,0,0,1,.021-.148.616.616,0,0,1,.217-.346.624.624,0,0,1,.217-.115l.284-.088,1.232-.365a4.417,4.417,0,0,0,2.96-3.02l.46-1.551a.625.625,0,0,1,.42-.434v.006a.538.538,0,0,1,.183-.033.574.574,0,0,1,.292.08.581.581,0,0,1,.3.381l.46,1.557a4.422,4.422,0,0,0,2.96,3.014l1.171.346.346.107a.647.647,0,0,1,.454.609.641.641,0,0,1-.447.611l-1.537.467a4.434,4.434,0,0,0-2.946,3.006l-.453,1.551a.631.631,0,0,1-.6.461A.636.636,0,0,1-9714.089-26971.615Zm-4.571-7.34-.183-.623a2.7,2.7,0,0,0-1.829-1.83l-.623-.189a.29.29,0,0,1-.2-.277.284.284,0,0,1,.2-.277l.61-.184a2.7,2.7,0,0,0,1.842-1.842l.183-.615a.32.32,0,0,1,.2-.2.327.327,0,0,1,.224.027.275.275,0,0,1,.143.17l.183.615a2.713,2.713,0,0,0,1.842,1.842l.609.184a.289.289,0,0,1,.21.277.3.3,0,0,1-.21.277l-.257.076-.366.113a2.725,2.725,0,0,0-1.828,1.83l-.183.623a.293.293,0,0,1-.278.2A.3.3,0,0,1-9718.66-26978.955Z" transform="translate(-8805.37 22282.029)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><image width="1920" height="1080" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><image width="582" height="667" xlink:href="data:image/png;base64,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VkILine truncated
|
||||
|
Before Width: | Height: | Size: 608 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="none" width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 50 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><image width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(32,194,255,0.15);stroke-width:5px;stroke-dasharray:7 5;}.e{stroke:none;}.g{filter:url(#e);}.h{filter:url(#b);}</style><linearGradient id="a" x1="0.351" y1="0.016" x2="0.065" y2="0.243" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#1ec6fe"/><stop offset="1" stop-color="#3279ff"/></linearGradient><filter id="b" x="88.068" y="60.324" width="217.004" height="181.59" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="c"/><feFlood flood-color="#1a9cff" flood-opacity="0.8"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.025" y1="0.971" x2="0.971" y2="0.034" xlink:href="#a"/><filter id="e" x="424.041" y="484.359" width="215.309" height="181.689" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="f"/><feFlood flood-color="#1a9cff" flood-opacity="0.8"/><feComposite operator="in" in2="f"/><feComposite in="SourceGraphic"/></filter></defs><g transform="translate(4 4)"><g transform="translate(-126.998 -127.002)"><g class="h" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="a" d="M18115.566-21172.494q4.919-8.443,10.389-16.543a294.531,294.531,0,0,1,35.713-43.289,294.545,294.545,0,0,1,43.293-35.717,292.5,292.5,0,0,1,49.682-26.969,290.614,290.614,0,0,1,31.271-11.162l1.656,5.506a288.092,288.092,0,0,0-166.973,131.084Z" transform="translate(-18005 21389)"/></g><g class="g" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="b" d="M18451.539-20750.967a288.209,288.209,0,0,0,165.279-131.172l5.029,2.92q-5.1,8.815-10.8,17.258a294.659,294.659,0,0,1-35.717,43.287,294.647,294.647,0,0,1-43.293,35.717,292.2,292.2,0,0,1-49.686,26.971,291.1,291.1,0,0,1-29.178,10.537Z" transform="translate(-18005 21389)"/></g><circle class="c" cx="359.5" cy="359.5" r="359.5" transform="translate(126.998 127.002)"/></g><g class="d" transform="translate(98 98)"><circle class="e" cx="261.5" cy="261.5" r="261.5"/><circle class="f" cx="261.5" cy="261.5" r="259"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="458" height="113.588" viewBox="0 0 458 113.588"><defs><style>.a{fill:url(#a);}.b{fill:#fff;}.c{filter:url(#d);}.d{filter:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f0feff"/><stop offset="1" stop-color="#c1e6ff"/></linearGradient><filter id="b" x="111.721" y="26.943" width="211.559" height="46.289" filterUnits="userSpaceOnUse"><feOffset dy="1" input="SourceAlpha"/><feGaussianBlur stdDeviation="3" result="c"/><feFlood flood-color="#00ffa2" flood-opacity="0.322"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><filter id="d" x="111.721" y="26.943" width="211.559" height="46.289" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="2" result="e"/><feFlood flood-color="#fff" result="f"/><feComposite operator="out" in="SourceGraphic" in2="e"/><feComposite operator="in" in="f"/><feComposite operator="in" in2="SourceGraphic"/></filter></defs><g transform="translate(-731 -564.412)"><image width="458" height="113.588" transform="translate(731 564.412)" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 813 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-14540.154 22987.455)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,48 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="188" height="241" viewBox="0 0 188 241" fill="none">
|
||||
<defs>
|
||||
<radialGradient id="surface" cx="0" cy="0" r="1" gradientTransform="translate(63 27) rotate(61) scale(148 156)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset=".2" stop-color="#FAFDFF"/>
|
||||
<stop offset=".48" stop-color="#E4F2FF"/>
|
||||
<stop offset=".72" stop-color="#A7D4FF"/>
|
||||
<stop offset="1" stop-color="#3C9AF4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="inner-blue" cx="0" cy="0" r="1" gradientTransform="translate(94 101) rotate(90) scale(68 84)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#68B6FF" stop-opacity=".58"/>
|
||||
<stop offset="1" stop-color="#1688F7" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="lower-blue" x1="94" y1="103" x2="94" y2="165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4EA7FA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#188AF4" stop-opacity=".72"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="38" y1="10" x2="151" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF" stop-opacity=".98"/>
|
||||
<stop offset=".52" stop-color="#FFFFFF" stop-opacity=".86"/>
|
||||
<stop offset="1" stop-color="#C6E8FF" stop-opacity=".9"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-13" y="-13" width="214" height="214" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="4.8"/>
|
||||
</filter>
|
||||
<filter id="shadow" x="18" y="162" width="152" height="49" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="7"/>
|
||||
</filter>
|
||||
<clipPath id="clip">
|
||||
<circle cx="94" cy="82" r="81"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="94" cy="188" rx="65" ry="9" fill="#147BD2" fill-opacity=".23" filter="url(#shadow)"/>
|
||||
<circle cx="94" cy="82" r="82" fill="#5EBAFF" fill-opacity=".2" filter="url(#glow)"/>
|
||||
<circle cx="94" cy="82" r="81" fill="url(#surface)"/>
|
||||
<g clip-path="url(#clip)">
|
||||
<ellipse cx="94" cy="106" rx="80" ry="61" fill="url(#inner-blue)"/>
|
||||
<ellipse cx="94" cy="143" rx="91" ry="38" fill="url(#lower-blue)"/>
|
||||
<ellipse cx="94" cy="151" rx="76" ry="14" fill="#FFFFFF" fill-opacity=".11"/>
|
||||
<path d="M20 145C47 157 141 157 168 145" stroke="#FFFFFF" stroke-opacity=".28" stroke-width="1.2"/>
|
||||
<path d="M31 160C55 168 133 168 157 160" stroke="#7CC4FF" stroke-opacity=".35" stroke-width="1.1"/>
|
||||
<path d="M22 64C27 37 49 16 77 9" stroke="#FFFFFF" stroke-opacity=".42" stroke-width="8" stroke-linecap="round" filter="url(#glow)"/>
|
||||
</g>
|
||||
<circle cx="94" cy="82" r="81" stroke="url(#edge)" stroke-width="2"/>
|
||||
<circle cx="94" cy="82" r="76" stroke="#FFFFFF" stroke-opacity=".24"/>
|
||||
<path d="M19 70C24 39 49 13 81 5" stroke="#FFFFFF" stroke-opacity=".88" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.337" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.414" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(2.236 118.236)"><path class="b" d="M9204.336-22218.762a6.4,6.4,0,0,1-2.489-.691,6.242,6.242,0,0,1-2-1.615,6.363,6.363,0,0,1-1.206-2.283,6.357,6.357,0,0,1-.246-1.863c-.047-.033-.094-.066-.139-.1a6.4,6.4,0,0,1-2.272-3.4,6.418,6.418,0,0,1,.268-4.076,6.336,6.336,0,0,1,1.233-1.912,6.383,6.383,0,0,1-.4-1.973,6.41,6.41,0,0,1,.53-2.809,6.441,6.441,0,0,1,1.7-2.289,6.424,6.424,0,0,1,1.751-1.057,4.969,4.969,0,0,1,.343-1.289,4.965,4.965,0,0,1,1.334-1.859,5.038,5.038,0,0,1,2.016-1.082,5.037,5.037,0,0,1,2.289-.074,5.024,5.024,0,0,1,2.089.943,4.947,4.947,0,0,1,.871.861,5.147,5.147,0,0,1,.876-.861,5,5,0,0,1,2.083-.943,5.035,5.035,0,0,1,2.289.074,5.06,5.06,0,0,1,2.021,1.082,5.031,5.031,0,0,1,1.334,1.859,5.008,5.008,0,0,1,.343,1.289,6.424,6.424,0,0,1,1.751,1.057,6.441,6.441,0,0,1,1.7,2.289,6.405,6.405,0,0,1,.524,2.809,6.434,6.434,0,0,1-.4,1.973,6.315,6.315,0,0,1,1.234,1.912,6.418,6.418,0,0,1,.268,4.076,6.4,6.4,0,0,1-2.272,3.4l-.143.105a6.214,6.214,0,0,1-.248,1.859,6.26,6.26,0,0,1-1.2,2.283,6.271,6.271,0,0,1-2.01,1.615,6.354,6.354,0,0,1-2.485.691c-.14.006-.278.012-.418.012a6.213,6.213,0,0,1-2.139-.375,6.394,6.394,0,0,1-2.2-1.34,6.4,6.4,0,0,1-.915-1.066,6.415,6.415,0,0,1-.91,1.066,6.427,6.427,0,0,1-2.206,1.34,6.283,6.283,0,0,1-2.143.377C9204.609-22218.748,9204.473-22218.752,9204.336-22218.762Zm6.768-6.342a4.232,4.232,0,0,0,.346,1.652,4.123,4.123,0,0,0,.972,1.379,4.116,4.116,0,0,0,1.44.877,4.062,4.062,0,0,0,1.67.234,4.159,4.159,0,0,0,1.63-.447,4.159,4.159,0,0,0,1.313-1.061,4.227,4.227,0,0,0,.787-1.49c.02-.07.037-.139.054-.209a6.32,6.32,0,0,1-1.422.164,1.1,1.1,0,0,1-1.1-1.1,1.1,1.1,0,0,1,1.1-1.1,4.178,4.178,0,0,0,2.529-.859,4.138,4.138,0,0,0,1.485-2.221,4.216,4.216,0,0,0-.173-2.67,4.2,4.2,0,0,0-1.765-2.01,1.1,1.1,0,0,1-.4-1.5,1.1,1.1,0,0,1,1.063-.543,4.16,4.16,0,0,0,.1-.758,4.249,4.249,0,0,0-.346-1.838,4.156,4.156,0,0,0-1.117-1.5,4.078,4.078,0,0,0-.569-.408,4.928,4.928,0,0,1-.385.838,1.1,1.1,0,0,1-.955.553,1.08,1.08,0,0,1-.547-.15,1.093,1.093,0,0,1-.4-1.5,2.906,2.906,0,0,0,.374-1.229,1.022,1.022,0,0,1,.006-.145,2.841,2.841,0,0,0-.218-1.133,2.836,2.836,0,0,0-.754-1.051,2.794,2.794,0,0,0-1.139-.607,2.8,2.8,0,0,0-1.29-.045,2.782,2.782,0,0,0-1.173.535,2.84,2.84,0,0,0-.82.994,2.912,2.912,0,0,0-.3,1.262v5.254a4.426,4.426,0,0,0,.877,2.625,4.4,4.4,0,0,0,2.277,1.58,1.1,1.1,0,0,1,.748,1.361,1.1,1.1,0,0,1-1.367.748,6.6,6.6,0,0,1-2.535-1.41v6.928Zm-10.352,1.145a4.119,4.119,0,0,0,.787,1.49,4.148,4.148,0,0,0,1.313,1.061,4.105,4.105,0,0,0,1.624.447,4.076,4.076,0,0,0,1.676-.234,4.125,4.125,0,0,0,1.44-.877,4.208,4.208,0,0,0,.972-1.379,4.226,4.226,0,0,0,.34-1.652v-6.924a6.628,6.628,0,0,1-2.534,1.406,1.117,1.117,0,0,1-.308.045,1.1,1.1,0,0,1-1.055-.793,1.093,1.093,0,0,1,.748-1.361,4.393,4.393,0,0,0,2.272-1.58,4.354,4.354,0,0,0,.876-2.625v-5.254a2.906,2.906,0,0,0-.29-1.262,2.84,2.84,0,0,0-.82-.994,2.778,2.778,0,0,0-1.179-.535,2.8,2.8,0,0,0-1.29.045,2.832,2.832,0,0,0-1.139.607,2.821,2.821,0,0,0-.748,1.051,2.819,2.819,0,0,0-.212,1.271,2.821,2.821,0,0,0,.374,1.234,1.1,1.1,0,0,1-.4,1.5,1.1,1.1,0,0,1-1.5-.4,5.009,5.009,0,0,1-.386-.838,4.127,4.127,0,0,0-.568.408,4.156,4.156,0,0,0-1.117,1.5,4.249,4.249,0,0,0-.346,1.838,4.246,4.246,0,0,0,.1.758,1.106,1.106,0,0,1,1.064.543,1.1,1.1,0,0,1-.4,1.5,4.2,4.2,0,0,0-1.765,2.01,4.216,4.216,0,0,0-.173,2.67,4.138,4.138,0,0,0,1.485,2.221,4.178,4.178,0,0,0,2.529.859,1.1,1.1,0,0,1,1.1,1.1,1.1,1.1,0,0,1-1.1,1.1,6.32,6.32,0,0,1-1.422-.164C9200.714-22224.1,9200.732-22224.029,9200.752-22223.959Z" transform="translate(-9186.242 22256.754)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{opacity:0;}.b,.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18559 4825)"><rect class="b" width="42" height="42" transform="translate(-18554 -4704)"/><path class="c" d="M-9736.837-26950.139a1.008,1.008,0,0,1-.542-.889v-3.371h-.752a4.506,4.506,0,0,1-4.5-4.5v-2.629c-1.89-.08-3.366-2-3.366-4.367s1.477-4.287,3.366-4.369v-2.633a4.5,4.5,0,0,1,4.5-4.5h15.658v.129a3.676,3.676,0,0,0,.494,1.869h-16.152a2.5,2.5,0,0,0-2.5,2.5v14a2.5,2.5,0,0,0,2.5,2.5h1.741a1.006,1.006,0,0,1,1.009,1v2.432l4.544-3.244a.97.97,0,0,1,.576-.189h13.124a2.5,2.5,0,0,0,2.506-2.5v-9.441a3.524,3.524,0,0,0,1.118.189h.033a3.634,3.634,0,0,0,2.764-1.293,4.793,4.793,0,0,1,1.456,3.549,5.014,5.014,0,0,1-.982,3.041,3.23,3.23,0,0,1-2.391,1.334v2.621a4.507,4.507,0,0,1-4.5,4.5h-12.806l-5.858,4.184a1.021,1.021,0,0,1-.582.191A1.007,1.007,0,0,1-9736.837-26950.139Zm1.334-10.389a1.876,1.876,0,0,1-1.876-1.877v-7.869a1.876,1.876,0,0,1,1.876-1.877h15.739a1.876,1.876,0,0,1,1.876,1.877v7.869a1.876,1.876,0,0,1-1.876,1.877Zm10.931-5.811a1.752,1.752,0,0,0,1.754,1.746,1.751,1.751,0,0,0,1.747-1.746,1.754,1.754,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9724.572-26966.338Zm-9.617,0a1.752,1.752,0,0,0,1.748,1.746,1.751,1.751,0,0,0,1.747-1.746,1.755,1.755,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9734.189-26966.338Zm20.1-5.277-.453-1.551a4.433,4.433,0,0,0-2.946-3.006l-1.537-.467a.649.649,0,0,1-.447-.611.524.524,0,0,1,.021-.148.616.616,0,0,1,.217-.346.624.624,0,0,1,.217-.115l.284-.088,1.232-.365a4.417,4.417,0,0,0,2.96-3.02l.46-1.551a.625.625,0,0,1,.42-.434v.006a.538.538,0,0,1,.183-.033.574.574,0,0,1,.292.08.581.581,0,0,1,.3.381l.46,1.557a4.422,4.422,0,0,0,2.96,3.014l1.171.346.346.107a.647.647,0,0,1,.454.609.641.641,0,0,1-.447.611l-1.537.467a4.434,4.434,0,0,0-2.946,3.006l-.453,1.551a.631.631,0,0,1-.6.461A.636.636,0,0,1-9714.089-26971.615Zm-4.571-7.34-.183-.623a2.7,2.7,0,0,0-1.829-1.83l-.623-.189a.29.29,0,0,1-.2-.277.284.284,0,0,1,.2-.277l.61-.184a2.7,2.7,0,0,0,1.842-1.842l.183-.615a.32.32,0,0,1,.2-.2.327.327,0,0,1,.224.027.275.275,0,0,1,.143.17l.183.615a2.713,2.713,0,0,0,1.842,1.842l.609.184a.289.289,0,0,1,.21.277.3.3,0,0,1-.21.277l-.257.076-.366.113a2.725,2.725,0,0,0-1.828,1.83l-.183.623a.293.293,0,0,1-.278.2A.3.3,0,0,1-9718.66-26978.955Z" transform="translate(-8805.37 22282.029)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 3840 2160"><image width="3840" height="2160" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 4.3 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 1164 1312"><image width="1164" height="1312" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 968 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="xMidYMid slice" width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 278 1530"><image width="278" height="1530" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-14540.154 22987.455)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,47 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="188" height="241" viewBox="0 0 188 241" fill="none">
|
||||
<defs>
|
||||
<radialGradient id="surface" cx="0" cy="0" r="1" gradientTransform="translate(60 30) rotate(60) scale(150 158)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#46B6FF"/>
|
||||
<stop offset=".24" stop-color="#1A83DE"/>
|
||||
<stop offset=".6" stop-color="#075FBC"/>
|
||||
<stop offset="1" stop-color="#003675"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="inner-light" cx="0" cy="0" r="1" gradientTransform="translate(94 82) rotate(90) scale(79 84)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0876DC" stop-opacity=".42"/>
|
||||
<stop offset="1" stop-color="#001F55" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="lower-depth" x1="94" y1="118" x2="94" y2="165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#005DBB" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#002A68" stop-opacity=".58"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="36" y1="12" x2="152" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF" stop-opacity=".98"/>
|
||||
<stop offset=".48" stop-color="#B6EAFF" stop-opacity=".78"/>
|
||||
<stop offset="1" stop-color="#48BBFF" stop-opacity=".72"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-12" y="-12" width="212" height="212" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="4.5"/>
|
||||
</filter>
|
||||
<filter id="shadow" x="22" y="163" width="144" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="6.5"/>
|
||||
</filter>
|
||||
<clipPath id="clip">
|
||||
<circle cx="94" cy="82" r="81"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="94" cy="188" rx="61" ry="9" fill="#00112F" fill-opacity=".58" filter="url(#shadow)"/>
|
||||
<circle cx="94" cy="82" r="82" fill="#139DFF" fill-opacity=".22" filter="url(#glow)"/>
|
||||
<circle cx="94" cy="82" r="81" fill="url(#surface)"/>
|
||||
<g clip-path="url(#clip)">
|
||||
<circle cx="94" cy="82" r="79" fill="url(#inner-light)"/>
|
||||
<ellipse cx="94" cy="142" rx="89" ry="38" fill="url(#lower-depth)"/>
|
||||
<ellipse cx="94" cy="150" rx="78" ry="15" fill="#55B9FF" fill-opacity=".13"/>
|
||||
<path d="M19 145C46 157 142 157 169 145" stroke="#BCEAFF" stroke-opacity=".24" stroke-width="1.2"/>
|
||||
<path d="M30 160C54 169 134 169 158 160" stroke="#1599F7" stroke-opacity=".28" stroke-width="1.2"/>
|
||||
<path d="M24 62C29 36 49 17 76 11" stroke="#D8F4FF" stroke-opacity=".3" stroke-width="9" stroke-linecap="round" filter="url(#glow)"/>
|
||||
</g>
|
||||
<circle cx="94" cy="82" r="81" stroke="url(#edge)" stroke-width="2"/>
|
||||
<circle cx="94" cy="82" r="76" stroke="#27B2FF" stroke-opacity=".28"/>
|
||||
<path d="M19 69C24 38 49 12 82 4" stroke="#FFFFFF" stroke-opacity=".88" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.337" x2="1" y2="1.007" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.207" x2="0.793" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="54.923" height="54.898" viewBox="0 0 54.923 54.898"><defs><style>.a{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><path class="a" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-63.8 -64)"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{opacity:0;fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.337" x2="1" xlink:href="#a"/></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{stroke:#707070;stroke-width:3.75px;opacity:0;fill:url(#a);}.b{fill:url(#b);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.414" x2="1" xlink:href="#a"/></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -110,12 +110,12 @@ export const CodeBlock = ({
|
||||
>
|
||||
<div className="relative size-full">
|
||||
<div
|
||||
className="[&>pre]:!bg-background [&>pre]:!text-foreground size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||
className="[&>pre]:bg-background! [&>pre]:text-foreground! size-full overflow-auto dark:hidden [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<div
|
||||
className="[&>pre]:!bg-background [&>pre]:!text-foreground hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||
className="[&>pre]:bg-background! [&>pre]:text-foreground! hidden size-full overflow-auto dark:block [&_code]:font-mono [&_code]:text-sm [&>pre]:m-0 [&>pre]:text-sm [&>pre]:whitespace-pre-wrap"
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
||||
dangerouslySetInnerHTML={{ __html: darkHtml }}
|
||||
/>
|
||||
|
||||
@@ -9,8 +9,8 @@ export type ControlsProps = ComponentProps<typeof ControlsPrimitive>;
|
||||
export const Controls = ({ className, ...props }: ControlsProps) => (
|
||||
<ControlsPrimitive
|
||||
className={cn(
|
||||
"bg-card gap-px overflow-hidden rounded-md border p-1 !shadow-none",
|
||||
"[&>button]:hover:!bg-secondary [&>button]:rounded-md [&>button]:!border-none [&>button]:!bg-transparent",
|
||||
"bg-card gap-px overflow-hidden rounded-md border p-1 shadow-none!",
|
||||
"[&>button]:hover:bg-secondary! [&>button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent!",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import type { StreamdownProps } from "streamdown";
|
||||
|
||||
import { SafeStreamdown } from "@/core/streamdown";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
@@ -304,11 +302,11 @@ export const MessageBranchPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageResponseProps = StreamdownProps;
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<SafeStreamdown
|
||||
<Streamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||
className,
|
||||
|
||||
@@ -36,7 +36,7 @@ export type NodeHeaderProps = ComponentProps<typeof CardHeader>;
|
||||
|
||||
export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
|
||||
<CardHeader
|
||||
className={cn("bg-secondary gap-0.5 rounded-t-md border-b !p-3", className)}
|
||||
className={cn("bg-secondary gap-0.5 rounded-t-md border-b p-3!", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -65,7 +65,7 @@ export type NodeFooterProps = ComponentProps<typeof CardFooter>;
|
||||
|
||||
export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
|
||||
<CardFooter
|
||||
className={cn("bg-secondary rounded-b-md border-t !p-3", className)}
|
||||
className={cn("bg-secondary rounded-b-md border-t p-3!", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -34,14 +34,14 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { PromptInputFilePart, UploadPolicy } from "@/core/uploads";
|
||||
import {
|
||||
DEFAULT_UPLOAD_POLICY,
|
||||
splitUnsupportedUploadFiles,
|
||||
useUploadPolicy,
|
||||
validateChatUploadFiles,
|
||||
} from "@/core/uploads";
|
||||
import { imeEnterAction } from "@/lib/ime";
|
||||
import type { PromptInputFilePart, UploadPolicy } from "@/core/uploads";
|
||||
import {
|
||||
DEFAULT_UPLOAD_POLICY,
|
||||
splitUnsupportedUploadFiles,
|
||||
useUploadPolicy,
|
||||
validateChatUploadFiles,
|
||||
} from "@/core/uploads";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChatStatus } from "ai";
|
||||
import {
|
||||
@@ -61,12 +61,10 @@ import {
|
||||
type ChangeEventHandler,
|
||||
Children,
|
||||
type ClipboardEventHandler,
|
||||
type ComponentProps,
|
||||
type CompositionEventHandler,
|
||||
createContext,
|
||||
type FormEvent,
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FormEvent,
|
||||
type FormEventHandler,
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type KeyboardEventHandler,
|
||||
type PropsWithChildren,
|
||||
@@ -130,11 +128,11 @@ export const usePromptInputController = () => {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Optional variants (do NOT throw). Useful for dual-mode components and for
|
||||
// Fast Refresh transitions where a page can briefly retain the previous
|
||||
// provider context instance.
|
||||
export const useOptionalPromptInputController = () =>
|
||||
useContext(PromptInputController);
|
||||
// Optional variants (do NOT throw). Useful for dual-mode components and for
|
||||
// Fast Refresh transitions where a page can briefly retain the previous
|
||||
// provider context instance.
|
||||
export const useOptionalPromptInputController = () =>
|
||||
useContext(PromptInputController);
|
||||
|
||||
export const useProviderAttachments = () => {
|
||||
const ctx = useContext(ProviderAttachmentsContext);
|
||||
@@ -150,94 +148,94 @@ const useOptionalProviderAttachments = () =>
|
||||
useContext(ProviderAttachmentsContext);
|
||||
const usePromptInputValidation = () => useContext(PromptInputValidationContext);
|
||||
|
||||
export type PromptInputProviderProps = PropsWithChildren<{
|
||||
initialInput?: string;
|
||||
uploadPolicy?: UploadPolicy;
|
||||
}>;
|
||||
export type PromptInputProviderProps = PropsWithChildren<{
|
||||
initialInput?: string;
|
||||
uploadPolicy?: UploadPolicy;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Optional global provider that lifts PromptInput state outside of PromptInput.
|
||||
* If you don't use it, PromptInput stays fully self-managed.
|
||||
*/
|
||||
export function PromptInputProvider({
|
||||
initialInput: initialTextInput = "",
|
||||
uploadPolicy,
|
||||
children,
|
||||
}: PromptInputProviderProps) {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
// ----- textInput state
|
||||
export function PromptInputProvider({
|
||||
initialInput: initialTextInput = "",
|
||||
uploadPolicy,
|
||||
children,
|
||||
}: PromptInputProviderProps) {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
// ----- textInput state
|
||||
const [textInput, setTextInput] = useState(initialTextInput);
|
||||
const clearInput = useCallback(() => setTextInput(""), []);
|
||||
|
||||
// ----- attachments state (global when wrapped)
|
||||
const [attachmentFiles, setAttachmentFiles] = useState<
|
||||
(PromptInputFilePart & { id: string })[]
|
||||
>([]);
|
||||
const attachmentsRef = useRef(attachmentFiles);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const openRef = useRef<() => void>(() => {});
|
||||
|
||||
const add = useCallback((files: File[] | FileList) => {
|
||||
const incoming = Array.from(files);
|
||||
>([]);
|
||||
const attachmentsRef = useRef(attachmentFiles);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const openRef = useRef<() => void>(() => {});
|
||||
|
||||
const add = useCallback((files: File[] | FileList) => {
|
||||
const incoming = Array.from(files);
|
||||
if (incoming.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = attachmentsRef.current;
|
||||
const existing = current.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
incoming,
|
||||
existing,
|
||||
effectiveUploadPolicy,
|
||||
);
|
||||
for (const message of new Set(
|
||||
validation.issues.map((issue) => issue.message),
|
||||
)) {
|
||||
toast.error(message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
const next = current.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, [effectiveUploadPolicy]);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
const current = attachmentsRef.current;
|
||||
const found = current.find((f) => f.id === id);
|
||||
if (found?.url) URL.revokeObjectURL(found.url);
|
||||
const next = current.filter((f) => f.id !== id);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
for (const f of attachmentsRef.current) {
|
||||
if (f.url) {
|
||||
URL.revokeObjectURL(f.url);
|
||||
}
|
||||
}
|
||||
attachmentsRef.current = [];
|
||||
setAttachmentFiles([]);
|
||||
}, []);
|
||||
|
||||
// Keep a ref to attachments for cleanup on unmount (avoids stale closure)
|
||||
attachmentsRef.current = attachmentFiles;
|
||||
return;
|
||||
}
|
||||
|
||||
const current = attachmentsRef.current;
|
||||
const existing = current.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
incoming,
|
||||
existing,
|
||||
effectiveUploadPolicy,
|
||||
);
|
||||
for (const message of new Set(
|
||||
validation.issues.map((issue) => issue.message),
|
||||
)) {
|
||||
toast.error(message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
const next = current.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, [effectiveUploadPolicy]);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
const current = attachmentsRef.current;
|
||||
const found = current.find((f) => f.id === id);
|
||||
if (found?.url) URL.revokeObjectURL(found.url);
|
||||
const next = current.filter((f) => f.id !== id);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
for (const f of attachmentsRef.current) {
|
||||
if (f.url) {
|
||||
URL.revokeObjectURL(f.url);
|
||||
}
|
||||
}
|
||||
attachmentsRef.current = [];
|
||||
setAttachmentFiles([]);
|
||||
}, []);
|
||||
|
||||
// Keep a ref to attachments for cleanup on unmount (avoids stale closure)
|
||||
attachmentsRef.current = attachmentFiles;
|
||||
|
||||
// Cleanup blob URLs on unmount to prevent memory leaks
|
||||
useEffect(() => {
|
||||
@@ -432,14 +430,14 @@ export function PromptInputAttachments({
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
|
||||
{...props}
|
||||
>
|
||||
{attachments.files.map((file) => (
|
||||
<div key={file.id} className="max-w-60">
|
||||
{children(file)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{...props}
|
||||
>
|
||||
{attachments.files.map((file) => (
|
||||
<div key={file.id} className="max-w-60">
|
||||
{children(file)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -485,17 +483,17 @@ export type PromptInputProps = Omit<
|
||||
// Render a hidden input with given name and keep it in sync for native form posts. Default false.
|
||||
syncHiddenInput?: boolean;
|
||||
// Minimal constraints
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // bytes
|
||||
uploadPolicy?: UploadPolicy;
|
||||
onError?: (err: {
|
||||
code:
|
||||
| "max_files"
|
||||
| "max_file_size"
|
||||
| "max_total_size"
|
||||
| "unsupported_type"
|
||||
| "accept"
|
||||
| "unsupported_package";
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // bytes
|
||||
uploadPolicy?: UploadPolicy;
|
||||
onError?: (err: {
|
||||
code:
|
||||
| "max_files"
|
||||
| "max_file_size"
|
||||
| "max_total_size"
|
||||
| "unsupported_type"
|
||||
| "accept"
|
||||
| "unsupported_package";
|
||||
message: string;
|
||||
}) => void;
|
||||
onSubmit: (
|
||||
@@ -511,25 +509,25 @@ export const PromptInput = ({
|
||||
multiple,
|
||||
globalDrop,
|
||||
syncHiddenInput,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
uploadPolicy,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
uploadPolicy,
|
||||
onError,
|
||||
onSubmit,
|
||||
children,
|
||||
...props
|
||||
}: PromptInputProps) => {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
const policyAccept = useMemo(
|
||||
() =>
|
||||
Object.values(effectiveUploadPolicy.groups)
|
||||
.flatMap((group) => group.extensions)
|
||||
.join(","),
|
||||
[effectiveUploadPolicy],
|
||||
);
|
||||
// Try to use a provider controller if present
|
||||
}: PromptInputProps) => {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
const policyAccept = useMemo(
|
||||
() =>
|
||||
Object.values(effectiveUploadPolicy.groups)
|
||||
.flatMap((group) => group.extensions)
|
||||
.join(","),
|
||||
[effectiveUploadPolicy],
|
||||
);
|
||||
// Try to use a provider controller if present
|
||||
const controller = useOptionalPromptInputController();
|
||||
const usingProvider = !!controller;
|
||||
|
||||
@@ -562,11 +560,11 @@ export const PromptInput = ({
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return patterns.some((pattern) => {
|
||||
if (pattern.startsWith(".")) {
|
||||
return f.name.toLowerCase().endsWith(pattern.toLowerCase());
|
||||
}
|
||||
if (pattern.endsWith("/*")) {
|
||||
return patterns.some((pattern) => {
|
||||
if (pattern.startsWith(".")) {
|
||||
return f.name.toLowerCase().endsWith(pattern.toLowerCase());
|
||||
}
|
||||
if (pattern.endsWith("/*")) {
|
||||
const prefix = pattern.slice(0, -1); // e.g: image/* -> image/
|
||||
return f.type.startsWith(prefix);
|
||||
}
|
||||
@@ -576,72 +574,72 @@ export const PromptInput = ({
|
||||
[accept],
|
||||
);
|
||||
|
||||
const addLocal = useCallback(
|
||||
(fileList: File[] | FileList) => {
|
||||
const incoming = Array.from(fileList);
|
||||
const accepted = incoming.filter((f) => matchesAccept(f));
|
||||
if (accepted.length < incoming.length) {
|
||||
const error = {
|
||||
code: "accept",
|
||||
message: "部分文件不符合当前输入框允许的格式。",
|
||||
} as const;
|
||||
onError?.(error);
|
||||
if (!onError) toast.error(error.message);
|
||||
}
|
||||
const localPolicy: UploadPolicy = {
|
||||
...effectiveUploadPolicy,
|
||||
max_files_per_message:
|
||||
typeof maxFiles === "number"
|
||||
? Math.min(effectiveUploadPolicy.max_files_per_message, maxFiles)
|
||||
: effectiveUploadPolicy.max_files_per_message,
|
||||
groups: Object.fromEntries(
|
||||
Object.entries(effectiveUploadPolicy.groups).map(([key, group]) => [
|
||||
key,
|
||||
{
|
||||
...group,
|
||||
max_bytes:
|
||||
typeof maxFileSize === "number"
|
||||
? Math.min(group.max_bytes, maxFileSize)
|
||||
: group.max_bytes,
|
||||
},
|
||||
]),
|
||||
) as UploadPolicy["groups"],
|
||||
};
|
||||
const existing = items.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
accepted,
|
||||
existing,
|
||||
localPolicy,
|
||||
);
|
||||
for (const issue of Array.from(
|
||||
new Map(
|
||||
validation.issues.map((item) => [item.message, item]),
|
||||
).values(),
|
||||
)) {
|
||||
onError?.({ code: issue.code, message: issue.message });
|
||||
if (!onError) toast.error(issue.message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
setItems((prev) =>
|
||||
prev.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
),
|
||||
);
|
||||
},
|
||||
[effectiveUploadPolicy, items, matchesAccept, maxFiles, maxFileSize, onError],
|
||||
);
|
||||
const addLocal = useCallback(
|
||||
(fileList: File[] | FileList) => {
|
||||
const incoming = Array.from(fileList);
|
||||
const accepted = incoming.filter((f) => matchesAccept(f));
|
||||
if (accepted.length < incoming.length) {
|
||||
const error = {
|
||||
code: "accept",
|
||||
message: "部分文件不符合当前输入框允许的格式。",
|
||||
} as const;
|
||||
onError?.(error);
|
||||
if (!onError) toast.error(error.message);
|
||||
}
|
||||
const localPolicy: UploadPolicy = {
|
||||
...effectiveUploadPolicy,
|
||||
max_files_per_message:
|
||||
typeof maxFiles === "number"
|
||||
? Math.min(effectiveUploadPolicy.max_files_per_message, maxFiles)
|
||||
: effectiveUploadPolicy.max_files_per_message,
|
||||
groups: Object.fromEntries(
|
||||
Object.entries(effectiveUploadPolicy.groups).map(([key, group]) => [
|
||||
key,
|
||||
{
|
||||
...group,
|
||||
max_bytes:
|
||||
typeof maxFileSize === "number"
|
||||
? Math.min(group.max_bytes, maxFileSize)
|
||||
: group.max_bytes,
|
||||
},
|
||||
]),
|
||||
) as UploadPolicy["groups"],
|
||||
};
|
||||
const existing = items.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
accepted,
|
||||
existing,
|
||||
localPolicy,
|
||||
);
|
||||
for (const issue of Array.from(
|
||||
new Map(
|
||||
validation.issues.map((item) => [item.message, item]),
|
||||
).values(),
|
||||
)) {
|
||||
onError?.({ code: issue.code, message: issue.message });
|
||||
if (!onError) toast.error(issue.message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
setItems((prev) =>
|
||||
prev.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
),
|
||||
);
|
||||
},
|
||||
[effectiveUploadPolicy, items, matchesAccept, maxFiles, maxFileSize, onError],
|
||||
);
|
||||
|
||||
const removeLocal = useCallback(
|
||||
(id: string) =>
|
||||
@@ -829,17 +827,11 @@ export const PromptInput = ({
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
// Prefer the live textarea. Firefox 102 applies the chosen candidate to
|
||||
// the DOM before React state catches up, and a send click in that gap
|
||||
// would otherwise submit the previous pinyin-less value.
|
||||
const liveText = form.querySelector<HTMLTextAreaElement>(
|
||||
"textarea[name='message']",
|
||||
)?.value;
|
||||
const text = usingProvider
|
||||
? (liveText ?? controller.textInput.value)
|
||||
? controller.textInput.value
|
||||
: (() => {
|
||||
const formData = new FormData(form);
|
||||
return (formData.get("message") as string) || liveText || "";
|
||||
return (formData.get("message") as string) || "";
|
||||
})();
|
||||
|
||||
// Reset form immediately after capturing text to avoid race condition
|
||||
@@ -902,7 +894,7 @@ export const PromptInput = ({
|
||||
const inner = (
|
||||
<PromptInputValidationContext.Provider value={sanitizeIncomingFiles}>
|
||||
<input
|
||||
accept={accept ?? policyAccept}
|
||||
accept={accept ?? policyAccept}
|
||||
aria-label="Upload files"
|
||||
className="hidden"
|
||||
multiple={multiple}
|
||||
@@ -917,12 +909,10 @@ export const PromptInput = ({
|
||||
ref={formRef}
|
||||
{...props}
|
||||
>
|
||||
{/* Do not rely on InputGroup's :has() selectors here: Chrome versions
|
||||
before 105 ignore them, keep the horizontal flex layout, and
|
||||
squeeze the textarea beside the footer controls. */}
|
||||
<InputGroup className="h-auto min-h-16 flex-col items-stretch">
|
||||
{children}
|
||||
</InputGroup>
|
||||
{/* Do not rely on InputGroup's :has(textarea) selector here: Chrome
|
||||
versions before 105 ignore :has(), retain h-9, and clip the whole
|
||||
multi-line composer to a single row. */}
|
||||
<InputGroup className="h-auto min-h-16">{children}</InputGroup>
|
||||
</form>
|
||||
</PromptInputValidationContext.Provider>
|
||||
);
|
||||
@@ -949,100 +939,58 @@ export type PromptInputTextareaProps = ComponentProps<
|
||||
typeof InputGroupTextarea
|
||||
>;
|
||||
|
||||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
onInput,
|
||||
onKeyDown,
|
||||
onCompositionStart,
|
||||
onCompositionEnd,
|
||||
className,
|
||||
placeholder = "What would you like to know?",
|
||||
defaultValue,
|
||||
...props
|
||||
}: PromptInputTextareaProps) => {
|
||||
const controller = useOptionalPromptInputController();
|
||||
const attachments = usePromptInputAttachments();
|
||||
const sanitizeIncomingFiles = usePromptInputValidation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const composingRef = useRef(false);
|
||||
const compositionEndedAtRef = useRef(0);
|
||||
const onChangeRef = useRef(onChange);
|
||||
const onInputRef = useRef(onInput);
|
||||
const onKeyDownRef = useRef(onKeyDown);
|
||||
const onCompositionStartRef = useRef(onCompositionStart);
|
||||
const onCompositionEndRef = useRef(onCompositionEnd);
|
||||
const setInputRef = useRef(controller?.textInput.setInput);
|
||||
const attachmentsRef = useRef(attachments);
|
||||
onChangeRef.current = onChange;
|
||||
onInputRef.current = onInput;
|
||||
onKeyDownRef.current = onKeyDown;
|
||||
onCompositionStartRef.current = onCompositionStart;
|
||||
onCompositionEndRef.current = onCompositionEnd;
|
||||
setInputRef.current = controller?.textInput.setInput;
|
||||
attachmentsRef.current = attachments;
|
||||
|
||||
const publishValue = useCallback((value: string) => {
|
||||
setInputRef.current?.(value);
|
||||
}, []);
|
||||
|
||||
const resizeForLegacyChrome = useCallback((textarea: HTMLTextAreaElement) => {
|
||||
// Firefox 102 commits an open IME composition as raw pinyin when the
|
||||
// textarea's height or overflow changes mid-composition.
|
||||
if (composingRef.current) return;
|
||||
if (
|
||||
typeof CSS !== "undefined" &&
|
||||
typeof CSS.supports === "function" &&
|
||||
CSS.supports("field-sizing", "content")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome 109 ignores field-sizing: content. Preserve the same 4rem–12rem
|
||||
// autosize range used by min-h-16/max-h-48 instead of leaving the prompt
|
||||
// stuck at its minimum height.
|
||||
textarea.style.height = "auto";
|
||||
const height = Math.max(64, Math.min(textarea.scrollHeight, 192));
|
||||
textarea.style.height = `${height}px`;
|
||||
textarea.style.overflowY = textarea.scrollHeight > 192 ? "auto" : "hidden";
|
||||
}, []);
|
||||
|
||||
// onInput does not run for a controlled value filled by a suggested question
|
||||
// or restored draft. Resize once after every render in legacy Chrome too.
|
||||
useEffect(() => {
|
||||
if (composingRef.current) return;
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
resizeForLegacyChrome(textarea);
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [controller?.textInput.value, resizeForLegacyChrome]);
|
||||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
onInput,
|
||||
className,
|
||||
placeholder = "What would you like to know?",
|
||||
...props
|
||||
}: PromptInputTextareaProps) => {
|
||||
const controller = useOptionalPromptInputController();
|
||||
const attachments = usePromptInputAttachments();
|
||||
const sanitizeIncomingFiles = usePromptInputValidation();
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const resizeForLegacyChrome = useCallback((textarea: HTMLTextAreaElement) => {
|
||||
if (
|
||||
typeof CSS !== "undefined" &&
|
||||
typeof CSS.supports === "function" &&
|
||||
CSS.supports("field-sizing", "content")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chrome 109 ignores field-sizing: content. Preserve the same 4rem–12rem
|
||||
// autosize range used by min-h-16/max-h-48 instead of leaving the prompt
|
||||
// stuck at its minimum height.
|
||||
textarea.style.height = "auto";
|
||||
const height = Math.max(64, Math.min(textarea.scrollHeight, 192));
|
||||
textarea.style.height = `${height}px`;
|
||||
textarea.style.overflowY = textarea.scrollHeight > 192 ? "auto" : "hidden";
|
||||
}, []);
|
||||
|
||||
// onInput does not run for a controlled value filled by a suggested question
|
||||
// or restored draft. Resize once after every render in legacy Chrome too.
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
resizeForLegacyChrome(textarea);
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [controller?.textInput.value, resizeForLegacyChrome]);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const action = imeEnterAction({
|
||||
isComposing: composingRef.current,
|
||||
nativeIsComposing: e.nativeEvent.isComposing || e.key === "Process",
|
||||
keyCode: e.keyCode,
|
||||
compositionEndedAt: compositionEndedAtRef.current,
|
||||
now: performance.now(),
|
||||
});
|
||||
// Firefox 102 confirms the candidate on this key. preventDefault here
|
||||
// cancels that confirmation and leaves the pinyin in the box.
|
||||
if (action === "ignore") {
|
||||
if (isIMEComposing(e, isComposing)) {
|
||||
return;
|
||||
}
|
||||
onKeyDownRef.current?.(e);
|
||||
if (e.defaultPrevented || e.shiftKey) {
|
||||
if (e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
// The confirming Enter already finished the candidate. Swallow it so
|
||||
// Firefox does not also insert a newline or send the message.
|
||||
e.preventDefault();
|
||||
if (action === "swallow") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the submit button is disabled before submitting
|
||||
const form = e.currentTarget.form;
|
||||
@@ -1054,59 +1002,22 @@ export const PromptInputTextarea = ({
|
||||
}
|
||||
|
||||
form?.requestSubmit();
|
||||
return;
|
||||
}
|
||||
|
||||
onKeyDownRef.current?.(e);
|
||||
if (e.defaultPrevented) return;
|
||||
|
||||
if (composingRef.current) return;
|
||||
|
||||
// Remove last attachment when Backspace is pressed and textarea is empty
|
||||
if (
|
||||
e.key === "Backspace" &&
|
||||
e.currentTarget.value === "" &&
|
||||
attachmentsRef.current.files.length > 0
|
||||
attachments.files.length > 0
|
||||
) {
|
||||
e.preventDefault();
|
||||
const lastAttachment = attachmentsRef.current.files.at(-1);
|
||||
const lastAttachment = attachments.files.at(-1);
|
||||
if (lastAttachment) {
|
||||
attachmentsRef.current.remove(lastAttachment.id);
|
||||
attachments.remove(lastAttachment.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompositionStart: CompositionEventHandler<HTMLTextAreaElement> = (
|
||||
event,
|
||||
) => {
|
||||
composingRef.current = true;
|
||||
compositionEndedAtRef.current = 0;
|
||||
onCompositionStartRef.current?.(event);
|
||||
};
|
||||
|
||||
const handleCompositionEnd: CompositionEventHandler<HTMLTextAreaElement> = (
|
||||
event,
|
||||
) => {
|
||||
onCompositionEndRef.current?.(event);
|
||||
composingRef.current = false;
|
||||
compositionEndedAtRef.current = performance.now();
|
||||
const el = event.currentTarget;
|
||||
// Firefox 102 dispatches compositionend before the committed candidate is
|
||||
// visible, and before the input event that carries it. Syncing the
|
||||
// compositionend value here would write the raw pinyin back over 汉字.
|
||||
window.setTimeout(() => {
|
||||
if (el.isConnected) publishValue(el.value);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
// Keep the controlled value identical to the DOM, including the pinyin
|
||||
// still being composed. Otherwise React restores the previous value at
|
||||
// the end of the input event and Firefox 102 cannot replace it.
|
||||
publishValue(event.currentTarget.value);
|
||||
onChangeRef.current?.(event);
|
||||
};
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
|
||||
const items = event.clipboardData?.items;
|
||||
|
||||
@@ -1136,28 +1047,35 @@ export const PromptInputTextarea = ({
|
||||
}
|
||||
};
|
||||
|
||||
const controlledProps = controller
|
||||
? {
|
||||
value: controller.textInput.value,
|
||||
onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
controller.textInput.setInput(e.currentTarget.value);
|
||||
onChange?.(e);
|
||||
},
|
||||
}
|
||||
: {
|
||||
onChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroupTextarea
|
||||
{...props}
|
||||
className={cn("field-sizing-content max-h-48 min-h-16", className)}
|
||||
name="message"
|
||||
ref={textareaRef}
|
||||
rows={3}
|
||||
onChange={handleChange}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={(event) => {
|
||||
if (!composingRef.current) {
|
||||
resizeForLegacyChrome(event.currentTarget);
|
||||
}
|
||||
onInputRef.current?.(event);
|
||||
}}
|
||||
<InputGroupTextarea
|
||||
className={cn("field-sizing-content max-h-48 min-h-16", className)}
|
||||
name="message"
|
||||
ref={textareaRef}
|
||||
rows={3}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={(event) => {
|
||||
resizeForLegacyChrome(event.currentTarget);
|
||||
onInput?.(event);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
placeholder={placeholder}
|
||||
{...(controller
|
||||
? { value: controller.textInput.value }
|
||||
: { defaultValue })}
|
||||
{...props}
|
||||
{...controlledProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1203,43 +1121,43 @@ export const PromptInputTools = ({
|
||||
<div className={cn("flex items-center gap-1", className)} {...props} />
|
||||
);
|
||||
|
||||
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
||||
|
||||
export const PromptInputButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
PromptInputButtonProps
|
||||
>(({ variant = "ghost", className, size, ...props }, ref) => (
|
||||
<InputGroupButton
|
||||
ref={ref}
|
||||
className={cn(className)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
PromptInputButton.displayName = "PromptInputButton";
|
||||
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
|
||||
|
||||
export const PromptInputButton = ({
|
||||
variant = "ghost",
|
||||
className,
|
||||
size,
|
||||
...props
|
||||
}: PromptInputButtonProps) => {
|
||||
return (
|
||||
<InputGroupButton
|
||||
className={cn(className)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
|
||||
export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
|
||||
<DropdownMenu {...props} />
|
||||
);
|
||||
|
||||
export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
|
||||
|
||||
export const PromptInputActionMenuTrigger = forwardRef<
|
||||
HTMLButtonElement,
|
||||
PromptInputActionMenuTriggerProps
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<PromptInputButton ref={ref} className={className} {...props}>
|
||||
{children ?? <PlusIcon className="size-4" />}
|
||||
</PromptInputButton>
|
||||
</DropdownMenuTrigger>
|
||||
));
|
||||
|
||||
PromptInputActionMenuTrigger.displayName = "PromptInputActionMenuTrigger";
|
||||
export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
|
||||
|
||||
export const PromptInputActionMenuTrigger = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: PromptInputActionMenuTriggerProps) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<PromptInputButton className={className} {...props}>
|
||||
{children ?? <PlusIcon className="size-4" />}
|
||||
</PromptInputButton>
|
||||
</DropdownMenuTrigger>
|
||||
);
|
||||
|
||||
export type PromptInputActionMenuContentProps = ComponentProps<
|
||||
typeof DropdownMenuContent
|
||||
|
||||
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
|
||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import { SafeStreamdown } from "@/core/streamdown";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { reasoningPlugins } from "@/core/streamdown/plugins";
|
||||
import { Shimmer } from "./shimmer";
|
||||
|
||||
@@ -179,7 +179,7 @@ export const ReasoningContent = memo(
|
||||
{...props}
|
||||
>
|
||||
{typeof children === "string" ? (
|
||||
<SafeStreamdown {...reasoningPlugins}>{children}</SafeStreamdown>
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -16,8 +16,6 @@ import {
|
||||
Settings,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
ScrollText,
|
||||
Timer,
|
||||
User,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
@@ -270,22 +268,6 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/skill-timing")}
|
||||
>
|
||||
<Timer className="size-4" />
|
||||
技能耗时排查
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/model-exchanges")}
|
||||
>
|
||||
<ScrollText className="size-4" />
|
||||
模型收发记录
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
@@ -605,22 +587,6 @@ function LegacyAccordionSidebar({
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/skill-timing")}
|
||||
>
|
||||
<Timer className="size-4" />
|
||||
技能耗时排查
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/model-exchanges")}
|
||||
>
|
||||
<ScrollText className="size-4" />
|
||||
模型收发记录
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { ReactUiErrorBoundary } from "./react-ui-error-boundary";
|
||||
|
||||
// Offline browser QA: real React production render/commit recursion, not a
|
||||
// fabricated #185 thrown by a unit-test stub. A manual button click reloads
|
||||
// this same page without submitting any conversation or model request.
|
||||
const key = "deerflow.boundary-browser-check";
|
||||
let mounts = 0;
|
||||
function InfiniteCommitLoop() {
|
||||
const [, update] = useState(0);
|
||||
useState(() => { mounts += 1; return 0; });
|
||||
useLayoutEffect(() => { update((value) => value + 1); });
|
||||
return <div>Loop fixture</div>;
|
||||
}
|
||||
|
||||
function result(passed: boolean, details: Record<string, unknown>) {
|
||||
const output = document.createElement("pre");
|
||||
output.id = "boundary-check-result";
|
||||
output.textContent = JSON.stringify({ passed, ...details });
|
||||
document.body.append(output);
|
||||
}
|
||||
|
||||
const resumed = sessionStorage.getItem(key) === "manual-reload";
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ReactUiErrorBoundary>
|
||||
{resumed ? <div id="healthy">Existing page restored</div> : <InfiniteCommitLoop />}
|
||||
</ReactUiErrorBoundary>,
|
||||
);
|
||||
|
||||
if (resumed) {
|
||||
sessionStorage.removeItem(key);
|
||||
window.setTimeout(() => result(Boolean(document.getElementById("healthy")), {
|
||||
phase: "manual-reload-restored", renderLoopCaught: true, noAutomaticRemount: true,
|
||||
}), 100);
|
||||
} else {
|
||||
window.setTimeout(() => {
|
||||
const alert = document.querySelector('[role="alert"]');
|
||||
const before = mounts;
|
||||
if (!alert || /#185|react\.dev|Maximum update depth/.test(alert.textContent ?? "")) {
|
||||
result(false, { phase: "friendly-fallback", hasAlert: Boolean(alert) });
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (mounts !== before || before !== 1) {
|
||||
result(false, { phase: "unexpected-automatic-remount", mounts, before });
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(key, "manual-reload");
|
||||
(alert.querySelector("button") as HTMLButtonElement).click();
|
||||
}, 250);
|
||||
}, 100);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
import { isReactMaximumUpdateDepthError } from "@/core/threads/stream-error-classification";
|
||||
import { REACT_UI_ERROR_MESSAGE } from "@/core/threads/react-ui-error";
|
||||
|
||||
/** Last-resort render/commit guard. It deliberately never auto-remounts a broken
|
||||
* tree, which could otherwise repeatedly resume the same run and crash again. */
|
||||
export class ReactUiErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ failed: boolean }
|
||||
> {
|
||||
state = { failed: false };
|
||||
|
||||
static getDerivedStateFromError(error: unknown) {
|
||||
if (!isReactMaximumUpdateDepthError(error)) throw error;
|
||||
return { failed: true };
|
||||
}
|
||||
|
||||
componentDidCatch(_error: unknown, info: ErrorInfo) {
|
||||
console.warn("[deer-flow] react_ui_render_error", {
|
||||
errorType: "react_maximum_update_depth",
|
||||
componentStack: info.componentStack,
|
||||
recovery: "manual_reload",
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.failed) return this.props.children;
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center p-6">
|
||||
<section role="alert" className="max-w-lg rounded-lg border p-6 text-center">
|
||||
<h1 className="mb-3 text-lg font-medium">页面暂时无法显示</h1>
|
||||
<p className="mb-5 text-sm text-muted-foreground">{REACT_UI_ERROR_MESSAGE}</p>
|
||||
<button
|
||||
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
|
||||
onClick={() => window.location.reload()}
|
||||
type="button"
|
||||
>
|
||||
重新载入页面
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ const DialogOverlay = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"legacy-dialog-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-[200] bg-black/50",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -71,42 +71,37 @@ const DialogOverlay = React.forwardRef<
|
||||
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
/** Use when this dialog is opened from a higher-layer surface such as a Drawer. */
|
||||
overlayClassName?: string;
|
||||
}
|
||||
>(({ className, children, showCloseButton = true, overlayClassName, ...props }, ref) => {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<div
|
||||
data-slot="dialog-viewport"
|
||||
className={cn("legacy-dialog-viewport", overlayClassName)}
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
/** Use when this dialog is opened from a higher-layer surface such as a Drawer. */
|
||||
overlayClassName?: string;
|
||||
}
|
||||
>(({ className, children, showCloseButton = true, overlayClassName, ...props }, ref) => {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay className={overlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 relative z-[200] grid min-h-0 max-h-full w-full max-w-[calc(100%-2rem)] pointer-events-auto gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</div>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,42 +20,42 @@ function DropdownMenuPortal({
|
||||
);
|
||||
}
|
||||
|
||||
const DropdownMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
||||
>(({ ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const DropdownMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
||||
>(({ ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
|
||||
|
||||
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"legacy-floating-layer bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
@@ -65,29 +65,29 @@ function DropdownMenuGroup({
|
||||
);
|
||||
}
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(({ className, inset, variant = "default", ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(({ className, inset, variant = "default", ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
@@ -205,51 +205,51 @@ function DropdownMenuSub({
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
});
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"legacy-floating-layer bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
|
||||
@@ -38,7 +38,7 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-[var(--radix-hover-card-content-transform-origin)] rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -97,22 +97,24 @@ const inputGroupButtonVariants = cva(
|
||||
},
|
||||
);
|
||||
|
||||
const InputGroupButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
Omit<React.ComponentPropsWithoutRef<typeof Button>, "size"> &
|
||||
VariantProps<typeof inputGroupButtonVariants>
|
||||
>(({ className, type = "button", variant = "ghost", size = "xs", ...props }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
InputGroupButton.displayName = "InputGroupButton";
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
||||
VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
|
||||
@@ -151,8 +151,8 @@
|
||||
padding: 6px;
|
||||
background: radial-gradient(
|
||||
var(--glow-radius) circle at var(--glow-x) var(--glow-y),
|
||||
rgba(132, 0, 255, 0.8) 0%,
|
||||
rgba(132, 0, 255, 0.4) 30%,
|
||||
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
|
||||
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
|
||||
transparent 60%
|
||||
);
|
||||
border-radius: inherit;
|
||||
@@ -165,7 +165,7 @@
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
opacity: var(--glow-intensity);
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const SelectContent = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"legacy-floating-layer bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
||||
@@ -178,7 +178,7 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-[var(--sidebar-width)] flex-col",
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -195,7 +195,7 @@ function Sidebar({
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-[var(--sidebar-width)] p-0 [&>button]:hidden"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
@@ -226,25 +226,25 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-[var(--sidebar-width)] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem)]"
|
||||
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]",
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed bottom-0 z-10 hidden w-[var(--sidebar-width)] transition-[left,right,width] duration-200 ease-linear md:flex [top:var(--header-height,4rem)]",
|
||||
"fixed bottom-0 z-10 hidden w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex [top:var(--header-height,4rem)]",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -486,7 +486,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding,background-color,box-shadow,transform] duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:!size-10 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:rounded-2xl group-data-[collapsible=icon]:!p-0 group-data-[collapsible=icon]:data-[active=true]:bg-[#fff1e8] group-data-[collapsible=icon]:data-[active=true]:text-primary group-data-[collapsible=icon]:data-[active=true]:shadow-[inset_0_0_0_1px_rgba(217,33,33,0.12),0_10px_24px_-18px_rgba(217,33,33,0.42)] group-data-[collapsible=icon]:[&>span]:hidden group-data-[collapsible=icon]:[&>svg]:mx-auto group-data-[collapsible=icon]:[&>svg]:size-5 [&>span:last-child]:truncate [&>svg]:size-[18px] [&>svg]:shrink-0",
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding,background-color,box-shadow,transform] duration-200 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:size-10! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:rounded-2xl group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:data-[active=true]:bg-[#fff1e8] group-data-[collapsible=icon]:data-[active=true]:text-primary group-data-[collapsible=icon]:data-[active=true]:shadow-[inset_0_0_0_1px_rgba(217,33,33,0.12),0_10px_24px_-18px_rgba(217,33,33,0.42)] group-data-[collapsible=icon]:[&>span]:hidden group-data-[collapsible=icon]:[&>svg]:mx-auto group-data-[collapsible=icon]:[&>svg]:size-5 [&>span:last-child]:truncate [&>svg]:size-[18px] [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -497,7 +497,7 @@ const sidebarMenuButtonVariants = cva(
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -649,7 +649,7 @@ function SidebarMenuSkeleton({
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[var(--skeleton-width)] flex-1"
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
|
||||
@@ -31,9 +31,9 @@ const tabsListVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-muted dark:group-data-[orientation=horizontal]/tabs:!h-8 dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||
line:
|
||||
"gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:!h-8 dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||
"gap-1 bg-transparent dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-visible dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -36,7 +36,7 @@ function ToggleGroup({
|
||||
data-spacing={spacing}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[calc(var(--gap)*0.25rem)] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||