Feat/3.0.0 beta1 - #2295
Merged
Merged
Conversation
The backend already said why a custom model could not be deleted, but the page still showed "delete failed, please refresh". Two reasons, both mine: - The response interceptor rejects a business error with a bare message string, so reading `error.data` for the reason always came up empty. Callers that want to explain a failure need the envelope; `silent` yields it. Declared the flag on AxiosRequestConfig so passing it no longer needs an `as any`. - The refusal lands while the batch is being *drafted*, not at publish — the catalog validates the changes as the draft is built. Asking for the envelope on the publish leg alone never saw it. The page-level tests now fail the draft leg, which is what actually happens; the earlier version tested a path that never fails first, which is why it passed while the toast stayed wrong.
…lance Both pages showed everything at once and explained none of it. Action board — the card carried a name, a code, a switch, a row of resource chips and a full-width level select, so twelve actions filled five columns with repetition. The card is now a line: drag handle, name, on/off, plus a compact level dropdown underneath. Dragging stays the fast path and the dropdown is the same move for a keyboard or a touch screen; the code and the resource scope moved into a tooltip. The drop target highlights while dragging, "unassigned" lost its amber warning styling (it is an ordinary state), and the pending-changes bar can now list what actually changed — a count alone told the author nothing before they published it. Model editor — the header showed a bare "3" with no label; it now names the model, marks it off when it is, and calls the number what it is, next to a line saying the level comes from the highest action selected. The action checkboxes are grouped by level so that derivation is visible in the structure rather than asserted. Both switches gained the consequence their labels never carried, "allow same level" in particular. The model list shows tier and on/off per row. Also, from review: - Every control here overrode its height to 44px; the spec tops out at 40. Dropped the overrides so buttons, inputs and selects use their own sizes. - The selected list row sat its two lines against the top edge. - The unpublished-draft notice was the last thing in the scrolling form, so the button that publishes it was below the fold and saving looked like a no-op. It now lives in the standing footer beside the button that produced it. - Hardcoded blue/amber/red gave way to theme tokens. Tests follow three changed contracts: level is picked from a menu (new `selectMenuOption` helper), resource scope is revealed on hover, and the derived level is asserted through `data-level` since the visible text is localized.
客户环境一个会话第 1 轮用任务模式跑完 1072 页标书拆析,第 2 轮起 悄悄变成日常会话,模型读不到文件,最后对用户宣称"文件已被平台回收, 这是平台的运行机制"——该归因是模型编的,文件当时仍在后端缓存目录。 根因在前端:模式的重置与恢复分在两个 effect 里。重置依赖 location.key,每次导航都跑 setTaskMode(false);恢复的依赖 [conversationId, isTaskConversation, canUseTaskMode] 在同一会话内 全是常量,重置之后永远不会再触发。于是任何一次不换会话的导航都会 把任务会话永久打到日常——最容易踩中的是侧边栏"首页",它的 to 就是 当前 pathname 且没有同路径守卫。而 (taskMode || taskRunning) 让按钮 在任务执行期间继续高亮,掩盖了这次重置,用户完全无从察觉。 - 两个 effect 合并为一个,模式统一由会话历史派生;决策逻辑抽成纯函数 resolveTaskMode.ts 便于回归。新增 userToggledRef,手动选择在本会话内 优先于派生值。自重写 flag 仅在落到真实 id 后消费,否则会在提交当次 commit 里被提前吃掉,反而把刚提交的任务模式清掉。 - MainLayout 侧边栏 NavLink 加同路径守卫(onNavigate 仍触发,H5 收抽屉 不受影响),从源头减少无谓导航。 顺带修掉同一条链路上让模型"读不到还硬答"的三处: - file_context 按字符硬切 maxTokens(名字叫 token,实际是字符)且无任何 标记,模型把 15000 字符的片段当全文,复述从未见过的页码。截断时追加 已展示/总长度说明,并禁止推断未展示部分。 - get_chat_history 丢弃 question 行的 files,历史里没有任何痕迹表明传过 文件。补上附件名(只补名字,events 单轮可达 80KB,回灌会击穿 history_max_tokens)。 - LocalExecutor 是同机 subprocess 而非沙箱,模型 os.walk 到全局共享的 下载缓存读到了其它会话的上传原件。新增 workspace_escape_guard 在执行前 拒绝越界访问,并把子进程 HOME 指向工作目录(模型正是搜了 ~)。匹配锚定 在访问动作上,prose 里提到宿主路径仍合法;local_sync_path 开头的绝对 路径是灵思自己的工作区,予以豁免。 - 删掉 has_citation_tool 死变量:它算完从未被读取,且默认日常提示词模板 已是 CITATION_PROMPT_RULES 的超集。 测试:新增 45 个后端用例 + 11 个前端用例。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
任务模式此前只能逐个选文件,且即使前端把路径塞进文件名,后端
`_safe_basename` 也会把 `/` 折成 `_`,目录层级被彻底销毁。
工作区本身早就支持嵌套——WorkspaceBackend 的 read/write/ls/glob/grep、
本地写穿缓存、跨轮 seed_workspace_from_previous 都是嵌套安全的,技能包
每次任务启动都以 `skills/<name>/<rel>` 的嵌套路径写进同一个前缀。缺的
只是把用户挑的那棵树带进来。
后端
- SubmitFileSchema 增 relative_path / size;chat_service 统一入口透传
- 新增 _safe_relpath:保留层级的清洗器(对照 skill_store._safe_rel_path),
逐段丢弃 . / .. / 空段,构造路径无法逃出 uploads/
- _write_attachment_to_workspace / _write_raw_original_to_workspace 落
`uploads/<rel_dir>/<name>`,原件与其 md 视图同目录
- _dedupe_workspace_name 去重命名空间改为完整相对路径:跨目录同名文件
不再互相覆盖,后缀只加在文件名上(目录 `v1.2` 不会被当扩展名改写)
- _validate_folder_upload:≤100 文件 / ≤500MB / 深度 ≤10,整批拒绝而非
静默截断——截断会交给用户一个看起来完整、实际残缺的工作区
- upload-file 端点补上单文件大小校验(此前完全没有)
- prepare_file_list 按目录分组;超过 40 个附件降级为目录概览 + 引导
ls/glob,避免第一条 user message 被上百行指针淹没
- task_exec._download_file 补 makedirs;顺带修 ls 前缀缺尾斜杠导致
`ls("/uploads/年报")` 会连带匹配 `年报备份/`
前端(仅任务模式开放,日常对话无工作区、其对象键按原始文件名易撞)
- 抽出 ~/utils/folderUpload.ts:递归读目录(含 readEntries 分批循环、
合成 webkitRelativePath)、拖拽目录同步取 entries、批量限额;知识空间
的同名实现改为复用它,不再两份
- InputFiles 增 webkitdirectory picker 与 openFolderPicker()
- 去重键由文件名改为相对路径:此前不同子目录的同名文件会被静默丢弃,
是无提示的数据丢失;删除也从按名改为按 clientId
- 拖入文件夹此前什么也拿不到(dataTransfer.files 不含目录内容)
- 文件夹收成单个 chip(根目录名 + 文件数),不铺满输入区
- 先过滤(隐藏文件 / 不支持类型)再限额:macOS 的 .DS_Store 不该占配额
测试:后端 test/linsight/test_folder_upload.py 23 例(路径穿越、跨目录
同名、嵌套 key、双轨原件、限额边界、提示词分组与降级);前端
folderUpload.test.ts 12 例。既有 11 个后端 / 8 个前端失败均为存量。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上传进行中点附件的 X,卡片不消失;等上传跑完,卡片依然在。用户看到的是 一个转着圈、点不掉的附件,自然理解成"文件传不上去"——实际上传是成功的。 根因是 InputFiles 里删除动作与在途上传回调打架:handleFileRemove 把文件 从列表里摘掉,但那次上传的 progress 与 then 回调各自闭包在删除前的快照上, 随后把它原样写回,删除等于没发生。 同一个文件还有一处更基础的问题:多处在 setFiles 的 updater 函数里调用父组件 回调(onFilesStateChange / onChange)。updater 在渲染阶段执行,于是渲染 AiChatInput 的过程中又去更新它,控制台常驻那条 "Cannot update a component while rendering a different component" 警告,而 React 会丢弃这次更新。讽刺 的是这个文件自己的注释(选择处理器里)就写明了不能这么做。 - 删除时先认领该 id(removedIdsRef)再动状态,并 abort 对应的上传:progress 与 then 回调在写回前检查该 id,已删除就跳过。43MB 的传输也不再白跑完。 - catch 分支区分主动取消与真实失败:取消不弹"上传失败",也不重复递减计数。 - 四处 updater 内的父组件通知全部移到 setFiles 之后,改为从 filesRef 派生 下一份列表(与 then 回调既有写法一致),渲染期更新父组件的问题随之消失。 - uploadChatFile 增加可选 signal,透传给 axios。 验证:114 环境实测三条路径——上传中删除(卡片立即消失、日志出现 aborted_by_remove、无失败提示)、正常上传、上传完成后删除,均符合预期。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11021–11024 只加进了 packages/locales 的源,没跑 gen:locales,于是 client/src/locales/*/api_errors.gen.json 与 platform/public/locales/*/ api_errors.json 里没有这四条——前端拿到错误码只会显示 key 本身。 产物由 packages/locales/scripts/build.mjs 生成,不手改。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
「上传文件夹」在任务模式下不出现。门控写的是 isLingsi,而这个 prop 是 遗留物——全仓没有任何调用方传 true,AiChatInput 里它恒为 false。同一个 误判还让拖入文件夹在对话内任务模式失效。 真正的信号是 taskMode,且它无法从 uploadMode 推出来:对话内任务模式的 附件仍走共享上传端点('workstation'),只在提交那一刻才变成任务。所以 InputFiles 不该自己猜,改由调用方显式传 allowFolderUpload。 - InputFiles 新增 allowFolderUpload prop,替代 uploadMode === 'linsight' - AiChatInput 的入口/拖拽/prop 全部改用 taskMode - TaskModeInput(执行页)显式传 allowFolderUpload 114 实测:任务模式菜单出现「上传文件夹」(图标与三语文案正常、无控制台 报错),日常对话仍隐藏。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
任务模式的可上传类型一直沿用「解析器能不能解析」这个判据,但它有工作区和 代码执行器,真正该问的是「工作区里有没有消费方」。结果 csv、py、json 这类 明摆着要交给 pandas / read_file 处理的文件反而传不进来。 前端 chatAccept 增加任务模式专属清单(csv/tsv/json/jsonl/xml/yaml/toml/ ini/conf/log/sql/py/js/ts/sh),与后端 _PASSTHROUGH_TEXT_EXTS 对齐。日常 对话保持窄白名单——它只能把附件正文塞进 prompt,解析不了就整轮报错;改为 在关闭任务模式时移除不兼容附件并提示。顺带修掉执行页追问框单独排除 .ofd 导致的两个入口不一致。 后端引入「直通」摄取:不解析,原件即工作区文件。 - _ingest_route 用显式表裁决 parse / passthrough / unsupported。csv、html、 txt、md 留在 _PARSE_WINS_EXTS 走解析,因为双轨(md 阅读视图 + 原件)比纯 直通严格更多;markdown 不在其中,解析器只注册了 md。 - parsing_status 不新增取值。前端有 7 处把 completed/failed 以外的值判成 「解析中」从而禁用发送或无限轮询,_process_submitted_files 更会直接拒收; 走哪条管线改记在正交的 ingest_mode 字段,老数据没有该键即走原分支。 同时修掉三处一直在说假话的地方: - 直通文件此前被标 failed/valid=False,附件 chip 报「解析失败」,但文件完全 可用;大 csv 尤其明显(ExcelLoader 超 1 万字符硬失败)。现按能否读成文本 三分支:文本降级为成功,二进制原件仍如实报失败,其余仍 unsupported。 - 指针块对文本原件写「不可 read_file」,而 read_file 完全读得到。说明段改为 单段按需拼接分句,避免三类文件共存时自相矛盾。 - 每个直通类文件都要先空跑一次 ETL、靠异常兜底,还留下一条误导性的 logger.exception;unsupported 同样在空跑。现在都在解析前短路。 另修一个已存在的路径 bug:兜底 entry 不带 raw_filename/original_file_path, 于是被平铺预取到任务目录根,而指针块告诉模型的是 uploads/ 下的路径——代码 执行器里那个路径根本不存在。直通 entry 镜像 raw_* 两个键,task_exec 的两条 筛选相应调整,避免同一份字节被下载两次。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
上一版把 csv/txt/md 留在解析路径,理由是"风险最低、可独立回退"——那是求稳 不是判断。核实后这三个都只有损失: - csv 走的是 ExcelLoader,一个 RAG 切片器:按 data_rows 切块、每块重复表头、 超限抛 KnowledgeExcelChunkMaxError。而 csv 本来就是纯文本,read_file 直接 读得到(还支持 offset/limit 切行看头部)。那个 md 视图是把已经是最终形态的 东西重排一遍,还多存一份。 - txt/md 解析完会被 splitter 切块、再用空行拼回去,模型读到的已经不是用户传 上来的那个文件。换回来的只有一个编码探测,而 WorkspaceBackend 读取时本来 就有 cchardet 兜底。 只有 html/htm 是真转换(剥标签),保留在解析路径。 顺带:markdown 不在解析表这件事不再需要单独说明——整个 md 家族都直通了。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
用户在任务模式提交了一个带 12 个附件的任务,等了很久没有任何反应, 连"任务模式"标识都消失了。查下来任务确实创建了,但 linsight_session_version 停在 NOT_STARTED——从没进过队列,worker 自然永远不会执行。 入队原本是浏览器的职责:submit 建好 session、流式返回 linsight_task_handoff, 浏览器收到后再 POST /workbench/start-execute 才真正入队。这两步之间的窗口 并不窄——submit_user_question 会**在请求内逐个解析附件**,实测 12 个 1.5MB 的 PDF 就要 4 分钟,用户的标书只会更久,而界面在此期间没有任何进度反馈。用户 一旦刷新、关页面或代理超时,第二个请求就永远不会来,session 成为无人认领的 孤儿;同时 category=task 的消息行也没写成,所以刷新后连任务面板和模式标识 一起消失。 - submit 建完 session 立即服务端入队(enqueue_session_for_execution, 与 start-execute 共用同一入口),任务是否执行不再取决于客户端是否还在听。 入队失败只记日志不中断,客户端的 start-execute 仍是兜底。 - start-execute 对 IN_PROGRESS 改为幂等返回成功。服务端入队后 worker 往往 已经把 session 领走,此时再报 LinsightSessionVersionRunningError 会让前端的 .catch 把一个正常运行的任务标成失败。COMPLETED/TERMINATED 仍然拒绝重跑。 - 重复入队本身安全:执行器读回 session 后由 _is_session_in_progress 挡住重入。 验证:114 实测——提交带 6 个附件的任务后 8 秒即关闭标签页(此时 handoff 尚未发出,客户端永远不会调 start-execute),任务仍自动入队并执行至 COMPLETED。旧行为下它必然停在 NOT_STARTED。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
入队逻辑移到 linsight_execute_utils 后,该测试仍只 patch endpoint 模块下的 get_redis_client,utils 用的是自己那个名字,桩没拦住,调用一路打到数据库并 抛 NoTenantContextError。给 utils 也打上 Redis 桩。 顺带把 test_start_execute_rejects_in_progress 更名为 ..._is_a_no_op: IN_PROGRESS 现在返回 200 而不是错误,旧名字会误导后来人。断言同时补上 "返回成功且不重复入队"。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
一次真实的投标类任务(9 份 PDF / 833 页 / 109 项参数逐项提取)在 115 轮处 只走完计划 7 步里的前 3 步——页级文本索引与项目结构核实做完了,109 项参数 一项都没提取就被软着陆强制收尾。而这还是同类任务里偏轻的一个,手上另一个 的工作量约为其两倍,故直接按最重的场景给到 600。 - max_model_turns 115 → 600(主图) - max_model_turns_subagent 30 → 120:逐项提取与交叉校验这类重活压在子代理上, 30 轮往往只够把材料读完;与主图同比例放宽。(该预算已按每次 task 调用独立 计数,见 resilience_middleware._budget_key,不是整次任务共享。) - max_steps 600 → 2500:保险丝必须晚于预算才能让软着陆生效, (600 + 10) × 4 + 20 = 2460,取 2500 留头。_resolve_recursion_limit 本就会 在配置偏低时自动抬高(既有部署的库里往往还是 200),这里显式写清楚, 省得排查时对不上号。 代价是跑飞时烧得更久(最坏约 2460 个 super-step)。超大任务仍建议拆成多次: 单次任务另受 checkpoint 全量快照的写放大与上下文长度制约,继续堆高单次预算, 先撞到的多半是这两个而不是轮数。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
init_config 只在数据库没有 initdb_config 这一行时才写入 yaml。装过一次的环境 这行早就在了,此后新版本里加的配置项永远到不了数据库——代码只能落到 settings.py 的 Field default,而系统配置页面里根本看不到这个项,运维也就无从 调整。灵思的 max_model_turns 就是这样:默认值早已前移,存量环境却一直按旧值 在跑,直到有人去读代码才发现。 改法是每次启动做一次增量补齐: - 只补数据库里缺失的 key,已有值一律原样保留——运维的调参永远优先于我们的 默认值,这条是它能无人值守地跑在每次启动上的前提。 - 数据库独有、文件里没有的 key 不动(手工加的配置不会被抹掉)。 - 新 key 连同其上方注释以原始文本搬运,不走 yaml.safe_load/dump 往返——那样 会把整份配置的注释清空,而那些注释正是运维在配置页面读的说明。 - 深度两层:顶层段缺失则整段带入(含嵌套内容);段已存在则只插入其缺失的直接 子项。够覆盖真实的新增形态,也避免深层合并带来的歧义。 - 文件与数据库对同一个 key 一个是段一个是标量时跳过,不猜哪边对。 - 失败只记日志不阻塞启动;补完清掉 config:initdb_config 的 redis 缓存, 否则新值要等 100s TTL 才可见。 114 实测:补入 9 个 key(linsight 的三项轮次预算、daily_chat 整段、 env.enable_media_upload 等),同时 linsight.max_steps 保持环境自己的 200、 etl4lm.url 与 office_url 等本地定制值原样未动;第二次启动不再写入。⚠️ 一个必须知道的副作用:补进数据库之后,该项就以数据库值为准,后续再改 settings.py 的默认值对这个环境不再生效——这正是 DB 优先的既有语义,本次只是 让新项在第一次启动时就进入数据库,而不是永远进不去。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The end-user chat already refuses more than five audio/video files per turn (client `InputFiles`), but the workflow editor's run panel had no such limit on either upload entry, so a debug run could queue dozens of clips and hold that many ASR slots. - dialog input (`ChatFiles`): counts what is already attached plus the incoming batch, same algorithm as the client - form input (`inputFileComponent` batch upload): counts only the picked batch — a form field replaces its value rather than appending Also pays down the hardcoded Chinese toasts in `inputFileComponent` (size-exceeded, no-file-selected) that this change touched.
Clicking a media file in 参考资料 landed on "preview not supported" in both apps, even though the clip is a normal knowledge-base file with a playable original URL (the citation's downloadUrl points at the file in MinIO, not at the transcript). - client: teach `FilePreview` the audio/video extensions and render the knowledge-space `MediaPlayer`, so a clip opened from a citation looks like the one opened from the file list. Zoom is off for players. - platform: native <video>/<audio> controls in the citation preview switch, matching how the platform knowledge space previews media. Also pays down the hardcoded Chinese in the platform citation drawer (unsupported-format copy, loading/empty states, four aria-labels).
…al file A knowledge file has two addresses: the original upload, and the renderable stand-in the backend derives from it (a clip's transcript, a pptx's converted PDF, a page's parsed markdown). Both apps used one URL for both jobs, from opposite ends: - client previewed and downloaded the stand-in, so a cited clip rendered as its transcript and downloaded as that markdown under an .mp4 name. docx/pptx had the same defect, downloading a PDF under the original extension. - platform previewed and downloaded the original, so a clip had no transcript beside it. Media citations now show what the knowledge space shows — the player plus the 识别文本 / 入库文本 pane — and downloads always hand back the original upload. Media is detected from the file name, since a clip's preview URL ends in .md. - client: `resolveCitationDocumentUrls` returns both addresses; the viewer picks per type and `FilePreview` gained `transcriptUrl` - platform: `getCitationDocumentPreviewUrl` finally returns previewUrl; the transcript pane is extracted from `RichPreviewFile` for reuse Also fixes the platform panel's dead download button (it read the unresolved citation, whose payload carries no file URL until resolved — the preview body resolved on its own, so only the button saw nothing), and both apps now report a missing URL instead of ignoring the click. Pays down the hardcoded Chinese in every file touched (~40 strings).
The citation panel is ~520px wide, so splitting it into player and transcript columns left both halves cramped. Media now stacks: player on top, 识别文本 / 入库文本 below — matching what the platform panel already does. Only audio/video takes this layout; other formats are unchanged.
文件夹上传的指针块与 >40 文件的目录概览,都逐字告诉模型用
glob(如 "/uploads/**/*.xlsx")定位文件。这个写法一直返回 0 匹配:
- ls 报的是带前导斜杠的工作区路径(/uploads/a/b.csv),但匹配是拿
去掉斜杠的对象键(uploads/a/b.csv)做的,fnmatch 对首字符不含糊,
所以提示词唯一教过的绝对写法从来没匹配上任何东西。
- fnmatch 没有 **,它只是一个能跨 / 的 *,于是 uploads/**/*.csv 要求
至少一层中间目录,直接漏掉直接躺在 uploads/ 下的文件。
失败形态是最糟的那种:大文件夹下概览块是模型拿到具体文件名的唯一
入口,glob 静默返回空,而同一段提示词下一句正好写着「不要假设文件
不存在」。grep(glob=...) 共用这段比较,一起修。
114 实测:修前 glob('/uploads/**/*.csv') 0 匹配、修后 3 匹配,与
glob('uploads/**/*.csv') 一致。
A placeholder that the template author underlined came out plain in the
generated report, and so did the rest of that line.
Replacement rebuilt the whole paragraph: it took `paragraph.text` — which
flattens every run into one string, dropping all run-level formatting —
then re-stamped the static text with the FIRST run's format and gave the
substituted value whatever formatting the value payload carried, which for
a report node is none at all. The rebuilt paragraph was also a bare `w:p`
with seven properties copied over, so the paragraph style, numbering,
borders and tab leaders went with it.
Text values are now written into the run that already holds the
placeholder, so its rPr — underline, bold, font, size — applies to the
substituted text, and every other run and the paragraph itself are left
untouched. Placeholders split across runs (Word does this routinely) are
handled by spanning the runs they cover. Values carrying a table, image or
heading still take the rebuild path: those cannot live inside a run.
Extra text items and any trailing text clone the placeholder run's
formatting rather than the first item's, so a value like
[{"content": "a"}, {"content": "b", "bold": true}] inherits the underline
for both parts and adds bold only to the second.
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.
No description provided.