Commit Graph

9005 Commits

Author SHA1 Message Date
GuoQing Zhang dfadbdace7 fix(channel): keep edit form initialization stable 2026-08-13 17:11:38 +08:00
Kinyoo 65669cd5b7 style(permission): one empty state for every permission list
The authorization panel had the illustration, the member list and the
three subject pickers had a bare grey line of text — so the same "there
is nothing here" read as two different states depending on which list
you were looking at, and the pickers' `py-4` left the message floating
near the top of a tall scroll area.

PermissionEmptyState holds the illustration + message and centers in
whatever height its parent gives it; all five lists consume it. Each
keeps its own copy — the pickers still distinguish no-results from
no-departments/no-groups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:16:29 +08:00
Kinyoo b6f48b6f06 style(chat): mobile drill panels match the menu chrome
The org / skill drill panels kept their pre-restyle back row — a
lucide chevron on slate greys, separated from the list by a hairline
that no other panel in this menu draws — and only the knowledge popup
had been switched to the list padding, so the "+" menu's drill panels
still doubled the gap under their last row.

The back row now uses Outlined.ArrowLeft on the menu's own greys with
a short divider before the title, drops the underline, and the list
padding rule keys off mobileTallPanel so every mobile list panel gets
it, not just the knowledge popup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 15:03:13 +08:00
Kinyoo f9b3bbe821 refactor(client): share the file-type icon map, drop two dead modules
AiMessageBubble carried its own extension → bisheng-icon table, a copy
of what FileIcon already knows. FileIcon exports getFileTypeIcon and the
bubble consumes it, so a new file type is registered once.

Also deletes FileListRow and the knowledge mock: neither has had a
call site for a while (the file list renders through FileTable /
FileCard). Suppressions shrink accordingly. The share dialog picks up
the icon import while it is here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:28:26 +08:00
Kinyoo a9409dd9b4 style(chat): one chrome for the input menus, one resting icon tint
The "+" menu, knowledge pill, skill panel and the model / group selects
had each drifted: 8px vs 16px radius, per-menu padding, rows sized by
padding instead of height, and icon greys ranging over #999 / slate-600.

- Menus settle on rounded-2xl, 12px padding, 8px gap and 32px rows with
  8px radius. Select grows a viewportClassName so its scroll viewport
  can carry that padding instead of the outer panel (which would clip
  the scrollbar).
- Resting icons all use #4E5969; active stays brand. The upload item
  drops its baked-colour link.svg for Outlined.Attachment so it can
  follow that tint.
- The "+" panel aligns to the input box's left edge rather than its own
  trigger, which sits 13px inside it. The knowledge pill keeps trigger
  alignment — mid-toolbar, where the input edge means nothing.
- Skill rows carry selection themselves (brand tint + trailing check)
  now that the leading checkbox is gone; name and description each
  clamp to one line and move the full text into a tooltip, but only
  when measurement says they actually overflow. Empty search gets the
  illustration instead of bare text.
- The knowledge panel takes its own 320px cap (skills/org sit at 440)
  so it still opens downward more often, and the hand-rolled
  useSubMenuLayout measuring goes away — align="center" plus Radix's
  own collision handling already place these panels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:28:26 +08:00
Kinyoo e2ab7d2576 style(chat): keep a conversation row filled while it owns transient UI
Opening a row's options menu or its rename input moves the pointer off
the row — into the portaled menu — so the hover fill dropped away and
the row you were acting on stopped looking involved.

Both sidebars now pin the hover fill for as long as the row owns that
UI. It stays the hover fill, never the active row's: this row is not
the open conversation. On coarse pointers it applies too, where it
reads as "this row owns the menu" rather than as a hover affordance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:28:26 +08:00
Kinyoo 1ccd2635bf fix(knowledge): commit an inline rename when the click lands outside
Rename is entered from a Radix dropdown item, and Radix restores focus
to the menu trigger as it unmounts — after our synchronous focus() call.
The input therefore never held focus, never fired blur, and clicking
away left the field open with the edit unsaved.

Two halves: re-assert focus once that restore has run (in the existing
selection timeout), and commit from a capture-phase pointerdown listener
so an outside click saves even when blur never fires. Since an outside
click now reaches the commit twice — the listener, then the input's own
blur — a ref mirrors isRenaming so the second one is a no-op instead of
a duplicate onRename. Tests cover both paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:28:26 +08:00
GuoQing Zhang 78fb5e122f Feat/2.6.0 0811 (#2293)
## What

简要描述做了什么改动。

## Why

为什么需要这个改动?

## How

实现方式、设计决策(如有)。

## Test

- [ ] 本地测试通过
- [ ] 114 测试服务器验证通过

## Related

- Issue/ticket:
2026-08-13 11:34:44 +08:00
Kinyoo cbe1cace36 style(error-page): plain copy link, toast instead of a label swap
The copy link was underlined on hover and swapped its own label to
"copied" for two seconds. Both read as emphasis the sentence does not
want, and a label that changes under the cursor is easy to miss on the
one screen a user is likely to be screenshotting rather than reading.

The link now never underlines and never changes text; ErrorPage fires a
new `onCopied` callback instead, and the client raises its usual toast
(existing `com_ui_copied_to_clipboard` key, all three languages). The
package still carries no i18n and no toast of its own. `labels.copied`
and the now-dead `com_error_page.copied` key are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:53:48 +08:00
GuoQing Zhang 787314c194 Merge remote-tracking branch 'origin/feat/2.6.0' into feat/2.6.0 2026-08-12 19:34:28 +08:00
Kinyoo ec1f09b692 style(client): knowledge icons from bisheng-icons, semibold sidebar titles
The two hand-drawn knowledge glyphs (BookOpen / Books) predate the
matching bisheng-icons entries. Every call site — hub nav tab, the
attachment bar's KB card, the task-mode context chips — switches to
Outlined.Book / Outlined.Books, and the local SVG components are
deleted (ChatKnowledge only carried dead imports).

Sidebar section titles drop from bold to semibold, matching the weight
the rest of the sidebar chrome uses.

Suppressions: pruned the entries the deleted icons left behind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:24:52 +08:00
Kinyoo f94d370883 style(chat): center submenu panels on their trigger row, drop dup headings
The knowledge / skill submenus aligned their top edge with the parent
menu via a hand-computed alignOffset (measure the trigger, subtract
rects, re-measure on resize), and each panel repeated a heading that
its own trigger row already carried right next to it.

- DropdownMenuSubContent accepts align="center". Radix types SubContent
  as 'start' | 'end' but hands the value straight to Popper, which does
  support 'center', so the type is widened rather than the offset
  hand-computed. The knowledge, org-knowledge and skill panels switch
  to it and the alignOffset machinery goes away — useSubMenuLayout now
  only clamps max height against the viewport.
- Panel headings drop where the trigger already names the list; the
  mobile drill panels keep theirs, since there the heading IS the
  back-navigation label. The knowledge pill's two variants collapse
  into one branch with a mobile-only heading (the narrow toolbar can
  render its trigger icon-only).
- Skill panels grow to 280×440 so the list isn't a letterbox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:12:22 +08:00
Kinyoo b66398e272 feat(chat): confirm a recorded feedback verdict with a toast
Rating an answer changed only the icon's highlight, which is easy to
miss and says nothing about what the rating is for. Every persisted
verdict — thumbs-up, or a dislike once its dialog is submitted — now
raises a "thanks, we'll use it to improve our answers" toast (new key,
all three languages).

Un-toggling stays silent: the icon losing its highlight is feedback
enough, and a toast there would read as if cancelling had itself been
recorded. onLike may now return the request promise, and the two call
sites do — so a failed request shows only the interceptor's error
toast, never both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:12:22 +08:00
Kinyoo d34f2511d1 style(permission): selectable access-mode rows, centered settings footer
- AccessModeSelector rows now behave like the options they are: hover
  tint, brand-tinted background while checked, and the label kept on
  one line so only the description truncates. Disabled rows opt out of
  the hover tint so they still read as unavailable.
- That truncated description gets the same hover-only tooltip the
  member list uses, so TruncatedTooltip moves out of PermissionListTab
  into its own module. Its polymorphic ref keeps one `any` — now with
  an inline reason — and the file's suppression entry shrinks.
- Both settings pages center their footer actions unconditionally; the
  knowledge page had centered only in create mode, so the same footer
  jumped between layouts depending on how you entered it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 18:04:22 +08:00
LineWalker e8132a78ee Revert "fix(file-viewers): 把 xlsx 图片绑定到它真正所属的 sheet"
This reverts commit 516130dd8258f8ac2e2f68e0e5b3d1ba38b98c9a.

修复留在 3.0 线,2.6 不再跟进——客户后续升级到 3.0 时一并拿到。
2026-08-12 17:57:53 +08:00
Kinyoo 748fc43455 refactor(permission): one level menu and one dialog shell across the surfaces
The member list, the create-page draft and the two "新增授权" dialogs had
each grown their own version of the same two things — so the level
control looked different depending on where you opened it, and the
draft picker was a look-alike of the share dialog rather than the same
dialog.

- PermissionLevelMenu extracts the list's dropdown (levels, separator,
  destructive 移除) and the draft editor adopts it, dropping its
  RelationSelect + separate delete icon button. Removal now always
  lives inside the menu, never as a button beside it; a row with
  neither permission renders the plain label at the same width, so the
  column never jitters.
- permissionDialogStyles holds the share dialog's chrome verbatim —
  shell sizing (80vh card / full-screen under 768px), subject tabs,
  包含子部门 toggle, footer captions — and the draft picker consumes it
  instead of re-approximating it.
- Empty authorization lists get their own copy (new key, three
  languages) instead of borrowing the search-result empty text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 17:40:23 +08:00
GuoQing Zhang 611f142a84 fix(frontend): prevent repeated knowledge space error toast 2026-08-12 17:05:10 +08:00
GuoQing Zhang f127c26ef6 fix(frontend): normalize permission revoke scope 2026-08-12 16:51:37 +08:00
GuoQing Zhang 9e66f290e2 fix(frontend): use white settings page backgrounds 2026-08-12 16:42:54 +08:00
LineWalker 516130dd85 fix(file-viewers): 把 xlsx 图片绑定到它真正所属的 sheet
图片索引原本是 Record<cellAddress, imageId[]>,没有 sheet 维度,getCellImage
也只按单元格地址查、不看 activeSheet —— 任何 sheet 的图都会漏到所有 sheet 的
同名单元格。一个首个 sheet 是整页报表截图的文件因此表现为:第一个 sheet 空白,
图跑到第二个 sheet 的 B2。

图归属只能顺 OPC 关系链查出来(workbook.xml 的 r:id → workbook.xml.rels →
worksheets/sheetN.xml → sheetN.xml.rels 的 drawing → drawings/drawingM.xml),
sheet 顺序和文件编号没有关系,删过 sheet 就会留下空号,不能按序号配对。

同批修掉的其它缺陷:
- 只有单元格没有 drawing 的 sheet 被判成「无数据」,纯图片 sheet 永远空白
- cleanData 删空行空列后坐标被压缩,锚点却按原始网格寻址,表中间有空行就错位
- 只认 twoCellAnchor,oneCellAnchor(WPS/报表导出常用)与 absoluteAnchor 的图不显示
- 按 xdr: 字面前缀匹配标签,换个生成器的命名空间前缀就整体失效
- DISPIMG 找不到对应图时返回 images[0],拿第一张图冒充
- cleanData 取值用 falsy 判断,把数字 0 吞成空串

锚在数据区之外的图不再静默消失,与纯图片 sheet 一起按 EMU 换算出的原始尺寸
展示。原文件 643 行已超单文件 600 行上限,顺势拆成 types / sheetUtils /
xlsxImages / ImageGallery 四个模块。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 16:26:06 +08:00
GuoQing Zhang d07580a992 fix(frontend): align channel settings layout styles 2026-08-12 11:03:56 +08:00
GuoQing Zhang 9c8cbdad67 fix(frontend): stop repeated permission user requests 2026-08-11 17:32:32 +08:00
GuoQing Zhang 32aac4b2d6 feat(frontend): upgrade bisheng icons to 0.2.30 2026-08-11 17:32:17 +08:00
GuoQing Zhang 365f194cf9 fix(permission): align unified creation pages with figma 2026-08-11 16:26:49 +08:00
Kinyoo 8b0a0d2ff3 style(chat): rework the conversation rename row
Renaming a conversation looked like a raw text field dropped into the
sidebar — a white boxed input, two lucide glyphs, and a row that lost
its shape as the options slot emptied.

- The row itself becomes the active surface: transparent input, and a
  1px #ECECEC hairline + soft drop shadow drawn as shadows (so the box
  size doesn't shift), matching the chat input's chrome.
- Confirm is the only action now: cancel is Esc / blur, and an empty or
  whitespace-only name silently restores the previous title, so the X
  button had nothing left to do. Its glyph moves to bisheng-icons.
- Opening rename selects the whole title, so typing replaces it.
- The options slot unmounts while renaming instead of rendering empty —
  an empty flex item still cost the row an 8px gap.
- Nav widens the scrollport by 1px on the left so the rename ring isn't
  clipped against the rows' left edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:16:00 +08:00
GuoQing Zhang 9a0e896777 feat(permission): add unified resource permission entry 2026-08-07 20:34:52 +08:00
dolphin 2002687193 feat(client): rebuild the crash screen around a screenshot
The fastest support round trip for a crash is a screenshot, so the screen
is built for one: a bordered card carrying the identifiers, and a QR code
beside it holding the same ones plus the error's signature. An engineer
reads the incident straight out of the image instead of asking the user
to find and describe anything, and the full stack and environment are a
click away in the downloaded log.

Three identifiers do the work. The trace id names this occurrence and
ties the screenshot, the QR and the log file together. The error code
names the *fault* — derived from the message and the frame it was thrown
in, so two users hitting the same bug quote the same code and the reports
pile up instead of scattering; its alphabet drops I, L, O and U, the four
that come back wrong when a code is read down a phone line. The build
stamp comes from a define rather than /api/v1/env, whose version field is
hardcoded and names no build at all.

The payloads live in @bisheng/ui because they have to stay identical
wherever the screen is used — an engineer reading a QR should not have to
know which app produced it. Copy and artwork are passed in, so the
package still holds no i18n keys and no illustrations.

The QR is trimmed to stay scannable at the size it is printed: it points
and signs, it does not carry everything. Two things quietly ate the
budget and had to go — stack frames carried their full origin, and routes
carry a 32-character conversation id — and both were crowding out the
file and line, which is the one part worth scanning for.

Also fixes a fault in the screen it replaces: it read the signed-in user
through a hook that throws when its provider is missing, and the provider
IS missing here, because an errorElement renders in place of its route's
element rather than inside it. The crash screen crashed, and the user got
a white page and nothing to report. Everything it reads now survives the
provider being absent.

Two deliberate departures from the design: no backdrop blur on the
buttons (banned repo-wide for what it costs on 信创 machines), and brand
colours through theme tokens rather than the literal hex, so the screen
follows the blue/green switch. The diagnostics card also gains the route
and the signed-in user — a frontend crash is hard to place without them.

The admin app keeps its existing screen for now: consuming the shared one
there means adopting the design-token layer, which would restyle 103
blue-* usages across 29 files. That is worth doing, as its own change.
2026-08-06 22:48:46 +08:00
LineWalker a53e65e2bd fix(knowledge): hard-split separator-proof fragments instead of failing the file
ETL4LM returns markdown tables whose rows can run past 10000 chars with no
\n / 。 / . inside them (one observed row: 7820 chars across 601 cells, the
result of a merged cell being expanded into every sub-cell). Such a fragment
survived ElemCharacterTextSplitter untouched — _split_text appends it verbatim
once new_separators is empty — and then tripped SplitterTransformer's
max_chunk_limit, failing the whole file with 10912. PaddleOCR and MinerU never
hit this because both rewrite tables to per-row markdown before splitting; ETL
is the only PDF loader without that step.

Add hard_split_limit to ElemCharacterTextSplitter, injected by
SplitterTransformer from its max chunk cap. _force_split kicks in only past
that limit, so documents that split fine today keep byte-identical chunk
boundaries and only files that would have been rejected change. This mirrors
what HierarchicalSplitterTransformer._split_long_text already does for the
hierarchical mode.

Also guard the separator_rule lookup against separators containing "".

Backport of 1ae4ca919. This branch predates KNOWLEDGE_MAX_CHUNK_CHARS, so the
cap stays the local 10000 literal and the test defines it inline; ruff
reformatted splitter.py while applying the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 22:19:41 +08:00
LineWalker 1f5c13442e fix(chat): load the stored conversation name instead of falling back to "New Chat"
The history endpoints return messages only, so useAiChat never learned an
existing conversation's name: `title` stayed empty and HeaderTitle fell back to
"New Chat" while the sidebar list showed the real name. Affects any conversation
opened by URL — a /c/<id> deep link as much as a share link (verified against a
session stored as "摩根大通" that rendered as "新对话" on both routes).

Fetch the name once via /api/v1/chat/info, carrying share-token so a share
recipient gets it too. Guarded so it cannot clobber a freshly generated title:
skipped while streaming (a brand-new conversation's row still reads "New Chat"
until gen_title lands) and never overwrites a title already held.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:43:23 +08:00
LineWalker fbfdc38dc8 fix(share): require login for share links, then return to the shared page
Opening /workspace/share/<token> while signed out rendered an empty ChatView
(header stuck on "New Chat") on top of a burst of 401s. Not a regression: every
endpoint behind a share has been LoginUserDep-gated since it was introduced, so
the share_link check inside those handlers was unreachable without a session —
the middleware anonymous allowlist only ever covered /api/v1/share-link itself.
Share links are viewable by any logged-in user (owner or not); anonymous viewing
was never supported. So gate the route explicitly instead of failing blank.

client:
- RequireLogin guards share/:token/:vid? — the route hangs directly off
  AuthLayout, so nothing used to stop it mounting content while unauthenticated.
  Three states: startup spinner / handoff card + redirect / blocked. A
  sessionStorage one-shot marker stops a redirect loop when the round trip comes
  back still anonymous.
  Authentication is read as `isAuthenticated || user`: isAuthenticated is state
  written from an effect and trails the Recoil user by one render. Root.tsx
  survives that frame by rendering null; a redirect cannot — it bounced
  signed-in users to the login page.
- loginRedirect centralises "go to login and come back" for the guard and the
  401 interceptor, which had drifted (the interceptor stored only
  location.pathname, losing query and hash). Stores an absolute same-origin URL
  so the platform app's return hop resolves against the right origin, is
  idempotent across a burst of parallel 401s, and takes force for a
  user-initiated retry.
- LoginHandoff is deliberately not an error screen: the link is fine, the viewer
  just has not identified themselves and will be returned here. Hence no lock
  illustration and no danger red; the one expressive element is a trace whose
  dot runs the rule and comes back, encoding the round trip.

platform:
- consumeLoginReturnTo validates (same-origin + 10 min freshness) and consumes
  the return target in one place. Also consumed on the authenticated landing in
  App.tsx, which is the only path an SSO/gateway callback takes — the local
  login form was the sole consumer before, so SSO users landed on the home page.

backend:
- bypass_tenant_filter_if widens share-token-authorized reads past tenant
  scoping. message / session / linsight_session_version / linsight_execute_task
  are all tenant-aware, so a recipient in another child tenant read nothing, the
  endpoint skipped its authorization branch and answered 200 [] — a blank page
  with no error. Only widens the read, only with a token; the owner /
  share_link check still runs afterwards.
- header_share_token_parser swallows NotFoundError to honour its
  Optional[ShareLink] contract. It started raising, which made ANY request
  carrying a stale share-token header 404 — including the session owner's own.
- file_download now accepts a whole-conversation (workbench_chat) share too, not
  just meta_data.versionId, so a task-mode share recipient can download the
  files the task produced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:43:17 +08:00
GuoQing Zhang 2a820648b9 fix(channel): log information subscription failures 2026-08-06 16:06:10 +08:00
GuoQing Zhang 008994987f fix(platform): save role menus from UI selection 2026-08-05 17:27:25 +08:00
Kinyoo 178c72c5fa fix(knowledge): center sidebar empty states on the sidebar, not the tree
The empty-state rows sat inside the shared w-max wrapper, so "暂无数据"
centered on the widest folder name rather than on the visible sidebar —
off to the right, and drifting further as the tree scrolled sideways.

They now size to the scroll container's inline width (100cqi, the same
trick SectionHeader already uses) and pin with sticky left-0, so the
text stays centered on the sidebar while the tree scrolls under it.
-mx-3 cancels the list row's own padding so the centering is exact;
mobilePageMode rows already carry their own width and opt out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:46:31 +08:00
Kinyoo 9c3155312f style(knowledge): regular weight for the in-folder header title
Same tweak as cofco's 424fcac79 applied to the main line: the folder
name in the space header drops font-medium for font-normal, so it reads
as a location label rather than competing with the page's real actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:34:34 +08:00
dolphin 3d0838e2ee perf(frontend): drop frosted glass everywhere and write the rule down
A customer on 信保安全浏览器 (Chromium 108, no GPU acceleration) had daily chat
stall on scroll and even on mouse movement. Forcing `backdrop-filter: none` from
the console fixed it outright, which pinned the cause: every such element gets
its own compositing layer and re-snapshots + re-blurs its backdrop each frame,
and that machine rasterizes it on the CPU.

The cost is per element, not per radius — a 4px blur on a 24px button costs the
same order as a full-screen one. What made it fatal was multiplication: the
per-message action buttons carry one each, so a long conversation ran dozens of
blur layers, each with a `hover:` transition that repaints them on mouse-over.

Removes all 40 occurrences (client 33, platform 7). Most were invisible anyway —
their backgrounds sit at 70-100% opacity, where the blur contributes nothing.
Two spellings were missed by a first pass that grepped only `backdrop-blur`:
the arbitrary-property form `[backdrop-filter:blur(8px)]` in the notification
dialog, and one inside an `@apply` in platform's applies.css.

The rule is recorded where each audience will meet it: the design spec gets the
reasoning plus the alternatives to reach for (translucent fill, noise texture,
pre-blurred asset for static backdrops), root AGENTS.md gets the hard constraint,
and the 总纲 hard-rules list gets a one-line pointer. Not duplicated into
packages/ui/AGENTS.md — the rule spans both apps and the library, so it belongs
at the root, and the library had no occurrences to begin with.

Both apps typecheck clean.
2026-08-05 14:24:54 +08:00
dolphin 2b8a4f9baa fix(chat): let Enter commit an IME candidate before it sends
Typing pinyin and pressing Enter sent the raw pinyin as the message. The chat
input treated every non-shift Enter as "send" and called preventDefault on it,
so the keystroke that should have committed the IME candidate never reached the
box.

Enter now sends only when no composition is in flight, and the event is left
untouched while one is — so the first Enter commits the candidate and the second
sends, which is what every other IME-aware input in the app already does.

Composition is detected the same way useTextarea does it: a compositionstart /
compositionend ref plus `isComposing`, `key === 'Process'` and the legacy 229
keyCode, because no single signal is reliable across Safari and the various IMEs.
2026-08-04 22:33:35 +08:00
dolphin b87cf76844 fix(chat): stop a stray navigation from dropping the new-task mode
"新建任务" intermittently opened a daily chat, and clicking it again worked.
The button sets the task-mode atom and navigates to /c/new carrying
`state.taskMode`. But `newConversation` runs an async chain that fires its own
state-less `navigate('/c/new')` a tick later, and ChatView read that second
landing as "no nav state, therefore daily" — overwriting the mode the button had
just set. Whether that stray navigate happens depends on the current
conversation state, which is why it only showed up after clicking around.

On /c/new both sidebar entries already set the atom explicitly ("新建任务" true,
"新建对话" false), so the effect only needs to honour a navigation that actually
declares a mode; an absent state now leaves the toggle alone. The switch-
conversation paths — reset on entering an existing conversation, restore once
history proves it is a task conversation — are untouched.

Also types the nav state instead of `any`, which brings the file's
no-explicit-any count back under its suppression (149 -> 120 reported).
2026-08-04 22:27:47 +08:00
dolphin 956073fd2e fix(chat): align the message column with the input box
The input sat a few pixels right of the messages above it. `scrollbar-on-scroll`
reserves the scrollbar gutter with `scrollbar-gutter: stable`, which puts the
whole 8px on the end edge, so the scroller's content box is asymmetric and the
centered message column lands half a scrollbar to the left. The input is
centered in the full width, outside that scroller, so the two never matched.

Reserving the gutter on both edges makes the box symmetric again and the two
columns share a centre. Applied inline rather than as a utility class:
`.scrollbar-on-scroll` is plain CSS declared after `@tailwind utilities`, so at
equal specificity it wins on source order and the class form had no effect.
2026-08-04 21:31:04 +08:00
Kinyoo 620361af41 fix(knowledge): enforce 10-layer folder depth cap in add_folder
add_folder allowed creating an 11th layer (level 10): it checked
level > 10 while the move path and folder-upload pre-check both cap at
level 9 (UI layer 1 = level 0). Converge all three checks on a shared
module constant MAX_FOLDER_LEVEL = 9. Legacy over-deep folders stay
accessible and movable; they just can't gain children.

Client hardening: createFolderApi keeps status_code on thrown errors so
the create-folder flow can surface the depth-limit message (18011) when
the async breadcrumb pre-check races a navigation.

Tests: level-9 parent rejects a child with SpaceFolderDepthError
(18011); level-8 parent still creates a level-9 child.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 18:15:37 +08:00
dolphin 8ad6e5aaac fix(i18n): move the 12047 wording into the source and resync the artifacts
The platform artifact carried "文件解析失败,并发过大,请稍后重试" while the
source in packages/locales said "文件解析失败,请稍后重试". Generated locale files
are checked against their source, so the drift failed `pnpm locales:check`.

Kept the wording and moved it to where it belongs — the source — then rebuilt,
so all three copies agree. en/ja keep their existing generic phrasing.
2026-08-03 22:13:20 +08:00
dolphin 7b961056b9 fix(subscription): stop a stacked confirm's mask from collapsing the source panel
Raising the "link unrecognized" confirm over the add-source panel appeared to
let clicks pass through the mask to the panel behind it. Instrumenting the shared
confirm showed the opposite: the mask covers the whole viewport, sits on top, and
does receive the click. The panel was collapsing itself.

Its outside-click guard asks `target.closest('[role=dialog], [role=alertdialog]')`
to tell "clicked another dialog" from "clicked outside". A modal's mask is a
SIBLING of its dialog node inside the same portal layer, never a descendant, so
that lookup returned null and the mask read as an outside click — collapsing the
panel (and applying the selection) underneath the confirm.

Fall back to looking for a dialog in the target's own portal layer when `closest`
finds none. The sheet's own mask still counts as outside, so dismissing the drawer
by its backdrop keeps working.
2026-08-03 22:10:40 +08:00
dolphin 3c7987cf45 fix(chat): block sending an attachment whose upload never landed
Sending several images occasionally left one rendering as "图片已失效 无法查看".
It was not an expired link: on 105 the offending attachment was the only one of
six persisted without a storage path, so the backend had no object name to sign
and the URL endpoint 404'd. The model never saw that file either — the answer
silently covered five of the six images.

The client already held everything needed to catch this: its own record for that
file carried no filepath. Check it at sendMessage, the single choke point both
daily chat and task mode go through, and abort the send naming the file rather
than dropping it quietly — a partial send is what made the failure invisible.
Files still uploading have no path either, so the same guard covers them.

Why the upload result went missing in the first place is still open; Redis TTL
and eviction are ruled out (24h TTL, noeviction, zero evictions, 7 weeks uptime),
so the metadata was likely never written. To be confirmed on the next repro.
2026-08-03 20:48:38 +08:00
dolphin b3b90834ea fix(chat): render a throttled file-parse failure as the red card
The busy notice offered a Retry button, but the turn is already lost by then —
the attachment never made it into the prompt, so retrying just re-runs against
the same throttled OCR service. Route `file_parse_busy` to the red failure card
like every other terminal outcome; it keeps its own "service is busy" wording
and the "view details" disclosure, and the user re-sends from the input box.

Model-side throttling (12046) is untouched and still gets the calm notice.
2026-08-03 20:48:19 +08:00
dolphin ce98e54c30 fix(chat): surface attachment-parse failures as a classified error card
Sending images in daily mode could fail with an opaque `status_code: 500`
"Server error". The real cause — the OCR service returning 429 "请求频率过高"
— rode along in `data.exception`, but the frontend resolved `api_errors.500`
to "服务器错误" and dropped it, so the user had nothing to act on.

Attachment parsing now raises ChatFileParseError (12047) instead of falling
through to the generic handler, carrying a stable `error_type` and the raw
upstream text plus the offending filename. Every daily-mode failure now ships
the same classification, not just parse errors. The classifier also learns the
PaddleOCR/Baidu throttle wording, which previously matched no rate-limit
signature at all.

On the client, task mode's failure card moves out of the Linsight tree into a
shared ChatErrorCard and is reused by the chat bubble, so both surfaces render
the same title/explanation/hint plus a "view details" disclosure. Transient
failures (a throttled OCR service) get the calm busy notice with Retry;
terminal ones get the red card. When an envelope carries no classification —
an older backend, or a domain error that doesn't classify itself — the status
code still decides busy-vs-failed, so nothing regresses.

Note this improves the message only: the parse path still fires one OCR call
per attachment with no concurrency cap or retry, and one failure still aborts
the whole turn.
2026-08-03 19:44:21 +08:00
GuoQing Zhang 073b420509 fix(channel): remove explicit nulls-last ordering 2026-08-03 15:29:07 +08:00
GuoQing Zhang 05278058d8 fix: tolerate knowledge args and DM8 channel sorting 2026-08-03 15:22:28 +08:00
dolphin b608efd6b8 fix(chat): render daily-mode thinking and tool calls in true event order
The stream, the DB and the grouped event array all carry a truthful
interleaving — the backend closes the open thinking segment before every tool
call precisely so each ReAct round can stand on its own. The final render then
threw that away: two filter() calls merged every thinking fragment into one
block at the top and dropped all tool rows below it, so reasoning that happened
after a search read as if it happened before.

Replace the filters with an in-order walk that folds only ADJACENT thinking
fragments (task mode's DeepStepGroup algorithm), leaving each tool call where
it occurred; the rail connector now follows "is there a next row" regardless of
kind. Adjacent rounds join with a paragraph break — they are complete passages,
not per-frame chunks. A just-opened live segment with no text yet is skipped so
the connector never points at a row that renders nothing. In-place completion
updates and collapse/reopen were already order-safe; backend untouched.
2026-07-31 18:53:13 +08:00
Kinyoo 2901c03087 style(knowledge): solid blue-50 for the selected file card and row
The 7% brand-alpha tint let the white below bleed through, reading a
touch dimmer than the design's selection fill. blue-50 is the same hue
as an opaque swatch — still a brand token, so it keeps following the
blue⇄green theme. Same change as cofco's fbcd7adc8, applied to the
main line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:25:03 +08:00
dolphin 9d321aa495 fix(chat): keep the newest knowledge selection leftmost after a reload
The attachment row orders chips by when each one first appeared, newest
leftmost. Knowledge selections are stored newest-first (the picker prepends)
while the file arrays are oldest-first, so when a whole set first appeared in
one frame — restoring a conversation from localStorage — the newest space was
handed the lowest sequence and the row came back reversed. Picking spaces one
at a time had always been right, which is why the two disagreed.

Feeding the knowledge array in oldest-first makes the sequence mean the same
thing for every source. File and skill chips are untouched.
2026-07-31 15:12:19 +08:00
Kinyoo 3b4be57eea chore(scripts): converge media files ingested with the ASR JSON envelope
One-off cleanup for rows the previous commit's bug left behind: media
knowledgefile rows marked SUCCESS whose stored transcript is empty or
the raw ASR response envelope.

The script scans media-extension SUCCESS rows, downloads each file's
MinIO transcript preview (preview/{id}.md) to judge the ingested text,
and converges hits to the same terminal state as every other
unrecognizable-audio case: Milvus/ES vectors deleted (MinIO source file
kept for re-parse/download), status=FAILED, remark=10956 error json —
the client then shows the 失败 badge with reason and stops the row
being clickable. Dry-run by default, --apply to write, scopable via
--knowledge-id / --file-id; idempotent since converged rows drop out of
the SUCCESS selection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:05:58 +08:00