无界 onlineTicket 对接 MaxKey 与 consumer 接口并用 sub 登录;新增 setup_writing 写作配置确认流;定时任务创建弹窗限制多行高度并支持滚动。
175 lines
8.1 KiB
TypeScript
175 lines
8.1 KiB
TypeScript
/**
|
||
* AI 写作入口页(左右双栏:聊天面板 + 草稿预览)。
|
||
*
|
||
* ──────────────────────────────────────────────────────────────────────────
|
||
* Docker 内网部署后「会话丢失 / transcript 报 500 / resume 报 400」排查指南
|
||
* ──────────────────────────────────────────────────────────────────────────
|
||
*
|
||
* 后端涉及接口(前缀 /api/ai-writing):
|
||
* POST /start 启动写作流程
|
||
* GET /{session_id}/stream SSE 进度流
|
||
* POST /{session_id}/resume 提交用户干预 → 继续流程
|
||
* PUT /sessions/{session_id}/transcript 保存对话时间线
|
||
*
|
||
* ── 1) 确认 alembic 迁移已跑过(修 transcript 500)─────────────────────
|
||
* transcript 列在 MySQL 上原是 TEXT(64 KB 上限),长时间线会溢出抛 500。
|
||
* 迁移 20260519_02 把它改成 LONGTEXT。容器启动会自动跑,验证方法:
|
||
*
|
||
* docker exec <gateway> sh -c "cd /app/backend \
|
||
* && PYTHONPATH=. uv run alembic current"
|
||
* docker exec <mysql> mysql -uroot -p<pwd> deerflow -e \
|
||
* "SHOW COLUMNS FROM ai_writing_sessions LIKE 'transcript';"
|
||
*
|
||
* 列类型应是 longtext。若仍是 text,手动执行:
|
||
* docker exec <gateway> sh -c "cd /app/backend \
|
||
* && PYTHONPATH=. uv run alembic upgrade head"
|
||
*
|
||
* ── 2) 确认 .deer-flow 数据卷已挂出(修「容器没重启也丢会话」)──────
|
||
* AI 写作 graph 的 checkpointer 已从 MemorySaver 换成 AsyncSqliteSaver,
|
||
* 文件落到 .deer-flow/data/checkpoints.db。docker-compose 必须把该目录
|
||
* 挂出来,否则容器重启数据照样丢:
|
||
*
|
||
* services:
|
||
* gateway:
|
||
* volumes:
|
||
* - ./data:/app/.deer-flow # 关键:挂出 .deer-flow
|
||
*
|
||
* ── 3) 确认 worker 数 = 1(关键嫌疑:多 worker 内存隔离)────────────
|
||
* _event_queues / _session_pending_pause 都是模块级 dict,每个 worker
|
||
* 一份。多 worker 下,/start 命中 worker A、/resume 命中 worker B,B
|
||
* 的 dict 没有这个 session → 404/「会话丢失」。
|
||
*
|
||
* docker exec <gateway> ps -ef | grep -E 'uvicorn|gunicorn'
|
||
*
|
||
* 若 > 1:要么把启动参数改成 --workers 1,要么在 nginx 加 sticky
|
||
* session(ip_hash / cookie),要么把队列搬 Redis。
|
||
*
|
||
* ── 4) 排查节点异常(嫌疑:graph 节点抛错触发清理分支)─────────────
|
||
* _stream_graph_segment 的 except 分支会清掉 _event_queues[session_id],
|
||
* 用户下一次 resume 就 404。日志里搜:
|
||
*
|
||
* docker logs <gateway> 2>&1 | grep -E \
|
||
* 'ai_writing graph segment failed|ai_writing.*error' | tail -50
|
||
*
|
||
* ── 5) 排查反向代理掐 SSE 长连接(嫌疑:proxy_read_timeout 太短)───
|
||
* SSE 默认连接挂半个小时以上,nginx 默认 60s 会断。检查内网反代:
|
||
*
|
||
* grep -rE 'proxy_read_timeout|sse|keepalive' /etc/nginx/
|
||
*
|
||
* 需在 SSE 路由上加:proxy_read_timeout 3600s; proxy_buffering off;
|
||
*
|
||
* ── 定期清理(每天 03:00 跑)────────────────────────────────────────
|
||
* config.yaml -> ai_writing.cleanup 段(retention_days: 7)。联动清理
|
||
* MySQL 业务表 + SQLite checkpointer 里的 thread 状态。手动验证:
|
||
*
|
||
* docker exec <mysql> mysql -uroot -p<pwd> deerflow -e \
|
||
* "SELECT COUNT(*) FROM ai_writing_sessions \
|
||
* WHERE updated_at < NOW() - INTERVAL 7 DAY;"
|
||
*
|
||
* ── 不想等凌晨 3 点?手动触发一次清理(admin only)──────────────────
|
||
* POST /api/ai-writing/admin/cleanup
|
||
* Body(全部可选,留空走 config 默认):
|
||
* { "retention_days": 7, "only_finished": false, "batch_size": 500 }
|
||
* 返回:
|
||
* { "deleted_db": N, "deleted_checkpoints": N,
|
||
* "elapsed_seconds": X, "params": {...} }
|
||
*
|
||
* 例:清掉所有 1 小时前的会话(测试用):
|
||
* curl -X POST http://<gateway>/api/ai-writing/admin/cleanup \
|
||
* -H 'Content-Type: application/json' \
|
||
* -H 'Cookie: <admin session cookie>' \
|
||
* -d '{"retention_days": 0}'
|
||
* ──────────────────────────────────────────────────────────────────────────
|
||
*/
|
||
import { useRef } from 'react';
|
||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||
import type { GroupImperativeHandle } from 'react-resizable-panels';
|
||
|
||
import { CanvasThreadProvider } from '../contexts/CanvasThreadContext';
|
||
import { CanvasGraphProvider } from '../contexts/CanvasGraphContext';
|
||
import { CanvasUIProvider } from '../contexts/CanvasUIContext';
|
||
import { AIWritingPanel } from '../components/ai-writing/AIWritingPanel';
|
||
import { AIWritingDraftPanel } from '../components/ai-writing/AIWritingDraftPanel';
|
||
import { WritingSetupChat } from '../components/ai-writing/WritingSetupChat';
|
||
import { AIWritingProvider, useAIWriting } from '../contexts/AIWritingContext';
|
||
import { WritingFormProvider } from '../contexts/WritingFormContext';
|
||
import {
|
||
ResizableHandle,
|
||
ResizablePanel,
|
||
ResizablePanelGroup,
|
||
} from '@/components/ui/resizable';
|
||
|
||
// Panel IDs used as keys in the v4 Layout map
|
||
const WRITING_ID = 'aw-writing';
|
||
const DRAFT_ID = 'aw-draft';
|
||
|
||
// Match the original canvas page proportions (chat=35, artifact=65)
|
||
const DEFAULT_LAYOUT = { [WRITING_ID]: 35, [DRAFT_ID]: 65 };
|
||
|
||
/**
|
||
* 右侧面板:未开始写作(idle)时显示「写作助手 Q&A」,帮用户聊清并回填左侧表单;
|
||
* 一旦开始写作(status !== idle)就切回草稿预览。
|
||
*/
|
||
function AIWritingRightPanel() {
|
||
const { state } = useAIWriting();
|
||
return state.status === 'idle' ? <WritingSetupChat /> : <AIWritingDraftPanel />;
|
||
}
|
||
|
||
export default function AIWritingPage({
|
||
hideAdvancedFields = false,
|
||
}: {
|
||
hideAdvancedFields?: boolean;
|
||
} = {}) {
|
||
const layoutRef = useRef<GroupImperativeHandle>(null);
|
||
|
||
return (
|
||
<CanvasThreadProvider>
|
||
<CanvasGraphProvider>
|
||
<CanvasUIProvider>
|
||
<AIWritingProvider>
|
||
<WritingFormProvider>
|
||
<TooltipProvider delayDuration={300}>
|
||
<div className="h-full flex flex-col overflow-hidden bg-background">
|
||
<div className="flex-1 min-h-0 overflow-hidden">
|
||
<ResizablePanelGroup
|
||
orientation="horizontal"
|
||
groupRef={layoutRef}
|
||
defaultLayout={DEFAULT_LAYOUT}
|
||
className="h-full"
|
||
>
|
||
{/* 左侧:AI 写作面板,与原画布页左侧等宽 */}
|
||
<ResizablePanel
|
||
id={WRITING_ID}
|
||
defaultSize={35}
|
||
minSize={20}
|
||
className="overflow-hidden flex flex-col"
|
||
>
|
||
{/* 与原布局保持相同的 flex-1 min-h-0 包裹,确保内部 h-full 正确解析 */}
|
||
<div className="flex-1 min-h-0 overflow-hidden">
|
||
<AIWritingPanel hideAdvancedFields={hideAdvancedFields} />
|
||
</div>
|
||
</ResizablePanel>
|
||
|
||
<ResizableHandle withHandle />
|
||
|
||
{/* 右侧:草稿预览面板 */}
|
||
<ResizablePanel
|
||
id={DRAFT_ID}
|
||
defaultSize={65}
|
||
minSize={40}
|
||
className="overflow-hidden"
|
||
>
|
||
<AIWritingRightPanel />
|
||
</ResizablePanel>
|
||
</ResizablePanelGroup>
|
||
</div>
|
||
</div>
|
||
</TooltipProvider>
|
||
</WritingFormProvider>
|
||
</AIWritingProvider>
|
||
</CanvasUIProvider>
|
||
</CanvasGraphProvider>
|
||
</CanvasThreadProvider>
|
||
);
|
||
}
|