Compare commits

..
1546 changed files with 229925 additions and 8051 deletions

No files matched your search

+55 -2
View File
@@ -23,6 +23,8 @@ frontend-web/.pnpm-store/
frontend-web/tsconfig.tsbuildinfo frontend-web/tsconfig.tsbuildinfo
frontend-web/vite-dev.log frontend-web/vite-dev.log
frontend-web/vite-dev.err.log frontend-web/vite-dev.err.log
frontend-web/.rspack-cache/
frontend-web/dist-firefox52/
# Frontend local packages/archives/docs (non-source) # Frontend local packages/archives/docs (non-source)
frontend-web/pnpm-lock.yaml frontend-web/pnpm-lock.yaml
@@ -71,6 +73,7 @@ offline-backend-20260512/backend/.deer-flow/data/*.db-*
offline-backend-20260512/backend/.deer-flow/* offline-backend-20260512/backend/.deer-flow/*
offline-backend-20260512/backend/.env offline-backend-20260512/backend/.env
offline-backend-20260512/backend/.env.local offline-backend-20260512/backend/.env.local
offline-backend-20260512/backend/.extensions_config.json.lock
offline-backend-20260512/start_backend.sh offline-backend-20260512/start_backend.sh
offline-backend-20260512/stop_backend.sh offline-backend-20260512/stop_backend.sh
offline-backend-20260512/check_backend.sh offline-backend-20260512/check_backend.sh
@@ -88,20 +91,45 @@ skills/
hermes-agent-main/ hermes-agent-main/
.gitignore .gitignore
offline-backend-20260512/backend/tests/* 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/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. # 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_gauss_database.py
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.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_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 !offline-backend-20260512/backend/tests/test_health_auth_bypass.py
# Keep formal Word OOXML and embedded-font regression coverage versioned. # Keep formal Word OOXML and embedded-font regression coverage versioned.
!offline-backend-20260512/backend/tests/test_word_export.py !offline-backend-20260512/backend/tests/test_word_export.py
# Keep generated-file library aggregation and filtering regression coverage versioned. # Keep generated-file library aggregation and filtering regression coverage versioned.
!offline-backend-20260512/backend/tests/test_artifact_library.py !offline-backend-20260512/backend/tests/test_artifact_library.py
# Keep Web App site artifact (multi-page site) regression coverage versioned.
!offline-backend-20260512/backend/tests/test_web_app_sites.py
# Keep the model configuration regression coverage versioned even though # Keep the model configuration regression coverage versioned even though
# this deployment repository otherwise ignores newly added backend tests. # this deployment repository otherwise ignores newly added backend tests.
!offline-backend-20260512/backend/tests/test_model_management.py !offline-backend-20260512/backend/tests/test_model_management.py
# Keep vLLM reasoning/reasoning_content field compatibility regressions versioned.
!offline-backend-20260512/backend/tests/test_reasoning_fields.py
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned. # Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
!offline-backend-20260512/backend/tests/test_forced_research_agent.py !offline-backend-20260512/backend/tests/test_forced_research_agent.py
# Keep strategy-selection built-in agent seed and safety-contract regressions versioned. # Keep strategy-selection built-in agent seed and safety-contract regressions versioned.
@@ -114,17 +142,26 @@ offline-backend-20260512/backend/tests/*
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py !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_vector_search.py
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.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_encoder_runtime.py
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.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_llmwiki_wiki_retrieval.py
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.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. # Deep-research reliability regression coverage.
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py !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_event_sink.py
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.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_deep_research_start_job.py
!offline-backend-20260512/backend/tests/test_summarization_human_message.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 !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. # Independent enterprise-research persistence, HTTP and race regressions.
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py !offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts). # Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
@@ -145,6 +182,10 @@ offline-backend-20260512/backend/tests/*
!offline-backend-20260512/backend/tests/test_writing_rewrite_prompt.py !offline-backend-20260512/backend/tests/test_writing_rewrite_prompt.py
# LLM call timeout retry classification regression coverage. # LLM call timeout retry classification regression coverage.
!offline-backend-20260512/backend/tests/test_llm_error_handling_middleware.py !offline-backend-20260512/backend/tests/test_llm_error_handling_middleware.py
# Adaptive context recovery, large-payload and real-adapter streaming regressions.
!offline-backend-20260512/backend/tests/test_context_recovery.py
!offline-backend-20260512/backend/tests/test_context_recovery_transport.py
!offline-backend-20260512/backend/tests/test_summarization_multiturn.py
# Writing-mode pipeline conversation-material regressions. # Writing-mode pipeline conversation-material regressions.
!offline-backend-20260512/backend/tests/test_deep_research_report_tool.py !offline-backend-20260512/backend/tests/test_deep_research_report_tool.py
# Report collaboration (RC-BE-000–018) regression coverage. # Report collaboration (RC-BE-000–018) regression coverage.
@@ -181,10 +222,19 @@ docs/backups
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above) # Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
!frontend-web/src/components/workspace/skills/ !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) # uv lockfile (not tracked per project preference)
offline-backend-20260512/backend/uv.lock offline-backend-20260512/backend/uv.lock
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.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) # Local-only scratch directory (not committed)
0622/ 0622/
@@ -194,3 +244,6 @@ __pycache__/
# misc # misc
.DS_Store .DS_Store
# Local Firefox 52 test profiles
.firefox52-profile*/
@@ -0,0 +1,113 @@
# Web App 产物模式:多页面站点 / 流程图 / 地图(可降级 + 独立页面风格)
## 总体架构
让智能体在沙箱里产出**完整多页面站点**(site.json 清单 + src/pages + public 数据),平台统一构建(预置 node_modules,永不 npm install)与受控静态托管,前端新注册一个**风格独立**的「站点预览」路由页。
**两条铁律(本次新增的核心原则)**:
1. **零侵入**:新页面不复用现有工作台风格/布局组件,走独立路由 + 独立样式表;对已有代码的修改仅限「一个带开关的入口按钮」。
2. **可降级**:沙箱/模板/node/构建链任何一环部署失败,Web App 功能整体退回"不可用"状态并优雅提示,智能体回退到现有单页 HTML 模式,**现有功能零影响**。
```
智能体(技能 web-app)
0. 检查可用性:/mnt/web-app-template 缺失 → 回退现有单页 HTML 模式(行为与今天完全一致)
1. scaffold → outputs/my-site/(排除 node_modules/dist)
2. 写 src/pages/*.tsx、public/data.geojson、编辑 site.json
3. build_site.py build → 软链/junction 预置 node_modules → vite build → dist/(失败自修复 ≤3 次)
4. present_files site.json
平台(Gateway,全部能力探测 + 开关保护)
GET /api/web-app/capability # 可用性探测(配置/模板/node_modules/node 四项检查)
GET /api/threads/{tid}/sites # 站点列表 + 构建状态
POST /api/threads/{tid}/sites/{name}/build # 触发构建
GET /api/threads/{tid}/sites/{name}/build-status
POST /api/threads/{tid}/sites/{name}/preview-url → 签发短时效签名 URL
GET /api/public/site-preview/{token}/{path} # 静态托管 dist/
前端(独立路由 + 独立风格)
#/sites/:threadId/:siteName # SiteStudioPage:全新视觉风格,无现有 chrome
产物面板 site.json →「打开站点预览」按钮(仅 capability 可用时渲染)
```
已确认决策:混合构建(技能自修复 + 平台按钮兜底);预览工作台先行;GeoJSON 优先 + 可配置底图。
---
## 一、页面风格:独立设计系统(不用现有风格)
新页面 `SiteStudioPage` 做成**深色"预览工作室"**风格,视觉上与现有工作台完全区隔:
- **独立路由**:直接注册在 `App.tsx` 顶层(`/share/:shareCode`、`/login` 同款先例),**不进** `PageLayout`/`WorkspaceLayout`/`Header`/`PageSidebar`——零现有 chrome 继承,零布局耦合。
- **独立样式**:新建 `src/pages/site-studio/site-studio.css`(自建 design tokens:深空灰底 `#0f1117` 系、靛蓝/青渐变强调色、玻璃拟态面板、细腻边框与阴影、系统字体栈),所有类名 `site-studio-` 前缀,**只被该页面 import**,不碰 tailwind 配置、全局 CSS、主题 provider。
- **布局**:
- 顶部悬浮工具栏:站点标题 + 构建状态胶囊(成功/构建中/失败三态,带呼吸灯动画)+ 设备切换分段控件(桌面/平板/手机)+ 刷新/重新构建/新窗口打开;
- 左侧窄导航栏:site.json 的页面列表(图标 + 标题,点击设 iframe hash 切页),可折叠成纯图标模式;
- 中央:**浏览器壳**造型(圆角 + 顶部仿 URL 地址条 + 红黄绿窗控点缀)内嵌 iframe,设备切换时宽度平滑过渡动画;
- 底部可折叠**构建控制台**(等宽字体日志、错误红色高亮、构建时长);
- 加载/失败/未启用态都有设计过的插画式空状态(纯 CSS/SVG,不引组件库新依赖)。
- 登录守卫:轻量检查 stored auth,缺失跳 `#/login`(复用既有跳转工具,不动鉴权体系)。
## 二、降级与故障隔离矩阵(部署失败不影响已有功能)
| 故障点 | 行为 | 对现有功能影响 |
|---|---|---|
| config `web_app.enabled: false`(默认关) | capability 返回不可用;入口按钮不渲染;技能第 0 步探测失败即回退单页 HTML | 无 |
| 模板目录/node_modules 未预置(发版遗漏) | 同上:capability 四项检查(配置/模板/依赖/node 二进制)逐项报告原因 | 无 |
| sandbox.mounts 加了模板挂载但目录缺失 | 已核实 `local_sandbox_provider.py:81-90` 对不存在宿主路径**静默跳过仅告警**;Aio provider 同样按"存在才挂"实现 | 无(沿用既有容错语义) |
| vite build 失败/超时 | 子进程隔离(`asyncio.to_thread` + 超时强杀 + 每站点并发锁),错误收进 JSON;预览页显示日志;智能体读报错自修复 | 无(不进 gateway 请求主链路) |
| 签名 URL 过期/伪造 | 403 JSON,页面自动重签 | 无 |
| 前端新页面任何异常 | 独立路由独立样式,最坏就是该页白屏,error boundary 兜底 | 无 |
| 产物面板入口按钮 | 唯一改动点:capability 请求失败/不可用 → 按钮不渲染,**原 UI 一字不变** | 无 |
| 新 router 注册 | 纯增量 include_router,异常全部在路由内转结构化响应 | 无 |
技能回退语义(关键):SKILL.md 第 0 步强制探测 `ls /mnt/web-app-template/scripts/build_site.py`——探测失败时明确指示"改用现有方式产出单个 xxx.html",保证部署不完整时智能体产物质量与今天一致。
## P1 站点模板(`offline-backend-20260512/backend/templates/web-app-template/`,源码入库,node_modules gitignore 由发版仓库预置)
- Vite + React + react-router-dom(**HashRouter**:静态托管路径下无需服务端 fallback)+ **纯 CSS design tokens**(不用 Tailwind v4,规避 oklch 在 chrome100 老内核失效)+ build target es2018。
- 依赖:react 系、@xyflow/react、maplibre-gl、echarts、mermaid、pmtiles、vite/@vitejs/plugin-react(lockfile 锁死版本)。
- 内置组件:`AppShell`(读 site.json 导航)、`FlowCanvas`(xyflow 流程图封装,自动分层布局)、`GeoMap`(**默认无底图模式**:背景 + GeoJSON 点线面离线开箱即用;site.json 可配 tilesUrl/PMTiles)、`ChartPanel`(echarts)、`MermaidBlock`(静态图)。
- `router.tsx` 用 `import.meta.glob('../pages/*.tsx')` 自动注册路由——智能体丢页面文件即生效,零手动接线。
- `site.json`:`{ title, description, nav[], map:{tilesUrl?, tilesType?, pmtiles?}, theme? }`。
- Makefile 加 `make webapp-template`(开发机一次性 pnpm install);发版仓库构建脚本负责镜像内预置 node_modules(文档写明)。
- config.yaml `sandbox.mounts` 增模板只读挂载(`templates/web-app-template → /mnt/web-app-template`,缺失自动跳过已验证);AioSandboxProvider `_get_thread_mounts` 小扩展:同样"存在才挂"地补读 config 自定义 mounts。
## P2 构建脚本 + 技能包(`skills/public/web-app/`)
- `SKILL.md`:触发词(多页面网站/站点/web app/带流程图或地图的网页);工作流 = ①可用性探测(失败即回退单页 HTML)②scaffold ③配置 site.json ④写页面 ⑤数据放 public/ ⑥build(失败读 JSON 报错修复重试 ≤3)⑦present_files site.json ⑧引导点「站点预览」;硬约束:不手写 dist、一律 `/mnt/user-data/...` 虚拟路径。
- `scripts/build_site.py`(纯标准库):`check`(能力探测 JSON)/ `scaffold`(copytree 忽略 node_modules、dist)/ `build`(定位 node → 软链 node_modules 进站点:Linux symlink、Windows `mklink /J` junction 免管理员 → `node vite.js build` → 结构化 JSON 输出,180s 超时)/ `status`。
- 模板定位顺序:env `DEERFLOW_WEBAPP_TEMPLATE` → `/mnt/web-app-template`(沙箱内)→ 后端相对路径(宿主侧)。
## P3 后端:纯引擎 + 路由(全部能力开关保护)
- `packages/harness/deerflow/webapp/`(纯引擎,不 import app.*,对齐 `address_scan.py` 先例):站点发现(outputs/* 下 site.json + dist 状态)、构建执行(子进程 + 锁 + 超时)、HMAC token(`tid|site|exp`,密钥首用生成持久化 `{base_dir}/.web-app-secret` 0600)。
- `app/gateway/routers/sites.py`(app.py 注册):
- `GET /api/web-app/capability`:enabled + 模板 + node_modules + node 四项检查,逐项给原因(前端与技能共用);
- 列表/build/build-status/preview-url:`@require_permission("threads", "read", owner_check=True)`,能力不可用时返回结构化 `{"available": false, "reason"}` 而非 5xx;
- `GET /api/public/site-preview/{token}/{path}`:验签 + 过期 → `aresolve_thread_virtual_path` 且**限定站点 dist/ 根**(复用遍历防护)→ 正确 MIME(.js/.mjs→text/javascript、.geojson→application/json、.wasm)→ **HTML inline** + `CSP: default-src 'self' data: blob:; worker-src 'self' blob:`(maplibre worker 需要)+ `X-Content-Type-Options: nosniff` + `Cache-Control: no-cache` + ETag(mtime)。
- config.yaml + `deerflow/config/configuration.py` 加 `web_app:`(enabled **默认 false**、build_timeout、token_ttl、template_dir、node 覆盖)——与 `workflows` 同款总开关模式。
- 测试 `tests/test_web_app_sites.py`:capability 降级路径、token 签发/过期/伪造、路径穿越拒绝、MIME、站点发现(构建子进程 mock)。
## P4 前端
- `src/core/sites/api.ts`:capability/sites/build/status/mintPreviewUrl + TanStack Query hooks(照 `light-apps.ts` 模式)。
- `src/pages/site-studio/SiteStudioPage.tsx` + `site-studio.css` + `App.tsx` 注册顶层路由 `/sites/:threadId/:siteName`(独立风格见第一节;iframe `sandbox="allow-scripts allow-forms allow-popups"` 无 allow-same-origin,延续既有安全契约;token 过期自动重签;设备切换;重新构建 → 轮询 build-status → cache-bust 刷新)。
- `artifact-file-detail.tsx` **唯一既有文件改动**:产物路径以 site.json 结尾且 capability 可用时渲染「打开站点预览」按钮(navigate 到新路由);capability 请求失败/超时一律不渲染,原 UI 不变。
- token URL 构造等纯函数补单测;`pnpm typecheck` 通过。
## P5 测试与文档
- 后端 `make test` + ruff;前端 typecheck + 单测;`build_site.py` 本机真实跑通 scaffold→build→静态服务器自测 dist。
- 文档:AGENTS.md 增「Web App 产物」小节(含降级矩阵);发版文档补 node_modules 预置说明。
## 安全边界
短时效签名 URL(默认 2h)只覆盖该站点 dist/ 静态读;dist/ 根限定 + 复用 `aresolve_thread_virtual_path` 遍历防护;HTML inline 仅在 site-preview 路径开放(不影响 artifacts 接口既有强制 attachment);CSP + nosniff + iframe sandbox 三重纵深。
## 二期(本期不做)
源码编辑 + 保存即重建;跨对话站点 gallery;PMTiles 底图资产打包;`visualizations/` 内联图表(进行中,互补不冲突)的底图配置统一(其 map-renderer.tsx 硬编码 Google 瓦片属离线隐患,另行处理)。
## 风险
- Windows 开发机 junction 链接免管理员,node 前端机必有;生产镜像已含 nodejs+npm。
- Aio provider 自定义 mounts 扩展是小改动,按"存在才挂"实现即可,不影响现有挂载。
- dist/ 出现在产物目录扫描(文件噪音)——一期保持现状,二期优化。
+4 -1
View File
@@ -90,11 +90,12 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps) - **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat`, sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` reuses the engine (`AIWritingContext` + `useStream` + `AIWritingDraftPanel`); **idle keeps the classic setup layout** (`WritingSetupChat` + collapsible form) and switches to the chat workbench once writing starts; left panel `components/ai-writing-chat/WritingChatPanel.tsx` = **unified chronological feed** (`ChatFlowTimeline.tsx`, chat bubbles anchor-interleaved into the progress timeline so triggered events render below the user's message) + button-only intervention cards (`ChatInterventionCards.tsx`, inline textareas removed) + bottom unified smart composer. Intent resolution: frontend fast path + `POST /api/ai-writing/sessions/{id}/intent` → `deerflow/agents/ai_writing/intent_router.py` (per-pause action whitelist, high-cost confidence gates, payload cleaning); supports QA bypass, clarify follow-ups, control intents, and material-stage auto re-search + pending-intent resume. Tests `tests/test_ai_writing_intent.py`; plan doc `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`. - **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat`, sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` reuses the engine (`AIWritingContext` + `useStream` + `AIWritingDraftPanel`); **idle keeps the classic setup layout** (`WritingSetupChat` + collapsible form) and switches to the chat workbench once writing starts; left panel `components/ai-writing-chat/WritingChatPanel.tsx` = **unified chronological feed** (`ChatFlowTimeline.tsx`, chat bubbles anchor-interleaved into the progress timeline so triggered events render below the user's message) + button-only intervention cards (`ChatInterventionCards.tsx`, inline textareas removed) + bottom unified smart composer. Intent resolution: frontend fast path + `POST /api/ai-writing/sessions/{id}/intent` → `deerflow/agents/ai_writing/intent_router.py` (per-pause action whitelist, high-cost confidence gates, payload cleaning); supports QA bypass, clarify follow-ups, control intents, and material-stage auto re-search + pending-intent resume. Tests `tests/test_ai_writing_intent.py`; plan doc `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes). - **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all). **简介模式** uses a separate overlay: every compact workspace menu item can be placed in `sidebar`(左侧菜单)、`settings`(设置和更多)or `toolbox`(右侧工具箱); `CompactToolbox` (`components/workspace/compact-toolbox.tsx`, mounted by `PageRoutes.tsx`) opens only when the square right-edge trigger is clicked, then slides a compact, content-height right-side drawer over a backdrop above global dropdowns; configured items render top-to-bottom as centered square cards with a large icon above the label while the effective layout is compact. `/api/compact-menu-overrides` stores two independent sets (type `A` / `B`); each website picks one via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`. - **菜单管理 (Menu Management, admin defaults + personal layouts)**: `MenuManagementPage` (`pages/MenuManagementPage.tsx`) is available from「设置和更多」and the global Header account dropdown. Administrators edit the shared default menu; ordinary users edit only their own full/compact menu snapshot. For users with a saved snapshot, known node ids keep the personal `label`/`disabled`/`parentId`/`sortOrder` (or compact placement/order), while administrator defaults for node ids absent from that snapshot flow through—so newly-added code menus and iframe light apps follow the current default without overwriting older personal choices. Temporarily absent dynamic nodes are preserved when an unrelated personal edit is saved;「恢复默认」deletes only that user's snapshot. The static tree (`core/page-layout/sidebar-menu.ts`) remains the source of icons/routes and administrator defaults remain sparse per-node deltas. Merge/snapshot helpers live in `core/page-layout/menu-overrides.ts` and `compact-menu-overrides.ts`; full-menu writes carry the complete effective parent hierarchy for cycle/strict three-level validation while persisting only the sparse admin delta or full personal snapshot. Backend `/api/menu-overrides` uses `menu_overrides` (default) + `user_menu_override_configs` (per-user snapshot); `/api/compact-menu-overrides` uses `compact_menu_overrides` (type `A` / `B` defaults) + `user_compact_menu_override_configs` (per-user/type snapshots). Each website selects its compact type via `runtime-config.js` `VITE_COMPACT_MENU_TYPE`; `CompactToolbox` renders `toolbox` items.
- **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答经 Gateway `POST /api/sentiment-agent/stream` 转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块 `src/core/sentiment-agent/`。 - **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 tab,固定一张虚拟智能体卡片(名称/简介可编辑,存 localStorage)。对话页 `pages/SentimentAgentChatPage.tsx`(路由 `agents/sentiment-analysis/chats/:thread_id`)复用普通问答布局 + `MessageList`,**输入框只有发送按钮**;问答经 Gateway `POST /api/sentiment-agent/stream` 转发外部 AG-UI 网关(不经过 LangGraph),会话仅存 sessionStorage。接口地址 + Basic Auth 在 `runtime-config.js` 配置(`VITE_SENTIMENT_AGENT_API_URL` / `_AUTH_USERNAME` / `_AUTH_PASSWORD`)并随请求传给后台;后台按传入地址转发、关闭 TLS 校验,失败时把完整错误回传。核心模块 `src/core/sentiment-agent/`。
- **简洁模式用户引导 (Compact-mode onboarding tour)**: 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React `Guide` 组件驱动的分步引导。组件 `components/workspace/compact-mode-guide.tsx`(挂在 `ChatPage`):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的 `data-compact-guide` 标记定位(`workspace-nav-chat-list` / `recent-chat-list` / `workspace-nav-menu` / `appearance-controls` / `custom-prompt-menu.tsx` 的 `CustomPromptBar`(提示词栏,仅新对话落地态渲染)与其 `ManagePromptsDialog`(`prompt-dialog` 锚点)/ ChatPage 输入框容器),**启动时按目标是否存在过滤步骤**(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。**提示词管理弹窗步骤是动态目标**:弹窗平时不渲染,引导切到该步前先广播 `PROMPT_DIALOG_GUIDE_EVENT`(`custom-prompt-menu.tsx` 导出)让 `CustomPromptBar` 打开 `ManagePromptsDialog` 并**接管**(`owner: true` 期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以 `owner: false` 收回弹窗,4 秒打不开则跳过该步防卡死。**TDesign Guide × Radix 模态互操作**:Radix 模态弹窗打开时会把 `body` 置 `pointer-events:none`(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),`styles/tdesign-theme.css` 末尾对 `.t-guide__overlay/__highlight/__popup/__wrapper` 显式 `pointer-events:auto` 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage `deerflow.compact-guide-finished` 记忆(浏览器级);**中途刷新/离开不记忆,下次进入还会再演示**。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播 `compact-guide:start` 窗口事件直接启动,否则写 sessionStorage 请求并导航 `/chats/new` 由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。`eligible` 排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。 - **简洁模式用户引导 (Compact-mode onboarding tour)**: 简洁模式(外观设置的「全量/简洁」= compact,无全局导航栏 + 页面侧边栏)新增 TDesign React `Guide` 组件驱动的分步引导。组件 `components/workspace/compact-mode-guide.tsx`(挂在 `ChatPage`):9 步 —— 欢迎弹窗(dialog 模式)→ 左侧功能导航 → 聊天记录 → 设置和更多 → 换肤 → 界面模式(切回全量)→ 常用提示词栏 → 提示词管理弹窗 → 输入框;高亮目标由各组件上的 `data-compact-guide` 标记定位(`workspace-nav-chat-list` / `recent-chat-list` / `workspace-nav-menu` / `appearance-controls` / `custom-prompt-menu.tsx` 的 `CustomPromptBar`(提示词栏,仅新对话落地态渲染)与其 `ManagePromptsDialog`(`prompt-dialog` 锚点)/ ChatPage 输入框容器),**启动时按目标是否存在过滤步骤**(如新用户无聊天记录、侧栏收起时自动剔除该步 —— Guide 对缺失元素会抛错,不能盲传)。**提示词管理弹窗步骤是动态目标**:弹窗平时不渲染,引导切到该步前先广播 `PROMPT_DIALOG_GUIDE_EVENT`(`custom-prompt-menu.tsx` 导出)让 `CustomPromptBar` 打开 `ManagePromptsDialog` 并**接管**(`owner: true` 期间 ESC/点遮罩不关闭,防止高亮目标消失导致 Guide 报错),轮询锚点真正挂载后才切步;离开该步 / 跳过 / 完成 / 中途卸载时以 `owner: false` 收回弹窗,4 秒打不开则跳过该步防卡死。**TDesign Guide × Radix 模态互操作**:Radix 模态弹窗打开时会把 `body` 置 `pointer-events:none`(滚动锁定),Guide 直接挂在 body 下的遮罩/高亮/弹层会一并继承失活(引导按钮点不动),`styles/tdesign-theme.css` 末尾对 `.t-guide__overlay/__highlight/__popup/__wrapper` 显式 `pointer-events:auto` 兜底。触发:① 自动 —— 简洁模式新对话落地页自动演示,直到用户显式点「完成」或「跳过」才写 localStorage `deerflow.compact-guide-finished` 记忆(浏览器级);**中途刷新/离开不记忆,下次进入还会再演示**。② 手动 —— 「设置和更多」下拉新增「界面使用引导」(该下拉仅存在于简洁模式工作区侧栏 footer):已在问答页时广播 `compact-guide:start` 窗口事件直接启动,否则写 sessionStorage 请求并导航 `/chats/new` 由挂载的引导消费(消费在定时器真正启动时才执行,规避 StrictMode effect 双跑吞请求)。`eligible` 排除 embed=1 / hideChrome=1 / chats_iframe / FORCE_COLLAPSED_SIDEBAR(无界)等同样无全局 chrome 的特殊环境;启动延迟 800ms 等侧栏宽度过渡稳定后再测量高亮框。
- **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。 - **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide.<stage>.finished`,中途刷新/离开不记忆)。**自动演示仅 create / chat 两段**(进智能体列表页不弹引导,点「创建智能体」进入新建页时才自动演示;list 段的 `AgentGuide` 传 `autoStart={false}`,只能由头部「使用引导」按钮手动触发)。三段各自挂载、各自记忆:① `list`(`agent-gallery.tsx`,/page/workspace/agents)——分区 Tabs、搜索排序、智能体卡片操作、右上「创建智能体」按钮;**完成/跳过就地结束(不跳转创建页,与 create 段无联动)**,最后一步仅提示进入创建页后会有配置引导;② `create`(`NewAgentPage.tsx`,/page/workspace/agents/new)——左侧对话式创建输入区、基本信息、运行模型+入库模板名、知识空间、SOUL.md、技能穿梭框、保存按钮(9 步);③ `chat`(`AgentChatPage.tsx`,agents/:id/chats)——左栏切换与历史、常用问题 chips、输入框(落地态/会话态两个挂点同一 `chat-input` 标记)、右侧详情面板;`eligible` 限定普通智能体对话页(`showAgentList && !isEmbedded && !taskCtxOn`,即排除通用问答原地嵌入与任务深链工作区),三段共用 `isAgentGuideEnvironment()` 排除 embed/hideChrome/无界环境。高亮锚点 `data-agent-guide`:列表页 `tabs`/`search`/`agent-card`(卡片+列表两种布局的根元素)/`new-agent-btn`,创建页 `create-chat-input`/`create-basic`/`create-model`/`create-knowledge`/`create-soul`/`create-skills`/`create-save-btn`,对话页 `chat-sidebar`/`chat-questions`/`chat-input`/`chat-detail`;同样**启动时按目标存在性过滤步骤**(新用户无卡片/无预设问题时自动剔除)。手动重放:三个页面头部各有「使用引导」按钮(列表页创建按钮左侧、创建页 header 右侧、对话页 header),广播 `agent-guide:start:<stage>` 窗口事件强制重放(忽略已看过记忆)。
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels. - **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
- **技能上下线 (Skill Availability, owner/admin)**: the global `Skill.enabled` flag is the online/offline switch. Taking a skill offline persists `skills.<name>.enabled=false` in `extensions_config.json` but does **not** delete its directory or remove it from existing agent/workflow configurations; the row remains visible and bringing it online restores those references. Administrators may toggle every skill from 「技能管理」; ordinary users may toggle only their own non-built-in `custom` skills from 「技能 → 技能列表 → 我的」. Offline skills must be absent from every model-facing skill catalogue (`lead` prompt, `search_skills`, `skill_list`, `skill_view`, subagents, AI writing/deep research/roundtable and Workflow run-start validation), from agent skill pickers, and from direct sandbox access: `read_file`/`ls`/`glob`/`grep`/`bash` reject offline, unknown, root-wide and category-wide `/mnt/skills` paths. The backend mutation is `PUT /api/skills/{name}`, authorizes either an administrator or the persisted owner of the target custom skill, patches the raw document so MCP placeholders and unrelated extension fields survive, uses atomic replacement when supported plus a Docker single-file-bind fallback, and refreshes the skill prompt cache. Launchers keep the runtime extensions file in a writable persisted directory. Existing agent/strategy-agent references stay visible as 「已下线」 and may be removed but not newly selected.
- **技能地址管理 (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/`。 - **技能地址管理 (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, 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, `appendTaskId`/`idParam` become numeric `queryParams` (task id, or action id when `idKind:"action"` — same selector as url buttons, xdfx only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`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 **or host** 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. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`. - **按钮管理 (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, 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, `appendTaskId`/`idParam` become numeric `queryParams` (task id, or action id when `idKind:"action"` — same selector as url buttons, xdfx only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`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 **or host** 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. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes` - **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
@@ -105,6 +106,8 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
- **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ 每次检索 JSON 原样落盘 `/mnt/user-data/workspace/hits/qN.json` → **一条命令** `task-report-build`(group `all`)技能函数转换成 14 类 `task-reports.json`(模型禁止手写该 JSON;分页技能 `task-report-{enemy,our,env,judge}` 仅用于事后补跑单页;五者都是共享引擎 `task-report-lib/convert_lib.py` 的瘦 CLI,`resolve_hit_files` 对虚拟绝对路径读空的情况自动回退到相对写法并在 `warnings` 里列出尝试过的路径,免得智能体自己写脚本排查)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` → 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "Line truncated - **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ 每次检索 JSON 原样落盘 `/mnt/user-data/workspace/hits/qN.json` → **一条命令** `task-report-build`(group `all`)技能函数转换成 14 类 `task-reports.json`(模型禁止手写该 JSON;分页技能 `task-report-{enemy,our,env,judge}` 仅用于事后补跑单页;五者都是共享引擎 `task-report-lib/convert_lib.py` 的瘦 CLI,`resolve_hit_files` 对虚拟绝对路径读空的情况自动回退到相对写法并在 `warnings` 里列出尝试过的路径,免得智能体自己写脚本排查)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` → 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "Line truncated
- **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。 - **iframe 嵌入模式 (外部系统用 iframe 套用深链问答/会商)** — 三个**按标签页**开关,全部由深链 URL 参数开启、存进同一份 `sessionStorage` 会话(`embed.session`,键名常量在 `core/embed/embed-session.ts`),跨登录跳转/刷新/页内导航存活,**绝不跨标签污染**同浏览器另开的正常系统页。核心隔离手法:用 `sessionStorage`(按标签页/浏览上下文隔离,iframe 即便同源也有独立一份)而非 `localStorage`(跨标签共享)。开关:(1) **`?embed=1`(可带 `&theme=`)** — 去全局 chrome(`PageLayout` 的 `pageSidebarAllowed && !isEmbedActive()` → 隐藏 Header + PageSidebar)+ 按外部主题色**强制**明/暗:走 next-themes 的 `forcedTheme`(`App.tsx` `AppProviders` 调 `resolveForcedEmbedTheme`,**不写** localStorage `strategy-theme`),离散映射 `dark-blue`/`dark`→深、其余→浅;嵌入侧**绝不**调 `setTheme`/`persistSidebarEnabled`(那俩落 localStorage 跨标签污染,`LoginPage` 据 `readEmbedParam` 跳过)。(2) **`?hideJump=1`** — 隐藏深链底部「快捷跳转」栏 `TaskJumpBar` 的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,`isJumpBarHidden()` 为真时整条 `return null`。(3) **登录用户隔离** — 嵌入态下鉴权/身份键(`deerflow.auth` token、`userInfo`、`login.*`、`tools_token`、maxkey/userDetail token)全部经 `core/auth/scoped-storage.ts` 的 `authStorage()`(嵌入→sessionStorage / 正常→localStorage)读写,使 iframe 里登录的用户与正常登录用户**完全隔离**、互不覆盖;所有 token 读取都过 `getStoredAuth()`/`getAuthorizationHeaderValue()`(API 客户端 `core/api/*` 全走这俩),改中心函数即让全体调用方自动隔离;跨标签账号变更监听 `AuthSyncWatcher`(`core/auth/sync.tsx`)在嵌入态禁用。嵌入会话在 `main.tsx` 渲染前 `syncEmbedSessionFromCurrentUrl()` 同步启用(早于任何鉴权写入,避免首帧 effect 顺序漏写 localStorage);`AppProviders` 的 effect 再按 SPA 导航同步。`embed=0`/`hideJump=0` 可显式关闭。正常(非嵌入)标签页所有 `isXxx()` 恒 false,行为**零变化**。第三个开关 `?isolate=1`(**独立登录会话**)把同一套**登录用户隔离**单独提供给**普通(带 chrome)标签页**——只切换鉴权存储、不去 chrome / 不强制主题 / 不动跳转栏:`authStorage()` 的条件从「仅嵌入」放宽为 `isEmbedActive() || isIsolatedSession()`,`AuthSyncWatcher` 的跨标签监听对嵌入态**或**独立态都即时忽略(handler 内实时判,运行时开关也即刻生效)。除 URL 参数外,**设置 → 外观 →「登录会话」** 有一个 Switch「独立登录会话(本标签页)」运行时切换(`setIsolatedSession`,`appearance-settings-page.tsx`):**开启时先 `migrateAuthToSession()`**(`scoped-storage.ts`,把 localStorage 的受保护键 `deerflow.auth`/`userInfo`/`userId`/`tools_token` + `login.*` 前缀快照搬进 sessionStorage)再翻标志,故无感不登出;此后本标签页登录态独立——别处登录/换号影响不到它,它换号也影响不到别人,但关掉该标签即结束(需重登)。开启给 toast 提示。
- **Web App 产物模式(多页面站点 / 流程图 / 地图,默认关闭、全链路可降级)**: 让智能体产出**完整站点**而非单个 xxx.html。沙箱技能 `skills/public/web-app` 用模板(`backend/templates/web-app-template`,经 `config.yaml sandbox.mounts` 只读挂载到 `/mnt/web-app-template`,目录缺失自动跳过)脚手架到 `outputs/<site>/`(site.json 清单 + `src/pages/*.tsx` 文件名即路由 + `public/` 数据),再跑 `scripts/build_site.py build` 用**预置 node_modules**(symlink/junction 链接,离线永不 npm install)执行 vite build 出 `dist/`;内置组件 `FlowCanvas`(@xyflow/react)/`GeoMap`(MapLibre,默认无底图 GeoJSON 模式,site.json 可配 tilesUrl/PMTiles)/`ChartPanel`(ECharts)/`MermaidBlock`。平台侧 `app/gateway/routers/web_app_sites.py` + 纯引擎 `deerflow/webapp/`(发现/子进程构建/HMAC 签名):`GET /api/web-app/capability`(enabled/模板/依赖/node 四项探测)、`GET|POST /api/threads/{tid}/sites[...]{build,build-status,preview-url}`,静态托管走 `GET /api/public/site-preview/{token}/{path}`(`/api/public/` 免鉴权命名空间,**唯一允许 HTML inline 的出口**——签名令牌 + dist/ 根限定 + 按请求真实 origin 显式声明的 CSP(iframe sandbox 不透明 origin 下 'self' 失效)+ nosniff;artifacts 接口既有的强制 attachment 不变)。前端:**独立风格**预览工作台 `pages/site-studio/SiteStudioPage.tsx`(顶层路由 `#/sites/:threadId/:siteName`,`App.tsx` 注册,不进 PageLayout/WorkspaceLayout;自带 site-studio.css 深色设计体系,零全局样式污染;iframe `sandbox="allow-scripts allow-forms allow-popups allow-modals"` 无 allow-same-origin)+ 产物面板 `artifact-file-detail.tsx` 里 site.json 的「站点预览」按钮(capability 可用才渲染,唯一既有文件改动)。**降级铁律**:默认**完全关闭**(`sandbox.mounts` 的模板挂载条目与文件末尾的 `web_app:` 段在 config.yaml 里**均为注释状态**);开启需**同时放开两处**(挂载让沙箱技能可探测 `/mnt/web-app-template`,`web_app.enabled: true` 开平台 API——只开一处会得到"生成了看不了"或"看不了也不生成"的中间态)。`build_site.py check` 在宿主侧会读 config.yaml 的 `web_app.enabled`(env `DEERFLOW_WEBAPP_ENABLED`/`DEERFLOW_WEBAPP_CONFIG` 可覆盖;沙箱内读不到配置时"挂载即视为开")。任何一环(配置/模板/node_modules/node)缺失 → capability 不可用 + 技能第 0 步探测失败即**回退单页 HTML 模式** + 入口按钮不渲染 + 构建失败只收进任务状态,现有功能零影响。配置 `web_app:`(enabled/template_dir/build_timeout_seconds/preview_token_ttl_seconds/node_path);测试 `tests/test_web_app_sites.py`;开发机 `make webapp-template` 预置依赖,离线发版由 deerflow-offline-deployment 仓库在 CI 预置 node_modules 进镜像。
### Configuration ### Configuration
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. 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.
+44 -1
View File
@@ -1,5 +1,40 @@
# CLAUDE.md # 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. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview ## Project Overview
@@ -81,6 +116,8 @@ 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` - **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) - **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。
- **定时任务「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. - **定时任务「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`. - **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`. - **策略优选**: 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`.
@@ -91,7 +128,9 @@ 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/`。 - **技能地址管理 (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 - **按钮管理 (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」. - **实时并发监控 (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」.
- **会话冲突 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()`. - **会话冲突与模型工具流恢复**: 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.
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes` - **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 - **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/` - **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
@@ -102,6 +141,10 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
### Configuration ### 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. 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 ## Detailed Backend Reference
Binary file not shown.
@@ -0,0 +1,188 @@
# 报告协作工作台 — 交互重设计方案(v2)
> 日期:2026-09-07
> 状态:**待确认**(确认后按 §7 分期实施)
> 取代:现行「规则澄清 + 三张模板方案卡 + 画布为主」的交互
> 原则:回到最初设计 —— **协调者智能体全程与用户对话的主线**
---
## 1. 设计目标与总体形态
用户视角只有一条主线:**和一个规划师智能体对话 → 它摸清意图并规划工作流 → 确认后执行、过程在流程图上可见 → 随时插话干预、指定智能体重跑 → 最终成稿、继续对话式修改**。
```
┌─────────────────────────────────────────────────────────┐
│ 阶段1 计划模式 阶段2/3 执行+干预 阶段4 成稿 │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ │ 规划师对话 │ 确认 │ 聊天时间线 │ 审稿过 │ 对话改稿 │ │
│ │ 需求摘要卡 │ ─────→ │ +成员Tab │ ─────→ │ 版本/diff│ │
│ │ 工作流卡 │ 冻结 │ +流程图动画│ 报告卡 │ 上游重跑 │ │
│ └───────────┘ └───────────┘ └──────────┘ │
│ ↑ 回到计划模式(replan)随时可触发 ↑ │
└─────────────────────────────────────────────────────────┘
```
与现行的核心差异:
| 维度 | 现行(做偏的) | v2 |
|------|----------------|-----|
| 需求澄清 | 规则模板问句秒回 | 规划师智能体(LLM)领域化提问 |
| 工作流来源 | 3 张确定性模板 | LLM 按意图生成自定义 DAG,对话迭代修改 |
| 执行展示 | 画布为主、聊天为辅 | **聊天时间线为主**(成员 Tab 切换保留),**执行状态在流程图上动画呈现** |
| 干预 | 命令确认卡体系 | 输入框常开 → 协调者智能体分类;支持**指定智能体重跑** |
| 报告修改 | 改写候选(纯文字层) | 意图分流:润色走改写;**事实性问题触发上游节点重跑** |
---
## 2. 阶段 1:计划模式 — 需求梳理与工作流规划
### 2.1 计划模式状态
- 用户发出第一条需求消息起进入**计划模式**,Header 显示徽章「📋 计划模式」,输入框提示语切换为「梳理需求中,你可以随时补充或纠正…」。
- 计划模式在**工作流确认冻结**时结束;执行期大改可通过 replan 回到计划模式(见 §5.4)。
### 2.2 规划师对话(LLM,替换现行规则路由)
- 必须调用独立内置智能体 `report-collaboration-intent` 的真实 LangGraph thread,并原样消费 `messages-tuple` SSE;禁止用 `ainvoke()` 等待完整 JSON 后再包装成一话。
- 流式正文、思考内容和工具步骤直接进入工作台现有 `MessageList`,与普通智能体问答采用同一套步骤条 / Reasoning 展示。
- 模型:`report_collaboration.planner_model`(回退 `coordinator_model` → 默认模型,复用现有 model_resolver 冻结机制)。
- 提问是领域化的:写市场分析就问「关注整车还是供应链 / 要不要竞品对比 / 给谁看 / 篇幅」,而非固定模板问句。
- 澄清轮次上限沿用 `max_clarification_rounds`;用户随时可以说「直接开始」按默认假设推进。
### 2.3 需求摘要卡
信息足够后,规划师在对话中输出一张**结构化需求摘要卡**(业务卡插槽渲染):主题 / 目的 / 读者 / 角度清单 / 篇幅与重点 / 默认假设。用户逐项看,可在对话里继续改(「读者改成投资人」→ 卡片更新出新版)。
### 2.4 工作流生成与迭代
- 规划师判断意图信息足够后,立即按已确认意图自动生成**一份**工作流方案(不是三选一),不再要求用户额外点击“确认意图/生成方案”;方案以工作流卡(成员构成、各节点使命、预计时长/成本)+ 右侧流程图预览呈现。
- 生成约束(保证可执行):**节点类型受词表约束** — `research / verify / analyze / write / review`;自由的是结构(几条线、并行还是串行、核验深度)与每个节点的使命/角度/工具。
- 对话式修改:「把价格策略单独拆一条线」「加一个政策风险分析」→ 规划师修订后出新版工作流卡(带变更说明),再确认。
- 「开始执行」→ 冻结需求 + 工作流(含模型/预算快照,沿用现有 freeze 机制)。
---
## 3. 阶段 2:执行 — 过程在流程图上可见
### 3.1 流程图动画规格(执行状态的可视化主载体)
节点状态 → 样式(`ReportFlowCanvas` / `ReportFlowNode`,状态源 `selectNodeStatusMap`,事件源 `node.status.changed` SSE):
| 状态 | 节点样式 | 连入边 |
|------|----------|--------|
| planned / queued | 灰色常态 | 静态 |
| **running** | 主色描边 + **呼吸脉冲动画**(ring)+ 角标 spinner | **流动动画**(@xyflow 内置 `animated: true` 的 dash 流动) |
| **completed** | 绿色 + ✓ 角标,常驻 | 恢复静态实线 |
| failed | 红色 + 错误角标 | 红色虚线 |
| awaiting_input | 橙色 + ?角标 | 橙色静态 |
| interrupted / superseded | 半透明降级 | 灰 |
- 语义:**边流动 = 数据正在流入该节点**(该节点执行中);节点完成即停动画换完成样式。
- 实现成本:@xyflow 原生支持 animated 边与自定义节点 className,纯 CSS 补呼吸动画即可,无需新依赖。
### 3.2 聊天区(保留现有 Tab 切换)
- 并行成员沿用 **AgentStageSelector Tab 切换**(维持现状,不改交错时间线)。
- 每个 Tab 内是该成员的完整流式输出:思考、工具调用卡、检索过程、正文 —— 与普通智能体问答同观感(message-adapter 已具备映射能力)。
- 协调者播报(「研究线 A 完成,核心发现 X,开始交叉核验」)进入总时间线 Tab。
---
## 4. 阶段 3:干预 — 对话即干预 + 指定重跑
### 4.1 输入框常开,协调者智能体分流
执行期输入框永远可用,所有输入先到协调者(LLM 分类,规则高精度直判兜底——复用 RC-BE-014 IntentRouter 骨架,把「规则优先」反转为「LLM 优先 + 规则快路径」):
| 用户意图 | 协调者行为 | 生效时机 |
|----------|-----------|----------|
| 提问(「查到什么了?」) | 直接回答 | 立即,不打断 |
| 修正方向(「重点看下沉市场」) | 小改:在安全点注入 overlay,并回复说明 | 下个安全点 |
| **指定重跑**(见 §4.2) | 出确认卡 → 重开节点 attempt | 确认后 |
| 大改(砍线/加线) | 提议 replan → 回计划模式 | 用户确认 |
| 停止 | 直接取消 | 立即 |
### 4.2 指定智能体重跑(新增核心交互)
**场景**:用户发现某节点传给下游的信息不对(如研究线 B 的结论有误)。
两个入口,同一后端动作:
1. **对话**:「研究线 B 的结果不对,重新查,重点看价格战」→ 协调者解析出 `node_id` + 新指令;
2. **画布**:点节点 → 抽屉(NodeInspectorDrawer)加「↻ 重新执行」按钮。
**级联规则**(复用并补全 ledger 的 targeted rework):
```
重跑节点 N(attempt+1,带用户新指令作为 mission overlay)
→ N 完成 → 所有直接/间接依赖 N 产物的下游节点标记 superseded
→ 按 DAG 依赖序逐波重跑(边动画重新流动,用户全程可见)
→ 受影响章节重新生成
```
- 确认卡必须展示**影响面**:「将级联重跑 核验B / 分析 / 写作 共 3 个节点,预计 +8 分钟」。
- 成本保护:级联重跑计入 run 预算(现有 budget 机制),预算不足时明确报因。
---
## 5. 阶段 4:报告成稿与对话式修改
### 5.1 成稿
审稿通过 → 报告卡出现在时间线(点开 ReportPreviewDialog 全文),协调者播报总结(各线核心发现 → 结论如何形成)。
### 5.2 修改意图分流(协调者 LLM 判定)
| 用户说 | 判定 | 动作 |
|--------|------|------|
| 「把第二章结论写得更笃定」 | 文字润色 | 改写候选(现有 rewrite API)→ 对比预览 → apply → 新版本 |
| 「这个数据不对 / 重新收集一下」 | **事实性问题** | 定位来源节点 → **触发该节点重跑(可带新角度)→ 级联 §4.2 → 受影响章节重生成 → 新版本** |
| 「再从供应链角度补一节」 | 结构性新增 | replan:回计划模式改工作流(只增不删已完成部分) |
| 「恢复到上一版」 | 版本回退 | 现有 restore API |
- 版本链:每次 apply / restore / 级联重跑产出均产生新 head,历史完整保留(现有机制)。
- 修改对话里协调者必须先**复述理解**(「你是想让研究线 B 换个角度重查,然后重写第三章,对吗?」)再执行,避免误触发昂贵的级联。
---
## 6. 工程改造清单
### 6.1 保留(本次联调已验证)
- 持久化 13 张表 + durable SSE + `after_seq` 断线续传;幂等键 / 乐观锁 / 预算框架。
- MessageList 复用链路(message-adapter → LangGraph Message → 业务卡插槽)。
- AgentStageSelector Tab、commands 安全点骨架(RC-BE-014)、报告版本三件套(RC-BE-015)、前端 hooks/reducer/selectors 主体。
- 「清空历史」「生成候选方案」等本次新增入口。
### 6.2 后端重做 / 新建
| 项 | 说明 |
|----|------|
| 规划师智能体 | LLM 多轮澄清(替换 `requirement/intent_router.py` 规则路由)+ 需求摘要结构化 + **工作流 LLM 生成**(受 §2.4 词表约束,替换 `planning/controller.py` 三模板)+ 对话式工作流修订 |
| 协调者智能体升级 | 干预分类 LLM 优先;修改意图分流(§5.2);执行前复述确认 |
| 指定重跑 + 级联 | ledger targeted rework 补全级联 superseded/重跑波次;确认卡带影响面计算 |
| AgentScope 适配器修复 | `count_tokens` 协议缺失(上下文压缩必崩);attempt 耗尽终态化(防僵尸 run) |
| 事件补齐 | 边流动所需的节点状态事件已有;补「级联影响预告」事件 |
### 6.3 前端重做 / 新建
| 项 | 说明 |
|----|------|
| 计划模式 UI | Header 徽章、composer 提示切换、需求摘要卡、工作流卡(替换现三张模板卡) |
| 流程图动画 | running 节点呼吸动画 + 入边 `animated`、completed ✓ 样式、级联重跑时边动画重流(§3.1) |
| 节点抽屉 | 加「重新执行」按钮 → 确认卡 |
| 干预交互 | 执行期输入框常开;意图确认卡(含影响面);修改对话复述确认 |
| 布局 | 聊天为主、画布为辅的默认比例微调(不动整体两栏结构) |
---
## 7. 实施分期
| 期 | 内容 | 验收 |
|----|------|------|
| **P0 地基** | AgentScope `count_tokens` 修复;attempt 耗尽终态化;级联重跑的 ledger 补全 | 执行阶段不崩、无僵尸 run、单节点重跑级联正确(后端测试) |
| **P1 计划模式 + 执行可视化** | 规划师 LLM 对话、需求摘要卡、工作流 LLM 生成与迭代、计划模式 UI、流程图动画 | 从新会话到确认工作流全程对话完成;执行时动画正确随状态流转 |
| **P2 干预 + 成稿修改** | 协调者 LLM 分类、指定重跑(两入口 + 影响面确认卡)、报告修改意图分流、上游级联重跑 | 四类干预各自走通;「数据不对→重跑→新章节→新版本」全链路 |
每期结束跑 RC 全量测试(当前基线 233 通过)+ 前端 26 单测,不引入对其他模块的影响。
@@ -229,23 +229,27 @@ sequenceDiagram
```http ```http
POST /api/report-collaboration/sessions POST /api/report-collaboration/sessions
POST /api/report-collaboration/sessions/{sessionId}/messages POST /api/intent/init
Content-Type: application/json POST /api/intent/stream
POST /api/report-collaboration/sessions/{sessionId}/intent-turns
{ "text": "请写一份…", "target_node_id": null, "agent_run_id": null }
``` ```
- `useReportCollaborationSession.send()`:当 `session.status` 不是 `running` / `awaiting_input` / `reviewing` 时走 `sendMessage`。 - 报告页面固定调用内置 `report-collaboration-intent` 智能体;`/api/intent/stream` 原样透传 LangGraph `messages-tuple`,不是等待模型完成后包装响应。
- 正文 token、provider reasoning 和工具调用步骤实时写入当前 `MessageList` 气泡;终态后 `/intent-turns` 只负责持久化镜像与状态交接,不再调用模型识别意图。
- 旧 `/sessions/{sessionId}/messages` 在 `empty/clarifying` 阶段返回 `409 USE_STREAMING_INTENT`,防止退回阻塞式 `ainvoke` 路径。
- 会话状态:`empty` → `clarifying` → `planning` → `proposal_ready`。 - 会话状态:`empty` → `clarifying` → `planning` → `proposal_ready`。
- 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。 - 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。
- 意图智能体输出 `INTENT_READY` 后,`POST /intent-turns` 自动创建并完成 `plan_request`;前端无需再展示“确认意图”或“生成候选方案”按钮。
### 5.3 阶段 2:候选方案 ### 5.3 阶段 2:候选方案
```http ```http
POST /api/report-collaboration/sessions/{sessionId}/plan-requests # 正常路径:由最后一轮 POST /messages 自动触发规划
POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select
``` ```
- `POST /plan-requests` 仅保留给“重新生成”与兼容调用,不是首次规划的必经步骤。
- 规划师尚未判定 `ready` 时直接调用该兼容接口会返回 `422 REQUIREMENT_NOT_READY`,防止跳过意图确认。
- **预览方案**(`previewPlanId`):纯前端,不调 API。 - **预览方案**(`previewPlanId`):纯前端,不调 API。
- **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。 - **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。
- 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。 - 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。
+31
View File
@@ -0,0 +1,31 @@
# 日常使用复查(2026-09-14)
范围:ji 分支上继续检查知识库管理、助手知识库、我的项目、项目问答和官方入口。沿用本地隔离后端数据目录 `.runtime/ji-acceptance`,浏览器检查 464×642 与 1440×900 两种尺寸;未向生产库写入验收项目。
## 发现与修复
**首屏白屏的直接业务触发点也已修复:** `LlmWikiKnowledgeSelector` 在懒加载/未加载知识目录时,每次渲染创建新的空数组,推荐问题 effect 随之回写父页状态,形成无限更新。将目录空结果和默认助手库选择稳定化后,重新从登录进入新对话成功,欢迎引导、输入框及知识空间下拉均可操作(截图 06)。仅升级依赖并不能消除这个业务循环;上述修复与依赖兼容更新共同保留。
1. **首次进入:高影响,已修复依赖。** 本轮首次登录后出现整页空白,控制台为 ScrollArea `Maximum update depth exceeded`。安装版本 1.2.10 使用不稳定引用回调;升级并锁定 1.2.18,同步 package-lock,核对源码使用稳定 setState 引用。冷启动仍复现组合引用循环,进一步发现各基础组件带入 Slot 1.2.3/1.2.4;统一锁定 Slot 1.3.3,npm 与 pnpm 均配置覆盖间接依赖。依据 [Radix 发布记录](https://www.radix-ui.com/primitives/docs/overview/releases) 的 React 19 无限重渲染及 Slot 引用修复。
2. **新建项目:已修复。** 手机表单出现两个菜单按钮(截图 01)。由外层提供菜单时不再显示内部按钮;官方入口也去掉无效的内部侧栏按钮。
3. **项目历史:已修复。** 我的项目侧栏链接跳到旧项目空间(截图 02),导致返回目的地变化。现在保留我的项目路径,普通聊天与旧项目空间保持原路由。手机点历史后菜单自动收起。官方侧栏原本把项目聊天打开为普通聊天,现按项目归属进入项目工作区;`/official/workspace/projects/groups` 根路径回到普通问答项目列表。
4. **项目设置与失败恢复:已修复。** 默认智能体更新后,同步新对话选择器;项目对话权限/归属加载失败、普通问答项目列表失败均有重试入口。
5. **助手知识检索:已修复。** 连续检索或切换库时,以递增请求序号隔离过期成功/失败/结束回调,防止旧结果覆盖新库;新检索清除旧结果;空输入禁止提交。无结果原本只有小标签(截图 03),现在说明可调整关键词并提供浏览目录按钮。输入框补充可访问名称。
6. **窄屏输入栏:已修复。** 464px 截图中发送箭头被工具栏挤出右边缘(截图 07);工具区允许换行、发送区不收缩,截图 08 确认按钮完整可见。项目问答网络错误也增加“重新加载对话”,只重建读取状态,不自动重发消息。
## 复测记录
- 具名项目创建成功;新建项目对话,通过真实模型收到“项目问答回归通过”;刷新保留消息与项目归属。
- 修复工具栏后,在窄屏实际点击发送并收到“窄屏发送通过”;首屏知识空间菜单完成加载、选择助手库后显示已选择 1 个,未再出现循环白屏。
- 手机项目历史链接确认留在 `qa/projects/groups`;桌面官方 groups 根路径跳回正确列表,官方项目历史进入相同项目。
- 官方和常规项目切换时偶发网络 `Failed to fetch`,刷新/重新挂载恢复历史消息;没有将其描述为已排除的网络问题,新增恢复按钮未进行独立网络故障注入测试。
- 助手知识库列表、阅读、检索无结果、返回入口实际操作;空查询按钮禁用。检索竞态由代码检查修复,未使用浏览器网络故障注入。
- 后端 22 个相关文件重新运行:**273 passed**,涵盖知识库管理/导入/检索、Wiki、编码与向量、项目归属隔离、策略持久化及调用边界。
- 前端项目与策略测试:**33 passed**,新增项目历史保持入口、旧项目和普通聊天/创建向导回归。
- 生产构建通过。全仓类型检查仍有 **59 个既有错误**,与上一轮错误列表一致;不宣称全仓类型检查通过。
## 证据与边界
本轮截图与日志存放在 `frontend-web/.runtime/daily-usage-audit/`:`01-create-before.png`、`02-history-before.png`、`03-knowledge-search.png`、`04-official-desktop.png`、`05-official-project-after.png`(网络失败状态)、后续 after 截图,以及 tests/build/typecheck 日志。后端日志为 `.runtime/ji-acceptance/daily-regression.txt`。
上一轮真实文件入库、向量命中、方案生成与保存的验证详见 `ji-knowledge-projects-acceptance.md`。本轮不重复付费生成完整方案;没有进行生产全量语料质量评估、负载测试、断网故障注入或完整无障碍合规审计。已检查可见导航、表单名称、窄屏显示和主要失败恢复,不能据此承诺所有模块没有缺陷。代码未提交、推送或部署。
+37
View File
@@ -0,0 +1,37 @@
# ji 分支知识库与我的项目验收
日期:2026-09-14。基线:`ji` / `f62a512a`,已从 origin fast-forward 更新。
## 修复范围
1. 简洁模式进入我的项目保留功能导航,覆盖列表、策略工作台和问答项目;移动端有菜单展开入口。
2. 策略结果卡片的模型思考前置;检查意图、评判、方案、展开视图保持过程在正文前。
3. 普通问答改为真实项目分组:先填写项目名称创建项目,再在项目中创建多条问答。服务端持久归属与权限校验沿用 analysis-projects;不自动迁移或标记历史普通聊天。官方入口支持对应 groups 子路由。
4. 修复方案列表要求预先选中策略导致无法进入的问题;任务变化后已有方案仍可查看;请求序号避免旧请求失败覆盖成功结果。
5. 修复知识库管理目录缓存导致上游删除后仍显示的问题;保留检索端缓存。
6. 项目编辑保留知识库绑定,助手知识库补充普通用户空态。
## 自动化
- 后端 22 个相关测试文件:273 passed(知识库、Wiki、向量、编码器、项目归属、策略配置与持久化、边界等)。
- 前端策略与项目测试:31 passed。新增用例覆盖未选择策略、已有方案变更任务、未完成评判及全部策略失败。
- `tests/test_knowledge_live_embedding.py`:真实接口 1 passed;默认跳过,设置 `KNOWLEDGE_TEST_EMBEDDING_KEY` 后运行。默认服务 SiliconFlow、模型 BAAI/bge-m3;可用 `KNOWLEDGE_TEST_EMBEDDING_URL` / `KNOWLEDGE_TEST_EMBEDDING_MODEL` 更换。
- `pnpm build`:通过。
- `pnpm typecheck`:未完全通过,59 个错误位于此次未改动文件(nextra 缺失、Markdown 插件类型、旧 JSX 类型及旧问答页等);修改文件无类型错误。不能将生产构建通过等同于全仓类型检查通过。
## 真实接口与浏览器
测试使用 `.runtime/ji-acceptance` 独立数据目录,保留测试数据便于复查,未操作业务数据库已有项目。
- BAAI/bge-m3:中英文 3 条输入,均返回 1024 维有效向量。
- 实际检索服务:相关正文排名、指定库范围、草稿排除、概览查询通过。
- 两个项目 A/B:A 创建两条对话,B 列表为零;跨项目读取 A 的对话返回 404。
- 浏览器:简洁模式完整菜单可见;新建具名普通问答项目;项目内真实发送并收到回答;第二条对话独立;回列表仍是一张项目卡,刷新后保留。
- 方案整理:真实模型返回 1539 字符正文,含 agent_done / all_done,流正常结束。
- 方案入口:完成评判但尚未勾选策略的测试项目出现在方案列表。浏览器进入后勾选策略、生成 2755 字符正文、保存成功;服务端校验 synthesisStatus=done 且 solutionSavedAt 非空,刷新后正文与导出按钮恢复。
- 官方入口:`/official/workspace/projects/groups/new` 正常展示具名问答项目表单,官方侧栏保留。
- 助手知识库:创建、重命名、列表、拒绝 .py 上传通过;上传测试文本任务 completed;生成 Wiki 页面,向量检索命中 1 条,详情 HTTP 200。
## 限制
真实模型测试使用最小合成读书活动/设备资料,未对生产全量知识语料进行质量评估。API 密钥仅用于测试进程,不提交配置。仓库原有未跟踪 docx 与 packages1.zip 保持不变。未提交、推送代码,也未执行离线发布。
@@ -0,0 +1,105 @@
# 本地问答、资料库与技能并发验收
测试日期: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` 测试。
@@ -0,0 +1,358 @@
# 技能并发与流式修复:配置、前端包与验收说明
日期: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
```
@@ -0,0 +1,73 @@
# 问答停在执行命令或读取技能:排查与修复
日期: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 未连接实际容器。未用生产模型调用或真实业务技能执行验证。
+52
View File
@@ -0,0 +1,52 @@
<!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>
@@ -0,0 +1,72 @@
"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);
};
@@ -0,0 +1,44 @@
"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);
});
@@ -0,0 +1,7 @@
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],
};
@@ -0,0 +1,15 @@
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,
],
};
@@ -0,0 +1,229 @@
"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;
};
@@ -0,0 +1,52 @@
"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}",
);
});
+70
View File
@@ -0,0 +1,70 @@
# 我的项目
入口:全量与简洁导航中的「我的项目」,路由 `/page/strategy/qa/projects`。旧地址 `/page/workspace/projects` 仅保留兼容跳转,不再展示独立入口或旧页面。
## 使用流程
1. 新建项目,选择普通问答、策略优选或方案生成。三类项目独立保存和展示,方案生成不会复用或标记策略优选项目。
2. 选择项目默认智能体,填写目标、约束等项目说明。新建项目对话时仍可换用另一个可访问的智能体。
3. 创建后进入项目工作区。普通问答项目可包含多条独立问答;方案项目可包含多条方案记录。最近对话根据 `analysis_project_mode` 回到普通问答分组或方案项目。
4. 方案生成可从「工作流配置 · 报告结构」选择、启停、编辑、删除结构,选择后自动填充项目报告结构配置;也可直接编辑大纲及固定/弹性约束。
5. 策略智能项目保存默认 Strategy Lens;每轮依次进行问题重构、各 Lens 独立分析、策略辩论和方案生成,并分为「意图理解」「多视角策略分析」「完整策略方案」三栏。右栏以文档形式给出执行摘要、目标原则、分阶段行动、资源配置、风险应对、评估复盘、形成依据与备选策略切换条件;原始对话和引用仍可切换查看。
6. 策略智能对话底部提供改变约束、增删视角、重新推演和挑战当前策略快捷动作;项目主页面可直接添加或删除 Lens。
模式在创建后不可修改,前后端都不提供修改入口。项目、对话归属和智能体访问权限在服务端校验;方案结构及每条对话所选智能体也以服务端配置为准。
## 普通问答跨会话记忆
- 仅普通问答项目显示「跨会话项目记忆」。它只受项目自身开关控制,与系统「问答记忆」总开关相互独立;成功完成的会话会在后台整理,状态包括等待沉淀、整理中、已就绪和异常。
- 新会话提问时会召回同一账号、同一项目中其他会话的事实、决策、约束、偏好、术语与未决事项。不会读取其他项目或其他账号,也不会混用普通聊天/智能体的通用记忆。
- 项目设置和问答页头部均可打开「项目记忆」面板,查看精简快照、条目可信度/重要度和来源对话;支持手工新增、编辑、固定、删除、清空以及从当前项目历史对话重建。项目暂停后仍可清空已有数据,但不能重建。
- 编辑、删除或清空原对话时,服务端先让对应记忆失效,再修改消息;重建期间不注入旧快照。固定或手工条目在重建时保留,自动条目按来源 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 开发与构建命令。
## 验证范围
后端回归命令:在 `offline-backend-20260512/backend` 执行
```shell
python -m pytest tests/test_analysis_projects.py tests/test_deep_research_report_tool.py tests/test_writing_mode_report_middleware.py -q
```
涵盖内存与 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 我的项目与知识库验收修复
日常使用复查:我的项目侧栏历史保持 `qa/projects/groups` 上下文,官方侧栏同样识别项目归属;手机选择历史后收起菜单;项目表单不重复显示侧栏按钮。设置保存后默认智能体同步到新对话选择器。读取项目对话失败提供重试,项目列表失败可原地重试。助手知识检索隔离过期响应,清空输入时禁用检索,无结果提供目录入口。ScrollArea 固定为包含 React 19 引用回调修复的 1.2.18。详细记录见 `docs/daily-usage-audit-20260914.md`。
- 简洁模式的「我的项目」列表、策略工作台和普通问答项目恢复工作区功能导航;窄屏提供展开菜单按钮。
- 普通问答先创建具名项目,再在项目内创建多个对话。列表读取 `/api/analysis-projects` 的 `qa` 项目,不再读取、标记或自动归入全局普通聊天。
- 项目设置更新保留既有知识库绑定。旧「项目空间」入口已移除并跳转「我的项目」;普通问答路由为 `qa/projects/groups/*`,方案生成路由为 `qa/projects/solutions/*`。
- 策略生成卡片统一先显示思考/执行过程,再显示正文;评判、方案及展开视图沿用相同顺序。
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步不重复展示问答记录,突出当前任务并扩大策略回答、评判正文;第三步将生成过程默认收起并为方案正文保留固定可视空间,集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载。保存后的项目卡也提供 Markdown/Word 下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
- 资料库把已完成的策略优选综合方案作为独立「策略生成」来源展示,并标注具体策略项目名称;条目可回到策略项目第三步,也可直接下载 Markdown/Word。普通策略项目线程生成的文件沿用原文件下载链路并显示同类来源标识。
- 方案生成使用独立项目和固定报告结构,通过 `deep_research_report` 直接生成并打开方案画布;策略优选项目不再出现在方案生成分类中。
- 知识库管理页读取上游最新目录,避免远端删除后命中旧缓存;检索调用仍共享短期目录缓存。
- 普通用户访问尚未初始化的助手知识库时显示明确空态及刷新按钮。
验证:后端相关回归 273 项、前端策略/项目回归 31 项通过;真实 BAAI/bge-m3 编码、指定库向量检索与草稿隔离通过;本地隔离服务的问答项目创建/分组、真实问答、方案整理、助手库创建/编辑/文件入库/向量检索/详情均通过。完整验收记录见仓库 `docs/ji-knowledge-projects-acceptance.md`。不在源码或文档中保存接口密钥。
@@ -0,0 +1,55 @@
# 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 与未捕获异常,数据写入流程由专门测试数据验收。
@@ -0,0 +1,56 @@
# 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 或实际业务数据写入的流程,仍应在对应部署环境补充人工验收。
@@ -0,0 +1,579 @@
# 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 丢失和内存异常。
- [ ] 新增功能与依赖具备持续兼容检查。
- [ ] 离线环境可以重复安装、构建和部署。
+2 -1
View File
@@ -93,7 +93,8 @@ frontend-web/
| 脚本 | 命令 | 说明 | | 脚本 | 命令 | 说明 |
|---|---|---| |---|---|---|
| `pnpm dev` | `vite` | 启动开发服务器(默认端口 5173;项目实际通过仓库根脚本统一为 5174) | | `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 build` | `node --max-old-space-size=8192 vite build` | 生产构建(提升 Node 内存阈值,避免大型依赖 OOM) | | `pnpm build` | `node --max-old-space-size=8192 vite build` | 生产构建(提升 Node 内存阈值,避免大型依赖 OOM) |
| `pnpm preview` | `vite preview` | 本地预览 dist 构建产物 | | `pnpm preview` | `vite preview` | 本地预览 dist 构建产物 |
| `pnpm typecheck` | `tsc --noEmit` | 仅做 TS 类型检查,不输出文件 | | `pnpm typecheck` | `tsc --noEmit` | 仅做 TS 类型检查,不输出文件 |
@@ -0,0 +1,977 @@
# 多智能体会商功能与接口梳理
## 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 |
+34
View File
@@ -12,6 +12,40 @@
root.classList.add("dark"); root.classList.add("dark");
} catch (e) {} } 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> </script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+5264 -751
View File
File diff suppressed because it is too large. Load diff
+64 -11
View File
@@ -4,16 +4,40 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "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": "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: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: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", "build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
"preview": "vite preview", "preview": "vite preview",
"preview:firefox52": "rspack serve --config rspack.firefox52.config.mjs --mode production --env preview=true",
"typecheck": "tsc --noEmit", "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", "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:chrome109-css": "node scripts/check-chrome109-css.mjs",
"check:legacy-chrome-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": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
@@ -36,10 +60,10 @@
"@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "1.2.18",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "1.3.3",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
@@ -67,28 +91,35 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"core-js": "3.50.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.21", "dayjs": "^1.11.21",
"dnd-kit": "^0.0.2", "dnd-kit": "^0.0.2",
"docx": "^9.5.1", "docx": "^9.5.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"fflate": "^0.8.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"gsap": "^3.13.0", "gsap": "^3.13.0",
"hast": "^1.0.0", "hast": "^1.0.0",
"katex": "^0.16.28", "katex": "^0.16.28",
"leaflet": "^1.9.4",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"maplibre-gl": "4.7.1",
"marked": "16.4.2",
"mermaid": "^11.15.0", "mermaid": "^11.15.0",
"motion": "^12.26.2", "motion": "^12.26.2",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"ogl": "^1.0.11", "ogl": "^1.0.11",
"react": "^19.0.0", "pepjs": "0.5.3",
"react-dom": "^19.0.0", "react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-resizable-panels": "^4.4.1", "react-resizable-panels": "^4.4.1",
"react-router-dom": "^7.9.4", "react-router-dom": "^7.9.4",
"react-syntax-highlighter": "^16.1.1", "react-syntax-highlighter": "^16.1.1",
"regenerator-runtime": "0.14.1",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
@@ -106,20 +137,42 @@
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.15", "@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/file-saver": "^2.0.7",
"@types/gsap": "^3.0.0", "@types/gsap": "^3.0.0",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
"@vitejs/plugin-legacy": "6.1.1",
"@vitejs/plugin-react": "^5.0.4", "@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", "lightningcss": "^1.30.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"tailwindcss": "^4.0.15", "postcss-loader": "8.2.1",
"tw-animate-css": "^1.4.0", "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", "typescript": "^5.8.2",
"vite": "^6.0.0", "vite": "^6.0.0",
"vite-plugin-source-identifier": "^1.1.2" "vite-plugin-source-identifier": "^1.1.2",
"web-streams-polyfill": "4.3.0"
}, },
"packageManager": "pnpm@10.26.2" "overrides": {
"@radix-ui/react-slot": "1.3.3"
},
"packageManager": "pnpm@10.26.2",
"browserslist": [
"Firefox >= 52"
]
} }
+3
View File
@@ -0,0 +1,3 @@
# Keep indirect Radix primitives on the React 19 ref-stability fix too.
overrides:
'@radix-ui/react-slot': 1.3.3
+4 -8
View File
@@ -1,11 +1,7 @@
import tailwindcss from "@tailwindcss/postcss"; import tailwindcss from "tailwindcss";
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs"; import darkDescendant from "./scripts/postcss-dark-descendant.mjs";
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs"; import autoprefixer from "autoprefixer";
export default { export default {
plugins: [ plugins: [tailwindcss, darkDescendant, autoprefixer],
tailwindcss(),
legacyColorFallback(),
lightningCssLegacyChrome(),
],
}; };
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52">
<defs>
<linearGradient id="ap-add" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f5222d"/>
<stop offset="1" stop-color="#a8071a"/>
</linearGradient>
</defs>
<rect width="52" height="52" rx="8" fill="#ffcfd1"/>
<path fill="url(#ap-add)" d="M26 14a2 2 0 0 1 2 2v8h8a2 2 0 1 1 0 4h-8v8a2 2 0 1 1-4 0v-8h-8a2 2 0 1 1 0-4h8v-8a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

@@ -0,0 +1 @@
<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: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(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>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<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{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="#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(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>

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -0,0 +1 @@
<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,iVBORw0KGgoAAAANSUhEUgAABIwAAAU2CAYAAAD3cC0IAAAgAElEQVR4nOy9iZPrynH1mVUAe9/u9nZJT/tqy5Zly+tnz/pHT8REzLfa48/SWLIsyZIla3l621379k4CVRNZlQUkQJAEu+/tbrDPLwIBEgRBEARJ4ODkSeMnYwLg1mPMcmvo7+hHapfcTm1uarst+/muGlf93F41q/79uev7Wz+MDFbdnjXY1vzrRLRBRNtEtCn312Ssh42Oaetqfh4yIsqJaCT3c3mNXO6PZJ5Mbo/ksVymtT9sr9Y1V8+3M7aKI6KSiCZEVMjz/YzlljJPIfNPZFp6/oU8lpY5VstN98cyX9dw3jEtzX9GRCdqHqfWVd+eNeh5wDz8im+i2/bz6O74LnlT+9tV94O7+rktu91W/fcErAQ5PkYAAAAACEYJL1p8aQ+ZEnPaw5oIRZtKFBq1hKC1DnForXV/lhhkldjTXpf2tHmC0SyRaBauh2BUKsGoaE0btwSjLlFJi0bjDhGpPS0JUeciGp2p12kPZUvUag8TNeAsBgAAAAAQjAAAAIA7jhFxZ0dcQTzeIqJdGadhWwSgJASl2+nxTSX0aBHJKhePHs+6rcda3Mla9+01+yEWCUzzjqmSoKQdPmWHE6jsGOtpXY+7DuFnLOLRqQxnIippYem0NRzJ/WNxKx3LfBCPAAAAgDsKBCMAAABgdciUaNM1zlsOopEqHUuCURqWEYySowh0Y27gmEsLQ1owOlVjPRyJUHTSEoxOWu6j9jCm6XGJ/QAAAAAYPhCMAAAAgOHDYs0+Ee3JsCvjHZm+I9O2lTC0q4QfXc41UhlBs8rRdNkXuJ0kEe9+qxxtVlnauFVOl8ZJaDoSESk5kJLAdCjT9PilDGfYNwAAAIDhAsEIAAAAuJ20M31m3d5SwtCyghESwO8GmXKfXQZ/CcHoUB4/nZHNdN7KZyru+ocEAAAA3DYgGAEAAAC3iyTy3JfxgYg/99R4XwlD20pQmlWKpkvS4AoCy2KUGFn2KEnTAlEqd0si0nM1fqHuP4MrCQAAALhdQDACAAAArodMRJ3NGa3kUx7Qfg/BaE85h4bAdDt3711Yb0NO1j9Od749L6nn1AHMpvV4bOPcfs5szFJt5HnuRW4so8aGrGlO0883xqgAb1K3+XnNx7yEbRtjG/M1n3+dJLfSxhKvqZ1H8wSjQxnOlPPoQjmSLtQYOUkAAADAa8b4yRjbGNx+zJLHxHe1p4u94rnDTW23ZT/fVeOqn9urZtW/P9e/v1kReB4S0QMZ35PbB3L7nghE26oV/XqrFE23px9d95tYEl9l5TgvreR97Ojl5b6X+6bqHla25iuViORkmhaXSvUY30zzeBnTnL3ZVx3KvJ8nHBkl7szrymZkv7IyvyUyUeixldBTt/mP4o9Vj/HzpTOcyRpd4bx0jEuPG5M35rNGZ0vd9h/TiRKALlolael+Ctx+oQSlFyIo8e2nMjwRAcr1eN1Xh1/xH8jbtge5O96k76b2t6vuB3f1c1t2u6367wlYCeAwAgAAAK5GEnc2OoZN5QZaJBjduyWna04NvhJiojBTCy3kXUvQ8UoASoJP0RKIykoocg3BqBaI0nxOLZ+FIlOtl3Yo1WOj1y0IT/MdRLUIRQsEI1LiziyMKJG2Epd8JQTZxrh+3DamxeVnIgzVAlISjKzRwlFWPe5EMGrPXy9TXtPYhvBlRbSiNK4em/c+r8qop9g56SkYvRDR6HzOgCujAAAAwCWBwwgMAziM+gGH0TCBw+h6eXX726aIPA/FHaTHD2RI5WObHWKSLkd7nSfpfWAhZyICj3THCuNSBJ+J3C+odKWaHsfO6ftlQ8Bpl5h5JQTV82lxx7XueyXq1NOM3Ha+nlaP/YIrt74q+Go+t40uKaO5gp6pStB0mRlJiRlVAlL9mFHPoYaYpO/Xz2sKUM0SNi1QJUEpJ2szcSTV48xKlzsTxZs4/4jI5HI/CVCjW5B3Vc4oSUvDmZSwvWgJSc9kzPcfi9B08UrWCA6j6wUOo5t5XTiMLgccRmAFgWAEhgEEo35AMBomEIyul/77m1G5QhuqTfmmlI7d7xCK2oLRwQ2cdJeVQyg5c6Kbpmzdl8EXjSBj5wolHCUhqZ7uWoIRqTG7huqyMb0du0jlWYvez2W+IDf1pbrMt2f+c+oTinnldTUsKAU3kghE1BKMrAhG1mqBqBaK0vQ45KpEzrZcSVm47+Uxo+a7XgoRjeYJRsmddCoiU9fQ77ODYHS9QDC6mdeFYHQ5IBiBFQSCERgGEIz6AcFomEAwul767W8sEr0hw5tE9Ejdf0N1KttQIdYbKrx645qFIh9cGCzueC8tyv045AdZugjiTuni2NCYSscC0KRRIua9a5R01aVerlGmljJ/jMzrpkrD2rSnNL09elrXfI0pZv7jfZbxeujzrann6T5J6JqoQ75nzec77tVOpFSW5imVxunSs+RMslWuErXuJ8eStSPK7Ig8rVHGopJZDyKTD/s8C01rIjytiTtp7Zrlh1K5jrqcSClw+1MZPhFR6WO5/1gEpflAMLpeIBjdzOtCMLocEIzACgLBCAwDCEb9gGA0TCAYXS/1/ralhuQkWpcSsoMFgtH+NaxzyuSJoo6pcn+ii6cZGs1C0VicQGO5PyHH7iAfhaQ0nXx8jMLjRaPz2Cxxp59Qc8e/yLeOxY6k2e4lJVQZE4O1zSgIQTx4MyJr1oKI5CkKRZkIRjwtCUc81qHdpnImZZLhlDquzQsUf5UcLhCMdCZSciGdKGcSBKPrBoLRzbwuBKPLAcEIrCAQjMAwgGDUDwhGwwSC0fVizKaUi71NRG/J+A25/UDKynZbJWh6eN3OISfCzgU5FoFkzE4hFn1cEIAuomPIFSIOlUE40qVmyTFUh0qn+7Ekrc4a0mgfSzOLp327mb/Tvg1uHv1Nbmc6+anbpmMaNRxLWUdQtlWCkG2UrvkqmHtExuaUWXYkrYvQtEaO+PY6Ed9ncSkITGvXUNJWivhzKsJQEoP49pEqbftEhKSP1PCUvD9/zet3s0Awul1AMBoWEIzACgLBCAwDCEb9gGA0TCAYvQ5sy0G0JblD22TMA3EMzRKMdl7jerkq/yd1A6NG17AUNj0hQyIKVcIRl5KNG0KSCS4hnRvUFHDmO4Mg8IBl6HYhdbuURGaSPCV2JJGIRTwObiS5z04kFpOiU2lNwrjzqmscmeRIyluOpNfF0RzB6Al5/1RcR8l5pIfrbfH/OoBgdLuAYDQsIBiBFQSCERgGEIz6AcFomEAwetXsiAD0tpSTvSO331Gdy9pCUnIQXf7DmLUfxwPCknzIVWFX0HlwKRh/XglCYXAXoUQsCka6/Kyr/XwqS0vt5qu1qMbTXb26unIB0JeurnXVXj6z65wX91EqR2u2/9dlakkgGoUSOCOOJBaTrF0nR5vhvrExK8yE8tFswffusnjlPNKC0ImUrXEp24dSzvahiEsficB0POg9CoLR7QKC0bCAYARWEAhGYBhAMOoHBKNhAsHosmyKOLQtw46Ukj0UB9E7HYLRvSuVlHXtqzEDqN1WvlQOopQplJxB5+T8eRCOWDCydB5KzaKjqFBt53X516zSL4g+YAjMKHfz7ekxrJtDtdlxxEKRNxtBNCKzKSVsG+JMWleOpKxyH9Xj2PWt+zt7lU1WSte1tmD0oeQgPRaXUnIhHcv4bBCfFASj2wUEo2EBwQisIBCMwDCAYNQPCEbDBILRsoxEFHqXiN4jos+IGPSeamW/qZxD2yrQ+tXA+2w80JuQpzPy7iwKQe6cDEUhiG/HMrLoHKodQlFYMlWYdancRF5lySTPhplyBdUCEgQjMARS+Zp2ILWzk2SeqpNbLEUjEYBqR1JeOZTYiRTK2YITaUNK2zaC0BSyyuwmmfAbkKvv7KviQrmOztT4uZS0/V6G36nxs5hPdouBYHS7gGA0LCAYgRUEghEYBhCM+gHBaJhAMJpFppxD2kX0QErOugSjg0u90uyyFhdyhXzIFtLZQ4U4hziA+iyIQ14EIx7HErNzaWE/lueYDsGHUCIGQCcd5W0twYndRFkK0OZSNRODtPk2C0ZpHMTiyokkWUg8Dr8xuZTFTXO5kzkvZWsfiOtIC0ZPZDjpcCCVt2I3gGB0u4BgNCwgGIEVBIIRGAYQjPoBwWiYQDDqYltEoCQKpXFqcb/bEpG2pcPS1Uj7YsgH4rwhfxaH4BY6C2JQyW4iKSeLzqGicgrFk754P2UPxVDrhGnlCyFXCIBpuvKSdE6S+pVSpWh1KHYKybZhbM0olrUFF1LMQiKzFfKQjImiEmciJeHo1ZzEjVuC0LEMqaV/24H0oTiWbhYIRrcLCEbDAoIRWEEgGIFhAMGoHxCMhsndFoyMiD87anwgJWddgtEbUl625KvMzB5K7qFC3EOFtLQfizjEbqHT4BgiEY/K8jzkDpVu0uhOBtcQADfFLDdS3a0tOpE2KMuiSMSZSEksymyathZKXqMLKRcXUspCmv4uX+5k70REoy7BiN1HhyIsHanx9f0qQzC6XUAwGhYQjMAKAsEIDAMIRv2AYDRM7q5gtCWOoc8R0WdlnMSh+y0H0ZaMr7axUo6JDyHUp2FgIYjcKZXuNIpEJY/HzfKzKnNIl6TpVvZwDQFwM3S7kaj6VkqAvOrSxmKQl3I0PS20+M+2gniU2S0iHoKwtBWHICa9ipM8L2LQqXIenUrGURKRfivDbyRc+3pCsyEY3S4gGA0LCEZgBYFgBIYBBKN+QDAaJndDMNJdzNLAYtFbHYLRWyHUdhm6HUSpPEyLine truncated

After

Width:  |  Height:  |  Size: 664 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 51 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="273" height="323" viewBox="0 0 273 323"><image width="273" height="323" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 193 KiB

@@ -0,0 +1 @@
<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(255,173,32,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.761" y1="0.205" x2="0.284" y2="0.581" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#ffd2ab"/><stop offset="1" stop-color="#ff7855"/></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="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.237" y1="0.761" x2="0.718" y2="0.394" 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="#ff941a" flood-opacity="0.4"/><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>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<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(255,173,32,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.761" y1="0.205" x2="0.284" y2="0.581" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#ffd2ab"/><stop offset="1" stop-color="#ff7855"/></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="#ff941a" flood-opacity="0.4"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.237" y1="0.761" x2="0.718" y2="0.394" 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="#ff941a" flood-opacity="0.4"/><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>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<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{opacity:0.78;}.b{fill:url(#a);}.c{fill:#fff;}.d{filter:url(#d);}.e{filter:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#fff9c1"/></linearGradient><filter id="b" x="104.221" y="19.443" width="226.559" height="61.289" filterUnits="userSpaceOnUse"><feOffset dy="1" input="SourceAlpha"/><feGaussianBlur stdDeviation="5.5" result="c"/><feFlood flood-color="#fe0" flood-opacity="0.71"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><filter id="d" x="104.221" y="19.443" width="226.559" height="61.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 class="a" width="458" height="113.588" transform="translate(731 564.412)" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 625 KiB

@@ -0,0 +1 @@
<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.405" x2="1" xlink:href="#a"/></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>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="188" height="241" viewBox="0 0 188 241"><defs><style>.a{fill:url(#a);}.b{fill:#fff;opacity:0.47;}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 376 482"><image width="376" height="482" xlink:href="data:image/png;base64,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 truncated

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,13 @@
<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>
<linearGradient id="ap-idea" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f3212c"/>
<stop offset="1" stop-color="#aa071a"/>
</linearGradient>
</defs>
<g fill="url(#ap-idea)" transform="translate(12 10)">
<path d="M22 2.2A12.8 12.8 0 0 0 9.2 15c0 5.1 2.9 9.5 7.1 11.6v3.8a2.4 2.4 0 0 0 2.4 2.4h6.6a2.4 2.4 0 0 0 2.4-2.4v-3.8A12.8 12.8 0 0 0 34.8 15 12.8 12.8 0 0 0 22 2.2Zm3.3 26.8h-6.6v-2.1h6.6Zm-3.3-26.8A10.4 10.4 0 0 1 32.4 15a10.4 10.4 0 0 1-6.3 9.5 2.4 2.4 0 0 0-1.3 2.1v.3h-5.6v-.3a2.4 2.4 0 0 0-1.3-2.1A10.4 10.4 0 0 1 11.6 15 10.4 10.4 0 0 1 22 2.2Z"/>
<path d="M17.6 39.2a2 2 0 0 0 2 2h4.8a2 2 0 0 0 2-2v-1.4h-8.8Zm-1.8-3.8h12.4a1.6 1.6 0 1 0 0-3.2H15.8a1.6 1.6 0 1 0 0 3.2Z"/>
<path d="M8.4 22.6a1.6 1.6 0 0 0 0-3.2H3.8a1.6 1.6 0 1 0 0 3.2Zm31.8 0a1.6 1.6 0 0 0 0-3.2h-4.6a1.6 1.6 0 1 0 0 3.2ZM7.2 10.8a1.6 1.6 0 0 0 2.3-2.3L6.2 5.3A1.6 1.6 0 0 0 4 7.6Zm29.3-2.3a1.6 1.6 0 0 0 2.3 2.3l3.2-3.2A1.6 1.6 0 1 0 39.7 5.3Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,14 @@
<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>
<linearGradient id="ap-task" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f3212c"/>
<stop offset="1" stop-color="#aa071a"/>
</linearGradient>
</defs>
<g fill="url(#ap-task)" transform="translate(14 12)">
<path d="M28.4 2.2H15.6A3.4 3.4 0 0 0 12.2 5.6v2.8H8.6A3.4 3.4 0 0 0 5.2 11.8v28.8A3.4 3.4 0 0 0 8.6 44h22.8a3.4 3.4 0 0 0 3.4-3.4V11.8a3.4 3.4 0 0 0-3.4-3.4h-3.6V5.6A3.4 3.4 0 0 0 28.4 2.2Zm-12.8 3.4h12.8v2.8H15.6Zm19.2 35.8H8.6V11.8h3.6v2.2a1.6 1.6 0 0 0 1.6 1.6h12.8a1.6 1.6 0 0 0 1.6-1.6v-2.2h3.6Z"/>
<rect x="11.4" y="24.2" width="3.6" height="11.6" rx="1.2"/>
<rect x="18.2" y="20.4" width="3.6" height="15.4" rx="1.2"/>
<rect x="25" y="27.2" width="3.6" height="8.6" rx="1.2"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 924 B

+6 -2
View File
@@ -40,6 +40,10 @@ window.__CM_RUNTIME_CONFIG__ = {
// Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。 // Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_LANGGRAPH_BASE_URL。
VITE_CANVAS_LANGGRAPH_BASE_URL: "", VITE_CANVAS_LANGGRAPH_BASE_URL: "",
// 地图瓦片地址模板:仅供普通问答消息列表中的交互地图使用。
// 后续接入内网瓦片服务时,只需替换此项并保留 {z}/{x}/{y} 占位符。
VITE_MAP_TILE_URL: "http://119.254.86.251:8023/tiles/{z}/{x}/{y}.png?v=zh-z14-13",
// routeKey 短路由的用户名直登口令:例如 #/login/admin?routeKey=a。 // routeKey 短路由的用户名直登口令:例如 #/login/admin?routeKey=a。
// 填值时优先使用这里的 password;留空时使用 URL 中的 ?password=。 // 填值时优先使用这里的 password;留空时使用 URL 中的 ?password=。
VITE_ROUTE_LOGIN_PASSWORD: "", VITE_ROUTE_LOGIN_PASSWORD: "",
@@ -94,10 +98,10 @@ window.__CM_RUNTIME_CONFIG__ = {
VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope", VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope",
// 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。 // 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。
VITE_AIPI_BACKENP_URL: "http://47.88.25.99:8000", VITE_AIPI_BACKENP_URL: "https://knowledgetoolserver.b.uat.4.cn",
// 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。 // 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。
VITE_NOTEBOOK_API_BASE_URL: "http://47.88.25.99:8000", VITE_NOTEBOOK_API_BASE_URL: "https://knowledgetoolserver.b.uat.4.cn",
// 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。 // 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000", VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
+335
View File
@@ -0,0 +1,335 @@
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,
};
};
@@ -0,0 +1,122 @@
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;
+64 -2
View File
@@ -28,8 +28,8 @@ const requiredLegacyFallbacks = [
}, },
{ {
file: "src/components/ai-elements/prompt-input.tsx", file: "src/components/ai-elements/prompt-input.tsx",
pattern: /<InputGroup className="h-auto min-h-16">/, pattern: /<InputGroup className="h-auto min-h-16 flex-col items-stretch">/,
description: "Prompt textarea must not rely on :has() for its parent height", description: "Prompt textarea must not rely on :has() for its layout",
}, },
{ {
file: "src/core/utils/mermaid.ts", file: "src/core/utils/mermaid.ts",
@@ -41,6 +41,68 @@ const requiredLegacyFallbacks = [
pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/, pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/,
description: "Generated HTML charts must downlevel modern CSS colors for Chrome 109", 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) { for (const file of cssFiles) {
@@ -0,0 +1,98 @@
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())。`,
);
@@ -0,0 +1,48 @@
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)。`,
);
@@ -0,0 +1,39 @@
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);
@@ -0,0 +1,31 @@
/**
* 把 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;
@@ -0,0 +1,164 @@
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 };
@@ -0,0 +1,47 @@
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,10 +2,12 @@ import postcss from "postcss";
import { transform } from "lightningcss"; import { transform } from "lightningcss";
const LEGACY_CHROME = 100 << 16; const LEGACY_CHROME = 100 << 16;
const LEGACY_FIREFOX = 52 << 16;
/** /**
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the * Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
* resulting CSS to Chrome 100-compatible syntax for both dev and production. * resulting CSS to the supported legacy browser syntax for both dev and
* production. Cascade layers are flattened by the preceding PostCSS plugin.
*/ */
export default function lightningCssLegacyChrome() { export default function lightningCssLegacyChrome() {
return { return {
@@ -15,7 +17,7 @@ export default function lightningCssLegacyChrome() {
const output = transform({ const output = transform({
filename, filename,
code: Buffer.from(root.toString()), code: Buffer.from(root.toString()),
targets: { chrome: LEGACY_CHROME }, targets: { chrome: LEGACY_CHROME, firefox: LEGACY_FIREFOX },
minify: false, minify: false,
}); });
const compiled = postcss.parse(output.code.toString(), { from: filename }); const compiled = postcss.parse(output.code.toString(), { from: filename });
@@ -0,0 +1,8 @@
<!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>
@@ -0,0 +1,48 @@
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;
@@ -0,0 +1,377 @@
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;
@@ -0,0 +1,163 @@
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;
+294
View File
@@ -0,0 +1,294 @@
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;
+3
View File
@@ -36,6 +36,7 @@ import PublicLeaderboardPage from "./pages/PublicLeaderboardPage";
import PublicRoundtableSharePage from "./pages/PublicRoundtableSharePage"; import PublicRoundtableSharePage from "./pages/PublicRoundtableSharePage";
import EmbedChatPage from "./pages/EmbedChatPage"; import EmbedChatPage from "./pages/EmbedChatPage";
import EmbedRoundtablePage from "./pages/EmbedRoundtablePage"; import EmbedRoundtablePage from "./pages/EmbedRoundtablePage";
import SiteStudioPage from "./pages/site-studio/SiteStudioPage";
import DashboardStandalonePage from "@/roundtable-planning/pages/DashboardStandalonePage"; import DashboardStandalonePage from "@/roundtable-planning/pages/DashboardStandalonePage";
import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage"; import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage";
import WeKnoraWikiPageView from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView"; import WeKnoraWikiPageView from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView";
@@ -205,6 +206,8 @@ export function App() {
element={<DashboardStandalonePage />} element={<DashboardStandalonePage />}
/> />
<Route path="/official/*" element={<OfficialDeerFlowEntry />} /> <Route path="/official/*" element={<OfficialDeerFlowEntry />} />
{/* Web App 站点预览工作室:独立路由 + 独立视觉风格(无主应用 chrome) */}
<Route path="/sites/:threadId/:siteName" element={<SiteStudioPage />} />
<Route path="/page/*" element={pageElement} /> <Route path="/page/*" element={pageElement} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52">
<defs>
<linearGradient id="ap-add" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f5222d"/>
<stop offset="1" stop-color="#a8071a"/>
</linearGradient>
</defs>
<rect width="52" height="52" rx="8" fill="#ffcfd1"/>
<path fill="url(#ap-add)" d="M26 14a2 2 0 0 1 2 2v8h8a2 2 0 1 1 0 4h-8v8a2 2 0 1 1-4 0v-8h-8a2 2 0 1 1 0-4h8v-8a2 2 0 0 1 2-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

@@ -0,0 +1 @@
<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: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(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>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<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{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="#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(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>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 1.7 MiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 1.9 MiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 608 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 50 KiB

@@ -0,0 +1 @@
<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,iVBORw0KGgoAAAANSUhEUgAAARYAAAX6CAYAAAA2hEelAAAgAElEQVR4nOy96Zcj2Xmf+UYggMyq3tVNNilqsTZTsiQvsjRjnxl7jv1x/to583XmzLF2L9pF0ZZIbSZFNltk71WVmUDEnRPAvZkXF3eLwBbL8/RBV+WCACoT8eIX7/J7i//7W0r6UKTvY77F+wD9HrU/nR/vyCd46X8fwJAo+W0AwKmp+h7PvCN3UC6hQwDAxECxAMDJ6a1YDBmyI0PUdD0kAAyZowPLEaQCDgEGYKScO7AUPRRLCAINwEi4pmIJ4QYiN09MgAEYOHZgOfWlif39fVRL4fwZenwCDcDAGKJiCZETaAgyAAOg6qAmOqkOpU9y606nyrW4hAINQQbgSlxSsZw6R+ILVAUBBuD6VObEU6dXFEXg7+eC8jXAQLiUYvGd1Odq9fflYlAxABfEDixK//+k6kI9ncS7P4uD3pZLq5n2eTTOxwBwQq6pWHzkNNR1DQTu8agqAZwZX2BxT67TKgrlVTDiCSrnVDL28KX9fFAyACfgWn0sMbUQUhj2x6fKw/g+h5IBOJKcwHIpBSOOYjAKpkwEm1MGGVfJqEDyFwAiDK3zNlS9iV0m+QJd30DgHtcElULcJDTBBiBIn8CinA/OlQsxj7NVMcV+GdlWMb6hxVM+J/fSy1YxTeR+ALNlDLNCMRUj1uVLKAl8zPCi+xhiBRTlqJrcYwJMnlMElvPmYJ4exJeLKYqnE7wIWG2eo6PYPI6bg2nIyQCMa7rZxlYIhXVT+qRXnkulU1STDL5jhi6X3K8BTJ5zBJbQSXRuJeP+6VaVcpVV3wY8+xLMrSg1BBeYE2NVLC72SVvrPwtHvRQBFWM4xYnvHjeWiyHQwGS5ZGC5SC7GebzHXIxzS/XGnAo7FyMeBUNVCSbJVBRLiFAuRjI+PsVEtFtVMoGl1EGl4DIJpsg1A8up+2Fycibm77UJIsVhRSnUH3MKfIFNCC4wNaauWCTi+m8neUtLScTyMKd8TqWTeyGwwGQYUmBRzkddT+he1Si1n+uoi2LvpA/lY06FHdQILjAZ5qBYclCOgrFzMspzCSMRJZT7eOIJLHXifgCjYMiB5VT9MCE7Bv83u34xOslaFAcqxp1J6qNq7OM1ViADGDUolidSuRjlVHdCVSbfMVKPW1rBCtUCo2eMgaVvP0wn5eLcr1Bqr3LTFMXjsRaZvjEhzPcsZKeYqBDB6EGxhIn5vKg26Vsczic1Ttk6N4i5lSlUC4yaKQQW77t7pC+mr3LZO0ahdEWpkKZ4uoRplU3pKJmcAONr2kO1wGhBsfSj0IpFPGXixlEfiw5BjFwLTIIpB5ZULiZXuYRa/Au1++z2j+LpKOaSqNFBxe3oTT3OQv9JrgVGi88YCXrgyBblBBg7QMQCTOncAEbJnC6Fjs2thJSLLhsFH7OxelRSXbw+qwdUC4wO3hVPRKQfv+loj3CJUQKAs0JgOX5GZ6/q47kkanQiNucxzPeYpG+XxC/AYCCwnJ8+uRa7G5fAAqNjjuXmrtWg0P26Dh3Wst+nknoOC32/TYfHARgEk1QsA812uqolpVhQLTBaJqVYlDydticILsdVkfbvZSuWLj9z08Hru5QCGCwolssRyrXEsFULwGiYc0t/KGcSyn+Evs/7sdKfLQ6Vi70tMXU5JFauJbeyBHB1eCe8LMrpa0kNKNKJC6NkEorF5FZO/HYeyrH0qQrZx6wtFZJTmSqtvhZUC4yCUb8LjtR9uk+ehVwLjIpRKha7+iPXDy5dcy1GtdgzRCF8nbhUh2DwoFiuQ9NxhqiwgguqBQYPRk9PAiikPEJk51qKw4SKncTNvcRZZCgcgEEwmsCiAn8fMbVl6pQKUubrdicul0MwWEYRWEJVn4GcWcmZIuPX4qSFaqvK08UTd2HlaAguMEhGpVgmdha5nbg5lzh2EneCPxKYCiQCnwidqCc5gQMHUZnqw1ZBC/2GQK4FBguB5bo0jhFUKljYlSF+dzBYBn0pZOdWrqD5u043e3MtSu1/g9lyZn1PbSVycztx7Z1FXA7B4BhcYHk8Ea8XUC6N289SJvpbCmd+iP1DMDgGFViUCSZq/3MjZ686ZOugYt8T1w4mIeVit8Tg0wKDZZCKZQLBpAvK6cTNyZ2U+ndXczkEQ4TO20OO3T9k6DIFbedaYvubD6pDSmR95PMEODlXrywoS6XMTKnYNNo0e9PDToHqEAyOqyqWNpA0w8+puM8o5DCXcvHf78Q9HNDeWHmT1O+lcJrl3BwNwFW5eGBxg4ibrJ0xff1wTV8LnbgwGC4aWA4Uyuld365JJ2e5gCeum8TNqQ5VWu1QHYLBcPHrczufwllwQNNRuZS0+MMQoSoUxvVpcUlVj0I5mL1v8Pi0mOqQnZhNVYfY8QyD4uyBRU330ucc2EncKqNHxc2z0NMCg+CsgcXNqZzJTf9SKL/IOCDXtCk0SV07VZ7cVawL674AV+UsgcUEkMZTTgbPD8ufwFUZQcxgd+KSxIWrcx7FogNKM71Ln1zVYuPPtZifjXXxsufkrx4DTCx/4s4OLawmO4CrcZaq0GM3Lb/XNP4fUuNYV6YwiqWiExeGAFWhNEZhdK26ZOda2iDcqhXrktHOtVRUh2BsHPXuVuydHc7MD3LlGHyzQ6kkrj07RHCBq3KUYlHWZU9tBRNlfwP0/dG6l0Kx4FI4qqUkiQvXpFdgsXtTzN8bVMpReLLBXbtwxWrxXzOUCNekU2B5LGCYUrKVoJ1RSTmVM8n9/N5xCqtKZN3BdpdLXbYqp8U/N/ELcHI65Vhs96HGDi68fI8m8CO0cy0561ULqkMwBDopFvdqp0sH1wTIdZZLzggFvj/VieuuY/Udh8ACgyD64nOrPo1zCZQqVcDRKEuxuL+S0K/MnR0CuDhJxWJXfeyA8tgtyu8sF/tHlXvxaAKLWx0K4c4OUR2Cq1D5XqV256zJp9SWhwrB5DzYZ7/HSqHrjufKqhARWOCiVJajwSOuQhFa9H0E4sCBsvDlUA6+31WC6rA6tHEMnUIWCbaz3MK6jAK4GFXtaRJvrMBinwkolfMSijxOdWiRkZh1FQtWCnBRqo1+ybnxxQQVFXlrhEciMSGbPWXj/LxDnbg+Ckex8H4AF+dQsTjwqhwEqufeoQpnObgGle/VRjCJktvP4pL6/tiJb1eHujjL2bNDXA7BxSjtfZ7kUQbEYZhRPWeH6GmBi1MhkI/mFPmVPSLOco2lXFLVIXGSuMwOwcWg7XuohJ3lNh3sJ2nxh6uAg9xpyU2s2rgB4qk6dFiSc138U4vK2sugpQ5E91f9ycCs4F1sXDTO7FAqd1I6CVyAi4Bi6Y/pN8lVICGi9y8Ov+abHYr1tCw8znIAZ4V3sYHjMebr4yzHxDNcFG8fC1yE5LSz0s5ycjg71PY15pg/mePa1SGmneHsoFgGTsBK2HWWS8HeIbgovMhOhwrHgSS+PMne59T+zdfiH+t1LHV1aMmlEFwCAss4cftZysTvEsUCF4Wq0PH4Zodi/Swxj9tcl7mu1SETTOyJZ/IscDZ49xoBnmssZamW3PJxYa0GWWR8P0BvqAqdj1wbyaRKCTjL2Yvj64T69FWHmB2Cs4FiGQGRrHDjsVOIwewQXARyLKcnd1NizAs39TmxPrex/HBDwaKwkrzb6pDazQ7h0QJngXetceP2s1AdgkGAH8t56bItJb3judDrbP3+LKnqkPn8kgQunBvetcaN6tiB67OsBDg5vLBOR6jrNtWN6yqM4PcqtX/TuAncVP+LOJdEdOLCySGwjB/X/CkHE1iWvAbgHFAVGh4h9RJzlnMXmqW6ftvvWellZg9Uh+DU8G41DZQOEraVQqo6tKQ6BOeCztvz4aqNlLOcz0kumHvxrGBd69tNxjYXLoXgrPCiGime2aGu1SHbspIELpwUAsvp8VWBTuXR8nQw9WTMYiVwuwwliqVcCC5wUggsI8UTvZQzlJjTnEfZGc4CVaHLcczO59zZoUY9XRKViTxLYe0dWvZQOwBBUCzTwp0dSq3jxrISzgKK5fzYyiInkHeqDnlWg6ydFay+ipTdgbukOgSnhhfTtHADS06OZUE/C5wa+lguSyzPEsqdxHIqoU2JaytfkpNnsa0UyLPA0fAuNXKUOog4bmXIkMq1UHaGk0GO5XzsTmrlde/POeFDrv9q75PFUz9L8XSP2pPAjT1OqWeH8MKFk4BiGTkexSKevUMpaPGHk4KD3OVJ9bPEfG+bxPeJ9X1mdqhKKFPbC3elRO6G+EODccG70zRpAkncEEw7w0khx3I5uvazuPTpZ1k794t54ZpLIbxw4Wh4d7oeJ7sIDWxKdKedY4qFaWc4KSiW82OGkX1qI3Wyux/7e1mcBK6uDu0uh5Q3L+N7LGXNDlUdEr8AB6BYrsMxNgoHwSFwMBVYDxLDVIdWqBY4Bjpvr0dIgaRO6PC0s98L1/ja3lj5k9js0Eqrlge6cKEvKJZx0VVF2EncnCDxWHbmtQHHQI7levT1Z4lWhxwarTzs6lARCTILS7EQWKA3vHgmhDrsxHU7cHP8WdjtDEeDYrk8x/qzeHMzSpedlPXVojhYCxI6pv15O7gs2DkEfeBd6fp0zZ9Hq0Oe2SG3MpR6vIJpZzgWFMvl2PWzHE47S08fXAlWhw6p1VOL/9L6/lCQsZ3lalQLdAXFcl0uVe1vApdEPpRTduY1Ap2pFI0s1ybUlxJTMU2P2SFTHVpZlzu+IGO797ff+2pcP04YArwbzQPXvT/1e0exwFGQY7k+faaeoz64HrpMO4tj/MS0M3SLine truncated

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 813 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,48 @@
<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>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1 @@
<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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 truncated

After

Width:  |  Height:  |  Size: 664 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 4.3 MiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 968 KiB

@@ -0,0 +1 @@
<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

After

Width:  |  Height:  |  Size: 53 KiB

@@ -0,0 +1 @@
<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,iVBORw0KGgoAAAANSUhEUgAAARYAAAX6CAYAAAA2hEelAAAgAElEQVR4nOy9aZfsSnaet4FEDWe6t+/U3ZfiJImDJEvysklZ1rIsLemjf4F/pv1ZXl4iKZHiZFEkm4NEdjd5SXXfoe9wphoygfACEJEZGRkRCCAnIPE83XlPnazKrDpViV1v7OHd2f/5fytJJc9ElBJ5KEVeLUXuVyKVEskykWz3Odq7lKR/ggEc9cltAp/oZJ8fYELkfb5UcxHVgaTI21sdPepgo7jCAEBT9PpGqDa4XOUiz4pWwbxVIo9le79HtUj4bn7ZA1wqvQKLiQR1QLle6PuK9h3LSh+LtKIBgPnST7FYAaaOHVcLkac6kLx9FHmsvMqlb5hByQBMnGGBRR+J1spFJ3VlJbIsk5VLV8AhwABMlEGBxbBWLrnI0yutXNSWcskGKJbYpwOACbBfYDmMcnFxP3pdjCLAAEwDO7AMPpoElItylEtfzGO6AgqBBmBk7KVYDDvKRTbKZVVZPS4DI8zm0Vt/+gIKQQZgBBQ9ciCdH+dUi1SjXJbrPpcsP86VHwo0BBmAM3EQxWJwlUslIg+r9mikVBsA9IccIqPre4qMAANwfgpPcvRQNAqlbvu/UpJVOvDUfx7pUqd8DTASDqpYDM0VrM9FtXLJc1F1hais2lJ01cqWdSDIth+7bwDw5WJQMQAnxA4sB1MuJlm7yERuFqKu2wNKUyXKlvX/JVObj83EOsNUcpSBRid2tcLJ+jsAHJCjKBb7am4moLPWPCHT0aOoNgGk3CgYE2S8SsZEngGKxg2UVJUAjowvsLgX12FyL5lIXYm+KURdt59B1aXoZSlZPcBYVpI1/9sMG+18XqXvOYCise0ilPUnSgbgABxVsYislciaItMKRh+VGpOoUqTM2w+rVUv9xkp5cjHt85jws09CJlRREpQMwP6kBJbDK5hs88dVLmphRgEyqRoLBtWUqbNazSiRXG0rmfrOrBkf0F/fngrG/ve4Ssa+AUAiR1csLq6CqXteFiZg6FxMri/nRSaqPptUSrL6qFSZSCI60LTBxjeKpHbeSMN+qswKKplzZBJUDECYIYHloApmXRKy7mhyMXU1KW8+WVW2LnXrXIzKJK+DjfbazZyvYK1t1FrS7PMV7jTd2SqmijwOYLacXLG4rHtenKvX+OnWd5e6opSbXEzrsdsomFI1QabNzbSqJ7Pjla/T1/PpQpiHuUekylEyqBgAi0MEluNUkaxnykO5mEqyh5Wo+s9Sxx67GUcZReN5WvdI1vOrMoHGzcFUzt8BZsnZFUuIpFxM3qqGvM6/1MFFNeomr9qEbqbVkCleZybfq5TnCNYv0NjdvT614gYcAg3MimMEltBFdJxcjO6LUarti3msGgUjOtnb5GPcPImOMCH/qb5VJjvIuGMEyjk6AcyC0SoWl2AuxuqLafIylZSLNhdTB5a8Djh1gFkpHWh0L4wxoLIC1tBKko07pxTLxRBo4GI5ZWA5SS6mriQV2TppWzXNdpVk96umqlRPWTe9Mesj0uYrWysYM3pwgCvfzsWIR8FQVYKLZDKKJYSbi1m4uRhpkryNalhUbT+MyLqq1CqZqkn0mk2x67hygKlrt6pkAkuug0rGMQkukXMGlkMrmMzzVvN5minrovGFUbqqVNaKpe7svVtJ9qi0irHdM63uGKXPTge4+rNATobgAhfF5BWLiycX01y8dXApdC5FSxJVV5rqo1GerStJudXhu1Y2cvir3pSs7dwLgQUuhjEFlq0LS6neCiZYjXIrSrZfzG0h6mahczFKyoelZI9Vk/TNVKtb8qaCdBjFsvO16QBDcIGL4uIUiw+jYnxX7VXWeGjWfTFq2V7adV9MU0lqTkBtb0xWJ4BXusM323T42vQJCnYfnx1YykP/2wHOwZgDy6H6YeyLOIzVF3Ol+2K0CXi1bKtKuSqbJrxGxdgzSQO/LrHyLHYyF9UCk2cWiiWGVVVqAkOxSamuczGLtqZT52TysnW9a8YIqla9ZNnGmGqIijGBJUe1wKUwxcAytJqUplzcj8hELUSy20KqeuK6Tuw+lFK9afcl1X0xi3w7Gdv3azMfo1e9USGC6TN7xeJiVZUy++qulUyuXe7yrMm7NCNHy7IJNnWBqVY4ldWz4lMwIexcS45qgalzCYGlby4mXbkEHni1EPW8rSqp10qqej6pPiFl28plYQWXrs/l9raQa4FJg2JJxO7wzTPJrhftUECtXKS2bihbj966NSbLttTHokcQI9cCF8ElB5auXEyqctkx2TYDjFe5yPOrtrP3tWqXselRgErnShbO0Sjl8yz0JyHXApMFxTIAd0e1Vi4iq2adSa1clO51MQEmd447oYBh97SQa4HJMqfAMji34jzOVS6ilYtsKZf2WGQmmCvZzr/EPofdI0OuBSYJimUPdpSLtMol08pF2qhQZZtjUQpu0xyBBSYHgeUASkbLC3W1EHmuI8gbEXlcacWSNUeaPjucTNKXGSKYJDk/tv1Ral0RapTLzaJVMbIZUao86iPlSNR1dAIYJXNULH2rQaHH7agIsxnAsxrAlI/dvpbY12AUy6rj6wQYHSiWA6Is9aJ3HdnRx1UtXYoF1QKThcASZnBuY+3/slEuRrGUPZ8zs3phCC4wGQgsB2Sdec3aPIuda5FwriWGrVoAJsOcq0KhnEko/xH6uM3f26NPUx16pv1d3qya6lD7AVl7UrLUUNdxSKxcS1+1A3A2+E14QLYUS9FWiBobfrXe82GrFkkYUMydG8AkoI8lTCjJGqwKuR8g2fa6RbVRHosExSLOUWiBaoGpwG/BI1BplZKbvpZis+tIDcuzkGuBSYFi2Z9grqXQuZZ8O9eidCeuPUMUwu3EXdDmD1OA34BHwFz1C6s6ZCkWsYJK6orVzAou/Mxg9KBYuqtDIZJyLZFOXNtSISVYLBIUDsAo4LffEenoxC17VIfoxIVJgWLZn86ZooRO3D6euAsrKJFrgVFCYDkibq6lqkRW1dZqabtClKJC7CQudgowWggsG0K5leF+LfroE6gONZ24lvqIHUvtr6EOKoVS2FbCeCHHckRMNCisTtxcNjkXS7GkHonsyhA/OxgtKJYwfZVKdL2qNtfe/gC1lWtJWRNil53xxIXRwm+9E7DTibvwduIaun4mGfNDMHZGp1jU5f3+3erEfZ5pN/9l64mr2gVnpRNcQsols45MiwE2DAAngd94JyDWietUh1KDRK5/KdDTAqOEwLLLocq4O01vJs+ShT1xy47czk51iMACY4TAckKUzrWU+lZtOnErbZq9GmCnwM8QRsfZcywTyKmEdkD3dfHf6sTNtr1ajBu/yZt0/Vwyp1nOzdEAnBXKzWfA14lrbUzs64dr+lroxIXRcPLAcoFVH0PntLOyOnGd6lBLtpXEjSVm7epQodUO1SEYDZzPT0hPn5bU6hBJXBgdHIXCNBe2Cl+wXZ250U5cZ9rZ/hhTHbITs26QcatDKV27ACfjJIqFzebb2NUhx6dFDawOsdQMRsVRFYuS3Wgy4eCinNxGiK5cS6pPS+V8fOi57OBiHgtwVk5yFEKpbGMCbsCnxc6x9PFpKTCAgrFwlMBiLpwLfHWnqhab3VyLqQ7lIs8Kv0+LNZwYy5+4s0ML6xgFcDaoCp2BLcVSBKtDfZbIG8VS8DOFMUBVqJv2wla9E6OduZbMCDsrqKjtjYmlEyyoDsEkOOhvN6UrHAjxNMy3qkqbHepK4tqzQwQXOCsHUSxuN62vGgRhArNDvmnnWK7FVi05SVw4J3sFlp2Awsu4FzEXf2t2qO/8UP0zXTKUCOdkUGCZeUDp6k9JvX9THTKzQyLyuq4Obay1K1FJLv7m67Jb/FMTvwAHZ68cyzqnAsO+f/rPndkhf64lZb1qRnUIxgBVoXT67nQ2dIXeTHfgqi1nObX+r+3iH/v8GYEFxkKvFx8K5ThsOctVba7FMzskPapDzA7BWUlSLCRpD4J9kXu/g9lu5DCBpcsL132KnOoQnJNoYCGgnIatXEvRqhdnx7M9A9RndshUiPjJwUnxBhYCShL2d8WnRlxv3PDHdzjLZVkbaxxDp9AWRNtZbmEdowBORhELGgSU02ArloXuZ7nT4UBHCbs6tEjIjbmKBSsFOClJigU6CamRPmxVhyQ8O5TiXGcrFhK4cHKiigVOi9JB3d47pMPCUGc5e1siP2k4GfSx9Ce1n8Wl6+PXF35k71BfZzl7dojjEJwMmqhGRIeLvxqwBD6jpwXOAYFlfw7nFa4bEOvq0Ivr9la/bVlRVD37WuwkLoEFTgaBZUR0KBbx+LR0QYs/nAVebIcl9WL3NNlu3t+kV3SOJbJ3qPI8l/uc9THoSt/4WcPJIHk7QiLVocqZHQpZVor1/kVi7wvAwSCwDCeU43Av8sHT0B3VoVRnuYXHWQ7gqBBYRkjMWc6z3zl1dsgEFlyJ4eggj89HOM8SqQ7phsbSMn+KBQnzPqpDcFIILCOkZ3Uo5WjD3iE4KRyFDsc+80LeCeid2SG1NTtkAktKsMit6tDDqb4hMF8ILCNmqzpUBatDygosIfWCYoGTQmDZH191KJZUjW1I9LrM9XCWC31OE0zsiWcSuHA0CCwjxs613BatellW69xuvXcoNcciVukZAyg4Osji49FnULine truncated

After

Width:  |  Height:  |  Size: 53 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,47 @@
<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>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.9 KiB

Loaded 100 of 1546 files, more files were not shown because too many files have changed in this diff. Show more