Feat/3.0.0 beta1 - #2303
Merged
Merged
Conversation
用户在任务模式带 12 个标书 PDF 提交,浏览器报 504 Gateway Time-out。
后端并没有停:trace 8467e765 从 12:37 一路跑到 12:56,19 分钟全花在
knowledge.rag.pipeline 里——submit_user_question 内联调用
_process_submitted_files,其 daily 分支对每个文件跑完整 ETL(单文件超时
600s),整批塞在一个 HTTP 请求里,nginx 300s 处截断。
把 I/O 那一半挪进 worker,提交只留元数据校验:
- validate_submitted_files 拆出来留在请求内。它是 11021/11022/11023 三个
typed code 的唯一来源(前端有三语文案分支),且是"文件还在解析中"的唯一
拦截点;纯内存判断,微秒级。下沉只会把精确报错变成几分钟后的笼统失败。
- 原始 file refs 落新列 pending_files,worker 在执行前 ingest 成 files。
files 的契约不变(永远只装已解析完成的条目),所以 _init_file_directory /
prepare_file_list / 工作区抽屉 / artifactUtils 全部零改动。
- pending_files 不出服务端:每条 entry 带着浏览器上传时的 presigned 临时链接
(7 天有效),而版本列表可经分享链接访问,回显等于把提交者原件的直链交给
分享接收方。新增 public_dump(),两处序列化出口都走它。
worker 侧的时序是硬约束,不是风格问题:ingest 必须早于
_init_file_directory(本地预取)、_generate_tools(代码解释器快照
os.walk(file_dir))、prepare_file_list(可用文件指针块),以及
WorkspaceBackend 提供 read_file 之前——因为物理写出
workspace/{svid}/uploads/* 的正是 ingest 本身。
IN_PROGRESS 的翻转随之提到 ingest 之前。_is_session_in_progress 只认
IN_PROGRESS,这是拒掉重复队列项的唯一闸门(提交入队 + 浏览器 start-execute
都会来),否则两个 slot 会同时往同一个 workspace 前缀写、共用一个只按
svid[:8] 命名的本地目录、其中一个还会 rmtree 它。ingest 之后补一次
_check_termination_now:解析窗口里落下的停止,会被 _execute_workflow 开头
那句无条件 IN_PROGRESS 写回覆盖,之后再没有任何东西写终态,会话就永远显示
运行中且停不掉。
失败语义分两层:单文件失败在 _process_submitted_files 内降级为
valid=False 继续(一个坏附件不能杀掉十文件的任务);整体性失败(存储不可达)
抛 TaskExecutionError 走 _handle_execution_error,pending_files 保留可重试。
不能吞——吞了就是 files 为空而任务照跑,模型对着"附件里的报告"侃侃而谈,
而它从没见过那份文档。
顺带修一个存量 bug:RedisClient.amget 是
[loads(v) for v in values if v is not None],会丢掉 miss 的键让返回列表变短,
而调用处用 zip(linsight_files, temp_list) 按位置配对——一个过期的
linsight_file: 键就会把 B 文件的 markdown_file_path 安到 A 文件头上。改成
长度一致才按位置配对,否则逐键重读。该 bug 在 2.6 线上同样存在,可独立回种。
前端:worker 推的解析进度只带数据(extra_info.ingest_progress 里的
phase/done/total),文案在 client 的 locale 里——后端格式化好的中文会让日语
用户看到中文,而且这些行会写进持久化历史,切语言之后也修不回来。另补
"准备中" 行:worker 取走任务后 queueCount 归 0,而解析还要几分钟,这段窗口
tasks/sessionSteps 全空,面板原本渲染成空 div。handoff 后拉取附件的轮询从
固定 40×3s(2 分钟)改成两档节奏 + 30 分钟截止,原来的上限扛不住实测 19 分钟。
新增 23 个测试。门禁:pytest test/linsight test/workstation
1101 passed(11 failed 为既有基线:5 需 MySQL、5 需外网、1 model-migration);
pnpm typecheck / lint / check-i18n 全绿;client jest Linsight 174 passed。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
一次任务模式运行整单 FAILED,报 400 `Kimi K3 image_url parts are supported only in user messages`(114 日志里 8 个 trace、62 次)。链路: 模型用 fitz 把 PDF 第 5 页渲染成 PNG 去核对文本是否丢字 → read_file 读它 → deepagents 把图片作为 content block 挂在 ToolMessage 上 → Kimi 拒收。 不是回归。此前的修复治的是块类型轴(file/audio/video 所有端点都不收), 而 image 是被刻意放行的——正是为了这种"看一眼渲染结果"的用法。Kimi 的 约束在角色轴上,守卫从来没建模过消息角色这个维度。 两处独立成因,分别处理: 1. 模型能不能看图 —— 接上 WSModel.visual(系统模型设置 → 工作台模型的 「视觉」列,日常会话早就在用它 gate 附件)。未勾选时 read_file 在工具层 直接拒绝并给出可执行提示,图片根本不进请求;纯文本模型不再 400。灵思的 模型就是从同一份 models[] 里选的,所以这个标记一定查得到。 提示明确禁止猜测图片内容——什么都不给、让模型自己猜,它一定会编。 2. 模型愿意在哪看 —— 勾选时把图片块从 ToolMessage 搬进合成的 user 消息。 所有厂商都接受这个形状,一次修完,不用维护永远慢一拍的厂商表。搬运点是 整批 tool 消息之后:OpenAI 协议要求同一 tool_calls 批次的 tool 消息连续 且先于其它角色,插在带图那条后面会把批次切开。 只改出站请求,持久化的 history 和 checkpoint 形状不变,可回滚。 build_binary_guards 的 supports_vision 做成关键字且无默认值:两个守卫都 fail closed,新调用点漏传会静默关掉读图而不是报错。⚠️ 上线后现存环境的灵思读图默认关闭(visual 字段默认 False,且现网无人 勾选),需管理员在管理后台勾「视觉」恢复。三语 tooltip 已说明这一点。 验证:新增 19 个单测(守卫 13 + visual 接线 6),钉住批次保序、多图保序、 id 不在表里 fail closed 等;用 114 那条 400 的真实请求体(37 条消息)双模 回放——开视觉时 image 从 tool 角色迁到 user,关视觉时一张不发。 test/linsight + test/workstation 1119 passed,红项与基线一致。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 实测(2026-08-13,grok4.6 经 tokenrouter,14.7 万 token 上下文):模型刚吐完 一句旁白「正在撰写完整参数清单与 D2 数据表。」,SSE 流就被上游关闭——没有异常、 没有 finish_reason、没有 usage。langchain 把已累积的内容组装成一条没有 tool_calls 的 AIMessage 返回,deepagents 读作「agent 结束了」,于是 5 步里还剩 3 步没做的任务 以 COMPLETED 收场,那句旁白既当最终答案又被兜底逻辑写成了唯一交付物「报告.md」。 新增不完整流守卫:无 finish_reason/stop_reason + 无 input usage + 完全没有 tool_call 三者同时成立,判定为流没跑完,用独立的小预算重发整次调用;仍然不完整则主图抛错、 子代理降级,绝不放行。IncompleteStreamError 继承 ConnectionError,直接落进既有分类器 的 RETRYABLE 桶和「网络中断」失败卡片,不必给共享 classifier 加灵思专用类型。 判定刻意排除两种形状:没有任何 provider 元数据的消息(合成/降级消息——最初的判据会 把它们全部误判),以及断在 tool_call 中途的流(图会继续跑、不静默,且不值得为它重发 一个六位数 token 的请求)。 test/linsight/test_incomplete_stream_guard.py 14 项覆盖检出、重试恢复、失败/降级分流 与经 TaskExecutionError 包裹后的错误分类。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
现象:后台「工作台配置」页打开后点保存,整份 daily 配置被写成默认值—— 欢迎语、功能描述、输入占位符、侧边栏/助手图标、组织知识库、systemPrompt、 可用工具池(含代码执行器)全部丢失,全链路无报错、无日志,事后只能靠 nginx body_bytes_sent 反推(正常 6100–6500 字节,事故那两次仅 1466/1464)。 链路:租户自动过滤按请求的 visible_tenant_ids 收窄每条 SELECT, Root 自己那行 tenant_workstation_config 因此可能读成"不存在"; aresolve 在 tenant_id == ROOT 时直接断言"无配置"(不再 bypass 重查), get_daily_chat_config_with_meta 于是静默 fabricate 一份内置默认配置, 而配置页原样回存它拿到的东西 —— 一次读缺失 + 一次点保存 = 永久覆盖。 三处修改: - aresolve/resolve:ROOT 读空时先在 bypass_tenant_filter() 里重查一次再下 "无配置"结论。对 ROOT 而言"被过滤掉"和"不存在"本就无法区分。 - get_daily_chat_config_with_meta:回落默认值时打 warning,并返回 is_fallback 供上层区分"捏造的默认值"和"管理员保存过的配置"; /config/daily 响应体透出该字段。 - 配置页:is_fallback 或 configMeta 为空(GET 从未返回)时,保存前二次确认。 顺带按规范收敛 ConfigInheritanceBanner 的 any(6→1)并 lint:prune。 验证:新增 12 条回归用例;反向打掉 DAO 修复后其中 4 条失败,符合预期。 test/workstation/ 175 passed;6 个失败为改动前既有(需真实 DB/MinIO)。 platform lint / typecheck / check-i18n 全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
任务模式的终态事件(final_result / task_terminated / error_message)只走 task-message-stream 一条路,而后端是用破坏性 BLPOP 派发的:socket 在 pop 与 send 之间死掉,事件就跟着一起没了。随后的重连 BLPOP 到空队列并永久阻塞, 于是一个在断线窗口内跑完的任务,会一直停在断线那一刻的快照上转圈、进度条 冻结在中间计数,只有刷新页面才能恢复。 线上实例(114,会话 94f631c8):worker 在 20:35:21.034 推送 FINAL_RESULT, 客户端的替补 socket 在 20:35:22.116 才连上——晚了 1.08 秒。DB 里那一行从 20:35:20 起就是 COMPLETED、5 个交付物齐全,页面却在两小时后仍显示「4/7」。 修法是在每次 WS open 时向 DB(权威)核对一次状态: - 只单向采纳终态。仍在运行的会话一概不动——快照是异步取的,覆盖会冲掉在途 的实时事件;终态之后不再有事件,所以只有那里是安全的。 - 不区分首连与重连:同一个事件也可能被共享队列的其它消费者吃掉(同会话第二 个标签页、上一条连接仍阻塞在 BLPOP 的服务端协程),首连并不比重连免疫。 - 重试预算耗尽时补一次:那之后不会再有 onopen,否则后端重启会把面板永久留在 转圈状态。 - 复用 mapSessionVersionStatus + buildTaskTree 组装,使对齐后的面板与刷新 后的逐字段一致;顺带带上 message_id/liked,点赞才不会打在流式占位 id 上。 waiting_for_user_input(HITL park)与 not_started(排队中)都不是终态, 采纳它们会掐掉 ClarifyCard 和排队卡片。 验证:新增 8 条单测覆盖全部分支;114 真机把 WebSocket 打成黑洞复现原状态 (后端 COMPLETED、前端「正在准备任务」转圈),恢复后重新挂载即自动收敛, 全程未刷新页面。探针实测 ws-OPEN 与对齐请求同毫秒发出,运行中不拉任务列表。 已知边界:若重连发生在终态事件之前、而该事件又被另一个消费者吃掉,本次修复 覆盖不到——根治要动后端的派发语义(建连补发终态,或放弃破坏性 pop)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
用户反馈「展示成工具不太好,给人的感觉是程序执行很慢」。诊断下来问题不是
慢,是把时间算错了账——12 个 PDF 的解析耗时被记到了智能体头上:
改前:读取 6 个文件 · 执行 1 步操作(用时 1416 秒)
🔧 已使用 ingest_uploads >
改后:读取 6 个文件
📄 上传文件解析完成 12/12
三处改动:
1. 秒表整个删掉(两个界面)。一个实时计数器是在承诺「这个数字重要」,
而对模型推理它不重要——没人根据 1416 做任何决定。它只度量你等了多久,
没有分母可推理,秒级精度还在暗示这本该很快。跑 20 分钟的任务因此看起来
像坏了而不是在忙。活着的信号交给旁白和 running 图标,它们说的是「在干
什么」而不是「已经疼了多久」。
副作用:随之消失的是两个 100ms setInterval——显示「有多慢」的东西本身
就在每秒 10 次重渲染。任务模式那个 ticker 当初还专门拆了个组件来隔离它。
2. 附件解析移出工具流(C1)。它是系统在准备用户的上传,不是智能体动作。
从 summarizeActivity 剔除(否则系统耗时算进智能体的动作数),改用
IngestPhaseRow 渲染:文档图标而非扳手,领头的是计数 done/total——这是
整个运行里唯一有先验分母的阶段。
3. 解析过程带实时文件名(C4)。done/total 和当前文件名本来就在
extra_info.ingest_progress 里,此前一个都没渲染,20 分钟一动不动,和卡死
无法区分。复用 NarrationTicker,失败时同一位置显示原始报错。
顺带清掉三个死文件:ToolRow.tsx(零 importer,上一轮的 i18n 错接在它身上)、
useElapsedTicker.ts、utils/duration.ts 的 formatSeconds(docstring 里点名的
两个消费者正是本次删掉的两处秒表)。
日常会话的 DeepThinkingGroup 文案改走任务模式已有的
com_linsight_deep_thinking_*_compact——两个界面共用一对 key 是刻意的,代码
注释本来就写着它们 deliberately isomorphic,共用才不会各改各的漂走。这同时
治好了该文件 6 条冻结的 no-restricted-syntax(已 lint:prune)。
一并修掉 reconcileLinsightFromServer.test.tsx 的 5 个既有 lint 错(来自
8d5fc1e,不在 suppressions 里,一直让分支的 frontend-quality gate 挂着):
store 夹具的 as any 换成 as unknown as Omit<LinsightInfo,'id'>(夹具故意只填
WS pump 真填过的字段,双重断言比补一堆假字段诚实)、删掉可推导的 t: any;
recoil import 与两处中文夹具用带理由的 disable——前者是既有 recoil 代码的测试
脚手架而非新 atom,后者是模型返回的中文答案而非 UI 文案。
验证:pnpm lint 从红转绿(exit 0)、typecheck 全绿、Chat/Linsight/hooks
195 passed(1 failed 为既有基线红,stash 后逐字复现);114 上前后对比截图
确认两个界面的秒表均已消失、摄取行显示为「上传文件解析完成 12/12」。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上一处修复(1f1c1bc4)保住了配置正文,但工具池仍有残留缺口: tools 来自 t_gpts_tools*,是另一批租户表,当请求的 visible_tenant_ids 不含配置所属租户时,sync_tool_info 把自己租户的行读成"查不到"并整批丢弃, 而此时 is_fallback=False,前端那道保存前确认挡不住 —— 在该状态下保存 仍会清空工具池(含代码执行器)。114 实测:修复前该场景 tools=[]。 sync_tool_info 现在在有工具组解析不出时,用 strict_tenant_filter() 重读一次 再下结论。strict 是收窄语义(钉到 tenant_id = 当前租户,不用 IN-list), 不会放宽越权面;只有严格重读也解析不出的才是真删除,照旧丢弃。 仅在异常时触发,正常路径零行为变化 —— 因此不会影响"子租户配置引用了 root 共享工具"的存量场景。 跨租户投影那条路径(_aproject_tools_for_current_tenant)本就在 strict_tenant_filter 里读,不受此问题影响,未改动。 验证:新增 5 条用例;反向打掉修复后"事故场景"那条失败,符合预期。 test/workstation/ 180 passed;6 个失败为改动前既有(需真实 DB/MinIO)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
会话 8a570723(114,kimi-k3 走 tokenrouter)实跑了 156 次代码执行、9 次 读文件、2 次写文件,history 里却只剩 4 条工具记录,页面显示"运行代码 1 次 · 编辑 1 个文件"。 根因不在模型也不在中转服务:OpenAI 规范只保证 tool_calls[].id 在单次响应 内可被 ToolMessage 引用,从不承诺跨请求唯一,tokenrouter 返回 <工具名>:<序号> 且每轮从 0 重数是合规的。越界的是我们—— add_execution_task_step 按 call_id 在整个会话 history 里倒序查找并覆盖, 于是每次调用都盖掉了上一次。 改为在 mapper 出口铸造对外 id(<原始 id>#<run_token>:<序号>),mapper 自己 的 open_calls / orphan_ends / tool_arg_buffers 仍以 provider 原值为键——那是 ToolMessage.tool_call_id 回来的值,配对路径一个字符没动。thinking 的 id 早就 是这么做的(见 StreamContext.run_token 的注释),工具侧只是当年假设了 id 唯一。 为什么不换个改法: - 不把 upsert 收窄成只比对 history[-1]:并行调用的帧序是 start1 start2 end1 end2,end1 的目标不在末尾,会把一次调用拆成两条。 - 不在 langchain client 层改 id:tool_call_id 会随消息历史回传给 provider (114 日志的 Request options 请求体里就带着),改它等于污染发给上游的内容 并写进 checkpoint。mapper 出口是纯观测侧,只出不进。 补 test_call_id_uniqueness.py:把真 mapper 的产出直接喂进真持久化,此前没有 任何贯通测试覆盖这条路径。其中一例专门钉住"对已经全局唯一的 provider 行为 不变"。既有测试里断言字面 id 的四处改为断言关系(start 与 end 相等、原始 id 仍是前缀)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
同一个会话 DB 里 8 个任务是 success,面板却显示"任务已完成 3/18",只有前 三行打勾,其余是灰色空心圈。 根因是 buildTaskTree 里的粘滞标志:遇到第一个 terminated/failed 行之后, 后面所有行的 status 一律被改写成 not_started。这行逻辑来自 2025-07 的 e1c2324(commit message 只有 "fix: some bugfix"),当时 terminated 只可能 是"用户点了停止";而 _diff_todos 现在也用 terminated 表示"模型把这条 todo 从自己的计划里剪掉了",两种语义撞在一起。 丢的还不只是勾:isTaskStarted('not_started') 为 false,而三个步骤流入口都是 tasks.filter(isTaskStarted),所以那些任务跑过的步骤在刷新后也整片消失。 直接删掉,逐分支核对过不会退化:手动停止时后端 _terminate_unfinished_tasks 已把未完成行扫成 TERMINATED、getLinsightTaskList 会改写残留的 in_progress、 TaskPanel 的停止态渲染读自己的 terminated prop;terminated 与 not_started 在 面板里命中同一个空心圈分支;没有 history 的剪掉行被 TaskStepRow 的空守卫拦 下。live 态本来就不走 buildTaskTree,这次是刷新态向 live 态对齐。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
execute-task-detail 对同一个会话返回 20 条,而 DB 里只有 11 行——9 个 todo 各出现两次,面板的分母因此翻倍。 sort_tasks_by_chain 把每个 previous_task_id 为空的行当链头、各走一遍链再 extend。而 _save_task_info 只 INSERT 新行,对已存在的行只更新 task_data, 从不回填 previous/next 指针:模型第二次 write_todos 新增的行接到了旧链头 前面,旧链头的 previous_task_id 却还是空,于是两个链头共用一条尾巴,尾巴被 走了两遍。原来的孤儿兜底用的是 processed_ids 集合,只保证"没走到的补上", 消不掉重复。 出口加 visited 集合:跨链去重,同时给这个原本无保护的 while 循环兜住成环 (今天没人会写出环,但一对坏指针就能把请求挂死在这里)。 没有改 _save_task_info 回填链指针:GenerateSubTask 带的是"新增 + 改名"的增量 而不是全量投影,模型把新 todo 插到计划中间时(旧 [A,B,C] → 新 [A,D,B,C], delta 里只有 D),这个函数根本不知道 D 该插在 A 和 B 之间——要正确回填得先 把事件升级成携带全量有序投影,那是跨 mapper/task_exec/DAO/前端的改动。 出口去重是纯读路径,且对存量会话立即生效。 同一处缺陷也影响案例展示的 get_sop_showcase_result,一并修好。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tool_arg_buffers 按 provider 的原始 id 存,正常由 end 帧清掉。但调用被中断时
(HITL park、递归中断、预算拒绝)不会有 end 帧,半截 JSON 就留在里面;而会
复用 id 的 provider(<工具名>:<序号>,每轮从 0 重数)转头就把同一个 id 发给
下一次调用,两段参数于是拼在一起。
多数时候只是 json.loads 失败、委派目标降级成空;但残留如果差一点闭合,正好
被新调用的第一片补上,就会解析成功并把**上一次**调用的目标当成这次的报出来
——测试里 `{"description": "stal` + `e"}` 就是这种情况。
清理点选在 _handle_tool_starts 里「open_calls 中已存在同 raw id」的那一刻:
正常结束的调用已经被自己的 end 帧 pop 掉了,所以这个条件只有真残留时成立。
不能改成在 start 时无脑重置——_accumulate_tool_args 在同一个 chunk 周期里先
于 _handle_tool_starts 运行,无脑重置会把本轮刚到的第一片参数一起清掉。也不
依赖「只有第一个 delta 带 id」这类 provider 行为假设。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o feat/3.0.0-beta1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
简要描述做了什么改动。
Why
为什么需要这个改动?
How
实现方式、设计决策(如有)。
Test
Related