From a1fa13a2386626d94e6419387e789fd6c2bf00be Mon Sep 17 00:00:00 2001 From: gozeros Date: Tue, 10 Nov 2026 14:04:00 +0800 Subject: [PATCH] sync update from LAN repo (source dhk-1 @ c21a2a4a) --- .gitignore | 5 + AGENTS.md | 20 + frontend-web/.env.example | 3 + frontend-web/public/runtime-config.js | 4 + frontend-web/scripts/check-chrome109-css.mjs | 15 + .../src/assets/img/top-bg-light-red.svg | 1 + frontend-web/src/components/page-sdebar.css | 19 + .../src/components/wiki/WikiHeader.tsx | 24 +- .../workspace/agents/agent-gallery.tsx | 15 +- .../artifacts/artifact-file-detail.tsx | 12 +- .../artifacts/artifact-file-list.tsx | 4 +- .../components/workspace/compact-welcome.tsx | 26 +- .../src/components/workspace/input-box.tsx | 5 +- .../workspace/messages/message-group.tsx | 23 +- .../workspace/question-navigator.tsx | 180 ++++++- .../settings/app-identity-settings-page.tsx | 510 +++++++++++++++++- .../settings/appearance-settings-page.tsx | 3 + .../settings/call-metrics-settings-page.tsx | 68 +++ .../knowledge-batch-create-settings-page.tsx | 200 +++++++ .../roundtable-room-settings-page.tsx | 54 +- .../workspace/settings/settings-dialog.tsx | 15 +- .../settings/skill-settings-page.tsx | 148 ++++- .../workspace/skills/skill-detail-dialog.tsx | 311 ++++++++++- .../src/components/workspace/thread-title.tsx | 22 +- .../src/components/workspace/welcome.tsx | 25 +- .../workspace/workspace-nav-menu.tsx | 11 + frontend-web/src/core/admin/api.ts | 58 ++ frontend-web/src/core/admin/hooks.ts | 26 + frontend-web/src/core/artifacts/download.ts | 12 +- frontend-web/src/core/artifacts/utils.ts | 13 +- .../branding/header-title-preview.test.ts | 33 ++ .../src/core/branding/header-title-preview.ts | 35 ++ .../light-red-navigation-preview.test.ts | 33 ++ .../branding/light-red-navigation-preview.ts | 37 ++ frontend-web/src/core/i18n/locales/en-US.ts | 4 +- frontend-web/src/core/i18n/locales/types.ts | 2 + frontend-web/src/core/i18n/locales/zh-CN.ts | 6 +- .../src/core/skill-created-at-filter.test.ts | 37 ++ .../src/core/skill-created-at-filter.ts | 42 ++ frontend-web/src/core/skills/api.ts | 51 +- frontend-web/src/core/skills/hooks.ts | 56 +- frontend-web/src/core/system-settings/api.ts | 233 +++++++- .../src/core/system-settings/hooks.ts | 78 +++ .../src/core/tools/skill-invocation.test.ts | 42 ++ .../src/core/tools/skill-invocation.ts | 54 ++ frontend-web/src/core/tools/utils.ts | 7 + frontend-web/src/env.ts | 13 + .../components/ai-writing/WritingWelcome.tsx | 40 +- .../src/pages/AdminUserLeaderboardPage.tsx | 314 ++++++++++- .../src/pages/ArtifactLibraryPage.tsx | 19 +- frontend-web/src/pages/ChatPage.tsx | 24 +- frontend-web/src/pages/KnowledgeBasePage.tsx | 5 + frontend-web/src/pages/LightAppListPage.tsx | 7 +- .../src/pages/LightAppManagementPage.tsx | 25 +- .../src/pages/PageDescriptionSettingsPage.tsx | 497 +++++++++++++++++ .../src/pages/SkillVersioningSettingsPage.tsx | 97 ++++ frontend-web/src/pages/SkillsPage.tsx | 7 +- frontend-web/src/pages/WorkspaceRoutes.tsx | 15 + .../deep-research/DeepResearchWorkbench.tsx | 29 +- .../pages/deep-research/GongwenWorkbench.tsx | 8 +- .../roundtable-planning/api/multi-agent.ts | 61 ++- .../src/roundtable-planning/api/rooms.ts | 1 + .../api/roundtable-jobs.ts | 22 +- .../components/RoundtableJobNotifier.tsx | 11 +- .../components/Step2Panel.tsx | 80 +-- .../hooks/useStep2Orchestration.ts | 34 +- .../lib/capacity-error.test.ts | 97 +++- .../roundtable-planning/lib/capacity-error.ts | 199 ++++++- .../lib/dag-flow-state.test.ts | 50 ++ .../roundtable-planning/lib/dag-flow-state.ts | 92 ++++ .../pages/BusinessChainListPage.tsx | 10 +- .../pages/RoundtablePlanningPage.tsx | 125 ++++- .../src/strategy-components/Header.tsx | 197 ++++++- .../llmwiki/WeKnoraKnowledgeWorkspace.tsx | 250 ++++++++- .../header-title-fit.test.ts | 93 ++++ .../strategy-components/header-title-fit.ts | 149 +++++ .../StrategyProjectsPage.tsx | 9 +- frontend-web/src/styles/globals.css | 70 ++- frontend-web/vite.config.ts | 101 +++- offline-backend-20260512/backend/CLAUDE.md | 29 +- offline-backend-20260512/backend/README.md | 34 +- .../app/gateway/roundtable_room_gate.py | 12 + .../app/gateway/routers/admin_users.py | 209 ++++++- .../app/gateway/routers/app_identity.py | 314 +++++++++-- .../backend/app/gateway/routers/artifacts.py | 65 ++- .../backend/app/gateway/routers/llmwiki.py | 97 +++- .../backend/app/gateway/routers/skills.py | 317 ++++++++++- .../app/gateway/routers/system_settings.py | 180 ++++++- .../app/gateway/routers/thread_shares.py | 7 + .../app/gateway/skill_catalog_cache.py | 23 + offline-backend-20260512/backend/config.yaml | 10 +- .../backend/docs/SKILL_EVOLUTION_ZH.md | 4 +- .../backend/extensions_config.json | 9 + .../deerflow/agents/lead_agent/prompt.py | 142 ++--- .../tool_error_handling_middleware.py | 53 +- .../middlewares/tool_metrics_middleware.py | 33 +- .../harness/deerflow/config/app_config.py | 3 +- .../config/runtime_performance_config.py | 24 +- .../deerflow/config/system_settings.py | 265 ++++++++- .../deerflow/html_runtime/offline_bundle.py | 321 +++++++++++ .../persistence/roundtable_rooms/sql.py | 25 +- .../harness/deerflow/sandbox/tools.py | 70 +-- .../packages/harness/deerflow/skills/usage.py | 115 +++- .../harness/deerflow/skills/versioning.py | 397 ++++++++++++++ .../deerflow/tools/builtins/skill_tools.py | 11 +- .../deerflow/tools/skill_manage_tool.py | 90 ++++ .../tests/test_runtime_performance_config.py | 18 +- .../harness/tests/test_skill_usage_signals.py | 95 ++++ .../tests/test_skill_view_fast_path.py | 26 +- .../test_tool_metrics_internal_visibility.py | 18 + .../harness/tests/test_tool_metrics_pause.py | 45 +- .../test_admin_leaderboard_query_shape.py | 89 ++- .../backend/tests/test_api_read_cache.py | 17 + .../backend/tests/test_app_identity.py | 144 +++++ .../backend/tests/test_artifact_preview.py | 166 ++++++ .../backend/tests/test_llmwiki_weknora.py | 66 ++- .../backend/tests/test_prompt_prefix.py | 24 +- .../tests/test_roundtable_room_limit.py | 43 ++ .../backend/tests/test_skill_versioning.py | 190 +++++++ .../backend/tests/test_skills_reconcile.py | 16 + .../tests/test_strategy_project_settings.py | 351 +++++++++++- offline-backend-20260512/config.yaml | 4 +- 122 files changed, 8881 insertions(+), 623 deletions(-) create mode 100644 frontend-web/src/assets/img/top-bg-light-red.svg create mode 100644 frontend-web/src/components/workspace/settings/call-metrics-settings-page.tsx create mode 100644 frontend-web/src/components/workspace/settings/knowledge-batch-create-settings-page.tsx create mode 100644 frontend-web/src/core/branding/header-title-preview.test.ts create mode 100644 frontend-web/src/core/branding/header-title-preview.ts create mode 100644 frontend-web/src/core/branding/light-red-navigation-preview.test.ts create mode 100644 frontend-web/src/core/branding/light-red-navigation-preview.ts create mode 100644 frontend-web/src/core/skill-created-at-filter.test.ts create mode 100644 frontend-web/src/core/skill-created-at-filter.ts create mode 100644 frontend-web/src/core/tools/skill-invocation.test.ts create mode 100644 frontend-web/src/core/tools/skill-invocation.ts create mode 100644 frontend-web/src/pages/PageDescriptionSettingsPage.tsx create mode 100644 frontend-web/src/pages/SkillVersioningSettingsPage.tsx create mode 100644 frontend-web/src/roundtable-planning/lib/dag-flow-state.test.ts create mode 100644 frontend-web/src/roundtable-planning/lib/dag-flow-state.ts create mode 100644 frontend-web/src/strategy-components/header-title-fit.test.ts create mode 100644 frontend-web/src/strategy-components/header-title-fit.ts create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/html_runtime/offline_bundle.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/skills/versioning.py create mode 100644 offline-backend-20260512/backend/packages/harness/tests/test_skill_usage_signals.py create mode 100644 offline-backend-20260512/backend/tests/test_skill_versioning.py diff --git a/.gitignore b/.gitignore index 8d2eb722..1756cbf0 100644 --- a/.gitignore +++ b/.gitignore @@ -88,9 +88,14 @@ offline-backend-20260512/backend/skill_evolution.json # Automatic config.yaml backups written by the admin models page on every save offline-backend-20260512/backend/.model-config-backups/ skills/ +# Harness skill-versioning logic is product source, not a runtime skill package. +!offline-backend-20260512/backend/packages/harness/deerflow/skills/ +offline-backend-20260512/backend/packages/harness/deerflow/skills/* +!offline-backend-20260512/backend/packages/harness/deerflow/skills/versioning.py hermes-agent-main/ .gitignore offline-backend-20260512/backend/tests/* +!offline-backend-20260512/backend/tests/test_skill_versioning.py !offline-backend-20260512/backend/tests/test_large_file_safety.py !offline-backend-20260512/backend/tests/test_account_authentication.py # HTML 结构化模板(no.1)引擎/存储/路由回归 diff --git a/AGENTS.md b/AGENTS.md index 004d365e..66c86fe4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,26 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA: - **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`. **素材检索技能池限定白名单**:`researcher_node` 通用检索的候选池 = **ai-writing-researcher 智能体 config.yaml `skills` 白名单里的启用技能**(管理员在智能体管理页增删,内置 `knowledge-base-search` 保底且始终排最前),不再列全部启用技能——300+ 技能部署把选择提示词撑爆导致素材检索不可用;容错兜底 `_fallback_candidate_skills` 同样限定白名单;白名单未配置/加载失败回退仅内置知识库检索。实现 `deerflow/agents/ai_writing/nodes/researcher.py::_load_candidate_skills/_researcher_skill_whitelist`,测试 `tests/test_ai_writing_knowledge_base.py`。 - **轻应用中心 (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 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. +- **生成网页标识 (Generated Page Branding, admin-only write)**: 管理员在设置弹窗「生成网页标识」中填写网页展示名称、上传 PNG/JPG/WebP Logo(≤10MB)、单独移除 Logo 或整体清除配置。配置保存在 runtime `system_settings.json -> generated_page_branding`,图片保存在 runtime `generated-page-branding/`,`GET /api/system-settings/generated-page-branding` 全员可读,`PUT/DELETE` 仅管理员;生成 HTML 通过公开只读 `/api/public/html-runtime/generated-page-logo` 获取图片。**只有网页展示名称非空且 Logo 文件真实存在时功能才启用**;缺少任意一项(含单独移除 Logo)都不注入任何内容,完全保持原有网页生成逻辑。该配置只约束后续生成的 HTML 网页内容,不修改 DeerFlow 自身的系统名称、主导航、欢迎区、工作区标题或 Logo。启用时,HTML 写入与 `page_report_template validate` 会由 `html_runtime/generated_page_chrome.py` 确定性注入平台套壳:页头**通栏全宽、贴顶、无圆角**,**背景直接用全量模式导航栏设计图**(`html_runtime/assets/topbar-light.svg` 浅色红调 / `topbar-dark.svg` 深色深蓝,已注册进网关 `html_runtime.py` 的 `_STATIC_ASSETS` 白名单以 `/api/public/html-runtime/topbar-*.svg` 提供;页头运行时按 body 明暗 tone 自动切图),**只注入页头、不注入页脚**;设置弹窗不再提供「品牌位置」选择,新配置默认使用 `center`:「Logo+系统名称」居中锁孔(26px 白色金属渐变字 + 深浅辉光,落在设计图中央发光基座上)+ 页面导航**分列两侧**白字页签(当前项白色加粗 + 白色墨条)。`generated_page_branding.header_layout` 字段继续兼容存量配置,既有 `left` 仍按品牌居左 + 导航居中单行渲染。页签均为通栏等高下划线式(当前项主色 + 底部渐变墨条),多页强制首个逻辑页初始可见。开关 `chrome_enabled` **默认开启**;明确关闭时完全回归无注入:不注入页头、也不对页面提任何品牌要求(校验不检查品牌,模型不会被迫自写)。页脚与「网页页脚文字」设置项均已按需求移除(`FOOTER_MARKER` 常量仅为旧页面兼容保留)。模板套件的兜底导航条同款(品牌左 + 导航中、通栏贴顶),平台注入时经 `body:has([data-df-generated-header])` + 兜底脚本双保险自动隐藏。后处理会隐藏旧的多页导航、移除模型写入的 `data-df-brand` 重复标识且保持幂等;lead prompt 只让模型生成正文和逻辑页。注入点共四处且全部**账号无关**(admin 配置对全部账号生效,非 admin 仅无配置入口):**`page_report_template prepare`(工作副本创建时即预注入页头,模型从第一秒起编辑的就是带品牌导航的页面,brief 的 `platform_branding.pre_injected` 标记此状态)**、`write_file`/`str_replace`(`sandbox/tools.py` 的 `maybe_fix_generated_html`)、`page_report_template validate`、以及 `present_files` 交付兜底(`present_file_tool._ensure_generated_html_chrome`,覆盖模型经 bash 等旁路写出的 HTML;幂等,>4MB 跳过);注入时还会**归一化 body/html 顶部 padding/margin(清零)**(防弱模型为自想页头留白)并**隐藏模型自写的顶部 nav/header**(仅正文首个内容容器之前的,正文内导航不受影响);品牌配置每次实时读取 `system_settings.json`,无缓存无账号隔离。 +- **生成网页默认明暗模式 (Generated Page Theme)**: `ChatPage` 与 `AgentChatPage` 每次发送只把当前界面的明暗模式作为 `presentation_theme=light|dark` 上下文传入。用户未明确指定网页主题时,自由生成 HTML、显式模板的 `auto` 选择以及平台注入的品牌页眉/页脚按该明暗模式生成;用户明确指定浅色、深色或其他配色时以用户要求为准。不得跟随系统的红、蓝、绿等主题主色或自定义主色,网页强调色由页面内容与可读性决定;科技、数据、看板、驾驶舱或地图题材本身不得再触发深色默认。 +- **生成网页离线下载包 (Generated HTML Offline Bundle)**: 产物面板下载 `.htm/.html/.xhtml` 时不再只保存单个 HTML,而是请求 `GET /api/threads/{thread_id}/artifacts/{path}?download=true&offline_bundle=true`(公开只读分享的文件端点同样支持),下载 `{原名}-离线包.zip`。纯引擎 `deerflow/html_runtime/offline_bundle.py` 把入口统一为 `index.html`,只将平台信任的 `/api/public/html-runtime/*` JS/CSS/GeoJSON/品牌资源、认知态势图运行目录和签名 `/api/public/html-images/*` 图片复制到 `assets/` 并改写为相对地址;不得联网抓取任意第三方 URL。包内还带 `manifest.json`、中文 `README.txt` 和实际使用库的许可证。Leaflet 在线瓦片及用户主动引用的公网图片/API 不复制,保留原地址并列入 `external_urls`。非 HTML 下载、HTML 预览/新窗口打开及流式未落盘的临时下载行为不变。 +- **系统名称与 Logo(与生成网页标识分开)**: 管理员在设置弹窗「系统名称」里填写本系统导航、侧栏和欢迎区使用的名称,并可上传一张 PNG/JPG/WebP Logo。顶部导航名称字号可选 `adaptive`(默认,30px 上限、24px 下限)或 `fixed`(16–40px);配置字段为 `app_identity.header_title_font_mode/header_title_font_size`,固定模式不再按分辨率缩放,超长名称仍单行完整显示。红色主题还可分别上传导航栏背景图与菜单选中样式图(PNG/JPG/WebP/SVG,不设应用层文件大小限制),字段为 `light_red_header_background_*` / `light_red_nav_active_*`;选择文件或恢复默认时顶部导航即时预览,未配置时继续使用 `top-bg-light-red.svg` / `nav-active-light-red.svg`。其余配置在 `system_settings.json -> app_identity`,图片统一放在运行目录 `app-identity/`,接口 `GET/PUT/DELETE /api/system-settings/app-identity`(写仅管理员),图片通过 `/api/public/app-identity/*` 读取。名称留空继续用「CM业务中心」/「CM 助手」;未上传 Logo 继续用各主题静态图片和既有 `runtime-config.js` 覆盖。不读写 `generated_page_branding`,也不改已生成的 HTML。 +- **平台模板套件 (Bootstrap page-template-suite 技能)**: 内置技能 `skills/public/page-template-suite`(public 类,缺省启用)让「画页面」走平台预制 Bootstrap 模板而不是从零手写。**套件只提供多页面门户(最低 2 个逻辑页),不做单页面 HTML**(单页请求自动回落共享响应式文档模板):5 套设计 × 明暗两版共 10 个 `deerflow/page_reports/assets/report-bootstrap-*-template.html`(图表门户 3 页 / 甘特双页「进度总览+里程碑风险」/ 瓦片地图门户 3 页 / 文字杂志双页「正文论述+脉络结论」/ 研判简报双页「核心结论+风险行动」),profile 注册于 `page_reports/templates.py`(id `bootstrap-charts-portal-{light,dark}`、`bootstrap-gantt-{light,dark}`、`bootstrap-map-portal-{light,dark}`、`bootstrap-text-magazine-{light,dark}`、`bootstrap-text-briefing-{light,dark}`)。显式 `template_id` 时**页数以模板为准**(prepare 会把 brief 的页数对齐到模板页数,技能无需依赖用户说出「多页」)。模板源文件用 `__DF_HTML_RUNTIME_BASE__` 占位符引用 Gateway 运行库(Bootstrap/Alpine/ECharts/Leaflet/deerflow-runtime.js),`page_report_template prepare` 在会话副本中替换为设置里保存的 HTML 运行库地址,部署无关;图表走 `data-df-chart` 严格 JSON(甘特 = 时间轴 + 阶段重叠柱 `barGap:-100%`),地图门户走 `data-df-map` Leaflet 瓦片契约(瓦片地址来自设置里保存的地图瓦片地址,需管理员在「外观设置 → HTML 地图」选择「地图网页」引擎,引擎不符时 prepare 的 brief 会带 `map_engine_warning`)。prepare 的 brief 还带 `platform_branding`(管理员「生成网页标识」的名称/Logo/页脚文字与注入规则,正文禁止自写品牌)和 `runtime_base_url`;`_choose_page_profile` 现在**显式 `template_id` 优先**(单页文档模式也能选中套件模板,此前单页一律回落共享文档模板),未显式指定时旧选择行为不变。多页门户模板自带锚点切页兜底脚本(检测 `data-df-generated-header` 让位平台套壳,勿用 `data-df-generated-chrome` 字面量——会被注入幂等检查误判)。测试 `tests/test_page_template_suite.py`(10 模板 prepare→validate 全绿 + 品牌注入 + 居左/居中布局 + 引擎门控)。 +- **Docker 内置技能同步**: Linux Docker 启动时不能让旧的持久化 `/app/skills` 遮住当前版本的 `backend/skills/public`。共享实现 `app/builtin_skill_sync.py` 会把 bundled public 覆盖同步到 live public 根、保留 `custom/`,并强制校验 Facebook/X/TikTok/YouTube/Threads/CNN/BBC/新华社/白宫官网/马尼拉时报/GMA News/雅虎日本/读卖新闻共 13 个硬路由页面技能;它使用跨进程文件锁和必需包内容摘要,多 worker 首次只复制一次,后续相同版本只校验跳过。两个受支持启动脚本仍在 Gateway 前调用 `backend/scripts/sync_builtin_skills.py`,把当前源码 `backend/skills` 只读挂载为 `/app/bundled-skills`、live skills 读写挂载到 `/app/skills`;同时 Gateway lifespan 在加载配置后、技能 ownership 迁移/MySQL 目录投影/提示词缓存之前再次调用同一共享实现,所以镜像默认入口、Compose 或外部发版脚本绕过 Shell 时也会自动补齐。任一硬路由技能缺失会 fail-fast 并打印名单,不能静默启动成“技能不可用”。 +- **自定义技能迭代与版本切换**: 管理员设置弹窗的「自定义技能迭代」总开关默认关闭(`GET/PUT /api/system-settings/skill-versioning`);关闭时保持原同名覆盖逻辑。开启后仅 `skills/custom` 参与:本人上传同名技能可选「迭代为新版本」(保留完整历史包并激活新版)或「直接覆盖」(清除旧版本链,以本次内容重新作为 v1);本人技能详情顶部也提供「迭代新版本」安装包入口,详情弹窗「版本」页可切换任一快照。历史包保存在 runtime `skill-versions//vNNNNNN`,位于技能扫描/沙箱挂载目录之外;`skills/custom/` 永远只保留一个活动包,所以大模型只能发现当前激活版本。版本元数据与调用统计解耦并继续以稳定技能名计数,上传迭代和版本切换都不清零外部调用次数。实现 `deerflow/skills/versioning.py`、`app/gateway/routers/skills.py` 的 `/versions` 接口,测试 `tests/test_skill_versioning.py` 与 `tests/test_strategy_project_settings.py`。 +- **Facebook 页面生成技能**: 内置公共技能 `skills/public/facebook-page-generator` 只在用户明确要求 Facebook/Facebook 风格网页时触发;`lead_agent/prompt.py` 的普通问答 HTML 分流会强制读取该技能,并排除通用 `page-report-generation`、模板套件和从零手写 HTML。单账号单动态场景强制只写小型中文规格 JSON,再调用一次 `scripts/fast_single_post.py --spec`;脚本可将总评论数与实际展示数分离,按 `commentTopics` 自动补齐最多 100 条中文热评,也接受用户指定的逐条评论,并在一次进程内完成七模块 JSON、内部校验和 HTML 渲染。用户已提供素材或明确不搜索时禁止联网、图像生成、读模板/脚本源码和临时构建脚本;动态与评论正文必须为简体中文。多帖子场景仍使用 `scripts/render_facebook_page.py` 的 `init → validate → render` 流程确定性注入 `assets/facebook-template.html`,模型不得重写模板 DOM/CSS;渲染器拒绝覆盖已有文件并对 `` 注入做转义。最终单文件 HTML 保留右上角设置按钮、分模块 JSON 编辑弹窗、实时校验与「应用并渲染」,以及响应式、深浅主题、发布/点赞/评论/搜索交互;浏览器缓存键包含初始 JSON 的 FNV-1a 指纹,同一制品刷新保留手动编辑,不同新制品不会再被沙箱同源旧 JSON 覆盖。测试 `tests/test_facebook_page_generator_skill.py`。 +- **X 页面生成技能**: 内置公共技能 `skills/public/x-page-generator` 只在用户明确要求 X 平台、Twitter/推特或 X 风格社交时间线网页时触发;专用路由会强制读取技能并绕开通用 HTML 生成器。单账号单推文场景强制只写小型中文规格 JSON,再调用一次 `scripts/fast_single_post.py --spec`;脚本可将总回复数与实际展示数分离,按 `replyTopics` 自动补齐最多 100 条中文热评,也接受用户指定的逐条回复,并在一次进程内完成六模块 JSON、内部校验和 HTML 渲染。用户已提供素材或明确不搜索时禁止联网、图像生成、读模板/脚本源码和临时构建脚本;推文与回复正文必须为简体中文。多帖子场景仍通过 `scripts/render_x_page.py` 的 `init → validate → render` 流程注入固定三栏模板与内联运行时。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、实时校验和「应用并渲染」,保留深浅主题、发布、回复、点赞、转发、收藏、搜索、关注与响应式移动布局;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_x_page_generator_skill.py`。 +- **TikTok 页面生成技能**: 内置公共技能 `skills/public/tiktok-page-generator` 只在用户明确要求 TikTok、抖音国际版或 TikTok 风格短视频信息流网页时触发。默认只写小型六模块补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_tiktok_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、实时校验和「应用并渲染」,保留滚动吸附、播放/暂停、双击点赞、静音、收藏、关注、评论、分享、搜索、方向键切换和移动端全屏布局;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_tiktok_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **YouTube 页面生成技能**: 内置公共技能 `skills/public/youtube-page-generator` 只在用户明确要求 YouTube 或 YouTube 风格视频门户、视频首页、频道页时触发。默认只写小型七模块补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_youtube_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、分类筛选、搜索、响应式视频网格、Shorts 横向列表、观看弹窗、点赞、收藏、订阅和评论;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_youtube_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **Threads 页面生成技能**: 内置公共技能 `skills/public/threads-page-generator` 只在用户明确要求 Threads 或 Threads 风格串文首页、文本社交信息流时触发。默认只写 `accountName`、`postText`、`topic`、`displayReplyCount` 等高层规格 JSON,并调用一次专用 `scripts/fast_page.py --spec`;脚本自动映射六模块、补齐作者/统计/中文热评、保留示例视觉并一次完成校验与渲染,禁止模型猜测 `$set` 深层路径或读取默认数据。只有结构改造才回退 `scripts/render_threads_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、深浅主题、发帖、搜索、点赞、回复、转发、分享、关注、动态面板和移动端导航;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_threads_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **CNN 页面生成技能**: 内置公共技能 `skills/public/cnn-page-generator` 只在用户明确要求 CNN 或 CNN 风格全球新闻门户、新闻首页时触发。默认只写以 `topic` 为核心、可选 `headline`/`breaking`/栏目标题数组的高层规格 JSON,并调用一次专用 `scripts/fast_page.py --spec`;脚本自动生成完整中文十模块与明确标注的非实时示意数据,禁止模型猜测 `$set` 深层路径、读取生成后的合并数据或探路后二次渲染。只有结构改造才回退 `scripts/render_cnn_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、突发条、搜索、直播弹窗、字幕切换、收藏、分享、移动菜单、市场快照和订阅表单;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_cnn_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **BBC 页面生成技能**: 内置公共技能 `skills/public/bbc-page-generator` 只在用户明确要求 BBC 或 BBC 风格国际公共新闻门户、新闻首页时触发。默认只写小型十模块补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_bbc_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、搜索、快讯、直播弹窗、字幕切换、音频进度、移动抽屉、新闻排行、地区天气和订阅反馈;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_bbc_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **新华社页面生成技能**: 内置公共技能 `skills/public/xinhua-page-generator` 只在用户明确要求新华社、新华网或新华社风格中文通讯社门户、新闻首页时触发。默认只写小型十模块中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_xinhua_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、多语种顶栏、报道专集、焦点图、要闻分类、名专栏、专题、记者工作室、地方切换、行业频道、媒体矩阵、直播字幕和订阅反馈;缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_xinhua_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **白宫官网页面生成技能**: 内置公共技能 `skills/public/white-house-page-generator` 只在用户明确要求白宫官网、White House 风格政府首页或美国行政机构门户时触发。技能把材料整理为 `site/navigation/hero/notice/updates/priorities/administration/calendar/participation/footer` 十模块 JSON;键名、`id`、锚点和视觉枚举保留英文技术值,所有读者可见内容强制使用简体中文,英文来源先翻译,`site.language` 固定为 `zh-CN`。默认只写小型中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_white_house_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、透明转吸顶导航、建筑主视觉、公告、官方动态、优先事项、行政团队、公共日历、全屏菜单搜索、直播字幕和订阅反馈;默认内容明确为概念模板,不冒充政府官网,缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_white_house_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **马尼拉时报页面生成技能**: 内置公共技能 `skills/public/manila-times-page-generator` 只在用户明确要求马尼拉时报、The Manila Times 或其风格的菲律宾大报门户时触发。技能把材料整理为 `site/navigation/ticker/lead/pulse/regions/opinion/sections/media/footer` 十模块 JSON;键名、`id`、锚点、视觉和涨跌枚举保留英文技术值,所有读者可见内容强制使用简体中文,英文或菲律宾语来源先翻译,`site.language` 固定为 `zh-CN`。默认只写小型中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_manila_times_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、菲律宾旗色带、衬线报头、三栏头条、地区切换、评论、商业体育、视频弹窗、数字报翻页和订阅反馈;默认内容明确为概念模板,不冒充官方网站,缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_manila_times_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **GMA News 页面生成技能**: 内置公共技能 `skills/public/gma-news-page-generator` 只在用户明确要求 GMA News、GMA 新闻或菲律宾广播新闻门户风格页面时触发。技能把材料整理为 `site/navigation/justIn/lead/news/services/broadcast/channels/media/footer` 十模块 JSON;键名、`id`、锚点和视觉枚举保留英文技术值,品牌缩写 `GMA` 可保留,其余读者可见内容强制使用简体中文,英文或菲律宾语来源先翻译,`site.language` 固定为 `zh-CN`。默认只写小型中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_gma_news_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、即时新闻条、天气切换、吸顶导航、头条视频、最新/热门/地方切换、公共服务、电视直播字幕、频道卡片、播客、彩票开奖和订阅反馈;默认内容明确为概念模板,不冒充官方网站,缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_gma_news_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **雅虎日本页面生成技能**: 内置公共技能 `skills/public/yahoo-japan-page-generator` 只在用户明确要求雅虎日本、Yahoo! JAPAN、Yahoo Japan 或日本综合门户风格页面时触发。技能把材料整理为 `site/search/services/news/account/weather/utilities/shopping/pulse/footer` 十模块 JSON;键名、`id`、锚点和视觉枚举保留英文技术值,品牌 `Yahoo! JAPAN` 可保留,其余读者可见内容强制使用简体中文,日文或英文来源先翻译,`site.language` 固定为 `zh-CN`。默认只写小型中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_yahoo_japan_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、多类型搜索、服务展开、分类新闻、推荐轮换、演示登录、城市天气、路线查询、市场切换、商品收藏、体育比分、实时热榜、移动抽屉和返回顶部;默认内容明确为概念模板,不冒充官方网站,缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_yahoo_japan_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 +- **读卖新闻页面生成技能**: 内置公共技能 `skills/public/yomiuri-news-page-generator` 只在用户明确要求读卖新闻、读买新闻、Yomiuri、Yomiuri Shimbun 或日本全国大报风格页面时触发。技能把材料整理为 `site/navigation/breaking/lead/edition/features/news/opinion/media/footer` 十模块 JSON;键名、`id`、锚点和视觉枚举保留英文技术值,品牌罗马字 `YOMIURI ONLINE` 可保留,其余读者可见内容强制使用简体中文,日文或英文来源先翻译,`site.language` 固定为 `zh-CN`。默认只写小型中文补丁规格 JSON,并调用一次 `scripts/fast_page.py --spec` 在内部合并、校验和渲染;只有结构改造才回退 `scripts/render_yomiuri_news_page.py` 的 `init → validate → render`。最终单文件 HTML 自带右上角设置按钮、按模块 JSON 编辑弹窗、字号切换、速报展开、三栏头条、文章收藏、吸顶导航、全屏菜单、搜索、演示登录、数字报翻页、专题与分类切换、社论、视频字幕和邮件订阅;默认内容明确为概念模板,不冒充官方网站,缓存键包含初始 JSON 指纹并兼容禁止 localStorage 的沙箱。测试 `tests/test_yomiuri_news_page_generator_skill.py` 与 `tests/test_fast_page_generator_specs.py`。 - **舆情分析智能体(前端虚拟智能体)**: 智能体管理页新增「舆情分析」类型 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 等侧栏宽度过渡稳定后再测量高亮框。 - **智能体使用引导 (Agent onboarding tour, 三段式)**: 智能体「浏览管理 → 创建与配置 → 发起问答」全流程分步引导,组件 `components/workspace/agents/agent-guide.tsx`(同一 TDesign `Guide` 封装模式,与简洁模式引导共享记忆语义:**仅显式点「完成/跳过」才写 localStorage** `deerflow.agent-guide..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:` 窗口事件强制重放(忽略已看过记忆)。 diff --git a/frontend-web/.env.example b/frontend-web/.env.example index 643809a0..ce1e9024 100644 --- a/frontend-web/.env.example +++ b/frontend-web/.env.example @@ -62,6 +62,9 @@ # 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。 # 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。 # VITE_TURBO_MODE_VISIBLE=false +# 知识库视图切换:true=显示“列表 / 卡片”切换;false 或未设=固定为列表(默认)。 +# 部署后优先改 dist/runtime-config.js 的 VITE_KNOWLEDGE_VIEW_SWITCH_ENABLED,刷新即可。 +# VITE_KNOWLEDGE_VIEW_SWITCH_ENABLED=false # VITE_GITHUB_OAUTH_TOKEN= # --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)--- diff --git a/frontend-web/public/runtime-config.js b/frontend-web/public/runtime-config.js index c02873c8..4c2eb1e6 100644 --- a/frontend-web/public/runtime-config.js +++ b/frontend-web/public/runtime-config.js @@ -147,6 +147,10 @@ window.__CM_RUNTIME_CONFIG__ = { // 静态站点模式开关;一般部署业务系统时保持 false。 VITE_STATIC_WEBSITE_ONLY: "false", + // 知识库页面是否显示“列表 / 卡片”视图切换。默认 false:固定使用紧凑列表; + // 改为 true 并刷新页面后,才允许用户切换到卡片视图。 + VITE_KNOWLEDGE_VIEW_SWITCH_ENABLED: "false", + // 紧凑导航开关:true 时固定使用简洁布局,工作台左侧栏收起为菜单图标, // 隐藏系统 Logo、换肤按钮以及“全量/简洁模式”切换。修改后刷新页面即可生效。 VITE_FORCE_COLLAPSED_SIDEBAR: "false", diff --git a/frontend-web/scripts/check-chrome109-css.mjs b/frontend-web/scripts/check-chrome109-css.mjs index 0e57723f..6cbc5cfe 100644 --- a/frontend-web/scripts/check-chrome109-css.mjs +++ b/frontend-web/scripts/check-chrome109-css.mjs @@ -93,6 +93,21 @@ const requiredLegacyFallbacks = [ pattern: /AbortSignal\.timeout = function/, description: "Chrome 102 needs AbortSignal.timeout before any module loads", }, + { + file: "vite.config.ts", + pattern: /export function jsx\(\)\{return null\}/, + description: "Stale Vite deps chunks must locally export jsx for Chrome 102", + }, + { + file: "vite.config.ts", + pattern: /export const \$\{name\} = __jsxRuntime\.\$\{name\}/, + description: "Vite jsx-runtime CJS interop must expose a named jsx export", + }, + { + file: "src/components/workspace/question-navigator.tsx", + pattern: /animateScrollTop\(parent, Math\.max\(0, Math\.min\(nextTop, maxScrollTop\(parent\)\)\)\)/, + description: "Question list anchors must set nested scrollTop for Chrome 102", + }, { file: "src/styles/globals.css", pattern: /\.workspace-chat-bg,\s*\n\s*\.canvas-writing-bg\s*\{\s*background-color:\s*var\(--background\);/, diff --git a/frontend-web/src/assets/img/top-bg-light-red.svg b/frontend-web/src/assets/img/top-bg-light-red.svg new file mode 100644 index 00000000..b460b268 --- /dev/null +++ b/frontend-web/src/assets/img/top-bg-light-red.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index 157ba030..8f79d5da 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -443,6 +443,25 @@ background-size: 152px 42px; } +/* 浅红科技感顶栏保持深色版的尺寸与排布,只替换文字光效和选中底图。 */ +.strategy-app-header--light-red-top-nav .top-submenu-item { + color: rgba(255, 255, 255, 0.86); +} + +.strategy-app-header--light-red-top-nav .top-submenu-item:hover { + color: #ffffff; + text-shadow: 0 0 10px rgba(196, 51, 40, 0.4); +} + +.strategy-app-header--light-red-top-nav .top-submenu-item--active { + color: #ffffff; + text-shadow: 0 0 12px rgba(177, 38, 28, 0.48); + background-image: var( + --light-red-nav-active-background-image, + url("../assets/img/nav-active-light-red.svg") + ); +} + .third-submenu-bar { position: relative; z-index: 34; diff --git a/frontend-web/src/components/wiki/WikiHeader.tsx b/frontend-web/src/components/wiki/WikiHeader.tsx index 1381250b..7f2baaaa 100644 --- a/frontend-web/src/components/wiki/WikiHeader.tsx +++ b/frontend-web/src/components/wiki/WikiHeader.tsx @@ -45,6 +45,8 @@ function HeaderButton({ /** Light 56px wiki header: plain page title on the left, action entries on the right. */ export function WikiHeader({ + title = "知识库管理", + description, onToggleSidebar, onImportFile, onBatchImport, @@ -55,6 +57,8 @@ export function WikiHeader({ onManageTemplates, extraActions, }: { + title?: string; + description?: string; onToggleSidebar: () => void; onImportFile: () => void; onBatchImport: () => void; @@ -66,8 +70,13 @@ export function WikiHeader({ extraActions?: ReactNode; }) { return ( -
-
+
+
-
- 知识库管理 +
+
+ {title} +
+ {description ? ( +

+ {description} +

+ ) : null}
diff --git a/frontend-web/src/components/workspace/agents/agent-gallery.tsx b/frontend-web/src/components/workspace/agents/agent-gallery.tsx index f0ae2639..6607ac04 100644 --- a/frontend-web/src/components/workspace/agents/agent-gallery.tsx +++ b/frontend-web/src/components/workspace/agents/agent-gallery.tsx @@ -18,7 +18,12 @@ import type { Agent, AgentSort } from "@/core/agents"; import { useI18n } from "@/core/i18n/hooks"; import { useRouteBase } from "@/core/route-base"; import { isSentimentAgentVisible } from "@/core/sentiment-agent/config"; -import { usePrivateSquareStatus, usePublishSquares, useViewMode } from "@/core/system-settings/hooks"; +import { + usePageDescriptionsSettings, + usePrivateSquareStatus, + usePublishSquares, + useViewMode, +} from "@/core/system-settings/hooks"; import { WX_RESTRICTED_TAB_DEFAULT_SQUARE, WX_RESTRICTED_TAB_NANJING_DB, @@ -51,6 +56,8 @@ export function AgentGallery({ const { t } = useI18n(); const router = useRouter(); const routeBase = useRouteBase(); + const pageDescriptions = usePageDescriptionsSettings(); + const pageDescription = pageDescriptions.data?.agents; const sentimentAgentVisible = isSentimentAgentVisible(); // 「内置智能体」筛选仅管理员可见(后端 scope=builtin 也做了 admin 闸门兜底)。 const isAdmin = getStoredAuth()?.system_role === "admin"; @@ -455,9 +462,11 @@ export function AgentGallery({ {/* Page header */}
-

{t.agents.title}

+

+ {pageDescription?.title || t.agents.title} +

- {t.agents.description} + {pageDescription?.description || t.agents.description}

diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index 092d2415..d03dfbe4 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -1706,8 +1706,16 @@ export function ArtifactFileDetail({ )} { if (onDownloadClick) { onDownloadClick(); diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-list.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-list.tsx index 10fc066b..1876bd58 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-list.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-list.tsx @@ -130,7 +130,9 @@ export function ArtifactFileList({ ) : ( )} - {t.common.download} + {!onDownloadArtifact && /\.(?:html?|xhtml)$/i.test(getFileName(file)) + ? "下载离线包" + : t.common.download} diff --git a/frontend-web/src/components/workspace/compact-welcome.tsx b/frontend-web/src/components/workspace/compact-welcome.tsx index 6aa6b7b5..44e31f75 100644 --- a/frontend-web/src/components/workspace/compact-welcome.tsx +++ b/frontend-web/src/components/workspace/compact-welcome.tsx @@ -3,7 +3,11 @@ import { useTheme } from "@/contexts/ThemeContext"; import { AppBrandImage } from "@/core/branding/app-brand-image"; import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desensitizer"; -import { useAppSystemName } from "@/core/system-settings/hooks"; +import { + useAppSystemName, + usePageDescriptionsSettings, +} from "@/core/system-settings/hooks"; +import type { GeneralQaPageDescription } from "@/core/system-settings/api"; import { readRuntimeFlag } from "@/env"; import { cn } from "@/lib/utils"; @@ -12,11 +16,25 @@ import { cn } from "@/lib/utils"; * 仅展示,无交互。配色走主题 token(--workspace-brand / --primary),红/蓝/深蓝主题自适应。 * 非紧凑模式仍用 ,本组件不参与。 */ -export function CompactWelcome({ className }: { className?: string }) { +export function CompactWelcome({ + className, + pageDescription, +}: { + className?: string; + /** The actual interface mode is resolved by ChatPage. */ + pageDescription?: GeneralQaPageDescription; +}) { const desensitizeText = useDisplayDesensitizer(); const { theme } = useTheme(); const systemName = useAppSystemName(""); - const welcomeTitle = systemName ? `欢迎使用${systemName}` : "欢迎使用CM助手"; + const { data: pageDescriptions } = usePageDescriptionsSettings(); + const configured = pageDescription ?? pageDescriptions?.general_qa.compact; + const welcomeTitle = + configured?.welcome_title || + (systemName ? `欢迎使用${systemName}` : "欢迎使用CM助手"); + const welcomeDescription = + configured?.welcome_description || + "基于先进深度推理模型,为您提供高效的市场分析、专业报告撰写、调研设计与协同脑暴支持。"; const isWelcomeLogoVisible = readRuntimeFlag("VITE_WELCOME_LOGO_VISIBLE"); return ( @@ -46,7 +64,7 @@ export function CompactWelcome({ className }: { className?: string }) { {desensitizeText(welcomeTitle)}

- 基于先进深度推理模型,为您提供高效的市场分析、专业报告撰写、调研设计与协同脑暴支持。 + {desensitizeText(welcomeDescription)}

); diff --git a/frontend-web/src/components/workspace/input-box.tsx b/frontend-web/src/components/workspace/input-box.tsx index 81a11080..96389623 100644 --- a/frontend-web/src/components/workspace/input-box.tsx +++ b/frontend-web/src/components/workspace/input-box.tsx @@ -140,6 +140,7 @@ export function InputBox({ knowledgeRetrieval, knowledgeSuggestedQuestions = [], extraModels = [], + placeholder, assistantId, hideAdminRecommendedQuestions, compact, @@ -227,6 +228,8 @@ export function InputBox({ knowledgeSuggestedQuestions?: string[]; /** Page-scoped model choices implemented outside the DeerFlow model API. */ extraModels?: Model[]; + /** Page-specific prompt. Omit to keep the localized product default. */ + placeholder?: string; }) { const { t } = useI18n(); const [localSettings, setLocalSettings] = useLocalSettings(); @@ -861,7 +864,7 @@ export function InputBox({ data-composer-height={composerMinHeight ? "" : undefined} style={composerHeightStyle} disabled={disabled} - placeholder={t.inputBox.placeholder} + placeholder={placeholder || t.inputBox.placeholder} autoFocus={autoFocus} defaultValue={initialValue} /> diff --git a/frontend-web/src/components/workspace/messages/message-group.tsx b/frontend-web/src/components/workspace/messages/message-group.tsx index 2bbfbe9b..9fcd7eb1 100644 --- a/frontend-web/src/components/workspace/messages/message-group.tsx +++ b/frontend-web/src/components/workspace/messages/message-group.tsx @@ -63,6 +63,10 @@ import { useLocalSettings } from "@/core/settings"; import { useSkills } from "@/core/skills/hooks"; import { useCitationDisplaySettings } from "@/core/system-settings/hooks"; import { isInternalToolCallName } from "@/core/tools/visibility"; +import { + extractInvokedSkillName, + skillInvocationLabel, +} from "@/core/tools/skill-invocation"; import { applyInlineCitations, buildSkillDisplayMap, @@ -371,6 +375,10 @@ function ToolCall({ ragModeEnabled?: boolean; }) { const { t } = useI18n(); + const invokedSkillName = extractInvokedSkillName(name, args); + const invokedSkillLabel = invokedSkillName + ? skillInvocationLabel(invokedSkillName) + : undefined; const displayMatch = result !== undefined && isDisplaySamplePayload(result) ? null @@ -566,7 +574,9 @@ function ToolCall({ } else if (name === "read_file") { let description: string | undefined = (args as { description: string }) ?.description; - if (!description) { + if (invokedSkillLabel) { + description = invokedSkillLabel; + } else if (!description) { description = t.toolCalls.readFile; } const { path } = args as { path: string; content: string }; @@ -598,11 +608,10 @@ function ToolCall({ /> ); } else if (name === "bash") { - const description: string | undefined = (args as { description: string }) - ?.description; - if (!description) { - return t.toolCalls.executeCommand; - } + const description = + invokedSkillLabel || + (args as { description?: string })?.description || + t.toolCalls.executeCommand; const command: string | undefined = (args as { command: string })?.command; return ( ); diff --git a/frontend-web/src/components/workspace/question-navigator.tsx b/frontend-web/src/components/workspace/question-navigator.tsx index aec0694c..c23e1fd5 100644 --- a/frontend-web/src/components/workspace/question-navigator.tsx +++ b/frontend-web/src/components/workspace/question-navigator.tsx @@ -27,21 +27,146 @@ const PANEL_GUTTER_MARGIN = 24; /** Questions needed before the navigator shows up at all. */ const MIN_QUESTIONS = 2; const COLLAPSE_STORAGE_KEY = "deerflow.question-navigator.collapsed"; +/** Matches `scroll-mt-20` so the target sits below the overlay chat header. */ +const QUESTION_SCROLL_TOP_OFFSET = 80; +/** Re-apply after the drawer overlay unmounts; StickToBottom may snap on resize. */ +const SHEET_SCROLL_RETRY_MS = 80; + +function verticalOverflow(node: HTMLElement): string { + const style = window.getComputedStyle(node); + const overflowY = style.overflowY; + if (overflowY && overflowY !== "visible") return overflowY; + const parts = style.overflow.split(" "); + return parts[1] || parts[0] || ""; +} + +function isVerticallyScrollable(node: HTMLElement): boolean { + const overflowY = verticalOverflow(node); + return overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; +} /** Walk up from a message anchor to the element that actually scrolls. */ function findScrollParent(el: HTMLElement | null): HTMLElement | null { let node = el?.parentElement ?? null; - while (node) { - const overflowY = window.getComputedStyle(node).overflowY; - if ( - (overflowY === "auto" || overflowY === "scroll") && - node.scrollHeight > node.clientHeight - ) { + while (node && node !== document.body) { + if (isVerticallyScrollable(node) && node.scrollHeight > node.clientHeight + 1) { return node; } node = node.parentElement; } - return null; + if (document.scrollingElement instanceof HTMLElement) { + return document.scrollingElement; + } + return document.documentElement; +} + +let questionScrollFrame = 0; +let questionScrollTarget: { scroller: HTMLElement; top: number } | null = null; + +function prefersReducedMotion(): boolean { + try { + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } catch { + return false; + } +} + +/** Nested-container smooth scroll that Chrome 102 can actually run. */ +function animateScrollTop(scroller: HTMLElement, top: number): void { + if ( + questionScrollTarget?.scroller === scroller && + Math.abs(questionScrollTarget.top - top) < 8 + ) { + return; + } + if (questionScrollFrame) cancelAnimationFrame(questionScrollFrame); + const from = scroller.scrollTop; + const delta = top - from; + questionScrollTarget = { scroller, top }; + if (Math.abs(delta) < 1 || prefersReducedMotion()) { + scroller.scrollTop = top; + questionScrollFrame = 0; + return; + } + const duration = Math.min(480, Math.max(240, Math.abs(delta) * 0.45)); + const start = performance.now(); + const stopListening = () => { + scroller.removeEventListener("wheel", cancelOnUserScroll); + scroller.removeEventListener("touchmove", cancelOnUserScroll); + }; + const cancelOnUserScroll = () => { + if (questionScrollFrame) cancelAnimationFrame(questionScrollFrame); + questionScrollFrame = 0; + questionScrollTarget = null; + stopListening(); + }; + const step = (now: number) => { + const t = Math.min(1, (now - start) / duration); + const eased = 1 - (1 - t) * (1 - t) * (1 - t); + scroller.scrollTop = from + delta * eased; + if (t < 1) { + questionScrollFrame = requestAnimationFrame(step); + return; + } + questionScrollFrame = 0; + stopListening(); + }; + scroller.addEventListener("wheel", cancelOnUserScroll, { passive: true }); + scroller.addEventListener("touchmove", cancelOnUserScroll, { passive: true }); + questionScrollFrame = requestAnimationFrame(step); +} + +function scrollChildIntoParent( + el: HTMLElement, + parent: HTMLElement, + topOffset = 0, +): void { + const parentRect = parent.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const nextTop = parent.scrollTop + (elRect.top - parentRect.top) - topOffset; + animateScrollTop(parent, Math.max(0, Math.min(nextTop, maxScrollTop(parent)))); +} + +function maxScrollTop(parent: HTMLElement): number { + return Math.max(0, parent.scrollHeight - parent.clientHeight); +} + +/** + * Chrome 102 (and other nested-scroller hosts) often no-ops + * `scrollIntoView({ behavior: "smooth" })` when the conversation is not the + * document. Animate `scrollTop` on the real overflow container instead. + */ +function scrollElementIntoContainer(el: HTMLElement, topOffset = 0): void { + const scroller = findScrollParent(el); + if (!scroller || scroller === el) { + try { + el.scrollIntoView(true); + } catch { + // ignore hosts that reject the boolean form + } + return; + } + try { + scroller.dispatchEvent( + new WheelEvent("wheel", { deltaY: -1, bubbles: true }), + ); + } catch { + // WheelEvent is available on Chrome 102; keep scrollTop even if this fails. + } + scrollChildIntoParent(el, scroller, topOffset); +} + +function scrollItemNearestInParent( + item: HTMLElement, + parent: HTMLElement, +): void { + const parentRect = parent.getBoundingClientRect(); + const itemRect = item.getBoundingClientRect(); + if (itemRect.top < parentRect.top) { + parent.scrollTop += itemRect.top - parentRect.top; + } else if (itemRect.bottom > parentRect.bottom) { + parent.scrollTop += itemRect.bottom - parentRect.bottom; + } } function readCollapsedPreference(): boolean { @@ -113,6 +238,7 @@ export function QuestionNavigator({ return; } const firstAnchor = document.getElementById(questions[0]!.anchorId); + if (!firstAnchor) return; const scroller = findScrollParent(firstAnchor); if (!scroller) return; @@ -145,17 +271,20 @@ export function QuestionNavigator({ const scrollToQuestion = useCallback((anchorId: string) => { const scroll = () => { - document - .getElementById(anchorId) - ?.scrollIntoView({ behavior: "smooth", block: "start" }); + const el = document.getElementById(anchorId); + if (!el) return false; + scrollElementIntoContainer(el, QUESTION_SCROLL_TOP_OFFSET); + return true; }; - if (!document.getElementById(anchorId)) { - window.dispatchEvent(new Event(REVEAL_CHAT_HISTORY_EVENT)); - requestAnimationFrame(() => requestAnimationFrame(scroll)); - } else { - scroll(); - } setActiveId(anchorId); + if (scroll()) return; + window.dispatchEvent(new Event(REVEAL_CHAT_HISTORY_EVENT)); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (scroll()) return; + window.setTimeout(scroll, 50); + }); + }); }, []); const toggleCollapsed = useCallback(() => { @@ -246,8 +375,12 @@ export function QuestionNavigator({ questions={questions} activeId={activeId} onSelect={(anchorId) => { - setSheetOpen(false); scrollToQuestion(anchorId); + setSheetOpen(false); + window.setTimeout( + () => scrollToQuestion(anchorId), + SHEET_SCROLL_RETRY_MS, + ); }} className="min-h-0 flex-1" /> @@ -272,17 +405,22 @@ function QuestionCardList({ className?: string; }) { const itemRefs = useRef(new Map()); + const navRef = useRef(null); - // Keep the active card visible as the chat scrolls. `block: "nearest"` only - // nudges this inner list — the card is already inside the on-screen panel, - // so no outer container moves. + // Keep the active card visible as the chat scrolls. Scroll only this inner + // list — `scrollIntoView({ block: "nearest" })` on Chrome 102 also moves + // ancestor conversation scrollers, which undoes the jump to the question. useEffect(() => { if (!activeId) return; - itemRefs.current.get(activeId)?.scrollIntoView({ block: "nearest" }); + const item = itemRefs.current.get(activeId); + const nav = navRef.current; + if (!item || !nav) return; + scrollItemNearestInParent(item, nav); }, [activeId]); return (
@@ -253,10 +737,10 @@ export function AppIdentitySettingsPage() { - 清除系统名称和 Logo? + 清除系统名称配置?

- 清除后,导航和欢迎区恢复为原来的名称,Logo 恢复为内置静态图片。 + 清除后,导航和欢迎区恢复为原来的名称,Logo 与红色主题导航图片恢复为内置静态图片,名称字号恢复为自适应。

diff --git a/frontend-web/src/components/workspace/settings/call-metrics-settings-page.tsx b/frontend-web/src/components/workspace/settings/call-metrics-settings-page.tsx new file mode 100644 index 00000000..1c4c694b --- /dev/null +++ b/frontend-web/src/components/workspace/settings/call-metrics-settings-page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { BarChart3Icon, Loader2Icon } from "lucide-react"; +import { toast } from "sonner"; + +import { Switch } from "@/components/ui/switch"; +import { + useCallMetricsSettings, + useUpdateCallMetricsSettings, +} from "@/core/system-settings/hooks"; + +export function CallMetricsSettingsPage() { + const { data, isError, isLoading } = useCallMetricsSettings(); + const update = useUpdateCallMetricsSettings(); + const enabled = data?.enabled ?? true; + + const handleChange = async (checked: boolean) => { + try { + await update.mutateAsync({ enabled: checked }); + toast.success(checked ? "工具与技能调用统计已开启" : "工具与技能调用统计已关闭"); + } catch (error) { + toast.error(error instanceof Error ? error.message : "保存失败"); + } + }; + + return ( +
+
+

+ + 工具与技能调用统计 +

+

+ 为综合管理中的“最常用工具”和“最常用技能”记录数据来源。统计会记录调用名称、 + 技能名称、耗时、状态及失败时的简短错误摘要,不记录工具参数或输出内容。 +

+
+ +
+
+
记录工具与技能调用
+

+ 关闭后停止写入新的统计记录;已有记录仍可在综合管理中查看。 +

+
+ {isLoading ? ( + + ) : ( + + )} +
+ + {isError && ( +

+ 无法读取调用统计开关,系统将按当前部署配置执行。 +

+ )} +

+ 保存后对下一次工具调用立即生效,无需重启后台;正在执行中的调用不会中途切换。 +

+
+ ); +} diff --git a/frontend-web/src/components/workspace/settings/knowledge-batch-create-settings-page.tsx b/frontend-web/src/components/workspace/settings/knowledge-batch-create-settings-page.tsx new file mode 100644 index 00000000..5591a34e --- /dev/null +++ b/frontend-web/src/components/workspace/settings/knowledge-batch-create-settings-page.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { AlertCircleIcon, Loader2Icon } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { buildWikiKnowledgeBaseCreateInput } from "@/core/llmwiki/knowledge-base-form"; +import { + useLlmWikiActions, + useLlmWikiEmbeddingModels, +} from "@/strategy-components/api/llmwiki"; + +import { SettingsSection } from "./settings-section"; + +type BatchCreateResult = { + createdNames: string[]; + failed: Array<{ name: string; message: string }>; +}; + +function messageOf(error: unknown): string { + return error instanceof Error ? error.message : "操作失败,请稍后重试"; +} + +function parseKnowledgeBaseNames(value: string): string[] { + const seen = new Set(); + return value + .split(/[,,\n\r]+/) + .map((name) => name.trim()) + .filter((name) => { + if (!name || seen.has(name)) return false; + seen.add(name); + return true; + }); +} + +export function KnowledgeBatchCreateSettingsPage() { + const actions = useLlmWikiActions(); + const [namesText, setNamesText] = useState(""); + const [embeddingModelId, setEmbeddingModelId] = useState(""); + const [submitting, setSubmitting] = useState(false); + const [result, setResult] = useState(null); + const embeddingModels = useLlmWikiEmbeddingModels(); + const availableEmbeddingModels = useMemo( + () => (embeddingModels.data?.models ?? []).filter((model) => model.available !== false), + [embeddingModels.data?.models], + ); + const names = useMemo(() => parseKnowledgeBaseNames(namesText), [namesText]); + + useEffect(() => { + if (!embeddingModels.data || availableEmbeddingModels.length === 0) return; + if (availableEmbeddingModels.some((model) => model.id === embeddingModelId)) return; + const defaultId = embeddingModels.data.default_model_id; + setEmbeddingModelId( + availableEmbeddingModels.some((model) => model.id === defaultId) + ? defaultId! + : availableEmbeddingModels[0]!.id, + ); + }, [availableEmbeddingModels, embeddingModelId, embeddingModels.data]); + + const modelSelectionUnavailable = + embeddingModels.isLoading || embeddingModels.isError || !embeddingModelId; + const modelLabel = (model: (typeof availableEmbeddingModels)[number]) => { + const suffix = [model.provider?.trim(), model.model_name?.trim()] + .filter(Boolean) + .join(" · "); + return suffix ? `${model.name}(${suffix})` : model.name; + }; + + const handleCreate = async () => { + if (names.length === 0 || modelSelectionUnavailable) return; + setSubmitting(true); + setResult(null); + actions.create.reset(); + + const nextResult: BatchCreateResult = { createdNames: [], failed: [] }; + for (const name of names) { + try { + await actions.create.mutateAsync(buildWikiKnowledgeBaseCreateInput({ + name, + description: "", + embeddingModelId, + wikiConfig: {}, + })); + nextResult.createdNames.push(name); + } catch (error) { + nextResult.failed.push({ name, message: messageOf(error) }); + } + } + + setResult(nextResult); + setSubmitting(false); + if (nextResult.createdNames.length > 0) { + toast.success(`已创建 ${nextResult.createdNames.length} 个知识库`); + } + if (nextResult.failed.length > 0) { + setNamesText(nextResult.failed.map((item) => item.name).join(",")); + toast.error(`${nextResult.failed.length} 个知识库创建失败,可修改后重试`); + } else { + setNamesText(""); + } + }; + + return ( + +
+
+ +