56 KiB
CLAUDE.md
Retrieved files in chat (2026-10-07)
The shared MessageGroup renders RetrievedFileList below its reasoning/tool
timeline, so retrieved originals remain visible when old tool steps collapse and
do not depend on a model calling present_files or enabling reference mode.
The file list starts collapsed, keeps the latest retrieved file visible with the
total count, and expands the remaining files in reverse retrieval order.
core/messages/retrieved-files.ts derives these cards only from completed skill
invocations (a known skill tool name or a skill script path in an execution tool).
Generic reads, listings, report generation tools, uploads and outputs are excluded;
workspace paths require a structured retrieval/file container and exclude paths
written in the same message group. It accepts structured file links and WeKnora identifiers such as knowledge_id and
document_file_name. It handles JSON wrappers, MCP text blocks and bounded JSON
following console logs, deduplicates by file identity and caps one group at 100
files / 2,000 visited payload nodes. Do not extract file cards from assistant prose,
failed tool calls, arbitrary document text or /mnt/skills / host filesystem paths.
The admin-only Settings → Appearance switch「技能检索文件展示」persists globally as
citation_display.retrieved_files_enabled (default true). The existing citation-display
PUT endpoint enforces administrator authorization. The UI waits for the setting
before collecting files, shares its query cache, and refreshes active clients every
60 seconds/on stale window focus. Disabling it hides retrieved cards, not normal artifacts.
Thread files reuse the authorized artifact endpoints and uploaded-file reader;
knowledge files reuse the authorized LLMWiki original-file API and
WeKnoraSourceDrawer. /api/llmwiki/sources/resolve maps provider knowledge-base
ids to local mapping ids only after checking the viewer's existing read access.
External links use HTTP(S) only, credential-free fetches for
inline preview, and a native original-file link for download/fallback; never send
the DeerFlow bearer token to them. External download behavior depends on the
source's response headers and browser cross-origin rules. Unknown formats or
unavailable Office sidecars fall back to downloading the original. Public shares
use their existing file route and do not query private knowledge document APIs.
Closing the shared preview dialog aborts in-flight requests and releases blob URLs.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
This is a customized offline deployment of DeerFlow — a LangGraph-based AI super agent platform. The repo contains two sub-projects:
offline-backend-20260512/backend/— Python backend (FastAPI Gateway + LangGraph agent runtime)frontend-web/— Vite + React 19 frontend (TypeScript, pnpm)
Managed together via shell scripts in scripts/.
Commands
Full Application (from repo root)
./scripts/start-all.sh # Start backend + frontend as background daemons
./scripts/stop-all.sh # Stop both
./scripts/restart-all.sh # Restart both
./scripts/status.sh # Check running status
Logs and PID files are written to .runtime/logs/ and .runtime/pids/.
Backend Only (from offline-backend-20260512/backend/)
make install # Install Python dependencies (uv)
make dev # Run Gateway API with hot-reload on port 8001
make gateway # Run Gateway API without reload
make test # Run all tests: PYTHONPATH=. uv run pytest tests/ -v
make lint # Lint with ruff
make format # Format with ruff
Run a single test file:
PYTHONPATH=. uv run pytest tests/test_<feature>.py -v
Frontend Only (from frontend-web/)
pnpm install # Install dependencies
pnpm dev # Start Vite dev server on port 5174
pnpm build # Production build
pnpm typecheck # TypeScript type checking (tsc --noEmit)
Service Ports
| Service | Port | Notes |
|---|---|---|
| Backend (Gateway API) | 8001 | FastAPI + LangGraph runtime |
| Frontend (Vite) | 5174 | React SPA |
Frontend connects to backend via env vars VITE_BACKEND_BASE_URL and VITE_LANGGRAPH_BASE_URL (injected by scripts/start-frontend.sh).
Architecture
Backend
The backend (offline-backend-20260512/backend/) is split into two layers:
packages/harness/deerflow/— publishable agent framework (deerflow.*imports): LangGraph lead agent, middleware chain, sandbox, tools, MCP, memory, models, skills. Never imports fromapp.*.app/— application layer (app.*imports): FastAPI Gateway routers, IM channel integrations (Feishu, Slack, Telegram, DingTalk).
The Lead Agent (deerflow/agents/lead_agent/) is the single LangGraph graph entry point. It wraps a middleware chain (~18 middlewares assembled in strict order) around LLM calls, handling: thread isolation, sandbox lifecycle, uploads, memory, plan mode, vision, subagent delegation, and loop detection.
Middleware order matters — middlewares are appended in a fixed sequence; ClarificationMiddleware must always be last. See offline-backend-20260512/backend/CLAUDE.md for the full ordered list.
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
- 账号登录、自主注册与单点登录:
/login提供用户名/邮箱与密码登录,/register提供自主注册。管理员在「设置 → 登录与注册」管理system_settings.json.authentication的四个开关:账号密码登录、自主注册、单点登录(同一账号仅一处在线)默认关闭;既有外部系统免登sso_enabled默认开启。已保存配置保持不变。开启密码登录后,私有页面先经/api/v1/auth/me验证,后端 Gateway/LangGraph 同步拒绝旧免密 JWT;合法登录在 JWT 写入auth_method。未设置个人密码的账号使用配置中的系统口令,未自定义时为123ewq;修改密码也须验证当前个人密码或默认口令。开启single_session_enabled后,每次成功登录原子递增token_version,旧会话下一次请求即失效;前端每 15 秒及窗口激活时检查并退出,失效页禁止自动重登录,避免互踢。关闭单处限制时允许多个登录会话。外部免登参数兼容 hash 前后两种位置(hash 参数优先)。公开配置GET /api/v1/auth/options,管理员读写GET/PUT /api/system-settings/authentication。个人信息下提供修改密码;LoginWhitelistPage.tsx提供管理员重置。密码与口令共用,更新后旧密码/链接失效并撤销旧会话;当前用户改密或管理员重置自身须保存返回的新 Bearer token。新密码不在用户列表回传明文,管理员仅在本次重置后临时复制链接。注册与新建外部账号均为普通用户,禁止依据名称自动授予管理员权限。
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 insrc/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) - 智能体对话跳转: 创建页、智能体管理编辑框和问答页详情面板都可以给智能体配置「对话跳转」链接、打开方式(新窗口 / 当前窗口)和路由参数(登录参数、主题参数,解析方式与应用管理相同)。配置保存在该智能体
config.yaml的chat_link,并镜像到agent_extras.chat_link供列表读取。留空时智能体管理的「对话」按钮仍进入本系统问答;填了地址后,按钮按解析后的链接跳转。 - 普通问答地图可视化: shared
MarkdownContentrecognizes only dedicatedmap-jsonfences and otherwise preserves the legacy Markdown/Streamdown path byte-for-byte.components/workspace/visualizations/validates the bounded map contract with Zod after recovering common marker/line aliases (coords,from/to,lng/lat) and dropping unusable lines so markers still render; markers accept per-pointcolor; lines are emitted only when the user explicitly asks to connect places, then render as colored SVG arrows that loop along the path (honoringprefers-reduced-motion); it lazy-loads MapLibre, and provides loading/error/text fallbacks so malformed or unreachable maps never blank the message. The lead-agent prompt emitsmap-jsononly 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 settingruntime-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。设置 → 外观的「HTML 运行地址」保存在system_settings.json -> html_runtime(GET|PUT /api/system-settings/html-runtime,管理员可写):base_url是写入新 HTML 的浏览器可访问绝对地址,map_tile_url是地图网页模式读取的瓦片模板。未保存时才回退到旧的config.yaml,最后才用本机默认地址。设置 → 外观的管理员选项「HTML 地图」可选echarts(本地 GeoJSON,data-df-geo,默认)或leaflet(地图网页,data-df-map);运行时同时保留两种初始化,已生成页面不因切换失效。用户上传的栅格图若要作为页面背景,使用同一 Gateway 源下的签名地址/api/public/html-images/{token}写入background-image。流程图使用data-df-flow+ G6(不用 Mermaid),图表使用data-df-chart。普通问答保持独立:消息列表map-json继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。 - 系统名称与 Logo: 管理员在设置弹窗「系统名称」里填写本系统导航、侧栏和欢迎区使用的名称,并可上传一张 PNG/JPG/WebP Logo。配置在
system_settings.json -> app_identity,图片在运行目录app-identity/,接口GET/PUT/DELETE /api/system-settings/app-identity(写仅管理员),图片地址GET /api/public/app-identity/logo。名称留空继续用「CM业务中心」/「CM 助手」;未上传 Logo 继续用各主题静态图片和既有runtime-config.js覆盖。不改已生成的 HTML。 - 定时任务「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 asexecution_context.template_config.template_id(TemplateFillConfigincore/scheduled-tasks/types.ts). At run time the backend converts the source JSON (the task prompt) into that template'sDATAstructure, 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'sSOUL.mdmaps it (or the plain text's semantics) to the template's schema skeleton. Chat-page debug entry (only this agent'sAgentChatPage): whenagent_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 toPOST /api/scheduled-tasks/template-fill/render(renderTemplateFill), and pops a dialog rendering the filled HTML in asandbox="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 「问答管理 → 课题研究(对话版)」. Pageopen-canvas/pages/AIWritingChatPage.tsxfully reuses the engine (AIWritingContextstate machine + LangGraphuseStream,AIWritingHistoryProvider, right-sideAIWritingDraftPanel) but replaces the presentation: left =components/ai-writing-chat/WritingChatPanel.tsx(timeline + button-only intervention cardsChatInterventionCards.tsx— the five pause cards copied fromInterventionCard.tsxwith 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) thenPOST /api/ai-writing/sessions/{id}/intent— the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes viadeerflow/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 tosubmitIntervention/control (暂停/继续/后台挂起)/QA bypass (answerrendered 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+ collapsibleAIWritingPanelform underWritingFormProvider(same asAIWritingPage'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.lengthat 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 atmaterial_confirm(the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submitsre_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 teststests/test_ai_writing_intent.py. Plan doc:frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md. - 策略优选: route
/page/strategy/qa/strategy-selection, sidebar 「问答管理 → 策略优选」. The standalonesrc/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 existingPOST /api/parallel-agents/run/streamprotocol, 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 seedsstrategy-selection-{intent,angle-*,judge,solution}only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live insrc/strategy-selection/lib.test.ts; backend seed tests live intests/test_strategy_selection_seed.py. - 轻应用中心 (Light App Center): a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers
window_openoriframemini-apps with a route-param builder: login params resolved fromlocalStorage.userInfo, theme params literal) and 应用列表 (card grid ofwindow_openapps,window.openlaunch).iframeapps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and dynamically injected into the sidebar bycore/page-layout/use-sidebar-menu.ts(useSidebarMenuItems), opening in theLightAppIframeViewhost. Client + React Query hooks instrategy-components/api/light-apps.ts; URL builder/constants instrategy-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 ofcomponents/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 incore/page-layout/menu-overrides.ts(applyMenuOverrides, applied inuse-sidebar-menu.tsafter light-app injection, before role filter; disabling a node hides its whole subtree) — strict 3-level cap (wouldExceedDepth). Clientstrategy-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-overridesstores two independent sets (typeA/B); each website picks one viaruntime-config.jsVITE_COMPACT_MENU_TYPE. - 白名单管理 (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-onlyGET|PUT /api/system-settings/login-whitelist) + a per-user 放行 flag (users.approvedcolumn, migration20260625_02). Users self-register as usual and every new account defaults toapproved=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/UserRowall defaultapproved=True; admins are always approved; existing rows are backfilledTrue.) Login interception is the single helperenforce_user_approved(user)inapp/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); elseapproved=False(admin-revoked) → 403{code:"PENDING_APPROVAL"}(the frontend'sextractErrorMessagesurfaces the message and login throws before writing any auth state, so no half-login). Admin approves onpages/LoginWhitelistPage.tsx(route/page/strategy/admin/login-whitelist, entry「白名单管理」in the bottom-left settings dropdown ofcomponents/page-sidebar.tsx, both sidebar modes): a top master-switch + a list of all registered users (GET /api/v1/auth/users, now carryingapproved) with a per-row 放行 Switch (PUT /api/v1/auth/users/{id}/approval, admin-only; revoking only blocks the next login —token_versionis left untouched, existing sessions are not force-killed). Client/hooks: switch instrategy-components/api/login-whitelist.ts, user list + approval incore/auth/{api,hooks}.ts(useSetUserApproval). Backend tests:tests/test_login_approval.py. 账号级登录口令与密码已统一,个人口令优先于系统共享口令;新密码保存哈希,旧明文口令兼容校验。管理员重置使用PUT /api/v1/auth/users/{id}/login-password(别名/reset-password),同时更新两种历史凭据字段并撤销旧会话,不再支持清空密码或读取新密码明文。未设置个人密码时使用配置共享口令(默认123ewq);登录开关关闭时保留旧入口兼容行为,开启后禁止免密进入。详见上方「账号登录、自主注册与单点登录」。 - 敏感词管理 (Sensitive Word Management): configures the 展示层脱敏映射 (原始词 → 替换词). Gated on the username
lqq(not the admin role) —getAccountDisplayName().toLowerCase() === "lqq"(username = email@prefix, elseuser_id, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check.src/lib/desensitize.tsships a frozen seed mapSEED_DESENSITIZE_MAP(the deployment seed) plus a live mutableDESENSITIZE_MAP(initialized from the seed sodesensitize()works before any fetch).DesensitizeWordsLoader(mounted inApp.tsxAppProviders) fetches/api/sensitive-wordson startup and, if non-empty, callsreplaceDesensitizeMap(...)to make the backend config the source of truth (empty/failed → keep seed). PageSensitiveWordsPage.tsx(route/page/strategy/admin/sensitive-words, opened from the 「敏感词管理」 item in the bottom-left settings dropdown ofcomponents/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 missingSEED_DESENSITIZE_MAPentries into the editor. Client + hooks instrategy-components/api/sensitive-words.ts(wordsToMapfolds enabled rows into the runtime map). Backend:/api/sensitive-words(global/shared; open read, write allowed for admin or thelqqaccount), mirroring 菜单管理's store shape. 侧边栏 + 导航栏 menu labels are desensitized at the data source:useSidebarMenuItems/useActiveThirdLevel(core/page-layout/use-sidebar-menu.ts) map every node'slabelthrough a desensitizer built directly from the/api/sensitive-wordsquery (makeDesensitizer(wordsToMap(words)), falling back to the global seeddesensitizewhile empty) — this sidesteps thereplaceDesensitizeMapeffect-timing race. Onlylabelis rewritten;path/id/iconare 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;MenuManagementPageuses 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,routerapp/gateway/routers/skill_addresses.py,teststests/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};linkTypeisurl(外链window.open, optional append of?{idParam}={id}—idParamis the editable query-param name, e.g.id/task_id),business(内部切换到另一业务, reusesresolveTaskDeeplinkTarget, always task id), orpurpose(弹出当前任务的「任务目的」表格 via the sharedTaskPurposeDialog, no target — configurable for any page incl. xdfx).host(宿主路由) asks the embedding parent to navigate:runTaskButtonsendswindow.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)—targetis the host route (e.g./task/item-aiceh/step-analysis-audience),appendTaskId/idParambecome numericqueryParams(task id only), origin fromruntime-config.jsVITE_HOST_NAVIGATE_ORIGIN(else ancestorOrigins/referrer; never*). Independent-tab clicks toast and do nothing.urlbuttons support two independent, coexisting login-append mechanisms: (1)loginParams(追加登录参数, like 应用管理/Light App Center) — a list of{key, source, customValue}resolved at click time:runTaskButtonreadsuserInfo(viareadUserInfofromstrategy-components/lib/light-app, embed-isolated throughauthStorage) and appends?{key}={value}for each (source= a userInfo field such asaccessToken/yUserId, orother→ literalcustomValue); empty list = nothing appended. ReusesLOGIN_SOURCE_OPTIONSfrom light-app; stored as JSON columntask_buttons.login_params_json(PortableJSON, migration20260625_01). (2)appendAuth(追加登录态) — appends?{authTokenParam}={token1}&{authNameParam}={username}(default param namestoken/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 fromgetTokenInfo. Both are stashed at login bypersistLoginCredentialsintologin.token1/login.username(core/auth/login-credentials.ts, scoped via scoped-storage so嵌入/独立会话隔离 + 随独立会话迁移). Only token login writes them → a non-token-login session has notoken1, sorunTaskButtonsilently skips the auth append (the jump still works, just without those two params). The frontend readsusernamestraight from the newUsernameLoginResponse.usernamefield (the stored email local-part is lossy — lowercased/normalized — so not used), not by decoding the JWT. Stored as columnstask_buttons.{append_auth, auth_token_param, auth_name_param}(migration20260625_02). The admin page renders both editors forurlbuttons (loginParams 虚线框 + 追加登录态 switch).idKind(task/action) only matters for xdfx: its deep-link?taskId=is an 行动id and the 任务id is resolved at login (fetchTaskIdFromAction) —TaskContextnow also stashes the rawactionId, so a url button can append either (defaulttask). PageTaskButtonsPage.tsx(route/page/strategy/admin/task-buttons, admin-gated, entry「按钮管理」in the bottom-left settings dropdown ofcomponents/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 todefaultTaskButtons(cfg)— rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the live/api/public/config/task-deeplinkplus 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:TaskJumpBarrenders only「上一步/下一步」-type buttons in the bottom bar — aprevgroup (id 含prev或文案「上一步」, 描边次要样式 + ←图标) rendered 左侧, then anextgroup (id 含next或文案「下一步」, 实心主色) on the 右侧 (containerjustify-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:TaskFxWorkspaceno longer hardcodes 切换到三情分析,TaskActionsPanelkeeps only the non-jump 清除记录, andTaskPurposePaneldropped its 3q详情 button (now configurable) and defaults the 目的树 table to collapsed (chevron toggle). Helperscore/auth/task-buttons.ts(TaskButton,defaultTaskButtons,resolveTaskButtons,buttonsForBusiness,runTaskButton), client+hooksstrategy-components/api/task-buttons.ts. Backend:/api/task-buttons(global/shared; open read, admin replace-all), storedeerflow.persistence.task_buttons(tabletask_buttons, auto-created bycreate_all), wiredapp.state.task_button_store. Tests:tests/test_task_buttons.py. - 实时并发监控 (Concurrency / System-Pressure Monitor, admin-only): a live panel mounted at the top of the admin 排行榜 page (
components/workspace/admin/concurrency-monitor.tsxConcurrencyMonitorPanel, rendered inpages/AdminUserLeaderboardPage.tsx) showing 当前并发的大模型调用数 + 列表(用户/智能体/类型/已运行时长), each row with a 「停止」button (and a 「全部停止」) that truly aborts that run's in-flight model stream viaRunManager.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 intoconcurrency_samples) and 大模型出字速度折线图 (tokens/sec,GET /api/admin/active-runs/token-speed, aggregated fromllm_call_metrics) so a dip pinpoints where the bottleneck is. A 「后台采样」Switch (system_settings.json → concurrency_monitor.enabled, default off / opt-in,GET|PUT /api/admin/active-runs/settings) turns the background sampling on/off at runtime, no restart (the live concurrency count + 「停止」 work even while sampling is off — only the 并发量 history chart needs it on); everything is designed to never impact production (one tiny INSERT/15s, retention-purged, best-effort). Client/hooks:core/admin/active-runs.ts+core/admin/hooks.ts(useActiveRuns/useConcurrencySeries/useTokenSpeedSeries/useCancel*/useMonitorSettings). See the backend CLAUDE.md「Admin Concurrency Monitor」. - 会话冲突与模型工具流恢复: when a user submits a new question while the previous turn is still streaming, the backend's
RunManager.create_or_rejectrejects with HTTP 409 (services.pyreturns the Chinese detail「当前对话还在生成回答…」).core/threads/hooks.tsdetects the conflict and routes it throughconfirmOrRejoin. Run-creation SSE responses exposeContent-Locationplus the SDKLocationreconnect endpoint;core/api/api-client.tsrecords the last consumed event id, so an in-page rejoin resumes after that cursor instead of replaying tool arguments from-1.write_fileargument updates carried bymessages-tuplecount as business activity. A known active run is probed only after 90 seconds without a business frame; the watchdog hideslg:stream, stops only the old browser reader, and rejoins without cancelling the backend run. Clean EOF, transport errors, 409 and watchdog recovery share one single-flight Promise to prevent duplicate joins/finalization. The backendPatchedChatOpenAIseparately normalizes confirmed cumulative argument snapshots and bounded compatible response shapes while preserving standard deltas; its content-free diagnostics never log prompts, arguments or file bodies. - React #185 与流式刷新保护: every direct LangGraph
useStreamsurface (main/agent chat, AI writing, Canvas and Studio notebook) uses sharedSTREAM_RENDER_BATCHING = true. The installed SDK implements numericthrottle: 16as a trailing debounce, which can indefinitely postpone UI updates during a continuous fast stream;trueretains asynchronous next-task batching without requiring 16ms silence.stream-error-classification.tsdetectsMaximum update depth exceeded/ production#185through bounded, cycle-safe wrappers. Main chat filters the SDK's stored error and duplicate submit rejection;react-ui-error.tsgives each send at most one automatic UI recovery, then pauses rejoin without cancelling the server run. Token activity must not reset that budget. The top-levelReactUiErrorBoundarycatches only this render/commit error, shows a static friendly reload panel, and never automatically remounts the failing tree. Other errors are not swallowed. Shared toasts are deduplicated for 30 seconds; diagnostics retain error type, recovery phase, thread/run identifiers and component stack but never message/tool content. Real SDK SSE replay covers partial reasoning and write_file Markdown before generation ends; the browser boundary check triggers a real production React layout-effect loop and requires manual reload to recover. These safeguards do not prove the unknown production component loop is fixed. - 流式游标作用域: resumable cursors belong to the current SDK message accumulator, not a module-global map.
createStreamAPIClientisolates same-reader recovery; newly mounted chats and real thread switches must replay the retained prefix instead of borrowing another reader's consumed position. Clearing replaces the cursor map so late old-reader events cannot pollute the new scope. The sharedgetAPIClientdoes not rewrite replay-from-start cursors.stream-cursor-replay.test.tsuses the real SDK to protect against both duplicate tool arguments on reconnect and missing prefixes on reopening. - 单智能体任务深链聊天记录(2026-10-19):
3qfx/rwfx/xdfx已不再通过useTaskThreads扫描、过滤通用threads_meta列表;TaskFxWorkspace/TaskFxIndexResolver统一调用useTaskDeeplinkThreads→GET /api/task-deeplink-threads?task_id=&go_path=,读取独立表task_deeplink_threads的(task_id, go_path)索引,再按thread_id关联 canonicalthreads_meta标题/状态(消息与 checkpoint 仍由 LangGraph 管理)。新会话 metadata 写taskRecordId、taskGoPath、thread_type=task_deeplink并由 thread-meta 仓储同事务登记;3qfx/rwfx的taskRecordId是任务 id,xdfx是 URL 原始行动 id(真实任务 id 仍保留在metadata.taskId维持共享访问语义)。普通聊天记录默认隐藏该 thread type;显式 taskId 查询仍可读取。迁移20261019_01只建表、不回填旧数据,因此上线前旧深链会话不会出现在新列表。/api/open/3qfx/ask同样写专用索引。Tests:packages/harness/tests/test_task_deeplink_threads.py,src/core/auth/task-purpose-metadata.test.ts,src/core/threads/hidden-chats.test.ts。 - State: TanStack Query for server state, context providers (
SubtasksProvider,ArtifactsProvider,PromptInputProvider) scoped toWorkspaceRoutes - LLM streaming:
@langchain/langgraph-sdkstreams messages from the backend via SSE - UI: Radix UI primitives + TailwindCSS v4 + shadcn-style components in
src/components/ui/ - Shims:
src/shims/providesnext/linkandnext/navigationpolyfills (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 APIGET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=(headerAuthorization: Bearer admin, auto-injected byutils/request.ts), stashes the fulldata(id/overview/taskDirection/taskName/taskContent/taskType; opening text mapstaskType1/2/3 to 打击/塑势/防御) intosessionStorage["login.copTaskDetail"], then routes bygoPath: base codes3qfx/rwfx/xdfx→ that business's configured single agent Q&A inside a 任务工作区 (resolved via/api/business-mapping;resolveTaskDeeplinkTargetcallspersistTaskContext({taskId, agentId, openingText})and navigates to the…/chatsindex, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOTpendingAgentMessage); the-Asuffix (3qfx-A/…) → multi-agent roundtable withroundtablePrefill.businessCodeso the page auto-selects the configured 业务链条. xdfx's deep-link?taskId=is actually an 行动id, not a 任务id —resolveDeeplinkTaskId(goPath, rawId)(called inLoginPagebefore everything) detectsxdfx/xdfx-Aand resolves it viaGET {action_detail_url}/{actionId}→data.taskId(fetchTaskIdFromAction;action_test+mock.action_detailfor offline testing); the resolved 任务id then drivescop-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:resolveTaskDeeplinkTargetsetspartitionByAction:true+ keeps the raw action_id asTaskContext.actionId, so new threads are taggedmetadata.{taskId,actionId}(taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys offmetadata.taskId) and the conversation list filters byactionId(+agent_id) — different actions of the same task get separate lists. xdfx's first-question opening text also appends行动id(action_id)为…(seebuildOpeningText/taskThreadMetadata). goPath→businessCode:3qfx→3Q,rwfx→6BF,xdfx→7BF. The roundtable target also threads the URLtaskIdintoroundtablePrefill.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 bytask_id(one taskId = many records, shared across users, no user 分权 at all;created_byis 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 clientroundtable-planning/api/task-drafts.ts, anduseDraftPersistence/useRoundtableDraftsroute all CRUD to it when ataskIdis present (else the personaldrafts.ts). The matching background jobs are shared by task too:roundtable_jobs.task_idmarks 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 byscripts/migrate_roundtable_task_drafts.py(migration20260618_01). The history dropdown filters to the task and 新建 keeps the binding; (2) Step-3 auto structured output — after theroundtable-summaryagent writes the Markdown report it automatically runs a second round emitting aflow-jsonflowchart (useStep3SummaryautoFlow+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 undertask-management/<business-path>/{roundtable,agent/:agent_id/chats}inStrategyRoutes.tsx(<business-path>=plan-task-sentiment/plan-task/plan-task-fx/plan-task/plan-task-action, matching the COH中任务 menu paths insidebar-menu.ts) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. All three single-agent businesses shareTaskFxWorkspace(task-components/TaskFxWorkspace.tsx): left = a「当前任务」card (readCopTaskDetail()任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (useTaskThreads(taskId, agentId, actionId?)— filtered by bothmetadata.taskIdANDmetadata.agent_idso the three businesses' conversations stay isolated even though they share one taskId; xdfx passes a thirdactionIdso it filters bymetadata.actionIdtoo, partitioning chat records per 行动 —useChatsBasederives it fromTaskContext.partitionByAction) + 新建对话; the…/chatsindex (TaskFxIndexResolver) opens the latest task thread or 新建. The right:thread_idhost differs by business: rwfx →TaskFxChat(AgentChatPage taskGate) = full workspace (non-lite): 目的树闸门 panel + 清除记录 (placed under 新建对话); xdfx → alsoTaskFxChat(same 目的树闸门 query+display, samepurpose_detail_url) but workspace passedlite, plus its own「清除记录」button (ActionClearButton, under 新建对话) that deletes by 行动id: opening its confirm dialog first runs a「行动历史是否存在」pre-check (fetchActionHistoryExists; query endpointtask_deeplink.action_history_urlis address-TBD — empty ⇒ unknown, does not block the delete; once a definite「无历史」comes back it disables 确认清除), then deletes via the shared delete endpointtask_deeplink.delete_url(migrated todropSixOrSevenData, used by both rwfx + xdfx — Six=6BF/rwfx, Seven=7BF/xdfx):deleteTaskPurpose(id, {idKind})sends rwfx?taskId=(omits actionId) / xdfx?actionId=(omits taskId) — the unused param is absent from the URL, not null.deleteTaskPurpose+fetchActionHistoryExistslive incore/auth/task-purpose.ts. 3qfx →TaskBindChat(AgentChatPage taskBind,lite) = 1:N list only, no gate, and 新建对话 auto-sends the task opening text (taskAutoSendText; gate businesses send it via the gate's 开始问答 instead). rwfx auto-starts the first Q&A when the 任务目的 query returns empty (TaskPurposePanel autoStartWhenEmpty, threaded from the rwfxTaskFxChatonly), but gated by the 3q precheck: on entering the rwfx workspaceRwfxSqGateProvider(task-components/rwfx-sq-gate.tsx, wraps the workspace, rwfx only) callsfetchSqReportStatus(taskId)—incomplete(3q 无数据) pops the「3q 未完成,是否前往 3qfx」dialog AND holds the auto-send; the question is only sent when the user picks「暂不」(or dismisses — i.e. does NOT go to 3q analysis), while「前往 3qfx」navigates and discards the send.complete/unknownsend immediately (no dialog). The panel's auto-start callsuseRwfxSqGate().gateStart(onStart)instead ofonStart(); the manual「开始问答」/覆盖确认 path is unchanged (ungated). This replaced the old standaloneRwfxSqReportGate(which popped the same dialog but did not gate the auto-send). The 目的树闸门 (TaskPurposePanel) defaults collapsed and now keeps only 弹框查看 + 开始问答; the former jump buttons (编辑/下一步/3q详情/切换三情/弹框查看) all moved to the configurable bottom 快捷跳转 bar (TaskJumpBar, red 红底白字 buttons, right-aligned) driven by 按钮管理 — see the 按钮管理 bullet.TaskPurposeDialog(exported fromTaskPurposePanel) backs both the gate zoom and the bottom bar'spurpose-type button.chatBasePathkeeps the post-send URL rewrite + new-chat button in the task-management context. Backend configtask_deeplink(config.yaml, surfaced byGET /api/public/config/task-deeplink, mirrored incore/auth/task-deeplink-config.ts) addsaction_test/action_detail_url/detail_3q_url+mock.action_detail. Any failure (no consumer URL / fetch error / unmapped goPath / unresolvable action id / no configured agent) silently falls back to the default home. Helpers:core/auth/task-deeplink.ts,core/auth/task-purpose.ts,core/auth/task-deeplink-config.ts,core/threads/use-task-threads.ts. - 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.tsxAppProviders调resolveForcedEmbedTheme,不写 localStoragestrategy-theme),离散映射dark-blue/dark→深、其余→浅;嵌入侧绝不调setTheme/persistSidebarEnabled(那俩落 localStorage 跨标签污染,LoginPage据readEmbedParam跳过)。(2)?hideJump=1— 隐藏深链底部「快捷跳转」栏TaskJumpBar的两部分(底部「下一步」等按钮 + 右侧垂直居中悬浮菜单),单智能体问答 + 会商共用该栏,isJumpBarHidden()为真时整条return null。(2.5)?hideChrome=1— 仅去全局 chrome(Header 导航栏 + PageSidebar 侧边栏),不强制主题、不隔离鉴权(区别于embed:只想要一个干净全屏页面,如「大屏绘制」/page/strategy/dashboard-agent?taskId=…&hideChrome=1)。新增EmbedSession.hideChrome字段 +readHideChromeParam()+isChromeHidden()(取embed || hideChrome之并),与hideJump完全对称;PageRoutes.tsx的PageLayout把chromeHidden(isChromeHidden() || readHideChromeParam(location.search)==="on")并入pageSidebarAllowed,且其优先级高于 dashboard-agent 路由原本的强制全局 chrome(forceChromeForDashboardAgent)。hideChrome=0可显式关闭。(3) 登录用户隔离 — 嵌入态下鉴权/身份键(deerflow.authtoken、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 提示。 - 操作日志(Y-Log)上报 — 用户的关键操作(登录登出/查询/查看/增删改/启停/导入导出/问答等)成功后,前端直连外部接口
POST {write_url}(默认https://ch1.b.uat.4.cn/api/y-log/write)记一条审计日志。核心模块src/core/log/:y-log.ts(writeYLog(params)+OptType枚举 0/1/3..12;异步即发即忘、永不 await、永不抛错、失败静默,自动把完整 url 转相对路径去 ip/host、对象入参/返回自动JSON.stringify+ 50k 裁剪、optResult恒 0 只报成功),y-log-config.ts(从公开只读GET /api/public/config/y-log拉{enabled, writeUrl},模块级缓存 + 去重 + 默认兜底;App.tsxAppProviders启动预热fetchYLogConfig()),y-log-token.ts(token1 = token 登录的?authToken=,经 scoped-storage 的authStorage*存取,嵌入态自动隔离)。双门控:仅当①存在 token1(即用 token 登录,LoginPage成功后persistYLogToken(authToken),登出account-settings-page.tsx先报后清 token1)且②配置enabled && writeUrl才上报;否则一律不发。optDetail统一「CM助手-全量-…」(禁内部框架名)。埋点广覆盖各 API client 的成功路径——原则是「一个用户操作(查看/查询/新增/编辑/删除/启停/导入导出)= 一条日志」,每个 API client 的单条详情 GET(查看→view 4)/ 列表查询 GET(search 3)/ 新增 POST(create 9)/ 改 PUT·PATCH(edit 5)/ 删 DELETE(delete 6)/ 启停(enabled 11 / disabled 10)/ 导入导出(import 7 / export 8)都各落一条,仅跳过高频 typeahead/状态轮询/纯 bootstrap config 读。覆盖文件:strategy-components/api/{sensitive-words,menu-overrides,task-buttons,skill-addresses,light-apps,business-mapping}.ts、roundtable-planning/api/{drafts,task-drafts,roundtable-jobs,recommend,chains,multi-agent,dashboard-sessions}.ts、core/{agents,scheduled-tasks,knowledge,positions,tags,studio,system-settings,memory,mcp,models,custom-prompts,user-preferences,notifications,recommended-questions,user-qa,curator,admin,thread-shares,roundtable-draft-shares,tool-metrics,llm-metrics,note,artifacts}/api.ts、core/api/markdown-docx-export.ts。问答(Q&A)改为「一次大模型调用 = 一条日志」:在共享的core/threads/hooks.tsuseThreadStream的onLangChainEvent里监听on_chat_model_end,每次模型调用结束就上报一条(optType 12,reqParam取本次 input 的最后一条 user 文本 + 模型名,returnParam取本次输出文本)——一个问答轮次里 lead agent / 子代理的每次 LLM 调用都各记一条,覆盖所有走useThreadStream的页面(ChatPage/AgentChatPage/创建智能体/iframe/会商等);ChatPage/AgentChatPage的onFinish仍额外保留一条轮次汇总。后端仅新增公开只读配置:deerflow/config/y_log_config.py(YLogConfig{enabled,write_url})挂到AppConfig.y_log,app/gateway/routers/public_config.py加GET /api/public/config/y-log,地址写在config.yaml的y_log段(mtime 热重载,前端无需重建);不做后端代理。Tests:tests/test_y_log_config.py。
Configuration
普通问答与智能体问答的 write_file 流式展示:namedStreamingWriteFilePath 统一判断路径是否可用;缺少路径、路径尚未完整或只有占位名时,不生成虚拟文件来自动打开沙箱,而由 message-group.tsx 在步骤内用绿色小字展示正文。步骤参数合并 tool_calls 与 tool_call_chunks,后续收到真实路径时复用原工具调用 ID 恢复正常文件预览;outputs 下真实文件的自动打开和沙箱流式预览保持原流程。
沙箱文件正在生成时,ArtifactsProvider 的 select / openStreamedArtifact 阻止切到其他文件;手动操作提示「当前文件正在生成,请稍等片刻,完成后就可以切换查看其他文件。」,自动选择静默跳过。ChatBox 按选中文件的未完成 write_file 和运行状态注册锁,预览组件同时覆盖流式写入及全文改写;完成、停止或卸载时移除所属锁。同一个文件的虚拟预览和真实路径视为同一目标。
Backend config lives in offline-backend-20260512/backend/config.yaml. Values starting with $ resolve as environment variables (e.g., $OPENAI_API_KEY). MCP servers and skills are configured in extensions_config.json in the same directory.
Detailed Backend Reference
The backend has its own comprehensive documentation at:
offline-backend-20260512/backend/CLAUDE.md— architecture, middleware chain, all API routes, config schema, sandbox/memory/subagent systemsoffline-backend-20260512/backend/README.md— setup guide, technology stack