Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
428588f1c4 | ||
|
|
1a080311bd | ||
|
|
ddf83f7728 | ||
|
|
d93cc62806 | ||
|
|
4fcd96af29 | ||
|
|
99642fca6c | ||
|
|
df408831e5 | ||
|
|
889a97a04d | ||
|
|
a55992e31c | ||
|
|
f82363d81f | ||
|
|
21c4c898e7 | ||
|
|
bd4131b4e2 | ||
|
|
e690475e6e |
No files matched your search
@@ -1,20 +0,0 @@
|
||||
import os
|
||||
|
||||
import paramiko
|
||||
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
client.connect(
|
||||
"119.254.86.251",
|
||||
port=22322,
|
||||
username="root",
|
||||
password=os.environ["TARGET_SSH_PASSWORD"],
|
||||
timeout=15,
|
||||
auth_timeout=15,
|
||||
banner_timeout=15,
|
||||
)
|
||||
_, stdout, stderr = client.exec_command(os.environ["TARGET_SSH_COMMAND"], timeout=30)
|
||||
print(stdout.read().decode(errors="replace"), end="")
|
||||
print(stderr.read().decode(errors="replace"), end="")
|
||||
client.close()
|
||||
@@ -1,9 +1,5 @@
|
||||
# Workspace/editor
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
.vscode/
|
||||
frontend-web/.vscode/
|
||||
node_modules/
|
||||
# Claude Code local-only settings (per-user permissions, not shared)
|
||||
@@ -57,7 +53,6 @@ offline-backend-20260505/bundle/stop_backend.sh
|
||||
|
||||
# offline-backend-20260512 runtime artifacts (large files, local config, data)
|
||||
offline-backend-20260512/.env.example
|
||||
offline-backend-20260512/.env
|
||||
offline-backend-20260512/DEPLOY_BACKEND_OFFLINE_ZH.md
|
||||
offline-backend-20260512/SHA256SUMS
|
||||
offline-backend-20260512/config.yaml.example
|
||||
@@ -69,10 +64,7 @@ offline-backend-20260512/wheelhouse/
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db-*
|
||||
offline-backend-20260512/backend/.deer-flow/*
|
||||
offline-backend-20260512/backend/.env
|
||||
offline-backend-20260512/backend/.env.local
|
||||
offline-backend-20260512/backend/.extensions_config.json.lock
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/stop_backend.sh
|
||||
offline-backend-20260512/check_backend.sh
|
||||
|
||||
@@ -88,89 +80,10 @@ offline-backend-20260512/backend/skill_evolution.json
|
||||
skills/
|
||||
hermes-agent-main/
|
||||
.gitignore
|
||||
offline-backend-20260512/backend/tests/*
|
||||
!offline-backend-20260512/backend/tests/test_large_file_safety.py
|
||||
# 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 openGauss URL/dialect/schema compatibility regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_gauss_database.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
|
||||
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
|
||||
# Keep formal Word OOXML and embedded-font regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_word_export.py
|
||||
# Keep generated-file library aggregation and filtering regression coverage versioned.
|
||||
!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
|
||||
# this deployment repository otherwise ignores newly added backend tests.
|
||||
!offline-backend-20260512/backend/tests/test_model_management.py
|
||||
# Keep the forced-research agent runtime gate and sandbox-policy regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_forced_research_agent.py
|
||||
# Keep strategy-selection built-in agent seed and safety-contract regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_strategy_selection_seed.py
|
||||
# Keep fixed-question exact-match and streaming regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_fixed_questions.py
|
||||
# Keep configurable es_query system-prompt routing regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_es_query_routing_prompt.py
|
||||
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||
# Keep assistant-source snapshot publication atomic across re-import jobs.
|
||||
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
|
||||
# Deep-research reliability regression coverage.
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
|
||||
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
|
||||
!offline-backend-20260512/backend/tests/test_menu_overrides.py
|
||||
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_upload_policy.py
|
||||
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
|
||||
!offline-backend-20260512/backend/tests/test_artifact_preview.py
|
||||
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
|
||||
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
|
||||
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
|
||||
# Roundtable coordinator/seat capability isolation regressions.
|
||||
!offline-backend-20260512/backend/tests/test_roundtable_role_boundary.py
|
||||
# Workflow Studio: schemas / definitions / engine / run-lifecycle regressions
|
||||
# plus the concurrency remediation suite (atomic resume, draft CAS, publish
|
||||
# counter, event seq) — required in CI per docs/WORKFLOW_STUDIO_BACKEND_REMEDIATION_ZH.md P1-06.
|
||||
!offline-backend-20260512/backend/tests/test_workflow_schemas.py
|
||||
!offline-backend-20260512/backend/tests/test_workflow_definitions.py
|
||||
!offline-backend-20260512/backend/tests/test_workflow_engine.py
|
||||
!offline-backend-20260512/backend/tests/test_workflow_runs.py
|
||||
!offline-backend-20260512/backend/tests/test_workflow_concurrency.py
|
||||
!offline-backend-20260512/backend/tests/test_workflow_schema_rebuild.py
|
||||
# Writing-mode first-report pipeline enforcement regressions.
|
||||
!offline-backend-20260512/backend/tests/test_writing_mode_report_middleware.py
|
||||
!offline-backend-20260512/backend/tests/test_writing_rewrite_prompt.py
|
||||
# LLM call timeout retry classification regression coverage.
|
||||
!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.
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_report_tool.py
|
||||
# Report collaboration (RC-BE-000–018) regression coverage.
|
||||
!offline-backend-20260512/backend/tests/test_report_collaboration_*.py
|
||||
!offline-backend-20260512/backend/tests/test_ensure_orm_columns_sync.py
|
||||
offline-backend-20260512/backend/tests/*
|
||||
# MAF roundtable tests must be tracked (excepted from the broad tests/* ignore above)
|
||||
!offline-backend-20260512/backend/tests/test_maf_*.py
|
||||
!offline-backend-20260512/backend/tests/maf_test_helpers.py
|
||||
deploy
|
||||
.history
|
||||
|
||||
@@ -180,42 +93,22 @@ git-clone/
|
||||
# Repo-root offline dependency pack (local only)
|
||||
/dist/
|
||||
|
||||
# Frontend offline pack working directories
|
||||
frontend-web/.offline-pack-build/
|
||||
frontend-web/.offline-extract-test/
|
||||
frontend-web-node_modules-offline-*.tar.gz
|
||||
|
||||
# Local dependency caches and backend scratch archives
|
||||
/.pnpm-store/
|
||||
/offline-backend-20260512/backend/backend*.zip
|
||||
|
||||
# Local offline bundles and scratch tooling
|
||||
/*.tar.gz
|
||||
/offline-backend-20260512/*.zip
|
||||
/tmp/
|
||||
|
||||
.codegraph
|
||||
codegraph.mdc
|
||||
# Frontend offline pack working directories
|
||||
frontend-web/.offline-pack-build/
|
||||
frontend-web/.offline-extract-test/
|
||||
frontend-web-node_modules-offline-*.tar.gz
|
||||
|
||||
.codegraph
|
||||
codegraph.mdc
|
||||
|
||||
docs/aidev-docs
|
||||
docs/backups
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
|
||||
!offline-backend-20260512/backend/skills/
|
||||
offline-backend-20260512/backend/skills/*
|
||||
!offline-backend-20260512/backend/skills/public/
|
||||
offline-backend-20260512/backend/skills/public/*
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# uv lockfile (not tracked per project preference)
|
||||
offline-backend-20260512/backend/uv.lock
|
||||
|
||||
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
|
||||
|
||||
# Local-only scratch directory (not committed)
|
||||
0622/
|
||||
|
||||
@@ -223,4 +116,4 @@ offline-backend-20260512/backend/uv.lock
|
||||
__pycache__/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.python",
|
||||
"ms-python.debugpy"
|
||||
]
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "DeerFlow: Backend FastAPI (8001)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"app.gateway.app:app",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"8001"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
|
||||
"python": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
||||
"envFile": "${workspaceFolder}/offline-backend-20260512/.env",
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceFolder}/offline-backend-20260512/backend;${workspaceFolder}/offline-backend-20260512/backend/.deer-flow/python-vendor",
|
||||
"GATEWAY_CORS_ORIGINS": "http://localhost:5174,http://127.0.0.1:5174,http://localhost:8080,http://127.0.0.1:8080",
|
||||
"UV_LINK_MODE": "copy",
|
||||
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
|
||||
},
|
||||
"console": "integratedTerminal",
|
||||
"redirectOutput": true,
|
||||
"justMyCode": false,
|
||||
"subProcess": true
|
||||
},
|
||||
{
|
||||
"name": "DeerFlow: Frontend Vite (5174)",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "npm.cmd run dev -- --host 0.0.0.0 --port 5174",
|
||||
"cwd": "${workspaceFolder}/frontend-web",
|
||||
"env": {
|
||||
"VITE_BACKEND_BASE_URL": "http://127.0.0.1:8001",
|
||||
"VITE_LANGGRAPH_BASE_URL": "http://127.0.0.1:8001/api"
|
||||
},
|
||||
"serverReadyAction": {
|
||||
"pattern": "Local:\\s+(https?://[^\\s]+)",
|
||||
"uriFormat": "%s",
|
||||
"action": "openExternally"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DeerFlow: Open Frontend in Chrome",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "DeerFlow: VS Code Start",
|
||||
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"Start-Process 'http://127.0.0.1:5174/'\"",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "DeerFlow: Full Stack (Backend + Frontend)",
|
||||
"preLaunchTask": "DeerFlow: Stop Full Stack",
|
||||
"configurations": [
|
||||
"DeerFlow: Backend FastAPI (8001)",
|
||||
"DeerFlow: Frontend Vite (5174)"
|
||||
],
|
||||
"stopAll": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"typescript.tsdk": "frontend-web/node_modules/typescript/lib",
|
||||
"npm.packageManager": "npm"
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "DeerFlow: VS Code Start",
|
||||
"type": "process",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}/scripts/vscode-start.ps1",
|
||||
"-OpenBrowser"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Start Full Stack and Wait",
|
||||
"type": "process",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}/scripts/start-all-and-wait-frontend.ps1"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Start Full Stack",
|
||||
"type": "process",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}/scripts/start-all.ps1"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Stop Full Stack",
|
||||
"type": "process",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}/scripts/stop-all.ps1"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Status",
|
||||
"type": "process",
|
||||
"command": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}/scripts/status.ps1"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Check Local Dev Environment",
|
||||
"type": "shell",
|
||||
"command": "$backendPython = '${workspaceFolder}\\offline-backend-20260512\\backend\\.venv\\Scripts\\python.exe'; if (-not (Test-Path -LiteralPath $backendPython)) { throw 'Backend virtual environment is missing. Run uv sync in offline-backend-20260512/backend first.' }; & $backendPython -c \"import uvicorn; print('Backend Python OK')\"; npm --prefix '${workspaceFolder}\\frontend-web' exec vite -- --version",
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command"
|
||||
]
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Install Frontend Dependencies",
|
||||
"type": "process",
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"ci",
|
||||
"--no-audit",
|
||||
"--no-fund"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend-web"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Frontend Typecheck",
|
||||
"type": "process",
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"run",
|
||||
"typecheck"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend-web"
|
||||
},
|
||||
"problemMatcher": "$tsc"
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Frontend Build",
|
||||
"type": "process",
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"run",
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/frontend-web"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: Backend Tests",
|
||||
"type": "process",
|
||||
"command": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
||||
"args": [
|
||||
"-m",
|
||||
"pytest",
|
||||
"tests/",
|
||||
"-v"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
|
||||
"env": {
|
||||
"PYTHONPATH": ".",
|
||||
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "DeerFlow: WeKnora Backend Tests",
|
||||
"type": "process",
|
||||
"command": "${workspaceFolder}/offline-backend-20260512/backend/.venv/Scripts/python.exe",
|
||||
"args": [
|
||||
"-m",
|
||||
"pytest",
|
||||
"tests/test_llmwiki_weknora.py",
|
||||
"-v"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/offline-backend-20260512/backend",
|
||||
"env": {
|
||||
"PYTHONPATH": ".",
|
||||
"WEKNORA_ADMIN_PASSWORD": "1qaz@WSX"
|
||||
}
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
# 深度研究第三轮:检索稳定性修复 + 参考文献确定性 + 页面对齐 AI 写作改造
|
||||
|
||||
## A. 两个 bug 的排查结论与修复
|
||||
|
||||
### A1. 第二次检索「网络波动」——确认是真问题(DDG 限流 + 错误语义混淆 + 零重试)
|
||||
|
||||
**排查结论**(用户判断"确实是网络波动"基本正确,但代码层面放大了它):
|
||||
- 「网络波动」文案不存在于任何代码中——是收集员 LLM 看到 `{"error": "No results found"}` 信封后自己编的解释
|
||||
- 真实根因:DDG html 端点对同 IP 快速连续请求限流(403/429 挑战页)→ `DDGSException` → `ddg_search/tools.py::_search_text` 把异常吞成 `[]` → 上层把「被限流」和「真没结果」混成同一个错误信封;工具层完全没有重试
|
||||
|
||||
**修复**(`packages/harness/deerflow/community/ddg_search/tools.py`):
|
||||
1. `_search_text` 对异常加 2 次重试 + 指数退避(约 1.5s / 3s + 抖动)
|
||||
2. 错误信封区分场景:真无结果 → `no results`;多次尝试仍失败 → `web search failed after N attempts (rate limited or network), 换一组检索词稍后重试`——让模型不再瞎猜「网络波动」
|
||||
3. 收集员 SOUL.md 行为准则加一条(同步改种子资产 + 已安装副本 `.deer-flow/agents/deep-research-collector/`):搜索返回 error 信封时先换检索词重试一轮,而不是直接告知用户网络异常
|
||||
|
||||
### A2. 参考文献缺失——代码确有隐患(当前是模型自发行为,时有时无)
|
||||
|
||||
**排查结论**:后端没有任何「参考来源」追加逻辑;四个 runner 的报告 prompt 只要求内联 `[N]` 引用。出现过的参考列表是模型看到上下文头 `[N] 标题(URL)` 后**自发**补写的——弱模型下不可靠,与"信息够不够"无必然关系。
|
||||
|
||||
**修复**(prompt 指示 + 确定性兜底双保险):
|
||||
1. 四个 runner(basic / detailed / deep / multi_agent)的报告 prompt 各加一条:报告最后附「## 参考来源」,按 `[N] 标题. URL` 列出正文实际引用过的来源
|
||||
2. `basic.py` 新增共享函数 `ensure_reference_section(report, context_list, materials)`:报告缺少参考小节时,解析正文引用的 `[N]` 编号,按压缩上下文顺序生成编号列表追加(无编号时列全部来源,封顶 20 条;已有参考小节则不重复追加)。在各 runner 最终 `write_text("report.md", ...)` 前调用(约 7 个调用点,跳过零材料 fallback)
|
||||
3. 新增测试 `tests/test_deep_research_references.py`:helper 单测(正常追加/编号映射/不重复追加/无 URL 来源)+ runner 集成断言
|
||||
|
||||
## B. 页面改造(核心需求):左列复用 AI 写作对话形态,右列改真沙箱
|
||||
|
||||
### B1. 左列 = AI 写作式对话收集面板(仿 WritingSetupChat)
|
||||
|
||||
- **空状态欢迎屏**(无消息时隐藏头部,展示欢迎卡片,WritingWelcome 式)+ 有消息后 `MessageList`(沿用现有绑 collector 线程的接线)+ 首 token 前「正在整理…」占位条
|
||||
- **输入框换成白卡片 PromptInput 套件**(与 AI 写作完全同款组件):卡片内一行「当前对话模型」Select(沿用 ModelNameSelect)+ 高级配置入口;下方 PromptInputTextarea + PromptInputFooter + PromptInputSubmit(`status` ready/streaming,流式中变停止按钮)。经典模式入口保留在高级配置对话框
|
||||
- **配置卡片进对话流**(对标 AI 写作「识别出意图后出现表单」):
|
||||
- `message-list.tsx` 最小扩展(约 15 行):新增分组 `assistant:research-approval`(识别 `additional_kwargs.setup_research_approval`)+ 可选 prop `researchApprovalSlot?: ReactNode`;不传 slot 或无该载荷时零行为变化——普通对话/AI 写作完全不受影响
|
||||
- workbench:收集回合结束(awaiting_report,即"意图识别完成")时向 `displayThread` 尾部注入一条虚拟 ai 消息(WritingSetupChat 的 displayThread 同款手法),slot 传现有 `ReportConfigCard` → 卡片作为助手下一轮出现在对话流里
|
||||
- 写作进度卡、完成卡、摘要气泡、追问列表保留在列表下方卡片区(报告正文移到右侧沙箱)
|
||||
|
||||
### B2. 右列 = 工作区真沙箱(Step2SandboxLayout 的 stub 线程模式)
|
||||
|
||||
- 新建 `deep-research/ResearchSandboxPanel.tsx`(约 100 行,含关闭/重开)+ `useReportSandbox.ts` hook(仿 `useStep3Report` 的 stub 机制):
|
||||
- threadStub:`messages = [{type:'ai', tool_calls:[{name:'write_file', args:{path:'/mnt/user-data/outputs/report.md', content: <流式报告>}}]}]`,`isLoading = job 运行中 && phase !== 'summarizing'`
|
||||
- `report_delta` 首帧 → `openArtifact(write-file: 虚拟URL)` 自动展开右栏;`run.report` 每帧 upsert stub(带防抹空守卫);刷新/切换会话用 `session.reportMarkdown` 重建
|
||||
- `ArtifactFileDetail` 自带能力全部免费获得:**流式期 code 视图自动滚底 → 报告写完 isLoading 翻 false 自动切 preview + 脉冲提示**(正是「生成完直接切预览」)、Streamdown 渲染的 Markdown(比现在的好看)、另存 Word/复制/下载、默认走展示层脱敏
|
||||
- 布局:ResizablePanelGroup chat 60 / sandbox 40(对齐 ChatBox/圆桌),沙箱可关闭、头部按钮重开
|
||||
- **删除自定义 `ResearchSandbox` 组件**;legacy 会话与经典模式共用同一右栏(`run.report` 数据源相同,行为一致)
|
||||
- 已知小限制(可接受):报告配图模式下沙箱内图片可能无法经 artifacts API 解析(配图默认关闭;真需要时后续把产物落 thread artifacts)
|
||||
|
||||
### B3. 验证
|
||||
|
||||
- 后端:`PYTHONPATH=. uv run --no-sync pytest tests/ -k deep_research -q --ignore=tests/test_notification_templates.py` 全绿 + ruff;改完配置/SOUL 后 touch 触发 dev 服务重载
|
||||
- 前端:`pnpm typecheck` 零新增错误(基线 51)
|
||||
- 手动走查:连续两次检索不再误报、弱模型报告末尾必带参考来源、页面全流程(欢迎屏 → 对话收集 → 流内配置卡 → 沙箱流式 → 自动预览 → 摘要/追问)
|
||||
|
||||
## 涉及文件
|
||||
|
||||
**后端**:`community/ddg_search/tools.py`、`deep_research/runners/{basic,detailed,deep,multi_agent}.py`、收集员 SOUL.md(种子 + 已安装副本)、`tests/test_deep_research_references.py`(新)
|
||||
**前端**:`components/workspace/messages/message-list.tsx`(可选 slot 分组)、`pages/deep-research/DeepResearchWorkbench.tsx`(大改)、`pages/deep-research/ResearchSandboxPanel.tsx`(新)、`pages/deep-research/useReportSandbox.ts`(新)
|
||||
|
||||
## 明确不动
|
||||
|
||||
- AI 写作 / 普通对话现有组件行为(message-list.tsx 仅加可选分组,缺省零变化)
|
||||
- 报告写作管线的写作逻辑本身(prompt 加一条要求 + 末尾确定性追加参考小节,均为附加式)
|
||||
- 深度研究后端会话/任务/事件模型(本轮不动)
|
||||
@@ -1,113 +0,0 @@
|
||||
# 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/ 出现在产物目录扫描(文件噪音)——一期保持现状,二期优化。
|
||||
@@ -0,0 +1,122 @@
|
||||
# MAF 多智能体会商 Phase-4 前端实施计划
|
||||
|
||||
## 决策回顾(已与用户确认)
|
||||
- **入口**:独立顶级菜单 `/page/maf/*`,新增侧边栏一级项「MAF 会商」
|
||||
- **实时更新**:纯前端轮询,`GET /runs/{id}/events?after_seq=N&limit=1000`,间隔 1.8s,复用现有后端,零 SSE、零后端改动
|
||||
- **流程图**:要。基于 `confirmed_facts`(来自 `run.maf_checkpoint.fact_ledger`)+ 末轮 `final_result.content` 解析的 `flow-json`,复用 `@xyflow/react` + `flow-graph.ts` + `FlowGraphPanel`
|
||||
- **审批**:卡片式,仿 `IntentClarificationCard` 的视觉/交互模式(参数摘要 + 批准/拒绝按钮 + 备注 textarea),落到 `POST /approvals/{id}/decision`
|
||||
|
||||
## 关键数据契约(已核实后端源码)
|
||||
- **后端只读**:16 个端点全部命中,无改动。`GET /health` 返回 `enabled` 决定页面内容(入口仍可见,未启用时显示空态)。
|
||||
- **事件流**:单调 `(run_id, seq)`,`GET /events?after_seq=N` 返回 `{events:[...], latest_seq}`;前端推进 `after_seq=latest_seq`。payload 已在后端**写入时脱敏**(`sanitize_payload`),前端直接渲染。
|
||||
- **事件分类**(`event_normalizer.py`):`run.started/completed/failed/cancelled`、`agent.turn_started/message.delta/message.completed`、`tool.requested/started/completed/failed`、`skill.started/completed/failed/selected`、`artifact.created`、`action.waiting_approval/approved/rejected`、`workflow.checkpointed`。
|
||||
- **事实账本**(`fact_ledger.empty_ledger`):`candidate_facts`/`confirmed_facts`/`evidence`/`tool_results`/`executed_actions`/`pending_actions`/`failed_or_unknown_actions`/`conflicts`。`confirmed_facts` 条目含 `{candidate_id, text, agent_id, confirmed_by, evidence_refs[]}`。
|
||||
- **Run 字段**(`run_to_dict`):`id, session_id, status, maf_checkpoint(含 fact_ledger + final_result + seat_outcomes), last_event_seq, started_at, finished_at, error_code, error_message_safe, token_usage, latency_ms`。
|
||||
- **终态判定**:`runIsLive(status) = ["pending","running","waiting_approval","cancelling","cancel_failed"].includes(status)`,非 live 即停轮询。
|
||||
- **Action/Approval**:`GET /runs/{id}/actions`、`GET /runs/{id}/approvals`;`POST /approvals/{id}/decision` `{decision:"approved"|"rejected"}`。
|
||||
|
||||
## 复用清单(已定位到文件:行)
|
||||
- `apiFetch`(`core/api/fetch-client.ts`):内部自动注入 `Authorization`(`getAuthorizationHeaderValue()`)+ `credentials:"include"`,**返回 Response 不解析、不拼 URL**。调用方拼 `${getBackendBaseURL()}/api/maf-roundtable/...`,自行 `res.ok` 检查 + `res.json()`。
|
||||
- `getBackendBaseURL`(`core/config/index.ts`):未设时返回 `""`,相对路径同源可用。
|
||||
- TanStack Query 模式(`strategy-components/api/task-buttons.ts`):`useQuery`+`refetchOnWindowFocus:false`;`useMutation`+`onSuccess invalidate`;queryKey 用嵌套数组 `["maf-roundtable", runId, "events"]`。
|
||||
- 轮询模式(`core/scheduled-tasks/hooks.ts:105-115`):函数式 `refetchInterval: (query) => runIsLive(query.state.data?.status) ? ms : false`。
|
||||
- `FlowGraphPanel`(`roundtable-planning/components/FlowGraphPanel.tsx`):props `{data: FlowGraphData, streaming, onClose, ...}`,自动 `useTheme()` 取明暗。`FlowGraphData = {title, nodes:[{id,label,type,detail?,group?}], edges:[{from,to,label?}]}`。`extractFlowGraph(text)`(`flow-graph.ts:263`)容错解析 ```flow-json``` 代码块(含半截 JSON)。
|
||||
- `IntentClarificationCard`(`roundtable-planning/components/IntentClarificationCard.tsx`):卡片视觉模板(`rounded-xl border-red-100` + 「需要确认」徽标 + 选项按钮 + textarea + 提交)。
|
||||
- 时间线 feed(`roundtable-planning/components/Step1Panel.tsx` + `MessageBubble` + `useAutoScroll` + `ScrollToBottomButton`):数组入、按 seq 序渲染。
|
||||
- `useDesensitizeSubtree`(`roundtable-planning/lib/use-desensitize-subtree.ts`):仅挂 header/面板框架;时间线 body 不挂(payload 已脱敏 + hook 自身警告勿用于消息列表)。
|
||||
- 路由(`pages/PageRoutes.tsx:143-169`):`<Route path="maf/*" element={<MafRoutes/>}/>`(仿 `RoundtableRoutes.tsx`)。
|
||||
- 菜单(`core/page-layout/sidebar-menu.ts`):在 `sidebarMenuItems` 数组加一级项,`icon` 用 lucide 组件(非字符串 key)。新节点经 `use-sidebar-menu.ts` 自动走 static→inject→override→role→desensitize 流水线,无需额外注册。
|
||||
|
||||
## 文件夹结构(镜像 `roundtable-planning/`,全新 `src/maf-roundtable/`)
|
||||
```
|
||||
frontend-web/src/maf-roundtable/
|
||||
├── api/
|
||||
│ ├── types.ts # TS 类型(snake_case 直传,后端即 snake_case)
|
||||
│ ├── client.ts # BASE()=getBackendBaseURL()+"/api/maf-roundtable";apiFetch 封装 + readJson<T>/parseDetail
|
||||
│ ├── sessions.ts # 会话 CRUD hooks(list/get/create/update/delete)
|
||||
│ ├── runs.ts # run hooks(get/list/create/cancel/resume/recover)+ useMafHealth
|
||||
│ ├── events.ts # listEvents(runId, afterSeq) 原始 fetch(供轮询 hook 用)
|
||||
│ └── approvals.ts # 审批 hooks(list + decide mutation)
|
||||
├── hooks/
|
||||
│ ├── use-maf-run-status.ts # GET /runs/{id} useQuery,函数式 refetchInterval 停终态
|
||||
│ └── use-maf-events.ts # ★ cursor 轮询 hook(核心)
|
||||
├── lib/
|
||||
│ ├── event-timeline.ts # 纯函数:events[] → TimelineEntry[](delta 聚合、tool 生命周期折叠、approval 锚点)
|
||||
│ └── fact-flow-graph.ts # 纯函数:confirmed_facts+evidence → FlowGraphData
|
||||
├── components/
|
||||
│ ├── MafRunHeader.tsx # 状态徽标 + token/延迟 + 取消/恢复/重试
|
||||
│ ├── MafEventTimeline.tsx # 时间线 feed(MessageBubble + useAutoScroll)
|
||||
│ ├── MafSeatStatusPanel.tsx # 席位状态矩阵 + execution_stage 徽标
|
||||
│ ├── MafFactFlowPanel.tsx # 包一层 FlowGraphPanel,数据来自 fact-flow-graph
|
||||
│ ├── MafApprovalCard.tsx # 卡片式审批(批准/拒绝 + 备注)
|
||||
│ └── MafHealthGate.tsx # health.enabled=false → 空态
|
||||
├── pages/
|
||||
│ ├── MafSessionsPage.tsx # 会话列表 + 新建
|
||||
│ └── MafRunDetailPage.tsx # 单 run 工作区(三栏)
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## 核心:cursor 轮询 hook(`hooks/use-maf-events.ts`)
|
||||
不用纯 `useQuery+refetchInterval`:cursor `after_seq` 需每次推进,queryFn 不持有「上次 latest_seq」,塞 queryKey 会抖动。**用自定义 hook**(结合 scheduled-tasks 的 refetchInterval 思路 + roundtable-planning 的 reader-loop 家族):
|
||||
```ts
|
||||
export function useMafEvents(runId?: string, active=false) {
|
||||
const [events, setEvents] = useState<MafEvent[]>([]);
|
||||
const afterSeqRef = useRef(0); // 避免 setInterval 闭包旧值
|
||||
useEffect(() => {
|
||||
if (!runId || !active) return;
|
||||
let cancelled = false;
|
||||
const poll = async () => {
|
||||
const res = await listEvents(runId, afterSeqRef.current);
|
||||
if (cancelled || !res.events.length) return;
|
||||
setEvents(prev => mergeBySeq(prev, res.events)); // seq 去重
|
||||
afterSeqRef.current = res.latest_seq;
|
||||
};
|
||||
poll(); // 进入即补拉(after_seq=0 全量历史)
|
||||
const id = setInterval(poll, 1800);
|
||||
return () => { cancelled = true; clearInterval(id); };
|
||||
}, [runId, active]);
|
||||
return events;
|
||||
}
|
||||
```
|
||||
- **补拉历史**:进入页面 `after_seq=0` 立即拉全部已落库事件 → 天然支持断线补拉/刷新恢复。
|
||||
- **终态停止**:`active` 由 `useMafRunStatus` 的 `runIsLive(status)` 传入,终态时 effect 清理 interval。
|
||||
- **去重**:后端 `(run_id,seq)` + `source_event_id` 双重去重,前端再按 `seq` 做 `mergeBySeq`,幂等。
|
||||
- **切换 run**:`runId` 变 → effect 重置(events 清空、afterSeq 归零)。
|
||||
|
||||
## 流程图数据流(`lib/fact-flow-graph.ts`)
|
||||
两源按优先级合并 → `FlowGraphData`:
|
||||
1. **`confirmed_facts`(主)**:每条已确认事实 → `task` 节点(label=text 截断、detail=全文、group=confirmed_by);`evidence_refs` 连到 `evidence`(→ `role`)或 `tool_results`(`tool:{name}:{turn}` → `deliverable`)节点;证据根用 `start`。
|
||||
2. **`final_result.content` 的 ```flow-json```(次)**:用现成 `extractFlowGraph(text)` 解析叠加为「结论流程图」。
|
||||
|
||||
输出喂 `<FlowGraphPanel data={...} streaming={!terminal}/>`(非终态蚂蚁线动)。两源皆空 → 空态「尚无可绘制事实」。
|
||||
|
||||
## 页面布局(`MafRunDetailPage`,三栏 / 窄屏堆叠)
|
||||
- **左:时间线**(`MafEventTimeline`)—— `event-timeline.ts` 折叠成可读条目(delta 聚合气泡、tool 折叠「调用 X→完成」、action.waiting_approval 插锚点),复用 MessageBubble + useAutoScroll + ScrollToBottomButton。
|
||||
- **中上:席位状态**(`MafSeatStatusPanel`)—— `maf_checkpoint.seat_outcomes` 的 agent × status 矩阵 + `execution_stage` 徽标。
|
||||
- **中下:事实/结论流程图**(`MafFactFlowPanel`)—— 折叠面板,默认展开。
|
||||
- **右:审批卡片**(`MafApprovalCard` 列表)—— 仅 pending 时显示;含工具名、参数摘要(脱敏)、批准/拒绝按钮 + 备注 textarea → `POST /approvals/{id}/decision`,成功后 invalidate `["maf-roundtable","approvals",runId]`,事件轮询自然看到 `action.approved/rejected`。
|
||||
|
||||
## 路由 + 菜单接入(3 处)
|
||||
1. **新建** `pages/MafRoutes.tsx`(仿 `RoundtableRoutes.tsx`):`index→MafSessionsPage`、`runs/:runId→MafRunDetailPage`、`*→重定向`。
|
||||
2. **改** `pages/PageRoutes.tsx`:加 `import MafRoutes` + `<Route path="maf/*" element={<MafRoutes/>}/>`(插 `notebook` 后)。
|
||||
3. **改** `core/page-layout/sidebar-menu.ts`:数组末尾加 `{ id:"maf-center", label:"MAF 会商", icon:Network, children:[{id:"maf-roundtable", label:"会商列表", icon:MessageSquareText, path:"/page/maf"}] }`。**不加 `adminOnly`**(全员可见;`/health.enabled=false` 由 `MafHealthGate` 客户端控制页面空态)。
|
||||
|
||||
## 实施顺序
|
||||
1. `api/types.ts` + `api/client.ts`(地基)
|
||||
2. `api/{sessions,runs,events,approvals}.ts`(端点 hook)
|
||||
3. `hooks/{use-maf-run-status,use-maf-events}.ts`(轮询核心)
|
||||
4. `lib/{event-timeline,fact-flow-graph}.ts`(纯函数)
|
||||
5. `components/*`(6 个组件)
|
||||
6. `pages/*`(2 个页面)+ `index.ts`
|
||||
7. `pages/MafRoutes.tsx` + 接入 `PageRoutes.tsx` + `sidebar-menu.ts`
|
||||
8. `pnpm typecheck` + 手动核对路由/菜单显隐
|
||||
|
||||
## 文件清单
|
||||
- **新建**(14):`maf-roundtable/api/{types,client,sessions,runs,events,approvals}.ts`、`maf-roundtable/hooks/{use-maf-run-status,use-maf-events}.ts`、`maf-roundtable/lib/{event-timeline,fact-flow-graph}.ts`、`maf-roundtable/components/{MafRunHeader,MafEventTimeline,MafSeatStatusPanel,MafFactFlowPanel,MafApprovalCard,MafHealthGate}.tsx`、`maf-roundtable/pages/{MafSessionsPage,MafRunDetailPage}.tsx`、`maf-roundtable/index.ts`、`pages/MafRoutes.tsx`
|
||||
- **改动**(2):`pages/PageRoutes.tsx`、`core/page-layout/sidebar-menu.ts`
|
||||
- **后端**:无改动
|
||||
|
||||
## 验证
|
||||
- `pnpm typecheck`(tsc --noEmit)通过
|
||||
- 手动:菜单「MAF 会商」→ 列表 → 新建会话 → 启动 run → 时间线滚动 → 席位状态更新 → 流程图随 confirmed_facts 增长 → 审批可批准 → 终态停轮询
|
||||
- 退路:`/health.enabled=false` → 入口可见但页面显示未启用提示;旧 `roundtable-planning` 零改动
|
||||
@@ -11,13 +11,6 @@ This is a customized offline deployment of **DeerFlow** — a LangGraph-based AI
|
||||
|
||||
Managed together via shell scripts in `scripts/`.
|
||||
|
||||
## Offline Release Repository
|
||||
|
||||
Linux AMD64 离线发版脚本、wheelhouse、`deploy.env` 模板和部署文档统一维护在
|
||||
`knowledge-base-bj/deerflow-offline-deployment`,不要把离线镜像或发版资产提交到
|
||||
本业务源码仓库。正式发版从该仓库执行 `scripts/build-latest-master.sh`,脚本会读取
|
||||
本仓库最新 `master` 的干净源码快照。
|
||||
|
||||
## Commands
|
||||
|
||||
### Full Application (from repo root)
|
||||
@@ -80,8 +73,6 @@ The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph
|
||||
|
||||
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
||||
|
||||
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `AGENTS.md`/`CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
|
||||
|
||||
### Frontend
|
||||
|
||||
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
@@ -90,24 +81,17 @@ 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)
|
||||
- **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).
|
||||
- **菜单管理 (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/`。
|
||||
- **简洁模式用户引导 (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>` 窗口事件强制重放(忽略已看过记忆)。
|
||||
- **菜单管理 (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).
|
||||
- **敏感词管理 (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/`。
|
||||
- **按钮管理 (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). **`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. 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`
|
||||
- **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/`
|
||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine 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 提示。
|
||||
|
||||
- **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
|
||||
|
||||
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.
|
||||
|
||||
@@ -73,27 +73,22 @@ The **Lead Agent** (`deerflow/agents/lead_agent/`) is the single LangGraph graph
|
||||
|
||||
**Harness → App import firewall** is enforced by `tests/test_harness_boundary.py` (runs in CI).
|
||||
|
||||
**Workflow Studio** (`deerflow/workflows/` + `/api/workflows*`) is a second, independent execution path beside the lead agent: a persisted DAG scheduler that runs published workflow graphs, streams a durable event log over SSE, and supports human-input pauses, cancellation, and crash recovery via lease reclaim. Coze Studio supplies the canvas UI only; LangGraph is used only *inside* an `agent`/`skill` node. Master switch and all limits live in `config.yaml` → `workflows`. See the backend `CLAUDE.md`「Workflow Studio」and `docs/WORKFLOW_STUDIO_BACKEND_DEV_ZH.md`.
|
||||
|
||||
### Frontend
|
||||
|
||||
The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
|
||||
- **Routing**: React Router v7; top-level routes in `src/pages/PageRoutes.tsx`, workspace sub-routes in `src/pages/WorkspaceRoutes.tsx`
|
||||
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
|
||||
- **普通问答地图可视化**: shared `MarkdownContent` recognizes only dedicated `map-json` fences and otherwise preserves the legacy Markdown/Streamdown path byte-for-byte. `components/workspace/visualizations/` validates the bounded map contract with Zod, lazy-loads MapLibre, and provides loading/error/text fallbacks so malformed or unreachable maps never blank the message. The lead-agent prompt emits `map-json` only when the user explicitly asks for a map; flowcharts and other graphics continue through the existing Markdown/Mermaid path. The raster tile template comes from the single post-build setting `runtime-config.js` → `VITE_MAP_TILE_URL` (Google by default); replace it with the intranet `{z}/{x}/{y}` endpoint for disconnected deployments.
|
||||
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是初始化脚本读取的瓦片模板。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`,地图使用 Leaflet `data-df-map`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
|
||||
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
|
||||
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
|
||||
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
|
||||
- **轻应用中心 (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). **简介模式** left-nav is a separate overlay: `/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-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).
|
||||
- **白名单管理 (Login Whitelist, admin-only)**: gates **who may log in**, run as a **blocklist (黑名单模式)**. Two pieces: a global master switch (`system_settings.json → login_whitelist.enabled`, surfaced by **admin-only** `GET|PUT /api/system-settings/login-whitelist`) + a per-user **放行** flag (`users.approved` column, migration `20260625_02`). Users **self-register** as usual and **every new account defaults to `approved=True`** — so a user who has never logged into this system before is allowed straight through; the switch only blocks users an admin has explicitly **取消放行** (`approved=False`). (`create_user`/`User`/`UserRow` all default `approved=True`; admins are always approved; existing rows are backfilled `True`.) Login interception is the single helper `enforce_user_approved(user)` in `app/gateway/routers/auth.py`, called in all three login endpoints (`/login/{local,username,token}`) **after** the account is resolved/created and **before** the JWT is issued: switch off → everyone passes; `system_role=="admin"` → always passes (anti-lockout); else `approved=False` (admin-revoked) → 403 `{code:"PENDING_APPROVAL"}` (the frontend's `extractErrorMessage` surfaces the message and login throws before writing any auth state, so no half-login). Admin approves on `pages/LoginWhitelistPage.tsx` (route `/page/strategy/admin/login-whitelist`, entry「白名单管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both sidebar modes): a top master-switch + a list of all registered users (`GET /api/v1/auth/users`, now carrying `approved`) with a per-row 放行 Switch (`PUT /api/v1/auth/users/{id}/approval`, admin-only; revoking only blocks the **next** login — `token_version` is left untouched, existing sessions are not force-killed). Client/hooks: switch in `strategy-components/api/login-whitelist.ts`, user list + approval in `core/auth/{api,hooks}.ts` (`useSetUserApproval`). Backend tests: `tests/test_login_approval.py`. **账号级「登录口令」(per-account password — 临时开通)**: the same page also lets an admin assign one specific account an individual **plaintext** password (`users.login_password` column, migration `20260625_04`, nullable; auto-added on startup by `_ensure_orm_columns_sync`). Once set, **that account's** username login (`POST /api/v1/auth/login/username`) **requires** `?password=` to exactly match it and that personal password **takes precedence over** the global shared-password gate (`username_login.require_password`); accounts without one keep today's shared-gate / passwordless behavior. The user then logs in via the **existing** frontend URL `#/login/<用户名>?password=xxxx` (LoginPage already forwards `?password=`; `<用户名>` = the account's email `@`-prefix). **Plaintext is intentional** — the password rides in the URL query anyway, so storing it readably lets the admin re-copy the full login link from the page; it is exposed only on the **admin-only** `GET /api/v1/auth/users` (`UserListItem.login_password`) and the new admin-only `PUT /api/v1/auth/users/{id}/login-password` (set non-empty / clear with null·empty; doesn't touch `token_version`). The whitelist page renders a per-user 「登录口令」 column with a set/修改 editor + a live 「复制登录链接」 button (`useSetUserLoginPassword` in `core/auth/hooks.ts`, `setUserLoginPassword` in `core/auth/api.ts`). Backend gate helper `_lookup_user_by_username` (lookup-only, no auto-register) in `routers/auth.py`; tests `tests/test_login_personal_password.py`.
|
||||
- **敏感词管理 (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 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). **`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`) 默认指向**应用内**「大屏绘制」页,在 **both** 单智能体 (`TaskFxWorkspace`) 与 多智能体会商 (`RoundtablePlanningPage` 深链 rwfx-A) 都显示(共用 `TaskJumpBar`)。**`runTaskButton` 的 `url` 类型支持应用内路由**:target 以单个 `/` 开头时按内部路由处理——`self` 走 react-router 同窗导航(不刷新),`blank` 拼成当前源的 hash 路由新标签打开;外链行为不变。 The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三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」.
|
||||
- **会话冲突 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()`.
|
||||
- **会话冲突 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) and shows a friendly `toast.info` instead of a raw error.
|
||||
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
|
||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
|
||||
@@ -5,9 +5,6 @@ const LEGACY_LOCAL_API_BASE_URL = "http://localhost:8001";
|
||||
const LEGACY_LOCAL_WEB_BASE_URL = "http://localhost:5174";
|
||||
let pollTimer = null;
|
||||
let isPolling = false;
|
||||
let startupPromise = null;
|
||||
|
||||
const POLLING_SCHEDULE_SETTING_KEYS = new Set(["taskPollingEnabled", "taskPollIntervalSeconds"]);
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -15,14 +12,6 @@ function delay(ms) {
|
||||
|
||||
async function getSettings() {
|
||||
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
||||
const discovered = await chrome.storage.local.get({
|
||||
discoveredApiBaseUrl: "",
|
||||
discoveredWebBaseUrl: "",
|
||||
});
|
||||
if (settings.autoDiscoverServiceUrl) {
|
||||
settings.apiBaseUrl = discovered.discoveredApiBaseUrl || settings.apiBaseUrl;
|
||||
settings.webBaseUrl = discovered.discoveredWebBaseUrl || settings.webBaseUrl;
|
||||
}
|
||||
const updates = {};
|
||||
if (!settings.apiBaseUrl || settings.apiBaseUrl === LEGACY_LOCAL_API_BASE_URL) {
|
||||
updates.apiBaseUrl = DEER_CLIPPER_DEFAULTS.apiBaseUrl;
|
||||
@@ -80,7 +69,10 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
|
||||
}
|
||||
};
|
||||
|
||||
add(currentApiBase);
|
||||
add(webBase);
|
||||
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
|
||||
add(LEGACY_LOCAL_API_BASE_URL);
|
||||
|
||||
try {
|
||||
const url = new URL(normalizeServiceBase(webBase));
|
||||
@@ -93,10 +85,6 @@ function apiCandidatesForWebBase(webBase, currentApiBase) {
|
||||
// Ignore invalid candidates.
|
||||
}
|
||||
|
||||
add(currentApiBase);
|
||||
add(DEER_CLIPPER_DEFAULTS.apiBaseUrl);
|
||||
add(LEGACY_LOCAL_API_BASE_URL);
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@@ -105,12 +93,6 @@ async function browserContextStatus(apiBaseUrl) {
|
||||
const response = await fetch(`${apiBaseUrl}/api/browser-context/status`, {
|
||||
credentials: "include",
|
||||
});
|
||||
// The gateway may enforce authentication globally before this otherwise
|
||||
// public discovery endpoint is reached. A 401/403 still proves that this
|
||||
// candidate is the zncm API paired with the open frontend tab.
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { enabled: true, authenticationRequired: true };
|
||||
}
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
@@ -219,12 +201,9 @@ async function discoverServiceBases(settings, force = false) {
|
||||
settings.webBaseUrl !== next.webBaseUrl ||
|
||||
settings.lastServiceSyncSource !== next.lastServiceSyncSource
|
||||
) {
|
||||
// Discovered endpoints are runtime state, not user-authored preferences.
|
||||
// Keep them in storage.local so Chrome sync write quotas cannot break task
|
||||
// polling. The options page may still persist explicit user choices.
|
||||
await chrome.storage.local.set({
|
||||
discoveredApiBaseUrl: next.apiBaseUrl,
|
||||
discoveredWebBaseUrl: next.webBaseUrl,
|
||||
await chrome.storage.sync.set({
|
||||
apiBaseUrl: next.apiBaseUrl,
|
||||
webBaseUrl: next.webBaseUrl,
|
||||
lastServiceSyncAt: next.lastServiceSyncAt,
|
||||
lastServiceSyncSource: next.lastServiceSyncSource,
|
||||
});
|
||||
@@ -847,50 +826,19 @@ async function pollOnce() {
|
||||
}
|
||||
}
|
||||
|
||||
async function initializePolling() {
|
||||
async function startup() {
|
||||
let settings = await getSettings();
|
||||
try {
|
||||
settings = await discoverServiceBases(settings);
|
||||
} catch (error) {
|
||||
// Service discovery persists diagnostic metadata in storage.sync. Chrome can
|
||||
// temporarily reject those writes after its quota is reached; polling must
|
||||
// still start with the last known settings.
|
||||
console.warn("zncm service discovery failed during startup", error);
|
||||
}
|
||||
try {
|
||||
settings = await syncAuthToken(settings);
|
||||
} catch (error) {
|
||||
// A failed status/token write must not prevent alarms from being installed.
|
||||
// pollOnce() will retry token synchronization before claiming each task.
|
||||
console.warn("zncm token sync failed during startup", error);
|
||||
}
|
||||
settings = await discoverServiceBases(settings);
|
||||
settings = await syncAuthToken(settings);
|
||||
await refreshAlarm(settings);
|
||||
await scheduleNextPoll(1000);
|
||||
}
|
||||
|
||||
function startup() {
|
||||
if (!startupPromise) {
|
||||
startupPromise = initializePolling().finally(() => {
|
||||
startupPromise = null;
|
||||
});
|
||||
}
|
||||
return startupPromise;
|
||||
}
|
||||
|
||||
async function restartPollingSchedule() {
|
||||
const settings = await getSettings();
|
||||
await refreshAlarm(settings);
|
||||
await scheduleNextPoll(0);
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
|
||||
chrome.runtime.onStartup.addListener(() => void startup().catch((error) => console.warn("zncm startup failed", error)));
|
||||
chrome.runtime.onInstalled.addListener(() => void startup());
|
||||
chrome.runtime.onStartup.addListener(() => void startup());
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (
|
||||
areaName === "sync" &&
|
||||
Object.keys(changes || {}).some((key) => POLLING_SCHEDULE_SETTING_KEYS.has(key))
|
||||
) {
|
||||
void restartPollingSchedule().catch((error) => console.warn("zncm polling reschedule failed", error));
|
||||
if (areaName === "sync") {
|
||||
void startup();
|
||||
}
|
||||
});
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
@@ -954,4 +902,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
});
|
||||
|
||||
void startup().catch((error) => console.warn("zncm startup failed", error));
|
||||
void startup();
|
||||
@@ -1,130 +0,0 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
const vm = require("node:vm");
|
||||
|
||||
const BACKGROUND_PATH = path.join(__dirname, "background.js");
|
||||
|
||||
function createHarness() {
|
||||
let storageChangedListener;
|
||||
let alarmCreateCount = 0;
|
||||
const settings = {
|
||||
apiBaseUrl: "http://localhost:8001",
|
||||
webBaseUrl: "http://localhost:5174",
|
||||
autoDiscoverServiceUrl: false,
|
||||
accessToken: "",
|
||||
tokenSyncEnabled: false,
|
||||
taskPollingEnabled: true,
|
||||
workerId: "test-worker",
|
||||
taskPollIntervalSeconds: 5,
|
||||
};
|
||||
|
||||
const chrome = {
|
||||
alarms: {
|
||||
clear: async () => {},
|
||||
create: async () => {
|
||||
alarmCreateCount += 1;
|
||||
},
|
||||
onAlarm: { addListener: () => {} },
|
||||
},
|
||||
cookies: { get: async () => null },
|
||||
runtime: {
|
||||
onInstalled: { addListener: () => {} },
|
||||
onMessage: { addListener: () => {} },
|
||||
onStartup: { addListener: () => {} },
|
||||
},
|
||||
storage: {
|
||||
local: {
|
||||
get: async (defaults) => ({ ...defaults }),
|
||||
set: async () => {},
|
||||
},
|
||||
session: { get: async () => ({ actSessions: {} }), set: async () => {} },
|
||||
sync: {
|
||||
get: async () => ({ ...settings }),
|
||||
set: async (updates) => Object.assign(settings, updates),
|
||||
},
|
||||
onChanged: {
|
||||
addListener: (listener) => {
|
||||
storageChangedListener = listener;
|
||||
},
|
||||
},
|
||||
},
|
||||
tabs: { query: async () => [] },
|
||||
};
|
||||
|
||||
const context = {
|
||||
chrome,
|
||||
console,
|
||||
fetch: async () => {
|
||||
throw new Error("offline test");
|
||||
},
|
||||
importScripts: () => {},
|
||||
setTimeout: () => 1,
|
||||
clearTimeout: () => {},
|
||||
URL,
|
||||
DEER_CLIPPER_DEFAULTS: settings,
|
||||
deerNormalizeBaseUrl: (value) => String(value || "").replace(/\/+$/, ""),
|
||||
deerMakeWorkerId: () => "test-worker",
|
||||
deerAuthHeaders: () => ({}),
|
||||
deerExtractAuthTokenFromPage: () => ({ token: "" }),
|
||||
};
|
||||
context.globalThis = context;
|
||||
vm.runInNewContext(fs.readFileSync(BACKGROUND_PATH, "utf8"), context, {
|
||||
filename: BACKGROUND_PATH,
|
||||
});
|
||||
|
||||
return {
|
||||
emitStorageChange(changes) {
|
||||
storageChangedListener(changes, "sync");
|
||||
},
|
||||
getAlarmCreateCount: () => alarmCreateCount,
|
||||
resetAlarmCreateCount: () => {
|
||||
alarmCreateCount = 0;
|
||||
},
|
||||
getApiCandidates: (webBase, currentApiBase) =>
|
||||
vm.runInContext(`apiCandidatesForWebBase(${JSON.stringify(webBase)}, ${JSON.stringify(currentApiBase)})`, context),
|
||||
};
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
test("self-written sync metadata does not restart browser task polling", async () => {
|
||||
const harness = createHarness();
|
||||
await settle();
|
||||
harness.resetAlarmCreateCount();
|
||||
|
||||
harness.emitStorageChange({
|
||||
lastTokenSyncAt: { oldValue: "before", newValue: "after" },
|
||||
});
|
||||
await settle();
|
||||
|
||||
assert.equal(harness.getAlarmCreateCount(), 0);
|
||||
});
|
||||
|
||||
test("polling configuration changes restart browser task polling", async () => {
|
||||
const harness = createHarness();
|
||||
await settle();
|
||||
harness.resetAlarmCreateCount();
|
||||
|
||||
harness.emitStorageChange({
|
||||
taskPollingEnabled: { oldValue: false, newValue: true },
|
||||
});
|
||||
await settle();
|
||||
|
||||
assert.equal(harness.getAlarmCreateCount(), 1);
|
||||
});
|
||||
|
||||
test("service discovery prefers the API paired with the open zncm tab", async () => {
|
||||
const harness = createHarness();
|
||||
|
||||
assert.deepEqual(
|
||||
Array.from(harness.getApiCandidates("http://localhost:5174", "http://47.94.209.59:2026")),
|
||||
[
|
||||
"http://localhost:5174",
|
||||
"http://localhost:8001",
|
||||
"http://47.94.209.59:2026",
|
||||
"http://localhost:8001",
|
||||
].filter((value, index, values) => values.indexOf(value) === index),
|
||||
);
|
||||
});
|
||||
@@ -7,8 +7,6 @@
|
||||
const HOST_ID = "zncm-page-assistant-host";
|
||||
const state = {
|
||||
enabled: true,
|
||||
taskPollingEnabled: true,
|
||||
webBaseUrl: "",
|
||||
open: false,
|
||||
busy: false,
|
||||
threadId: "",
|
||||
@@ -711,58 +709,20 @@
|
||||
async function loadSettings() {
|
||||
const settings = await chrome.storage.sync.get(DEER_CLIPPER_DEFAULTS);
|
||||
state.enabled = Boolean(settings.pageAssistantEnabled);
|
||||
state.taskPollingEnabled = Boolean(settings.taskPollingEnabled);
|
||||
state.webBaseUrl = deerNormalizeBaseUrl(settings.webBaseUrl || DEER_CLIPPER_DEFAULTS.webBaseUrl);
|
||||
ensureWidget();
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName !== "sync") {
|
||||
if (areaName !== "sync" || !changes.pageAssistantEnabled) {
|
||||
return;
|
||||
}
|
||||
if (changes.pageAssistantEnabled) {
|
||||
state.enabled = Boolean(changes.pageAssistantEnabled.newValue);
|
||||
ensureWidget();
|
||||
if (state.enabled) {
|
||||
setStatus("页面问答框已开启。");
|
||||
}
|
||||
}
|
||||
if (changes.taskPollingEnabled) {
|
||||
state.taskPollingEnabled = Boolean(changes.taskPollingEnabled.newValue);
|
||||
}
|
||||
if (changes.webBaseUrl) {
|
||||
state.webBaseUrl = deerNormalizeBaseUrl(changes.webBaseUrl.newValue || DEER_CLIPPER_DEFAULTS.webBaseUrl);
|
||||
state.enabled = Boolean(changes.pageAssistantEnabled.newValue);
|
||||
ensureWidget();
|
||||
if (state.enabled) {
|
||||
setStatus("页面问答框已开启。");
|
||||
}
|
||||
});
|
||||
|
||||
function isZncmPage() {
|
||||
const appPath = `${location.pathname}${location.hash}`.toLowerCase();
|
||||
if (
|
||||
appPath.includes("/page/workspace") ||
|
||||
appPath.includes("/chats/") ||
|
||||
appPath.includes("#/login")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const configured = new URL(state.webBaseUrl);
|
||||
return configured.origin === location.origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// MV3 service workers may be suspended between their short polling timers,
|
||||
// while chrome.alarms cannot reliably run more often than every 30 seconds.
|
||||
// A visible zncm tab provides a cheap wake-up signal so browser_act requests
|
||||
// are normally claimed within the backend/model timeout window.
|
||||
setInterval(() => {
|
||||
if (!state.taskPollingEnabled || !isZncmPage()) {
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage({ type: "zncm-poll-now" }).catch(() => {});
|
||||
}, 3000);
|
||||
|
||||
setInterval(() => {
|
||||
if (!state.enabled || !ui.pageTitle) {
|
||||
return;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Wiki 数据流转与本地编码验收(2026-09-03)
|
||||
|
||||
## 结论
|
||||
|
||||
本地 BGE-M3 小批量编码已安装、配置并实际运行。已有 Wiki 的向量化、自动沉淀、
|
||||
普通库文件导出、助手库异步导入、助手库文件导出、空普通库回流及两端向量检索已实测通过。
|
||||
**技能自动生成 Wiki 尚未完成验收**:WeKnora 的两个生成模型都返回 HTTP 402
|
||||
`Insufficient Balance`。不能把后半段通过等同于全链路通过。
|
||||
|
||||
本次修改运行在本地 DeerFlow 开发服务(前端 5175、后端 8001),连接用户提供的远端
|
||||
WeKnora;没有把业务代码发布到服务器,没有提交 Git,也没有直接写 WeKnora 图数据库。
|
||||
|
||||
## 本地编码配置
|
||||
|
||||
- 实现:FastEmbed / ONNX Runtime CPU,官方 `BAAI/bge-m3` 权重。
|
||||
- 配置名:`bge-embedding-m3`,1024 维,每批 2 条,2 个 CPU 推理线程。
|
||||
- 运行依赖:FastEmbed 随标准后端依赖安装;模型必须提前放在离线目录中,仍按需加载,不影响主服务启动。
|
||||
- 权重:约 2.3 GB,持久缓存 `C:\Users\ji\.cache\deerflow\embeddings`,不进入业务仓库。
|
||||
- 配置:`llmwiki.local_wiki_index.embedding.provider: local`;可用
|
||||
`local_model_path` 指定离线权重目录,详见后端 README。
|
||||
- 查询向量在本地实时编码。已有 Wiki 在内容哈希、模型指纹匹配且向量非空时直接复用。
|
||||
不同模型或维度不能混用;导入不静默重新编码。
|
||||
- 原始文档分段向量与模型生成后的 Wiki 文本不同,不能相互冒用。本次新建的是 Wiki
|
||||
正文的 BGE-M3 索引,不是把既有 512 维文档向量改名为 1024 维。
|
||||
|
||||
## 实际数据验收
|
||||
|
||||
来源是已有普通知识库“图片测试111”,没有改写其 Wiki 内容。
|
||||
|
||||
| 阶段 | Wiki | Wiki 向量 | 实体 | 引用关系 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 普通知识库导出 | 19 | 101 | 19 | 108 |
|
||||
| 助手知识库导入后导出 | 19 | 101 | 19 | 108 |
|
||||
| 新建普通知识库回流后导出 | 19 | 101 | 19 | 108 |
|
||||
| 知识梳理总库导出(7 个来源记录) | 19 | 101 | 19 | 108 |
|
||||
|
||||
逐项断言通过:
|
||||
|
||||
1. 三份包的 Wiki slug、标题、正文、摘要、页面类型、别名、分类路径、出链一致。
|
||||
2. 101 条向量的原始 float32 字节、分段编号、维度和模型指纹完全一致。
|
||||
3. 将导入前后实体 ID 映射回原 Wiki slug 后,108 条关系的端点及谓词集合完全一致。
|
||||
4. 总库导出没有因多个来源重复同步而重复导出同一当前页面的分段向量。
|
||||
5. 元数据回归测试另覆盖 tags、目录恢复、原始文档片段不计入 Wiki 数量、原生技能无
|
||||
ownership 记录时可被选中、短 Wiki 不漏向量、模型指纹不兼容时不假装向量检索。
|
||||
|
||||
普通包约 1.01 MB,助手包约 0.79 MB,回流包约 0.83 MB。普通包还带 2 个原始文档及
|
||||
108 个原始片段;助手导出提供 Wiki、Wiki 向量和关系,不重新上传原始文档触发二次生成。
|
||||
|
||||
### 检索
|
||||
|
||||
实际查询:“军方用什么工具评估极端天气带来的风险?”
|
||||
|
||||
- 回流普通库 `/api/llmwiki/search` 返回 `mode=vector`,首条“国防气候评估工具”。
|
||||
- 助手库 `/api/assistant-knowledge/search` 返回 `retrieval_mode=vector`,首条同上;
|
||||
首个命中分段余弦相似度约 0.6343。
|
||||
- 浏览器实际提交总库检索,显示“向量检索 · 余弦相似度”和相同命中;不是只检查返回字段。
|
||||
- 普通库采用已有页面级聚合评分,因此页面最终分数与单分段余弦值不必相等。
|
||||
- 回流的向量写入 **DeerFlow 管理的本地 Wiki 索引**。未写入 WeKnora 原生向量数据库,
|
||||
本次结论适用于本系统问答检索,不能声称原生 WeKnora 自身的检索也已完成该向量导入。
|
||||
|
||||
### 页面
|
||||
|
||||
普通与助手库复用同一 Wiki 阅读组件:目录树、类型/标签筛选、分类面包屑、别名标签、
|
||||
正文内链、出链、反向链接及邻接关系视图。窄窗口可收起目录。
|
||||
|
||||
浏览器验证了“索引 → 国防气候评估工具”、展开关系视图、助手 Wiki 跳回普通库来源,
|
||||
以及普通 Wiki 的“在知识梳理中查看”反向跳转。
|
||||
|
||||
### 验收数据位置
|
||||
|
||||
- 原有普通库:`48f669dc-433e-4aab-b5ea-897b6f7a3d2f`
|
||||
- 知识梳理总库:`c3fd9841-0718-44dc-860e-aae3020fd485`
|
||||
- 最终助手测试库:`489be905-08c8-40fe-8d60-42ac21dd945a`
|
||||
- 最终普通回流测试库:`428162aa-03f6-4c61-8994-360f2f39e0c9`
|
||||
|
||||
新建测试库名称均以“Wiki链路验收-0903-1826”开头,保留用于复核;未删除用户已有库。
|
||||
本地 `.runtime/wiki-live-result.json` 保存验收结果,`.runtime/wiki_live_acceptance.py`
|
||||
保存本次验收流程,均为忽略的运行产物,不包含认证口令。
|
||||
|
||||
## 这次实际修复的问题
|
||||
|
||||
- 内置技能只有磁盘目录、没有归属记录时,被错误判定为不存在。
|
||||
- 技能应上传去代码后的知识正文,让 WeKnora 生成 Wiki,而不是伪造一个 `skill` 类型页面。
|
||||
- Wiki 生成等待使用实际 stats 接口;零页结果明确失败,不无期限等待。
|
||||
- 向量化完成后自动导出沉淀到总库,并阻断导出回调重复触发全量同步。
|
||||
- 短摘要和短正文同时存在时,旧分段阈值会将整页过滤为零向量。
|
||||
- 分类路径必须先创建 WeKnora folder 并绑定 folder_id;只传 category_path 会被服务端清空。
|
||||
- Wiki 的标签/分类/别名/原始引用保留;图节点绑定真实 Wiki,避免生成重复的空实体页面。
|
||||
- 保留归一化 float32 原始字节;导入中不能用 `section_index or index`,因为合法编号 0 会被改写。
|
||||
- 总库多个来源保留审计,但检索与导出只使用当前 Wiki 版本对应的向量,避免旧贡献污染检索。
|
||||
- 包解析改为同盘临时 SQLite,上传先落盘再异步导入,导出完成后才提供文件下载。
|
||||
|
||||
## 阻塞与未通过的范围
|
||||
|
||||
1. 技能 `knowledge-base-ingest` 已送入专门创建的普通测试库,但远端
|
||||
`deepseek-v4-flash` 和 `qwen3.6-flash` 均因余额不足失败。WeKnora 即使失败也可能把
|
||||
文档 parse_status 标为 completed。需恢复可用生成模型后重新运行技能全链路验收。
|
||||
2. 已采用磁盘暂存和有界批处理,但本次只有约 1 MB 实测,**未做数 GB 压测、断点续传、
|
||||
进程中断恢复验收**。大包部署仍需磁盘容量、代理 body size 和超时配置。
|
||||
3. 当前实体去重主要按类型及规范化名称;这次没有完成跨别名、歧义实体的模型对齐及
|
||||
多来源事实冲突合并验收,不能视为这一历史需求已全面完成。
|
||||
4. 重复上传仍会保留独立来源记录。向量导出已消除旧版本重复,但上传身份幂等、部分远端
|
||||
写入失败后的无损重试、跨实例图片资源打包仍需独立验证。
|
||||
5. 当前版本已补充自动增量链路:服务启动立即补扫,之后默认每 30 秒扫描全部普通
|
||||
知识库;Wiki 抽取完成后无需手工点击向量化,并自动以来源快照覆盖同步到知识梳理总库。
|
||||
6. 回流当前只允许新建空 Wiki 普通库,避免覆盖已有页面。普通库现有页面列表的超大规模
|
||||
分页与助手问答的最终生成回答不属于这轮通过结论。
|
||||
|
||||
## 自动化检查
|
||||
|
||||
- 后端相关测试:118 passed(assistant repository、Wiki sync、package roundtrip、
|
||||
WeKnora 接口、向量检索与知识库管理)。
|
||||
- 修改的 Python 功能文件 Ruff 检查通过;app.py 既有启动顺序导致的 E402 不在此次改动中处理。
|
||||
- `git diff --check` 通过(只有仓库换行提示)。
|
||||
- 前端完整 `pnpm typecheck` 未通过:57 个既有/本轮范围外错误,涉及 nextra 缺失、Markdown
|
||||
插件类型及其他模块;本轮新建/修改的 Wiki 组件、助手 API、技能面板没有出现在错误清单中。
|
||||
浏览器实际页面与跳转已验证,但不能声称全项目构建通过。
|
||||
@@ -1,188 +0,0 @@
|
||||
# 报告协作工作台 — 交互重设计方案(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 单测,不引入对其他模块的影响。
|
||||
@@ -1,719 +0,0 @@
|
||||
# AgentScope 2.0 多智能体报告协作工作台——前端实施计划
|
||||
|
||||
> 文档状态:前端代码已落地、契约冻结;联调等待后端运行中干预与报告改写
|
||||
> 编写日期:2026-09-02;进度更新日期:2026-09-06
|
||||
> 目标项目:`F:\react01\deeflow-code\deerflow-server\frontend-web`
|
||||
> 视觉与交互参考:`F:\react01\coze-frontend\frontend\apps\coze-studio`
|
||||
> 参考页面:`http://localhost:3000/work_flow?space_id=1&workflow_id=wf_88cb9f4c6642`
|
||||
> 配套后端计划:`docs/agentscope-多智能体报告协作工作台-后端实施计划.md`
|
||||
> 本文只规划新增页面,不修改现有多智能体会商页面,不复用旧 Workflow Studio 执行状态。
|
||||
|
||||
## 0. 当前进度
|
||||
|
||||
`frontend-web/src/report-collaboration/` 已实现两栏工作台、会话/方案/run API 客户端、mock driver、SSE reducer、`MessageList` 适配、方案卡、画布、节点抽屉、Composer、报告预览与历史。`RC-FE-001`~`013` 的页面骨架与契约层视为完成。
|
||||
|
||||
后端已通规划前路径(澄清 → 出方案 → 选定 → `queued` run),见后端计划 §0。前端真实执行体验仍受阻:
|
||||
|
||||
- `GET /runs/{id}/stream` 已挂载(`RC-BE-011`);dispatcher/租约已落地(`RC-BE-012`),默认 `worker_enabled=false` 仍不自动领取 queued run。
|
||||
- 报告改写 / 恢复接口已由后端 `RC-BE-015` 挂载。
|
||||
- 质量角色已由 `QualityRoleKernel` 跑通(`RC-BE-013`);真实 AgentScope 模型/`web_search` 仍待后续票。
|
||||
|
||||
联调顺序:先打开 `report_collaboration.enabled` 验证规划前 REST 与 `GET /runs/{id}/stream` 重放;画布自动执行需同时打开 `worker_enabled`(默认关)。
|
||||
|
||||
## 1. 前端目标
|
||||
|
||||
新增独立的「报告协作」工作台。用户可以通过对话提出任意报告需求,查看系统生成的多个协作方案,点击方案卡片在流程图中预览协作结构,确认后观察多智能体执行,并在全过程通过统一对话框提出修改意见。
|
||||
|
||||
前端必须满足:
|
||||
|
||||
1. 页面采用“左侧对话、右侧流程画布”的固定两栏布局,画布、工具栏、弹窗和抽屉参考 Coze Studio 页面风格。
|
||||
2. 消息列表必须复用 DeerFlow 现有 `MessageList`,不复制 Coze 的会话消息实现。
|
||||
3. 所有执行过程、AgentScope 团队消息、智能体回复、工具调用及其原始返回、错误、协助卡、返工和最终报告入口都通过现有消息协议按时间顺序显示在左侧对话区,不设置独立执行结果面板,也不另做“阶段摘要”替代实时消息。
|
||||
4. 候选方案、流程节点、对话进度和报告使用同一份后端事实,不各自推断状态。
|
||||
5. 用户可在规划中、运行中、等待输入和报告完成后继续对话。
|
||||
6. 用户协助卡的出现和提交都不能在前端被解释为“节点已经完成”。
|
||||
7. 页面刷新、SSE 断线重连、历史会话切换后能恢复到一致状态。
|
||||
|
||||
暂定:
|
||||
|
||||
- 页面名称:`报告协作`。
|
||||
- 路由:`/page/workspace/report-collaboration`。
|
||||
- 会话路由:`/page/workspace/report-collaboration/:sessionId`。
|
||||
- 默认候选方案数量:3。
|
||||
- 点击画布节点时打开覆盖式节点说明抽屉;抽屉只展示节点职责和合同,不展示节点执行结果。
|
||||
|
||||
---
|
||||
|
||||
## 2. 范围与非目标
|
||||
|
||||
### 2.1 本期范围
|
||||
|
||||
- 新建、查看、重命名、删除协作报告会话。
|
||||
- 使用对话澄清报告需求。
|
||||
- 显示 2~3 个候选方案卡片。
|
||||
- 卡片与右侧流程图预览联动。
|
||||
- 节点说明抽屉:职责、所依据的信息类型、预期产出、工具、资料要求和验收条件。
|
||||
- 实时展示节点排队、运行、校验、等待用户、完成、返工、失败和取消。
|
||||
- 在左侧对话时间线中直接展示每个智能体的流式文本、团队对话、工具调用和工具返回,并展示审稿意见、报告文件和报告版本。
|
||||
- 并行阶段用横向的智能体选择器切换当前查看对象;选择器只控制同一份消息流的聚焦范围,不创建另一套执行结果视图。
|
||||
- 运行期间发送补充要求、重新检索、重新分析、节点重跑等指令。
|
||||
- 完成后进行报告问答、章节重写、全文润色和版本恢复。
|
||||
- 报告模板选择、Markdown 预览、Markdown/Word 下载。
|
||||
- 响应式布局、键盘操作、加载反馈和错误恢复。
|
||||
|
||||
### 2.2 明确不做
|
||||
|
||||
- 不引入 `@coze-workflow/playground` 或 `@coze-arch/coze-design`。
|
||||
- 不加载 Coze workflow definition、run 或 node run。
|
||||
- 不允许前端拖拽连线后直接改变执行逻辑。
|
||||
- 不新增第二套消息气泡、Markdown、引用和文件卡渲染器。
|
||||
- 不展示模型原始 chain-of-thought、系统/内部 prompt、密钥和被标记为内部控制信息的字段。
|
||||
- 不把用户有权查看的 AgentScope TeamSay、智能体文本、工具调用或工具返回压缩成“正在检索”等摘要卡;这些原始业务消息必须进入复用的 `MessageList`。敏感字段只能按统一脱敏策略替换,不能以摘要替代。
|
||||
- 不增加第三栏、常驻详情栏、节点执行结果栏或独立执行时间线。
|
||||
- 节点抽屉不展示节点生成正文、工具返回、证据列表、错误堆栈或 attempt 历史;这些过程统一在左侧 `MessageList` 的原生消息/工具步骤中查看。
|
||||
- 不改造现有圆桌、多智能体会商、深度研究和 AI 写作页面。
|
||||
- 不把浏览器状态作为业务事实源。
|
||||
|
||||
---
|
||||
|
||||
## 3. 参考页面行为映射
|
||||
|
||||
| 目标行为 | Coze 参考文件 | DeerFlow 目标组件 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 两栏骨架 | `workflow-studio-layout.tsx` 的面板比例与分隔条 | `ReportCollaborationLayout.tsx` | 左对话、右画布;只保留一条可调整分隔线 |
|
||||
| 顶栏 | `workflow-studio-header.tsx` | `ReportCollaborationHeader.tsx` | 替换为会话、模板、报告、停止等操作 |
|
||||
| 候选卡片 | `workflow-proposal-cards.tsx` | `ReportPlanCards.tsx` | 点击预览,明确确认后才执行 |
|
||||
| 右侧画布 | `workflow-canvas.tsx` | `ReportFlowCanvas.tsx` | 使用 `@xyflow/react` 重建,不引入 Coze Playground |
|
||||
| 左侧消息区 | `run-conversation-panel.tsx` | `ReportConversationPanel.tsx` | 外层风格参考 Coze,内部复用 DeerFlow `MessageList` |
|
||||
| 节点说明抽屉 | Coze 节点配置面板的视觉与抽屉行为 | `NodeInspectorDrawer.tsx` | 点击画布节点打开,只展示职责与输入输出合同 |
|
||||
| 全程执行反馈 | 参考其进度和状态表达 | `AgentStageSelector` + DeerFlow `MessageList` | 选择智能体后直接显示其流式消息、TeamSay、工具调用与返回;不再合成为阶段摘要 |
|
||||
| 状态栏 | `connection-status.tsx` | `ConnectionStatusBar.tsx` | 展示连接、重连和事件游标 |
|
||||
| 对话历史 | `left-panel-tabs.tsx` | `ConversationSidebarTabs.tsx` | 对话与历史,不增加“节点资源”Tab |
|
||||
| 异步反馈 | 规划进度、运行状态 | MessageList 步骤条 + 画布状态 | 操作超过 300ms 必须有反馈 |
|
||||
|
||||
主要参考源:
|
||||
|
||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-layout.tsx`
|
||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-composition.tsx`
|
||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\workflow-studio-header.tsx`
|
||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\components\run-conversation\workflow-proposal-cards.tsx`
|
||||
- `F:\react01\coze-frontend\frontend\apps\coze-studio\src\workflow-standalone\styles.module.less`
|
||||
|
||||
若直接复制 Apache-2.0 源码片段或样式,必须保留版权头并维护来源说明;默认做行为映射和视觉重建。
|
||||
|
||||
---
|
||||
|
||||
## 4. 页面结构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ 返回 报告协作 / 会话标题 保存状态 模板 报告 停止/继续 更多 │
|
||||
├────────────────────────────────┬─────────────────────────────────────────┤
|
||||
│ 对话 / 历史 │ 协作流程图 │
|
||||
│ │ │
|
||||
│ DeerFlow MessageList │ @xyflow/react │
|
||||
│ · 并行时:[协调者][市场][政策] │ · 节点状态与活动连线 │
|
||||
│ · 用户与协调者/成员消息 │ · 缩放、适配视图、小地图 │
|
||||
│ · 原生 ToolCall / ToolMessage │ · 点击节点打开覆盖式说明抽屉 │
|
||||
│ · 协助卡、报告文件和版本 │ │
|
||||
│ │ │
|
||||
│ 统一输入框 │ │
|
||||
├────────────────────────────────┴─────────────────────────────────────────┤
|
||||
│ 已连接 · run_xxx · 事件 128 │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
节点抽屉覆盖在右侧画布之上,不占用第三栏:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 节点名称 关闭 × │
|
||||
│ 角色 / 当前状态 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 节点职责 │
|
||||
│ 要根据什么:所需输入、上游类型、资料要求 │
|
||||
│ 要产出什么:成果类型、格式、验收条件 │
|
||||
│ 可用工具与边界 │
|
||||
│ 上下游关系 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ 在对话中对此节点提出意见 │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
抽屉明确不展示实际执行结果、生成正文、工具返回、来源明细、错误堆栈或 attempt 时间线;这些信息只在左侧原生消息流中展示。
|
||||
|
||||
布局默认值:
|
||||
|
||||
- 顶栏高度:56px。
|
||||
- 左侧面板:默认 480px,最小 360px,最大 720px。
|
||||
- 右侧画布:最小 480px,使用剩余空间。
|
||||
- 节点说明抽屉:覆盖画布右侧,建议宽度 420px;关闭后完整归还画布空间。
|
||||
- 状态栏:24~28px。
|
||||
- 低于 960px:左侧对话和右侧画布改为顶部按钮切换的单面板模式;节点说明仍使用覆盖式 Drawer。
|
||||
- 两栏分隔条:支持 pointer 拖动、方向键、Home、End。
|
||||
|
||||
### 4.1 原生消息流与并行智能体切换
|
||||
|
||||
“复用 `MessageList`”在本页面的含义是复用它现有的 assistant 文本、`tool_calls`、`tool` 消息、引用、文件和协助卡渲染语义;不把 AgentScope 的运行过程先解析成专用步骤条、进度摘要或阶段结果卡。
|
||||
|
||||
单个智能体:
|
||||
|
||||
- `agent.reply.started` 立即创建一条稳定的 assistant 消息;`message.delta` 持续更新同一 `messageId`,所以用户会先看到流式状态、再看到逐字回复,而不是等待节点结束。
|
||||
- 工具调用创建/更新该 assistant 消息的 `tool_calls`;工具返回创建匹配 `tool_call_id` 的原生 `tool` 消息。现有 `MessageGroup` 负责把两者配对显示,不再另写工具过程渲染器。
|
||||
- AgentScope TeamSay 作为带 `agentRunId` 的普通 assistant 消息进入同一流,按到达顺序显示。它不是节点完成的证据,节点状态仍由右侧画布和后端状态机决定。
|
||||
|
||||
并行智能体:
|
||||
|
||||
```text
|
||||
多角度并行研究 3/4 进行中
|
||||
[ 协调者 ] [ 市场规模 ✓ ] [ 品牌竞争 ● ] [ 海外市场 ● ] [ 政策风险 ! ]
|
||||
└──────────────────── 当前选中智能体的原生 MessageList ────────────────────┘
|
||||
```
|
||||
|
||||
- `AgentStageSelector` 只在并行阶段出现,横向放置在左侧对话区顶部;使用现有图标体系的头像/角色图标、名称、状态文字和非纯颜色状态标识。默认选中第一位已开始运行的成员;用户手动切换后保持选择,不因其他成员返回而抢走阅读焦点。
|
||||
- 所有成员的事件都持续接收、按每个 `agentRunId` 持久化和更新,即使当前未选中;切回即可看到完整历史与正在更新的同一条消息,不丢 token、不重新请求、不把结果拼成摘要。
|
||||
- 当前选择只将共享用户消息、协调者广播消息和目标成员消息组成一个 `BaseStream<AgentThreadState>` 交给同一个 `MessageList`。切换标签仅切换该视图的消息范围,不改变 run、节点或消息的后端事实。
|
||||
- 某成员请求用户协助时,其标签显示“待回复”徽标,选择器显示待处理数;点击徽标切到该成员,原生协助工具消息仍由现有 `MessageList` 协助卡渲染。其他成员可继续运行。
|
||||
- 节点抽屉打开某并行成员时,同步聚焦对应标签;抽屉仍只展示合同。用户从抽屉或输入框发送反馈时带 `targetNodeId`/`agentRunId`,后端在安全点生效。
|
||||
|
||||
这不是把完整日志另做成调试面板:对话区展示的是模型和工具的真实业务消息;只有 chain-of-thought、内部提示词及按权限必须脱敏的字段不进入该消息流。
|
||||
|
||||
### 4.2 页面主要状态
|
||||
|
||||
| 状态 | 左侧对话区 | 右侧流程画布 | 主要操作 |
|
||||
|---|---|---|---|
|
||||
| `empty` | 欢迎语和输入框 | 空态 | 输入需求、选模板 |
|
||||
| `clarifying` | 澄清问题和协助卡 | 需求理解节点 | 回答或补充 |
|
||||
| `planning` | 用户消息和规划进度 | 骨架或需求图 | 继续补充,等待安全点生效 |
|
||||
| `proposal_ready` | 2~3 张方案卡 | 点击卡片预览对应图 | 切换、修改、采用 |
|
||||
| `running` | 协调者与成员原生消息、工具过程和用户反馈 | 实时节点状态 | 干预、停止、在对话中要求重跑 |
|
||||
| `awaiting_input` | 等待用户卡片、原因和影响 | 对应节点显示等待 | 回答、取消 |
|
||||
| `reviewing` | 审稿意见和返工过程 | 评审/返工节点状态 | 接受或补充要求 |
|
||||
| `completed` | 总结、报告文件卡、版本和后续对话 | 完整执行图 | 问答、改写、导出 |
|
||||
| `failed` | 错误说明和恢复建议 | 失败节点 | 通过对话重试或取消 |
|
||||
| `cancelled` | 已保留成果说明 | 未完成节点取消 | 从已有成果继续或新建方案 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端目录与组件边界
|
||||
|
||||
建议新增:
|
||||
|
||||
```text
|
||||
frontend-web/src/report-collaboration/
|
||||
api/
|
||||
client.ts
|
||||
sessions.ts
|
||||
plans.ts
|
||||
runs.ts
|
||||
reports.ts
|
||||
types.ts
|
||||
components/
|
||||
ReportCollaborationLayout.tsx
|
||||
ReportCollaborationHeader.tsx
|
||||
ConversationSidebarTabs.tsx
|
||||
ReportConversationPanel.tsx
|
||||
AgentStageSelector.tsx
|
||||
ReportComposer.tsx
|
||||
ReportPlanCards.tsx
|
||||
ReportFlowCanvas.tsx
|
||||
ReportFlowNode.tsx
|
||||
NodeInspectorDrawer.tsx
|
||||
ReportPreviewDialog.tsx
|
||||
InterventionCard.tsx
|
||||
SessionHistoryPanel.tsx
|
||||
ConnectionStatusBar.tsx
|
||||
hooks/
|
||||
useReportCollaborationSession.ts
|
||||
useReportCollaborationStream.ts
|
||||
useReportCollaborationMessages.ts
|
||||
useReportPlanSelection.ts
|
||||
useReportPanelLayout.ts
|
||||
state/
|
||||
event-normalizer.ts
|
||||
event-reducer.ts
|
||||
selectors.ts
|
||||
message-adapter.ts
|
||||
pages/
|
||||
ReportCollaborationPage.tsx
|
||||
styles/
|
||||
report-collaboration.css
|
||||
tests/
|
||||
```
|
||||
|
||||
修改范围:
|
||||
|
||||
- `frontend-web/src/pages/WorkspaceRoutes.tsx`:增加页面和会话路由。
|
||||
- `frontend-web/src/components/workspace/workspace-nav-chat-list.tsx`:增加导航入口。
|
||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`:仅允许增加一个向后兼容的通用业务卡片插槽。
|
||||
- `frontend-web/src/core/messages/utils.ts`:如确需识别通用业务卡类型,只做集中式扩展。
|
||||
|
||||
不得把新功能放进:
|
||||
|
||||
- `frontend-web/src/roundtable-planning/`
|
||||
- `frontend-web/src/open-canvas/`
|
||||
- Coze 项目的 `workflow-standalone/`
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据来源与前端状态
|
||||
|
||||
### 6.1 三层状态
|
||||
|
||||
前端状态分为:
|
||||
|
||||
1. 服务器快照:session、原生 messages、plans、run、nodes、artifacts、report versions;每条消息带稳定的 `agentRunId`、`nodeRunId`、`phaseId` 和可见性标记,其中 artifacts 只用于生成左侧消息卡和报告文件,不形成独立结果面板。
|
||||
2. durable SSE reducer:从 `seq` 连续应用事件,更新服务器快照投影。
|
||||
3. 纯展示状态:预览中的方案、选中的节点、聚焦的并行 `agentRunId`、两栏宽度、节点抽屉开关、窄屏当前视图和画布位置。
|
||||
|
||||
纯展示状态不能改变执行结果。点击卡片只改变 `previewPlanId`;只有点击“采用方案”才改变服务器 `selectedPlanId`。
|
||||
|
||||
### 6.2 前端事件包
|
||||
|
||||
```ts
|
||||
interface CollaborationEvent<T = unknown> {
|
||||
eventId: string;
|
||||
sessionId: string;
|
||||
runId?: string;
|
||||
seq: number;
|
||||
type: string;
|
||||
timestamp: string;
|
||||
data: T;
|
||||
}
|
||||
```
|
||||
|
||||
必须处理:
|
||||
|
||||
- 快照先到、事件后到。
|
||||
- 事件重复。
|
||||
- 事件短暂乱序。
|
||||
- SSE 断线后从 `after_seq` 重放。
|
||||
- 页面切换时旧连接晚到事件。
|
||||
- token delta 多次更新同一消息。
|
||||
- TeamSay、assistant 文本、tool-call 参数 delta、tool 返回 delta 分别更新各自稳定消息,不得等节点完成后批量生成摘要。
|
||||
- 并行成员的消息可交错到达;Reducer 必须按 `agentRunId + messageId` 更新,而不是按当前被选中的标签丢弃未聚焦成员的事件。
|
||||
- 节点重新打开后出现新 attempt。
|
||||
|
||||
Reducer 以 `sessionId + runId + seq` 去重;消息行以 `messageId`、工具结果以 `toolCallId`、标签以 `agentRunId` 使用后端稳定 ID,禁止使用数组下标。
|
||||
|
||||
---
|
||||
|
||||
## 7. 分步实施计划
|
||||
|
||||
### FE-0:冻结参考视觉与行为基线
|
||||
|
||||
任务:
|
||||
|
||||
1. 在 1440×900、1280×800、1024×768 打开参考页面。
|
||||
2. 保存初始态、方案态、运行态和节点说明抽屉四组截图。
|
||||
3. 记录顶栏、两栏比例、间距、圆角、边框、阴影、画布工具栏和状态色。
|
||||
4. 形成“参考组件—目标组件—允许差异”表。
|
||||
5. 明确消息列表保持 DeerFlow 原样,不纳入像素对齐。
|
||||
|
||||
交付物:
|
||||
|
||||
- 视觉基线截图。
|
||||
- 视觉 token 表。
|
||||
- 页面行为清单。
|
||||
|
||||
验收:参考的每个用户可见状态都有截图或明确说明不存在。
|
||||
|
||||
### FE-1:路由、菜单和静态页面骨架
|
||||
|
||||
任务:
|
||||
|
||||
1. 增加两个路由和菜单项。
|
||||
2. 创建 `ReportCollaborationPage`。
|
||||
3. 创建顶栏、左侧对话栏、右侧画布和底部连接栏。
|
||||
4. 实现可调整宽度的两栏布局,以及“专注对话/专注画布”切换。
|
||||
5. 实现窄屏单面板切换与覆盖式节点 Drawer。
|
||||
|
||||
验收:
|
||||
|
||||
- 不连接后端也能呈现完整空态。
|
||||
- 375~1440px 不出现页面级横向滚动。
|
||||
- 对话/画布切换不会卸载会话状态或清空输入内容。
|
||||
- 键盘可以调整两栏分隔条。
|
||||
|
||||
### FE-2:页面设计系统与 Coze 风格重建
|
||||
|
||||
建立页面专用语义变量:
|
||||
|
||||
```text
|
||||
--rc-bg-page
|
||||
--rc-bg-panel
|
||||
--rc-bg-canvas
|
||||
--rc-border
|
||||
--rc-text-primary
|
||||
--rc-text-secondary
|
||||
--rc-accent
|
||||
--rc-running
|
||||
--rc-waiting
|
||||
--rc-success
|
||||
--rc-danger
|
||||
--rc-panel-shadow
|
||||
--rc-card-radius
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 从 DeerFlow 主题 token 派生,不在组件中散落十六进制颜色。
|
||||
- 使用 Lucide SVG 图标,不使用 emoji。
|
||||
- hover/focus/展开动画控制在 150~300ms。
|
||||
- 支持 `prefers-reduced-motion`。
|
||||
- 交互状态不能只靠颜色表达。
|
||||
- 正文对比度达到 4.5:1。
|
||||
- 操作超过 300ms 显示 spinner、骨架或流式状态。
|
||||
|
||||
验收:除 MessageList 内部外,两栏布局、密度、卡片、画布工具栏、弹窗和节点抽屉达到参考基线。
|
||||
|
||||
### FE-3:API 类型和 TanStack Query 层
|
||||
|
||||
实现:
|
||||
|
||||
- session CRUD。
|
||||
- messages 查询与发送。
|
||||
- plan request、plans 查询、select。
|
||||
- run 创建、查询、停止、重试。
|
||||
- command 创建、确认、取消。
|
||||
- artifacts、sources、reports、versions 查询。
|
||||
|
||||
每个写请求携带:
|
||||
|
||||
- `idempotencyKey`
|
||||
- `expectedRevision`
|
||||
|
||||
冲突行为:
|
||||
|
||||
- `409`:刷新当前快照并提示用户重新确认。
|
||||
- `422`:在对应操作附近显示具体错误。
|
||||
- `429`:显示预算/并发限制,不自动重试。
|
||||
- `5xx`:保留用户输入,允许使用同一幂等键重试。
|
||||
|
||||
验收:API 层不包含页面展示逻辑,类型与后端 OpenAPI 一致。
|
||||
|
||||
### FE-4:SSE、快照和事件 Reducer
|
||||
|
||||
实现 `useReportCollaborationStream`:
|
||||
|
||||
1. 先拉 session snapshot。
|
||||
2. 使用 snapshot 的 `lastSeq` 建立 SSE。
|
||||
3. 校验每个事件 session/run 身份。
|
||||
4. 按 seq 应用事件并去重。
|
||||
5. 出现序号缺口时暂停 live apply,补拉缺失事件。
|
||||
6. 断线指数退避并支持 `Last-Event-ID`。
|
||||
7. 终态后完成最后一次事件补拉再关闭连接。
|
||||
|
||||
Reducer 必须覆盖:
|
||||
|
||||
- 消息流增量。
|
||||
- 计划候选和 revision。
|
||||
- node/node attempt 状态。
|
||||
- artifact 创建和校验。
|
||||
- command 分类和执行。
|
||||
- report delta 和正式版本。
|
||||
- 错误、取消、接管恢复。
|
||||
- AgentScope 原始可见消息和工具事件:`message.created/delta/completed`、`tool_call.created/delta/completed`、`tool_result.created/delta/completed`、`team_message.created/delta/completed`。
|
||||
|
||||
验收:重复事件、乱序事件和重连都不产生重复消息、重复节点或状态倒退。
|
||||
|
||||
### FE-5:复用 DeerFlow `MessageList`
|
||||
|
||||
复用:
|
||||
|
||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`
|
||||
- `frontend-web/src/components/workspace/messages/message-list-item.tsx`
|
||||
- `frontend-web/src/components/workspace/messages/message-group.tsx`
|
||||
- `frontend-web/src/components/workspace/messages/context.ts`
|
||||
|
||||
实现无损协议适配 `message-adapter.ts`:
|
||||
|
||||
1. 将已持久化的 `CollaborationMessage` 一对一映射为 LangGraph SDK `Message[]`,不将文本或工具结果二次概括。
|
||||
2. 构造最小 `BaseStream<AgentThreadState>` 外观:`messages`、`isLoading`、`isThreadLoading`、`values`。
|
||||
3. 协调者、成员 AgentScope TeamSay 和成员回复都映射为普通 assistant 消息;保留 `messageId`、文本 chunk、角色/成员名称、`agentRunId`、`nodeRunId`、`phaseId`、来源事件 ID 和完成态。
|
||||
4. AgentScope 工具调用映射为该 assistant 消息的标准 `tool_calls`;调用结果映射为标准 `tool` 消息,并严格复用原有 `tool_call_id` 配对。工具参数、返回正文及增量保持原文,只有后端明确标注的脱敏字段被替换。
|
||||
5. 将来源、文件、报告 Markdown delta 和正式版本继续映射为既有引用、文件卡和普通 assistant 消息;规划卡、用户确认卡和报告替换卡仍通过一个通用业务卡片插槽渲染。
|
||||
6. 错误、恢复建议、命令影响范围和报告版本变化也必须进入同一时间线;错误堆栈以可读错误消息替代,但不能吞掉已发生的工具/成员消息。
|
||||
7. `selectMessagesForAgentRun(allMessages, focusedAgentRunId)` 只在客户端从已同步消息中选出共享消息与目标成员消息;并行选择器不得重新拉取、重新排序或改写消息。
|
||||
|
||||
约束:
|
||||
|
||||
- 不使用 LangGraph `useThreadStream` 发送消息。
|
||||
- 新页面输入框调用报告协作 API。
|
||||
- 不增加多个页面专用 MessageList props。
|
||||
- 普通聊天、AI 写作、深度研究等未传插槽时必须零变化。
|
||||
- 不另建执行结果列表、节点结果面板或成果时间线。
|
||||
- 不用“运行中”“已检索 N 条”等专用摘要替代真实消息。`MessageList` 本身的流式状态和原生工具步骤是唯一过程视图。
|
||||
|
||||
验收:
|
||||
|
||||
- 历史和流式消息使用同一行稳定更新。
|
||||
- 单成员首次开始回复、每次工具启动和工具返回均在 300ms 内出现可见的原生消息行或原生工具步骤;不允许持续数秒只有静态加载圈。
|
||||
- 并行标签切换后显示目标成员的完整已持久化消息和实时 delta;未选中成员持续接收,标签上的运行/待回复状态及时更新。
|
||||
- 用户上翻阅读时不强制滚底。
|
||||
- 用户发送新消息后滚到最新位置。
|
||||
- 卡片显示成功不改变后端节点状态。
|
||||
|
||||
### FE-6:需求澄清和候选方案卡片
|
||||
|
||||
`ReportPlanCards` 展示:
|
||||
|
||||
- 推荐标签、策略、标题、摘要和推荐理由。
|
||||
- 分析角度和参与角色。
|
||||
- 关键步骤与质量门。
|
||||
- 预计时间、成本等级和资料范围。
|
||||
- 方案验证错误。
|
||||
|
||||
交互:
|
||||
|
||||
- 点击卡片或“查看流程”只切换预览。
|
||||
- “采用此方案”调用 select API。
|
||||
- “开始协作”再次校验并创建 run。
|
||||
- 方案调整后显示新 revision;旧 revision 只读。
|
||||
- 过期 revision 返回 409 后不静默覆盖。
|
||||
|
||||
验收:连续快速切换三张卡时,画布最终显示最后一次选择,不闪回旧图。
|
||||
|
||||
### FE-7:协作流程画布
|
||||
|
||||
技术:`@xyflow/react`。可以借鉴但不直接耦合:
|
||||
|
||||
- `frontend-web/src/components/ai-elements/canvas.tsx`
|
||||
- `frontend-web/src/roundtable-planning/components/FlowGraphPanel.tsx`
|
||||
|
||||
节点显示:
|
||||
|
||||
- 节点名、角色名和分析角度。
|
||||
- 状态文字、图标和颜色。
|
||||
- 等待用户、返工、失败和 superseded 标记。
|
||||
|
||||
画布能力:
|
||||
|
||||
- 自动布局。
|
||||
- 拖动节点只保存浏览器展示位置。
|
||||
- 缩放、适配视图、方向切换、小地图。
|
||||
- 活动边低强度流动动画。
|
||||
- 点击节点打开覆盖在画布上的节点说明抽屉。
|
||||
- 节点重跑、补充或纠偏一律通过左侧对话完成;抽屉只提供“在对话中对此节点提出意见”入口。
|
||||
- 方案切换后清理不属于新方案的本地坐标。
|
||||
|
||||
验收:
|
||||
|
||||
- 30 个节点内流畅操作。
|
||||
- 单个 node event 不导致整个 React Flow 重挂。
|
||||
- `awaiting_input`、`reopened`、`superseded` 不与 `completed` 混淆。
|
||||
|
||||
### FE-8:节点说明抽屉与对话定位
|
||||
|
||||
点击画布节点打开 `NodeInspectorDrawer`。抽屉只展示计划合同:
|
||||
|
||||
1. 节点名称、角色、分析角度和当前状态标签。
|
||||
2. 节点职责:负责解决什么问题。
|
||||
3. 要根据什么:所需输入类型、上游节点类型、资料范围和时效要求。
|
||||
4. 要产出什么:成果类型、结构要求和验收条件。
|
||||
5. 可用工具、禁止事项、预算上限。
|
||||
6. 上游与下游关系。
|
||||
|
||||
抽屉不展示:
|
||||
|
||||
- 节点实际生成内容。
|
||||
- 工具调用返回值。
|
||||
- EvidenceBundle 或其他成果物正文。
|
||||
- attempt 执行时间线。
|
||||
- 错误堆栈和调试信息。
|
||||
- 报告正文。
|
||||
|
||||
交互:
|
||||
|
||||
- “在对话中对此节点提出意见”关闭抽屉、聚焦左侧 Composer,并绑定 `targetNodeId` 上下文。
|
||||
- 若该节点对应正在并行运行的成员,同时绑定该成员的 `agentRunId` 并切换左侧选择器,使用户发送前能先看到该成员的真实对话与工具过程。
|
||||
- 用户仍需在对话区输入“重新检索”“重新分析”等要求;抽屉不直接发起业务变更。
|
||||
- 状态标签来自后端节点状态,但不加载节点结果接口。
|
||||
- 关闭抽屉不清空 selected node;再次点击同一节点可恢复说明。
|
||||
|
||||
报告预览不使用常驻第三栏。最终报告、章节候选和版本先作为左侧 MessageList 中的普通消息/文件卡出现;点击文件卡时复用 `ArtifactFileDetail` 或现有 Markdown 能力打开大尺寸 Dialog,关闭后回到两栏工作台。
|
||||
|
||||
验收:
|
||||
|
||||
- 抽屉宽度不改变两栏布局,不挤压左侧对话区。
|
||||
- 抽屉只读取节点合同和图结构,不请求节点执行结果。
|
||||
- 所有原生成员消息、工具过程、错误和报告入口都能在左侧对话时间线找到。
|
||||
|
||||
### FE-9:全过程对话干预
|
||||
|
||||
`ReportComposer` 根据状态显示不同提示:
|
||||
|
||||
- 空闲:输入报告需求。
|
||||
- 规划中:补充要求将在规划安全点应用。
|
||||
- 运行中:协调者正在判断影响范围。
|
||||
- 等待用户:优先回答等待问题。
|
||||
- 完成后:可问答、重写、润色或重新研究。
|
||||
|
||||
发送流程:
|
||||
|
||||
1. 生成 idempotency key。
|
||||
2. 插入 pending 用户消息;带 `targetNodeId`/`agentRunId` 时在相关成员消息流和共享上下文中引用同一条用户消息,不重复持久化。
|
||||
3. 显示“正在识别修改范围”。
|
||||
4. `command.classified` 后展示意图和影响节点。
|
||||
5. 高成本或低置信度命令显示确认卡。
|
||||
6. `command.accepted` 后显示排队或等待安全点。
|
||||
7. 完成后关联新 node attempt 或 report version。
|
||||
8. 失败时保留原消息和重试入口。
|
||||
|
||||
典型映射:
|
||||
|
||||
| 用户输入 | 预期操作 |
|
||||
|---|---|
|
||||
| “增加政策风险角度” | 新增角度节点和受影响下游 |
|
||||
| “资料太旧,重新找” | 重新检索指定证据包 |
|
||||
| “竞争格局分析不到位” | 复用证据,重新分析 |
|
||||
| “重写第二章” | 只生成章节替换候选 |
|
||||
| “语气更正式” | 新报告版本,不重新研究 |
|
||||
| “为什么得出这个结论” | 只读问答,不修改报告 |
|
||||
|
||||
验收:
|
||||
|
||||
- 双击发送不创建重复命令。
|
||||
- 选中“品牌竞争分析师”后发送补充要求,只影响该成员及依赖分析,其他并行成员仍保持消息流和运行状态。
|
||||
- 运行中反馈不会让 run 或 node 误变 completed。
|
||||
- “重写第二章”不会触发全量检索。
|
||||
- “资料太旧”不会只做文字润色。
|
||||
|
||||
### FE-10:会话历史、报告版本与导出
|
||||
|
||||
- History Tab 显示标题、状态、更新时间、当前版本。
|
||||
- 新建会话不删除旧会话。
|
||||
- 会话切换时先关闭旧 SSE,再加载新快照。
|
||||
- 报告修改先生成候选,确认后创建新版本。
|
||||
- 版本恢复创建新的头版本,不删除历史。
|
||||
- 下载复用现有 Markdown/Word 能力。
|
||||
- 文件名来自报告标题,不暴露宿主机或沙箱路径。
|
||||
|
||||
验收:历史会话可完整恢复左侧消息流程、右侧节点状态和报告版本;不额外恢复节点结果面板。
|
||||
|
||||
### FE-11:测试、视觉回归和性能
|
||||
|
||||
单元测试:
|
||||
|
||||
- event normalizer/reducer。
|
||||
- message adapter。
|
||||
- plan revision 和选择状态。
|
||||
- node status 和 available actions。
|
||||
- command pending/idempotency。
|
||||
|
||||
组件测试:
|
||||
|
||||
- 空态、澄清、规划、候选、运行、等待、完成、失败、取消。
|
||||
- 卡片选择、节点点击、只读说明 Drawer、确认弹窗和报告预览 Dialog。
|
||||
- MessageList 通用业务卡插槽零回归。
|
||||
- 单成员流式文本、TeamSay、工具调用与工具返回的原样重放;并行成员交错到达、切换标签、切回后不中断 delta。
|
||||
- 非当前标签成员发起协助时显示待回复徽标,点击后使用原生协助卡回复到正确节点。
|
||||
|
||||
E2E:
|
||||
|
||||
1. 新建会话并输入新能源汽车报告要求。
|
||||
2. 选择候选并开始。
|
||||
3. 运行中增加风险角度。
|
||||
4. 重新检索海外最新资料。
|
||||
5. 完成后重写第二章。
|
||||
6. 刷新、断网重连、切换会话。
|
||||
7. 停止和重试失败节点。
|
||||
|
||||
性能门:
|
||||
|
||||
- 30 节点画布不卡顿。
|
||||
- 长消息列表使用现有优化策略。
|
||||
- SSE 高频 delta 合帧后再 setState。
|
||||
- 画布和消息区使用独立 selector,避免互相触发整树重渲染。
|
||||
- 节点抽屉只订阅节点合同和状态,不能因阶段产出更新而反复重渲染。
|
||||
- 只渲染当前聚焦成员和共享消息的可视窗口;非当前成员的消息仍写入 store,但不因 token delta 重渲染当前 `MessageList`。
|
||||
|
||||
---
|
||||
|
||||
## 8. 后端接口依赖
|
||||
|
||||
前端开发前,后端必须冻结以下接口:
|
||||
|
||||
```text
|
||||
POST /api/report-collaboration/sessions
|
||||
GET /api/report-collaboration/sessions
|
||||
GET /api/report-collaboration/sessions/{session_id}
|
||||
PATCH /api/report-collaboration/sessions/{session_id}
|
||||
DELETE /api/report-collaboration/sessions/{session_id}
|
||||
|
||||
POST /api/report-collaboration/sessions/{session_id}/messages
|
||||
GET /api/report-collaboration/sessions/{session_id}/messages
|
||||
POST /api/report-collaboration/sessions/{session_id}/plan-requests
|
||||
GET /api/report-collaboration/sessions/{session_id}/plans
|
||||
POST /api/report-collaboration/sessions/{session_id}/plans/{plan_id}/select
|
||||
POST /api/report-collaboration/sessions/{session_id}/runs
|
||||
|
||||
GET /api/report-collaboration/runs/{run_id}
|
||||
GET /api/report-collaboration/runs/{run_id}/events
|
||||
GET /api/report-collaboration/runs/{run_id}/stream
|
||||
POST /api/report-collaboration/runs/{run_id}/commands
|
||||
POST /api/report-collaboration/runs/{run_id}/cancel
|
||||
GET /api/report-collaboration/runs/{run_id}/nodes/{node_id}/contract
|
||||
|
||||
GET /api/report-collaboration/sessions/{session_id}/reports
|
||||
POST /api/report-collaboration/sessions/{session_id}/report-rewrites
|
||||
POST /api/report-collaboration/sessions/{session_id}/report-rewrites/{id}/apply
|
||||
```
|
||||
|
||||
前后端共享枚举:
|
||||
|
||||
- session status。
|
||||
- run status。
|
||||
- node/node attempt status。
|
||||
- command intent/status。
|
||||
- event type/version。
|
||||
- artifact type/schema version。
|
||||
- report version/change type。
|
||||
|
||||
---
|
||||
|
||||
## 9. 前端开发顺序
|
||||
|
||||
页面与契约层已完成(代码在 `frontend-web/src/report-collaboration/`):
|
||||
|
||||
1. `RC-FE-001`:视觉截图与行为基线。
|
||||
2. `RC-FE-002`:路由、菜单和左对话/右画布两栏空壳。
|
||||
3. `RC-FE-003`:页面 token、弹窗、抽屉和响应式。
|
||||
4. `RC-FE-004`:API 类型和 mock server。
|
||||
5. `RC-FE-005`:SSE reducer 与快照恢复。
|
||||
6. `RC-FE-006`:MessageList adapter 和通用卡片插槽。
|
||||
7. `RC-FE-007`:需求澄清与候选方案卡。
|
||||
8. `RC-FE-008`:流程画布和自定义节点。
|
||||
9. `RC-FE-009`:只读节点说明抽屉与对话目标定位。
|
||||
10. `RC-FE-010`:统一 Composer 和命令确认。
|
||||
11. `RC-FE-011`:报告预览、版本和导出。
|
||||
12. `RC-FE-012`:历史、重连和异常恢复。
|
||||
13. `RC-FE-013`:组件/E2E/视觉/性能验收。
|
||||
|
||||
依赖规则:
|
||||
|
||||
- 契约已冻结为 `src/report-collaboration/api/types.ts`;后端 `RC-BE-001` 已对齐。
|
||||
- 真实 SSE 联调:后端 `RC-BE-011` 已挂载 `GET /runs/{id}/stream`;默认无后台循环时流只会重放已入库事件。
|
||||
- 报告版本操作已接后端 `RC-BE-015`(改写候选 / apply / restore)。
|
||||
- 执行画布实时状态依赖打开 `worker_enabled`;质量角色内核已在 `RC-BE-013` 落地,真实成员见 `RC-BE-017`。
|
||||
|
||||
---
|
||||
|
||||
## 10. 前端完成定义
|
||||
|
||||
页面与 mock 联调路径已落地。真实后端可联调 `GET /runs/{id}/stream`、领取/恢复(`RC-BE-012`,默认不挂 worker)、质量角色(`RC-BE-013`)以及报告改写/恢复(`RC-BE-015`)。此前以 mock driver 覆盖交互,不以「前端未编码」计未完成。
|
||||
|
||||
前端只有同时满足以下条件才算完成:
|
||||
|
||||
1. 页面结构与 Coze 参考页面逐状态验收通过。
|
||||
2. 消息区复用 DeerFlow `MessageList`,其他聊天页面无回归。
|
||||
3. 候选卡、左侧对话流程、右侧流程图和报告来自同一 session/run/event 状态。
|
||||
4. 点击候选只预览,明确确认后才执行。
|
||||
5. 用户可以在规划、运行、完成后三个阶段继续对话。
|
||||
6. 协助卡显示或提交不会导致前端误标完成。
|
||||
7. 节点点击只打开说明抽屉,可查看职责、所需输入、预期输出、工具和验收条件,不展示执行结果。
|
||||
8. 节点重开、返工和 superseded 状态表达正确。
|
||||
9. SSE 重连、刷新和历史切换无重复消息和状态倒退。
|
||||
10. 报告修改以候选和版本形式完成,不直接覆盖正式报告。
|
||||
11. 页面始终只有对话与画布两个常驻区域,所有执行过程和产出入口都能在对话时间线中找到。
|
||||
12. 响应式、键盘、对比度、加载反馈和错误恢复通过验收。
|
||||
@@ -1,485 +0,0 @@
|
||||
# 报告协作工作台 — 前端对接指南
|
||||
|
||||
> **适用版本:** RC-BE-000~018 已落地(2026-09-06)
|
||||
> **配套文档:**
|
||||
> - [前端实施计划](./agentscope-多智能体报告协作工作台-前端实施计划.md)
|
||||
> - [后端实施计划](./agentscope-多智能体报告协作工作台-后端实施计划.md)
|
||||
> - 模块 README:`frontend-web/src/report-collaboration/README.md`
|
||||
|
||||
本文说明**前端如何对接真实后端**:环境配置、API 契约、用户流程、状态机、错误处理与联调验收。前端 API 客户端与页面已实现,通常**无需再写一套 HTTP 层**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论速览
|
||||
|
||||
| 问题 | 答案 |
|
||||
|------|------|
|
||||
| 前端还要写接口吗? | **不用**。`src/report-collaboration/api/` 已封装 REST + SSE,页面通过 `getReportCollaborationDriver()` 调用。 |
|
||||
| 为什么调不通? | 最常见:`config.yaml` 里 `report_collaboration.enabled: false` → 全部 **503**;或 `worker_enabled: false` → run 不入队执行。 |
|
||||
| Mock 和真后端怎么切? | **Mock 已移除**。前端永远请求真实后端(`deerflow.rc-mock` / `VITE_RC_MOCK` 开关已删除);联调需 `enabled: true`。 |
|
||||
| 字段会和 MySQL 对不上吗? | 前端只认 JSON 契约(`api/types.ts`),不直连表。缺列表现为后端 500/SQL 错误,部署后需**重启 Gateway** 跑 `create_all` + 列同步。 |
|
||||
| 从哪进页面? | `/page/workspace/report-collaboration`(新会话)或 `/page/workspace/report-collaboration/:sessionId`。侧边栏入口默认注释,可直接 URL 访问。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构:前端已有什么
|
||||
|
||||
```
|
||||
ReportCollaborationPage
|
||||
└─ useReportCollaborationSession(唯一数据入口)
|
||||
├─ driver.getSnapshot() ← REST 冷启动
|
||||
├─ driver.openRunStream() ← SSE 实时事件
|
||||
├─ driver.sendMessage / createCommand / startRun …
|
||||
└─ event-reducer ← 事件 → UI 状态
|
||||
|
||||
ReportConversationPanel
|
||||
└─ MessageList(message-adapter 映射 CollaborationMessage → LangGraph Message)
|
||||
```
|
||||
|
||||
### 2.1 目录结构
|
||||
|
||||
```text
|
||||
frontend-web/src/report-collaboration/
|
||||
api/
|
||||
types.ts # 冻结契约(snake_case,与后端 Pydantic 一致)
|
||||
client.ts # rcFetch:鉴权、幂等键、乐观锁、错误映射
|
||||
sessions.ts # 会话 CRUD、消息
|
||||
plans.ts # 方案请求 / 列表 / 选定
|
||||
runs.ts # Run、SSE、命令、节点合同
|
||||
reports.ts # 报告版本、改写、恢复
|
||||
driver.ts # 真实后端统一门面(Mock 驱动已移除)
|
||||
state/
|
||||
event-normalizer.ts
|
||||
event-reducer.ts
|
||||
selectors.ts
|
||||
message-adapter.ts
|
||||
hooks/
|
||||
useReportCollaborationSession.ts # 快照 → SSE → reducer → 动作
|
||||
useReportCollaborationStream.ts # 断线重连、after_seq 续接
|
||||
useReportCollaborationMessages.ts
|
||||
components/ …
|
||||
pages/ReportCollaborationPage.tsx
|
||||
```
|
||||
|
||||
### 2.2 事实源原则
|
||||
|
||||
- **冷启动:** `GET /sessions/{id}` 返回 `SessionSnapshot`(session、messages、plans、run、nodes、agent_runs、commands、report_versions、last_seq)。
|
||||
- **运行中:** `GET /runs/{id}/stream?after_seq=N` 订阅 durable SSE。
|
||||
- **断线 / 序号缺口:** `GET /runs/{id}/events?after_seq=N` REST 补拉。
|
||||
- **澄清 / 规划阶段(尚无 run):** 每 2.5s 轮询快照(后端事件主要在 run 流上)。
|
||||
|
||||
纯展示状态(预览方案、聚焦成员、面板宽度)**不**写入服务器,仅存前端。
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端启用与部署
|
||||
|
||||
### 3.1 必改配置
|
||||
|
||||
文件:`offline-backend-20260512/backend/config.yaml`
|
||||
|
||||
```yaml
|
||||
report_collaboration:
|
||||
enabled: false # 联调必须改为 true
|
||||
worker_enabled: false # 要看成员执行必须 true
|
||||
planner_model: null # 建议填可用模型名
|
||||
coordinator_model: null
|
||||
research_model: null
|
||||
writer_model: null
|
||||
reviewer_model: null
|
||||
# … token_budget、max_concurrent_runs_per_user 等见同文件
|
||||
```
|
||||
|
||||
| 配置 | 现象 |
|
||||
|------|------|
|
||||
| `enabled: false` | 所有路由 **503**,`code: REPORT_COLLABORATION_DISABLED` |
|
||||
| `enabled: true`,`worker_enabled: false` | REST/SSE 可用,run 入队但不自动执行(测 API 可用,无成员流式输出) |
|
||||
| 两者 `true` + 模型配置完整 | 完整联调 |
|
||||
|
||||
### 3.2 MySQL / 表结构
|
||||
|
||||
生产环境 Alembic 自动升级默认关闭,启动时走 `create_all` + `_ensure_orm_columns_sync`。
|
||||
|
||||
- 迁移脚本(可选手动跑):`20260905_01`、`20260906_01`、`20260907_01`
|
||||
- ORM:`packages/harness/deerflow/persistence/report_collaboration/model.py`(13 张表)
|
||||
- **部署后务必重启 Gateway**,观察日志是否有 `Failed to auto-add column report_collaboration_*`
|
||||
|
||||
已有 RC-003 时代旧表会通过列同步补上 `template_snapshot_json`、`budget_snapshot_json`、`usage_json`、`budget_exhausted_reason` 等可空列,并创建 `rewrite_proposals`、`audits`。
|
||||
|
||||
### 3.3 前端环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `VITE_BACKEND_BASE_URL` | Gateway 地址,默认 `http://127.0.0.1:8001` |
|
||||
|
||||
请求经 `apiFetch`,自动携带登录 JWT(`credentials: "include"`)。**必须先登录 DeerFlow**。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 基址与请求约定
|
||||
|
||||
### 4.1 Base URL
|
||||
|
||||
```text
|
||||
{VITE_BACKEND_BASE_URL}/api/report-collaboration
|
||||
```
|
||||
|
||||
代码常量:`REPORT_COLLABORATION_BASE()`(`api/client.ts`)。
|
||||
|
||||
### 4.2 请求头
|
||||
|
||||
| Header | 何时携带 | 说明 |
|
||||
|--------|----------|------|
|
||||
| `Authorization` | 全部 | 由 `apiFetch` 注入 |
|
||||
| `X-Idempotency-Key` | 写操作 | 格式 `rc-{uuid}`;重复提交返回首次结果 |
|
||||
| `X-Expected-Revision` | 选方案、开 run、改会话等 | 会话 `requirement_revision` 或方案 `revision` |
|
||||
| `Accept: text/event-stream` | SSE | `openRunStream` |
|
||||
|
||||
### 4.3 错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "REPORT_COLLABORATION_DISABLED",
|
||||
"message": "报告协作工作台未启用",
|
||||
"detail": { }
|
||||
}
|
||||
```
|
||||
|
||||
前端映射(`api/client.ts`):
|
||||
|
||||
| HTTP | 异常类 | 典型场景 |
|
||||
|------|--------|----------|
|
||||
| 409 | `ConflictError` | revision 过期、active run 冲突 |
|
||||
| 422 | `ValidationError` | 参数/语义错误、缺幂等键 |
|
||||
| 429 | `RateLimitError` | 预算耗尽、每用户并发 run 上限 |
|
||||
| 5xx | `ServerError` | 可带同一幂等键重试 |
|
||||
| 0 | `NetworkError` | fetch 失败 |
|
||||
|
||||
### 4.4 完整路由清单(与前端 client 对齐)
|
||||
|
||||
| 方法 | 路径 | 前端模块 |
|
||||
|------|------|----------|
|
||||
| POST | `/sessions` | `sessions.createSession` |
|
||||
| GET | `/sessions` | `sessions.listSessions` |
|
||||
| GET | `/sessions/{id}` | `sessions.getSessionSnapshot` |
|
||||
| PATCH | `/sessions/{id}` | `sessions.updateSession` |
|
||||
| DELETE | `/sessions/{id}` | `sessions.deleteSession` |
|
||||
| GET | `/sessions/{id}/messages` | `sessions.listMessages` |
|
||||
| POST | `/sessions/{id}/messages` | `sessions.sendMessage` |
|
||||
| POST | `/sessions/{id}/plan-requests` | `plans.requestPlans` |
|
||||
| GET | `/sessions/{id}/plans` | `plans.listPlans` |
|
||||
| POST | `/sessions/{id}/plans/{planId}/select` | `plans.selectPlan` |
|
||||
| POST | `/sessions/{id}/runs` | `runs.startRun` |
|
||||
| GET | `/sessions/{id}/reports` | `reports.listReportVersions` |
|
||||
| POST | `/sessions/{id}/report-rewrites` | `reports.createRewrite` |
|
||||
| POST | `/sessions/{id}/report-rewrites/{id}/apply` | `reports.applyRewrite` |
|
||||
| POST | `/sessions/{id}/report-versions/{id}/restore` | `reports.restoreReportVersion` |
|
||||
| GET | `/runs/{id}` | `runs.getRun` |
|
||||
| GET | `/runs/{id}/events` | `runs.listRunEvents` |
|
||||
| GET | `/runs/{id}/stream` | `runs.openRunStream` |
|
||||
| POST | `/runs/{id}/cancel` | `runs.cancelRun` |
|
||||
| POST | `/runs/{id}/commands` | `runs.createCommand` |
|
||||
| POST | `/runs/{id}/commands/{id}/confirm` | `runs.confirmCommand` |
|
||||
| POST | `/runs/{id}/commands/{id}/cancel` | `runs.cancelCommand` |
|
||||
| GET | `/runs/{id}/nodes/{nodeId}/contract` | `runs.getNodeContract` |
|
||||
| GET | `/runs/{id}/artifacts` | (后端已暴露,前端按需接) |
|
||||
| GET | `/runs/{id}/sources` | (后端已暴露,前端按需接) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 用户流程与 API 时序
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant FE as 前端 RC
|
||||
participant API as Gateway
|
||||
participant W as Worker
|
||||
|
||||
U->>FE: 输入需求
|
||||
FE->>API: POST /sessions
|
||||
FE->>API: POST /sessions/{id}/messages
|
||||
API-->>FE: 澄清(快照 messages)
|
||||
U->>FE: 回答澄清
|
||||
FE->>API: POST /messages
|
||||
FE->>API: POST /plan-requests
|
||||
API-->>FE: 候选方案(快照 plans)
|
||||
U->>FE: 采用方案
|
||||
FE->>API: POST /plans/{id}/select
|
||||
U->>FE: 开始协作
|
||||
FE->>API: POST /runs
|
||||
FE->>API: GET /runs/{id}/stream
|
||||
W-->>API: 持久化事件
|
||||
API-->>FE: SSE
|
||||
U->>FE: 运行中干预
|
||||
FE->>API: POST /commands → confirm
|
||||
```
|
||||
|
||||
### 5.1 阶段 0:进入页面
|
||||
|
||||
| 路由 | 行为 |
|
||||
|------|------|
|
||||
| `/page/workspace/report-collaboration` | 空态;`POST /sessions` 后跳转带 `sessionId` |
|
||||
| `/page/workspace/report-collaboration/:sessionId` | 加载快照;有 `location.state.rcInitialMessage` 时自动发首条消息 |
|
||||
|
||||
路由注册:`frontend-web/src/pages/WorkspaceRoutes.tsx`。
|
||||
侧边栏入口已在 `workspace-nav-chat-list.tsx` 中放开(「报告协作」项),也可直接访问 URL。
|
||||
|
||||
### 5.2 阶段 1:需求澄清(无 run)
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/sessions
|
||||
POST /api/intent/init
|
||||
POST /api/intent/stream
|
||||
POST /api/report-collaboration/sessions/{sessionId}/intent-turns
|
||||
```
|
||||
|
||||
- 报告页面固定调用内置 `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`。
|
||||
- 澄清问题以 `CollaborationMessage` 进入时间线;业务卡通过 `MessageList` 的 `businessCardSlot` 渲染(`additional_kwargs.report_collaboration_card`)。
|
||||
- 意图智能体输出 `INTENT_READY` 后,`POST /intent-turns` 自动创建并完成 `plan_request`;前端无需再展示“确认意图”或“生成候选方案”按钮。
|
||||
|
||||
### 5.3 阶段 2:候选方案
|
||||
|
||||
```http
|
||||
# 正常路径:由最后一轮 POST /messages 自动触发规划
|
||||
POST /api/report-collaboration/sessions/{sessionId}/plans/{planId}/select
|
||||
```
|
||||
|
||||
- `POST /plan-requests` 仅保留给“重新生成”与兼容调用,不是首次规划的必经步骤。
|
||||
- 规划师尚未判定 `ready` 时直接调用该兼容接口会返回 `422 REQUIREMENT_NOT_READY`,防止跳过意图确认。
|
||||
- **预览方案**(`previewPlanId`):纯前端,不调 API。
|
||||
- **采用方案**:`select` 写 `selected_plan_id`,**不**创建 run。
|
||||
- 409:方案 revision 过期 → 前端 `refreshSnapshot`,提示用户重选。
|
||||
- 右栏 `ReportFlowCanvas` 使用 `PlanNode` / `PlanEdge` 渲染 DAG。
|
||||
|
||||
### 5.4 阶段 3:开始执行与 SSE
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/sessions/{sessionId}/runs
|
||||
{ "plan_id": "plan-xxx" }
|
||||
|
||||
GET /api/report-collaboration/runs/{runId}/stream?after_seq=0
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
SSE 每条 `data:` 为 JSON:
|
||||
|
||||
```typescript
|
||||
interface CollaborationEvent {
|
||||
eventId: string;
|
||||
sessionId: string;
|
||||
runId?: string | null;
|
||||
seq: number;
|
||||
type: CollaborationEventType;
|
||||
timestamp: string;
|
||||
data: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
常用 `type`(完整列表见 `api/types.ts`):
|
||||
|
||||
| 类型 | 含义 |
|
||||
|------|------|
|
||||
| `message.created` / `message.delta` / `message.completed` | 对话镜像流式 |
|
||||
| `tool_call.*` / `tool_result.*` | 工具调用与返回 |
|
||||
| `team_message.*` | 成员 / 协调者广播 |
|
||||
| `plan.proposed` / `plan.selected` | 方案生命周期 |
|
||||
| `run.status.changed` | Run 状态 |
|
||||
| `node.status.changed` / `node.progress` | 节点执行 |
|
||||
| `command.confirmation.required` | 需用户确认的干预 |
|
||||
| `report.version.created` | 报告版本落盘 |
|
||||
| `heartbeat` | 保活 |
|
||||
|
||||
### 5.5 阶段 4:运行中干预
|
||||
|
||||
```http
|
||||
POST /api/report-collaboration/runs/{runId}/commands
|
||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/confirm
|
||||
POST /api/report-collaboration/runs/{runId}/commands/{commandId}/cancel
|
||||
POST /api/report-collaboration/runs/{runId}/cancel
|
||||
```
|
||||
|
||||
- Run 活跃时 `send()` 自动走 `createCommand`(而非 `sendMessage`)。
|
||||
- 可从节点抽屉 / `AgentStageSelector` 传入 `target_node_id`、`agent_run_id`。
|
||||
- `InterventionCard` 展示 `confirmation_required` 命令;确认后调 `confirmCommand`。
|
||||
|
||||
`CommandIntent` 枚举(后端分类,前端展示 reason/impact):`answer_question`、`update_requirements`、`research_again`、`rewrite_section`、`rerun_node`、`replan`、`cancel` 等。
|
||||
|
||||
### 5.6 阶段 5:报告版本与改写
|
||||
|
||||
```http
|
||||
GET /api/report-collaboration/sessions/{sessionId}/reports
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-rewrites/{rewriteId}/apply
|
||||
POST /api/report-collaboration/sessions/{sessionId}/report-versions/{versionId}/restore
|
||||
```
|
||||
|
||||
- `createRewrite` 只生成候选,不直接覆盖正式版本。
|
||||
- `applyRewrite` / `restoreReportVersion` 产生新的 head 版本,历史保留。
|
||||
- `ReportPreviewDialog` 展示 `ReportVersion.markdown`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端状态机(接入方须知)
|
||||
|
||||
**不要**在页面里用零散 REST 字段拼 UI 状态。统一路径:
|
||||
|
||||
```text
|
||||
SessionSnapshot → applySessionSnapshot()
|
||||
CollaborationEvent[] → normalizeCollaborationEvent() → applyCollaborationEvents()
|
||||
→ selectors(derivePagePhase、selectPlanForCanvas、…)
|
||||
```
|
||||
|
||||
### 6.1 核心 Selector
|
||||
|
||||
| Selector | 用途 |
|
||||
|----------|------|
|
||||
| `derivePagePhase` | Composer 占位、方案卡、空态 |
|
||||
| `selectPlanForCanvas` | 画布 DAG(含预览方案) |
|
||||
| `selectAgentTabs` | 并行成员 Tab |
|
||||
| `selectNodeStatusMap` | 节点颜色 / 状态 |
|
||||
| `selectPendingConfirmationCommands` | 干预确认卡 |
|
||||
| `selectHeadReportVersion` | 报告预览入口 |
|
||||
| `selectStreamingAgentRunIds` | 流式成员指示 |
|
||||
|
||||
### 6.2 SSE 连接策略(`useReportCollaborationStream`)
|
||||
|
||||
- Run 非终态时连接;`completed` / `failed` / `cancelled` 后关流并 REST 最终补拉。
|
||||
- 断线:指数退避重连(1s~30s),`after_seq` 取 reducer 内 `lastSeq`。
|
||||
- `hasSeqGap`:暂停 live 应用,先 `listRunEvents` 回填。
|
||||
|
||||
### 6.3 乐观消息
|
||||
|
||||
用户发送时插入 `metadata.optimistic: true` 的 pending 消息;服务端镜像带相同 `idempotency_key` 时 reducer 原地替换。
|
||||
|
||||
---
|
||||
|
||||
## 7. 消息与 MessageList 复用
|
||||
|
||||
`useReportCollaborationMessages` 将 `CollaborationMessage[]` 转为 `@langchain/langgraph-sdk` 的 `Message[]`,并构造合成 `BaseStream` 供 `MessageList` 消费。
|
||||
|
||||
| 来源 | 渲染 |
|
||||
|------|------|
|
||||
| `human` / `ai` 文本 | 普通气泡 |
|
||||
| `tool_calls` + `tool` | 现有工具卡 |
|
||||
| `team_message` 事件 | 带 `name` 的成员发言 |
|
||||
| 业务卡 | `businessCardSlot`(方案组、命令确认、报告可用) |
|
||||
|
||||
分组键:`core/messages/utils.ts` 中 `assistant:task-report-import` 同级扩展 `assistant:business-card`,判据 `additional_kwargs.report_collaboration_card`。
|
||||
|
||||
### 7.1 在其他页面嵌入 RC(最小集)
|
||||
|
||||
1. `useReportCollaborationSession(sessionId)`
|
||||
2. `ThreadContext.Provider` + `MessageList` + `businessCardSlot`
|
||||
3. 引入 `report-collaboration/styles/report-collaboration.css`
|
||||
|
||||
---
|
||||
|
||||
## 8. 单测
|
||||
|
||||
Mock 驱动(原 `api/mock.ts`)与 `deerflow.rc-mock` / `VITE_RC_MOCK` 开关已移除,前端只请求真实后端;本地演示请直接联调后端。
|
||||
|
||||
单测:
|
||||
|
||||
```bash
|
||||
cd frontend-web
|
||||
pnpm test:report-collaboration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 联调验收清单
|
||||
|
||||
### 9.1 准备
|
||||
|
||||
- [ ] `report_collaboration.enabled: true`
|
||||
- [ ] `report_collaboration.worker_enabled: true`
|
||||
- [ ] 五个 `*_model` 已配置且可用
|
||||
- [ ] Gateway 已重启(MySQL 表/列就绪)
|
||||
- [ ] 前端已登录(JWT 有效)
|
||||
|
||||
### 9.2 探活
|
||||
|
||||
```bash
|
||||
curl -s -H "Authorization: Bearer <token>" \
|
||||
http://127.0.0.1:8001/api/report-collaboration/sessions
|
||||
```
|
||||
|
||||
期望:**200** + `{ "sessions": [...] }`,而非 503。
|
||||
|
||||
### 9.3 页面流程(浏览器 Network)
|
||||
|
||||
1. 打开 `/#/page/workspace/report-collaboration`
|
||||
2. 输入需求 → `POST .../sessions` **200**
|
||||
3. 跳转 → `GET .../sessions/{id}` **200**,响应含 `SessionSnapshot`
|
||||
4. 首条消息 → `POST .../messages` **200**
|
||||
5. 请求方案 → `POST .../plan-requests` **200**
|
||||
6. 采用方案 → `POST .../plans/{id}/select` **200**
|
||||
7. 开始协作 → `POST .../runs` **200**
|
||||
8. `GET .../runs/{id}/stream` 持续收到 SSE(`message.delta`、`node.status.changed` 等)
|
||||
9. 刷新页面 → 消息与 `last_seq` 续接正确
|
||||
10. 运行中输入「重新检索」→ `POST .../commands` → 确认卡 → `confirm` **200**
|
||||
|
||||
### 9.4 常见失败
|
||||
|
||||
| 现象 | 原因 | 处理 |
|
||||
|------|------|------|
|
||||
| 全部 503 | `enabled: false` | 改配置并重启 |
|
||||
| run 一直 queued | `worker_enabled: false` | 改为 true |
|
||||
| 409 选方案 | revision 过期 | 刷新后重选(前端已自动 refresh) |
|
||||
| 429 | 预算/并发上限 | 调大配置或取消其他 run |
|
||||
| 500 + SQL | 表缺列 | 重启 Gateway,查列同步日志 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 契约与类型维护
|
||||
|
||||
- **单一事实源:** `frontend-web/src/report-collaboration/api/types.ts`
|
||||
- **后端镜像:** `offline-backend-20260512/backend/app/report_collaboration/contracts/`
|
||||
- 修改字段须同步两份实施计划与本指南,并跑前后端相关测试。
|
||||
|
||||
`SessionSnapshot` 形状(摘要):
|
||||
|
||||
```typescript
|
||||
interface SessionSnapshot {
|
||||
session: ReportSession;
|
||||
messages: CollaborationMessage[];
|
||||
plans: ReportPlanCandidate[];
|
||||
run: ReportRun | null;
|
||||
nodes: CollaborationNodeState[];
|
||||
agent_runs: AgentRun[];
|
||||
commands: CollaborationCommand[];
|
||||
report_versions: ReportVersion[];
|
||||
last_seq: number;
|
||||
}
|
||||
```
|
||||
|
||||
`ReportRun` 可选字段(RC-BE-016):`usage`、`budget_exhausted_reason`。
|
||||
|
||||
---
|
||||
|
||||
## 11. 后续工作(非对接阻塞)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| RC-BE-019 | 灰度发布与离线部署文档 |
|
||||
| 侧边栏入口 | 已放开(`workspace-nav-chat-list.tsx` 的「报告协作」项) |
|
||||
| iframe 嵌入 | RC 页尚未专门适配 `embed=1`,可参考现有深链模式扩展 |
|
||||
| 真人盲评 | RC-BE-018 为确定性评测集,上线前仍需计划 §11.4 人工评审 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 相关代码索引
|
||||
|
||||
| 用途 | 路径 |
|
||||
|------|------|
|
||||
| API 客户端 | `frontend-web/src/report-collaboration/api/` |
|
||||
| 会话 Hook | `frontend-web/src/report-collaboration/hooks/useReportCollaborationSession.ts` |
|
||||
| 页面 | `frontend-web/src/report-collaboration/pages/ReportCollaborationPage.tsx` |
|
||||
| 路由 | `frontend-web/src/pages/WorkspaceRoutes.tsx` |
|
||||
| 后端路由 | `offline-backend-20260512/backend/app/gateway/routers/report_collaboration.py` |
|
||||
| 配置 | `offline-backend-20260512/backend/config.yaml` → `report_collaboration` |
|
||||
| 持久化 | `offline-backend-20260512/backend/packages/harness/deerflow/persistence/report_collaboration/` |
|
||||
@@ -1,31 +0,0 @@
|
||||
# 日常使用复查(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`。本轮不重复付费生成完整方案;没有进行生产全量语料质量评估、负载测试、断网故障注入或完整无障碍合规审计。已检查可见导航、表单名称、窄屏显示和主要失败恢复,不能据此承诺所有模块没有缺陷。代码未提交、推送或部署。
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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 保持不变。未提交、推送代码,也未执行离线发布。
|
||||
@@ -1,304 +0,0 @@
|
||||
# OpenVibeCoding 内网可视化应用生成平台:分步开发方案
|
||||
|
||||
> 文档状态:待实施
|
||||
>
|
||||
> 目标环境:现有 Ubuntu 22.04 Docker 服务器;对外可用端口限定为 `8020-8030`。
|
||||
>
|
||||
> 本文基于 2026-08-31 的只读巡检编写。未在目标服务器上安装、停止或修改任何服务。
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
将 OpenVibeCoding 改造成一个使用公司内网模型、在公司服务器 Docker 沙箱中生成并运行可视化应用的工作台。用户通过对话生成项目,查看并修改文件,在浏览器中预览运行结果。
|
||||
|
||||
第一期只服务以下项目类型:
|
||||
|
||||
- 数据大屏、数据看板、海报和展示型页面;
|
||||
- React + Vite + TypeScript 前端;
|
||||
- ECharts 图表、流程图;
|
||||
- 可选的轻量 Python FastAPI 接口;
|
||||
- 项目文件浏览、编辑、运行日志和实时预览。
|
||||
|
||||
第一期明确不做:
|
||||
|
||||
- 腾讯云 CloudBase、SCF、TCR、CodeBuddy、云端一键部署;
|
||||
- 任意语言、任意 npm/PyPI 包的无限制安装;
|
||||
- 用户自定义 Dockerfile、Docker Socket 挂载、宿主机命令执行;
|
||||
- 直接将沙箱暴露为独立公网端口;
|
||||
- 对不可信代码提供“绝对安全”的隔离承诺。
|
||||
|
||||
## 2. 已确认的服务器基线
|
||||
|
||||
目标服务器当前具备:
|
||||
|
||||
| 项目 | 巡检结果 | 判断 |
|
||||
|---|---:|---|
|
||||
| 操作系统 | Ubuntu 22.04.5 LTS | 支持 Docker 方案 |
|
||||
| CPU | 32 vCPU | 足够第一期 |
|
||||
| 内存 | 125 GiB;巡检时可用约 106 GiB | 足够第一期 |
|
||||
| 根磁盘 | 2 TB;巡检时可用约 1.6 TB | 足够 Harbor、镜像与工作区 |
|
||||
| Docker | 29.1.3,Docker Compose 2.40.3 | 已满足 |
|
||||
| 负载 | 约 0.5 | 当前有余量 |
|
||||
| `8020-8030` | 巡检时无监听、无 Docker 端口映射 | 可规划使用 |
|
||||
|
||||
该服务器已承载 DeerFlow、Coze、LiteLLM、数据库和其他容器;`80`、`8000`、`8005`、`8006`、`3306`、`5432` 等端口已经被使用。因此,本项目不能占用现有端口、网络、数据库或容器名称。
|
||||
|
||||
该服务器有公网 IP,且已有多项服务绑定在 `0.0.0.0`。本方案将它视作“公网可达的共享主机”处理;云安全组和宿主机防火墙必须额外核验,不能仅凭端口监听结果假设安全。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[用户浏览器] -->|:8020| G[App Builder Gateway]
|
||||
U -->|:8022 /preview/<sandboxId>/| P[Preview Router]
|
||||
|
||||
G --> O[OpenVibe Web / Agent Server]
|
||||
O --> M[现有内网模型网关]
|
||||
O --> DB[(独立 PostgreSQL)]
|
||||
O --> R[(独立 Redis)]
|
||||
O --> SM[Local Sandbox Manager]
|
||||
|
||||
SM --> D[Docker Engine]
|
||||
D --> S1[visual-ui Sandbox]
|
||||
D --> S2[visual-fullstack Sandbox]
|
||||
P --> S1
|
||||
P --> S2
|
||||
|
||||
D --> H[Harbor :8023]
|
||||
S1 --> N[内网 pnpm/npm 源或预置离线依赖]
|
||||
S2 --> N
|
||||
```
|
||||
|
||||
设计原则:
|
||||
|
||||
1. 保留 OpenVibe 的对话、任务、文件树、工具卡片和 OpenCode ACP 代码代理体验。
|
||||
2. 模型请求仅由 OpenVibe 服务端发往既有内网模型网关;API Key 不进入沙箱。
|
||||
3. 每个项目运行于独立 Docker 容器和独立工作区;沙箱只运行项目代码,不承担平台控制逻辑。
|
||||
4. 浏览器预览只经过统一预览路由,不为每个项目映射新的公网端口。
|
||||
5. 先用固定模板和固定依赖清单保证稳定,后续再按白名单增加依赖。
|
||||
|
||||
## 4. 端口规划
|
||||
|
||||
端口虽然可用,但应尽量少暴露。除 8020、8022 和按需开放的 8023 外,其余服务只加入 Docker 内部网络。
|
||||
|
||||
| 端口 | 服务 | 绑定范围 | 用途 |
|
||||
|---:|---|---|---|
|
||||
| 8020 | `appbuilder-gateway` | 公网访问面或 VPN 白名单 | 控制台页面、`/api`、SSE/WebSocket |
|
||||
| 8021 | 预留 | 仅 `127.0.0.1` | 平台调试;第一期不对外开放 |
|
||||
| 8022 | `preview-router` | 公网访问面或 VPN 白名单 | 统一预览入口:`/preview/<sandboxId>/` |
|
||||
| 8023 | Harbor | 仅沙箱节点、构建节点和管理员网段 | 私有镜像仓库 HTTPS Registry |
|
||||
| 8024 | 预留 | 不对外开放 | 后续 npm 私服或运维服务 |
|
||||
| 8025-8030 | 预留 | 不对外开放 | 扩展、迁移或故障切换 |
|
||||
|
||||
### 4.1 预览路由规则
|
||||
|
||||
暂时没有可用泛域名时,使用路径型预览:
|
||||
|
||||
```text
|
||||
http(s)://<服务器地址>:8022/preview/<sandboxId>/
|
||||
```
|
||||
|
||||
项目模板的 Vite `base` 和 HMR 路径必须由启动脚本注入为该前缀,确保图片、JS/CSS 资源及热更新不会错误请求到根路径。后续若获得 `*.preview.company.local` 内网泛域名,可切换为子域名预览,用户体验更好。
|
||||
|
||||
## 5. 镜像、模板与离线依赖策略
|
||||
|
||||
### 5.1 标准镜像
|
||||
|
||||
第一期维护两种版本化镜像,不允许用户自行构建 Dockerfile:
|
||||
|
||||
| 镜像 | 用途 | 预置内容 |
|
||||
|---|---|---|
|
||||
| `appbuilder/visual-ui:<version>` | 大屏、海报、纯前端 | Node.js 20、pnpm、React、Vite、TypeScript、ECharts、流程图库、统一 UI 库、Tailwind、图标与中文字体 |
|
||||
| `appbuilder/visual-fullstack:<version>` | 前端加接口 | 上述前端依赖 + Python 3.12、FastAPI、Uvicorn、Pydantic |
|
||||
|
||||
流程图库第一期统一选择一种(建议 React Flow),避免同时生成多种流程图语法和样式。地图、背景图、字体等资源也应作为受版本控制的本地资产打包,不能依赖 Google Fonts 或公网 CDN。
|
||||
|
||||
### 5.2 项目模板
|
||||
|
||||
每次创建项目时从镜像中的模板初始化到 `/workspace`:
|
||||
|
||||
```text
|
||||
templates/
|
||||
visual-screen/ # React + ECharts + React Flow + Mock 数据
|
||||
visual-screen-api/ # visual-screen + FastAPI + /api 反向代理
|
||||
poster-page/ # 海报、营销展示、静态动效
|
||||
```
|
||||
|
||||
`visual-screen-api` 约定:前端只请求相对路径 `/api/*`;预览网关将其转发到同一沙箱的 FastAPI `8000` 端口。没有后端时保留 Mock 数据模块,生成的页面仍可运行。
|
||||
|
||||
### 5.3 依赖预置方式
|
||||
|
||||
不能仅把安装命令写进 Dockerfile 后在用户创建项目时再联网执行。正确做法是:
|
||||
|
||||
1. 在受控构建环境中锁定 `package.json`、`pnpm-lock.yaml`、`requirements.lock`。
|
||||
2. 构建镜像时执行 `pnpm fetch`,在镜像保存只读 pnpm store;Python 包保存到 `/opt/wheelhouse`。
|
||||
3. 沙箱初始化使用 `pnpm install --offline --frozen-lockfile` 和 `pip install --no-index --find-links=/opt/wheelhouse`。
|
||||
4. 第一期开启“固定依赖白名单”:AI 只能使用镜像已安装的依赖;新依赖先由管理员审核、加入锁文件和下一版基础镜像。
|
||||
5. 第二期部署 Verdaccio/Nexus 等内部制品源,作为受控补充而非允许访问公网 npm/PyPI。
|
||||
|
||||
## 6. OpenVibe 改造边界
|
||||
|
||||
### 6.1 保留
|
||||
|
||||
- 前端交互:对话、任务状态、文件浏览与修改、预览、日志;
|
||||
- OpenCode ACP 代码代理;
|
||||
- 项目与会话领域模型;
|
||||
- 代码代理的流式事件协议。
|
||||
|
||||
### 6.2 替换或关闭
|
||||
|
||||
| OpenVibe 当前能力 | 第一期开源/内网实现 |
|
||||
|---|---|
|
||||
| CodeBuddy | 只启用 OpenCode,配置内网 OpenAI 兼容模型网关 |
|
||||
| CloudBase、SCF 沙箱 | 新增 `LocalDockerSandboxManager` |
|
||||
| TCR | Harbor |
|
||||
| CloudBase 环境、用户环境池 | Docker 容器、工作区卷、沙箱记录 |
|
||||
| CloudBase 消息与事件持久化 | 独立 PostgreSQL;Redis 用于临时队列与状态 |
|
||||
| CloudBase 存储、函数、MCP、资源管理后台 | 第一期开关关闭或移除入口 |
|
||||
| CloudBase 项目模板和系统提示词 | 替换为大屏/海报/FastAPI 模板与受控依赖提示词 |
|
||||
|
||||
重点改造点是将 OpenVibe 的 `scf-sandbox-manager` 和 CloudBase 生命周期调用替换为本地 Docker 生命周期。为降低前端和 OpenCode 工具改动范围,新管理器应适配现有的文件、命令、日志、健康检查与工作区信息契约,而不是改写所有 Agent 工具。
|
||||
|
||||
## 7. 沙箱安全基线
|
||||
|
||||
每个沙箱容器必须满足:
|
||||
|
||||
- 非 root 用户运行;禁止 `privileged`,禁止 Docker Socket,禁止宿主机目录挂载;
|
||||
- `--cap-drop=ALL`、`no-new-privileges`、`pids-limit`;
|
||||
- 首期默认限制:`1 vCPU`、`3 GB 内存`、`10 GB 工作区`、60 分钟空闲回收;
|
||||
- 使用独立 Docker network;默认无公网出口,只允许访问预置依赖、必要的内网 DNS 和平台回调;
|
||||
- 数据库、Redis、Harbor 管理接口、宿主机管理端口不在沙箱网络可访问范围;
|
||||
- 运行命令、容器生命周期、文件变更和预览访问写入审计日志;
|
||||
- 平台服务由受限服务账号调用 Docker API;用户代码永远不直接获得 Docker 权限。
|
||||
|
||||
第一期为“内部可信用户的容器隔离”。若后续允许大量非可信代码或文件上传,应迁移为 Kubernetes 节点,并评估 gVisor/Kata 等更强运行时隔离。
|
||||
|
||||
## 8. 分步实施计划
|
||||
|
||||
### 阶段 0:部署前隔离与基线确认
|
||||
|
||||
**工作项**
|
||||
|
||||
1. 为本项目建立独立目录、Docker Compose Project、卷和网络,命名统一为 `appbuilder-*`。
|
||||
2. 确认云安全组、iptables/nftables 规则:仅允许管理员 SSH;8020、8022、8023 仅向 VPN 或指定办公出口开放。
|
||||
3. 不复用当前服务器上已有的 MySQL、PostgreSQL、Redis、Nginx 配置;建立独立实例或独立命名空间。
|
||||
4. 创建内部 CA 证书或确定可信 HTTPS 证书方案;Docker 构建/运行节点信任 Harbor 证书。
|
||||
5. 轮换目标服务器 root 密码,改用 SSH 密钥和受限运维账号;关闭 root 密码远程登录。
|
||||
|
||||
**验收**
|
||||
|
||||
- `8020-8030` 无端口冲突;
|
||||
- 新容器、网络和数据卷均不影响既有 DeerFlow 等服务;
|
||||
- 业务数据库和沙箱端口无法从非授权公网地址访问。
|
||||
|
||||
### 阶段 1:内网基础设施与镜像供应链
|
||||
|
||||
**工作项**
|
||||
|
||||
1. 在独立 Compose 项目中部署 Harbor,主入口为 `8023`,镜像数据使用独立持久化目录。
|
||||
2. 构建并推送 `visual-ui`、`visual-fullstack` 两种基础镜像;记录镜像版本、SBOM 和依赖锁文件。
|
||||
3. 完成三个模板及离线依赖初始化脚本。
|
||||
4. 先不启动漏洞扫描;严格断网环境下后续通过离线包维护漏洞库。
|
||||
5. 预留内部 npm/PyPI 源接入方式;第一期仅使用预置依赖。
|
||||
|
||||
**验收**
|
||||
|
||||
- Docker daemon 能从 Harbor 拉取指定版本镜像;
|
||||
- 在禁止公网访问的测试网络中,三个模板均能启动并构建;
|
||||
- 一个 ECharts 页面、一个流程图页面和一个 FastAPI `/api/health` 均可运行。
|
||||
|
||||
### 阶段 2:OpenVibe 去 CloudBase 化与内网模型接入
|
||||
|
||||
**工作项**
|
||||
|
||||
1. Fork 固定版本的 OpenVibeCoding,建立 `local-platform` 改造分支,记录上游 commit 与许可证。
|
||||
2. 只保留 OpenCode Agent;将 provider 指向现有内网模型网关,模型凭证只保存在服务端环境变量或密钥服务。
|
||||
3. 将基础实体、任务、消息、流式事件的持久化迁移为独立 PostgreSQL;不再调用 CloudBase 集合。
|
||||
4. 关闭 CloudBase 环境、函数、存储、数据库、资源仪表盘和相关 MCP 入口。
|
||||
5. 将系统提示词和项目创建默认项改为“可视化页面 + 受控模板/依赖”。
|
||||
|
||||
**验收**
|
||||
|
||||
- 不设置任何腾讯云、CloudBase、CodeBuddy、TCR 环境变量时,平台可启动;
|
||||
- 用户发起“生成 ECharts 大屏”后,OpenCode 能调用内网模型并生成项目文件;
|
||||
- 服务端日志和沙箱环境中均没有模型 API Key。
|
||||
|
||||
### 阶段 3:本地沙箱管理器与预览路由
|
||||
|
||||
**工作项**
|
||||
|
||||
1. 实现 `LocalDockerSandboxManager`:创建、启动、停止、超时回收、查询状态和删除容器。
|
||||
2. 每个沙箱分配工作区卷、随机内部容器地址、模板类型和资源限制;在数据库记录 `sandbox_id`、项目、用户、镜像版本、过期时间和状态。
|
||||
3. 实现或适配沙箱内 runner:文件读写、受限命令执行、日志流、健康检查、前端端口和 FastAPI 端口发现。
|
||||
4. 实现 `preview-router`:由平台根据 `sandboxId` 维护路由,将 `/preview/<sandboxId>/` 代理到对应内部容器;支持 WebSocket/HMR。
|
||||
5. 增加容器异常退出、端口未就绪、构建失败、超时与手动重启的可见状态。
|
||||
|
||||
**验收**
|
||||
|
||||
- 两个用户创建的项目存在于不同容器和不同工作区;
|
||||
- 修改 ECharts 配置文件后,预览地址显示修改结果;
|
||||
- FastAPI 模板的 `/api/health` 可通过前端相对路径访问;
|
||||
- 沙箱不能访问 Docker Socket、宿主机数据库和非白名单网络;
|
||||
- 空闲超时后容器被回收,项目文件仍可恢复。
|
||||
|
||||
### 阶段 4:产品能力收口
|
||||
|
||||
**工作项**
|
||||
|
||||
1. 在文件面板支持安全的文件查看、编辑、保存、目录树刷新与下载;限制文件大小和可编辑扩展名。
|
||||
2. 在对话中增加模板选择和能力提示:大屏、流程图、海报、可选 FastAPI。
|
||||
3. 为 Agent 增加依赖白名单、图表主题、中文字体、Mock 数据、接口契约等明确提示词。
|
||||
4. 增加“运行/停止/重启/删除沙箱”和日志面板;仅项目所有者或管理员有相应权限。
|
||||
5. 增加项目快照、模板版本记录和基础审计日志。
|
||||
|
||||
**验收**
|
||||
|
||||
- 从一句自然语言需求到可预览项目的完整链路可完成;
|
||||
- 用户可查看并手动修改文件,刷新后改动保留;
|
||||
- 页面覆盖 ECharts、流程图、响应式大屏和可选 FastAPI 接口;
|
||||
- 非拥有者不能读取、修改或预览另一用户的项目。
|
||||
|
||||
### 阶段 5:上线前安全与运行验收
|
||||
|
||||
**工作项**
|
||||
|
||||
1. 压测 6 个并发沙箱,并记录 CPU、内存、磁盘、容器启动时间、首屏预览时间和模型等待时间。
|
||||
2. 验证镜像保留策略、工作区配额、容器回收、数据库备份和 Harbor 备份恢复。
|
||||
3. 执行越权、路径穿越、命令注入、网络探测、资源耗尽和模型密钥泄露测试。
|
||||
4. 建立日常运维操作:新增基础依赖、构建镜像、发布模板、回滚镜像、清理工作区和审计查询。
|
||||
|
||||
**验收**
|
||||
|
||||
- 并发运行时既有 DeerFlow 等服务不受明显影响;
|
||||
- 任一沙箱异常退出不会影响其他项目或平台控制服务;
|
||||
- 备份可恢复项目元数据、工作区和 Harbor 镜像;
|
||||
- 仅开放计划中的入口端口,内部组件无不必要公网监听。
|
||||
|
||||
## 9. 首期资源与并发建议
|
||||
|
||||
由于该主机已有多项业务,首期不应按理论最大值分配:
|
||||
|
||||
| 组件 | 首期限制 |
|
||||
|---|---|
|
||||
| Harbor | 8 vCPU、16 GB 内存、500 GB 数据盘配额 |
|
||||
| OpenVibe 控制服务 | 4 vCPU、8-16 GB 内存 |
|
||||
| 单个 UI 沙箱 | 1 vCPU、3 GB 内存、10 GB 工作区 |
|
||||
| 单个 Fullstack 沙箱 | 1.5 vCPU、4 GB 内存、12 GB 工作区 |
|
||||
| 沙箱并发 | 先限制为 6;稳定后再逐步上调 |
|
||||
|
||||
当并发超过 10 个,或要面向更广泛用户开放时,应优先增加独立沙箱节点,而不是继续在本共享主机上加容器。
|
||||
|
||||
## 10. 实施顺序与交付物
|
||||
|
||||
建议按以下提交/交付顺序推进,每一步都可独立验收和回退:
|
||||
|
||||
1. `docs: confirm appbuilder network, ports and security baseline`
|
||||
2. `infra: add Harbor and versioned visual sandbox images`
|
||||
3. `infra: add offline templates and dependency locks`
|
||||
4. `feat: replace OpenVibe CloudBase persistence and model provider`
|
||||
5. `feat: add local Docker sandbox manager`
|
||||
6. `feat: add sandbox runner and path-based preview router`
|
||||
7. `feat: add visual-screen and FastAPI project workflows`
|
||||
8. `test: add sandbox isolation, preview and recovery coverage`
|
||||
9. `ops: add backup, cleanup, monitoring and rollout runbook`
|
||||
|
||||
达到阶段 3 即可交付“内网模型驱动、生成并预览大屏/海报页面”的 PoC;达到阶段 5 后再作为受控的内部多人平台发布。
|
||||
@@ -8,7 +8,7 @@ VITE_USE_REAL_DATA=true
|
||||
VITE_MOCK_COP_TASK_DETAIL=true
|
||||
|
||||
# 研判情报报告(大屏绘制智能体深链 selectSqReport)数据源:true 走本地样例假数据(真实接口未就绪时调试用);联调通过后置 false。
|
||||
VITE_MOCK_SQ_REPORT=false
|
||||
VITE_MOCK_SQ_REPORT=true
|
||||
|
||||
# 深链任务详情(cop-task-detail:任务名称/描述)数据源:true 走本地假数据(真实接口未就绪时调试用);联调通过后置 false。
|
||||
VITE_MOCK_COP_TASK_DETAIL=true
|
||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=true
|
||||
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
|
||||
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
|
||||
|
||||
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
|
||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
||||
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||
# CMZS后端接口
|
||||
|
||||
@@ -56,16 +56,4 @@
|
||||
# VITE_STATIC_WEBSITE_ONLY=
|
||||
# 部署级紧凑导航:true 时固定收起工作台左侧栏,并隐藏换肤与全量/简洁模式切换。
|
||||
# VITE_FORCE_COLLAPSED_SIDEBAR=false
|
||||
# 简介模式左侧菜单类型:与菜单管理页「简介模式」的类型 A / B 对应。两套网页分别填 A 或 B。
|
||||
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
|
||||
# VITE_COMPACT_MENU_TYPE=A
|
||||
# 问答页「极速」模式:true=在模式菜单显示并直连大模型;false 或未设=隐藏。
|
||||
# 部署后优先改 dist/runtime-config.js 的 VITE_TURBO_MODE_VISIBLE,刷新即可。
|
||||
# VITE_TURBO_MODE_VISIBLE=false
|
||||
# VITE_GITHUB_OAUTH_TOKEN=
|
||||
|
||||
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
|
||||
# true 时,普通登录后进入同一 Vite 应用中隔离的 /official/*。
|
||||
# 已指定 redirect 或 taskId/goPath 的登录仍保持原来的优先级。
|
||||
# 仅 .env.development 默认 true;其余环境保持 false。
|
||||
# VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=false
|
||||
VITE_BACKEND_BASE_URL=http://10.19.200.5:2026
|
||||
VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api
|
||||
|
||||
# 内网环境保持当前定制版默认入口。
|
||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
||||
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||
# CMZS后端接口
|
||||
|
||||
@@ -14,9 +14,6 @@ VITE_MOCK_SQ_REPORT=false
|
||||
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
|
||||
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
|
||||
|
||||
# 线上保持当前定制版默认入口。
|
||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
||||
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||
# CMZS后端接口
|
||||
|
||||
@@ -17,9 +17,6 @@ VITE_MOCK_COP_TASK_DETAIL=false
|
||||
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
|
||||
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
|
||||
|
||||
# SIT 环境保持当前定制版默认入口。
|
||||
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
|
||||
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||
# CMZS后端接口
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# 我的项目
|
||||
|
||||
入口:全量与简洁导航中的「我的项目」,路由 `/page/strategy/qa/projects`。旧地址 `/page/workspace/projects` 仅保留兼容跳转,不再展示独立入口或旧页面。
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 新建项目,选择普通问答、策略优选或方案生成。三类项目独立保存和展示,方案生成不会复用或标记策略优选项目。
|
||||
2. 选择项目默认智能体,填写目标、约束等项目说明。新建项目对话时仍可换用另一个可访问的智能体。
|
||||
3. 创建后进入项目工作区。普通问答项目可包含多条独立问答;方案项目可包含多条方案记录。最近对话根据 `analysis_project_mode` 回到普通问答分组或方案项目。
|
||||
4. 方案生成可从「工作流配置 · 报告结构」选择、启停、编辑、删除结构,选择后自动填充项目报告结构配置;也可直接编辑大纲及固定/弹性约束。
|
||||
5. 策略智能项目保存默认 Strategy Lens;每轮依次进行问题重构、各 Lens 独立分析、策略辩论和方案生成,并分为「意图理解」「多视角策略分析」「完整策略方案」三栏。右栏以文档形式给出执行摘要、目标原则、分阶段行动、资源配置、风险应对、评估复盘、形成依据与备选策略切换条件;原始对话和引用仍可切换查看。
|
||||
6. 策略智能对话底部提供改变约束、增删视角、重新推演和挑战当前策略快捷动作;项目主页面可直接添加或删除 Lens。
|
||||
|
||||
模式在创建后不可修改,前后端都不提供修改入口。项目、对话归属和智能体访问权限在服务端校验;方案结构及每条对话所选智能体也以服务端配置为准。
|
||||
|
||||
## 代码与运行
|
||||
|
||||
- `src/pages/AnalysisProjectsPage.tsx`:普通问答与方案项目的新建、设置、历史和工作台布局。
|
||||
- `src/strategy-selection/StrategyProjectsPage.tsx`:「我的项目」三类项目列表及创建入口。
|
||||
- `src/pages/analysis-projects/ProjectConversation.tsx`:现有聊天、研究进度、方案画布的项目级组合。
|
||||
- `src/core/analysis-projects/api.ts`:Gateway 项目 API 与按账号隔离的缓存。
|
||||
- 后端:`/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 存储、跨用户隔离、禁止修改模式、运行时覆盖伪造模式与知识库、更新配置、对话归属与失败清理、固定方案模板及知识库资料进入研究管线。
|
||||
|
||||
部署前还需使用实际知识库和模型验证三类问答、流式断线续接及方案后续修改。接口模拟与单元测试不能替代真实模型联调。
|
||||
|
||||
## 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 直接下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
|
||||
- 资料库把已完成的策略优选综合方案作为独立「策略生成」来源展示,并标注具体策略项目名称;条目可回到策略项目第三步,也可直接下载 Markdown/Word。普通策略项目线程生成的文件沿用原文件下载链路并显示同类来源标识。
|
||||
- 方案生成使用独立项目和固定报告结构,通过 `deep_research_report` 直接生成并打开方案画布;策略优选项目不再出现在方案生成分类中。
|
||||
- 知识库管理页读取上游最新目录,避免远端删除后命中旧缓存;检索调用仍共享短期目录缓存。
|
||||
- 普通用户访问尚未初始化的助手知识库时显示明确空态及刷新按钮。
|
||||
|
||||
验证:后端相关回归 273 项、前端策略/项目回归 31 项通过;真实 BAAI/bge-m3 编码、指定库向量检索与草稿隔离通过;本地隔离服务的问答项目创建/分组、真实问答、方案整理、助手库创建/编辑/文件入库/向量检索/详情均通过。完整验收记录见仓库 `docs/ji-knowledge-projects-acceptance.md`。不在源码或文档中保存接口密钥。
|
||||
@@ -0,0 +1,198 @@
|
||||
# MAF-R0 阶段 0 交付文档:旧会商交互基线与组件映射
|
||||
|
||||
> 状态:已完成
|
||||
> 更新时间:2026-08-03
|
||||
|
||||
---
|
||||
|
||||
## 1. 实验原型隐藏说明
|
||||
|
||||
### 已完成的操作
|
||||
|
||||
- `/page/maf/*` 路由(`MafSessionsPage` + `MafRunDetailPage`)已从正式菜单移除。
|
||||
- `workspace-nav-chat-list.tsx:199` 保留了墓碑注释:`{/* [MAF-R0] 实验原型已从正式菜单隐藏,仅保留路由供开发诊断:/page/maf */}`
|
||||
- `MafRoutes.tsx` 文件头部标注为 `[MAF-R0 实验原型 — 仅供开发诊断,已从正式菜单移除]`。
|
||||
- 该原型仅可通过手动输入 URL `/page/maf` 访问,不在任何侧边栏、导航菜单或工作区入口中出现。
|
||||
- 停止在该原型上新增业务功能。
|
||||
|
||||
### 原型文件位置
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `src/pages/MafRoutes.tsx` | 路由定义(index → MafSessionsPage, runs/:runId → MafRunDetailPage) |
|
||||
| `src/maf-roundtable/pages/MafSessionsPage.tsx` | 会话列表页(642 行) |
|
||||
| `src/maf-roundtable/pages/MafRunDetailPage.tsx` | 单 run 工作区(195 行) |
|
||||
| `src/maf-roundtable/` | 完整模块(33 文件:api/hooks/components/lib/tests) |
|
||||
| `vitest.maf.config.ts` | MAF 专用测试配置 |
|
||||
| `package.json` → `test:maf` | MAF 测试脚本 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 旧会商 8 类页面状态交互地图
|
||||
|
||||
旧会商入口:`src/roundtable-planning/pages/RoundtablePlanningPage.tsx`(4376 行,三步合一)。
|
||||
|
||||
### 状态 1:空白 / 新建
|
||||
|
||||
- 触发:点击「新建会商」或首次进入。
|
||||
- 表现:中心区域显示欢迎语 + 业务链选择入口 + 底部输入框。左侧历史列表为空或显示已有草稿。
|
||||
- 关键组件:`RoundtablePlanningPage`(currentStep=1 初始态)、`Step1Panel`(空消息列表 + 输入栏)。
|
||||
- 数据:`useDraftPersistence` 创建新草稿记录。
|
||||
|
||||
### 状态 2:历史切换
|
||||
|
||||
- 触发:点击左侧草稿列表中的某一条。
|
||||
- 表现:加载该草稿的 Step1/Step2/Step3 快照,恢复到对应步骤。
|
||||
- 关键组件:`useRoundtableDrafts`(列表)、`useDraftPersistence`(加载 + hydrate)。
|
||||
- 数据:`GET /api/roundtable-drafts/{id}` 或 `GET /api/roundtable-task-drafts/{id}`。
|
||||
|
||||
### 状态 3:用户提问
|
||||
|
||||
- 触发:用户在 Step1 输入框输入任务文本并点击发送。
|
||||
- 表现:用户气泡立即插入,随后出现 `MessageLoadingPlaceholder`(spinner + 跳动点),等待 SSE 首帧。
|
||||
- 关键组件:`Step1Panel`(输入栏 → 发送)、`MessageBubble`(用户气泡)、`MessageLoadingPlaceholder`。
|
||||
- 数据:`POST /api/intent/init` → `POST /api/intent/stream`(SSE)。
|
||||
|
||||
### 状态 4:意图澄清
|
||||
|
||||
- 触发:总控/意图智能体判断信息不足,返回 `ask_clarification` 帧。
|
||||
- 表现:对话流中嵌入 `IntentClarificationCard`(单选/多选 + 自定义输入),用户选择后提交。
|
||||
- 关键组件:`IntentClarificationCard`(选项卡)、`Step1Panel`(澄清提交逻辑)。
|
||||
- 数据:SSE 帧 `phase=clarification`,用户回答后继续 `POST /api/intent/stream`。
|
||||
|
||||
### 状态 5:总控派活
|
||||
|
||||
- 触发:Step1 完成(INTENT_READY)→ 进入 Step2 → 总控输出派活计划。
|
||||
- 表现:总控气泡显示派活计划(谁做什么),`ParallelSeatStrip` 显示参与席位头像条(running/done/error 徽章)。
|
||||
- 关键组件:`Step2Panel`(派活叙事)、`ParallelSeatStrip`(席位条)、`MessageBubble`(总控气泡)。
|
||||
- 数据:`POST /api/multi-agent/init`(SSE,建立 thread map)→ `POST /api/multi-agent/run/stream`。
|
||||
|
||||
### 状态 6:席位发言
|
||||
|
||||
- 触发:总控派活后,各席位按序/并行执行。
|
||||
- 表现:每个席位的气泡流式出现(`StreamingCursor` 闪烁尾标),工具调用显示为 `MessageStepsCard`(ChainOfThought 折叠卡)。
|
||||
- 关键组件:`MessageBubble`(席位气泡 + avatar)、`StreamingCursor`、`MessageStepsCard`(工具步骤)、`DialogueSpeedMeter`(tok/s)。
|
||||
- 数据:SSE delta 帧按 message id 合并。
|
||||
|
||||
### 状态 7:审批
|
||||
|
||||
- 触发:席位执行外部写操作,WriteGuard 拦截并请求审批。
|
||||
- 表现:对话流中嵌入审批卡(工具名 + 风险等级 + 摘要),用户点击批准/拒绝。
|
||||
- 关键组件:MAF 原型中为 `MafApprovalCard`;旧会商中审批通过 `roundtable-jobs` 的 clarify-resume 机制。
|
||||
- 数据:`POST /api/maf-roundtable/approvals/{id}/decision`(MAF)或 `POST /api/roundtable-jobs/{id}/clarify-resume`(旧)。
|
||||
|
||||
### 状态 8:完成 / 失败恢复
|
||||
|
||||
- 触发:所有席位完成 + 总控收束 → Step3;或 SSE 错误帧 / 网络断开。
|
||||
- 表现(完成):Step3 显示最终报告(Markdown 总结 / HTML 看板 / 大屏),可追问。
|
||||
- 表现(失败):错误提示 + 恢复按钮;后台任务可挂起/恢复。
|
||||
- 关键组件:`Step3Panel` → `HighFidelityReport`(Report/Dashboard/Summary/FileBrowse 模式);失败时 `RoundtableJobNotifier`。
|
||||
- 数据:`useStep3Summary` / `useStep3Report` / `useStep3Dashboard`;失败恢复 via `POST /api/roundtable-jobs/{id}/resume`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 旧组件 → 新 MAF view-model 映射表
|
||||
|
||||
| 旧组件 / hook | 新 MAF 对应 | 映射说明 |
|
||||
|---|---|---|
|
||||
| `RoundtablePlanningPage`(三步壳) | `MafRoundtableConversationPage` | 不再有步骤切换;单一连续时间线。 |
|
||||
| `Step1Panel`(意图问答) | `ConversationTimeline` + `ClarificationCard` | 意图理解变为 `manager.intent` 事件卡。 |
|
||||
| `Step2Panel`(研讨对话流) | `ConversationTimeline` + `SeatMessageBubble` | 席位发言由 SSE `agent.message.*` 驱动。 |
|
||||
| `Step3Panel` / `HighFidelityReport` | `FinalResultCard` | 结论作为时间线中的结果卡。 |
|
||||
| `MessageBubble` | `SeatMessageBubble` / 总控气泡 | 重写为 MAF view model;不复用旧 props。 |
|
||||
| `MessageStepsCard` | `ToolProgressCard` | 工具过程由 `tool.started/progress/completed` 事件驱动。 |
|
||||
| `IntentClarificationCard` | `ClarificationCard` | 由 `clarification.required` 事件触发。 |
|
||||
| `ParallelSeatStrip` | `ParticipantStrip` | 状态改由 `manager.dispatch` + `seat.delivery.submitted` + `manager.review.decision` 驱动。 |
|
||||
| `useStep1Intent` | 删除 | 意图理解完全服务端化。 |
|
||||
| `useStep2Orchestration` | 删除 | 编排完全服务端化;前端只消费 SSE。 |
|
||||
| `useDraftPersistence` | `use-maf-conversations` | 历史改为 conversation 列表。 |
|
||||
| `useRoundtableDrafts` | `use-maf-conversations` | 同上。 |
|
||||
| `useStep3Summary/Report/Dashboard` | 删除 | 结论由服务端生成,前端只渲染结果卡。 |
|
||||
| `useAutoScroll` | 可复用(无业务依赖) | 纯 UI 滚动逻辑。 |
|
||||
| `useDialogueSpeedMetrics` | 可复用(无业务依赖) | 纯 UI 速度指标。 |
|
||||
| `MafApprovalCard`(原型) | `ApprovalMessageCard` | 重写为 conversation 时间线内卡片。 |
|
||||
| `MafEventTimeline`(原型) | 删除 | 不再暴露 run/event 管理后台。 |
|
||||
| `MafSeatStatusPanel`(原型) | `ParticipantStrip` | 合并到右侧席位条。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 禁止复用的旧业务 hook / API / 状态清单
|
||||
|
||||
以下模块 **严禁** 被新 MAF conversation 页面直接 import 或调用:
|
||||
|
||||
### 4.1 旧会商 API 客户端(`src/roundtable-planning/api/`)
|
||||
|
||||
| 文件 | 禁止原因 |
|
||||
|------|----------|
|
||||
| `multi-agent.ts` | 调用 `/api/multi-agent/*`,包含前端 thread map 和编排逻辑 |
|
||||
| `intent.ts` | 调用 `/api/intent/*`,前端驱动意图流 |
|
||||
| `drafts.ts` | 调用 `/api/roundtable-drafts`,旧草稿状态 |
|
||||
| `task-drafts.ts` | 调用 `/api/roundtable-task-drafts`,旧任务草稿 |
|
||||
| `roundtable-jobs.ts` | 调用 `/api/roundtable-jobs`,旧后台任务 |
|
||||
| `chains.ts` | 调用 `/api/roundtable-chains`,旧业务链 CRUD |
|
||||
| `recommend.ts` | 调用 `/api/recommend/stream`,旧推荐流 |
|
||||
| `flow-extract.ts` | 调用 `/api/flow-extract/*`,旧流程图提取 |
|
||||
| `ingest.ts` | 调用 `/api/flow-extract/ingest-eligibility` |
|
||||
| `dashboard-sessions.ts` | 调用 `/api/dashboard-sessions` |
|
||||
| `diagnostics-report.ts` | 调用 `/api/roundtable-diagnostics/report` |
|
||||
|
||||
### 4.2 旧会商业务 Hook(`src/roundtable-planning/hooks/`)
|
||||
|
||||
| 文件 | 禁止原因 |
|
||||
|------|----------|
|
||||
| `useStep1Intent.ts` | 前端驱动意图状态机 |
|
||||
| `useStep2Orchestration.ts` | 前端编排循环(thread map、派活、轮次控制) |
|
||||
| `useDraftPersistence.ts` | 旧草稿持久化逻辑 |
|
||||
| `useRoundtableDrafts.ts` | 旧草稿列表 |
|
||||
| `useStep3Summary.ts` | 前端驱动总结生成 |
|
||||
| `useStep3Report.ts` | 前端驱动看板生成 |
|
||||
| `useStep3Dashboard.ts` | 前端驱动大屏生成 |
|
||||
|
||||
### 4.3 旧会商业务状态类型
|
||||
|
||||
- `Step1Snapshot`、`Step2Snapshot`、`Step2Dialogue`、`StubAiMessage`、`ParallelSeatView`
|
||||
- `ThreadIdMap`、`ClarificationPayload`、`LeaderDispatch`、`StreamPhase`
|
||||
- `DraftRecord`、`DraftMeta`、`DraftStep1Snapshot`、`DraftStep2Snapshot`、`DraftStep3Snapshot`
|
||||
- `RoundtableJob`、`JobStatus`、`DispatchChainNode`、`StartJobPayload`
|
||||
- `IntentSummary`、`IntentFileRef`、`IntentStreamPhase`
|
||||
|
||||
### 4.4 MAF 原型中禁止继续扩展的模块
|
||||
|
||||
| 文件 | 禁止原因 |
|
||||
|------|----------|
|
||||
| `src/maf-roundtable/pages/MafSessionsPage.tsx` | 实验原型,不作为产品主界面 |
|
||||
| `src/maf-roundtable/pages/MafRunDetailPage.tsx` | 实验原型,暴露 run/session 管理 |
|
||||
| `src/maf-roundtable/hooks/use-maf-events.ts` | 原型轮询 hook |
|
||||
| `src/maf-roundtable/api/sessions.ts` | 原型 session CRUD |
|
||||
| `src/maf-roundtable/api/runs.ts` | 原型 run 管理 |
|
||||
| `src/maf-roundtable/api/events.ts` | 原型事件轮询 |
|
||||
| `src/maf-roundtable/lib/session-form.ts` | 原型表单逻辑 |
|
||||
|
||||
### 4.5 允许参考但必须重写的视觉组件
|
||||
|
||||
以下组件的 **交互模式和信息架构** 可作为参考,但新 MAF 页面必须使用独立实现(MAF view model + tdesign-react),不得直接 import:
|
||||
|
||||
- `MessageBubble.tsx`(气泡布局)
|
||||
- `MessageStepsCard.tsx`(工具步骤折叠)
|
||||
- `IntentClarificationCard.tsx`(澄清选项卡)
|
||||
- `ParallelSeatStrip.tsx`(席位头像条)
|
||||
- `ScrollToBottomButton.tsx`(回到底部按钮)
|
||||
- `Avatars.tsx`(头像 SVG)
|
||||
|
||||
### 4.6 可安全复用的无业务依赖工具
|
||||
|
||||
- `useAutoScroll.ts`(纯滚动逻辑)
|
||||
- `useDialogueSpeedMetrics.ts`(纯速度指标)
|
||||
- `src/lib/use-desensitize-subtree.ts`(DOM 脱敏,已标注多模块共享)
|
||||
- `markdown-components.tsx`(markdown 渲染配置,需确认无旧状态依赖)
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收确认
|
||||
|
||||
- [x] 旧会商 `RoundtablePlanningPage` 无代码行为变化(未修改任何 `src/roundtable-planning/` 文件)。
|
||||
- [x] `/page/maf/*` 原型已从菜单隐藏,仅保留路由供开发诊断。
|
||||
- [x] 8 类页面状态已记录,含触发条件、表现、关键组件和数据流。
|
||||
- [x] 旧组件到新 MAF view-model 的映射表已完成。
|
||||
- [x] 禁止复用的 hook/API/状态清单已明确。
|
||||
- [x] 任何人可根据本文档判断新版是否「像原页面」。
|
||||
@@ -0,0 +1,645 @@
|
||||
# MAF 多智能体会商升级版实施计划(V2:保留原会商体验,替换底层编排)
|
||||
|
||||
> 文档状态:**V2 重写,待按本计划重新实施**
|
||||
> 更新时间:2026-08-02
|
||||
> 本轮决策:原 `/page/maf/*` 的「会话列表 + run 详情 + 前端轮询」原型不再作为目标产品继续扩展。
|
||||
> 产品目标:新 MAF 会商是**原多智能体会商页面的底层升级版**,不是另造一套会商产品。
|
||||
|
||||
---
|
||||
|
||||
## 1. 本次重新确认的产品目标
|
||||
|
||||
用户认可的是现有多智能体会商的视觉语言和交互链路:左侧历史会话、中心对话流、输入框发问、意图理解、总控派活、席位发言、过程卡片、澄清与审批、最终结论。
|
||||
|
||||
本次升级只替换以下“看不见的发动机”:
|
||||
|
||||
- 旧的前端驱动编排、多个前端可见线程和旧会商作业,替换为 MAF Workflow + DeerFlow Agent Adapter;
|
||||
- 浏览器定时轮询,替换为一条可恢复的 SSE 对话事件流;
|
||||
- 用户手动创建/管理多个智能体线程,收敛为服务端按需创建和持久化的内部执行线程;
|
||||
- 保留现有工具、技能、MCP、沙箱、WriteGuard、审批和审计能力,并让它们在原有会商体验中自然出现。
|
||||
|
||||
**不能接受的结果:** 新页面没有对话列表、没有输入框、让用户先创建 session/run、需要用户选择或理解多个内部线程、靠多个定时 GET 拼出实时效果,或把审批/流程图做成脱离聊天主链路的独立后台页。
|
||||
|
||||
### 1.1 必须保留的用户体验
|
||||
|
||||
| 原会商能力 | 升级版要求 |
|
||||
|---|---|
|
||||
| 左侧会商历史、新建会商 | 保留。一个条目就是一个用户可见会商,不暴露 MAF session/run/thread。 |
|
||||
| 中央连续对话流 | 保留。用户消息、总控消息、席位发言、工具进度、澄清、审批、最终报告按时间顺序呈现。 |
|
||||
| 底部对话输入框 | 保留。用户可在首轮发起任务、在澄清时回答、在会商结束后追问/继续会商。 |
|
||||
| 意图理解 | 保留并服务端化。先识别“可直接会商 / 需要澄清 / 只回答问题 / 需要工具或写入”。 |
|
||||
| 总控智能体派活 | 保留。总控是唯一能选择席位、分派任务、收束轮次和生成结论的主体。 |
|
||||
| 业务链、席位与过程可视化 | 保留其信息架构和交互,不复用旧业务状态;数据改由 MAF 事件驱动。 |
|
||||
| 澄清、取消、恢复、审批 | 保留,并成为对话流中的卡片,而不是跳转到 run 管理页。 |
|
||||
|
||||
### 1.2 明确不保留的实现方式
|
||||
|
||||
- 不在浏览器端 `setInterval` 轮询 run、events、actions、approvals;
|
||||
- 不让浏览器创建或维护 seat thread、manager thread、thread map;
|
||||
- 不让用户先填写“经理、席位、模式、最大轮数”才能开始一次正常会商;
|
||||
- 不把 `MafSessionsPage` / `MafRunDetailPage` 当作新产品主界面继续堆功能;
|
||||
- 不调用旧的 `/api/roundtable-*`、`/api/multi-agent/*`、`roundtable_jobs`、旧草稿或旧会商状态;
|
||||
- 不修改旧 `RoundtablePlanningPage` 的业务状态机、历史数据和对外行为。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体架构与边界
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[用户:会商对话页] -->|POST 新建/发送消息| API[MAF Conversation API]
|
||||
API --> C[(MAF Conversation / Message)]
|
||||
API --> D[MAF Dispatcher]
|
||||
D --> W[MAF Workflow]
|
||||
W --> M[总控:意图、派活、收束]
|
||||
M --> S[服务端按需创建席位执行线程]
|
||||
S --> A[DeerFlow Agent Adapter]
|
||||
A --> T[工具 / 技能 / MCP / 沙箱]
|
||||
T --> G[WriteGuard + 审批]
|
||||
W --> E[(持久化会商事件)]
|
||||
E --> H[SSE Hub:重放 + 实时推送]
|
||||
H -->|一条 SSE 流| U
|
||||
```
|
||||
|
||||
### 2.1 前后端职责
|
||||
|
||||
| 层 | 职责 | 禁止事项 |
|
||||
|---|---|---|
|
||||
| 前端 | 呈现会商、发送用户消息、消费 SSE、执行用户明确点击的取消/审批/重试 | 编排席位、决定下一位发言者、创建内部线程、轮询事件。 |
|
||||
| MAF Conversation Service | 会话聚合、消息持久化、运行创建、SSE 重放、所有权校验 | 让浏览器传入 user_id、工具能力快照、目标键或内部幂等键。 |
|
||||
| MAF Workflow | 意图判断、总控派活、席位协作、交叉审查、结论、恢复 | 把模型输出直接当作可写外部系统的授权。 |
|
||||
| DeerFlow Adapter | 为总控/席位提供既有模型、工具、技能、MCP、沙箱和用户上下文 | 从 `packages/harness/deerflow/**` 导入 `app.*`。 |
|
||||
| WriteGuard | 风险识别、审批、目标租约、一次执行、审计 | 将密钥、target_key、idempotency_key 回传给浏览器或模型。 |
|
||||
|
||||
### 2.2 新旧隔离规则
|
||||
|
||||
1. 旧会商继续运行,旧路由、旧 API、旧数据表、旧任务深链全部不变。
|
||||
2. 新页面独立使用 `/api/maf-roundtable/*` 和 `maf_roundtable_*` 表;只允许参考旧页面的布局、文案、交互和通用视觉规范。
|
||||
3. 旧 `roundtable-planning/**` 中的业务 hook、草稿、Job、SSE 消费器和状态类型不能被新页面直接 import。
|
||||
4. 若需要复用旧组件的视觉效果,必须复制/迁移为 `maf-roundtable/legacy-compatible/**` 中的新组件,替换为 MAF view model;或抽到无业务依赖的公共展示组件。旧页面不得因此改变行为。
|
||||
5. 新旧页面都只能使用仓库本地依赖;UI 基础组件统一采用 **`tdesign-react`**,不接远程 UI、CDN 或外部运行时。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户可见的新版会商流程
|
||||
|
||||
### 3.1 首次进入
|
||||
|
||||
1. 用户点击独立菜单“MAF 会商(升级版)”,进入与旧会商相同层级的页面壳。
|
||||
2. 左侧显示自己的 MAF 会商历史和“新建会商”。新建只创建一个**用户可见会商**,不显示 session/run/thread 表单。
|
||||
3. 中心区域显示欢迎语、业务链/任务上下文入口和对话输入框。
|
||||
4. 用户输入任务后,前端立即插入本地的用户气泡,调用一次 `POST /messages`,然后消费同一会商的 SSE 流。
|
||||
|
||||
### 3.2 一次会商轮次
|
||||
|
||||
1. SSE 推送“正在理解任务”事件;页面以旧会商的过程卡/总控气泡表现。
|
||||
2. 若信息不足,推送 `clarification.required`,页面显示可回复的澄清卡;用户回答后仍通过输入框或卡片提交。
|
||||
3. 若可以执行,总控输出“派活计划”;页面以旧的席位条、派活卡和消息气泡展示。
|
||||
4. 每个席位的可见发言、工具进度、技能过程、交叉审查和总控收束都写入同一条时间线。
|
||||
5. 出现外部写操作时,SSE 推送审批卡。用户批准/拒绝后,后端恢复工作流,SSE 继续推送;前端不手工轮询 run 状态。
|
||||
6. 最终结论、事实账本、执行动作和报告作为对话流中的结果卡呈现;用户可以继续在同一会商输入追问。
|
||||
|
||||
### 3.3 “一个会商”与内部线程的关系
|
||||
|
||||
- 用户只认识 `conversation_id`;左侧历史、URL 和前端状态都以它为主键。
|
||||
- 一个 conversation 可以有多轮用户消息,每轮消息产生一个内部 `maf_run`。
|
||||
- `maf_session`、manager/seat thread、seat run 都是服务端内部执行身份。服务端使用既有 `ExecutionPrincipal` 绑定当前用户,并在真正需要某席位执行时懒创建/复用线程。
|
||||
- 浏览器只收到经过脱敏的消息和事件,不会收到内部 thread id、能力快照、目标键、幂等键、密钥或原始工具结果。
|
||||
|
||||
### 3.4 总控质量闸门:派活不是“发出即完成”
|
||||
|
||||
当前旧会商的问题不是没有派活,而是席位只说一两句话、格式和证据都不合格时,总控仍把它当作完成并直接进入下一步。新版 MAF 必须把总控从“消息转发者”升级为**每项席位交付的验收责任人**。
|
||||
|
||||
每一个由总控发布的席位任务都必须有服务端持久化的 `task_contract`:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `objective` | 本席位要回答的具体问题,不能只写泛泛角色描述。 |
|
||||
| `expected_deliverable` | 交付类型和结构,例如“风险清单 + 依据 + 优先级 + 建议”。 |
|
||||
| `acceptance_criteria` | 由总控给出的可验收标准:覆盖范围、最少关键点、证据/工具依据、格式与约束。 |
|
||||
| `required` | 是否为本阶段必需交付;必需项不能被静默跳过。 |
|
||||
| `max_rework_attempts` | 同一席位最多返工次数,默认 **2**(初稿之外最多两次返工),服务端配置且写入快照。 |
|
||||
| `fallback_policy` | 到达返工上限后的策略:改派、请求用户澄清,或带缺口受控推进。 |
|
||||
|
||||
#### 3.4.1 席位交付与总控验收状态机
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> assigned
|
||||
assigned --> delivering
|
||||
delivering --> submitted
|
||||
submitted --> manager_review
|
||||
manager_review --> accepted: 达到合同标准
|
||||
manager_review --> rework: 不合格且未到返工上限
|
||||
rework --> delivering: 带明确退回意见
|
||||
manager_review --> reassigned: 达到上限且可改派
|
||||
reassigned --> delivering
|
||||
manager_review --> exhausted: 达到上限且无合适改派
|
||||
exhausted --> controlled_gap: 记录缺口、风险与原因
|
||||
accepted --> [*]
|
||||
controlled_gap --> [*]
|
||||
```
|
||||
|
||||
验收规则如下:
|
||||
|
||||
1. 席位先提交结构化 `delivery`,至少包含结论、依据/证据、未解决项、自检结果;不能只输出“已完成”或一两句空泛结论。
|
||||
2. 进入 `manager_review` 前先执行确定性检查:空内容、最小有效长度、必须章节、必填字段、所需证据/工具引用、重复文本、错误 schema。确定性不通过时不允许直接 accepted。
|
||||
3. 总控随后根据 `task_contract` 做结构化审查,决策只能是 `accept`、`return_for_rework`、`reassign`、`ask_user`、`controlled_gap`,并必须写明 `reason` 和针对性修改要求。
|
||||
4. **总控的模型自评不是验收证据。** 席位自评分只作为参考;验收由确定性规则 + 总控审查 + 必要时交叉审查共同决定。
|
||||
5. 对同一席位,默认最多两次返工。每次打回都必须把“缺什么、怎样补、哪些原结论不可用”带入下一次任务上下文。
|
||||
6. 到达上限后绝不把不合格结果伪装成合格:总控应优先改派给一个合适备用席位(默认最多一次改派);仍无法满足时生成 `controlled_gap`,明确标记未覆盖问题、风险和对最终结论的影响,再受控进入下一阶段。
|
||||
7. 一个阶段的所有 `required` 任务必须处于 `accepted` 或 `controlled_gap` 等明确终态,阶段闸门才可通过。总控不能因为“轮数到了”或“已有部分回答”跳过未决必需项。
|
||||
8. 最终报告不得将 `controlled_gap` 的内容写成已验证事实;它必须在结论中的风险/待补信息部分可追溯显示。
|
||||
|
||||
这既满足“打回几次后不要死循环”的要求,也避免“超过次数就把垃圾答案强行推进”的错误行为。
|
||||
|
||||
#### 3.4.2 对话页必须可见的质量反馈
|
||||
|
||||
- 总控派活卡展示任务目标、交付要求和当前第几次尝试;
|
||||
- 席位提交后显示“总控审核中”,而不是立即标为完成;
|
||||
- 打回时以总控消息卡显示具体退回理由和补充要求;
|
||||
- 改派时显示原席位未通过和新席位承接范围,但不羞辱性展示内部评分;
|
||||
- 受控降级时显示“存在未解决缺口”,并在最终结果卡中保留风险提示;
|
||||
- 右侧席位条区分“执行中、待返工、已验收、已改派、存在缺口”,不能只显示“发过言”。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 与 SSE 契约(必须先定,前端不得猜测)
|
||||
|
||||
### 4.1 Conversation API
|
||||
|
||||
以下接口全部位于 `/api/maf-roundtable`,身份只从服务端鉴权上下文读取。
|
||||
|
||||
| 接口 | 用途 | 前端调用时机 |
|
||||
|---|---|---|
|
||||
| `GET /conversations` | 左侧历史列表(分页) | 首次进入、用户显式刷新、创建/删除后失效刷新;**无定时轮询**。 |
|
||||
| `POST /conversations` | 创建空白用户会商 | 点击“新建会商”。只接收 title、任务上下文等安全字段。 |
|
||||
| `GET /conversations/{id}` | 获取会商快照、权限和当前状态 | 进入会商或浏览器刷新时。 |
|
||||
| `GET /conversations/{id}/messages` | 获取历史消息分页 | 进入会商、向上滚动加载历史。 |
|
||||
| `POST /conversations/{id}/messages` | 原子写入用户消息并启动 MAF run | 用户点击发送或提交澄清。返回 `message_id`、`run_id`、`conversation_id`。 |
|
||||
| `POST /conversations/{id}/cancel` | 取消当前活跃 run | 用户显式点击取消。 |
|
||||
| `POST /approvals/{id}/decision` | 批准/拒绝写入动作 | 审批卡点击;成功后由后端恢复 workflow。 |
|
||||
| `POST /conversations/{id}/recover` | 显式恢复异常中断的活跃会商 | 仅在后端判定可恢复时展示。 |
|
||||
|
||||
不再让页面以“创建 session → 创建 run → GET events 循环”的方式驱动会商。原有低层 session/run 接口可以作为内部诊断 API 暂时保留,但从用户页面和导航移除。
|
||||
|
||||
### 4.2 SSE:唯一的实时通道
|
||||
|
||||
```text
|
||||
GET /api/maf-roundtable/conversations/{conversation_id}/stream?after_seq={last_seen_seq}
|
||||
Accept: text/event-stream
|
||||
Authorization: 与现有 fetch 客户端一致
|
||||
```
|
||||
|
||||
- 前端在进入一个 conversation 后只建立**一条** fetch-reader SSE 连接;切换会商、卸载页面、权限失效时 Abort。
|
||||
- 浏览器不能用定时 GET 代替 SSE。连接断开后采用指数退避重连,并携带最后已确认的 `after_seq`;不产生全量重复气泡。
|
||||
- `after_seq` 是 conversation 维度、服务端单调递增的序号,不能复用每个 run 独立的 `seq`。
|
||||
- 首次连接先由 `GET messages` 加载历史,再从该历史的最大 `seq` 订阅 SSE;服务端也必须支持重放,避免“历史请求和订阅之间”丢事件。
|
||||
- SSE 使用 `fetch` + `ReadableStream` 解析,以复用项目鉴权头;响应必须带 `Content-Type: text/event-stream`、`Cache-Control: no-cache`、`X-Accel-Buffering: no`,并每 15 秒发送心跳。
|
||||
- 服务端负责等待和推送。单 worker 可用进程内 conversation event hub;生产多 worker 部署前必须接入 Redis Pub/Sub 或等价分布式广播,不能退回浏览器轮询。
|
||||
|
||||
### 4.3 SSE 事件信封
|
||||
|
||||
所有事件统一为:
|
||||
|
||||
```json
|
||||
{
|
||||
"seq": 42,
|
||||
"event_id": "evt_...",
|
||||
"conversation_id": "conv_...",
|
||||
"run_id": "run_...",
|
||||
"type": "agent.message.delta",
|
||||
"occurred_at": "2026-08-02T12:00:00Z",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
允许的首批 `type`:
|
||||
|
||||
| 类型 | 对话页表现 |
|
||||
|---|---|
|
||||
| `conversation.snapshot` | 首帧状态同步,不重复插入消息。 |
|
||||
| `user.message.created` | 刷新/多标签同步时补齐用户气泡。 |
|
||||
| `manager.intent` | 总控“正在理解/已理解”的过程卡。 |
|
||||
| `clarification.required` / `clarification.resolved` | 澄清卡及其回答状态。 |
|
||||
| `manager.dispatch` | 总控派活卡、席位条状态更新。 |
|
||||
| `seat.delivery.submitted` | 席位提交交付,进入总控审核而非直接完成。 |
|
||||
| `manager.review.decision` | 展示验收、打回、改派或受控降级及其安全摘要。 |
|
||||
| `seat.rework.assigned` / `seat.reassigned` | 更新第几次返工、原/新席位和新的任务要求。 |
|
||||
| `stage.quality_gate` | 阶段质量闸门通过、阻塞或带缺口推进。 |
|
||||
| `agent.message.start` / `delta` / `complete` | 席位或总控流式气泡。 |
|
||||
| `tool.started` / `tool.progress` / `tool.completed` | 现有步骤卡/工具过程,不显示敏感参数。 |
|
||||
| `approval.required` / `approval.decided` | 嵌入对话流的审批卡。 |
|
||||
| `fact.confirmed` / `review.completed` | 事实/交叉审查摘要卡。 |
|
||||
| `run.status` / `run.error` / `run.completed` | 顶部状态、失败重试与最终结果。 |
|
||||
|
||||
事件 `data` 必须先经过现有 MAF 安全规范化和二次展示脱敏;前端不负责猜测工具风险或修复敏感数据。
|
||||
|
||||
---
|
||||
|
||||
## 5. 后端改造计划
|
||||
|
||||
### MAF-R0:冻结错误原型并建立旧会商基线
|
||||
|
||||
**难度:D2(中低)|负责人:前端/测试各 1 人|前置:无**
|
||||
|
||||
- 将现有 `/page/maf/*` 标记为“实验原型”,从正式菜单移除;不得在其上继续新增业务功能。
|
||||
- 记录旧会商的页面状态:空会商、发送后、意图澄清、总控派活、席位流式发言、审批、完成、失败恢复、历史切换。
|
||||
- 为每种状态保存可复现的测试数据与截图基线;截图只作为视觉验收参考,不作为数据契约。
|
||||
- 输出旧组件到新 MAF view-model 的映射清单,禁止直接搬旧 hook。
|
||||
|
||||
**验收:** 旧会商无行为回归;团队对“复制体验、替换编排”有书面映射表。
|
||||
|
||||
### MAF-R1:Conversation 聚合与消息持久化
|
||||
|
||||
**难度:D5(高)|负责人:后端资深 1 人|前置:R0**
|
||||
|
||||
新增/迁移 MAF 专用持久化模型:
|
||||
|
||||
- `maf_roundtable_conversations`:用户可见会商,至少包含 id、owner_user_id、title、task_context、安全状态、active_run_id、created_at、updated_at;
|
||||
- `maf_roundtable_messages`:用户消息、总控/席位消息、澄清/审批/结果卡的稳定历史表示;
|
||||
- `maf_roundtable_conversation_events`:conversation 维度严格递增的 `seq`、event_id、run_id、type、安全 data、created_at;
|
||||
- 现有 `maf_roundtable_sessions/runs/events/seat_runs/actions/approvals` 保留为底层执行与审计记录;conversation 与内部 session/run 显式关联。
|
||||
|
||||
要求:
|
||||
|
||||
1. 发送用户消息、创建/占用 run、写入首个用户消息事件必须在清晰的事务边界内完成;失败不留“消息已显示但未运行”的孤儿状态。
|
||||
2. 消息历史必须可在刷新后重建对话;不能只依赖内存或 SSE 临时帧。
|
||||
3. 所有读写继续 owner-only;管理员诊断只读不扩大写权限。
|
||||
4. 新迁移必须 SQLite/PostgreSQL/MySQL 可重复执行;旧 MAF 原型数据可保留,不强制伪造为新对话历史。
|
||||
|
||||
**验收:** 会话创建、连续发消息、刷新恢复、并发发送冲突、跨用户隔离、迁移幂等都有 API 和存储测试。
|
||||
|
||||
### MAF-R2:服务端 SSE Hub 与断线续传
|
||||
|
||||
**难度:D5(高)|负责人:后端流式/并发经验者 1 人|前置:R1**
|
||||
|
||||
- 新增 conversation stream 路由和 `ConversationEventHub`;Workflow/Dispatcher 的规范化事件落库后再发布,顺序是“先持久化、后通知”。
|
||||
- SSE 连接先按 `after_seq` 重放,再订阅实时事件;重放和订阅切换期间必须有序、不丢帧、不重复。
|
||||
- workflow 终态、审批等待、审批恢复、取消、失败重试都必须通过同一流通知。
|
||||
- 不允许每个 SSE 客户端创建数据库轮询循环。服务端可以阻塞等待 hub 通知,事件重放只在连接/重连时发生。
|
||||
- 写出单 worker 与多 worker 的实现边界;若部署配置多 worker,未接入分布式广播时健康检查必须明确告警并禁止该模式上线。
|
||||
|
||||
**验收:** 测试覆盖慢客户端、断线重连、`after_seq` 重放、同时打开两个标签、切换会商、取消、审批后恢复、进程内 hub 异常和终态自动关流。
|
||||
|
||||
### MAF-R3:总控意图理解与派活语义对齐
|
||||
|
||||
**难度:D5(高)|负责人:MAF/Agent 负责人 1 人|前置:R1,可与 R2 并行**
|
||||
|
||||
在现有 MAF Workflow 上增加用户对话入口状态,而不是让前端预先填 manager/seat/run:
|
||||
|
||||
1. 总控首先进行意图判断:普通问答、需要澄清、单席位任务、并行专家会商、链式业务会商、需要工具/写入。
|
||||
2. 总控产出受 schema 校验的派活计划;只有服务端可将计划转成实际席位执行。
|
||||
3. 每个计划项必须生成并冻结 `task_contract`(目标、交付结构、验收标准、必需性、返工上限和降级策略),席位的发言不等于交付完成。
|
||||
4. 席位完成后先做确定性完整性检查,再由总控做结构化验收;总控可接受、打回同一席位、改派、请求用户补充或记录受控缺口。所有决策、次数和理由持久化到 checkpoint 与审计事件。
|
||||
5. 默认初稿后最多两次同席位返工;到达上限优先改派一次,仍失败才以带风险标记的 `controlled_gap` 结束。严禁自动把不合格交付推进为合格事实。
|
||||
6. 阶段迁移需经过质量闸门:所有必需任务均有 `accepted` 或受控缺口终态;最终总结只能消费已验收事实,缺口必须显式传入风险章节。
|
||||
7. 席位集合来自业务链、可信能力快照和上下文;前端可呈现“建议/已参与席位”,但不能提交任意工具权限或 thread id。
|
||||
8. 总控每轮应吸收已确认事实、验收结果、退回原因和用户追问;弱模型无效派活、重复派活、无收敛时走确定性纠偏/终止策略。
|
||||
9. 已有 structured/group_chat、事实账本、交叉审查、审批 checkpoint、执行阶段身份均继续复用;只重构其输入输出为 conversation event。
|
||||
|
||||
**验收:** 用同一任务对照旧会商的意图澄清、总控派活、席位协作、打回/改派和收束结论;弱模型的短答、空答、错误 schema、缺证据、重复派活、非法席位、超轮次、返工上限和受控降级都有确定性保护测试。
|
||||
|
||||
#### R3 当前实施记录(2026-08-03)
|
||||
|
||||
后端已先完成 R3 的服务端核心骨架,作为 R1/R2 Conversation API/SSE 的后续:
|
||||
|
||||
- `app/maf_roundtable/dispatch_contracts.py` 提供确定性意图分类、服务端业务路线解析、schema 校验的 `MafDispatchPlan` 和弱模型防护;请求、模型输出都不能提供 agent id、thread id、tool metadata 或能力快照。
|
||||
- `MafRoundtableConfig` 增加 `default_manager_agent_id`、`default_seat_agent_ids`、`business_routes` 和 `max_seats_per_run`。仓库配置保持空路线;上线前必须由部署配置填入实际 agent catalog 中的精确 id,未知业务路线失败关闭。
|
||||
- 首条消息在同一事务内创建内部 session、服务端选定的 manager/seat participants 和冻结能力快照;澄清意图进入 `waiting_input`,不创建 run,用户补充信息后才允许重新 claim。
|
||||
- Conversation service 已接入现有 `MafRunDispatcher`/`WorkflowRunner`,持久化 `manager.intent`、`manager.dispatch` 和 run 状态桥接事件;现有结构化工作流的席位集合仍只来自服务端 participants,manager 文本不能扩席或改派非法席位。
|
||||
- V2 `POST /conversations/{id}/messages` 只返回 `message_id`、`conversation_id`、`seq`;`run_id` 仍是持久层/服务端内部关联字段,不返回给浏览器。
|
||||
- 新增 `tests/test_maf_phase3_conversation_dispatch.py`,覆盖未知业务路线、默认路线、澄清、完成状态桥接、重复/越权派发计划等确定性用例。R3 尚未整体验收完成:仍需完成新页面、完整真实 MAF Workflow E2E、质量门禁和与旧会商体验的浏览器对照验收。
|
||||
|
||||
### MAF-R4:将审批和外部写入嵌回对话主链路
|
||||
|
||||
**难度:D4(较高)|负责人:后端安全 + 前端各 1 人|前置:R2、R3**
|
||||
|
||||
- 保留 Phase 5 的 WriteGuard、能力快照、HMAC 目标键、租约、加密参数、一次执行和审计;不得为了 SSE 改弱任何安全边界。
|
||||
- `approval.required` 只携带白名单展示字段和安全摘要。审批决定后由后端自动续跑,前端只等待同一 SSE 流中的新事件。
|
||||
- 拒绝、过期、执行失败、unknown 均形成清晰对话卡和可恢复状态;不得把“未知结果”伪装成成功。
|
||||
- 行为审计可在最终结果区展开查看,但不能替代对话中的关键用户反馈。
|
||||
|
||||
**验收:** HTTP → MAF → ToolNode/outbox → 审批 → SSE → 前端卡片 的端到端测试;确保 target_key、idempotency_key、internal_canary、密钥和原始工具结果从不进入消息/API/SSE。
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端改造计划:复刻体验,替换状态源
|
||||
|
||||
### MAF-R5:新会商页面壳与历史列表
|
||||
|
||||
**难度:D4(较高)|负责人:熟悉旧会商的前端 1 人|前置:R0、R1**
|
||||
|
||||
新建 `src/maf-roundtable/conversation/**`,建议结构:
|
||||
|
||||
```text
|
||||
maf-roundtable/conversation/
|
||||
pages/MafRoundtableConversationPage.tsx
|
||||
components/
|
||||
ConversationSidebar.tsx
|
||||
ConversationTimeline.tsx
|
||||
ConversationComposer.tsx
|
||||
ManagerDispatchCard.tsx
|
||||
SeatMessageBubble.tsx
|
||||
ToolProgressCard.tsx
|
||||
ClarificationCard.tsx
|
||||
ApprovalMessageCard.tsx
|
||||
FinalResultCard.tsx
|
||||
ParticipantStrip.tsx
|
||||
hooks/
|
||||
use-maf-conversations.ts
|
||||
use-maf-conversation-stream.ts
|
||||
use-maf-conversation-reducer.ts
|
||||
api/conversations.ts
|
||||
lib/event-reducer.ts
|
||||
styles/maf-roundtable-legacy-compatible.css
|
||||
```
|
||||
|
||||
页面要求:
|
||||
|
||||
- 左侧 `ConversationSidebar` 的层级、历史列表、新建、激活态、删除/重命名交互对齐旧会商;
|
||||
- 中间 `ConversationTimeline` 使用连续消息流,保留旧会商的用户气泡、总控气泡、席位气泡和时间锚点;
|
||||
- `ConversationComposer` 常驻底部;发送、停止、澄清回答、追问都在该入口完成;
|
||||
- 右侧仅保留旧体验中真正有用的参与席位、当前阶段和业务链摘要,不再摆放 run/session 管理后台;
|
||||
- 使用本地 `tdesign-react` 组件实现按钮、输入、Dialog、Tag、Tabs 等基础交互,同时通过 scoped CSS 对齐旧视觉;
|
||||
- 视觉相似不等于 import 旧业务组件。旧组件仅作参考,数据类型、hook 和 API 必须是 MAF 专用实现。
|
||||
|
||||
**验收:** 不连接后端时,使用固定 MAF 事件样本可完整演示旧会商同等的 8 种关键状态;没有 session/run/thread 表单。
|
||||
|
||||
### MAF-R6:SSE 客户端、事件 reducer 与刷新恢复
|
||||
|
||||
**难度:D5(高)|负责人:前端流式经验者 1 人|前置:R2、R5**
|
||||
|
||||
- `use-maf-conversation-stream` 只维护一个连接、一个 cursor、一个 AbortController;不得含 `setInterval`、`refetchInterval` 或轮询 fallback。
|
||||
- reducer 以 `(conversation_id, seq)` 去重并排序,将原始事件转为稳定 view model;流式 delta 必须按 `message_id` 合并,不能用数组下标。
|
||||
- 切换会商时先 abort 旧流并增加 generation;旧 SSE 帧永远不能污染新会商。
|
||||
- 刷新时先加载消息历史,再以最后 seq 订阅;网络断开显示“正在重连”,重连成功不重复气泡。
|
||||
- 只有历史列表等非实时资源可在用户主动操作后调用 GET;不能靠后台刷新伪装实时性。
|
||||
|
||||
**验收:** 模拟 1 万条历史、跨页加载、慢旧流切换、新流先到、断线重连、重复帧、乱序帧、终态关流和多标签同步;所有案例必须组件级测试。
|
||||
|
||||
### MAF-R7:意图/派活/审批卡的旧体验兼容
|
||||
|
||||
**难度:D4(较高)|负责人:前端业务负责人 1 人|前置:R3、R4、R6**
|
||||
|
||||
- 用旧 `IntentClarificationCard`、`MessageStepsCard`、`ParallelSeatStrip`、`MessageBubble` 的交互作为视觉和信息架构参照,重新实现 MAF 版本。
|
||||
- 总控派活计划必须先显示“将由谁做什么”,席位开始/结束、工具过程和交叉审查要回到原有对话叙事中。
|
||||
- 席位“发言结束”只能显示为“已提交,待总控审核”;只有收到 `manager.review.decision=accept` 才能显示“已验收”。打回次数、改派和缺口必须清楚可见。
|
||||
- 审批卡需要同步防双击锁、二次脱敏、拒绝/通过后的“等待工作流继续”状态;不可另外跳转到 action 审计页。
|
||||
- 最终结论卡要区分“建议动作”“审批中动作”“已执行动作”“失败/未知动作”。
|
||||
- 继续追问时,页面不新建多个用户可见线程;后端根据 conversation 上下文决定是否创建新 run 或继续恢复。
|
||||
|
||||
**验收:** 产品人员可在不看技术术语的情况下完成“发起会商 → 澄清 → 观察派活 → 审批 → 查看结论 → 追问”全链路。
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试、上线与回滚
|
||||
|
||||
### 7.1 必须新增的自动化测试
|
||||
|
||||
| 类别 | 核心用例 |
|
||||
|---|---|
|
||||
| 后端事务 | 发消息/创建 run 原子性、并发发送、跨用户隔离、迁移幂等。 |
|
||||
| SSE 服务端 | 重放、顺序、心跳、取消、断线、审批恢复、多订阅者、终态关流。 |
|
||||
| Workflow | 意图澄清、总控派活、弱模型格式错误、重复派活、最大轮次、恢复。 |
|
||||
| 质量闸门 | 短答/空答、缺少必填章节、无证据、总控打回、两次返工上限、一次改派、受控缺口、阶段阻塞和最终报告不把缺口伪装成事实。 |
|
||||
| 安全 | SSE/API/历史消息不泄露敏感键和值;审批和一次写入仍保持现有 Phase 5 保证。 |
|
||||
| 前端 reducer | delta 合并、去重、乱序、切换代次隔离、重连 cursor。 |
|
||||
| 前端组件 | 输入发送、澄清、派活、审批锁、取消、失败恢复、历史切换、无轮询断言。 |
|
||||
| E2E | 从用户输入到真实 MAF Workflow、ToolNode/outbox、审批、最终结果的 SSE 全链路。 |
|
||||
| 回归 | 旧 `roundtable-planning`、旧 DeerFlow、AI 写作以及既有 MAF Phase 1–5 用例全部通过。 |
|
||||
|
||||
### 7.2 人工视觉验收
|
||||
|
||||
在同一浏览器尺寸下对照旧会商,至少核对:
|
||||
|
||||
1. 空白页和新建会商;
|
||||
2. 左侧历史/当前会商切换;
|
||||
3. 用户输入、流式总控消息、席位消息;
|
||||
4. 意图澄清和总控派活;
|
||||
5. 工具过程、审批、取消、失败恢复;
|
||||
6. 最终结论和连续追问;
|
||||
7. 窄屏滚动、长内容、断线重连和页面刷新。
|
||||
|
||||
### 7.3 灰度与回滚
|
||||
|
||||
- 新入口使用独立 feature flag,例如 `maf_roundtable.conversation_ui_enabled`;旧会商不受该开关影响。
|
||||
- 先仅向内部账号开放,记录连接数、重连率、事件延迟、工作流失败率、审批耗时和前端异常。
|
||||
- 出现问题时关闭新入口即可;不迁移或删除旧会商数据,也不需要回滚旧页面。
|
||||
- 当前错误原型可保留为仅开发者诊断路由,或在新版稳定后删除;它不应继续出现在用户菜单。
|
||||
|
||||
---
|
||||
|
||||
## 8. 分阶段任务分配、难度与交付顺序
|
||||
|
||||
本节是分工的唯一依据。难度定义:D1=很低(局部文案/样式),D2=低(明确边界的小模块),D3=中(跨 2–3 个模块),D4=较高(跨前后端或需回归),D5=高(并发、状态机、安全或 Agent 行为,必须由有经验者负责)。
|
||||
|
||||
### 阶段 0:冻结错误原型,提取旧会商交互基线(R0)
|
||||
|
||||
**难度:D2|建议人员:1 名熟悉旧会商的前端 + 1 名测试|依赖:无**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 将现有 `/page/maf/*` 的 session/run 详情原型从正式菜单隐藏,标注为开发诊断原型,停止继续加功能。
|
||||
2. 逐一记录旧会商的 8 类页面状态:空白/新建、历史切换、用户提问、意图澄清、总控派活、席位发言、审批、完成/失败恢复。
|
||||
3. 给每种状态保留可复现 mock 数据、页面截图和操作步骤;梳理旧组件到新 MAF view-model 的映射。
|
||||
4. 明确哪些只能参考视觉和交互,哪些旧业务 hook/API/状态禁止复用。
|
||||
|
||||
交付物:旧会商交互地图、截图基线、组件映射表、被隐藏的原型入口说明。
|
||||
|
||||
验收:旧会商没有代码行为变化;任何人可以根据基线判断新版是否“像原页面”。
|
||||
|
||||
### 阶段 1:建立用户可见 Conversation 与消息历史(R1)
|
||||
|
||||
**难度:D5|建议人员:1 名资深后端|依赖:阶段 0**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 新增 `maf_roundtable_conversations`、`maf_roundtable_messages`、`maf_roundtable_conversation_events`,设计 owner、标题、消息、conversation 全局序号、活跃 run 和安全状态字段。
|
||||
2. 建立 conversation 与现有 MAF session/run/seat-run 的内部关联;用户只得到 conversation id,不能看到内部 thread id。
|
||||
3. 实现会商历史列表、创建空白会商、读取会商快照、读取消息分页、发送用户消息五类 API。
|
||||
4. 发送消息时,用户消息持久化、run 创建/占用和首个事件必须有明确事务边界;并发发送、重复请求、失败回滚都要处理。
|
||||
5. 完成 SQLite/PostgreSQL/MySQL 迁移与 owner-only 权限测试。
|
||||
|
||||
交付物:迁移脚本、repository/service/router、API schema、后端事务与隔离测试。
|
||||
|
||||
验收:刷新后能从消息历史恢复完整会商;跨用户不可读写;失败不会留下“已显示但没有实际运行”的孤儿消息。
|
||||
|
||||
### 阶段 2:服务端 SSE 对话流、事件 Hub 与断线续传(R2)
|
||||
|
||||
**难度:D5|建议人员:1 名熟悉 FastAPI 异步流与并发的后端|依赖:阶段 1**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 实现 `GET /conversations/{id}/stream?after_seq=`,按 conversation 全局 seq 先重放、后订阅实时事件。
|
||||
2. 新建 `ConversationEventHub`;所有 workflow 事件先安全落库,再按顺序广播。禁止 SSE 客户端通过定时查库等待更新。
|
||||
3. SSE 实现心跳、连接取消、权限校验、终态关流、异常帧和断线后的 cursor 续传。
|
||||
4. 明确多 worker 部署方案:单 worker 可进程内广播;多 worker 必须 Redis Pub/Sub 或等价方案,未满足时不能灰度上线。
|
||||
5. 把 run 状态、总控派活、席位消息 delta、工具过程、审批、质量审核、最终结论全部映射为统一事件信封。
|
||||
|
||||
交付物:SSE router/hub、事件发布适配器、集成测试、部署说明。
|
||||
|
||||
验收:慢连接、断线重连、乱序/重复防护、多标签订阅、审批后续跑、取消和终态均不丢事件;前端不需要轮询任何实时资源。
|
||||
|
||||
### 阶段 3:总控意图理解、业务链选择与服务端派活(R3)
|
||||
|
||||
**难度:D5|建议人员:1 名 MAF/Agent 工程师|依赖:阶段 1;可与阶段 2 并行**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 将旧会商的意图理解能力迁到 MAF 总控入口:识别直接问答、需澄清、单席位、并行会商、链式业务会商、工具/写入请求。
|
||||
2. 总控根据业务链、用户上下文、可信能力快照选择席位,生成 schema 校验的派活计划;前端只展示,不参与编排。
|
||||
3. Dispatcher 在服务端按需创建/复用 manager 和 seat 的内部执行线程,并由 `ExecutionPrincipal` 绑定当前用户。
|
||||
4. 对弱模型增加确定性保护:非法席位、空派活、重复派活、无效 schema、超轮次、无法收束必须被拒绝、纠偏或终止。
|
||||
5. 保持既有工具、技能、MCP、沙箱、事实账本、交叉审查和 checkpoint 恢复可用。
|
||||
|
||||
交付物:总控状态 schema、workflow executor、派活事件、弱模型测试矩阵。
|
||||
|
||||
验收:用户只输入任务,就能看到“意图理解 → 总控派活 → 席位执行”;浏览器没有 thread map 和派活决策代码。
|
||||
|
||||
### 阶段 4:席位任务合同、总控验收、打回与强制收束(R3-Q)
|
||||
|
||||
**难度:D5|建议人员:1 名 Agent/MAF 工程师 + 1 名后端状态机工程师|依赖:阶段 3**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 总控发布任务时冻结 `task_contract`:具体目标、交付格式、验收标准、必需性、返工次数、改派与降级策略。
|
||||
2. 为每项席位任务持久化状态机:`assigned → delivering → submitted → manager_review → accepted/rework/reassigned/controlled_gap`。
|
||||
3. 席位提交后先做确定性质量检查:空答、过短、必填章节缺失、格式错、无证据、重复内容等均不能自动完成。
|
||||
4. 总控必须生成结构化验收决定和原因;不合格时打回并带精确补充要求。默认最多两次同席位返工,之后最多一次改派。
|
||||
5. 改派仍失败时只能产生 `controlled_gap`,标记风险、缺失项和最终报告影响;严禁伪装为成功。
|
||||
6. 增加阶段质量闸门:所有必需任务处于 `accepted` 或 `controlled_gap`,才允许进入交叉审查/下一阶段/最终总结。
|
||||
7. 将尝试次数、退回理由、验收结果和缺口写入 checkpoint、审计和 SSE。
|
||||
|
||||
交付物:任务合同模型、持久化表/迁移、质量状态机、总控审查 prompt/schema、事件和测试。
|
||||
|
||||
验收:席位只说一两句话、无依据或内容垃圾时会被明确打回;达到上限不会死循环,也不会被当作合格产出;最终报告可追溯所有缺口。
|
||||
|
||||
### 阶段 5:审批、WriteGuard 与外部动作嵌入对话流(R4)
|
||||
|
||||
**难度:D4|建议人员:1 名安全后端 + 1 名前端|依赖:阶段 2、3、4**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 保留现有 Phase 5 的可信能力快照、WriteGuard、目标租约、HMAC、加密参数、一次执行与审计,不允许为新页面降低安全性。
|
||||
2. 将工具开始、审批请求、批准/拒绝、执行成功/失败/unknown 变成安全 SSE 事件和对话卡。
|
||||
3. 审批点击只调用明确决策 API;后端负责恢复 checkpoint/workflow,浏览器只等待 SSE。
|
||||
4. 安全展示统一二次脱敏,禁止 target_key、idempotency_key、internal_canary、密钥、凭据和原始工具结果进入 API、消息、SSE 或 DOM。
|
||||
|
||||
交付物:事件映射、审批消息卡契约、端到端 ToolNode/outbox 测试。
|
||||
|
||||
验收:一次真实测试写入能在对话里经历“申请 → 审批 → 继续 → 结果”;重复点击或断线都不会重复外部写入。
|
||||
|
||||
### 阶段 6:复刻旧会商页面壳、历史列表和输入框(R5)
|
||||
|
||||
**难度:D4|建议人员:1 名熟悉旧会商代码的前端|依赖:阶段 0、1**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 新建 MAF 专用 conversation 页面、路由和独立 feature flag;不修改旧 `RoundtablePlanningPage`。
|
||||
2. 依据阶段 0 的映射,复刻左侧历史会话、新建会商、中心时间线、常驻输入框、参与席位区和业务链摘要的视觉与交互层级。
|
||||
3. 基础组件使用本地 `tdesign-react`,旧页面仅参考;需要的视觉组件在 MAF 目录重写或抽无业务依赖公共组件。
|
||||
4. 接上 Conversation API:创建、历史、切换、消息分页。此阶段不允许先做任何轮询实时逻辑。
|
||||
5. 删除新页面中暴露给用户的 manager/seat/max round/session/run 创建表单。
|
||||
|
||||
交付物:新页面壳、MAF 专用 view model、静态/接口 mock 组件测试、视觉对照截图。
|
||||
|
||||
验收:即使 SSE 未接通,用户也能看到与旧会商同类的历史、对话与输入体验;新旧业务状态零耦合。
|
||||
|
||||
### 阶段 7:前端 SSE 消费、对话 reducer 与过程交互(R6 + R7)
|
||||
|
||||
**难度:D5|建议人员:1 名前端流式工程师 + 1 名前端业务工程师|依赖:阶段 2、4、5、6**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 实现单连接 `use-maf-conversation-stream`:fetch-reader SSE、cursor、AbortController、指数退避重连和会商切换 generation 隔离。
|
||||
2. 实现 event reducer:按 `conversation_id + seq` 去重排序;按 message_id 合并 delta;刷新后历史与 SSE 无缝衔接。
|
||||
3. 重写旧会商对应的 MAF 版组件:总控意图卡、派活卡、席位条、席位气泡、工具步骤、澄清卡、审批卡、质量审核/打回卡、结果卡。
|
||||
4. 所有卡片在时间线内叙事,不另开 run/session 管理后台;继续追问仍使用同一输入框。
|
||||
5. 页面必须显示待审核、已验收、返工次数、改派、受控缺口和恢复状态,不能把“发过言”显示为“已完成”。
|
||||
6. 代码审查和测试中检查不存在 `setInterval`、`refetchInterval` 或事件轮询 fallback。
|
||||
|
||||
交付物:SSE hook、reducer、对话组件、组件/API mock 测试、无轮询静态检查。
|
||||
|
||||
验收:切换会商、断线重连、审批续跑、慢旧流回包、长会话翻页均不污染、不丢消息、不重复;整个会商过程只依赖 SSE 实时更新。
|
||||
|
||||
### 阶段 8:端到端验收、灰度和回滚演练(R8)
|
||||
|
||||
**难度:D4|建议人员:1 名测试/集成人 + 各阶段负责人配合|依赖:阶段 1–7**
|
||||
|
||||
具体要做:
|
||||
|
||||
1. 跑完后端事务/SSE/Workflow/质量闸门/WriteGuard 测试、前端 reducer/组件测试、旧会商与 DeerFlow 回归。
|
||||
2. 在真实浏览器按阶段 0 截图基线逐项对照,并完成用户任务:发起、澄清、派活、短答打回、两次返工、改派、审批、受控缺口、结论和追问。
|
||||
3. 内部账号灰度,观测 SSE 建连/重连、事件时延、派活/验收/返工次数、workflow 失败率、审批耗时和前端错误。
|
||||
4. 验证关闭 feature flag 后立即回到旧会商入口,旧数据和旧任务不受影响。
|
||||
5. 若需生产外部写入,补齐 UAT 系统、最小权限凭据、核验 API、补偿联系人和生产 `MAF_ACTION_SECRET_KEY`。
|
||||
|
||||
交付物:自动化结果、人工验收记录、灰度指标面板、回滚演练记录、上线清单。
|
||||
|
||||
验收:满足第 9 节全部条件后才允许标记开发完成。
|
||||
|
||||
### 8.1 可并行边界
|
||||
|
||||
- 阶段 2 与阶段 3 可在阶段 1 的 schema/API 定稿后并行,但二者必须共用同一事件信封。
|
||||
- 阶段 6 可在阶段 1 的 API mock 契约定稿后提前开始;不得自行发明事件字段。
|
||||
- 阶段 5 必须等待阶段 2、3、4 的安全事件与状态机定稿。
|
||||
- 阶段 7 必须等待阶段 2、4、6,不能以轮询临时替代 SSE。
|
||||
- 阶段 8 只做集成、验收和上线准备,不补写前面阶段缺失的核心设计。
|
||||
|
||||
建议主顺序:`阶段 0 → 阶段 1 → (阶段 2 + 阶段 3 + 阶段 6) → 阶段 4 → 阶段 5 → 阶段 7 → 阶段 8`。
|
||||
|
||||
---
|
||||
|
||||
## 9. 最终验收定义
|
||||
|
||||
只有同时满足以下条件,才可以说“本次 MAF 会商升级开发完成”:
|
||||
|
||||
- 新页面在视觉和主交互上可被用户认作原多智能体会商的升级版;
|
||||
- 页面具备历史会话、连续对话流、输入框、意图理解、总控派活、席位协作、澄清、审批、结果和追问;
|
||||
- 每个必需席位任务都有任务合同、总控验收、有限返工和改派/受控缺口处理;席位短答或垃圾产出不能自动被当作完成;
|
||||
- 浏览器没有会商事件/run/approval 的定时轮询;实时交互通过可重连 SSE 完成;
|
||||
- 用户不需要创建或管理多个智能体线程,内部线程由服务端按用户身份安全创建和恢复;
|
||||
- 新页面未调用旧会商业务 API/状态,旧会商零回归;
|
||||
- 工具、技能、MCP、沙箱、审批、WriteGuard、一次写入和审计都仍可用;
|
||||
- 自动化测试、SSE 故障测试、真实浏览器人工验收和旧功能回归全部通过;
|
||||
- 真实 UAT 写入系统、最小权限凭据、外部核验 API、补偿协议及生产 `MAF_ACTION_SECRET_KEY` 已按 Phase 5 前置条件完成。
|
||||
|
||||
在这之前,任何“会话/run 原型页面通过构建或轮询测试”的结果,都只能算技术验证,不算产品升级验收。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3 实施记录(当前代码基线)
|
||||
|
||||
本轮已补齐阶段 3 的服务端安全路由与真实 MAF Workflow 入口,范围仅针对新 MAF 会商;旧 DeerFlow、旧 roundtable 和 AI 写作行为不变。
|
||||
|
||||
### 配置与入口闸门
|
||||
|
||||
- 仓库默认 `maf_roundtable.enabled: false`。部署环境必须注入真实的 `default_manager_agent_id`、席位 ID 和业务路由后再开启,避免出现“入口显示正常但首条消息必然 503”的空配置。
|
||||
- 健康接口返回非敏感的 `conversation_dispatch_ready` 与 `conversation_dispatch_status`;新会商入口只有在配置路由可用且服务已启用时才开放。
|
||||
- 首条消息使用服务端可信能力快照校验 manager/seat 的可访问性;浏览器、会话请求和模型输出都不能提供或扩大 agent、thread、tool、capability 范围。
|
||||
- 配置模型会清理并校验 manager/seat ID,拒绝 manager 出现在 seat 列表、重复席位、超过 `max_seats_per_run` 的路由和非法业务 code。
|
||||
|
||||
### 受约束的总控派活
|
||||
|
||||
服务端先由业务链与可信 capability snapshot 生成候选席位集合。总控模型只能返回严格 JSON:`intent_type`、`selected_seat_agent_ids`、每席 `task_goal`/`delivery_requirements`、`execution_mode`、`convergence_path` 和 `reason`。Pydantic schema、服务端 allowlist 和顺序校验全部通过后,最终计划才会写入 checkpoint,并产生 `manager.dispatch` 审计/SSE 投影。
|
||||
|
||||
非法席位、重复席位、空任务、错误 schema、席位超限、缺少收敛路径或链式顺序错误,统一回退到服务端安全计划,不会扩大权限或启动越权席位。
|
||||
|
||||
四种结构化拓扑已固定为:
|
||||
|
||||
- `direct_answer`:manager-only Workflow,不创建专家席位任务;
|
||||
- `single_seat` / `tool_write`:一个可信席位;
|
||||
- `parallel_consultation`:至少两个可信席位并行;
|
||||
- `chain_consultation`:按服务端业务链顺序串行。
|
||||
|
||||
### 阶段 3 验证状态
|
||||
|
||||
已覆盖配置校验、候选路由、非法总控计划回退、manager-only 真实 `WorkflowBuilder` 拓扑、线程/执行阶段回归、旧 DeerFlow 回归和会话 API 回归。当前仍不能据此标记整个 MAF 项目或阶段 5 为“验收完成”;真实 UAT 写入系统、最小权限凭据、外部核验 API、补偿协议和生产 `MAF_ACTION_SECRET_KEY` 仍按阶段 5/8 前置条件保持阻塞。
|
||||
|
||||
### Stage 3 P0 dispatch correction
|
||||
|
||||
- New MAF conversations no longer classify user text with server keyword rules. The server first freezes only the business-route candidate set and trusted, redacted capability summary; `ManagerDispatchExecutor` then returns the strict intent-and-dispatch JSON contract.
|
||||
- The final plan is Pydantic-validated and allowlisted before it is checkpointed. Invalid JSON, illegal intent or seat, missing convergence, manager failure, and timeout all finish on the clarification branch; they do not create expert threads or start expert/tool work.
|
||||
- A conversation initially persists and binds the manager only. After a valid decision, only `selected_seat_agent_ids` are added to the participant store and receive internal threads.
|
||||
- Every non-clarification route, including `group_chat`, starts at the same manager dispatch node. Group speakers are limited to the verified selected seats; static candidate configuration cannot bypass the plan.
|
||||
@@ -1,15 +0,0 @@
|
||||
# 岗位会商沙箱文档
|
||||
|
||||
本目录存放**岗位会商文件预览沙箱**的对接与排障说明,独立于 `src/position-roundtable/docs/`,避免与模块内其它文档互相覆盖。
|
||||
|
||||
| 文档 | 说明 |
|
||||
| --- | --- |
|
||||
| [沙箱打开关闭与步骤条.md](./沙箱打开关闭与步骤条.md) | **详细版**:打开/关闭完整时序、步骤条关系、对接接口字段说明、必守规则、历史 bug 根因、排障手册、自测清单、接线示例与代码索引 |
|
||||
|
||||
模块总览仍见:`frontend-web/src/position-roundtable/README.md`。
|
||||
|
||||
建议读法:
|
||||
|
||||
- **从零对接新页面**:正文第 3、5、6、7、8、12、15、16 节
|
||||
- **线上排障**:第 13、14 节
|
||||
- **只要一页纸**:文末「附录 A」
|
||||
@@ -1,988 +0,0 @@
|
||||
# 岗位会商 · 沙箱打开 / 关闭与步骤条(详细对接与排障指南)
|
||||
|
||||
> **文档位置**:`frontend-web/docs/position-roundtable-sandbox/`
|
||||
> **读者**:下次重做或对接岗位会商页面的同事;线上「沙箱不实时 / 卡死 / 第二次不刷新」时的排障同学。
|
||||
> **最后依据**:当前仓库 `PositionRoundtablePage` + `PositionNodeChat` 实现(含临时名打开、外部 store、前沿节流、按 tool-call 重挂)。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [文档用途与读法](#1-文档用途与读法)
|
||||
2. [背景:为什么岗位会商要单独做沙箱](#2-背景为什么岗位会商要单独做沙箱)
|
||||
3. [用户可见的正确行为(验收标准)](#3-用户可见的正确行为验收标准)
|
||||
4. [整体架构与数据流](#4-整体架构与数据流)
|
||||
5. [职责划分:谁拥有什么状态](#5-职责划分谁拥有什么状态)
|
||||
6. [对接接口详解](#6-对接接口详解)
|
||||
7. [打开沙箱的完整步骤](#7-打开沙箱的完整步骤)
|
||||
8. [关闭沙箱的完整步骤](#8-关闭沙箱的完整步骤)
|
||||
9. [步骤条与沙箱的关系](#9-步骤条与沙箱的关系)
|
||||
10. [流式内容如何进入沙箱(性能关键)](#10-流式内容如何进入沙箱性能关键)
|
||||
11. [与普通对话页的对照](#11-与普通对话页的对照)
|
||||
12. [重做页面时的必守规则](#12-重做页面时的必守规则)
|
||||
13. [历史问题与根因(务必读)](#13-历史问题与根因务必读)
|
||||
14. [排障手册(按症状)](#14-排障手册按症状)
|
||||
15. [对接自测清单](#15-对接自测清单)
|
||||
16. [最小接线示例](#16-最小接线示例)
|
||||
17. [关键代码索引](#17-关键代码索引)
|
||||
18. [术语表](#18-术语表)
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档用途与读法
|
||||
|
||||
### 1.1 这份文档解决什么问题
|
||||
|
||||
岗位会商右侧「文件预览沙箱」看起来像普通对话页的产物预览,但实现路径完全不同。历史上这块反复出过:
|
||||
|
||||
- 沙箱不实时打开,要等全文写完才弹;
|
||||
- 打开了但内容冻住,生成完才一口气灌进去;
|
||||
- 整页卡顿,连消息列表文字都不实时;
|
||||
- 同轮「重新生成」第二次写入时,步骤条变了,沙箱却不动;
|
||||
- 第一次正常,关掉后再问就不实时。
|
||||
|
||||
这些 bug **特别容易在「重做页面 / 换布局 / 自己重写开沙箱逻辑」时复现**。本文把正确对接方式、时序、接口、禁区和排障步骤写全,方便下一位同事直接按文档接线,而不是再靠猜。
|
||||
|
||||
### 1.2 建议读法
|
||||
|
||||
| 你的目标 | 先读哪些章节 |
|
||||
| --- | --- |
|
||||
| 从零接一个新页面壳 | 第 3、5、6、7、8、12、15、16 节 |
|
||||
| 只改对话子组件探测逻辑 | 第 7、9、12、13 节 |
|
||||
| 线上排障 | 第 13、14 节,必要时回看第 10 节 |
|
||||
| 理解为什么不能用页面 state | 第 2、10、13 节 |
|
||||
|
||||
### 1.3 对照代码(以仓库为准)
|
||||
|
||||
| 角色 | 路径 |
|
||||
| --- | --- |
|
||||
| 页面壳(唯一沙箱宿主) | `frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx` |
|
||||
| 节点对话(探测 write_file、上报预览) | `frontend-web/src/position-roundtable/components/PositionNodeChat.tsx` |
|
||||
| 收口对话 | `frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx` |
|
||||
| 预览 UI(共享组件,勿分叉逻辑) | `frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx` |
|
||||
| 流式正文读取 | `frontend-web/src/core/artifacts/loader.ts` → `loadArtifactContentFromToolCall` |
|
||||
| 普通对话页对照(步骤条自动开) | `frontend-web/src/components/workspace/messages/message-group.tsx` |
|
||||
| 模块总览 | `frontend-web/src/position-roundtable/README.md` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 背景:为什么岗位会商要单独做沙箱
|
||||
|
||||
### 2.1 普通对话页怎么开沙箱
|
||||
|
||||
普通对话页(`ChatPage`)大致是:
|
||||
|
||||
```text
|
||||
ArtifactsProvider(页面级)
|
||||
└─ ChatBox / MessageList
|
||||
└─ message-group 渲染 write_file 步骤
|
||||
└─ 发现 path 且 autoOpen → artifacts.select + setOpen(true)
|
||||
└─ 右侧 ArtifactFileDetail 读同一套 ThreadContext
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 对话和预览共享同一套 `ThreadContext`(同一个 `useThreadStream`)。
|
||||
- 步骤条组件自己就能触发打开。
|
||||
- 流式 `content` 本来就在当前 React 树的 thread 里,不需要「把 thread 镜像到别的地方」。
|
||||
|
||||
### 2.2 岗位会商为什么不能照搬
|
||||
|
||||
岗位会商页面同时有:
|
||||
|
||||
- 左侧任务 / 意图 / 流程图;
|
||||
- 中间某个节点的对话(`PositionNodeChat`);
|
||||
- 右侧配置抽屉;
|
||||
- **以及一个页面级单例沙箱**,它是对话区的**兄弟节点**,不在 `PositionNodeChat` 的 ThreadContext 里面。
|
||||
|
||||
因此:
|
||||
|
||||
1. 沙箱拿不到节点对话内部的 `thread`,除非节点主动上报;
|
||||
2. 若把每次流式 thread 写进**页面级 `useState`**,整页(任务卡、意图、流程图、对话)都会按流式频率重渲染,主线程会被打爆;
|
||||
3. 页面必须自己决定「开 / 关 / 抑制自动再开 / 切节点时清理」,不能让多个子组件各自 `setOpen`。
|
||||
|
||||
所以岗位会商采用:
|
||||
|
||||
```text
|
||||
对话子组件探测 + 上报
|
||||
→ 页面壳统一打开唯一 ArtifactFileDetail
|
||||
→ 流式 thread 走外部 store,只让沙箱子树订阅
|
||||
```
|
||||
|
||||
### 2.3 和「多智能体会商 Step2 沙箱」的区别
|
||||
|
||||
仓库里还有 `frontend-web/docs/roundtable-step2-sandbox-progress.md`,那是另一套圆桌 Step2 沙箱进度文档,**不是**岗位会商这条链路。对接岗位会商时以本文为准。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户可见的正确行为(验收标准)
|
||||
|
||||
重做或对接完成后,下列行为必须全部成立(用户感知对齐普通对话页):
|
||||
|
||||
| # | 行为 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| A | 步骤条刚出现「写入文件」时,沙箱应已打开 | **不必等**步骤条上出现文件名 |
|
||||
| B | 沙箱源码边写边滚;消息列表也实时出字 | 不能整页卡死、不能「生成完一口气刷」 |
|
||||
| C | content 先于 path 时:先临时名打开,path 到了再改名 | 不闪屏、不重挂 |
|
||||
| D | 同轮第二次同路径 `write_file`(重新生成) | 沙箱清空并重新流式 |
|
||||
| E | 整轮结束后自动切 Markdown 预览 | 「正在生成,完成后自动预览」消失 |
|
||||
| F | 用户手动关闭后,同一次写入不再自动弹 | 下一轮新写入仍可自动开 |
|
||||
| G | 切岗位 / 切节点 / 新会话 / 切历史 | 沙箱关掉且不串内容 |
|
||||
|
||||
**不要误判的正常现象:**
|
||||
|
||||
- 流式期间沙箱停在「源码 / 编辑」视图,结束后才切「预览」——这是 `ArtifactFileDetail` 对 markdown/html 的既定行为,与普通对话页相同,**不是**打开时机 bug。
|
||||
- 步骤条长时间显示「写入文件」、没有路径,但沙箱已经打开并在滚动——content-first 模型下这是**预期**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 整体架构与数据流
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ PositionRoundtablePage(页面壳) │
|
||||
│ │
|
||||
│ state: sandboxArtifact | null │
|
||||
│ ref: sandboxArtifactRef / closedSandboxKeyRef / latestSnapshotRef │
|
||||
│ store: createSandboxThreadStore() ←── 禁止改成 useState │
|
||||
│ │
|
||||
│ handlePreviewArtifact(artifact) ←──────────────────────────────────┐ │
|
||||
│ handleNodeThreadSnapshot(...) ←───────────────────────────────┐ │ │
|
||||
│ closeSandbox() │ │ │
|
||||
│ │ │ │
|
||||
│ {sandboxOpen && ( │ │ │
|
||||
│ SandboxThreadProvider(store) │ │ │
|
||||
│ └─ ArtifactFileDetail key=sandboxCloseKey(...) │ │ │
|
||||
│ )} │ │ │
|
||||
└───────────────────────────────────────────────────────────────────│──│───┘
|
||||
│ │
|
||||
┌───────────────────────────────────────────────────────────────────│──│───┐
|
||||
│ PositionNodeChat / PositionBuiltinAgentChat │ │ │
|
||||
│ │ │ │
|
||||
│ useThreadStream → thread.messages / thread.isLoading │ │ │
|
||||
│ │ │ │ │
|
||||
│ ├─ MessageList → 步骤条(只展示 tool call) │ │ │
|
||||
│ │ │ │ │
|
||||
│ ├─ latestStreamingWriteFile(messages) │ │ │
|
||||
│ │ → onPreviewArtifact({..., auto:true}) ───────────────────┘ │
|
||||
│ │ │
|
||||
│ └─ useEffect → onThreadSnapshot({threadId, thread}) ───────────────┘
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
一句话:
|
||||
|
||||
- **谁打开**:对话子组件决定「该不该开、开哪次交付」;
|
||||
- **谁真正开面板**:页面壳;
|
||||
- **内容怎么更新**:子组件持续上报 thread 快照 → 页面壳 store → 只有沙箱子树重渲染。
|
||||
|
||||
---
|
||||
|
||||
## 5. 职责划分:谁拥有什么状态
|
||||
|
||||
### 5.1 页面壳必须拥有
|
||||
|
||||
| 状态 / 能力 | 为什么必须在页面壳 |
|
||||
| --- | --- |
|
||||
| `sandboxArtifact` | 唯一预览目标;切节点时要清 |
|
||||
| `closedSandboxKeyRef` | 用户关过后的抑制;跨子组件生命周期 |
|
||||
| `sandboxThreadStore` | 流式内容通道;不能进大页面 state |
|
||||
| `closeSandbox()` | 切岗位 / 切节点 / 新会话统一入口 |
|
||||
| 唯一 `ArtifactFileDetail` | 避免双实例、关闭失效、深度异常 |
|
||||
|
||||
### 5.2 对话子组件必须拥有
|
||||
|
||||
| 状态 / 能力 | 作用 |
|
||||
| --- | --- |
|
||||
| `autoPreviewRunArmedRef`(armed) | 只有本轮用户发送后才允许自动开 |
|
||||
| `autoPreviewBaselineCallKeysRef`(baseline) | 忽略发送前已有的历史 write_file |
|
||||
| `activeWriteFilePreviewRef` | 记录当前打开的 previewKey / path / toolCallId |
|
||||
| `autoPreviewedArtifactKeyRef` | 同一次 identity 去重,避免每 chunk 重复上报 |
|
||||
| `latestStreamingWriteFile` | 从 streaming messages 探测可预览的 write |
|
||||
| `onThreadSnapshot` 上报 | 把 thread 喂给页面壳 |
|
||||
|
||||
### 5.3 明确禁止
|
||||
|
||||
1. 在 `PositionNodeChat` 内再挂一套 `ArtifactFileDetail`。
|
||||
2. 让步骤条组件直接 `setOpen(true)` 作为岗位会商主路径(对照普通页可以,岗位会商不行)。
|
||||
3. 把 `onThreadSnapshot` 落到 `PositionRoundtablePage` 的 `useState`。
|
||||
4. 用「整个 threadId」做关闭抑制。
|
||||
5. 用「path 相同」复用 `previewKey`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 对接接口详解
|
||||
|
||||
### 6.1 `PositionArtifactPreview`
|
||||
|
||||
```ts
|
||||
type PositionArtifactPreview = {
|
||||
threadId: string; // 必填:LangGraph thread id
|
||||
path: string; // 必填:真实路径,或 write-file: 虚拟 URL,或临时文件名路径
|
||||
name: string; // 展示名(文件名)
|
||||
mimeType: string | null; // 如 text/markdown;帮助预览语言判断
|
||||
previewKey?: string; // 强烈建议:一次交付身份,推荐 `${messageId}:${toolCallId}`
|
||||
auto?: boolean; // true = 流式自动打开;缺省/false = 用户点击打开
|
||||
};
|
||||
```
|
||||
|
||||
字段说明:
|
||||
|
||||
| 字段 | 详细含义 |
|
||||
| --- | --- |
|
||||
| `threadId` | 沙箱用来匹配 `onThreadSnapshot` 的线程;也对关闭抑制键有贡献 |
|
||||
| `path` | 传给 `ArtifactFileDetail.filepath`。流式阶段通常是 `write-file:...` 虚拟 URL;落盘后的手动打开可以是真实 artifacts 路径 |
|
||||
| `name` | UI 展示用;临时名阶段可能是 `未命名文件.md` |
|
||||
| `mimeType` | 当 path 还不稳定时,帮助把语言判成 markdown/html,避免落成 TEXT |
|
||||
| `previewKey` | **一次交付的身份**。同一次 tool call 的 path 升级要保持不变;新的 tool call 必须变 |
|
||||
| `auto` | 自动打开才走关闭抑制;手动打开应清抑制 |
|
||||
|
||||
### 6.2 `onPreviewArtifact(artifact)` —— 对话 → 页面
|
||||
|
||||
页面壳 `handlePreviewArtifact` 当前逻辑顺序:
|
||||
|
||||
1. 校验 `threadId`、`path`,缺则 toast 并 return。
|
||||
2. 计算 `artifactKey = sandboxCloseKey(artifact)`。
|
||||
3. 若 `artifact.auto === true` 且 `closedSandboxKeyRef.current === artifactKey` → **抑制,直接 return**。
|
||||
4. 清 `closedSandboxKeyRef`(手动打开或新的自动打开)。
|
||||
5. `ArtifactsContext.select(path, true)` + `setOpen(true)`。
|
||||
6. 判断是否同一预览 / 是否仅 path upgrade:
|
||||
- `isSamePreview`:threadId + path + previewKey 全同 → 不重复 setState。
|
||||
- `isPathUpgrade`:previewKey 相同、path 不同 → 更新 artifact,**不要**再折叠右侧抽屉。
|
||||
7. 若不是 same preview:
|
||||
- `setSandboxArtifact(artifact)`;
|
||||
- **同步** `sandboxArtifactRef.current = artifact`(关键!flush 会立刻读 ref);
|
||||
- `sandboxSnapshotAppliedAtRef.current = 0`(允许立刻推首帧内容)。
|
||||
8. 若 store 里已有别的 thread 快照,先 `publish(null)`。
|
||||
9. 若 `latestNodeThreadSnapshotRef` 的 threadId 匹配,立刻 `queueSandboxThreadSnapshot`。
|
||||
10. 非 path upgrade 时 `setRightPanelOpen(false)`。
|
||||
|
||||
`sandboxCloseKey`:
|
||||
|
||||
```ts
|
||||
if (artifact.previewKey) {
|
||||
return `${artifact.threadId}:write:${artifact.previewKey}`;
|
||||
}
|
||||
return `${artifact.threadId}:${normalizePreviewArtifactPath(artifact.path)}`;
|
||||
```
|
||||
|
||||
`ArtifactFileDetail` 的 React `key` 必须用这个 key:
|
||||
**新 previewKey → 组件重挂 → 视觉上「清空再生成」。**
|
||||
|
||||
### 6.3 `onThreadSnapshot({ threadId, thread })` —— 对话 → 页面
|
||||
|
||||
每次 `thread` 引用变化(消息 chunk 到达)子组件都会调:
|
||||
|
||||
```ts
|
||||
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||
```
|
||||
|
||||
页面壳应:
|
||||
|
||||
1. 计算 `signature = sandboxThreadSignature(thread)`(用 isLoading、messages 长度、最近 write_file 的 path/content 长度/尾部等拼签名,用于去重)。
|
||||
2. 写入 `latestNodeThreadSnapshotRef`(即使沙箱还没开,打开瞬间也要用)。
|
||||
3. 若当前没有打开的沙箱,或打开的沙箱 threadId 不一致 → **只更新 ref,不 publish**。
|
||||
4. 若一致 → `queueSandboxThreadSnapshot`(前沿节流后 `store.publish`)。
|
||||
|
||||
### 6.4 虚拟 URL 与正文读取
|
||||
|
||||
流式阶段 path 形如:
|
||||
|
||||
```text
|
||||
write-file:${sourcePath}?message_id=${messageId}&tool_call_id=${toolCallId}&language=markdown
|
||||
```
|
||||
|
||||
`loadArtifactContentFromToolCall`:
|
||||
|
||||
1. 解析 URL 的 `message_id`、`tool_call_id`;
|
||||
2. 在 `thread.messages` 里找对应 AI message 的 tool call;
|
||||
3. 返回 `toolCall.args.content`。
|
||||
|
||||
结论:
|
||||
|
||||
- 沙箱跟的是 **tool call 身份**,不是磁盘文件是否已经写完;
|
||||
- 没有真实 path 也能显示正文;
|
||||
- 临时文件名只影响展示名和语言猜测。
|
||||
|
||||
### 6.5 对话子组件 → 页面壳的其它相关回调
|
||||
|
||||
| 回调 | 用途 |
|
||||
| --- | --- |
|
||||
| `onPreviewArtifact` | 打开 / 更新沙箱 |
|
||||
| `onThreadSnapshot` | 推送流式 thread |
|
||||
| 页面壳 `closeSandbox` 传给 `ArtifactFileDetail.onClose` | 用户点关闭 |
|
||||
|
||||
手动从流程图 / 产物列表打开时,也可以直接调 `handlePreviewArtifact`(通常不带 `auto`,或不带 previewKey)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 打开沙箱的完整步骤
|
||||
|
||||
### 7.1 武装(arm):只有「本轮用户提交」才允许自动开
|
||||
|
||||
在 `PositionNodeChat.handleSubmit` 真正 `sendMessage` 之前:
|
||||
|
||||
```text
|
||||
activeWriteFilePreviewRef = null
|
||||
autoPreviewBaselineCallKeysRef = writeFileCallKeys(thread.messages) // 历史 write 的 messageId:toolCallId
|
||||
autoPreviewRunArmedRef = true
|
||||
→ sendMessage(...)
|
||||
```
|
||||
|
||||
发送失败要把 `armed` 设回 `false`。
|
||||
|
||||
为什么要 baseline?
|
||||
|
||||
线程里往往已经有上几次问答写过的 `write_file`。如果只看「messages 里有没有 write_file」,一提问就会把旧文件再打开一次。baseline 的含义是:**只有这次提交之后新出现的 tool call 才能自动开沙箱**。
|
||||
|
||||
### 7.2 探测 `write_file`:不要等完整 path
|
||||
|
||||
`write_file` 参数是一整段 JSON 流,**键顺序由模型决定**。
|
||||
|
||||
| 模型吐参顺序 | 步骤条表现 | 错误做法的后果 |
|
||||
| --- | --- | --- |
|
||||
| path / description 先到,再 content | 很快显示描述和文件名 | 普通对话页常见;等 path 也能开 |
|
||||
| **content 先到,最后才 path** | 长时间只有「写入文件」,文件名很晚才出现 | 若硬等完整 path,沙箱会晚几十秒甚至一分钟 |
|
||||
|
||||
岗位会商报告类 agent **经常是第二种**。
|
||||
|
||||
`latestStreamingWriteFile` 规则:
|
||||
|
||||
1. 倒序扫描 AI messages 的 `tool_calls`;
|
||||
2. 工具名是 `write_file` 或 `str_replace`;
|
||||
3. **有完整扩展名的 path**(`hasCompleteArtifactExtension`)→ 用真实路径;
|
||||
4. **尚无可用 path,但 `write_file` 的 content 已有非空字符** → 临时文件名:
|
||||
- 正文头像 HTML → `未命名文件.html`
|
||||
- 否则 → `未命名文件.md`
|
||||
5. `str_replace` 必须等真实 path(它改已有文件);
|
||||
6. 组装:
|
||||
- `key = messageId:toolCallId`
|
||||
- `path = write-file:虚拟URL`(write_file)或真实 path(str_replace)
|
||||
- `mimeType`:markdown 则 `text/markdown`
|
||||
|
||||
为什么扩展名必须完整?
|
||||
|
||||
流式 path 会经历 `/a/b/report.m` → `/a/b/report.md`。若在 `.m` 时就当真实文件打开,预览语言可能被永久判成 TEXT。
|
||||
|
||||
### 7.3 上报 effect 的四个门闩
|
||||
|
||||
自动上报 `onPreviewArtifact` 前必须同时满足:
|
||||
|
||||
1. `thread.isLoading === true`
|
||||
2. 探测到 streaming artifact,且其 key **不在** baseline
|
||||
3. 已有 `previewThreadId`
|
||||
4. `autoPreviewRunArmedRef === true`
|
||||
|
||||
然后处理 previewKey:
|
||||
|
||||
```text
|
||||
若与 activePreview 是同一次 tool call(含 tool-start 前缀兼容)
|
||||
→ 复用旧 previewKey // 用于临时名 → 真实 path
|
||||
否则
|
||||
→ previewKey = 新的 messageId:toolCallId
|
||||
```
|
||||
|
||||
去重键:
|
||||
|
||||
```text
|
||||
`${previewThreadId}:${previewKey}:${sourcePath}`
|
||||
```
|
||||
|
||||
把 `sourcePath` 算进去,是为了:**同 previewKey 下临时名变成真实 path 时,还要再报一次**(path upgrade),但不会每来一个 content chunk 都报。
|
||||
|
||||
### 7.4 线程绑定竞态(不要误 disarm)
|
||||
|
||||
首轮对话常出现:
|
||||
|
||||
1. 用户发送(本地 draft threadId / 稍后 SDK 创建真实 id);
|
||||
2. 流已经开始、armed=true;
|
||||
3. 会话 API 把 `node.threadId` 从 `null` 写成真实 id。
|
||||
|
||||
这是「本节点绑定自己的新线程」,**不是**切节点。若绑定 effect 无脑清空 armed / baseline,首轮会永远开不了沙箱。
|
||||
|
||||
正确:识别 `isOwnThreadBinding`(同 nodeKey、旧 threadId 为空、新 threadId 等于当前 boundThreadId)时 **直接 return,不清 armed**。
|
||||
|
||||
### 7.5 path upgrade(改名)≠ 第二次交付
|
||||
|
||||
同一次 tool call:
|
||||
|
||||
```text
|
||||
T1 content 到达 → path=未命名文件.md, previewKey=msg:tc1 → 打开
|
||||
T2 真实 path 到达 → path=/mnt/.../报告.md, previewKey=msg:tc1 → 只改名
|
||||
```
|
||||
|
||||
页面壳看到相同 previewKey、不同 path → `isPathUpgrade`:
|
||||
|
||||
- 更新 `sandboxArtifact`;
|
||||
- **不**重挂(React key 仍基于 previewKey);
|
||||
- **不**再折叠右侧抽屉。
|
||||
|
||||
### 7.6 同路径二次写入(重新生成)
|
||||
|
||||
同一次问答里模型可能:
|
||||
|
||||
```text
|
||||
1) write_file → 步骤条「写入文件」→ 打开沙箱
|
||||
2) 再 write_file 同 path,description=「重新生成优化版…」
|
||||
```
|
||||
|
||||
这是**两次 tool call**,必须两个 previewKey。
|
||||
若错误地「path 相同就复用 previewKey」:
|
||||
|
||||
- 步骤条出现第二条;
|
||||
- 沙箱 React key 不变;
|
||||
- 用户看到「重新生成了,但沙箱没变化」。
|
||||
|
||||
正确:身份只看 tool call。
|
||||
|
||||
### 7.7 时序总图(content-first)
|
||||
|
||||
```text
|
||||
T0 用户发送 → armed=true,拍 baseline
|
||||
T1 步骤条出现「写入文件」(尚无 path)
|
||||
T1+ args.content 首字符到达 → 临时名打开沙箱,源码开始滚动
|
||||
… store 约每 160ms 推一次 thread 快照 …
|
||||
T2 args.path 拼完整(含 .md)→ path upgrade,标题改真实文件名(不重挂)
|
||||
T3 write 结束 / isLoading=false → 自动切 Markdown 预览
|
||||
```
|
||||
|
||||
### 7.8 时序总图(同轮二次写入)
|
||||
|
||||
```text
|
||||
T1 write_file#1 → previewKey=msg:tc1 → 打开沙箱 A
|
||||
T2 #1 结束(可能短暂切预览)
|
||||
T3 write_file#2(同 path,新 description)→ previewKey=msg:tc2
|
||||
→ key 变 → 组件重挂清空 → 跟 #2 重新流式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 关闭沙箱的完整步骤
|
||||
|
||||
### 8.1 用户手动关闭
|
||||
|
||||
`closeSandbox()` 应做:
|
||||
|
||||
1. `closedSandboxKeyRef = sandboxCloseKey(current)` —— 抑制**这一次**自动再开;
|
||||
2. 关闭 `ArtifactsContext.open`(若开着);
|
||||
3. 清节流定时器、pending 快照;
|
||||
4. `sandboxSnapshotAppliedAtRef = 0`;
|
||||
5. `sandboxArtifactRef = null` + `setSandboxArtifact(null)`;
|
||||
6. `sandboxThreadStore.publish(null)`。
|
||||
|
||||
抑制与 path upgrade 的关系:
|
||||
|
||||
- 关掉临时名面板后,同一次 tool call 升级真实 path **仍被抑制**(同一 previewKey)——符合「用户不要看这次」;
|
||||
- 下一次新的 `write_file`(新 previewKey)**可以**再自动开。
|
||||
|
||||
### 8.2 场景切换时强制关闭
|
||||
|
||||
以下操作应先调用 `closeSandbox()`,避免串台:
|
||||
|
||||
- 切岗位;
|
||||
- 切节点;
|
||||
- 切收口阶段;
|
||||
- 新建会话;
|
||||
- 切换历史会话。
|
||||
|
||||
是否写入 `closedSandboxKeyRef` 可按产品决定;至少必须清面板和 store。
|
||||
|
||||
### 8.3 错误的抑制粒度(历史坑)
|
||||
|
||||
若做成「关过一次就整个 thread 不再自动开」:
|
||||
|
||||
- 第一次实时开正常;
|
||||
- 用户关掉;
|
||||
- 再问一次 → 同 thread 被永久抑制 → 看起来像「第二次不实时 / 要等生成完」。
|
||||
|
||||
正确粒度:**单次交付的 `sandboxCloseKey`(含 previewKey)**。
|
||||
|
||||
---
|
||||
|
||||
## 9. 步骤条与沙箱的关系
|
||||
|
||||
### 9.1 步骤条从哪来
|
||||
|
||||
步骤条由 `MessageList` → `message-group.tsx` 渲染 tool call:
|
||||
|
||||
- 标题:优先 `args.description`;没有则 i18n 兜底「写入文件」;
|
||||
- 子行:有 `args.path` 才显示路径;
|
||||
- 普通对话页还会在「有 path + autoOpen」时自己 `select/setOpen`。
|
||||
|
||||
岗位会商里,步骤条**仍然只负责展示**;打开沙箱走 `onPreviewArtifact`,不要让两套逻辑抢。
|
||||
|
||||
### 9.2 对照表
|
||||
|
||||
| 你看到的步骤条 | 含义 | 沙箱应处于 |
|
||||
| --- | --- | --- |
|
||||
| 仅「写入文件」,无路径 | tool call 已开始;path/description 可能还没流到 | **应已打开**(临时名),源码在滚 |
|
||||
| 「写入文件」+ 路径 | path 已可用 | 已打开;若刚从临时名升级则只改名 |
|
||||
| 带自定义 description 的标题 + 路径 | 模型给了 description | 同上 |
|
||||
| 第二条「重新生成…」+ 同路径 | 新的 write_file(新 toolCallId) | **应重挂**并重新流式 |
|
||||
| 顶部「正在生成,完成后自动预览」 | 仍在 code 视图 | 正常,等 isLoading 结束切预览 |
|
||||
| 生成结束,出现文件卡片 / present_files | 工具结果已落 | 通常已是预览模式;可手动再点开 |
|
||||
|
||||
### 9.3 常见误解
|
||||
|
||||
**误解 1:**「步骤条出现文件名 = 打开沙箱的正确时机」
|
||||
→ 错。那是 path 到达的时机。content-first 时正确打开时机更早:content 首字节 / 工具调用已开始。
|
||||
|
||||
**误解 2:**「步骤条还在写『写入文件』,沙箱却开了,是 bug」
|
||||
→ 通常不是。两边数据源相同,但沙箱允许无 path 打开,步骤条标题可能还没 description。
|
||||
|
||||
**误解 3:**「等生成完从编辑切预览 = 沙箱打开晚了」
|
||||
→ 错。那是 `ArtifactFileDetail` 的 viewMode 策略。
|
||||
|
||||
---
|
||||
|
||||
## 10. 流式内容如何进入沙箱(性能关键)
|
||||
|
||||
### 10.1 为什么不能用页面 `useState` 存线程快照
|
||||
|
||||
错误写法:
|
||||
|
||||
```ts
|
||||
setSandboxThreadSnapshot(snapshot) // 页面级 useState
|
||||
```
|
||||
|
||||
后果:
|
||||
|
||||
- 消息 chunk 约每 30ms 一次,节流后仍约 6 次/秒;
|
||||
- 每次都重渲染整页:任务卡、意图结果、流程图、节点对话……;
|
||||
- 主线程占满 → 消息列表停绘、沙箱冻住;
|
||||
- 纯尾随 `setTimeout(160)` 会被饿死,十几秒不 flush。
|
||||
|
||||
这同时解释了两个历史现象:
|
||||
|
||||
1. 「沙箱打开了但内容不更新」;
|
||||
2. 「修完节流后整页更卡、消息也不实时」——因为节流终于开始真正 setState 了。
|
||||
|
||||
### 10.2 正确做法:外部 store + 子树订阅
|
||||
|
||||
```ts
|
||||
createSandboxThreadStore()
|
||||
subscribe / getSnapshot / publish
|
||||
|
||||
SandboxThreadProvider
|
||||
useSyncExternalStore(store.subscribe, store.getSnapshot)
|
||||
→ ThreadContext.Provider value={{ thread, isMock:false }}
|
||||
→ ArtifactFileDetail
|
||||
```
|
||||
|
||||
页面壳本身**不**因 snapshot 变化而重渲染;只有 Provider 及其子树重渲染。
|
||||
|
||||
### 10.3 节流必须是前沿(leading-edge)
|
||||
|
||||
`SANDBOX_SNAPSHOT_MIN_INTERVAL_MS = 160`:
|
||||
|
||||
```text
|
||||
queue(snapshot):
|
||||
pending = snapshot
|
||||
if 距上次 publish >= 160ms:
|
||||
立刻 flush(同步 publish)
|
||||
else:
|
||||
若没有在途 timer,则 setTimeout(剩余时间) 做补尾
|
||||
flush:
|
||||
清 timer
|
||||
校验 pending.threadId === sandboxArtifactRef.threadId
|
||||
publish(pending)
|
||||
```
|
||||
|
||||
**不要**改回:
|
||||
|
||||
```ts
|
||||
if (timer != null) return;
|
||||
timer = setTimeout(flush, 160);
|
||||
```
|
||||
|
||||
那是纯尾随;忙主线程下会假死。
|
||||
|
||||
### 10.4 signature 去重
|
||||
|
||||
`sandboxThreadSignature` 用最近 write_file 的 path、content 长度、content 尾 32 字等拼签名。
|
||||
store.publish 时若 threadId+signature 未变,则不通知订阅者,减少无效渲染。
|
||||
|
||||
### 10.5 打开瞬间的首帧
|
||||
|
||||
`handlePreviewArtifact` 里往往会立刻 `queueSandboxThreadSnapshot(latestSnapshot)`。
|
||||
此时 React 的 `setSandboxArtifact` 可能还没提交,若 flush 只读旧的 `sandboxArtifactRef`,会因 threadId 不匹配把首帧丢掉。
|
||||
|
||||
所以打开新预览时必须:
|
||||
|
||||
```ts
|
||||
sandboxArtifactRef.current = artifact; // 同步
|
||||
sandboxSnapshotAppliedAtRef.current = 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 与普通对话页的对照
|
||||
|
||||
| 点 | 普通对话页 | 岗位会商 |
|
||||
| --- | --- | --- |
|
||||
| 谁触发打开 | `message-group` 内 `artifacts.select` | 子组件 `onPreviewArtifact` → 页面壳 |
|
||||
| 是否要求 path | 是(有 path 才自动开) | 否(write_file + content 可临时名开) |
|
||||
| thread 从哪来 | 同树 ThreadContext | `onThreadSnapshot` + 外部 store |
|
||||
| 关闭抑制 | 主要靠 ArtifactsContext / 用户操作 | `closedSandboxKeyRef` 按 previewKey |
|
||||
| 同 path 重写 | 依赖新的 tool call / 选择逻辑 | **必须**新 previewKey 以重挂 |
|
||||
| 结束后切预览 | ArtifactFileDetail 同一套 | 同一套 |
|
||||
|
||||
对齐目标是**用户感知一致**,不是代码路径完全相同。
|
||||
|
||||
普通对话页自动开的核心条件(对照用):
|
||||
|
||||
```ts
|
||||
artifacts && isLoading && isLast && autoOpen && autoSelect && path && !result
|
||||
```
|
||||
|
||||
它也要求 path,所以 content-first 时普通页同样会偏晚——只是很多主聊天示例 agent 先吐 path,观感更好。岗位会商报告 agent 更常 content-first,所以必须做临时名提前开。
|
||||
|
||||
---
|
||||
|
||||
## 12. 重做页面时的必守规则
|
||||
|
||||
按优先级:
|
||||
|
||||
1. **流式 thread 快照禁止进页面级 `useState`** → 用 store + `useSyncExternalStore`。
|
||||
2. **节流用前沿 + 补尾**,不要纯尾随 timer。
|
||||
3. **不要等完整 path 才开**;`write_file` + content 即可临时名打开。
|
||||
4. **扩展名完整前不要把 path 当最终 path**。
|
||||
5. **previewKey 按 tool call,不按 path**。
|
||||
6. **打开时同步写 `sandboxArtifactRef`,并重置节流时间戳**。
|
||||
7. **主路径扫描 `thread.messages`,不要只靠 `onToolStart`**。
|
||||
8. **path upgrade 保持 previewKey,不要当第二次交付**。
|
||||
9. **关闭抑制按 sandboxCloseKey,不要按整 thread**。
|
||||
10. **沙箱单例在页面壳;子组件只上报**。
|
||||
11. **本节点 threadId 回写不要 disarm**。
|
||||
12. **「结束后才切预览」不要当打开 bug 去「修」掉共享组件**,除非产品明确要求边生成边渲染 Markdown(需单独评估性能)。
|
||||
|
||||
---
|
||||
|
||||
## 13. 历史问题与根因(务必读)
|
||||
|
||||
下面每个都在真实联调里出现过。重做时若踩中,优先回来对照。
|
||||
|
||||
### 13.1 沙箱要等文件名出来才开(晚几十秒)
|
||||
|
||||
**现象:** 步骤条早就「写入文件」,沙箱很晚才开;普通对话页对比显得「实时」。
|
||||
|
||||
**根因:** 探测逻辑硬等完整 `path`;而该 agent 先流 `content`、最后才流 `path`。
|
||||
|
||||
**修复:** content 首批字符即可用临时名打开;path 到了再 upgrade。
|
||||
|
||||
### 13.2 沙箱打开了,内容冻住,生成完才灌满
|
||||
|
||||
**现象:** 面板开了,但里面一直是空/旧内容;日志里 queue 很多,flush 很少。
|
||||
|
||||
**根因:** 纯尾随 160ms 定时器在忙主线程下被饿死。
|
||||
|
||||
**修复:** 前沿节流——到期就在流回调里同步 flush。
|
||||
|
||||
### 13.3 修完「内容更新」后,整页卡顿、消息也不实时
|
||||
|
||||
**现象:** 沙箱开始更新了,但页面更卡,消息列表也「攒到最后刷」。
|
||||
|
||||
**根因:** flush 走的是页面 `useState`,每 160ms 整页重渲染。以前 timer 饿死时反而「不卡」。
|
||||
|
||||
**修复:** 快照改外部 store,只让沙箱子树订阅。
|
||||
|
||||
### 13.4 第二次「重新生成」步骤有了,沙箱不变
|
||||
|
||||
**现象:** 同轮第二条二条 write 步骤,沙箱仍显示第一次完整结果。
|
||||
|
||||
**根因:** 用「path 相同」复用了第一次 previewKey,React key 不变,组件不重挂。
|
||||
|
||||
**修复:** previewKey 只按 tool call;同 path 新 call 必须新 key。
|
||||
|
||||
### 13.5 第一次实时,关掉后再问就不实时
|
||||
|
||||
**现象:** 关过沙箱后,下一轮要等生成完才开,或不开。
|
||||
|
||||
**根因:** 抑制绑在整个 threadId,或把 path upgrade / 新 call 误判成同一次。
|
||||
|
||||
**修复:** 抑制键 = `sandboxCloseKey(previewKey)`;仅抑制那一次交付。
|
||||
|
||||
### 13.6 首轮永远不开
|
||||
|
||||
**现象:** 新建节点第一次提问,armed 后很快又不满足条件。
|
||||
|
||||
**根因:** `node.threadId` 从 null 回写真实 id 时,绑定 effect 误 disarm。
|
||||
|
||||
**修复:** `isOwnThreadBinding` 时不清 armed。
|
||||
|
||||
### 13.7 预览变成纯文本
|
||||
|
||||
**现象:** 明明是 md,却按 TEXT 打开。
|
||||
|
||||
**根因:** 在 `.m` 阶段就当完整 path;或临时名没有 `.md` / 没传 language、mimeType;或 path 带空格。
|
||||
|
||||
**修复:** `hasCompleteArtifactExtension`;临时名带扩展名;URL 加 `language=markdown`;path trim。
|
||||
|
||||
### 13.8 依赖 onToolStart,线上偶发不开
|
||||
|
||||
**现象:** 本地偶发正常,联调经常不开。
|
||||
|
||||
**根因:** 后端 StreamBridge / worker 可能过滤 tools/events 类 stream mode。
|
||||
|
||||
**修复:** 主路径必须是 messages 里渐进出现的 `tool_calls`;onToolStart 只能当补充。
|
||||
|
||||
---
|
||||
|
||||
## 14. 排障手册(按症状)
|
||||
|
||||
排障时建议先看 Chrome Performance / React 重渲染是否整页抖,再看消息里 tool_calls 的 args 是 path-first 还是 content-first。
|
||||
|
||||
### 14.1 沙箱开得很晚
|
||||
|
||||
检查顺序:
|
||||
|
||||
1. `latestStreamingWriteFile` 是否仍要求完整 path?
|
||||
2. armed 是否被 threadId 回写清掉?
|
||||
3. 是否命中 `closedSandboxKey`?
|
||||
4. 新 call 是否被 baseline 当成历史?
|
||||
5. `previewThreadId` 是否为空(线程还没绑上)?
|
||||
|
||||
### 14.2 打开了但内容空白 / 冻住
|
||||
|
||||
1. 快照是否进了页面 useState?
|
||||
2. 节流是否纯尾随?
|
||||
3. `SandboxThreadProvider` 是否拿到带 messages 的 thread?
|
||||
4. 虚拟 URL 的 message_id / tool_call_id 是否对得上?
|
||||
5. 打开时 `sandboxArtifactRef` 是否同步?(首帧是否被 drop)
|
||||
6. signature 是否因实现错误一直不变,导致 publish 被去重?
|
||||
|
||||
### 14.3 整页卡、消息列表也不实时
|
||||
|
||||
1. 搜页面壳是否还有 `setSandboxThreadSnapshot` / 类似大对象 state。
|
||||
2. 是否在每 chunk `console.debug` 打大对象(DevTools 打开时更卡)。
|
||||
3. 是否错误地让页面壳订阅了 store(页面壳不应 `useSyncExternalStore` 这份 store;只有 Provider 订)。
|
||||
|
||||
### 14.4 重新生成不刷新
|
||||
|
||||
1. 第二次 write 的 toolCallId 是否变了?
|
||||
2. previewKey 是否仍用 path 复用?
|
||||
3. `ArtifactFileDetail` 的 `key=` 是否基于 sandboxCloseKey/previewKey?
|
||||
|
||||
### 14.5 关后再问异常
|
||||
|
||||
1. 打印 `closedSandboxKeyRef` 与新 artifact 的 sandboxCloseKey。
|
||||
2. 确认新一轮 send 时 armed/baseline 已重置。
|
||||
3. 确认抑制不是整 thread。
|
||||
|
||||
### 14.6 语言 / 预览模式不对
|
||||
|
||||
1. 看打开时的 path / mimeType / language 查询参数。
|
||||
2. 确认不是在扩展名不完整时打开。
|
||||
3. 确认「源码→预览」发生在 isLoading false 之后(属正常)。
|
||||
|
||||
---
|
||||
|
||||
## 15. 对接自测清单
|
||||
|
||||
重做页面后至少跑完:
|
||||
|
||||
### 功能
|
||||
|
||||
- [ ] 首轮:步骤条「写入文件」出现时,沙箱是否已开并开始出字?
|
||||
- [ ] content-first agent:无文件名阶段是否也能开?
|
||||
- [ ] path 到达后:是否只改名、不整页闪、不重挂?
|
||||
- [ ] 同轮第二次同路径 write:是否清空并重新流式?
|
||||
- [ ] 结束后是否自动切 Markdown 预览?
|
||||
- [ ] 手动点步骤条路径 / 产物卡片:能否打开正确文件?
|
||||
|
||||
### 关闭与抑制
|
||||
|
||||
- [ ] 流式中手动关闭:本次是否不再自动弹?
|
||||
- [ ] 关闭后再发一轮新问题:是否仍可自动开?
|
||||
- [ ] path upgrade 是否不会绕过「用户刚关闭」的抑制?
|
||||
|
||||
### 场景切换
|
||||
|
||||
- [ ] 切节点:沙箱关闭,旧内容不残留。
|
||||
- [ ] 切岗位:同上。
|
||||
- [ ] 新建会话 / 切历史:同上。
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 流式中消息列表是否实时出字?
|
||||
- [ ] 页面是否明显掉帧 / 卡死?
|
||||
- [ ] React DevTools 中,大页面壳是否在每 160ms 整页 update?(不应)
|
||||
|
||||
---
|
||||
|
||||
## 16. 最小接线示例
|
||||
|
||||
### 16.1 页面壳(示意)
|
||||
|
||||
```tsx
|
||||
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
||||
const sandboxArtifactRef = useRef<PositionArtifactPreview | null>(null);
|
||||
const closedSandboxKeyRef = useRef<string | null>(null);
|
||||
const latestNodeThreadSnapshotRef = useRef<SandboxThreadSnapshot | null>(null);
|
||||
const store = useMemo(createSandboxThreadStore, []);
|
||||
// pending + timer + appliedAt 用于前沿节流(见 PositionRoundtablePage)
|
||||
|
||||
function handlePreviewArtifact(artifact: PositionArtifactPreview) {
|
||||
if (!artifact.threadId || !artifact.path) return;
|
||||
const key = sandboxCloseKey(artifact);
|
||||
if (artifact.auto && closedSandboxKeyRef.current === key) return;
|
||||
closedSandboxKeyRef.current = null;
|
||||
|
||||
artifactsCtx?.select(artifact.path, true);
|
||||
artifactsCtx?.setOpen(true);
|
||||
|
||||
const prev = sandboxArtifactRef.current;
|
||||
const isSame =
|
||||
prev?.threadId === artifact.threadId &&
|
||||
prev.path === artifact.path &&
|
||||
prev.previewKey === artifact.previewKey;
|
||||
const isPathUpgrade =
|
||||
!isSame &&
|
||||
Boolean(artifact.previewKey) &&
|
||||
prev?.threadId === artifact.threadId &&
|
||||
prev.previewKey === artifact.previewKey;
|
||||
|
||||
if (!isSame) {
|
||||
sandboxArtifactRef.current = artifact; // 必须同步
|
||||
setSandboxArtifact(artifact);
|
||||
sandboxSnapshotAppliedAtRef.current = 0;
|
||||
}
|
||||
|
||||
if (store.getSnapshot()?.threadId !== artifact.threadId) {
|
||||
store.publish(null);
|
||||
}
|
||||
const latest = latestNodeThreadSnapshotRef.current;
|
||||
if (latest?.threadId === artifact.threadId) {
|
||||
queueSandboxThreadSnapshot(latest);
|
||||
}
|
||||
if (!isPathUpgrade) setRightPanelOpen(false);
|
||||
}
|
||||
|
||||
function handleNodeThreadSnapshot(snapshot: { threadId: string; thread: BaseStream<AgentThreadState> }) {
|
||||
const next = { ...snapshot, signature: sandboxThreadSignature(snapshot.thread) };
|
||||
latestNodeThreadSnapshotRef.current = next;
|
||||
if (sandboxArtifactRef.current?.threadId !== snapshot.threadId) return;
|
||||
queueSandboxThreadSnapshot(next);
|
||||
}
|
||||
|
||||
function closeSandbox() {
|
||||
if (sandboxArtifactRef.current) {
|
||||
closedSandboxKeyRef.current = sandboxCloseKey(sandboxArtifactRef.current);
|
||||
}
|
||||
// clear timer / pending / appliedAt
|
||||
sandboxArtifactRef.current = null;
|
||||
setSandboxArtifact(null);
|
||||
store.publish(null);
|
||||
artifactsCtx?.setOpen(false);
|
||||
}
|
||||
|
||||
// 渲染
|
||||
{sandboxArtifact && (
|
||||
<SandboxThreadProvider
|
||||
store={store}
|
||||
threadId={sandboxArtifact.threadId}
|
||||
fallbackThread={stub}
|
||||
>
|
||||
<ArtifactFileDetail
|
||||
key={sandboxCloseKey(sandboxArtifact)}
|
||||
filepath={sandboxArtifact.path}
|
||||
threadId={sandboxArtifact.threadId}
|
||||
mimeType={sandboxArtifact.mimeType}
|
||||
onClose={closeSandbox}
|
||||
allowVirtualMarkdownEdit
|
||||
/>
|
||||
</SandboxThreadProvider>
|
||||
)}
|
||||
```
|
||||
|
||||
### 16.2 对话子组件(示意)
|
||||
|
||||
```tsx
|
||||
// send 前
|
||||
activeWriteFilePreviewRef.current = null;
|
||||
autoPreviewBaselineCallKeysRef.current = writeFileCallKeys(thread.messages);
|
||||
autoPreviewRunArmedRef.current = true;
|
||||
await sendMessage(...);
|
||||
|
||||
// 探测
|
||||
const streaming = thread.isLoading ? latestStreamingWriteFile(thread.messages) : null;
|
||||
const next =
|
||||
streaming && !autoPreviewBaselineCallKeysRef.current.has(streaming.key)
|
||||
? streaming
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!thread.isLoading || !next || !previewThreadId || !autoPreviewRunArmedRef.current) return;
|
||||
|
||||
// previewKey:同 tool call 复用,否则用 next.key
|
||||
// artifactKey = threadId:previewKey:sourcePath 去重
|
||||
onPreviewArtifact?.({
|
||||
threadId: previewThreadId,
|
||||
path: next.path,
|
||||
name: next.name,
|
||||
mimeType: next.mimeType,
|
||||
previewKey,
|
||||
auto: true,
|
||||
});
|
||||
}, [next, previewThreadId, thread.isLoading, onPreviewArtifact]);
|
||||
|
||||
useEffect(() => {
|
||||
onThreadSnapshot?.({ threadId: activeThreadId, thread });
|
||||
}, [activeThreadId, thread, onThreadSnapshot]);
|
||||
```
|
||||
|
||||
**强烈建议:** 探测 / armed / baseline / 临时名逻辑直接复用或抽取自 `PositionNodeChat`,不要从零重写。
|
||||
|
||||
---
|
||||
|
||||
## 17. 关键代码索引
|
||||
|
||||
| 主题 | 位置 |
|
||||
| --- | --- |
|
||||
| 临时名 / 完整扩展名 / latestStreamingWriteFile | `PositionNodeChat.tsx` 中 `provisionalWriteFilePath`、`hasCompleteArtifactExtension`、`latestStreamingWriteFile` |
|
||||
| armed / baseline / 上报 effect | `PositionNodeChat.tsx` `handleSubmit` + streaming preview `useEffect` |
|
||||
| 同 tool call 判断 / 二次写入重挂 | 同上,`isSameToolCall` / `previewKey` |
|
||||
| 线程绑定保护 | `PositionNodeChat.tsx` node binding `useEffect` 中 `isOwnThreadBinding` |
|
||||
| sandboxCloseKey / store / 前沿节流 | `PositionRoundtablePage.tsx` `sandboxCloseKey`、`createSandboxThreadStore`、`queueSandboxThreadSnapshot` |
|
||||
| 打开 / path upgrade / 抑制 | `PositionRoundtablePage.tsx` `handlePreviewArtifact` |
|
||||
| 关闭 | `PositionRoundtablePage.tsx` `closeSandbox` |
|
||||
| 虚拟 URL 读 content | `core/artifacts/loader.ts` `loadArtifactContentFromToolCall` |
|
||||
| 源码/预览切换 | `artifact-file-detail.tsx` 中 `viewMode` + `thread.isLoading` |
|
||||
| 普通页步骤条自动开 | `message-group.tsx` `write_file` 分支 |
|
||||
|
||||
相关提交(背景):
|
||||
|
||||
- `fix(frontend): 岗位会商沙箱在 write_file 流式阶段实时打开并刷新`
|
||||
— 临时名打开、外部 store、前沿节流
|
||||
- `fix(frontend): 同路径二次 write_file 时重挂岗位会商沙箱`
|
||||
— previewKey 按 tool call,禁止按 path 复用
|
||||
|
||||
---
|
||||
|
||||
## 18. 术语表
|
||||
|
||||
| 术语 | 含义 |
|
||||
| --- | --- |
|
||||
| armed | 本轮用户发送后,允许自动打开沙箱的开关 |
|
||||
| baseline | send 前已有的 write tool call 集合,用于忽略历史产物 |
|
||||
| previewKey | 一次交付的稳定身份,推荐 `messageId:toolCallId` |
|
||||
| sandboxCloseKey | 关闭抑制键,同时用作 `ArtifactFileDetail` 的 React key |
|
||||
| provisional path / 临时名 | 尚无真实 path 时的 `未命名文件.md` / `.html` |
|
||||
| path upgrade | 同 previewKey 下临时名升级为真实 path |
|
||||
| sandboxThreadStore | 流式线程外部 store,避免整页重渲染 |
|
||||
| write-file: URL | 用 message_id + tool_call_id 定位流式 content 的虚拟路径 |
|
||||
| leading-edge throttle / 前沿节流 | 到期立即在回调里 publish,而不是只挂尾随 timer |
|
||||
| content-first / path-first | 模型流式 JSON 参数时 content 与 path 哪个先到 |
|
||||
| isOwnThreadBinding | 本节点把自己的新 threadId 写回 props,不是切节点 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:给对接同学的一页纸摘要
|
||||
|
||||
1. 页面壳只挂一个沙箱;子组件只调用 `onPreviewArtifact` / `onThreadSnapshot`。
|
||||
2. 自动打开时机 ≈ 步骤条出现「写入文件」时,而不是出现文件名时。
|
||||
3. `write_file` 可以没有 path:临时名打开,path 到了再改名。
|
||||
4. 流式 thread 进外部 store,进页面 state 会卡死。
|
||||
5. 节流要前沿;previewKey 看 tool call;关闭抑制看 sandboxCloseKey。
|
||||
6. 同路径重新生成 = 新 previewKey = 沙箱重挂。
|
||||
7. 出问题先对照第 13、14 节,再看代码索引第 17 节。
|
||||
@@ -1,7 +0,0 @@
|
||||
# 技能知识归纳与“知识梳理”助手知识库
|
||||
|
||||
管理员在「技能管理 → 技能知识归纳」中选择一个或多个技能、一个或多个目标,系统按“技能 × 目标”创建后台任务。界面提供总体进度、单项失败重试、实体/关系审核、绑定重归纳、自动同步和两种解绑动作。技能列表通过一次 bindings 查询展示归纳目标,避免逐技能请求。
|
||||
|
||||
扫描仅处理知识资料。代码、脚本、锁文件、密钥文件以及 Markdown 代码块不会进入上传、摘要、提示词或知识指纹;ZIP 只读取逐项验证通过的知识成员。自动监听只比较该知识指纹,所以纯代码改动不会产生 stale 状态。
|
||||
|
||||
知识库页面新增「普通知识库 / 助手知识库」并行入口。管理员可将系统默认入口设为 v1(普通 WeKnora)或 v2(全局“知识梳理”)。助手库无需 WeKnora 即可初始化,所有用户可只读查看和搜索 Wiki;管理员可上传本地文件、从普通库导入、重新导入或删除来源、查看版本并回滚。来源删除只移除对应贡献,共享实体与关系继续保留。
|
||||
@@ -1,822 +0,0 @@
|
||||
# 沙箱统一写作管线:可实现开发方案
|
||||
|
||||
> 状态:设计稿;本文件只定义实施方案,不包含本轮业务代码改动。
|
||||
> 覆盖范围:普通沙箱 Markdown、深度研究 `report.md`、现有 Markdown 编辑器的选区/段落改写。
|
||||
|
||||
## 1. 目标与结论
|
||||
|
||||
目标不是再增加一个“AI 写作页面”,而是将现有的四类能力统一为一个安全、可流式、可追溯的**文档改写管线**:
|
||||
|
||||
1. Markdown 编辑器选区改写;
|
||||
2. Markdown 编辑器段落改写;
|
||||
3. 深度研究报告的局部候选改写;
|
||||
4. 沙箱中 Markdown 文件的全文改写。
|
||||
|
||||
该管线负责固定原文版本、构造上下文、逐 token 输出、校验 Markdown、生成可核验 diff、按策略提交,以及在全文覆盖时保存可撤销版本。
|
||||
|
||||
结论:需求可行,现有工程已有足够的复用基础:
|
||||
|
||||
- `frontend-web/src/open-canvas/components/artifact/MarkdownAiEditor.tsx` 已具备局部改写弹窗与人工“替换/插入/重试/追问”交互。
|
||||
- `frontend-web/src/open-canvas/hooks/useWritingRewrite.ts` 与 `frontend-web/src/open-canvas/api/rewrite.ts` 已具备 SSE 文本流消费。
|
||||
- `offline-backend-20260512/backend/app/gateway/routers/writing.py` 已具备模型流式读取实现。
|
||||
- `offline-backend-20260512/backend/app/gateway/routers/deep_research.py` 已具备报告哈希、候选内容和用户确认应用机制。
|
||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`、`MessageGroup.tsx` 已可承载普通对话的工具步骤条、流式消息和文件卡片。
|
||||
- `frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx` 已经通过虚拟消息把研究进度接入普通 `MessageList`。
|
||||
|
||||
因此应该复用这些基础,建设一个执行内核;不能为全文改写另建独立聊天列表或独立写作流。
|
||||
|
||||
## 2. 产品边界与交互策略
|
||||
|
||||
### 2.1 支持范围
|
||||
|
||||
第一版仅对 `.md`、`.markdown` 文件显示“AI 全文改写”按钮。
|
||||
|
||||
不支持 PDF、DOCX、HTML、图片和二进制文件;不在改写过程中发起新的联网搜索;超大文件不静默截断后覆盖,而是提示用户选择章节改写。
|
||||
|
||||
### 2.2 范围、模式和提交策略
|
||||
|
||||
| 入口 | 文本范围 | 管线模式 | 提交策略 | 展示位置 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 编辑器气泡菜单 | 当前选区 | `selection` | `preview_only` | 现有改写预览弹窗 |
|
||||
| 编辑器段落菜单 | 当前段落 | `paragraph` | `preview_only` | 现有改写预览弹窗 |
|
||||
| 深度研究“改写第一段/章节” | 被解析出的报告片段 | `report_section` | `manual_apply` | 普通消息列表候选稿 + 确认卡 |
|
||||
| 文件右上角“AI 全文改写” | 当前完整 Markdown | `document` | `auto_commit_after_validation` | 普通消息列表步骤条 + 右侧沙箱 |
|
||||
|
||||
统一内核中的枚举:
|
||||
|
||||
~~~python
|
||||
class RewriteMode(str, Enum):
|
||||
SELECTION = "selection"
|
||||
PARAGRAPH = "paragraph"
|
||||
REPORT_SECTION = "report_section"
|
||||
DOCUMENT = "document"
|
||||
|
||||
class CommitPolicy(str, Enum):
|
||||
PREVIEW_ONLY = "preview_only"
|
||||
MANUAL_APPLY = "manual_apply"
|
||||
AUTO_COMMIT_AFTER_VALIDATION = "auto_commit_after_validation"
|
||||
~~~
|
||||
|
||||
这三类策略必须严格区分:
|
||||
|
||||
- `preview_only`:只生成候选,前端编辑器确认后才改本地 TipTap 文档;不写真实沙箱文件。
|
||||
- `manual_apply`:后端保留基准哈希和候选片段,用户点确认后才将该片段应用进研究报告。
|
||||
- `auto_commit_after_validation`:全文生成完成并校验通过后,后端进行一次带哈希校验的原子覆盖;同时保存可撤销版本。
|
||||
|
||||
### 2.3 局部编辑器:保持当前体验
|
||||
|
||||
现有文件:
|
||||
|
||||
- `frontend-web/src/open-canvas/components/artifact/MarkdownAiEditor.tsx`
|
||||
- `frontend-web/src/open-canvas/hooks/useWritingRewrite.ts`
|
||||
- `frontend-web/src/open-canvas/api/rewrite.ts`
|
||||
- `offline-backend-20260512/backend/app/gateway/routers/writing.py`
|
||||
|
||||
局部改写仍然是:
|
||||
|
||||
1. 选中文字或打开段落菜单;
|
||||
2. 选择润色、扩写、缩写、改写等动作;
|
||||
3. 在现有弹窗中流式出现候选;
|
||||
4. 用户手动选择替换、插入、重试或继续追问。
|
||||
|
||||
不要让局部改写每次都往普通消息列表插完整步骤条,否则频繁编辑会淹没聊天历史。它只共享底层管线,不共享全文改写的消息展示方式。
|
||||
|
||||
需要补充本地版本保护:发起请求时保存当前完整编辑器文本哈希、选区 `from/to` 和选区文本;用户点击“替换原文”前再次检查该范围仍匹配。若用户已修改原文,提示“原文已变化,请重新生成候选稿”,禁止将过期候选替换到错误位置。
|
||||
|
||||
### 2.4 右上角“AI 全文改写”
|
||||
|
||||
宿主文件:
|
||||
|
||||
- `frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx`
|
||||
|
||||
新增独立图标按钮,位置在右上角文件操作区,靠近保存、复制、下载一类操作。它**不能**替代页面中间当前 Sparkles 编辑模式:
|
||||
|
||||
| 控件 | 职责 |
|
||||
| --- | --- |
|
||||
| 中部 Sparkles/编辑模式 | 打开 `MarkdownAiEditor`,处理选区和段落 |
|
||||
| 新增右上角 AI 全文改写 | 对整个 Markdown 发起后台全文重写任务 |
|
||||
|
||||
建议显示条件:
|
||||
|
||||
~~~ts
|
||||
const canUseFullDocumentRewrite =
|
||||
isMarkdownFile &&
|
||||
canEditArtifact &&
|
||||
!isSkillFile &&
|
||||
!isMock &&
|
||||
!useWriteFileStreaming &&
|
||||
!hasLocalEdits;
|
||||
~~~
|
||||
|
||||
若 `hasLocalEdits` 为真,按钮置灰并提示“请先保存当前修改后再进行全文改写”。后端全文任务从真实文件读取快照,不能在用户本地还有未保存修改时读取旧版本并最终覆盖新修改。
|
||||
|
||||
用户体验:
|
||||
|
||||
1. 点击按钮,打开简洁配置弹窗。
|
||||
2. 弹窗显示文件名、原文字数;用户填写改写要求、选择模型、选择风格。
|
||||
3. 点击开始,左侧普通消息列表创建一组步骤条。
|
||||
4. 右侧沙箱自动切到源码视图,先显示“正在准备临时草稿,原文件尚未修改”。
|
||||
5. 新 Markdown 从空临时草稿开始逐 token 写入右侧代码区域。
|
||||
6. 校验通过后,后端原子替换真实内容;前端再同步真实 Artifact 缓存。
|
||||
7. 左侧保留最终对比总结、查看 diff 按钮和撤销按钮。
|
||||
|
||||
任务进行中,同一个文件的保存、直接编辑、局部编辑和再次全文改写必须禁用;其他文件不受影响。
|
||||
|
||||
### 2.5 深度研究报告的约束
|
||||
|
||||
普通 Markdown 和深度研究报告使用同一管线,但不能使用同一上下文策略:
|
||||
|
||||
| 能力 | 普通沙箱 Markdown | 深度研究 report.md |
|
||||
| --- | --- | --- |
|
||||
| 输入上下文 | 原文 + 用户指令 | 原文 + 当前会话已选资料 |
|
||||
| 联网 | 不联网 | 不联网 |
|
||||
| 事实约束 | 不新增无依据事实 | 只能引用已选资料,禁止编造 |
|
||||
| 引用校验 | 可选 | 必须校验来源 ID 白名单 |
|
||||
| 最终存储 | Artifact 文件 | 研究 session 的 `report_markdown` |
|
||||
| 局部改写 | 编辑器确认 | 现有确认卡片 |
|
||||
|
||||
深度研究右侧的 `report.md` 是由 `ResearchSandboxPanel.tsx` 以虚拟 `write_file` 方式展示的;它不能当作普通磁盘 Artifact 直接写入。全文任务必须通过研究 session 更新 `report_markdown`,并使 `report_html` 缓存失效。
|
||||
|
||||
## 3. 总体架构
|
||||
|
||||
~~~mermaid
|
||||
flowchart LR
|
||||
A["MarkdownAiEditor<br/>选区 / 段落"] --> P
|
||||
B["ArtifactFileDetail<br/>AI 全文改写"] --> P
|
||||
C["DeepResearch<br/>局部 / 全文"] --> P
|
||||
|
||||
P["Document Rewrite Pipeline<br/>冻结版本 → 构造上下文 → 流式生成<br/>校验 → 差异总结 → 按策略提交"]
|
||||
|
||||
P --> I["InlineDocumentSource"]
|
||||
P --> F["ArtifactDocumentSource"]
|
||||
P --> R["DeepResearchReportSource"]
|
||||
|
||||
P --> E["可恢复 SSE 事件 / Job"]
|
||||
E --> L["普通 MessageList"]
|
||||
E --> S["右侧沙箱临时草稿"]
|
||||
P --> V["内容哈希、版本快照、原子提交"]
|
||||
~~~
|
||||
|
||||
设计原则:
|
||||
|
||||
1. 纯执行内核不依赖 FastAPI、路由、用户身份或操作系统路径。
|
||||
2. 鉴权、线程路径解析、研究 session 获取只在 `app` 层的来源适配器中完成。
|
||||
3. 完整文档任务使用持久化后台 Job;局部弹窗可维持单次 SSE,避免过度持久化。
|
||||
4. 全文流式过程中只写临时草稿;真实内容只在最终 compare-and-swap 成功时改变。
|
||||
|
||||
不要直接把现有完整“AI 写作 Agent”塞进改写按钮。那个流程适合从主题写新文章,可能包含大纲、资料和章节生命周期;本需求是严格以当前文档为基准改写。第一版复用它的流式视觉表达和消息列表能力,但使用专门的文档改写执行内核。
|
||||
|
||||
## 4. 后端模块设计
|
||||
|
||||
### 4.1 目录与职责
|
||||
|
||||
建议新增:
|
||||
|
||||
~~~text
|
||||
offline-backend-20260512/backend/
|
||||
├─ packages/harness/deerflow/
|
||||
│ ├─ document_rewrite/
|
||||
│ │ ├─ __init__.py
|
||||
│ │ ├─ models.py # 请求、事件、校验、差异领域模型
|
||||
│ │ ├─ ports.py # Source / Model / Store 协议
|
||||
│ │ ├─ pipeline.py # 统一编排,不导入 app.*
|
||||
│ │ ├─ prompt_builder.py # 普通与深度研究提示词
|
||||
│ │ ├─ validation.py # Markdown / 引用 / 截断检查
|
||||
│ │ └─ diff_summary.py # 字数、标题、块、引用差异
|
||||
│ └─ persistence/
|
||||
│ └─ document_rewrites/
|
||||
│ ├─ base.py
|
||||
│ ├─ model.py
|
||||
│ └─ sql.py
|
||||
├─ app/gateway/
|
||||
│ ├─ routers/document_rewrites.py
|
||||
│ ├─ document_rewrite_sources.py
|
||||
│ └─ document_rewrite_executor.py
|
||||
└─ packages/harness/deerflow/persistence/migrations/versions/
|
||||
└─ <next_revision>_document_rewrites.py
|
||||
~~~
|
||||
|
||||
`packages/harness/deerflow/document_rewrite` 不能导入 `app.*`,以符合项目 harness→app import firewall。`app/gateway/document_rewrite_sources.py` 负责把已鉴权的 Artifact/研究会话包装成内核端口。
|
||||
|
||||
### 4.2 来源端口
|
||||
|
||||
~~~python
|
||||
class DocumentSource(Protocol):
|
||||
async def read_snapshot(self) -> DocumentSnapshot: ...
|
||||
async def read_current_hash(self) -> str: ...
|
||||
async def commit_if_unchanged(
|
||||
self,
|
||||
*,
|
||||
expected_hash: str,
|
||||
content: str,
|
||||
metadata: CommitMetadata,
|
||||
) -> CommitResult: ...
|
||||
async def restore_if_unchanged(
|
||||
self,
|
||||
*,
|
||||
expected_hash: str,
|
||||
content: str,
|
||||
) -> CommitResult: ...
|
||||
~~~
|
||||
|
||||
三种实现:
|
||||
|
||||
| 适配器 | 使用场景 | 读取方式 | 提交方式 |
|
||||
| --- | --- | --- | --- |
|
||||
| `InlineDocumentSource` | 选区/段落候选 | 前端编辑器快照 | 不提交 |
|
||||
| `ArtifactDocumentSource` | 普通 Markdown | 复用 Artifact 权限与虚拟路径解析 | 同目录临时文件 + `os.replace` |
|
||||
| `DeepResearchReportSource` | 研究报告 | session `report_markdown` + 已选来源 | 数据库哈希条件更新 |
|
||||
|
||||
`ArtifactDocumentSource` 必须复用 `app/gateway/routers/artifacts.py` 中已有的线程访问校验、虚拟路径解析逻辑,绝不允许请求体传入任意本机绝对路径。
|
||||
|
||||
### 4.3 持久化 Job、事件和版本
|
||||
|
||||
全文改写使用持久化 Job,参考 `app/gateway/deep_research_job_executor.py` 的后台任务、租约、取消监听和事件重放模式。
|
||||
|
||||
#### document_rewrite_jobs
|
||||
|
||||
| 字段 | 作用 |
|
||||
| --- | --- |
|
||||
| `id` | `drw_xxx` Job ID |
|
||||
| `user_id` | 发起人 |
|
||||
| `source_kind` | `artifact` / `deep_research_report` |
|
||||
| `thread_id`、`artifact_path` | 普通文件来源 |
|
||||
| `research_session_id` | 研究报告来源 |
|
||||
| `presentation_thread_id` | 普通消息列表归属 |
|
||||
| `mode`、`commit_policy` | 范围和提交语义 |
|
||||
| `instruction`、`style`、`model_name` | 用户配置 |
|
||||
| `base_sha256` | 原文冻结哈希 |
|
||||
| `draft_content`、`draft_sha256` | 临时流式草稿 |
|
||||
| `status`、`stage` | 生命周期 |
|
||||
| `validation_json`、`diff_summary_json` | 可展示结果 |
|
||||
| `committed_sha256` | 成功提交后的哈希 |
|
||||
| `error_message`、时间字段 | 错误、审计、取消 |
|
||||
|
||||
#### document_rewrite_events
|
||||
|
||||
至少包含 `job_id`、`seq`、`event_type`、`stage`、`payload_json`、`created_at`。`seq` 用于断线重连时通过 `after_seq` 重放。
|
||||
|
||||
不必将每个 token 单独入库:每 250ms 或每积累 4KB 持久化一次 `draft_content`;状态转换、警告、完成、冲突、取消必须立即写事件。重连时先发送最新草稿快照,再发送快照序号后的增量事件。
|
||||
|
||||
#### document_rewrite_versions
|
||||
|
||||
保存真实全文覆盖前的原文版本,字段包含 `job_id`、`source_identity`、`version_no`、`content`、`sha256`、`reason`、`created_at`。初版每个文档保留最近 20 个版本即可。
|
||||
|
||||
索引要求:
|
||||
|
||||
- `(user_id, presentation_thread_id, created_at DESC)`:恢复历史消息。
|
||||
- 真实来源标识 + 活跃状态:防止同一文档并行两个全文改写。
|
||||
|
||||
### 4.4 Job 状态机
|
||||
|
||||
~~~mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> queued
|
||||
queued --> snapshotting
|
||||
snapshotting --> generating
|
||||
generating --> validating
|
||||
validating --> comparing
|
||||
comparing --> committing
|
||||
committing --> completed
|
||||
completed --> reverted
|
||||
|
||||
queued --> cancelled
|
||||
generating --> cancelled
|
||||
snapshotting --> failed
|
||||
generating --> failed
|
||||
validating --> failed
|
||||
committing --> conflict
|
||||
committing --> failed
|
||||
~~~
|
||||
|
||||
状态约束:
|
||||
|
||||
- 同一真实文档只允许一个活跃 Job。
|
||||
- 生成期间取消、异常、断线,真实文档绝不能变化。
|
||||
- 提交前重新读取当前哈希;与 `base_sha256` 不一致时进入 `conflict`,草稿保留但不覆盖。
|
||||
|
||||
### 4.5 API 与 SSE 契约
|
||||
|
||||
创建全文任务:
|
||||
|
||||
~~~http
|
||||
POST /api/document-rewrites
|
||||
Content-Type: application/json
|
||||
~~~
|
||||
|
||||
普通 Artifact 请求:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"source": {
|
||||
"kind": "artifact",
|
||||
"thread_id": "thread_xxx",
|
||||
"path": "/mnt/user-data/outputs/report.md"
|
||||
},
|
||||
"presentation_thread_id": "thread_xxx",
|
||||
"mode": "document",
|
||||
"instruction": "统一为正式行业分析语气,减少重复,不新增外部事实",
|
||||
"style": "formal_analysis",
|
||||
"model_name": "system_default"
|
||||
}
|
||||
~~~
|
||||
|
||||
深度研究请求:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"source": {
|
||||
"kind": "deep_research_report",
|
||||
"session_id": "drs_xxx"
|
||||
},
|
||||
"presentation_thread_id": "collector_thread_xxx",
|
||||
"mode": "document",
|
||||
"instruction": "重新组织全文逻辑,保留全部可用引用",
|
||||
"style": "formal_analysis",
|
||||
"model_name": "system_default"
|
||||
}
|
||||
~~~
|
||||
|
||||
其他接口:
|
||||
|
||||
~~~text
|
||||
GET /api/document-rewrites/{job_id}
|
||||
GET /api/document-rewrites/{job_id}/stream?after_seq=42
|
||||
POST /api/document-rewrites/{job_id}/cancel
|
||||
POST /api/document-rewrites/{job_id}/undo
|
||||
GET /api/document-rewrites?presentation_thread_id={thread_id}
|
||||
~~~
|
||||
|
||||
SSE 事件:
|
||||
|
||||
| 事件 | 关键数据 | 前端动作 |
|
||||
| --- | --- | --- |
|
||||
| `job_started` | 文件名、模式 | 建立步骤条 |
|
||||
| `snapshot_fixed` | 原文字数、哈希 | 完成“固定原文版本” |
|
||||
| `requirements_resolved` | 模型、风格、要求 | 完成“理解改写要求” |
|
||||
| `rewrite_delta` | `delta`、累计字符数 | 追加右侧临时草稿 |
|
||||
| `validation_started` | 校验项 | 进入校验步骤 |
|
||||
| `validation_completed` | passed、warnings | 显示校验结果 |
|
||||
| `comparison_ready` | `DiffSummary` | 渲染总结数据 |
|
||||
| `commit_started` | 无 | 显示安全替换中 |
|
||||
| `committed` | 最终哈希、版本 ID | 刷新真实文件缓存 |
|
||||
| `completed` | 完整结果 | 固化历史步骤条 |
|
||||
| `conflict` / `error` / `cancelled` | 原因 | 恢复原文显示,不保存草稿 |
|
||||
|
||||
局部改写兼容方案:
|
||||
|
||||
- `POST /api/writing/rewrite` 的请求和 SSE 格式先不变。
|
||||
- `writing.py` 内部逐步把 prompt、模型流、基础 Markdown 校验下沉给统一管线的 `preview_only` 分支。
|
||||
- 深度研究 `/sessions/{id}/rewrite/stream` 和确认候选接口也先保持 URL、前端行为不变,再内部复用 `manual_apply` 分支。
|
||||
|
||||
这样可以先交付全文改写,不会一次性破坏已有编辑器和深度研究流程。
|
||||
|
||||
### 4.6 模型、提示词与校验
|
||||
|
||||
管线只依赖一个流式模型端口:
|
||||
|
||||
~~~python
|
||||
class StreamingRewriteModel(Protocol):
|
||||
async def stream_text(
|
||||
self,
|
||||
*,
|
||||
messages: list[BaseMessage],
|
||||
model_name: str | None,
|
||||
) -> AsyncIterator[TextChunk]: ...
|
||||
~~~
|
||||
|
||||
第一版可以有两个 adapter:
|
||||
|
||||
- 普通文档复用 `writing.py` 的 chat model `astream`;
|
||||
- 深度研究复用 `deep_research.py` 使用的 `DeerFlowCompletionBackend.stream_complete`。
|
||||
|
||||
先统一编排,不强制立即统一所有模型配置实现。
|
||||
|
||||
普通 Markdown 系统约束:
|
||||
|
||||
~~~text
|
||||
仅根据原文与用户要求改写,不联网,不补充未给出的事实。
|
||||
保留有效链接、代码块、表格、标题语义和用户明确要求保留的内容。
|
||||
只输出完整 Markdown 正文;不得输出解释、前缀或“改写如下”。
|
||||
~~~
|
||||
|
||||
深度研究额外约束:
|
||||
|
||||
~~~text
|
||||
不得联网。事实、数字、结论和引用只能来自当前 session 已选资料。
|
||||
只允许使用给出的 source_id;资料不足时保持谨慎表述,禁止自行补全。
|
||||
~~~
|
||||
|
||||
校验规则:
|
||||
|
||||
| 类别 | 硬失败 | 告警但允许完成 |
|
||||
| --- | --- | --- |
|
||||
| 正文 | 空文档、仅空白、无可见内容 | 改写后与原文完全一致 |
|
||||
| Markdown | 未闭合围栏、明确截断 | 标题层级跳级、章节数明显减少 |
|
||||
| 深度研究来源 | 未选来源 ID、非法引用 | 引用数量下降、可能需核验的数字断言 |
|
||||
|
||||
## 5. 安全提交、对比与撤销
|
||||
|
||||
### 5.1 临时草稿不等于真实文件
|
||||
|
||||
用户视觉上看到“先清空再逐 token 写入”,但真实文件的处理必须是:
|
||||
|
||||
1. 读取原文并保存内容哈希、版本快照。
|
||||
2. 右侧只展示内存/Job 中的 `draft_content`。
|
||||
3. 生成完成后做校验和 diff。
|
||||
4. 提交前再次读取真实来源,并比较 `SHA-256`。
|
||||
5. 哈希一致才原子替换;否则进入冲突,不覆盖。
|
||||
|
||||
普通 Artifact 使用同目录临时文件、刷盘和 `os.replace`;深度研究通过数据库事务内的“旧哈希条件更新”提交。任何失败、取消或断流都保留真实原文。
|
||||
|
||||
### 5.2 撤销
|
||||
|
||||
撤销流程:
|
||||
|
||||
1. 读取该 Job 的 `before_full_rewrite` 快照。
|
||||
2. 确认当前真实内容哈希仍等于该 Job 的 `committed_sha256`。
|
||||
3. 若用户在改写后又手工编辑,返回冲突,不盲目覆盖。
|
||||
4. 先保存当前内容为 `before_undo` 版本。
|
||||
5. 原子恢复旧版本,Job 状态改为 `reverted`,消息列表卡片同步显示“已撤销”。
|
||||
|
||||
### 5.3 结构化 DiffSummary
|
||||
|
||||
后端产出结构化差异,前端仅渲染,避免生成空泛结论:
|
||||
|
||||
~~~ts
|
||||
type DiffSummary = {
|
||||
sourceDisplayName: string;
|
||||
instruction: string;
|
||||
modelName: string;
|
||||
originalCharCount: number;
|
||||
rewrittenCharCount: number;
|
||||
charChangeRatio: number;
|
||||
changedBlockCount: number;
|
||||
headings: {
|
||||
added: string[];
|
||||
removed: string[];
|
||||
reordered: string[];
|
||||
levelWarnings: string[];
|
||||
};
|
||||
citations?: {
|
||||
beforeCount: number;
|
||||
afterCount: number;
|
||||
retainedCount: number;
|
||||
removedIds: string[];
|
||||
invalidIds: string[];
|
||||
};
|
||||
requestedOptimizations: string[];
|
||||
validationWarnings: string[];
|
||||
};
|
||||
~~~
|
||||
|
||||
推荐总结展示:
|
||||
|
||||
~~~text
|
||||
已完成《新能源汽车市场分析报告》的整体改写。
|
||||
|
||||
- 改写范围:全文;要求为“统一正式分析语气、减少重复”;模型:默认模型。
|
||||
- 数量变化:原文 8,260 字,改写后 8,110 字,减少 1.8%;调整 14 个内容块。
|
||||
- 结构变化:标题层级保持不变;未新增或删除一级标题。
|
||||
- 已执行的优化:按要求合并重复表述、统一语气、调整段落衔接。
|
||||
- 保留情况:保留 35 条已选来源引用,未新增外部来源。
|
||||
- 风险提示:无。
|
||||
|
||||
[查看前后对比] [撤销到改写前版本]
|
||||
~~~
|
||||
|
||||
“已执行的优化”仅来自用户要求和可计算的结构差异;不能无依据宣称“文章更专业、更严谨”。
|
||||
|
||||
## 6. 前端实现设计
|
||||
|
||||
### 6.1 新增模块
|
||||
|
||||
~~~text
|
||||
frontend-web/src/core/document-rewrite/
|
||||
├─ types.ts
|
||||
├─ api.ts
|
||||
├─ sse.ts
|
||||
├─ use-document-rewrite-job.ts
|
||||
├─ document-rewrite-provider.tsx
|
||||
└─ document-rewrite-presentation.ts
|
||||
|
||||
frontend-web/src/components/workspace/document-rewrite/
|
||||
├─ DocumentRewriteDialog.tsx
|
||||
├─ DocumentRewriteProgressStep.tsx
|
||||
├─ DocumentRewriteSummaryCard.tsx
|
||||
└─ DocumentDiffDialog.tsx
|
||||
~~~
|
||||
|
||||
职责:
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `api.ts` | 创建、查询、取消、撤销 Job |
|
||||
| `sse.ts` | SSE 解析、`after_seq` 重连 |
|
||||
| `use-document-rewrite-job.ts` | Job 状态、草稿、rAF 合并 token |
|
||||
| Provider | 同时向文件面板与消息列表提供同一任务状态 |
|
||||
| `DocumentRewriteDialog` | 改写要求、模型、风格配置 |
|
||||
| ProgressStep | 普通消息列表内的五步步骤条 |
|
||||
| SummaryCard | 结构化总结、diff、撤销动作 |
|
||||
| DiffDialog | 原文与改写后双栏/行级对比 |
|
||||
|
||||
模型选择使用项目已有的 `tdesign-react` 选择组件,放在全文改写配置弹窗中;局部编辑器保留当前模型传递方式,后续再统一选择器。
|
||||
|
||||
### 6.2 流式平滑:每帧合并 delta
|
||||
|
||||
不能因为 SSE 是 token 流就每 token 调一次 `setState`。应当网络层完整读取、渲染层按动画帧合并:
|
||||
|
||||
~~~ts
|
||||
const pendingDeltaRef = useRef("");
|
||||
const frameRef = useRef<number | null>(null);
|
||||
|
||||
function onRewriteDelta(delta: string) {
|
||||
pendingDeltaRef.current += delta;
|
||||
if (frameRef.current !== null) return;
|
||||
|
||||
frameRef.current = requestAnimationFrame(() => {
|
||||
const pending = pendingDeltaRef.current;
|
||||
pendingDeltaRef.current = "";
|
||||
frameRef.current = null;
|
||||
setDraftContent((previous) => previous + pending);
|
||||
});
|
||||
}
|
||||
~~~
|
||||
|
||||
完成、失败、取消前必须 flush 最后一段缓存。右侧代码编辑器只消费 `draftContent`;真实 Artifact 查询缓存直到 `committed` 事件才更新。
|
||||
|
||||
### 6.3 ArtifactFileDetail 改造
|
||||
|
||||
在 `artifact-file-detail.tsx` 增加:
|
||||
|
||||
~~~ts
|
||||
type DocumentRewriteTarget =
|
||||
| {
|
||||
kind: "artifact";
|
||||
threadId: string;
|
||||
path: string;
|
||||
presentationThreadId: string;
|
||||
}
|
||||
| {
|
||||
kind: "deep_research_report";
|
||||
sessionId: string;
|
||||
presentationThreadId: string;
|
||||
};
|
||||
~~~
|
||||
|
||||
改造步骤:
|
||||
|
||||
1. 加入右上角全文改写图标按钮和 `DocumentRewriteDialog`。
|
||||
2. 有活跃任务时强制 `viewMode = "code"`。
|
||||
3. 内容优先级为:`activeRewrite.draftContent ?? draftContent ?? artifactContent`。
|
||||
4. 活跃任务中代码只读,禁用保存、局部编辑和重复全文改写。
|
||||
5. 收到 `committed` 后用现有 `useUpdateArtifactContent` 或等价 Query cache 更新,使真实文件立即同步。
|
||||
6. 收到 `conflict/error/cancelled` 时废弃临时草稿显示,回到真实文件内容。
|
||||
|
||||
### 6.4 ResearchSandboxPanel 接入
|
||||
|
||||
相关文件:
|
||||
|
||||
- `frontend-web/src/pages/deep-research/ResearchSandboxPanel.tsx`
|
||||
- `frontend-web/src/pages/deep-research/useReportSandbox.ts`
|
||||
- `frontend-web/src/pages/deep-research/DeepResearchWorkbench.tsx`
|
||||
|
||||
改造:
|
||||
|
||||
1. `ResearchSandboxPanel` 为虚拟 `report.md` 传入 `kind: "deep_research_report"` target。
|
||||
2. `useReportSandbox` 接受 `externalDraftOverride`:
|
||||
|
||||
~~~ts
|
||||
const visibleReport = activeDocumentRewrite?.isActive
|
||||
? activeDocumentRewrite.draftContent
|
||||
: reportMarkdown;
|
||||
~~~
|
||||
|
||||
3. 流式期间只显示临时草稿,不提前写 session 报告。
|
||||
4. `committed` 后刷新/替换 session 的 `report_markdown`;失败后恢复旧报告。
|
||||
5. 当前“改写第一段 → 候选消息 → 确认应用”逻辑继续保留,不因全文改写而改变。
|
||||
|
||||
### 6.5 普通消息列表接入
|
||||
|
||||
复用:
|
||||
|
||||
- `frontend-web/src/components/workspace/messages/message-list.tsx`
|
||||
- `frontend-web/src/components/workspace/messages/MessageGroup.tsx`
|
||||
- `DeepResearchWorkbench.tsx` 中的虚拟消息注入模式。
|
||||
|
||||
`DocumentRewritePresentationProvider` 按 `presentation_thread_id` 读取 Job 历史并投影为:
|
||||
|
||||
1. 用户消息:“AI 全文改写《xxx.md》”;
|
||||
2. AI 工具调用:`document_rewrite_progress`;
|
||||
3. AI 富消息:`document_rewrite_summary`。
|
||||
|
||||
`MessageGroup.tsx` 增加 `document_rewrite_progress` 分支,渲染 `DocumentRewriteProgressStep`。最终摘要渲染 `DocumentRewriteSummaryCard`,其内含“查看前后对比”和“撤销”。
|
||||
|
||||
全文步骤固定为:
|
||||
|
||||
| 步骤 | 展示内容 |
|
||||
| --- | --- |
|
||||
| 固定原文版本 | 文件名、原文字数、快照已保存 |
|
||||
| 理解改写要求 | 改写目标、模型、风格 |
|
||||
| 流式重写 Markdown | 正在写入右侧 xxx.md、累计字数 |
|
||||
| 校验文档 | 非空、标题、围栏、截断;报告还含来源检查 |
|
||||
| 对比原文并生成说明 | 差异、风险、撤销入口 |
|
||||
|
||||
步骤条必须在完成后保留,不能因为流结束被隐藏。
|
||||
|
||||
## 7. 核心执行伪代码
|
||||
|
||||
~~~python
|
||||
async def run_document_rewrite(job_id: str) -> None:
|
||||
job = await jobs.acquire_lease(job_id)
|
||||
source = source_factory.create(job) # 已完成鉴权的来源适配器
|
||||
|
||||
await emit("snapshotting")
|
||||
snapshot = await source.read_snapshot()
|
||||
base_hash = sha256(snapshot.content)
|
||||
await jobs.freeze_snapshot(job_id, snapshot, base_hash)
|
||||
await emit("snapshot_fixed", char_count=count_chars(snapshot.content))
|
||||
|
||||
messages = prompt_builder.build(
|
||||
mode=job.mode,
|
||||
source_kind=job.source_kind,
|
||||
original=snapshot.content,
|
||||
instruction=job.instruction,
|
||||
style=job.style,
|
||||
research_sources=snapshot.allowed_sources,
|
||||
)
|
||||
await emit("requirements_resolved", ...)
|
||||
|
||||
draft = ""
|
||||
async for chunk in model.stream_text(messages=messages, model_name=job.model_name):
|
||||
if await jobs.cancel_requested(job_id):
|
||||
await jobs.mark_cancelled(job_id)
|
||||
await emit("cancelled")
|
||||
return
|
||||
draft += chunk.text
|
||||
await jobs.append_draft_periodically(job_id, draft)
|
||||
await emit("rewrite_delta", delta=chunk.text)
|
||||
|
||||
await emit("validation_started")
|
||||
validation = validate_markdown(
|
||||
draft,
|
||||
source_kind=job.source_kind,
|
||||
allowed_citations=snapshot.allowed_citation_ids,
|
||||
)
|
||||
if validation.has_fatal_error:
|
||||
await jobs.mark_failed(job_id, validation.message)
|
||||
await emit("error", ...)
|
||||
return
|
||||
await emit("validation_completed", ...)
|
||||
|
||||
diff = build_diff_summary(snapshot.content, draft, validation)
|
||||
await jobs.save_diff(job_id, diff)
|
||||
await emit("comparison_ready", diff)
|
||||
|
||||
if job.commit_policy != AUTO_COMMIT_AFTER_VALIDATION:
|
||||
await jobs.mark_candidate_ready(job_id)
|
||||
return
|
||||
|
||||
await emit("commit_started")
|
||||
await versions.save_before_rewrite(job, snapshot)
|
||||
result = await source.commit_if_unchanged(
|
||||
expected_hash=base_hash,
|
||||
content=draft,
|
||||
metadata=CommitMetadata(job_id=job_id),
|
||||
)
|
||||
if not result.ok:
|
||||
await jobs.mark_conflict(job_id)
|
||||
await emit("conflict", ...)
|
||||
return
|
||||
|
||||
await jobs.mark_completed(job_id, result.sha256)
|
||||
await emit("committed", ...)
|
||||
await emit("completed", diff)
|
||||
~~~
|
||||
|
||||
## 8. 实施顺序
|
||||
|
||||
### 阶段 A:后端内核与安全基础
|
||||
|
||||
1. 新增领域模型、来源端口、校验、确定性 diff。
|
||||
2. 新增 Job、事件、版本数据表与 migration。
|
||||
3. 参考深度研究 executor 完成后台任务、取消、SSE 重放。
|
||||
4. 实现普通 Artifact 读取、哈希比较、原子提交和撤销 API。
|
||||
5. 编写接口测试。
|
||||
|
||||
验收:可以通过 API 对普通 Markdown 创建全文任务;取消、失败、冲突均不改原文件;成功后可撤销。
|
||||
|
||||
### 阶段 B:普通沙箱全文 UI
|
||||
|
||||
1. 新增 `core/document-rewrite` 前端状态、SSE、rAF hook。
|
||||
2. 改 `ArtifactFileDetail`:右上角按钮、配置弹窗、临时草稿、禁用态。
|
||||
3. 接入普通 `MessageList` 的步骤条、总结卡、diff 和撤销。
|
||||
4. 支持刷新后恢复 Job 状态与已完成历史。
|
||||
|
||||
验收:右侧可平滑逐 token 写入;完成后替换真实 Markdown;左侧步骤和总结保留;撤销成功恢复。
|
||||
|
||||
### 阶段 C:深度研究接入
|
||||
|
||||
1. 实现 `DeepResearchReportSource`。
|
||||
2. 由 `ResearchSandboxPanel` 传入报告 target。
|
||||
3. `useReportSandbox` 支持外部临时草稿。
|
||||
4. 对深度研究添加引用白名单校验。
|
||||
5. 保持现有报告局部候选确认卡不变,再逐步下沉公共 prompt/校验逻辑。
|
||||
|
||||
验收:报告全文改写不触发联网搜索,不允许新来源;局部报告改写依然先确认再应用。
|
||||
|
||||
### 阶段 D:局部改写内核迁移
|
||||
|
||||
1. 保持 `/api/writing/rewrite` 契约不变。
|
||||
2. 将 `writing.py` 转为 `InlineDocumentSource + preview_only` 的兼容 facade。
|
||||
3. 给 `useWritingRewrite` 增加原文快照失效检查。
|
||||
4. 回归替换、插入、重试、继续追问。
|
||||
|
||||
## 9. 测试与验收
|
||||
|
||||
建议新增后端测试:
|
||||
|
||||
~~~text
|
||||
offline-backend-20260512/backend/tests/
|
||||
├─ test_document_rewrite_pipeline.py
|
||||
├─ test_document_rewrite_api.py
|
||||
├─ test_document_rewrite_versions.py
|
||||
└─ test_document_rewrite_deep_research.py
|
||||
~~~
|
||||
|
||||
必须覆盖:
|
||||
|
||||
- Job 状态、SSE 顺序和断线恢复;
|
||||
- 空输出、围栏不闭合、截断、非法深度研究引用;
|
||||
- 同一文件并发任务;
|
||||
- 提交前手工修改导致的冲突;
|
||||
- 取消/异常不改真实文件;
|
||||
- 正常撤销、撤销前手工编辑导致的冲突;
|
||||
- 旧 `/api/writing/rewrite`、深度研究 `/rewrite/stream` 兼容回归。
|
||||
|
||||
前端必须覆盖:
|
||||
|
||||
- 多个 delta 在一帧合并,最终内容不丢失;
|
||||
- `committed` 前 Artifact 缓存不被临时草稿污染;
|
||||
- 失败/取消/冲突后右侧恢复原文;
|
||||
- 活跃任务禁用同文件编辑、保存、重复改写;
|
||||
- 刷新后历史步骤条与总结仍存在;
|
||||
- 深度研究虚拟 `report.md` 可用全文改写;
|
||||
- 局部编辑器原文变化后不能应用过期候选。
|
||||
|
||||
执行基线:
|
||||
|
||||
~~~powershell
|
||||
cd offline-backend-20260512/backend
|
||||
$env:PYTHONPATH = "."
|
||||
uv run --no-sync pytest tests/test_document_rewrite_pipeline.py -v
|
||||
uv run --no-sync pytest tests/test_document_rewrite_api.py -v
|
||||
uv run --no-sync ruff check app packages/harness
|
||||
|
||||
cd ../../frontend-web
|
||||
pnpm typecheck
|
||||
pnpm build
|
||||
~~~
|
||||
|
||||
## 10. 风险控制
|
||||
|
||||
| 风险 | 控制措施 |
|
||||
| --- | --- |
|
||||
| 用户误以为真实文件已经清空 | 明确显示“临时草稿,原文件尚未修改” |
|
||||
| 流式显示仍然卡顿 | 网络完整读取 + `requestAnimationFrame` 合并渲染 |
|
||||
| 改写中用户手动编辑 | `SHA-256` compare-and-swap,冲突即拒绝覆盖 |
|
||||
| 输出 Markdown 不完整 | 非空、围栏、截断和标题检查 |
|
||||
| 深度研究幻觉 | 来源白名单、禁止联网、引用 ID 校验 |
|
||||
| 文档过大 | 第一版拒绝并提示按章节改写,不隐式截断 |
|
||||
| 局部编辑污染聊天历史 | 局部只在编辑器弹窗展示,不创建完整步骤条 |
|
||||
| 历史记录只存在浏览器内存 | 全文 Job/Event/Diff 持久化,按展示线程恢复 |
|
||||
|
||||
## 11. 复用点清单
|
||||
|
||||
| 现有实现 | 在新方案中的用途 |
|
||||
| --- | --- |
|
||||
| `MarkdownAiEditor.tsx` | 保留局部编辑菜单、预览弹窗、确认动作 |
|
||||
| `useWritingRewrite.ts` | 保留局部流控制,增加快照失效保护 |
|
||||
| `open-canvas/api/rewrite.ts` | 保持兼容,逐步接入统一内核 |
|
||||
| `routers/writing.py` | 复用当前模型流式读取,抽取 prompt/流逻辑 |
|
||||
| `routers/deep_research.py` | 复用报告来源、哈希候选、来源约束 |
|
||||
| `deep_research_job_executor.py` | 复用后台 Job、取消、事件重放模式 |
|
||||
| `artifact-file-detail.tsx` | 复用沙箱文件操作、代码/预览、缓存同步 |
|
||||
| `useUpdateArtifactContent` | 完成后更新普通 Artifact 缓存 |
|
||||
| `ResearchSandboxPanel.tsx` | 复用研究报告的右侧沙箱宿主 |
|
||||
| `useReportSandbox.ts` | 加入临时草稿覆盖显示 |
|
||||
| `MessageList.tsx / MessageGroup.tsx` | 复用普通消息步骤条、总结消息、文件卡片能力 |
|
||||
| `DeepResearchWorkbench.tsx` | 复用向普通消息列表投影过程事件的模式 |
|
||||
|
||||
## 12. 完成定义
|
||||
|
||||
完成后应满足:
|
||||
|
||||
1. 局部编辑依旧是编辑器弹窗中的人工确认操作。
|
||||
2. 全文改写在普通消息列表展示完整且不隐藏的步骤条。
|
||||
3. 右侧沙箱可平滑地逐 token 展示临时 Markdown 草稿。
|
||||
4. 任何中断、取消、校验失败和冲突都不损坏真实文件。
|
||||
5. 完成后用户可以看到量化差异、结构变化、来源保留和风险提示。
|
||||
6. 普通 Markdown 与深度研究报告都可全文改写,但研究报告绝不绕过已选来源约束。
|
||||
7. 每一次真实全文覆盖都有版本快照,并且能够安全撤销。
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
@@ -69,9 +69,7 @@
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^6.9.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
@@ -1704,92 +1702,6 @@
|
||||
"@lezer/lr": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/jsonlint-lines-primitives": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz",
|
||||
"integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 22"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/point-geometry": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
|
||||
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@mapbox/tiny-sdf": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz",
|
||||
"integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/unitbezier": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz",
|
||||
"integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/@mapbox/vector-tile": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz",
|
||||
"integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "~1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/geojson-vt": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@maplibre/geojson-vt/-/geojson-vt-6.1.1.tgz",
|
||||
"integrity": "sha512-FVMOcmSP/yqol45t7StApEyTL5/vmqBCuFhH9n+fFuINenhaX+YgHHIt1yJ86S8kln3uJLcMvmEU2cfn6E2eCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"kdbush": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||
"version": "26.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.2.tgz",
|
||||
"integrity": "sha512-6J0vZqMZvRAJKtdWJdDGHEh1YJ2ZHG08/GOur8gCArYhO8ZkM//OXqtI2AAEz4jc2G+Wq4MfcePg8qNK5TZ4Kg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mapbox/jsonlint-lines-primitives": "^2.0.3",
|
||||
"@mapbox/unitbezier": "^1.0.0",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
"minimist": "^1.2.8",
|
||||
"quickselect": "^3.0.0",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gl-style-format": "dist/gl-style-format.mjs",
|
||||
"gl-style-migrate": "dist/gl-style-migrate.mjs",
|
||||
"gl-style-validate": "dist/gl-style-validate.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/mlt": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@maplibre/mlt/-/mlt-1.2.1.tgz",
|
||||
"integrity": "sha512-5n5dgolE2EYxwCKgx8vlwURCB8A+kyfJJyThlYimjlGgOcOe2Bhw9VxxnnHC91OC9PgHg+nVdgVPTYPkIo62vg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@maplibre/vt-pbf": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz",
|
||||
"integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"pbf": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
@@ -1875,6 +1787,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.1.11",
|
||||
"license": "MIT",
|
||||
@@ -1964,6 +1893,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
@@ -2025,6 +1971,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
@@ -2123,6 +2086,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
@@ -2185,6 +2165,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||
"version": "2.1.16",
|
||||
"license": "MIT",
|
||||
@@ -2248,6 +2245,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||
@@ -2308,6 +2322,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card": {
|
||||
"version": "1.1.15",
|
||||
"license": "MIT",
|
||||
@@ -2373,6 +2404,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.3.2",
|
||||
"license": "MIT",
|
||||
@@ -2472,6 +2520,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
@@ -2539,6 +2604,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||
@@ -2584,6 +2666,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||
@@ -2717,21 +2816,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz",
|
||||
"integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==",
|
||||
"version": "1.2.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.3",
|
||||
"@radix-ui/primitive": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.5",
|
||||
"@radix-ui/react-context": "1.2.2",
|
||||
"@radix-ui/react-direction": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.10",
|
||||
"@radix-ui/react-primitive": "2.1.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.4",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.3",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2748,38 +2862,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz",
|
||||
"integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz",
|
||||
"integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz",
|
||||
"integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz",
|
||||
"integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
@@ -2790,51 +2876,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz",
|
||||
"integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz",
|
||||
"integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz",
|
||||
"integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==",
|
||||
"license": "MIT",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2851,26 +2898,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz",
|
||||
"integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz",
|
||||
"integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
@@ -2958,6 +2992,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.8",
|
||||
"license": "MIT",
|
||||
@@ -2980,12 +3031,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz",
|
||||
"integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==",
|
||||
"version": "1.2.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.5"
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2997,21 +3046,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz",
|
||||
"integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch": {
|
||||
"version": "1.2.6",
|
||||
"license": "MIT",
|
||||
@@ -3075,6 +3109,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.13",
|
||||
"license": "MIT",
|
||||
@@ -3139,6 +3190,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.1.10",
|
||||
"license": "MIT",
|
||||
@@ -3225,6 +3293,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
@@ -3247,6 +3332,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.2.8",
|
||||
"license": "MIT",
|
||||
@@ -3315,6 +3417,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||
@@ -3503,6 +3622,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||
@@ -5421,15 +5557,6 @@
|
||||
"version": "1.2.1",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.1.0.tgz",
|
||||
"integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.28.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
||||
@@ -6332,12 +6459,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/earcut": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/earcut/-/earcut-3.2.3.tgz",
|
||||
"integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.349",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.349.tgz",
|
||||
@@ -6732,12 +6853,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -7236,12 +7351,6 @@
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
|
||||
},
|
||||
"node_modules/json-stringify-pretty-compact": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
|
||||
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -7281,12 +7390,6 @@
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/kdbush": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/kdbush/-/kdbush-4.1.0.tgz",
|
||||
"integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/khroma": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
|
||||
@@ -7329,12 +7432,6 @@
|
||||
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
|
||||
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmmirror.com/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
@@ -7669,39 +7766,6 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/maplibre-gl/-/maplibre-gl-6.9.0.tgz",
|
||||
"integrity": "sha512-vFMwMK0Zs+NM/rOMSdtu8bO30DIexhBEVi5KC6f70/XtI+L/K2wC3LsDCAXFZ4s8ik5gAuDugfCNbpllpdJ9bA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@mapbox/point-geometry": "^1.1.0",
|
||||
"@mapbox/tiny-sdf": "^2.2.0",
|
||||
"@mapbox/unitbezier": "^1.0.0",
|
||||
"@mapbox/vector-tile": "^3.0.0",
|
||||
"@maplibre/geojson-vt": "^6.1.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^26.4.2",
|
||||
"@maplibre/mlt": "^1.2.1",
|
||||
"@maplibre/vt-pbf": "^4.3.2",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"bidi-js": "^1.1.0",
|
||||
"earcut": "^3.2.3",
|
||||
"gl-matrix": "^3.4.4",
|
||||
"kdbush": "^4.1.0",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"pbf": "^5.1.2",
|
||||
"potpack": "^2.1.0",
|
||||
"quickselect": "^3.0.0",
|
||||
"tinyqueue": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14.0",
|
||||
"npm": ">=8.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
@@ -8649,15 +8713,6 @@
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
@@ -8716,12 +8771,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/murmurhash-js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
|
||||
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
@@ -8894,18 +8943,6 @@
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/pbf/-/pbf-5.1.2.tgz",
|
||||
"integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
@@ -8998,12 +9035,6 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/potpack/-/potpack-2.1.0.tgz",
|
||||
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
|
||||
@@ -9187,12 +9218,6 @@
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/protocol-buffers-schema": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz",
|
||||
"integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
@@ -9210,12 +9235,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/quickselect/-/quickselect-3.0.0.tgz",
|
||||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/raf": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||
@@ -9599,24 +9618,6 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-protobuf-schema": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
|
||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"protocol-buffers-schema": "^3.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
||||
@@ -9970,12 +9971,6 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyqueue": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/tinyqueue/-/tinyqueue-3.0.0.tgz",
|
||||
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/tiptap-markdown": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.9.0.tgz",
|
||||
|
||||
@@ -1,135 +1,129 @@
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
|
||||
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
|
||||
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
|
||||
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
|
||||
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
|
||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^6.9.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
}
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:maf": "vitest run --config vitest.maf.config.ts",
|
||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"lucide-react": "^0.562.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"jsdom": "^30.0.1",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2",
|
||||
"vitest": "^4.1.10"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# Keep indirect Radix primitives on the React 19 ref-stability fix too.
|
||||
overrides:
|
||||
'@radix-ui/react-slot': 1.3.3
|
||||
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 503 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{fill: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>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#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>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 3840 2160"><image width="3840" height="2160" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><image width="582" height="667" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 664 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="xMidYMid slice" width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 51 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 278 1530"><image width="278" height="1530" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 51 KiB |
@@ -1 +0,0 @@
|
||||
<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
|
||||
|
Before Width: | Height: | Size: 193 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(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>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="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>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="458" height="113.588" viewBox="0 0 458 113.588"><defs><style>.a{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
|
||||
|
Before Width: | Height: | Size: 625 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{opacity:0;fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.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>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="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
|
||||
|
Before Width: | Height: | Size: 68 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68">
|
||||
<defs>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68">
|
||||
<defs>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 924 B |
@@ -32,18 +32,14 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
VITE_BRAND_WELCOME_DARK_URL: "",
|
||||
|
||||
// DeerFlow Gateway 地址:前端自有后端接口,例如 /api/agents、/api/auth、/api/threads 等。
|
||||
// VITE_BACKEND_BASE_URL: "https://ch1.b.uat.4.cn/deerflow",
|
||||
// VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026",
|
||||
|
||||
// LangGraph 流式对话接口地址:普通问答、智能体对话等流式能力使用。
|
||||
// VITE_LANGGRAPH_BASE_URL: "https://ch1.b.uat.4.cn/deerflow/api",
|
||||
// VITE_LANGGRAPH_BASE_URL: "http://10.19.200.5:2026/api",
|
||||
|
||||
// Canvas/AI 写作独立 LangGraph 地址:为空时前端会回退使用 VITE_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。
|
||||
// 填值时优先使用这里的 password;留空时使用 URL 中的 ?password=。
|
||||
VITE_ROUTE_LOGIN_PASSWORD: "",
|
||||
@@ -54,19 +50,6 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
// CMZS 后端接口:任务管理、任务分析、三情分析、行动分析等业务接口。
|
||||
VITE_API_BASE_URL: "https://ch1.b.uat.4.cn/zncm",
|
||||
|
||||
// 8BF 嵌入页专用任务系统接口。部署到其他系统时只需修改这一项,
|
||||
// 不影响原多智能体会商使用的任务详情接口。
|
||||
VITE_8BF_TASK_API_BASE_URL: "https://ch1.b.uat.4.cn/zl_consumer",
|
||||
|
||||
// 8BF 会商页右上角「返回」按钮的跳转地址(当前窗口 window.location.href)。
|
||||
// 部署后直接改 dist/runtime-config.js 即可,无需重新打包。留空则点击时提示未配置。
|
||||
VITE_8BF_ROUNDTABLE_BACK_URL: "",
|
||||
|
||||
// 任务深链「宿主路由」按钮的 postMessage targetOrigin(HOST_NAVIGATE)。
|
||||
// 填宿主系统 origin,例如 https://localhost:4912;生产改成他们线上地址。
|
||||
// 留空时尝试用 iframe 的 ancestorOrigins / referrer 自动探测,探测失败则不发送。
|
||||
VITE_HOST_NAVIGATE_ORIGIN: "",
|
||||
|
||||
// 佳来模型对话接口。
|
||||
VITE_AIPI_CHAT_URL: "https://ch1.b.uat.4.cn/gpt",
|
||||
|
||||
@@ -107,7 +90,7 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
|
||||
|
||||
// Consumer 接口:深链任务详情 cop-task-detail、研判情报报告 selectSqReport 等使用。
|
||||
VITE_AIPI_CONSUMER_URL: "https://ch1.b.uat.4.cn/consumer",
|
||||
VITE_AIPI_CONSUMER_URL: "http://10.19.200.5:8002/consumer",
|
||||
|
||||
// A 系统跳转地址:人物、组织、事件等列表项打开链接使用。
|
||||
VITE_AIPI_PRESENT_URL: "http://present.a.uat.4.cn/present/domain",
|
||||
@@ -147,30 +130,6 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
// 隐藏系统 Logo、换肤按钮以及“全量/简洁模式”切换。修改后刷新页面即可生效。
|
||||
VITE_FORCE_COLLAPSED_SIDEBAR: "false",
|
||||
|
||||
// 简介模式左侧菜单类型:与菜单管理页「简介模式」里的类型 A / B 对应。
|
||||
// 两套网页部署时分别填 "A" 或 "B"。不填、留空、或旧配置没有这一项,都按类型 A,
|
||||
// 继续读取原来的简介菜单数据,无需改旧页面。
|
||||
VITE_COMPACT_MENU_TYPE: "A",
|
||||
|
||||
// GitHub OAuth Token;不使用 GitHub 相关能力时留空。
|
||||
VITE_GITHUB_OAUTH_TOKEN: "",
|
||||
|
||||
// 是否在问答输入框「模式」菜单显示「极速」:"true" 显示,"false" 隐藏。
|
||||
// 默认关闭。开启后可直连大模型流式问答,不走智能体工具与技能。
|
||||
// 部署后改 dist/runtime-config.js 再刷新页面即可,无需重新打包。
|
||||
VITE_TURBO_MODE_VISIBLE: "true",
|
||||
|
||||
// 是否在智能体管理页显示「舆情分析」类型:"true" 显示,"false" 隐藏。
|
||||
// 关闭后,该智能体的专用对话地址也会自动跳回智能体管理页。
|
||||
VITE_SENTIMENT_AGENT_VISIBLE: "true",
|
||||
|
||||
// 舆情分析智能体:外部流式问答接口地址(POST,SSE 流式返回)。
|
||||
// 前端会把该地址随请求传给后台转发。留空时该智能体对话页会提示未配置。
|
||||
VITE_SENTIMENT_AGENT_API_URL: "http://alg.gw.y.uat.cn/a6_report_agent/alg/stream/report-agent",
|
||||
|
||||
// 舆情分析智能体:请求头 Authorization Basic Auth 用户名。
|
||||
VITE_SENTIMENT_AGENT_AUTH_USERNAME: "1054841959975223296",
|
||||
|
||||
// 舆情分析智能体:请求头 Authorization Basic Auth 密码。
|
||||
VITE_SENTIMENT_AGENT_AUTH_PASSWORD: "LzqqMTUxMTIwMjQxNjQ2MjY5MDUduC",
|
||||
};
|
||||
@@ -31,16 +31,6 @@ const requiredLegacyFallbacks = [
|
||||
pattern: /<InputGroup className="h-auto min-h-16">/,
|
||||
description: "Prompt textarea must not rely on :has() for its parent height",
|
||||
},
|
||||
{
|
||||
file: "src/core/utils/mermaid.ts",
|
||||
pattern: /htmlLabels:\s*false[\s\S]*secure:\s*\[[\s\S]*"htmlLabels"/,
|
||||
description: "Sandbox flowcharts must use native SVG labels in legacy browsers",
|
||||
},
|
||||
{
|
||||
file: "src/core/utils/legacy-html-preview.ts",
|
||||
pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/,
|
||||
description: "Generated HTML charts must downlevel modern CSS colors for Chrome 109",
|
||||
},
|
||||
];
|
||||
|
||||
for (const file of cssFiles) {
|
||||
|
||||
@@ -36,24 +36,16 @@ import PublicLeaderboardPage from "./pages/PublicLeaderboardPage";
|
||||
import PublicRoundtableSharePage from "./pages/PublicRoundtableSharePage";
|
||||
import EmbedChatPage from "./pages/EmbedChatPage";
|
||||
import EmbedRoundtablePage from "./pages/EmbedRoundtablePage";
|
||||
import SiteStudioPage from "./pages/site-studio/SiteStudioPage";
|
||||
import DashboardStandalonePage from "@/roundtable-planning/pages/DashboardStandalonePage";
|
||||
import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage";
|
||||
import WeKnoraWikiPageView from "@/strategy-components/components/resource-management/llmwiki/WeKnoraWikiPageView";
|
||||
import {
|
||||
isPositionRoundtableStandaloneMode,
|
||||
POSITION_ROUNDTABLE_ROUTE,
|
||||
} from "@/position-roundtable/demo/position-roundtable-demo-config";
|
||||
import { ChatRuntime } from "./pages/WorkspaceRoutes";
|
||||
import { NotFoundPage } from "./pages/NotFoundPage";
|
||||
import OfficialDeerFlowEntry from "./pages/OfficialDeerFlowEntry";
|
||||
|
||||
const PUBLIC_EMBED_PREFIXES = [
|
||||
"/share/",
|
||||
"/public/",
|
||||
"/roundtable-share/",
|
||||
"/embed/knowledge/",
|
||||
];
|
||||
const PUBLIC_EMBED_PREFIXES = ["/share/", "/public/", "/roundtable-share/"];
|
||||
|
||||
function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
const locale = useMemo(() => detectLocale(), []);
|
||||
@@ -110,17 +102,16 @@ function AppProviders({ children }: { children: React.ReactNode }) {
|
||||
forcedTheme={forcedTheme}
|
||||
>
|
||||
<QueryClientProvider>
|
||||
<AuthSyncWatcher disabled={positionStandaloneMode}>
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<ThemeProvider>
|
||||
<ChatProvider>
|
||||
{!positionStandaloneMode && <DesensitizeWordsLoader />}
|
||||
<FontScaleApplier />
|
||||
{children}
|
||||
</ChatProvider>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
</AuthSyncWatcher>
|
||||
<I18nProvider initialLocale={locale}>
|
||||
<ThemeProvider>
|
||||
<ChatProvider>
|
||||
{!positionStandaloneMode && <AuthSyncWatcher />}
|
||||
{!positionStandaloneMode && <DesensitizeWordsLoader />}
|
||||
<FontScaleApplier />
|
||||
{children}
|
||||
</ChatProvider>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
</NextThemesProvider>
|
||||
);
|
||||
@@ -197,17 +188,10 @@ export function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="/embed/roundtable" element={<EmbedRoundtablePage />} />
|
||||
<Route
|
||||
path="/embed/knowledge/:noteId"
|
||||
element={<WeKnoraWikiPageView publicEmbed />}
|
||||
/>
|
||||
<Route
|
||||
path="/roundtable-dashboard"
|
||||
element={<DashboardStandalonePage />}
|
||||
/>
|
||||
<Route path="/official/*" element={<OfficialDeerFlowEntry />} />
|
||||
{/* Web App 站点预览工作室:独立路由 + 独立视觉风格(无主应用 chrome) */}
|
||||
<Route path="/sites/:threadId/:siteName" element={<SiteStudioPage />} />
|
||||
<Route path="/page/*" element={pageElement} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 503 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{fill: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>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#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>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 3840 2160"><image width="3840" height="2160" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><image width="582" height="667" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 664 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="xMidYMid slice" width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 51 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 278 1530"><image width="278" height="1530" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 51 KiB |
@@ -1 +0,0 @@
|
||||
<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,iVBORw0KGgoAAAANSUhEUgAAAiIAAAKGCAYAAAB3OfDSAAAgAElEQVR4nOy9abMlO3YdtpDDme5YVW/ufv16YLNp0gzJCsuOsBz+YDkUIf1ef/QnR9AWJYeowSRlNalmz/3mV1V3OFMmHBsJZO7cB8jhDLdu3cKuypsnkUgkcgIW1h6g/ve/1IgSJcrTk6IElAKuFsDH19qsabtKV0hUtc3FbmcAJgCeAfgMwI8B/ATADwB8YtOvAJwBmNFhANYA7gG8BvBSa3wB4HcAfgHg7zXwdxr4Qmvcao01tNbU8tCfUgO6BEpU66qOGjdL4OsbhZulMvnSJL6kUaI8RcniU40SJQoTAhXXAD6woINAyGca+CGA7yngQ9qvgUsAc1Xlhx3OrLXGjQJea21ACi1TAitaGcDyGwB/AAxIuYs3PUqUKIhAJEqUKEI+BfCPAPwPAP5Ua8OCXGrgwgALhQVvNxyfSoyFBiZa47lhTLQBMj/WwJ+piiX5yjAjGv8OwP8D4G8A3MSbHyVKlAhEokSJkgL4yKph/lxr/BMLRH5qWY1KlF2aFVqKXV0tmnZrzIkxIWCimwN+ZlmWj1S1/lvLkty+808gSpR3WCIQiRIlCgGQ/wXA/6Q1/rwkkKDxoSK1ikJlS4IKTKgAELEYBGUCKF3Ze7idpQMoGh8rpUlV82Ol8N8D+L+0xv8B4D9C4Q7RXC1KlHdSIhCJEuXdlbm1AflnWuNfEAuiNT7VyoKIBLVBaw1AxFpb8GCwhqpAiLbAoyxR79S6Vt88B6lvFH6oYNQ4U6vu+U9WfRPhSJQo75hEIBIlyjsmzjNGA38CjX+lgX+uNf5Mk4GqtvsZAJELOCPCAIkFGoYNUQHQorUynjFaYaqAP1dKX5IBrNbG+PXfQOH3EYxEifJuSQQiUaK8e0IeLH+sNf65Bv4l2YToih0xQrghsWyIWZIqMelhRErLiMAxIUnljkvr0rrlOrBigcsCCv9NooxnTQJlTvEXAD6P72SUKO+ORCASJco7JlrjR4D+lwD+VanVP9KVGy5U0jAZnYtgRAwW0UBiQUYrjxJl2jS4+CFaEYD5LFH6Xygg1Rp3SuEvofBtfC+jRHk3JAKRKFGeuOjGvZa+9w808E+h8b9qjf+u1DgHAwkcMCi004kZ2QEirHxjE2JVO0rXeGNHeLplRlIofKoU/mcAXxvD1crFN8YaiRLlHZAIRKJEeeJCnX1BkVY1XiSVZwyBEHLTvdC6HV21lw0RKhqtG01MbVsiAIzSAuCwtTNkLao8nyml/7dEGw+arwH11/HdjBLl6UsEIlGivAOiNTKt8X0N/LOyNN4xLyQI6ZLELs5+pGUjUjYMRzHyVipu6KqQK4U/KRW+1MBfa43flxqvtMY2vqNRojxdiUAkSpTHKQMhQqe4WGKEId7XGn9awqhjfqS1mlQGHeJETtci2A0nNSti533hRqhgDEhQLxMQ400Dc+y0UPgjKP0/augvAPUfFPCNK/5Y9yRKlCiPRyIQiRLliUrtTqtxpjV+Co1/rDV+WJaYMayxl5ykN7cGrFB4rhT+aaLw+2mGXxc5vtls6yxHQSNRokR5PBLns4wS5TSiDlz2lea8DdvxPoB/Umr8k0KrD0pdzWbbe5a2q209U25pDVPNUnu/tGOJuPVgUU2ckaLAeVGq/zZN8I8WE/3xYqKRp7oOlHYgFnlTzyVKlCgBiYxIlChPTBzIyBJgkkHNJ/ggTfBnZYmfamBKACIZOASpQ7fbDWUtU1vGqmVjtKoFWyK3e89XgRkKX3KdJvhsmuPTosT1coPXKBxhEiVKlKckEYhEiTJMTt3/HbN8TR36LAcu5np+McdHWaJ/VGgTwTTRlhGR7IU1GG3tA2M23LosRLRUwZoctFh7EdootSYm54+Uwo+UUj9ns/Uek50Yq2Uae95okxIlSo9EIBIlyhMTraFJMZOlyOYTPJ9NzGy3z7clMl22r1XLpbErMaJsZFQFFiNEhm1nbrycIdkBO06tM4ApsfPUnGuFH1IANg0T+v2m45AoUaK8pRKBSJR3XU7FdOxb7qHHOU8Z6voXSmly2f0UWl1UAccUs8ewBxBwSBhogGBCWBh3SAChmzxdzIjXloSdT+aFAUPqHNCfao0fmBl629cqJwAeK5qVNUZOxaBE5iTKOysRiESJ8rSk7vg0cFZqfFIW+Ijml3EdfMJAQ81sWEBSqsbfl6b0T5iLbsvVV3SbWgAN87u0C9CmQXSbeSnRLLwuiubESfChBj4EzAy9TqKZSJQoT0giEIny1OXYndbY8k6VXzICSu7RGnOtTfyQ98lkpDVBnf2hArYgBBCS0s6uq+zEdcxSVKn2ca4cXQowss9SOjsRTFDi/bK6hnpSvsA9kmnHZhjGMijHZk4iYxLlyUoEIlGiPA45hSpnojWuyhKXGpgYbxm0IYxkSWBn0SUQUibNCVQpApsJ4KI8qhjDsDDgUzI1TE1/lP4u1oIcpRUuAFzRtexx/V1yqGonSpQoR5IIRKK87XIsxuPUzMWh+2WHqXr2k+S6CmZGS+5UL6rp6GsAUhunwoIQ1YANM1dM0swZ45OSMyIem49eZsQdwxgVVOXNzcR8Foioppp99+dY+8fKUObkWIxJBFJR3nqJQCRKlLdD9gE0Galkykotk2o3I66z9dCNXQiRH9qFbRcsiFnK3XDvXLzGqiIOiQMkNUsijGN3yqxWVKspgLTnWofcjyhRojxCiUAkytsiD818HIvJCKXLkfNQ5gMiXyg0mTLgoySVjM7h4ocILxhtC3DMhWIFun3uR1cwsZYHjnDn9bIi0sXXw6S4gsuq7gmbv8Z3zcIxeW+mZGz6WDk2YxJtS6K89RKBSJQob0b6gEpo20lfbFRltSptI1T7g6Mb4ynDGQ/VrMHYEfQwIm6t4QcfrQisuqlPzZyUgTgjpfe+hO5HCFj0SWRQokR5QxKBSJTHJod2CKdmPMYyHaF8fczI0PJDI/XE9P0am7LE2mhCrM7F2H8wQ1RuE5Jw1Yy2IIXyMG8Zn7SAiLI2J7q93oktwgALOEPCbFbssQUzYZHEzLGYjbGMx6mYk4diTCJTEuXRSAQiUaIcV4YyGnL/UOATYkJ4utOwUH++pAUltpp7njivF9W2CXFsSL2w/BgCRDi4YGvOkJQin1PLlCzwGVMHFQBWFh/RNRXiWvuAWV96SEKqsMicRIlyZIlAJMqbklMzH6diPPrKkx2eBA4yva+DC+ULdqzW9pRsRDZa40aXuAExI1YlU+tsnCcMRy5g3jGqYUI05Fn8V+UAScm2ue1HGVDdlDs2Ihqq1FSfm1LjNdWfARFfLQ5lKFy+UmwjkD60vGMxJkNVTpEpifLWSQQiUaIcJmOBiwQmcjvEqPSl8/2ZLZeAyMsSeAlgXXunWOWGi+ehVTtyag1UhJ1I51WFgAgkwGDMiFXHgIETLoRDtDJ1/47qb68pCTAiUsaquEIAse/Kj2XEGiXKOysRiEQ5tbwtzMe+5ciOq297KNAYW0+enkKRpwlWpVZflhpfKo17XSORKqsu2126ZtYX2lpjaDHJXUgaL5emrJaHDrP98MYRKRtD2sZWRK+01l+WpfpKAysFpNbxp6s2Q5mIQ5mS0PZQOTaA6WPWIlMS5dFKBCJRonTLPkAABzAfx9jOrLHqfVHic1oSpZdAFTq9Vr0kLGKqaoKdmYKYTUgLiPTYiOwAEbbf58pbckbEedWY6O70D/cAPrfLPYsl0sWIhDrksaqUPqak73mNBTxRoryzEoFIlGPJ2858HJvxGKtiObS+iv1KybxDa7XcFvoPidK/S1P9WlkQ4gKbgduMtDUxrd/ae5K2+IBIiba6ZQeIlIwxacUV0RbE6BsN/Fprs9xZIKIOfNf6AMdY4HAoY3Lo+YcCr2MzJRFYRTmaRCAS5anK0M7q2IzHqQDImG3TWZcltluFV0mBL5XCV4nS99BqLoFI4jxUWAh3x4LUhQ70mvExIk4jtOOeK71qWBFlxYi8Lkv8ugR+o4Fb216VAxmRnSqK9VAA0JfvUMYkSpR3XiIQibKv7DsqfWzMx6kYj32BxMGApO4pNdS2VJsE6qskwX9VCX6stf4BNIVMV+3p/Us2o64DIp65ZYa477ofWqYLRgRs3XjTaANCSmBbQn1Ravyy1OoPxO4AmDmPoAMYjbG2IceyKZHH9dmUHMqUhKSPKYm2JFEeXCIQifK2y7GYj1MxHmMBxtGYEQ2Y0O6FVt+WJf6mhP5UQX8AqCnAVDQMeLgExe1FeOGBbobbgrTSJSDhbrpl27vGrTV0UZb4vAT+odTqcw11twcTMnQ7lG8sU9LHkEg5FdCIEuWtkwhEogyVh2ZADmU+hjIdsgMIzt0SWIf2nyp9+LaqAphprb4rNP5aaf1JovTPgPJK6cRkgAMJci4Z3XjOtC5uJCNSpzEGxAGRmhmpjyN/XcOIfKc1/pOG+k9ak8eMOWsunuFYW4+hgOLQ9LFMx1jvm1MzJdGWJMqDSwQiUd4WORXz4STk1XIo43FsgDJmO7dYYlmU+K0C/gtIRQP9EaAvdKlaqhkocXLOkLj0PkaEJWiW4J0UT7ePr9JMJPovSqj/oKH+GsArVBFhtZufT9yLUzEhofR9bUVC5fflj0xJlCcvEYhEkfK2MR99KhXZscsGfkjIdF85ofSHBizB7fqCNdZFqW6h1S8B/PtE6WcK+k+h9bmyRUnVjHYARNytvt4wyIzwOCJobEOcGLsQXRIj8nUJ9XOt1f+roX5h1TK55xr7gEMoXx8w6Ov4hwKDsQDiTTElISbkVLYkEVBLine truncated
|
||||
|
Before Width: | Height: | Size: 193 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(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>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="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>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="458" height="113.588" viewBox="0 0 458 113.588"><defs><style>.a{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
|
||||
|
Before Width: | Height: | Size: 625 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{opacity:0;fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.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>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="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
|
||||
|
Before Width: | Height: | Size: 68 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68">
|
||||
<defs>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,14 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68">
|
||||
<defs>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 924 B |
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="2064"
|
||||
height="2591" viewBox="0 0 2064 2591">
|
||||
<path fill-rule="nonzero"
|
||||
d="M 68.582031 0 C 66.890625 0 65.300781 1 64.617188 2.640625 C 49.46875 38.800781 33.71875 76.199219 33.628906 106.539062 C 33.550781 134.230469 46.445312 155.730469 59.949219 175.71875 L 59.949219 175.730469 C 52.414062 175.671875 44.992188 175.371094 37.472656 174.171875 C 27.210938 172.53125 16.585938 169.199219 5.585938 165.738281 C 5.15625 165.609375 4.722656 165.539062 4.296875 165.539062 C 2.824219 165.539062 1.417969 166.300781 0.625 167.609375 C -0.402344 169.300781 -0.140625 171.46875 1.257812 172.871094 L 1.261719 172.878906 C 11.230469 182.871094 21.8125 193.53125 36.980469 200.050781 C 51.5625 206.320312 70.167969 208.628906 88.085938 210.78125 L 88.027344 210.78125 C 127.152344 253.410156 166.617188 296.300781 215.761719 334.941406 C 230.558594 346.578125 246.292969 357.878906 261.785156 369 C 277.171875 380.050781 292.441406 391.011719 306.660156 402.191406 C 326.132812 417.511719 343.5625 433.179688 356.773438 450.019531 C 377.808594 476.828125 388.449219 506.960938 395.4375 538.628906 L 395.4375 538.640625 C 372.683594 510.710938 349.328125 483.929688 324.855469 469.339844 C 295.957031 452.121094 265.800781 452 236.960938 451.808594 L 236.933594 451.820312 C 235.792969 451.808594 234.707031 452.261719 233.898438 453.070312 C 233.085938 453.871094 232.632812 454.96875 232.636719 456.121094 C 232.722656 494.011719 232.699219 533.539062 256.304688 567.480469 C 279.351562 600.621094 324.738281 628.019531 369.191406 654.679688 L 369.132812 654.691406 C 371.476562 683.539062 373.40625 710.691406 367.109375 728.03125 C 365.828125 731.558594 364.191406 734.71875 362.058594 738.820312 C 358.910156 744.871094 354.964844 752.441406 350.417969 764.960938 C 342.890625 785.699219 333.769531 819.839844 323.648438 883.109375 C 303.347656 1010.019531 278.722656 1255.949219 308.882812 1409.269531 C 326.265625 1497.660156 361.828125 1555.210938 393.511719 1606.519531 C 417.042969 1644.632812 438.207031 1678.960938 448.835938 1720.09375 C 466.921875 1790.089844 454.648438 1880.246094 444.5 1954.386719 C 440.613281 1982.78125 437.042969 2008.839844 435.726562 2030.261719 C 433.410156 2067.929688 438.066406 2091.179688 442.304688 2112.347656 L 442.304688 2112.355469 C 445.910156 2130.347656 449.152344 2146.589844 447.757812 2169.28125 C 447.429688 2174.613281 446.835938 2180.359375 445.933594 2186.535156 L 445.933594 2186.542969 C 436.582031 2250.328125 393.613281 2362.808594 362.105469 2416.636719 C 339.761719 2454.804688 323.546875 2462.851562 309.597656 2469.925781 C 303.980469 2472.777344 298.261719 2475.660156 292.691406 2480.238281 C 273.378906 2496.113281 256.457031 2532.078125 240.183594 2566.417969 C 239.578125 2567.703125 239.640625 2569.203125 240.363281 2570.429688 C 241.082031 2571.65625 242.359375 2572.449219 243.777344 2572.542969 L 243.789062 2572.542969 L 243.882812 2572.550781 L 243.882812 2572.554688 C 261.582031 2573.746094 279.460938 2574.957031 296.386719 2574.957031 C 323.671875 2574.957031 349.332031 2571.84375 368.355469 2559.785156 C 392.640625 2544.394531 405.109375 2514.960938 414.488281 2493.074219 C 417.011719 2487.183594 419.289062 2481.871094 421.546875 2477.308594 C 432.054688 2456.039062 441.753906 2451.664062 451.855469 2446.960938 C 453.054688 2446.402344 453.929688 2445.320312 454.226562 2444.042969 L 454.238281 2443.996094 L 454.238281 2443.988281 C 476.210938 2348.800781 498.035156 2254.347656 524.300781 2196.363281 L 524.300781 2196.367188 C 536.363281 2169.730469 549.363281 2150.796875 562.695312 2131.402344 C 576.28125 2111.625 590.1875 2091.402344 603.300781 2062.445312 L 603.304688 2062.453125 C 605.183594 2058.292969 607.054688 2053.949219 608.894531 2049.421875 C 625.066406 2009.675781 639.746094 1954.289062 653.585938 1902.101562 C 663.0625 1866.367188 672.15625 1832.09375 681.085938 1805.085938 C 699.308594 1749.992188 716.863281 1725.09375 734.371094 1705.144531 L 734.421875 1705.148438 C 727.285156 1740.746094 721.597656 1776.628906 729.320312 1807.472656 C 733.351562 1823.570312 740.996094 1838.160156 749.066406 1853.613281 C 758.828125 1872.296875 769.433594 1892.636719 776.359375 1919.675781 C 783.289062 1946.71875 786.574219 1980.605469 789.527344 2011.140625 L 789.527344 2011.148438 C 791.875 2035.402344 794.023438 2057.78125 797.746094 2072.316406 L 797.746094 2072.320312 C 797.929688 2073.023438 798.128906 2073.769531 798.316406 2074.441406 L 798.320312 2074.441406 C 802.132812 2087.941406 807.617188 2093.914062 813.515625 2100.324219 C 821.132812 2108.597656 830.351562 2118.441406 839.742188 2151.671875 L 839.738281 2151.675781 C 840.105469 2152.96875 840.402344 2154.039062 840.765625 2155.398438 C 857.484375 2217.601562 874.097656 2354.589844 872.078125 2423.390625 C 870.472656 2478.15625 857.140625 2488.390625 845.339844 2498.136719 C 842.320312 2500.632812 839.085938 2503.296875 836.09375 2506.566406 C 820.847656 2523.230469 812.652344 2554.886719 804.800781 2584.753906 C 804.457031 2586.0625 804.75 2587.453125 805.589844 2588.515625 C 806.40625 Line truncated
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="12" viewBox="0 0 14 12" fill="none" aria-hidden="true">
|
||||
<path d="M13 6H1.5M1.5 6L6.2 1.4M1.5 6L6.2 10.6" stroke="#A2E1F9" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 258 B |
@@ -34,13 +34,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { PromptInputFilePart, UploadPolicy } from "@/core/uploads";
|
||||
import {
|
||||
DEFAULT_UPLOAD_POLICY,
|
||||
splitUnsupportedUploadFiles,
|
||||
useUploadPolicy,
|
||||
validateChatUploadFiles,
|
||||
} from "@/core/uploads";
|
||||
import type { PromptInputFilePart } from "@/core/uploads";
|
||||
import { splitUnsupportedUploadFiles } from "@/core/uploads";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ChatStatus } from "ai";
|
||||
@@ -61,11 +56,12 @@ import {
|
||||
type ChangeEventHandler,
|
||||
Children,
|
||||
type ClipboardEventHandler,
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FormEvent,
|
||||
type FormEventHandler,
|
||||
type HTMLAttributes,
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FormEvent,
|
||||
type FormEventHandler,
|
||||
Fragment,
|
||||
type HTMLAttributes,
|
||||
type KeyboardEventHandler,
|
||||
type PropsWithChildren,
|
||||
type ReactNode,
|
||||
@@ -128,11 +124,9 @@ export const usePromptInputController = () => {
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Optional variants (do NOT throw). Useful for dual-mode components and for
|
||||
// Fast Refresh transitions where a page can briefly retain the previous
|
||||
// provider context instance.
|
||||
export const useOptionalPromptInputController = () =>
|
||||
useContext(PromptInputController);
|
||||
// Optional variants (do NOT throw). Useful for dual-mode components.
|
||||
const useOptionalPromptInputController = () =>
|
||||
useContext(PromptInputController);
|
||||
|
||||
export const useProviderAttachments = () => {
|
||||
const ctx = useContext(ProviderAttachmentsContext);
|
||||
@@ -148,94 +142,73 @@ const useOptionalProviderAttachments = () =>
|
||||
useContext(ProviderAttachmentsContext);
|
||||
const usePromptInputValidation = () => useContext(PromptInputValidationContext);
|
||||
|
||||
export type PromptInputProviderProps = PropsWithChildren<{
|
||||
initialInput?: string;
|
||||
uploadPolicy?: UploadPolicy;
|
||||
}>;
|
||||
export type PromptInputProviderProps = PropsWithChildren<{
|
||||
initialInput?: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Optional global provider that lifts PromptInput state outside of PromptInput.
|
||||
* If you don't use it, PromptInput stays fully self-managed.
|
||||
*/
|
||||
export function PromptInputProvider({
|
||||
initialInput: initialTextInput = "",
|
||||
uploadPolicy,
|
||||
children,
|
||||
}: PromptInputProviderProps) {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
// ----- textInput state
|
||||
export function PromptInputProvider({
|
||||
initialInput: initialTextInput = "",
|
||||
children,
|
||||
}: PromptInputProviderProps) {
|
||||
// ----- textInput state
|
||||
const [textInput, setTextInput] = useState(initialTextInput);
|
||||
const clearInput = useCallback(() => setTextInput(""), []);
|
||||
|
||||
// ----- attachments state (global when wrapped)
|
||||
const [attachmentFiles, setAttachmentFiles] = useState<
|
||||
(PromptInputFilePart & { id: string })[]
|
||||
>([]);
|
||||
const attachmentsRef = useRef(attachmentFiles);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const openRef = useRef<() => void>(() => {});
|
||||
|
||||
const add = useCallback((files: File[] | FileList) => {
|
||||
const incoming = Array.from(files);
|
||||
>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const openRef = useRef<() => void>(() => {});
|
||||
|
||||
const add = useCallback((files: File[] | FileList) => {
|
||||
const incoming = Array.from(files);
|
||||
if (incoming.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = attachmentsRef.current;
|
||||
const existing = current.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
incoming,
|
||||
existing,
|
||||
effectiveUploadPolicy,
|
||||
);
|
||||
for (const message of new Set(
|
||||
validation.issues.map((issue) => issue.message),
|
||||
)) {
|
||||
toast.error(message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
const next = current.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, [effectiveUploadPolicy]);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
const current = attachmentsRef.current;
|
||||
const found = current.find((f) => f.id === id);
|
||||
if (found?.url) URL.revokeObjectURL(found.url);
|
||||
const next = current.filter((f) => f.id !== id);
|
||||
attachmentsRef.current = next;
|
||||
setAttachmentFiles(next);
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
for (const f of attachmentsRef.current) {
|
||||
if (f.url) {
|
||||
URL.revokeObjectURL(f.url);
|
||||
}
|
||||
}
|
||||
attachmentsRef.current = [];
|
||||
setAttachmentFiles([]);
|
||||
}, []);
|
||||
|
||||
// Keep a ref to attachments for cleanup on unmount (avoids stale closure)
|
||||
attachmentsRef.current = attachmentFiles;
|
||||
return;
|
||||
}
|
||||
|
||||
setAttachmentFiles((prev) =>
|
||||
prev.concat(
|
||||
incoming.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const remove = useCallback((id: string) => {
|
||||
setAttachmentFiles((prev) => {
|
||||
const found = prev.find((f) => f.id === id);
|
||||
if (found?.url) {
|
||||
URL.revokeObjectURL(found.url);
|
||||
}
|
||||
return prev.filter((f) => f.id !== id);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setAttachmentFiles((prev) => {
|
||||
for (const f of prev) {
|
||||
if (f.url) {
|
||||
URL.revokeObjectURL(f.url);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keep a ref to attachments for cleanup on unmount (avoids stale closure)
|
||||
const attachmentsRef = useRef(attachmentFiles);
|
||||
attachmentsRef.current = attachmentFiles;
|
||||
|
||||
// Cleanup blob URLs on unmount to prevent memory leaks
|
||||
useEffect(() => {
|
||||
@@ -430,14 +403,14 @@ export function PromptInputAttachments({
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-wrap items-center gap-2 p-3", className)}
|
||||
{...props}
|
||||
>
|
||||
{attachments.files.map((file) => (
|
||||
<div key={file.id} className="max-w-60">
|
||||
{children(file)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{...props}
|
||||
>
|
||||
{attachments.files.map((file) => (
|
||||
<Fragment key={file.id}>
|
||||
<div className="max-w-60">{children(file)}</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -483,17 +456,10 @@ export type PromptInputProps = Omit<
|
||||
// Render a hidden input with given name and keep it in sync for native form posts. Default false.
|
||||
syncHiddenInput?: boolean;
|
||||
// Minimal constraints
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // bytes
|
||||
uploadPolicy?: UploadPolicy;
|
||||
onError?: (err: {
|
||||
code:
|
||||
| "max_files"
|
||||
| "max_file_size"
|
||||
| "max_total_size"
|
||||
| "unsupported_type"
|
||||
| "accept"
|
||||
| "unsupported_package";
|
||||
maxFiles?: number;
|
||||
maxFileSize?: number; // bytes
|
||||
onError?: (err: {
|
||||
code: "max_files" | "max_file_size" | "accept" | "unsupported_package";
|
||||
message: string;
|
||||
}) => void;
|
||||
onSubmit: (
|
||||
@@ -509,25 +475,14 @@ export const PromptInput = ({
|
||||
multiple,
|
||||
globalDrop,
|
||||
syncHiddenInput,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
uploadPolicy,
|
||||
maxFiles,
|
||||
maxFileSize,
|
||||
onError,
|
||||
onSubmit,
|
||||
children,
|
||||
...props
|
||||
}: PromptInputProps) => {
|
||||
const { data: fetchedUploadPolicy } = useUploadPolicy();
|
||||
const effectiveUploadPolicy =
|
||||
uploadPolicy ?? fetchedUploadPolicy ?? DEFAULT_UPLOAD_POLICY;
|
||||
const policyAccept = useMemo(
|
||||
() =>
|
||||
Object.values(effectiveUploadPolicy.groups)
|
||||
.flatMap((group) => group.extensions)
|
||||
.join(","),
|
||||
[effectiveUploadPolicy],
|
||||
);
|
||||
// Try to use a provider controller if present
|
||||
}: PromptInputProps) => {
|
||||
// Try to use a provider controller if present
|
||||
const controller = useOptionalPromptInputController();
|
||||
const usingProvider = !!controller;
|
||||
|
||||
@@ -560,11 +515,8 @@ export const PromptInput = ({
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return patterns.some((pattern) => {
|
||||
if (pattern.startsWith(".")) {
|
||||
return f.name.toLowerCase().endsWith(pattern.toLowerCase());
|
||||
}
|
||||
if (pattern.endsWith("/*")) {
|
||||
return patterns.some((pattern) => {
|
||||
if (pattern.endsWith("/*")) {
|
||||
const prefix = pattern.slice(0, -1); // e.g: image/* -> image/
|
||||
return f.type.startsWith(prefix);
|
||||
}
|
||||
@@ -574,72 +526,57 @@ export const PromptInput = ({
|
||||
[accept],
|
||||
);
|
||||
|
||||
const addLocal = useCallback(
|
||||
(fileList: File[] | FileList) => {
|
||||
const incoming = Array.from(fileList);
|
||||
const accepted = incoming.filter((f) => matchesAccept(f));
|
||||
if (accepted.length < incoming.length) {
|
||||
const error = {
|
||||
code: "accept",
|
||||
message: "部分文件不符合当前输入框允许的格式。",
|
||||
} as const;
|
||||
onError?.(error);
|
||||
if (!onError) toast.error(error.message);
|
||||
}
|
||||
const localPolicy: UploadPolicy = {
|
||||
...effectiveUploadPolicy,
|
||||
max_files_per_message:
|
||||
typeof maxFiles === "number"
|
||||
? Math.min(effectiveUploadPolicy.max_files_per_message, maxFiles)
|
||||
: effectiveUploadPolicy.max_files_per_message,
|
||||
groups: Object.fromEntries(
|
||||
Object.entries(effectiveUploadPolicy.groups).map(([key, group]) => [
|
||||
key,
|
||||
{
|
||||
...group,
|
||||
max_bytes:
|
||||
typeof maxFileSize === "number"
|
||||
? Math.min(group.max_bytes, maxFileSize)
|
||||
: group.max_bytes,
|
||||
},
|
||||
]),
|
||||
) as UploadPolicy["groups"],
|
||||
};
|
||||
const existing = items.map((item) => ({
|
||||
name: item.filename ?? "attachment",
|
||||
size: item.file?.size ?? 0,
|
||||
type: item.mediaType ?? "",
|
||||
}));
|
||||
const validation = validateChatUploadFiles(
|
||||
accepted,
|
||||
existing,
|
||||
localPolicy,
|
||||
);
|
||||
for (const issue of Array.from(
|
||||
new Map(
|
||||
validation.issues.map((item) => [item.message, item]),
|
||||
).values(),
|
||||
)) {
|
||||
onError?.({ code: issue.code, message: issue.message });
|
||||
if (!onError) toast.error(issue.message);
|
||||
}
|
||||
if (validation.accepted.length === 0) return;
|
||||
|
||||
setItems((prev) =>
|
||||
prev.concat(
|
||||
validation.accepted.map((file) => ({
|
||||
id: nanoid(),
|
||||
type: "file" as const,
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
})),
|
||||
),
|
||||
);
|
||||
},
|
||||
[effectiveUploadPolicy, items, matchesAccept, maxFiles, maxFileSize, onError],
|
||||
);
|
||||
const addLocal = useCallback(
|
||||
(fileList: File[] | FileList) => {
|
||||
const incoming = Array.from(fileList);
|
||||
const accepted = incoming.filter((f) => matchesAccept(f));
|
||||
if (incoming.length && accepted.length === 0) {
|
||||
onError?.({
|
||||
code: "accept",
|
||||
message: "No files match the accepted types.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const withinSize = (f: File) =>
|
||||
maxFileSize ? f.size <= maxFileSize : true;
|
||||
const sized = accepted.filter(withinSize);
|
||||
if (accepted.length > 0 && sized.length === 0) {
|
||||
onError?.({
|
||||
code: "max_file_size",
|
||||
message: "All files exceed the maximum size.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setItems((prev) => {
|
||||
const capacity =
|
||||
typeof maxFiles === "number"
|
||||
? Math.max(0, maxFiles - prev.length)
|
||||
: undefined;
|
||||
const capped =
|
||||
typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
||||
if (typeof capacity === "number" && sized.length > capacity) {
|
||||
onError?.({
|
||||
code: "max_files",
|
||||
message: "Too many files. Some were not added.",
|
||||
});
|
||||
}
|
||||
const next: (PromptInputFilePart & { id: string })[] = [];
|
||||
for (const file of capped) {
|
||||
next.push({
|
||||
id: nanoid(),
|
||||
type: "file",
|
||||
url: URL.createObjectURL(file),
|
||||
mediaType: file.type,
|
||||
filename: file.name,
|
||||
file,
|
||||
});
|
||||
}
|
||||
return prev.concat(next);
|
||||
});
|
||||
},
|
||||
[matchesAccept, maxFiles, maxFileSize, onError],
|
||||
);
|
||||
|
||||
const removeLocal = useCallback(
|
||||
(id: string) =>
|
||||
@@ -894,7 +831,7 @@ export const PromptInput = ({
|
||||
const inner = (
|
||||
<PromptInputValidationContext.Provider value={sanitizeIncomingFiles}>
|
||||
<input
|
||||
accept={accept ?? policyAccept}
|
||||
accept={accept}
|
||||
aria-label="Upload files"
|
||||
className="hidden"
|
||||
multiple={multiple}
|
||||
|
||||
@@ -165,7 +165,7 @@ export const ReasoningTrigger = memo(
|
||||
export type ReasoningContentProps = ComponentProps<
|
||||
typeof CollapsibleContent
|
||||
> & {
|
||||
children: ReactNode;
|
||||
children: string;
|
||||
};
|
||||
|
||||
export const ReasoningContent = memo(
|
||||
@@ -178,11 +178,7 @@ export const ReasoningContent = memo(
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{typeof children === "string" ? (
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
</CollapsibleContent>
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { FileOutput } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import responsibilityIcon from "@/assets/position-roundtable/责任岗位.svg";
|
||||
import resultTimeIcon from "@/assets/position-roundtable/时间.svg";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import "./analysis-results.css";
|
||||
import type { AnalysisResultCardItem } from "./types";
|
||||
|
||||
function formatResultTime(value: string | undefined, fallback: string): string {
|
||||
if (!value) return fallback;
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function AnalysisResultCardList({
|
||||
items,
|
||||
emptyTitle = "暂无交付产物",
|
||||
emptyDescription = "第二步研讨与第三步总结生成 Markdown 后,产物会显示在这里。",
|
||||
padded = true,
|
||||
onOpen,
|
||||
}: {
|
||||
items: AnalysisResultCardItem[];
|
||||
emptyTitle?: string;
|
||||
emptyDescription?: string;
|
||||
padded?: boolean;
|
||||
onOpen: (item: AnalysisResultCardItem) => void;
|
||||
}) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="position-roundtable-analysis-results-empty flex h-full min-h-[260px] flex-col items-center justify-center p-6 text-center">
|
||||
<FileOutput className="mb-3 size-8 text-muted-foreground/50" />
|
||||
<p className="text-sm font-semibold">{emptyTitle}</p>
|
||||
<p className="mt-1 max-w-[240px] text-xs leading-5 text-muted-foreground">
|
||||
{emptyDescription}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("analysis-result-card-list", padded && "p-[15px]")}>
|
||||
{items.map((item, index) => {
|
||||
const displayedTime = formatResultTime(
|
||||
item.updatedAt,
|
||||
item.fallbackTime || "刚刚",
|
||||
);
|
||||
const displayedProductName = item.delivered ? item.productName : "未交付";
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className="position-roundtable-analysis-result-card"
|
||||
onClick={() => {
|
||||
if (!item.delivered) {
|
||||
toast.info(
|
||||
item.unavailableMessage ||
|
||||
`「${desensitize(item.agentName)}」尚未交付可预览的 Markdown 文件。`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
onOpen(item);
|
||||
}}
|
||||
>
|
||||
<div className="position-roundtable-analysis-result-card-body">
|
||||
<div
|
||||
className={cn(
|
||||
"position-roundtable-analysis-result-tag",
|
||||
`position-roundtable-analysis-result-tag--${(index % 6) + 1}`,
|
||||
)}
|
||||
aria-label={desensitize(item.agentName)}
|
||||
>
|
||||
<span>{desensitize(item.agentName)}</span>
|
||||
</div>
|
||||
<p className="position-roundtable-analysis-result-product">
|
||||
{desensitize(displayedProductName)}
|
||||
</p>
|
||||
<footer className="position-roundtable-analysis-result-meta">
|
||||
<span>
|
||||
<img src={responsibilityIcon} alt="" aria-hidden="true" />
|
||||
{desensitize(item.positionName)}
|
||||
</span>
|
||||
<i aria-hidden="true" />
|
||||
<span>
|
||||
<img src={resultTimeIcon} alt="" aria-hidden="true" />
|
||||
{displayedTime}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FileOutput, Loader2 } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ArtifactFilePreview } from "@/components/workspace/artifacts";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { loadArtifactContent } from "@/core/artifacts/loader";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { Button as TButton } from "tdesign-react";
|
||||
|
||||
import type { AnalysisResultCardItem } from "./types";
|
||||
|
||||
function isArtifactErrorContent(content: string | null | undefined): boolean {
|
||||
const trimmed = content?.trim();
|
||||
if (!trimmed || !trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
const detail = parsed.detail ?? parsed.message ?? parsed.error;
|
||||
return typeof detail === "string" && detail.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackContent(item: AnalysisResultCardItem | null): string {
|
||||
return (
|
||||
item?.artifactContent?.trim() ||
|
||||
item?.summary?.trim() ||
|
||||
"暂无产物正文"
|
||||
);
|
||||
}
|
||||
|
||||
export function AnalysisResultDetailDialog({
|
||||
item,
|
||||
onOpenChange,
|
||||
extraBody,
|
||||
footerStart,
|
||||
}: {
|
||||
item: AnalysisResultCardItem | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
extraBody?: ReactNode;
|
||||
footerStart?: ReactNode;
|
||||
}) {
|
||||
const artifactThreadId = item?.artifactThreadId || null;
|
||||
const artifactPathValue = item?.artifactPath || "";
|
||||
const artifactContentQuery = useQuery({
|
||||
queryKey: [
|
||||
"analysis-results",
|
||||
"deliverable-artifact-content",
|
||||
artifactThreadId,
|
||||
artifactPathValue,
|
||||
],
|
||||
queryFn: async () => {
|
||||
if (!artifactThreadId || !artifactPathValue) return "";
|
||||
const result = await loadArtifactContent({
|
||||
filepath: artifactPathValue,
|
||||
threadId: artifactThreadId,
|
||||
isMock: false,
|
||||
});
|
||||
return result.content;
|
||||
},
|
||||
enabled: Boolean(item && artifactThreadId && artifactPathValue),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const loadedArtifactContent =
|
||||
typeof artifactContentQuery.data === "string" ? artifactContentQuery.data : "";
|
||||
const loadedArtifactIsError = isArtifactErrorContent(loadedArtifactContent);
|
||||
const artifactContent =
|
||||
loadedArtifactContent.trim() && !loadedArtifactIsError ? loadedArtifactContent : null;
|
||||
const displayedContent = artifactContent || fallbackContent(item);
|
||||
const artifactLoadFailed =
|
||||
(artifactContentQuery.isError || loadedArtifactIsError) &&
|
||||
!item?.artifactContent?.trim();
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(item)} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
overlayClassName="!z-[6000]"
|
||||
className="!z-[6001] flex max-h-[86vh] w-[900px] !max-w-[calc(100vw-2rem)] flex-col gap-0 overflow-hidden p-0"
|
||||
>
|
||||
<DialogHeader className="shrink-0 border-b px-5 py-4 dark:border-[#2b8ffe]/25">
|
||||
<DialogTitle className="flex items-center gap-2 text-base">
|
||||
<FileOutput className="size-4 text-primary" />
|
||||
交付产物详情
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{!item ? null : (
|
||||
<div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-extrabold text-slate-900 dark:text-[#e8edf5]">
|
||||
{desensitize(item.productName)}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-xs text-muted-foreground">
|
||||
{desensitize(item.agentName)} · {desensitize(item.positionName)}
|
||||
</p>
|
||||
</div>
|
||||
{extraBody}
|
||||
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-3 dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/65">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-bold text-slate-700 dark:text-[#e8edf5]">产物内容</p>
|
||||
</div>
|
||||
<div className="h-[52vh] min-h-[320px] overflow-hidden rounded-lg border border-slate-100 bg-slate-50/60 dark:border-[#2b8ffe]/20 dark:bg-[#021d3d]/70">
|
||||
{artifactContentQuery.isLoading ? (
|
||||
<div className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground dark:text-[#8aa0c4]">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
正在加载 Markdown 文件内容…
|
||||
</div>
|
||||
) : (
|
||||
<ArtifactFilePreview
|
||||
content={displayedContent}
|
||||
language="markdown"
|
||||
threadId={artifactThreadId ?? ""}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{artifactLoadFailed && (
|
||||
<p className="mt-2 text-[11px] text-amber-600 dark:text-amber-300">
|
||||
Markdown 文件内容加载失败,当前展示的是已保存正文。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="shrink-0 border-t px-5 py-3 dark:border-[#2b8ffe]/25">
|
||||
{footerStart}
|
||||
<TButton variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</TButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
.analysis-result-card-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 102px;
|
||||
box-sizing: border-box;
|
||||
padding: 40px 12px 12px;
|
||||
border: 1px solid rgba(85, 166, 254, 0.1);
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 98, 217, 0.1);
|
||||
color: #ffffff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card:hover,
|
||||
.position-roundtable-analysis-result-card:focus-visible {
|
||||
border-color: #0062d9;
|
||||
background: rgba(0, 98, 217, 0.3);
|
||||
box-shadow: 0 4px 14px rgba(0, 44, 110, 0.18);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-card-body {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: -12px;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: calc(100% + 12px);
|
||||
height: 26px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0 22px 0 12px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px 0 0 0;
|
||||
background: rgba(212, 120, 0, 0.5);
|
||||
color: #ffffff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
clip-path: polygon(0 0, 100% 0, calc(100% - 14px) 100%, 0 100%);
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-tag--1 { background: rgba(212, 120, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--2 { background: rgba(207, 19, 34, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--3 { background: rgba(29, 171, 123, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--4 { background: rgba(132, 85, 230, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--5 { background: rgba(201, 181, 0, 0.5); }
|
||||
.position-roundtable-analysis-result-tag--6 { background: rgba(69, 195, 243, 0.5); }
|
||||
|
||||
.position-roundtable-analysis-result-tag span {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-product {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
color: #80b7ff;
|
||||
font-family: "Noto Sans S Chinese", "Noto Sans SC", sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta span {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-result-meta i {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
background: rgba(128, 183, 255, 0.45);
|
||||
}
|
||||
|
||||
.position-roundtable-analysis-results-empty {
|
||||
color: #80b7ff;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { AnalysisResultCardList } from "./AnalysisResultCardList";
|
||||
export { AnalysisResultDetailDialog } from "./AnalysisResultDetailDialog";
|
||||
export type { AnalysisResultCardItem } from "./types";
|
||||
@@ -1,17 +0,0 @@
|
||||
export interface AnalysisResultCardItem {
|
||||
id: string;
|
||||
agentName: string;
|
||||
productName: string;
|
||||
positionName: string;
|
||||
updatedAt?: string;
|
||||
/** Shown when `updatedAt` is missing. */
|
||||
fallbackTime?: string;
|
||||
/** Clickable preview is available. */
|
||||
delivered: boolean;
|
||||
artifactPath?: string;
|
||||
artifactMimeType?: string | null;
|
||||
artifactThreadId?: string | null;
|
||||
artifactContent?: string | null;
|
||||
summary?: string;
|
||||
unavailableMessage?: string;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { prepareGeneratedHtml } from "@/core/utils/legacy-html-preview";
|
||||
import { injectInPageAnchorHandler } from "@/core/utils/markdown";
|
||||
|
||||
/**
|
||||
* Sandboxed preview of a generated HTML document.
|
||||
@@ -9,8 +9,8 @@ import { prepareGeneratedHtml } from "@/core/utils/legacy-html-preview";
|
||||
* is `about:srcdoc`, so clicking an in-page anchor (`<a href="#section">`)
|
||||
* navigates the frame to `about:blank#section` and BLANKS it. A `blob:` URL is
|
||||
* a real document URL, so fragment links scroll within the frame as expected —
|
||||
* The sandbox permits scripts so the preview can use the app-bundled HTML
|
||||
* visualization runtimes, while still withholding same-origin privileges.
|
||||
* all without enabling scripts (the sandbox stays script-free for security; the
|
||||
* server already sanitizes script vectors and this is defense-in-depth).
|
||||
*
|
||||
* The `charset=utf-8` on the blob type is required: a blob: URL whose
|
||||
* Content-Type omits the charset is decoded with the OS default (GBK/Latin-1 on
|
||||
@@ -26,9 +26,7 @@ export function HtmlPreviewFrame({
|
||||
const [url, setUrl] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const blob = new Blob([
|
||||
prepareGeneratedHtml(html ?? ""),
|
||||
], {
|
||||
const blob = new Blob([injectInPageAnchorHandler(html ?? "")], {
|
||||
type: "text/html;charset=utf-8",
|
||||
});
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import deerMaskUrl from "@/assets/deer.svg";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
||||
import Galaxy from "@/components/ui/galaxy";
|
||||
@@ -30,11 +29,7 @@ export function Hero({ className }: { className?: string }) {
|
||||
/>
|
||||
</div>
|
||||
<FlickeringGrid
|
||||
className="absolute inset-0 z-0 translate-y-8 mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
||||
style={{
|
||||
WebkitMaskImage: `url("${deerMaskUrl}")`,
|
||||
maskImage: `url("${deerMaskUrl}")`,
|
||||
}}
|
||||
className="absolute inset-0 z-0 translate-y-8 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
||||
squareSize={4}
|
||||
gridGap={4}
|
||||
color={"white"}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useEffect, useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Bell,
|
||||
Cpu,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
FolderOpen,
|
||||
Gauge,
|
||||
ListTree,
|
||||
MessageSquareText,
|
||||
MousePointerClick,
|
||||
BriefcaseBusiness,
|
||||
Settings,
|
||||
@@ -56,7 +54,7 @@ type PageSidebarProps = {
|
||||
headerHidden?: boolean;
|
||||
};
|
||||
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "encoder" | "about";
|
||||
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
||||
|
||||
const hasActiveChild = (item: MenuItem, pathname: string): boolean =>
|
||||
menuItemContainsPath(item, pathname);
|
||||
@@ -222,12 +220,14 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
||||
<User className="size-4" />
|
||||
个人信息
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/menu-management")}
|
||||
>
|
||||
<ListTree className="size-4" />
|
||||
{isAdmin ? "默认菜单管理" : "我的菜单"}
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/menu-management")}
|
||||
>
|
||||
<ListTree className="size-4" />
|
||||
菜单管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/task-buttons")}
|
||||
@@ -236,14 +236,6 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
||||
按钮管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/fixed-questions")}
|
||||
>
|
||||
<MessageSquareText className="size-4" />
|
||||
问题配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/admin/position-roles")}
|
||||
@@ -260,20 +252,6 @@ function SecondaryNavSidebar({ headerHidden }: { headerHidden: boolean }) {
|
||||
白名单管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/workspace/admin/models")}
|
||||
>
|
||||
<Cpu className="size-4" />
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
本地编码器
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => navigate("/page/strategy/settings/model-concurrency")}
|
||||
@@ -559,40 +537,24 @@ function LegacyAccordionSidebar({
|
||||
<User className="size-4" />
|
||||
个人信息
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/menu-management")}>
|
||||
<ListTree className="size-4" />
|
||||
{isAdmin ? "默认菜单管理" : "我的菜单"}
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/menu-management")}>
|
||||
<ListTree className="size-4" />
|
||||
菜单管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/task-buttons")}>
|
||||
<MousePointerClick className="size-4" />
|
||||
按钮管理
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/fixed-questions")}>
|
||||
<MessageSquareText className="size-4" />
|
||||
问题配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/position-roles")}>
|
||||
<BriefcaseBusiness className="size-4" />
|
||||
岗位角色配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/workspace/admin/models")}>
|
||||
<Cpu className="size-4" />
|
||||
模型配置
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => openSettings("encoder")}>
|
||||
<Cpu className="size-4" />
|
||||
本地编码器
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<DropdownMenuItem onClick={() => navigate("/page/strategy/admin/login-whitelist")}>
|
||||
<ShieldCheck className="size-4" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -20,12 +21,12 @@ export function Breadcrumb({
|
||||
)}
|
||||
>
|
||||
{segments.map((segment, i) => (
|
||||
<div className="contents" key={`${segment}-${i}`}>
|
||||
<Fragment key={`${segment}-${i}`}>
|
||||
<span className="hidden shrink-0 whitespace-nowrap sm:inline">
|
||||
{segment}
|
||||
</span>
|
||||
<ChevronRight className="hidden size-3 shrink-0 text-slate-300 sm:inline dark:text-slate-600" />
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
<span
|
||||
className="truncate text-[13px] font-medium text-slate-600 dark:text-slate-300"
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
|
||||
import { languages } from "@codemirror/language-data";
|
||||
import { basicLightInit } from "@uiw/codemirror-theme-basic";
|
||||
import { monokaiInit } from "@uiw/codemirror-theme-monokai";
|
||||
import CodeMirror from "@uiw/react-codemirror";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
|
||||
const lightTheme = basicLightInit({ settings: { background: "transparent", fontSize: "14px" } });
|
||||
const darkTheme = monokaiInit({
|
||||
settings: { background: "transparent", gutterBackground: "transparent", fontSize: "14px" },
|
||||
});
|
||||
|
||||
/** Markdown-aware source editor shared by Wiki create/edit dialogs. */
|
||||
export function MarkdownCodeEditor({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const extensions = useMemo(
|
||||
() => [markdown({ base: markdownLanguage, codeLanguages: languages })],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("grid min-h-[46vh] overflow-hidden rounded-lg border bg-background lg:grid-cols-2", className)}>
|
||||
<section className="flex min-h-0 flex-col border-b lg:border-r lg:border-b-0">
|
||||
<div className="flex h-9 shrink-0 items-center border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground">
|
||||
Markdown 编辑
|
||||
</div>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
theme={resolvedTheme === "dark" ? darkTheme : lightTheme}
|
||||
extensions={extensions}
|
||||
basicSetup={{
|
||||
foldGutter: true,
|
||||
highlightActiveLine: true,
|
||||
highlightActiveLineGutter: false,
|
||||
lineNumbers: true,
|
||||
}}
|
||||
className="min-h-[42vh] flex-1 overflow-auto font-mono text-sm [&_.cm-editor]:min-h-[42vh] [&_.cm-focused]:outline-none!"
|
||||
/>
|
||||
</section>
|
||||
<section className="flex min-h-0 flex-col bg-muted/10">
|
||||
<div className="flex h-9 shrink-0 items-center border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground">
|
||||
实时渲染
|
||||
</div>
|
||||
<div className="min-h-[42vh] flex-1 overflow-auto p-4">
|
||||
<MarkdownContent
|
||||
content={value || "*暂无正文*"}
|
||||
isLoading={false}
|
||||
remarkPlugins={reasoningPlugins.remarkPlugins}
|
||||
rehypePlugins={reasoningPlugins.rehypePlugins}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
SlidersHorizontal,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -19,7 +18,7 @@ function HeaderButton({
|
||||
variant = "outline",
|
||||
className,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
variant?: "outline" | "primary";
|
||||
@@ -53,7 +52,6 @@ export function WikiHeader({
|
||||
onCreate,
|
||||
onManageFolders,
|
||||
onManageTemplates,
|
||||
extraActions,
|
||||
}: {
|
||||
onToggleSidebar: () => void;
|
||||
onImportFile: () => void;
|
||||
@@ -63,7 +61,6 @@ export function WikiHeader({
|
||||
onCreate: () => void;
|
||||
onManageFolders: () => void;
|
||||
onManageTemplates: () => void;
|
||||
extraActions?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<header className="flex h-14 shrink-0 items-center justify-between border-b border-slate-100 bg-white px-4 sm:px-6 dark:border-slate-800 dark:bg-slate-900">
|
||||
@@ -77,12 +74,11 @@ export function WikiHeader({
|
||||
<Menu className="size-5" />
|
||||
</button>
|
||||
<div className="text-base font-semibold tracking-tight text-slate-900 dark:text-slate-100">
|
||||
知识库管理
|
||||
问答梳理
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{extraActions}
|
||||
<HeaderButton
|
||||
icon={<Upload className="size-4" />}
|
||||
label="导入文件"
|
||||
|
||||
@@ -204,7 +204,7 @@ export function noteFolderSegments(note: KnowledgeNote): string[] {
|
||||
|
||||
/** Breadcrumb segments for a note (folders only; the title is appended by the caller). */
|
||||
export function noteBreadcrumbSegments(note: KnowledgeNote): string[] {
|
||||
return ["知识库管理", ...noteFolderSegments(note).map(localizeFolderName)];
|
||||
return ["问答梳理", ...noteFolderSegments(note).map(localizeFolderName)];
|
||||
}
|
||||
|
||||
function sortTree(nodes: TreeNode[]): void {
|
||||
|
||||
@@ -534,6 +534,11 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
}
|
||||
}
|
||||
|
||||
const enabledSkills = useMemo(
|
||||
() => skills.filter((skill) => skill.enabled),
|
||||
[skills],
|
||||
);
|
||||
|
||||
// Status badges (内置 / 我的 / 来自他人 / 已发布 / 私密 / 首页推荐) — shared by
|
||||
// both the card header and the compact list row.
|
||||
const statusBadges = (
|
||||
@@ -830,14 +835,11 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
);
|
||||
|
||||
const cardLayout = (
|
||||
<Card
|
||||
className="group flex min-w-0 flex-col transition-shadow hover:shadow-md"
|
||||
data-agent-guide="agent-card"
|
||||
>
|
||||
<Card className="group flex min-w-0 flex-col transition-shadow hover:shadow-md">
|
||||
<CardHeader className="min-w-0 pb-2">
|
||||
<div className="flex min-w-0 items-start justify-between gap-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
||||
<BotIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
@@ -891,11 +893,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
);
|
||||
|
||||
const listLayout = (
|
||||
<div
|
||||
className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]"
|
||||
data-agent-guide="agent-card"
|
||||
>
|
||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
||||
<div className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]">
|
||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
||||
<BotIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
@@ -960,9 +959,9 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
已选 {selectedSkills.length} 个
|
||||
</span>
|
||||
</div>
|
||||
<SkillTransferSelector
|
||||
skills={skills}
|
||||
selected={selectedSkills}
|
||||
<SkillTransferSelector
|
||||
skills={enabledSkills}
|
||||
selected={selectedSkills}
|
||||
onChange={setSelectedSkills}
|
||||
isLoading={isSkillsLoading}
|
||||
onViewDetail={setDetailSkillName}
|
||||
@@ -981,7 +980,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
||||
? "正在加载 SOUL.md..."
|
||||
: "输入 SOUL.md 内容"
|
||||
}
|
||||
className="max-h-[45vh] min-h-48 resize-y text-xs"
|
||||
className="max-h-[45vh] min-h-48 resize-y text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -51,10 +51,6 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
|
||||
import {
|
||||
MyProjectThreadBadge,
|
||||
MyProjectThreadIcon,
|
||||
} from "@/components/workspace/my-project-thread-indicator";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { useAgentsPaginated } from "@/core/agents";
|
||||
import { getAPIClient } from "@/core/api";
|
||||
@@ -73,12 +69,9 @@ import {
|
||||
useToggleThreadPin,
|
||||
} from "@/core/threads/hooks";
|
||||
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||
import { myProjectConversationInfoOfThread } from "@/core/threads/project-metadata";
|
||||
import { useProjectThreadNames } from "@/core/threads/use-project-thread-names";
|
||||
import {
|
||||
bootstrapResumePath,
|
||||
isBootstrapThread,
|
||||
isHiddenFromRecentChats,
|
||||
isThreadFavorite,
|
||||
isThreadPinned,
|
||||
sortThreadsPinnedFirst,
|
||||
@@ -379,13 +372,19 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
||||
|
||||
// 排除调度/会商/报告结构&写作配置向导等系统会话,与首页一致;
|
||||
// 智能体创建(bootstrap)会话保留展示(带机器人图标、点击回创建页);置顶优先。
|
||||
// 排除调度/会商类系统会话,与首页一致;智能体创建(bootstrap)会话保留展示
|
||||
// (带机器人图标、点击回创建页);置顶优先。
|
||||
const visibleThreads = useMemo(
|
||||
() => sortThreadsPinnedFirst(threads.filter((thread) => !isHiddenFromRecentChats(thread))),
|
||||
() =>
|
||||
sortThreadsPinnedFirst(
|
||||
threads.filter(
|
||||
(thread) =>
|
||||
thread.metadata?.thread_type !== "scheduler" &&
|
||||
thread.metadata?.thread_type !== "roundtable",
|
||||
),
|
||||
),
|
||||
[threads],
|
||||
);
|
||||
const projectThreadNames = useProjectThreadNames(visibleThreads);
|
||||
|
||||
const filteredThreads = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
@@ -486,10 +485,6 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
) : (
|
||||
filteredThreads.map((thread) => {
|
||||
const isBoot = isBootstrapThread(thread);
|
||||
const projectInfo = myProjectConversationInfoOfThread(
|
||||
thread,
|
||||
projectThreadNames,
|
||||
);
|
||||
const href = isBoot ? bootstrapResumePath(thread.thread_id) : pathOf(thread);
|
||||
const isActive = href === pathname;
|
||||
const pinned = isThreadPinned(thread);
|
||||
@@ -503,7 +498,6 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
href={href}
|
||||
className={cn(
|
||||
"relative flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg px-2.5 text-[13px] transition-colors",
|
||||
projectInfo && "h-auto min-h-12 py-1.5",
|
||||
isActive
|
||||
? "border border-primary/40 bg-primary/5 font-medium text-primary"
|
||||
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
||||
@@ -517,8 +511,6 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
)}
|
||||
aria-label="智能体创建会话"
|
||||
/>
|
||||
) : projectInfo ? (
|
||||
<MyProjectThreadIcon type={projectInfo.type} />
|
||||
) : (
|
||||
<MessageSquare
|
||||
className={cn(
|
||||
@@ -530,22 +522,8 @@ function RecentChatsPane({ activeAgentId }: { activeAgentId?: string }) {
|
||||
{pinned && (
|
||||
<Pin className="text-muted-foreground/70 size-3 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 flex-1",
|
||||
projectInfo && "flex flex-col items-start gap-0.5",
|
||||
)}
|
||||
>
|
||||
<span className="w-full truncate">
|
||||
{desensitize(titleOfThread(thread, t.pages.newChat))}
|
||||
</span>
|
||||
{projectInfo && (
|
||||
<MyProjectThreadBadge
|
||||
className="mr-4 max-w-[calc(100%_-_1rem)]"
|
||||
projectName={projectInfo.projectName}
|
||||
type={projectInfo.type}
|
||||
/>
|
||||
)}
|
||||
<span className="truncate">
|
||||
{desensitize(titleOfThread(thread, t.pages.newChat))}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
BriefcaseBusinessIcon,
|
||||
CheckIcon,
|
||||
DatabaseIcon,
|
||||
GlobeIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
@@ -45,7 +43,6 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor";
|
||||
import { LlmWikiKnowledgeSelector } from "@/components/workspace/llmwiki-knowledge-selector";
|
||||
import { AgentRecentChats } from "@/components/workspace/agents/agent-recent-chats";
|
||||
import { useArtifactsOptional } from "@/components/workspace/artifacts";
|
||||
import { EnabledSkillList } from "@/components/workspace/agents/enabled-skill-list";
|
||||
@@ -83,7 +80,6 @@ interface AgentDraft {
|
||||
templateName: string;
|
||||
/** 会商席位「技能识别强化」开关。 */
|
||||
seatSkillDirective: boolean;
|
||||
knowledgeBaseIds: string[];
|
||||
}
|
||||
|
||||
function draftFromAgent(agent: Agent): AgentDraft {
|
||||
@@ -97,7 +93,6 @@ function draftFromAgent(agent: Agent): AgentDraft {
|
||||
featured: agent.featured_order != null,
|
||||
templateName: agent.template_name ?? "",
|
||||
seatSkillDirective: agent.seat_skill_directive ?? false,
|
||||
knowledgeBaseIds: agent.llmwiki_knowledge_base_ids ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,18 +139,13 @@ export function AgentDetailPanel({
|
||||
onAskQuestion,
|
||||
disabled,
|
||||
defaultCollapsed,
|
||||
collapsed: collapsedProp,
|
||||
onCollapsedChange,
|
||||
showRecentChats,
|
||||
compactAppearanceActions,
|
||||
avatarSrc,
|
||||
positionName,
|
||||
skillsIcon,
|
||||
skillsTitle,
|
||||
belowSkillsSlot,
|
||||
feedbackScoreSlot,
|
||||
showCommonQuestions = false,
|
||||
jobResponsibilities,
|
||||
panelTitle,
|
||||
embedded,
|
||||
hideStatusBadges = false,
|
||||
@@ -167,14 +157,6 @@ export function AgentDetailPanel({
|
||||
disabled?: boolean;
|
||||
/** 初始收起(任务工作区进入时默认收起智能体信息面板)。 */
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* 受控折叠态:传入时由父级持有状态(智能体问答页把展开/收起按钮放在顶部
|
||||
* 「新对话」右侧)。此时收起 = 整列宽度动画归零(不保留细条),面板内置的
|
||||
* 收起按钮不再渲染。不传则沿用面板内置状态 + 细条收起交互(会商等工作区)。
|
||||
*/
|
||||
collapsed?: boolean;
|
||||
/** 受控折叠态变化回调(配合 `collapsed` 使用)。 */
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
/** 在面板底部展示「有关此智能体的最近对话」(仅智能体问答页开启,任务工作区不展示)。 */
|
||||
showRecentChats?: boolean;
|
||||
/** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */
|
||||
@@ -183,16 +165,7 @@ export function AgentDetailPanel({
|
||||
positionName?: string;
|
||||
skillsIcon?: ReactNode;
|
||||
skillsTitle?: string;
|
||||
/** 渲染在技能列表区块下方的自定义内容(8BF 用它放「产物提交 position」配置)。 */
|
||||
belowSkillsSlot?: ReactNode;
|
||||
/** Optional answer-feedback summary rendered near the agent identity. */
|
||||
feedbackScoreSlot?: ReactNode;
|
||||
showCommonQuestions?: boolean;
|
||||
/**
|
||||
* 业务链席位上配置的岗位职责。传入(含空字符串)时在常用问题上方展示;
|
||||
* 不传则隐藏该区块。此字段来自业务链,不随智能体本身编辑。
|
||||
*/
|
||||
jobResponsibilities?: string | null;
|
||||
/** Overrides the standard panel heading for an embedded consumer. */
|
||||
panelTitle?: string;
|
||||
/** Renders one fixed panel instead of the responsive collapsible rail. */
|
||||
@@ -203,15 +176,7 @@ export function AgentDetailPanel({
|
||||
editIcon?: ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
// 折叠态:受控(collapsedProp 传入,智能体问答页)优先;否则面板内置(会商等工作区)。
|
||||
const [internalCollapsed, setInternalCollapsed] = useState(
|
||||
defaultCollapsed ?? false,
|
||||
);
|
||||
const collapsed = collapsedProp ?? internalCollapsed;
|
||||
const setCollapsed = (next: boolean) => {
|
||||
if (collapsedProp === undefined) setInternalCollapsed(next);
|
||||
onCollapsedChange?.(next);
|
||||
};
|
||||
const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState<AgentDraft | null>(null);
|
||||
@@ -261,10 +226,9 @@ export function AgentDetailPanel({
|
||||
|
||||
const isSaving = updateAgent.isPending || setFeatured.isPending;
|
||||
|
||||
// 对话生成文件(md/txt 等 artifact)并首次展开预览时,自动收起智能体
|
||||
// 信息面板给预览让位。目录内切换文件不能再次改写用户的手动选择:否则用户
|
||||
// 刚展开智能体信息,点另一个文件时面板又会突然收起,看起来就像收起按钮被
|
||||
// 移除了。编辑中也不打断用户。
|
||||
// 对话生成文件(md/txt 等 artifact)并在右侧展开预览时,自动收起智能体
|
||||
// 信息面板给预览让位;切换到新文件时同样收起。编辑中不打断用户,用户随时
|
||||
// 可手动重新展开。
|
||||
//
|
||||
// 关键:判定用 open + selectedArtifact,而非 artifacts.length。流式写文件时
|
||||
// message-group 一开始就调用 select()+setOpen(true) 打开预览并选中该文件,但
|
||||
@@ -273,19 +237,20 @@ export function AgentDetailPanel({
|
||||
const artifactsCtx = useArtifactsOptional();
|
||||
const artifactCount = artifactsCtx?.artifacts.length ?? 0;
|
||||
const selectedArtifact = artifactsCtx?.selectedArtifact ?? null;
|
||||
// preOpen:流式自动开沙箱的两阶段标记 —— 沙箱真正展开(open 翻转)前就
|
||||
// 先收起本面板,让收起布局先落地,避免与沙箱展开同帧叠加造成卡顿。
|
||||
const artifactPanelVisible =
|
||||
(!!artifactsCtx?.open || !!artifactsCtx?.preOpen) &&
|
||||
(!!selectedArtifact || artifactCount > 0);
|
||||
!!artifactsCtx?.open && (!!selectedArtifact || artifactCount > 0);
|
||||
const prevArtifactPanelVisible = useRef(false);
|
||||
const prevSelectedArtifact = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
const opened = artifactPanelVisible && !prevArtifactPanelVisible.current;
|
||||
const switched =
|
||||
artifactPanelVisible && selectedArtifact !== prevSelectedArtifact.current;
|
||||
prevArtifactPanelVisible.current = artifactPanelVisible;
|
||||
if (opened && !editing) {
|
||||
prevSelectedArtifact.current = selectedArtifact;
|
||||
if ((opened || switched) && !editing) {
|
||||
setCollapsed(true);
|
||||
}
|
||||
}, [artifactPanelVisible, editing]);
|
||||
}, [artifactPanelVisible, selectedArtifact, editing]);
|
||||
|
||||
const displayName = desensitize(agent?.name ?? agentName);
|
||||
const questions = useMemo(
|
||||
@@ -369,11 +334,6 @@ export function AgentDetailPanel({
|
||||
request.template_name = draft.templateName.trim();
|
||||
if (draft.seatSkillDirective !== (agent.seat_skill_directive ?? false))
|
||||
request.seat_skill_directive = draft.seatSkillDirective;
|
||||
if (
|
||||
JSON.stringify(draft.knowledgeBaseIds) !==
|
||||
JSON.stringify(agent.llmwiki_knowledge_base_ids ?? [])
|
||||
)
|
||||
request.llmwiki_knowledge_base_ids = draft.knowledgeBaseIds;
|
||||
if (nextPublished !== agent.published) request.published = nextPublished;
|
||||
if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? []))
|
||||
request.skills = draft.skills;
|
||||
@@ -670,13 +630,6 @@ export function AgentDetailPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!editing && (agent.llmwiki_knowledge_base_ids?.length ?? 0) > 0 && (
|
||||
<div className="text-muted-foreground mt-2 flex items-center gap-1 text-xs">
|
||||
<DatabaseIcon className="size-3 shrink-0" />
|
||||
LLMWiki 知识库:{agent.llmwiki_knowledge_base_ids?.length ?? 0} 个
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Publish / featured toggles — only meaningful in edit mode. */}
|
||||
{editing && draft && (
|
||||
<div className="mt-3 space-y-2">
|
||||
@@ -694,14 +647,6 @@ export function AgentDetailPanel({
|
||||
配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。
|
||||
</p>
|
||||
</div>
|
||||
<LlmWikiKnowledgeSelector
|
||||
source="all"
|
||||
value={draft.knowledgeBaseIds}
|
||||
onChange={(knowledgeBaseIds) =>
|
||||
setDraft({ ...draft, knowledgeBaseIds })
|
||||
}
|
||||
variant="panel"
|
||||
/>
|
||||
<div className="bg-background/60 flex items-center justify-between rounded-lg border px-3 py-2">
|
||||
<div className="pr-3">
|
||||
<p className="text-sm font-medium">会商技能识别强化</p>
|
||||
@@ -750,25 +695,6 @@ export function AgentDetailPanel({
|
||||
)}
|
||||
</section>
|
||||
|
||||
{!editing && feedbackScoreSlot}
|
||||
|
||||
|
||||
{/* ── 岗位职责(业务链席位配置,只读)────────────────────────────── */}
|
||||
{jobResponsibilities !== undefined && (
|
||||
<Section
|
||||
icon={<BriefcaseBusinessIcon className="size-3.5" />}
|
||||
title="岗位职责"
|
||||
>
|
||||
{jobResponsibilities?.trim() ? (
|
||||
<p className="text-muted-foreground text-sm leading-relaxed whitespace-pre-wrap">
|
||||
{desensitize(jobResponsibilities)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground/60 text-sm">暂无岗位职责</p>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
|
||||
{/* ── 常用问题 ───────────────────────────────────────────────────── */}
|
||||
{(showCommonQuestions || editing || questions.length > 0 || canEdit) && (
|
||||
@@ -913,7 +839,7 @@ export function AgentDetailPanel({
|
||||
{/* ── 技能管理 ───────────────────────────────────────────────────── */}
|
||||
<Section
|
||||
icon={skillsIcon ?? <SparklesIcon className="size-3.5" />}
|
||||
title={skillsTitle ?? `已配置技能 ${skillNamesShown.length} 个`}
|
||||
title={skillsTitle ?? `已启用技能 ${skillNamesShown.length} 个`}
|
||||
className="position-roundtable-skills-section"
|
||||
extra={
|
||||
editing ? (
|
||||
@@ -948,9 +874,6 @@ export function AgentDetailPanel({
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* ── 技能列表下方插槽(8BF 产物提交 position 等)────────────────── */}
|
||||
{belowSkillsSlot}
|
||||
|
||||
{/* ── 最近对话(右下角,仅智能体问答页展示,编辑态隐藏)──────────── */}
|
||||
{showRecentChats && !editing && <AgentRecentChats agentId={agent.id} />}
|
||||
</div>
|
||||
@@ -958,18 +881,14 @@ export function AgentDetailPanel({
|
||||
|
||||
const panelChrome = (collapseButton: ReactNode) => (
|
||||
<>
|
||||
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-4">
|
||||
{/* 标题不参与压缩(此前被简洁模式外观控件挤成单字);空间不足时只
|
||||
裁剪 compactAppearanceActions,编辑等主操作与标题始终完整。 */}
|
||||
<span className="shrink-0 text-sm font-semibold whitespace-nowrap">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
||||
<span className="shrink-0 whitespace-nowrap text-sm font-semibold">
|
||||
{panelTitle ?? "智能体信息"}
|
||||
</span>
|
||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-1">
|
||||
<div className="flex shrink-0 items-center gap-1">{headerActions}</div>
|
||||
<div className="flex min-w-0 items-center justify-end gap-1 overflow-hidden">
|
||||
{compactAppearanceActions}
|
||||
</div>
|
||||
{collapseButton && <div className="shrink-0">{collapseButton}</div>}
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{headerActions}
|
||||
{compactAppearanceActions}
|
||||
{collapseButton}
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">{body}</div>
|
||||
@@ -978,48 +897,21 @@ export function AgentDetailPanel({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Desktop (lg+) ────────────────────────────────────────────────── */}
|
||||
{/* ── Desktop (lg+): inline aside, collapsible to a rail ───────────── */}
|
||||
{embedded ? (
|
||||
<aside className="bg-card/40 flex h-full w-full min-w-0 shrink-0 flex-col border-l">
|
||||
{panelChrome(null)}
|
||||
</aside>
|
||||
) : collapsedProp !== undefined ? (
|
||||
/* 受控模式(智能体问答页):收起时整列宽度动画归零(含左边框),不再保留细条;
|
||||
展开态的「收起」按钮在面板右上角,收起态的「展开」按钮在页面
|
||||
header「新对话」按钮右侧。 */
|
||||
<div
|
||||
className={cn(
|
||||
"hidden h-full shrink-0 overflow-hidden transition-[width] duration-300 ease-in-out lg:block",
|
||||
collapsed ? "w-0" : "w-[360px]",
|
||||
)}
|
||||
>
|
||||
<aside className="bg-card/40 flex h-full w-[360px] flex-col border-l">
|
||||
{panelChrome(
|
||||
<Tooltip content="收起">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCollapsed(true)}
|
||||
aria-label="收起智能体信息"
|
||||
aria-expanded={true}
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
</Tooltip>,
|
||||
)}
|
||||
</aside>
|
||||
</div>
|
||||
) : collapsed ? (
|
||||
<div className="hidden shrink-0 items-start border-l px-2 pt-3 lg:flex">
|
||||
<div className="flex items-center gap-1">
|
||||
{compactAppearanceActions}
|
||||
<Tooltip content="展开智能体信息">
|
||||
<Tooltip content="智能体信息">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setCollapsed(false)}
|
||||
aria-label="展开智能体信息"
|
||||
aria-expanded={false}
|
||||
>
|
||||
<PanelRightOpenIcon className="size-4" />
|
||||
</Button>
|
||||
@@ -1035,7 +927,6 @@ export function AgentDetailPanel({
|
||||
size="icon-sm"
|
||||
onClick={() => setCollapsed(true)}
|
||||
aria-label="收起智能体信息"
|
||||
aria-expanded={true}
|
||||
>
|
||||
<PanelRightCloseIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { BarChart3Icon, MessageSquareTextIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
useAgentFeedbackScore,
|
||||
type AnswerFeedbackReason,
|
||||
type FeedbackDimensionKey,
|
||||
} from "@/strategy-components/api/answer-feedback";
|
||||
|
||||
const DIMENSIONS: Array<{
|
||||
key: FeedbackDimensionKey;
|
||||
label: string;
|
||||
fallbackWeight: number;
|
||||
}> = [
|
||||
{ key: "correctness", label: "正确性", fallbackWeight: 30 },
|
||||
{ key: "completeness", label: "完整性", fallbackWeight: 20 },
|
||||
{ key: "evidence_grounding", label: "证据与依据", fallbackWeight: 20 },
|
||||
{ key: "relevance", label: "相关性", fallbackWeight: 15 },
|
||||
{ key: "actionability", label: "可操作性", fallbackWeight: 15 },
|
||||
];
|
||||
|
||||
const REASON_LABELS: Record<AnswerFeedbackReason, string> = {
|
||||
incorrect_or_incomplete: "不正确或不完整",
|
||||
unmet_expectations: "与期望不符",
|
||||
slow_or_problematic: "速度慢或存在问题",
|
||||
style_or_tone: "风格或语气",
|
||||
safety_or_legal: "安全或法律疑虑",
|
||||
other: "其他",
|
||||
};
|
||||
|
||||
const CONFIDENCE_LABELS = {
|
||||
none: "暂无置信度",
|
||||
low: "低置信度",
|
||||
medium: "中等置信度",
|
||||
high: "高置信度",
|
||||
} as const;
|
||||
|
||||
function scoreText(value: number | null | undefined) {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? value.toFixed(1)
|
||||
: "—";
|
||||
}
|
||||
|
||||
function percent(value: number | null | undefined) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
||||
const normalized = value <= 1 ? value * 100 : value;
|
||||
return Math.max(0, Math.min(100, normalized));
|
||||
}
|
||||
|
||||
function weightPercent(value: number | undefined, fallback: number) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
||||
return value <= 1 ? Math.round(value * 100) : Math.round(value);
|
||||
}
|
||||
|
||||
export function AgentFeedbackScoreCard({ agentId }: { agentId: string }) {
|
||||
const { data, isLoading, isError } = useAgentFeedbackScore(agentId);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section className="bg-card/60 rounded-xl border p-4" aria-label="回答评价加载中">
|
||||
<div className="bg-muted h-4 w-24 animate-pulse rounded" />
|
||||
<div className="bg-muted mt-4 h-12 w-28 animate-pulse rounded-lg" />
|
||||
<div className="mt-4 space-y-2">
|
||||
{DIMENSIONS.map((item) => (
|
||||
<div key={item.key} className="bg-muted h-3 animate-pulse rounded" />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !data) {
|
||||
return (
|
||||
<section className="bg-card/60 rounded-xl border p-4">
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold">
|
||||
<BarChart3Icon className="size-3.5" />
|
||||
回答评价
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-xs">评价数据暂不可用</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const positiveRate = data.sample_count
|
||||
? (data.positive_count / data.sample_count) * 100
|
||||
: null;
|
||||
const taskSuccessRate = percent(data.task_success_rate);
|
||||
const topReasons = Object.entries(data.reason_breakdown ?? {})
|
||||
.filter((entry): entry is [AnswerFeedbackReason, { count: number; rate: number }] =>
|
||||
Boolean(entry[1]?.count),
|
||||
)
|
||||
.sort((left, right) => right[1].count - left[1].count)
|
||||
.slice(0, 3);
|
||||
|
||||
return (
|
||||
<section className="bg-card/60 rounded-xl border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 className="text-foreground flex items-center gap-1.5 text-sm font-semibold">
|
||||
<BarChart3Icon className="size-3.5" />
|
||||
回答评价
|
||||
</h4>
|
||||
<p className="text-muted-foreground mt-1 text-xs">用户评价分</p>
|
||||
</div>
|
||||
{data.score_band && data.score !== null && (
|
||||
<span className="border-primary/20 bg-primary/5 text-primary rounded-full border px-2 py-1 text-[11px] font-medium">
|
||||
{data.score_band.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-end gap-2">
|
||||
{data.score === null ? (
|
||||
<span className="text-muted-foreground text-xl font-semibold">暂无评价</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-3xl font-bold tracking-tight tabular-nums">
|
||||
{scoreText(data.score)}
|
||||
</span>
|
||||
<span className="text-muted-foreground mb-1 text-xs">/ 100</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[11px]">
|
||||
<span>{data.sample_count} 条评价</span>
|
||||
<span>
|
||||
{CONFIDENCE_LABELS[data.confidence_level]}
|
||||
{` ${Math.round(data.confidence * 100)}%`}
|
||||
</span>
|
||||
<span>
|
||||
好评率 {positiveRate === null ? "—" : `${positiveRate.toFixed(0)}%`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{data.sample_count > 0 && (
|
||||
<div className="mt-4 space-y-3" aria-label="回答评价维度">
|
||||
{DIMENSIONS.map((item) => {
|
||||
const dimension = data.dimensions[item.key];
|
||||
const value = percent(dimension?.score);
|
||||
return (
|
||||
<div key={item.key}>
|
||||
<div className="mb-1 flex items-center justify-between gap-2 text-xs">
|
||||
<span className="text-foreground">
|
||||
{item.label}
|
||||
<span className="text-muted-foreground ml-1">
|
||||
{weightPercent(dimension?.weight, item.fallbackWeight)}%
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-muted-foreground tabular-nums">
|
||||
{value === null ? "—" : value.toFixed(1)}
|
||||
{dimension?.evidence_count
|
||||
? ` · ${dimension.evidence_count} 条依据`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-muted h-1.5 overflow-hidden rounded-full">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-[width]",
|
||||
value !== null && value >= 80
|
||||
? "bg-emerald-500"
|
||||
: value !== null && value >= 60
|
||||
? "bg-amber-500"
|
||||
: "bg-rose-500",
|
||||
)}
|
||||
style={{ width: `${value ?? 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(data.task_score !== null || taskSuccessRate !== null) && (
|
||||
<div className="bg-muted/50 mt-4 flex items-center justify-between rounded-lg px-3 py-2 text-xs">
|
||||
<span className="flex items-center gap-1.5 font-medium">
|
||||
<MessageSquareTextIcon className="size-3.5" />
|
||||
任务成功与稳定性
|
||||
</span>
|
||||
<span className="text-muted-foreground tabular-nums">
|
||||
{data.task_score !== null && `评分 ${scoreText(data.task_score)}`}
|
||||
{data.task_score !== null && taskSuccessRate !== null && " · "}
|
||||
{taskSuccessRate !== null && `成功率 ${taskSuccessRate.toFixed(0)}%`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{topReasons.length > 0 && (
|
||||
<div className="mt-4 border-t pt-3">
|
||||
<p className="text-muted-foreground mb-2 text-[11px] font-medium">主要改进反馈</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{topReasons.map(([reason, breakdown]) => (
|
||||
<span
|
||||
key={reason}
|
||||
className="bg-muted text-muted-foreground rounded-full px-2 py-1 text-[11px]"
|
||||
>
|
||||
{REASON_LABELS[reason]} · {breakdown.count}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-muted-foreground/80 mt-4 border-t pt-3 text-[10px] leading-relaxed">
|
||||
该分数仅反映用户对回答结果的评价,不代表完整任务轨迹、工具调用或技能能力。
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
// Node's type-stripping test runner requires the explicit TypeScript suffix.
|
||||
// @ts-expect-error The application compiler intentionally uses extensionless imports.
|
||||
import { canViewAgentFeedbackScore } from "./agent-feedback-visibility.ts";
|
||||
|
||||
test("allows only the canonical admin role to view agent feedback scores", () => {
|
||||
assert.equal(canViewAgentFeedbackScore({ system_role: "admin" }), true);
|
||||
assert.equal(canViewAgentFeedbackScore({ system_role: "user" }), false);
|
||||
assert.equal(canViewAgentFeedbackScore({ system_role: "Admin" }), false);
|
||||
});
|
||||
|
||||
test("fails closed when authentication or its role is unavailable", () => {
|
||||
assert.equal(canViewAgentFeedbackScore(null), false);
|
||||
assert.equal(canViewAgentFeedbackScore(undefined), false);
|
||||
assert.equal(canViewAgentFeedbackScore({ system_role: "" }), false);
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { LoginByUsernameResponse } from "@/core/auth";
|
||||
|
||||
/**
|
||||
* Agent-level answer scores are an administrative quality-control view.
|
||||
* Keep the role check strict so missing, stale or unknown auth roles fail
|
||||
* closed instead of exposing aggregate evaluation data.
|
||||
*/
|
||||
export function canViewAgentFeedbackScore(
|
||||
auth: Pick<LoginByUsernameResponse, "system_role"> | null | undefined,
|
||||
): boolean {
|
||||
return auth?.system_role === "admin";
|
||||
}
|
||||