Merge remote-tracking branch 'origin/master' into ji
This commit is contained in:
commit
cee38f93c8
21 files changed
+6764
-2745
No files matched your search
@@ -59,15 +59,29 @@ export async function deleteAIWritingSession(id: string): Promise<void> {
|
||||
if (!resp.ok) throw new Error(`删除失败: ${resp.status}`);
|
||||
}
|
||||
|
||||
/** 保存会话的完整对话时间线,供历史回显出与实时对话一致的富时间线。 */
|
||||
/**
|
||||
* 保存会话的完整对话时间线,供历史回显出与实时对话一致的富时间线。
|
||||
*
|
||||
* 该功能属于「锦上添花」:保存失败不应影响写作主流程,因此本函数永不抛错,
|
||||
* 内部捕获所有异常(网络错误 / 接口 500 等),仅打印告警并返回是否成功。
|
||||
*/
|
||||
export async function saveAIWritingTranscript(
|
||||
id: string,
|
||||
transcript: AIWritingTranscript,
|
||||
): Promise<void> {
|
||||
const resp = await apiFetch(`${BASE()}/${id}/transcript`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transcript }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`保存对话时间线失败: ${resp.status}`);
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const resp = await apiFetch(`${BASE()}/${id}/transcript`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transcript }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.warn(`[ai-writing] 保存对话时间线失败: HTTP ${resp.status}(已忽略,不影响写作流程)`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[ai-writing] 保存对话时间线异常(已忽略,不影响写作流程):', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
||||
const [articleType, setArticleType] = useState('analysis');
|
||||
const [audience, setAudience] = useState('普通读者');
|
||||
const [wordCountInput, setWordCountInput] = useState('800');
|
||||
const [keywordCountInput, setKeywordCountInput] = useState('6');
|
||||
const [keywordCountInput, setKeywordCountInput] = useState('1');
|
||||
const [strictMode, setStrictMode] = useState(false);
|
||||
const [modelName, setModelName] = useState('');
|
||||
const [models, setModels] = useState<ModelOption[]>([]);
|
||||
@@ -100,7 +100,7 @@ export function AIWritingForm({ onSubmit, disabled }: AIWritingFormProps) {
|
||||
function handleKeywordCountBlur() {
|
||||
const num = parseInt(keywordCountInput, 10);
|
||||
if (isNaN(num)) {
|
||||
setKeywordCountInput('6');
|
||||
setKeywordCountInput('1');
|
||||
} else {
|
||||
setKeywordCountInput(String(Math.max(1, Math.min(10, num))));
|
||||
}
|
||||
|
||||
@@ -295,15 +295,19 @@ export function useAIWriting() {
|
||||
async (payload: UserInterventionPayload) => {
|
||||
if (!state.sessionId || !state.currentPause) return;
|
||||
|
||||
// 若处于暂停状态,先重连 SSE 再提交,实现"干预即续接"
|
||||
// 提交干预前始终重连 SSE:
|
||||
// - 正常流程下旧连接仍在,abort 后重建无副作用(后端通过 _session_pending_pause
|
||||
// 重发 await_user,前端 handleEvent 对同一暂停点有去重);
|
||||
// - 若旧连接已因服务重启 / 反向代理空闲超时而断开,这一步能确保后端恢复出的
|
||||
// 会话有消费者,否则 resume 后的进度事件会堆在队列里无人接收。
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = new AbortController();
|
||||
unsubRef.current = subscribeToProgress(
|
||||
state.sessionId,
|
||||
handleEvent,
|
||||
abortRef.current.signal,
|
||||
);
|
||||
if (state.isPaused) {
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = new AbortController();
|
||||
unsubRef.current = subscribeToProgress(
|
||||
state.sessionId,
|
||||
handleEvent,
|
||||
abortRef.current.signal,
|
||||
);
|
||||
setState((prev) => ({ ...prev, isPaused: false }));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,86 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
Reference in new issue
Block a user