@ibiz-template-plugin/ai-chat 0.0.119 → 0.0.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -166,6 +166,10 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
166
166
  * @memberof ChatContainer
167
167
  */
168
168
  private interactable;
169
+ /**
170
+ * resize 节流句柄
171
+ */
172
+ private resizeRafId;
169
173
  /**
170
174
  * 消息完成事件回调
171
175
  */
@@ -174,6 +178,19 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
174
178
  * 最小化状态变化事件回调
175
179
  */
176
180
  private onMinimizeChangeHandler;
181
+ /**
182
+ * 浏览器窗口尺寸变化处理
183
+ * - 触发节流后统一重算:容器最小宽高、左侧栏宽度、右侧面板宽度
184
+ */
185
+ private handleWindowResize;
186
+ /**
187
+ * 浏览器窗口尺寸变化后的重算逻辑
188
+ * 1. 校正窗口自身尺寸,不低于最小宽高,且不超出可视区域
189
+ * 2. 收敛左侧栏宽度到 [左最小宽, 容器宽 - 中最小宽 - 右占用]
190
+ * 3. 收敛右侧面板宽度到 [右最小宽, 容器宽 - 中最小宽 - 左占用]
191
+ * 4. 同步输入区高度,保证消息区最小高
192
+ */
193
+ private recalcOnResize;
177
194
  listener(): void;
178
195
  componentDidUpdate(): void;
179
196
  /**
@@ -382,6 +399,7 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
382
399
  getRightWidth(): string;
383
400
  /**
384
401
  * @description 获取宽度
402
+ * @private
385
403
  * @returns {*} {{
386
404
  * centreMinWidth: number;
387
405
  * leftWidth: number;
@@ -391,13 +409,7 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
391
409
  * }}
392
410
  * @memberof ChatContainer
393
411
  */
394
- getWidth(): {
395
- centreMinWidth: number;
396
- leftWidth: number;
397
- rightWidth: number;
398
- leftMinWidth: number;
399
- rightMinWidth: number;
400
- };
412
+ private getWidth;
401
413
  /**
402
414
  * 侧栏宽度拖动开始
403
415
  */
@@ -411,7 +423,7 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
411
423
  */
412
424
  handleSidebarResizeEnd: () => void;
413
425
  /**
414
- * 将侧栏宽度校正到合理区间 [200, 容器宽度/3, 容器宽度-右侧最小宽]
426
+ * 将侧栏宽度校正到合理区间 [最小宽, 容器宽度-右侧最小宽]
415
427
  */
416
428
  clampSidebarWidth(width: number): number;
417
429
  /**
@@ -460,6 +472,11 @@ export declare class ChatContainer extends Component<ChatContainerProps, ChatCon
460
472
  * 提交拖动边框后的输入区高度(持久化到状态与本地缓存)
461
473
  */
462
474
  commitInputHeight: () => void;
475
+ /**
476
+ * 将面板宽度矫正到合理区间 [最小宽, 容器宽度-左侧最小宽]
477
+ * @param width
478
+ */
479
+ clampPanelWidth(width: number): void;
463
480
  /**
464
481
  * 操作区面板宽度拖动开始
465
482
  */
@@ -22,6 +22,13 @@ interface FileNewPanelProps {
22
22
  ok: boolean;
23
23
  data: LazyTreeNode;
24
24
  }) => void;
25
+ /**
26
+ * @description 后缀名
27
+ * @default '.txt'
28
+ * @type {string}
29
+ * @memberof FileNewPanelProps
30
+ */
31
+ suffixName?: string;
25
32
  }
26
33
  export declare const FileNewPanel: (props: FileNewPanelProps) => import("preact").JSX.Element;
27
34
  export {};
@@ -8,7 +8,7 @@ interface BreadcrumbProps {
8
8
  label: string;
9
9
  }[];
10
10
  /**
11
- * 点击某一层跳转,-1 表示回到根
11
+ * 点击某一层跳转
12
12
  */
13
13
  onJump: (index: number) => void;
14
14
  }
@@ -5,7 +5,7 @@ export { Slider } from './slider/slider';
5
5
  export { Switch } from './switch/switch';
6
6
  export { Actions, type IAction } from './actions/actions';
7
7
  export { Breadcrumb } from './breadcrumb/breadcrumb';
8
- export { LazyListTree, type LazyListTreeRef, type LazyListTreeMode, } from './lazy-list-tree/lazy-list-tree';
8
+ export { LazyLevelList, type LazyLevelListRef, type LazyLevelListMode, } from './lazy-level-list/lazy-level-list';
9
9
  export { ContextMenu, type IContextMenuAction, } from './context-menu/context-menu';
10
10
  export { Drawer } from './drawer/drawer';
11
11
  export { LazyTree, type LazyTreeRef, type LazyTreeNode, type INodeEditInfo, } from './lazy-tree/lazy-tree';
@@ -1,8 +1,8 @@
1
1
  import { INodeEditInfo, LazyTreeNode } from '../lazy-tree/lazy-tree';
2
2
  import { IAction } from '../actions/actions';
3
3
 
4
- export type LazyListTreeMode = 'list' | 'card';
5
- interface LazyListTreeProps {
4
+ export type LazyLevelListMode = 'list' | 'card';
5
+ interface LazyLevelListProps {
6
6
  /**
7
7
  * 根节点数据
8
8
  */
@@ -83,17 +83,17 @@ interface LazyListTreeProps {
83
83
  * 展示模式
84
84
  * @default 'list'
85
85
  */
86
- mode?: LazyListTreeMode;
86
+ mode?: LazyLevelListMode;
87
87
  }
88
- export interface LazyListTreeRef {
88
+ export interface LazyLevelListRef {
89
89
  /**
90
90
  * 重载
91
91
  * @param
92
92
  * @returns
93
93
  */
94
- overloadNode: () => Promise<void>;
94
+ overloadNode: (node: LazyTreeNode) => Promise<void>;
95
95
  }
96
- export declare const LazyListTree: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<LazyListTreeProps> & {
97
- ref?: import('preact').Ref<LazyListTreeRef> | undefined;
96
+ export declare const LazyLevelList: import('preact').FunctionalComponent<import('preact/compat').PropsWithoutRef<LazyLevelListProps> & {
97
+ ref?: import('preact').Ref<LazyLevelListRef> | undefined;
98
98
  }>;
99
99
  export {};
@@ -54,6 +54,10 @@ export interface LazyTreeNode {
54
54
  * 根节点
55
55
  */
56
56
  isRoot?: boolean;
57
+ /**
58
+ * 是否显示
59
+ */
60
+ visible?: boolean;
57
61
  }
58
62
  export interface INodeEditInfo {
59
63
  /**
@@ -112,10 +112,42 @@ export declare class FlatChatContainer extends Component<FlatChatContainerProps>
112
112
  * 操作区面板宽度拖动起始值(px)
113
113
  */
114
114
  private panelDragStartWidth;
115
+ /**
116
+ * resize 节流句柄
117
+ */
118
+ private resizeRafId;
115
119
  state: {
116
120
  mountedSlots: Record<string, boolean>;
117
121
  inputHeight: number | null;
118
122
  };
123
+ /**
124
+ * @description 获取宽度
125
+ * @private
126
+ * @returns {*} {{
127
+ * centreMinWidth: number;
128
+ * leftWidth: number;
129
+ * rightWidth: number;
130
+ * leftMinWidth: number;
131
+ * rightMinWidth: number;
132
+ * }}
133
+ * @memberof FlatChatContainer
134
+ */
135
+ private getWidth;
136
+ /**
137
+ * 浏览器窗口尺寸变化处理
138
+ * - 触发节流后统一重算:容器最小宽高、左侧栏宽度、右侧面板宽度
139
+ */
140
+ private handleWindowResize;
141
+ /**
142
+ * @description 浏览器窗口尺寸变化后的重算逻辑
143
+ * 1. 收敛左侧栏宽度到 [左最小宽, 容器宽 - 中最小宽 - 右占用]
144
+ * 2. 收敛右侧面板宽度到 [右最小宽, 容器宽 - 中最小宽 - 左占用]
145
+ * 3. 同步输入区高度,保证消息区最小高
146
+ * @private
147
+ * @returns {*} {void}
148
+ * @memberof FlatChatContainer
149
+ */
150
+ private recalcOnResize;
119
151
  /**
120
152
  * @description 添加扩展插槽
121
153
  * @private
@@ -131,11 +163,24 @@ export declare class FlatChatContainer extends Component<FlatChatContainerProps>
131
163
  * @memberof FlatChatContainer
132
164
  */
133
165
  componentDidMount(): void;
166
+ /**
167
+ * @description 销毁
168
+ * @memberof FlatChatContainer
169
+ */
170
+ componentWillUnmount(): void;
134
171
  /**
135
172
  * @description 更新
136
173
  * @memberof FlatChatContainer
137
174
  */
138
175
  componentDidUpdate(prevProps: FlatChatContainerProps): void;
176
+ /**
177
+ * 拖动容器边框时实时校正输入区高度,保证消息区不小于最小高度
178
+ */
179
+ syncInputHeight: () => void;
180
+ /**
181
+ * 提交拖动边框后的输入区高度(持久化到状态与本地缓存)
182
+ */
183
+ commitInputHeight: () => void;
139
184
  /**
140
185
  * @description 同步操作区面板状态(展开收起动画由 CSS 过渡完成)
141
186
  * @memberof FlatChatContainer
@@ -166,6 +211,10 @@ export declare class FlatChatContainer extends Component<FlatChatContainerProps>
166
211
  * 右侧聊天区宽度
167
212
  */
168
213
  getRightWidth(): string;
214
+ /**
215
+ * 将侧栏宽度校正到合理区间 [最小宽, 容器宽度-右侧最小宽]
216
+ */
217
+ clampSidebarWidth(width: number): number;
169
218
  /**
170
219
  * 侧栏宽度拖动开始
171
220
  */
@@ -178,6 +227,11 @@ export declare class FlatChatContainer extends Component<FlatChatContainerProps>
178
227
  * 侧栏宽度拖动结束
179
228
  */
180
229
  handleSidebarResizeEnd: () => void;
230
+ /**
231
+ * 将面板宽度矫正到合理区间 [最小宽, 容器宽度-左侧最小宽]
232
+ * @param width
233
+ */
234
+ clampPanelWidth(width: number): void;
181
235
  /**
182
236
  * 操作区面板宽度拖动开始
183
237
  */
@@ -6,7 +6,7 @@ interface ActionItem {
6
6
  caption: string;
7
7
  icon?: VNode;
8
8
  }
9
- export declare const Popup: ({ triggerMode, children, actions, content, position, isOpen: isOpenProp, onToggleOpen, popupClassName, onAction, }: {
9
+ export declare const Popup: ({ triggerMode, children, actions, content, disabled, position, isOpen: isOpenProp, onToggleOpen, popupClassName, onAction, }: {
10
10
  triggerMode: "click" | "hover";
11
11
  children: import('preact').ComponentChildren;
12
12
  actions?: ActionItem[];
@@ -14,6 +14,7 @@ export declare const Popup: ({ triggerMode, children, actions, content, position
14
14
  position?: Position;
15
15
  isOpen?: boolean;
16
16
  popupClassName?: string;
17
+ disabled?: boolean;
17
18
  onToggleOpen?: (isOpen: boolean) => void;
18
19
  onAction?: (actionId: string, event: MouseEvent) => void;
19
20
  }) => import("preact").JSX.Element;
@@ -1,14 +1,6 @@
1
1
  import { Signal } from '@preact/signals';
2
2
  import { TActionPanelComponent } from '../../interface';
3
3
 
4
- /**
5
- * 将操作区面板宽度校正到合理区间 [最小宽度, 容器宽度/3]
6
- *
7
- * @param {number} width 目标宽度
8
- * @param {number} containerWidth 容器整体宽度
9
- * @returns {*} {number}
10
- */
11
- export declare function clampPanelWidth(width: number, containerWidth: number): number;
12
4
  /**
13
5
  * 操作区面板控制器
14
6
  * - 面板为容器级单例,挂在 ChatBaseController 上,切话题不重建
@@ -1,4 +1,4 @@
1
- import { IFile, IFileItem, IMessageUtil, IFileItemParams, IFileUploadPrams, IFileSystemOptions, FileUploaderOptions } from '../../interface';
1
+ import { IFile, IFileItem, IMessageUtil, IConfirmUtil, IFileItemParams, IFileUploadPrams, IFileSystemOptions, FileUploaderOptions } from '../../interface';
2
2
  import { AiChatController } from '../ai-chat/ai-chat.controller';
3
3
 
4
4
  /**
@@ -38,6 +38,13 @@ export declare class FileSystemController {
38
38
  * @memberof FileSystemController
39
39
  */
40
40
  get message(): IMessageUtil | undefined;
41
+ /**
42
+ * @description 提示
43
+ * @readonly
44
+ * @type {(IConfirmUtil | undefined)}
45
+ * @memberof FileSystemController
46
+ */
47
+ get confirm(): IConfirmUtil | undefined;
41
48
  /**
42
49
  * @description 文件预览模式
43
50
  * @readonly
@@ -66,12 +73,12 @@ export declare class FileSystemController {
66
73
  */
67
74
  fetch(params: IFileItemParams): Promise<IFileItem | undefined>;
68
75
  /**
69
- * @description 文件上传
76
+ * @description 系统文件上传
70
77
  * @param {IFileUploadPrams} params
71
78
  * @returns {*} {(Promise<IFile | undefined>)}
72
79
  * @memberof FileSystemController
73
80
  */
74
- uploadFile(params: IFileUploadPrams): Promise<IFile | undefined>;
81
+ uploadSystemFile(params: IFileUploadPrams): Promise<IFile | undefined>;
75
82
  /**
76
83
  * @description 获取文件内容
77
84
  * @param {string} id
@@ -149,4 +156,20 @@ export declare class FileSystemController {
149
156
  * @memberof FileSystemController
150
157
  */
151
158
  delete(file_path: string): Promise<boolean>;
159
+ /**
160
+ * @description 选择文件
161
+ * @param {string} [accept='']
162
+ * @param {boolean} [multiple=false]
163
+ * @returns {*} {(Promise<FileList | undefined>)}
164
+ * @memberof FileSystemController
165
+ */
166
+ selectFile(accept?: string, multiple?: boolean): Promise<FileList | undefined>;
167
+ /**
168
+ * @description 上传文件到系统
169
+ * @param {string} file_path 文件路径
170
+ * @param {File} file 文件
171
+ * @returns {*} {Promise<void>}
172
+ * @memberof FileSystemController
173
+ */
174
+ uploadFileToSystem(file_path: string, file: File): Promise<boolean>;
152
175
  }
@@ -23,6 +23,19 @@ interface IFileSystemUploadParams {
23
23
  srfaiagent?: string;
24
24
  srfscope?: string;
25
25
  }
26
+ /**
27
+ * @description 下载文件参数
28
+ * @interface IFileSystemDownLoadParams
29
+ */
30
+ interface IFileSystemDownLoadParams {
31
+ sessionid: string;
32
+ folder: string;
33
+ id: string;
34
+ file_path: string;
35
+ srfaiagent?: string;
36
+ srfscope?: string;
37
+ timeout?: number;
38
+ }
26
39
  /**
27
40
  * @description 新建目录参数
28
41
  * @interface IFileSystemCreateParams
@@ -143,6 +156,11 @@ export interface IFileSystemOptions {
143
156
  name: string;
144
157
  folder: string;
145
158
  } | undefined>;
159
+ /**
160
+ * @description 下载文件
161
+ * @memberof IFileSystemOptions
162
+ */
163
+ downloadFile: (ctx: object, param: object, other: IFileSystemDownLoadParams) => Promise<IFileSystemResult>;
146
164
  /**
147
165
  * @description 获取文件内容
148
166
  * @param {object} ctx
@@ -180,4 +180,5 @@ export declare const EN: {
180
180
  uploadFile: string;
181
181
  fileContent: string;
182
182
  clear: string;
183
+ fileExists: string;
183
184
  };
@@ -180,4 +180,5 @@ export declare const ZH_CN: {
180
180
  uploadFile: string;
181
181
  fileContent: string;
182
182
  clear: string;
183
+ fileExists: string;
183
184
  };
@@ -0,0 +1,90 @@
1
+ # 消息列表内存优化:视口外占位回收(msglist-memory-optimization)
2
+
3
+ > 记录本次「聊天消息列表内存暴增/崩溃」优化的背景、方案演进与现行实现机制,供后续新增任务与 bug 修复参考。
4
+ > 涉及文件:`src/components/chat-message-item/chat-message-item.tsx`(核心)、`markdown-message.tsx` / `user-message.tsx` / `error-message.tsx`(配套 Cherry 清理)。
5
+
6
+ ## 1. 背景:为什么内存会崩溃
7
+
8
+ - 消息正文由 Cherry Markdown 渲染,每个实例内含 CodeMirror、解析器、事件监听器,**单实例内存开销大**。
9
+ - 原方案「渐进真实绘制 + 绘制过永久留 DOM」(`chat-messages.tsx`):初始绘制最新 20 条,向上滚动逐批扩展,**绘制过的消息永久真实渲染**。
10
+ - 结果:长会话从头滚到尾后,所有消息的 Cherry 实例常驻内存,内存随滚动单调增长,最终崩溃。
11
+
12
+ ## 2. 方案演进(均已验证并放弃,勿恢复)
13
+
14
+ | 方案 | 放弃原因 |
15
+ | --- | --- |
16
+ | 自研虚拟列表(绝对定位 + spacer) | 滚动条抖动严重 |
17
+ | react-virtuoso | 滚动条乱滚/抖动 |
18
+ | 列表级 IO 驱动 + 估算参与(rAF 队列/距离排序/renderStartIndex 收窄) | 复杂度高,滚动条抖动未解决,被有意推翻 |
19
+ | 估算高度占位 | 高度失准导致滚动条跳动 |
20
+ | 图片 lazy 加载 | 加载后撑高导致占位失准 |
21
+ | CSS `content-visibility: auto` | 只跳过渲染不卸载 DOM,Cherry/CodeMirror 内存无法释放,不解决根本问题 |
22
+
23
+ **现行方案**:渐进渲染(列表级,管「从未绘制的更早历史」)+ item 级实测高度占位(管「绘制过但离开视口的消息」),两层互补,`chat-messages.tsx` / `use-message-renderer.ts` 不做改动。
24
+
25
+ ## 3. 现行机制:item 级「实测高度占位 + 双缓冲过渡」
26
+
27
+ 全部逻辑在 `ChatMessageItem` 内自管理(`chat-message-item.tsx`)。
28
+
29
+ ### 3.1 状态
30
+
31
+ | 状态 | 实现 | 说明 |
32
+ | --- | --- | --- |
33
+ | `height` | `useSignal(0)` | 高度缓存:RO 持续写入组件实际外高(小数),随图片加载/Cherry 异步渲染同步更新 |
34
+ | `phase` | `useSignal<Phase>('real')` | `real`(真实绘制)/ `placeholder`(占位)/ `transition`(双缓冲过渡) |
35
+ | `isWaitingRef` | `useRef` | 流式标记,IO 回调闭包经它读取,避免持有过期 message |
36
+
37
+ ### 3.2 阶段流转
38
+
39
+ ```
40
+ ┌──────────────────────────────────────────────┐
41
+ │ IO isIntersecting=true │
42
+ ┌──────┐ │ (占位继续锚定,真实组件隐藏重挂) ┌────┐ │
43
+ │ real │──────┼─────────────────────────────────▶ │占位 │◀───┼── IO isIntersecting=false
44
+ └──────┘ ▼ └────┘ │ (iswaiting/height=0 不占位)
45
+ transition ── RO 实测隐藏中的真实组件: ▲
46
+ │ ├ 高度≈缓存(≤0.5px) ──▶ 切 real │
47
+ │ └ 不一致且稳定 100ms ──▶ 接受新高度切 real(只动一次滚动条)
48
+ └── 滚走可随时中断回 placeholder
49
+ ```
50
+
51
+ - **real → placeholder**:滚出视口(150% 缓冲带外)→ 卸载真实组件(Cherry destroy 释放内存)→ 渲染 `height` 精确高度的占位 div。
52
+ - **placeholder → transition**:进入视口 → 占位**继续留在文档流锚定 scrollHeight**,真实组件以 `absolute + visibility: hidden` 隐藏重挂(不影响布局)。
53
+ - **transition → real**:RO 实测隐藏真实组件,高度与缓存一致(差 ≤ `HEIGHT_EPSILON`=0.5px)立即切换;不一致则无 RO 事件稳定 `SETTLE_DELAY`=100ms 后接受新高度(内容真实变化时只动一次滚动条)。
54
+
55
+ **为什么需要双缓冲**:直接占位→真实重挂载时,内容重建(Cherry 异步初始化、代码高亮重跑、图片重新解码)会产生高度瞬变,`scrollHeight` 反复变化 → 滚动条跳动。双缓冲下过渡全程 scrollHeight 零变化。
56
+
57
+ ### 3.3 关键实现细节(改代码前必读)
58
+
59
+ 1. **观察目标分工**:IO 观察稳定的 wrapper(`containerRef`,含 `id=messageid`,永不重挂);RO 观察真实组件容器(`realRef`,transition 期间是隐藏的 absolute 元素)。
60
+ 2. **测高用 `borderBoxSize[0].blockSize`**:占位 div 直接替换真实组件容器,高度必须与元素**外高**完全一致;`contentBox`/`clientHeight` 会漏掉 padding 导致漂移。
61
+ 3. **wrapper `minHeight + boxSizing: 'content-box'` 锁定**:挡住真实绘制阶段的「变矮」瞬变;显式 content-box 防宿主应用全局 border-box 重置(否则 min-height 会多算 wrapper 上下 6px padding)。用 min-height 而非 height:视口内正常长高(图片、流式)仍能跟随。
62
+ 4. **`position: relative` 仅在 transition 期间设置**:真实阶段 wrapper 无定位,工具栏弹层的定位基准不变(避免影响既有弹层行为)。
63
+ 5. **IO 配置**:`root=null`(视口)——intersectionRect 会被中间滚动容器裁剪,滚出容器即不相交,组件自包含、多聊天实例可用;`rootMargin: '150% 0px'` 提供缓冲带减少切换频率。
64
+ 6. **三类守卫**(缺一不可):
65
+ - `el.offsetParent === null`:容器隐藏(keep-alive 失活)忽略 IO 事件,激活后 IO 自动重估;
66
+ - `iswaiting === true`:流式消息高度持续增长,不占位;
67
+ - `height <= 0`:未实测过高度不占位(真实渲染至少一次才有占位资格)。
68
+ 7. **Cherry destroy 清理**(占位回收释放内存的前提,勿删):`markdown-message.tsx` / `user-message.tsx` / `error-message.tsx` 卸载时 `cherry.destroy()`,`deep-research-result.tsx:218` 已有。Cherry 均为单实例守卫(有实例则 `setMarkdown`),无重复创建泄漏。
69
+
70
+ ### 3.4 与 chat-messages(列表层)的关系
71
+
72
+ - 列表层的渐进渲染(初始 20 条 + 逐帧切片 + 触顶加载历史)、贴底判定、keep-alive 恢复**全部未动**。
73
+ - 消息首次被渐进渲染纳入 → item 挂载实测高度 → 滚出视口自动被 item 占位回收 → 再次进入视口经 transition 恢复。
74
+ - 话题切换:messages 清空重载,item 卸载重建,item 内状态(height/phase)自然重置。
75
+
76
+ ## 4. 已知边界与注意事项(新增任务/修 bug 指引)
77
+
78
+ 1. **transition 兜底路径的瞬变风险**:若重挂后内容迟迟不渲染(如 Cherry 初始化阻塞 >100ms 且期间无布局变化触发 RO),settle 定时器会接受「中间态小高度」,内容渲染完成后再一次长高 → 单次跳动。概率低;若实测出现,调大 `SETTLE_DELAY`(150~200ms)或在 settle 接受时要求 `boxHeight > 0`。
79
+ 2. **不要引入**:估算高度占位、图片 lazy、绝对定位虚拟列表、手动 scrollTop 补偿——均已被实测否决(见第 2 节)。
80
+ 3. **不要删除**:wrapper 的 minHeight 锁定、transition 双缓冲、三类守卫、Cherry destroy——滚动条稳定性与内存回收都依赖它们。
81
+ 4. **占位回收会卸载 ChatToolbar**(children 随真实组件卸载),视口外工具栏状态不保留;如需保留状态需提升到 controller 层。
82
+ 5. **tsc 已知既有错误**:`chat-message-item.tsx` 的 TS2769(`h(com, ...)` 联合类型)在优化前就存在(HEAD 同样报错),仓库多处历史类型错误导致 `pnpm build` 失败,与本次改动无关;本文件 eslint 通过。
83
+ 6. **`vite.config.ts` 的 `isDebug=true`** 为本地调试构建配置(sourcemap/不压缩/preserveModules),**不要随功能一起提交**。
84
+
85
+ ## 5. 验证清单(改动后回归)
86
+
87
+ 1. **内存**:长会话来回滚动一遍 → DevTools Performance Monitor 内存曲线有界;heap snapshot 无 Cherry/CodeMirror 残留实例。
88
+ 2. **滚动条稳定**:快速拖动滚动条,占位↔真实切换全程 thumb 无跳动/闪动。
89
+ 3. **内容填充**:快速拖动经过区域 0.5s 内填满,无长时间空白。
90
+ 4. **回归**:贴底跟随、发送消息强制贴底、流式输出不占位、回到底部按钮、keep-alive 失活/激活恢复、话题切换、向上滚动渐进扩展与历史加载。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibiz-template-plugin/ai-chat",
3
- "version": "0.0.119",
3
+ "version": "0.0.120",
4
4
  "description": "iBiz AI交互框插件",
5
5
  "type": "module",
6
6
  "main": "dist/index.es.js",