@roaming-ai/dsh-group-chat 0.2.2 → 0.3.0

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.
Files changed (59) hide show
  1. package/README.md +3 -2
  2. package/lib/client.js +984 -296
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +524 -52
  5. package/lib/types/client/components/Bubble.d.ts +6 -3
  6. package/lib/types/client/components/ChatPanel.d.ts +1 -0
  7. package/lib/types/client/components/ConstraintList.d.ts +11 -0
  8. package/lib/types/client/components/FailCard.d.ts +9 -0
  9. package/lib/types/client/components/Fold.d.ts +19 -0
  10. package/lib/types/client/components/HoverTip.d.ts +22 -0
  11. package/lib/types/client/components/MessageFlow.d.ts +1 -0
  12. package/lib/types/client/components/MsgActions.d.ts +14 -0
  13. package/lib/types/client/hooks/useComposer.d.ts +6 -1
  14. package/lib/types/client/lib/composer-draft.d.ts +17 -0
  15. package/lib/types/core/constraints.d.ts +61 -0
  16. package/lib/types/core/errors.d.ts +54 -0
  17. package/lib/types/core/types.d.ts +22 -0
  18. package/lib/types/host/api/actions.d.ts +1 -1
  19. package/lib/types/host/engine/conversation.d.ts +5 -3
  20. package/lib/types/host/engine/fold.d.ts +17 -0
  21. package/lib/types/host/engine/index.d.ts +1 -0
  22. package/lib/types/host/engine/retitle.d.ts +5 -5
  23. package/lib/types/host/service.d.ts +1 -1
  24. package/lib/types/host/state.d.ts +2 -0
  25. package/lib/types/index.d.ts +2 -0
  26. package/package.json +1 -1
  27. package/src/client/GroupChatPanel.tsx +18 -3
  28. package/src/client/components/AsidePanel.tsx +10 -8
  29. package/src/client/components/Bubble.tsx +45 -18
  30. package/src/client/components/ChatPanel.tsx +17 -12
  31. package/src/client/components/Composer.tsx +26 -20
  32. package/src/client/components/ConstraintList.tsx +69 -0
  33. package/src/client/components/FailCard.tsx +49 -0
  34. package/src/client/components/Fold.tsx +72 -0
  35. package/src/client/components/HoverTip.tsx +134 -0
  36. package/src/client/components/MessageFlow.tsx +77 -54
  37. package/src/client/components/MsgActions.tsx +85 -0
  38. package/src/client/components/NavPanel.tsx +6 -1
  39. package/src/client/components/ThinkRow.tsx +7 -3
  40. package/src/client/components/ToolRow.tsx +7 -3
  41. package/src/client/hooks/useComposer.ts +40 -3
  42. package/src/client/hooks/useGroupChatState.ts +5 -4
  43. package/src/client/lib/composer-draft.ts +47 -0
  44. package/src/client/lib/styles.ts +73 -12
  45. package/src/client/react-dom-shim.d.ts +2 -0
  46. package/src/core/constraints.ts +210 -0
  47. package/src/core/errors.ts +184 -0
  48. package/src/core/json.ts +1 -0
  49. package/src/core/types.ts +28 -3
  50. package/src/host/api/actions.ts +35 -1
  51. package/src/host/broadcast.ts +3 -3
  52. package/src/host/engine/conversation.ts +103 -38
  53. package/src/host/engine/fold.ts +114 -0
  54. package/src/host/engine/index.ts +1 -0
  55. package/src/host/engine/retitle.ts +16 -11
  56. package/src/host/persistence/persistence.ts +18 -1
  57. package/src/host/service.ts +1 -1
  58. package/src/host/state.ts +5 -4
  59. package/src/index.ts +2 -0
@@ -8,12 +8,15 @@
8
8
  * @module dsh-group-chat/client/Bubble
9
9
  */
10
10
  import { type ReactNode } from 'react';
11
- import { type ClientSnapshot, type SnapshotRole } from '../lib/model.ts';
11
+ import { type SnapshotRole } from '../lib/model.ts';
12
+ import type { SnapshotMessage } from '../lib/model.ts';
12
13
  export interface BubbleProps {
13
- m: ClientSnapshot['messages'][number];
14
+ m: SnapshotMessage;
14
15
  /** 父级解析好的发言角色(user/system 消息为 null)——避免 Bubble 依赖 snap identity。 */
15
16
  role: SnapshotRole | null;
17
+ busy?: boolean;
18
+ onRetry?: (messageId: string) => void;
16
19
  }
17
- declare function BubbleInner({ m, role }: BubbleProps): ReactNode;
20
+ declare function BubbleInner({ m, role, busy, onRetry }: BubbleProps): ReactNode;
18
21
  export declare const Bubble: import("react").MemoExoticComponent<typeof BubbleInner>;
19
22
  export {};
@@ -52,6 +52,7 @@ interface ChatPanelProps {
52
52
  action: (payload: Record<string, unknown>) => Promise<unknown>;
53
53
  mutate: (args: Record<string, unknown>) => Promise<unknown>;
54
54
  setMention: (val: AtToken | null) => void;
55
+ onRetrySpeak: (messageId: string) => void;
55
56
  }
56
57
  export declare function ChatPanel(props: ChatPanelProps): ReactNode;
57
58
  export {};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 会话流折点处:只读结论备忘卡(超过 4 条默认露 3 条)。
3
+ * @module dsh-group-chat/client/components
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ import type { SessionConstraint } from '../../core/types.ts';
7
+ interface ConstraintListProps {
8
+ items: SessionConstraint[];
9
+ }
10
+ export declare function ConstraintList(props: ConstraintListProps): ReactNode;
11
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 角色发言失败卡:人话标题 + 可展开原文。操作条由 Bubble 放在气泡外下方。
3
+ * @module dsh-group-chat/client/FailCard
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ export interface FailCardProps {
7
+ raw: string;
8
+ }
9
+ export declare function FailCard(props: FailCardProps): ReactNode;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * 会话内折叠:高度 0fr→1fr + 溢出滚动遮罩。思考 / 工具 / 失败原文 / 结论备忘共用。
3
+ * @module dsh-group-chat/client/components
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ interface FoldProps {
7
+ open: boolean;
8
+ children: ReactNode;
9
+ className?: string;
10
+ }
11
+ export declare function Fold(props: FoldProps): ReactNode;
12
+ interface ClipWellProps {
13
+ children: ReactNode;
14
+ maxHeight: number;
15
+ className?: string;
16
+ watch?: unknown;
17
+ }
18
+ export declare function ClipWell(props: ClipWellProps): ReactNode;
19
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * 对齐宿主 Tooltip 的 hover 气泡:portal 到 document.body,躲开
3
+ * `.dsgc-root` 的 container-type 把 position:fixed 按容器定位。
4
+ * @module dsh-group-chat/client/components
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ export type HoverTipSide = 'top' | 'right';
8
+ interface HoverTipProps {
9
+ label: string;
10
+ side?: HoverTipSide;
11
+ delayMs?: number;
12
+ maxWidth?: number;
13
+ className?: string;
14
+ children: ReactNode;
15
+ }
16
+ /**
17
+ * @param props.label 气泡正文(pre-line)
18
+ * @param props.side 默认 right(对齐侧栏会话行);composer 轮数用 top
19
+ * @param props.delayMs hover 延迟,默认 500;键盘 focus 立即出
20
+ */
21
+ export declare function HoverTip(props: HoverTipProps): ReactNode;
22
+ export {};
@@ -10,6 +10,7 @@ interface MessageFlowProps {
10
10
  busyNow: boolean;
11
11
  msgById: Record<string, ClientSnapshot['messages'][number]>;
12
12
  action: (payload: Record<string, unknown>) => Promise<unknown>;
13
+ onRetrySpeak: (messageId: string) => void;
13
14
  }
14
15
  export declare function MessageFlow(props: MessageFlowProps): ReactNode;
15
16
  export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 消息操作条:贴在气泡外下方。用户/角色消息悬停出「复制」;失败卡常驻「复制 / 重试」。
3
+ * @module dsh-group-chat/client/MsgActions
4
+ */
5
+ import { type ReactNode } from 'react';
6
+ export interface MsgActionsProps {
7
+ copyText: string;
8
+ onRetry?: () => void;
9
+ retryDisabled?: boolean;
10
+ retryTitle?: string;
11
+ /** 失败卡:不依赖悬停,始终可见。 */
12
+ always?: boolean;
13
+ }
14
+ export declare function MsgActions(props: MsgActionsProps): ReactNode;
@@ -8,13 +8,18 @@ import type { SnapshotRole } from '../lib/model.ts';
8
8
  export declare function useComposerEffects(inputRef: React.RefObject<HTMLDivElement>, scrollRef: React.RefObject<HTMLDivElement>, input: string, atBottom: boolean, snap: unknown): {
9
9
  onMsgsScroll: () => boolean | undefined;
10
10
  };
11
- export declare function useComposerInput(inputRef: React.RefObject<HTMLDivElement>, setInput: (val: string) => void, setMention: (val: AtToken | null) => void, setMentionIdx: (val: number) => void): {
11
+ export declare function useComposerInput(inputRef: React.RefObject<HTMLDivElement>, setInput: (val: string) => void, setMention: (val: AtToken | null) => void, setMentionIdx: (val: number) => void, sessionId?: string | null): {
12
12
  syncFromDOM: () => void;
13
13
  onInputCE: () => void;
14
14
  onPasteCE: (e: ReactClipboardEvent<HTMLDivElement>) => void;
15
15
  onDragOverCE: (e: ReactDragEvent<HTMLDivElement>) => void;
16
16
  onDropCE: (e: ReactDragEvent<HTMLDivElement>) => void;
17
17
  };
18
+ /**
19
+ * 按会话恢复草稿:切会话时先把当前 HTML 写入旧槽,再灌入新槽;
20
+ * 面板重挂载(主会话⇄群聊)时 editor 是新节点,从模块缓存灌回。
21
+ */
22
+ export declare function useComposerDraft(sessionId: string | null | undefined, inputRef: React.RefObject<HTMLDivElement>, setInput: (val: string) => void, setMention: (val: AtToken | null) => void): void;
18
23
  export declare function useMentionChip(inputRef: React.RefObject<HTMLDivElement>, setMention: (val: AtToken | null) => void, setMentionIdx: (val: number) => void, syncFromDOM: () => void): {
19
24
  insertChip: (role: SnapshotRole) => void;
20
25
  insertFileChip: (path: string, kind: "file" | "directory") => void;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 未发送草稿:按会话 id 分槽,模块级跨挂载存活。
3
+ * 主会话⇄群聊会整页卸载 composer,contentEditable 的 HTML 必须自行记住。
4
+ * @module dsh-group-chat/client/composer-draft
5
+ */
6
+ export interface ComposerDraft {
7
+ html: string;
8
+ text: string;
9
+ }
10
+ /** 读指定会话草稿;命中则提到 LRU 最近端。 */
11
+ export declare function readComposerDraft(sessionId: string | null | undefined): ComposerDraft;
12
+ /** 写入;空草稿删槽。超出上限淘汰最旧槽。 */
13
+ export declare function writeComposerDraft(sessionId: string | null | undefined, html: string, text: string): void;
14
+ /** 发送成功或明确丢弃时摘槽。 */
15
+ export declare function clearComposerDraft(sessionId: string | null | undefined): void;
16
+ /** 测试用:清空全部槽位。 */
17
+ export declare function resetComposerDrafts(): void;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * 会话约束备忘纯逻辑:滑动 40 条窗口、挤出集、压行、临时原文、解析与截断。
3
+ * @module dsh-group-chat/core/constraints
4
+ */
5
+ import { type MessageRecord, type SessionConstraint, type SessionRecord } from './types.ts';
6
+ /** 当场原文窗口(含系统行)。 */
7
+ export declare const WINDOW_SIZE = 40;
8
+ /** 折叠失败时临时原文条数上限。 */
9
+ export declare const TEMP_MAX_MESSAGES = 20;
10
+ /** 折叠失败时临时原文总长上限。 */
11
+ export declare const TEMP_MAX_CHARS = 16000;
12
+ /** 单轮折叠最多消耗的挤出条数(含系统行;超出留待下一轮)。 */
13
+ export declare const FOLD_MAX_MESSAGES = 40;
14
+ /** 单轮折叠输入总长上限(格式化后)。 */
15
+ export declare const FOLD_MAX_CHARS = 16000;
16
+ /** 备忘条数上限。 */
17
+ export declare const CONSTRAINT_MAX_ITEMS = 12;
18
+ /** 备忘总长上限(条目前缀+正文)。 */
19
+ export declare const CONSTRAINT_MAX_CHARS = 1200;
20
+ /** 单条备忘正文上限。 */
21
+ export declare const CONSTRAINT_ITEM_MAX_CHARS = 160;
22
+ export declare const KIND_LABEL: Record<SessionConstraint['kind'], string>;
23
+ /** 已折入水位(缺省 0)。 */
24
+ export declare function constraintsWatermark(sess: SessionRecord): number;
25
+ /** 重试:只取失败卡之前的时间线;untilId 不在列表则原样。 */
26
+ export declare function prefixIds(ids: string[], untilId?: string): string[];
27
+ /**
28
+ * 新挤出:seq > 水位 且不在最近 40 条。只扫窗口外前缀(旧→新)。
29
+ * untilId:重试时把窗口截到该消息之前,不带上后面已经发生的发言。
30
+ */
31
+ export declare function squeezedMessages(messages: Map<string, MessageRecord>, sess: SessionRecord, untilId?: string): MessageRecord[];
32
+ /** 说话人展示名(折叠输入 / transcript 共用)。 */
33
+ export declare function speakerLabel(speaker: string, roleName?: string): string;
34
+ /** 单条消息压成 transcript 行(正文 8k + 工具一行摘要)。 */
35
+ export declare function formatTranscriptLine(m: MessageRecord, name: string): string;
36
+ /**
37
+ * 单轮折叠消耗前缀:从最旧挤出起,最多 40 条 / 16k;系统行与失败卡计入消耗但不进模型。
38
+ * 水位只能推到 consumed 的 max seq,剩余留待下一轮。
39
+ */
40
+ export declare function takeFoldBatch(squeezed: MessageRecord[], nameOf: (m: MessageRecord) => string): {
41
+ consumed: MessageRecord[];
42
+ lines: string[];
43
+ hasUser: boolean;
44
+ allSystem: boolean;
45
+ };
46
+ /**
47
+ * 未折入的挤出原文(失败缓冲):只格式化最近 20 条,再按 16k 从最旧往下丢。
48
+ */
49
+ export declare function tempTranscript(squeezed: MessageRecord[], nameOf: (m: MessageRecord) => string): string;
50
+ /** 水位 = 本批挤出的 max(seq);空批为 0。 */
51
+ export declare function squeezedMaxSeq(squeezed: MessageRecord[]): number;
52
+ /** hydrate / 模型输出:非法 kind 丢条目;空 text 丢;条数与总长截断。 */
53
+ export declare function sanitizeConstraints(raw: unknown): SessionConstraint[];
54
+ /**
55
+ * 解析折叠模型输出。null = 解析失败(水位不推);[] = 无新结论(水位推、备忘不动)。
56
+ */
57
+ export declare function parseConstraints(raw: string): SessionConstraint[] | null;
58
+ /** 本批无用户消息时,新的已定/否决降为未决。 */
59
+ export declare function downgradeWithoutUser(list: SessionConstraint[], hasUser: boolean): SessionConstraint[];
60
+ /** system 提示词「已确认约束」块;空则空串。 */
61
+ export declare function constraintBlock(list: SessionConstraint[] | undefined): string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 角色发言失败:供应商原文 → 人话标题/原因(core 纯函数,无 React)。
3
+ * 原始 JSON 仍落盘在消息 text 里,UI 默认只展示短因,展开才看原文。
4
+ * @module dsh-group-chat/core/errors
5
+ */
6
+ export interface SpeakFailureView {
7
+ /** 一行标题,例如「额度已用尽」。 */
8
+ title: string;
9
+ /** 可选短因:重置时间、HTTP 状态等。 */
10
+ detail?: string;
11
+ /** 复制/排障用的供应商原文(已剥「模型输出异常终止: 」前缀)。 */
12
+ raw: string;
13
+ }
14
+ /** 剥旧系统胶囊与引擎包装前缀,保留供应商原文。 */
15
+ export declare function unwrapSpeakFailure(raw: string): string;
16
+ /** 旧系统胶囊文案:角色「名」发言失败:原文。对不上则 null。 */
17
+ export declare function parseLegacyRoleFailure(text: string): {
18
+ roleName: string;
19
+ rest: string;
20
+ } | null;
21
+ export interface RoleRef {
22
+ id: string;
23
+ name: string;
24
+ provider?: string;
25
+ model?: string;
26
+ }
27
+ export interface FailedMessageRef {
28
+ speaker: string;
29
+ text: string;
30
+ error?: boolean;
31
+ failedRoleId?: string;
32
+ }
33
+ /** 发言失败卡:error 标记,或旧系统胶囊文案。 */
34
+ export declare function isSpeakFailure(m: FailedMessageRef): boolean;
35
+ /** 失败回合对应角色:failedRoleId / speaker / 旧文案里的角色名(恰好一名才命中)。 */
36
+ export declare function resolveFailedRole<T extends RoleRef>(m: FailedMessageRef, roles: T[]): T | null;
37
+ /**
38
+ * 把旧系统失败行挂到对应角色(恰好一名命中才迁)。
39
+ * 返回是否改写了记录。
40
+ */
41
+ export declare function repairFailedMessage(m: {
42
+ speaker: string;
43
+ text: string;
44
+ error?: boolean;
45
+ failedRoleId?: string;
46
+ model?: string;
47
+ }, roles: RoleRef[]): boolean;
48
+ /**
49
+ * 把供应商错误压成可扫描的标题 + 短因。
50
+ * 未知形态回退为「发言失败」,原文仍可展开。
51
+ */
52
+ export declare function classifySpeakFailure(raw: string): SpeakFailureView;
53
+ /** 失败卡默认复制内容:标题 + 短因 + 原文。 */
54
+ export declare function formatSpeakFailureCopy(view: SpeakFailureView): string;
@@ -29,6 +29,17 @@ export interface GroupRecord {
29
29
  roleIds: string[];
30
30
  sessionIds: string[];
31
31
  }
32
+ /** 会话约束条目类型(窗口外结论/约束备忘)。 */
33
+ export type ConstraintKind = 'decided' | 'rejected' | 'open';
34
+ /** 全部合法约束类型。 */
35
+ export declare const CONSTRAINT_KINDS: readonly ConstraintKind[];
36
+ /** 合法 kind 原样,其余 undefined。 */
37
+ export declare function asConstraintKind(value: unknown): ConstraintKind | undefined;
38
+ /** 一条无主结论/约束(已定 / 否决 / 未决)。 */
39
+ export interface SessionConstraint {
40
+ kind: ConstraintKind;
41
+ text: string;
42
+ }
32
43
  /** 会话:消息挂在会话上。 */
33
44
  export interface SessionRecord {
34
45
  id: string;
@@ -39,6 +50,10 @@ export interface SessionRecord {
39
50
  namePinned?: boolean;
40
51
  /** 主题已被手动编辑:自动整理永久跳过(隐式固定)。 */
41
52
  topicPinned?: boolean;
53
+ /** 窗口外结论/约束备忘;空则省略。 */
54
+ constraints?: SessionConstraint[];
55
+ /** 已折入备忘的最大消息 seq;0/缺省 = 尚未折过。 */
56
+ constraintsUpToSeq?: number;
42
57
  messageIds: string[];
43
58
  createdAt: number;
44
59
  }
@@ -76,6 +91,8 @@ export interface MessageRecord {
76
91
  thinkingSummary?: string;
77
92
  model?: string;
78
93
  error?: boolean;
94
+ /** 发言失败时的角色 id;刷新后仍可对该条点重试。角色消息 speaker 即角色 id,此字段冗余兼容旧系统错误行。 */
95
+ failedRoleId?: string;
79
96
  toolCalls?: ToolCallRecord[];
80
97
  ts: number;
81
98
  }
@@ -114,6 +131,8 @@ export interface RunState {
114
131
  childProc: import('node:child_process').ChildProcess | null;
115
132
  /** 最近一次 run 的结束标记:会话列表「已完成/已出错」状态的数据源。 */
116
133
  finished: RunFinished | null;
134
+ /** 原地重试时被覆盖的失败消息 id;普通 send 为 null。 */
135
+ replaceMessageId: string | null;
117
136
  }
118
137
  /** 角色发言的引擎产物。 */
119
138
  export interface SpeakResult {
@@ -185,6 +204,7 @@ export interface Snapshot {
185
204
  partialReasoning: string;
186
205
  pendingConfirm: PendingConfirm | null;
187
206
  finished: RunFinished | null;
207
+ replaceMessageId: string | null;
188
208
  };
189
209
  lastCreated: LastCreated | null;
190
210
  groups: {
@@ -200,6 +220,7 @@ export interface Snapshot {
200
220
  groupId: string;
201
221
  name: string;
202
222
  topic: string;
223
+ constraints?: SessionConstraint[];
203
224
  messageIds: string[];
204
225
  createdAt: number;
205
226
  }[];
@@ -225,6 +246,7 @@ export interface Snapshot {
225
246
  reasoning?: string;
226
247
  model?: string;
227
248
  error?: boolean;
249
+ failedRoleId?: string;
228
250
  toolCalls?: ToolCallRecord[];
229
251
  ts: number;
230
252
  }[];
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 动作分发(handleAction,POST /api/group-chat/action 的载荷):
3
- * mutate(12 种 CRUD/配置操作)| send | stop | confirmCommand | models | efforts。
3
+ * mutate(12 种 CRUD/配置操作)| send | retrySpeak | stop | confirmCommand | models | efforts。
4
4
  * @module dsh-group-chat/host/api/actions
5
5
  */
6
6
  import type { Snapshot } from '../../core/types.ts';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 对话引擎:消息追加、群聊记录转写、角色发言(speak:prompt 构建 + 流式
3
- * 轮次 + 工具回注循环)、多轮 runLoop(每轮结束触发 retitle 标题整理)。
3
+ * 轮次 + 工具回注循环)、多轮 runLoop(结束时后台 retitle + 窗口外约束折叠)。
4
4
  * @module dsh-group-chat/host/engine/conversation
5
5
  */
6
6
  import type { MessageRecord, SessionRecord } from '../../core/types.ts';
@@ -11,8 +11,10 @@ import type { Tools } from '../tools/index.ts';
11
11
  export interface Conversation {
12
12
  /** 追加一条消息到会话(touch + 落盘调度)。 */
13
13
  appendMessage: (sess: SessionRecord, speaker: string, text: string, extra?: Partial<MessageRecord>) => MessageRecord;
14
- /** 多轮 round-robin 主循环(send 触发;finally 复位 run)。 */
15
- runLoop: (sess: SessionRecord) => Promise<void>;
14
+ /** 多轮 round-robin 主循环(send / retrySpeak 触发;finally 复位 run)。 */
15
+ runLoop: (sess: SessionRecord, opts?: {
16
+ replaceMessageId?: string;
17
+ }) => Promise<void>;
16
18
  }
17
19
  /** 创建对话引擎。 */
18
20
  export declare function createConversation(core: HostState, deps: {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 窗口外结论/约束备忘:整次 runLoop 结束后后台折叠(仿 retitle)。
3
+ * 立刻 idle;未折完用水位 + 临时原文表达。v1 复用 purpose session-title 关思考。
4
+ * @module dsh-group-chat/host/engine/fold
5
+ */
6
+ import type { SessionRecord } from '../../core/types.ts';
7
+ import type { HostState } from '../state.ts';
8
+ /**
9
+ * 每轮 send 结束后折叠窗口外约束:fire-and-forget、不产生消息、静默失败。
10
+ * 同会话去重;会话已清空则 abort。内存 {constraints, constraintsUpToSeq} 一次挂上。
11
+ */
12
+ export declare function createFold(core: HostState, deps: {
13
+ touch: () => void;
14
+ schedulePersist: (targets?: {
15
+ session?: string | null;
16
+ }) => void;
17
+ }): (sess: SessionRecord) => Promise<void>;
@@ -4,4 +4,5 @@
4
4
  */
5
5
  export { createConversation } from './conversation.ts';
6
6
  export type { Conversation } from './conversation.ts';
7
+ export { createFold } from './fold.ts';
7
8
  export { createRetitle } from './retitle.ts';
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * 会话标题/主题自动整理(参照 oil-codex-title):每轮结束后用 DSH 默认模型
3
- * 后台生成「类别 emoji + 对象|目标」名称与演进式主题;手动编辑过的字段
3
+ * 后台生成。名称只在仍为默认占位时写一次;主题每轮演进。手动编辑过的字段
4
4
  * 永久跳过(隐式固定)。
5
5
  * @module dsh-group-chat/host/engine/retitle
6
6
  */
7
7
  import type { SessionRecord } from '../../core/types.ts';
8
- import type { HostState } from '../state.ts';
8
+ import { type HostState } from '../state.ts';
9
9
  /**
10
10
  * 每轮结束后根据聊天内容整理会话名称与主题:后台 fire-and-forget、不产生
11
- * 消息、静默失败。名称 =「类别 emoji + 对象|目标」(类别固定、对象稳定、
12
- * 目标实质变化才改);主题 = 演进式一句话摘要(对象+目标+当前焦点),注入
13
- * 后续轮次的角色上下文。手动编辑过的字段永久跳过(隐式固定,apply 时复查)。
11
+ * 消息、静默失败。名称只在仍为默认占位时生成一次(「类别 emoji + 对象|目标」);
12
+ * 主题 = 演进式一句话摘要(对象+目标+当前焦点),每轮更新,注入后续角色上下文。
13
+ * 手动编辑过的字段永久跳过(隐式固定,apply 时复查)。
14
14
  */
15
15
  export declare function createRetitle(core: HostState, deps: {
16
16
  touch: () => void;
@@ -6,7 +6,7 @@
6
6
  * persistence/ 持久化(store 文件原语 + 脏标记合并落盘 + 启动恢复)
7
7
  * materials/ 资料读取与路径解析 + 目录浏览器
8
8
  * tools/ 工具执行(沙箱 + 确认闸门)
9
- * engine/ 对话引擎(conversation:speak/runLoop;retitle:标题整理)
9
+ * engine/ 对话引擎(conversation:speak/runLoop;retitle:标题整理;fold:窗口外约束)
10
10
  * api/ HTTP 传输(http 护栏 + routes 路由)与动作分发(actions)
11
11
  * @module dsh-group-chat/host/service
12
12
  */
@@ -20,6 +20,8 @@ declare module '@deepseek-ai/cordis' {
20
20
  }
21
21
  /** 角色标识色板(新增角色依序取色)。 */
22
22
  export declare const PALETTE: string[];
23
+ /** 新建会话默认名称(自动标题只在仍为此占位名时生成一次)。 */
24
+ export declare const DEFAULT_SESSION_NAME = "\u65B0\u4F1A\u8BDD";
23
25
  /** 宿主服务共享状态容器(见模块注释;可变原始值一律经 core.* 访问)。 */
24
26
  export interface HostState {
25
27
  ctx: Context;
@@ -10,6 +10,8 @@
10
10
  * 数据模型:Group 1..N Session,消息挂在会话上;角色与工作区目录挂在群组上
11
11
  * - 群组与会话的增删改;群组/会话检索由客户端在快照上过滤
12
12
  * - 角色发言经 `llm` 服务流式生成,按角色绑定的 provider/model 路由
13
+ * - 整次 runLoop 结束后后台折叠被 40 条窗口挤出的消息为会话级约束备忘
14
+ * (engine/fold.ts;立刻 idle、fire-and-forget,不产生消息)
13
15
  * - 群组工作区目录经 `fs` 服务读取(根下一层文本文件,最多 20 个),
14
16
  * 以「共享资料」块注入每个角色的 system 提示词;无独立笔记/文件清单
15
17
  * - 经 `webServer` 暴露 HTTP API:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@roaming-ai/dsh-group-chat",
3
3
  "description": "DSH 模型群聊:多模型角色群组对话面板。角色绑定不同 provider/model,群内共享对话记录;设置页支持启用/停用。",
4
- "version": "0.2.2",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -15,9 +15,10 @@ import { NavPanel } from './components/NavPanel.tsx'
15
15
  import { AsidePanel } from './components/AsidePanel.tsx'
16
16
  import { ChatPanel } from './components/ChatPanel.tsx'
17
17
  import { useGroupChatState, type ActionOk } from './hooks/useGroupChatState.ts'
18
- import { useComposerEffects, useComposerInput, useMentionChip, useInputKeyboard } from './hooks/useComposer.ts'
18
+ import { useComposerEffects, useComposerInput, useComposerDraft, useMentionChip, useInputKeyboard } from './hooks/useComposer.ts'
19
19
  import { useFileSearch } from './hooks/useFileSearch.ts'
20
20
  import { useMentionCandidates, useMentionedRoles, useSafeMentionIndex } from './hooks/useMentionState.ts'
21
+ import { clearComposerDraft } from './lib/composer-draft.ts'
21
22
  import { draftFromRole, blankDraft, groupById, roleById, sessById, type ClientSnapshot, type ModelsResponse, type SnapshotRole } from './lib/model.ts'
22
23
 
23
24
  export function GroupChatPanel(): ReactNode {
@@ -83,7 +84,6 @@ export function GroupChatPanel(): ReactNode {
83
84
  // ---- 所有 Hooks 必须在条件返回之前调用 ----
84
85
  // Composer effects
85
86
  const { onMsgsScroll } = useComposerEffects(inputRef, scrollRef, input, atBottom, snap)
86
- const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx)
87
87
 
88
88
  // 选中群组/会话解析(用于 hooks 依赖)
89
89
  let group = snap && gid ? groupById(snap, gid) : null
@@ -96,6 +96,9 @@ export function GroupChatPanel(): ReactNode {
96
96
  sess = group.sessionIds.length ? sessById(snap, group.sessionIds[group.sessionIds.length - 1]) : null
97
97
  }
98
98
 
99
+ const { syncFromDOM, onInputCE, onPasteCE, onDragOverCE, onDropCE } = useComposerInput(inputRef, setInput, setMention, setMentionIdx, sess ? sess.id : null)
100
+ useComposerDraft(sess ? sess.id : null, inputRef, setInput, setMention)
101
+
99
102
  const enabledRoles = group ? group.roleIds.map((id) => roleById(snap!, id)).filter((r): r is SnapshotRole => !!r && r.enabled) : []
100
103
  const participants = partsSel || enabledRoles.map((r) => r.id)
101
104
  const busyNow = !!(sess && snap && snap.run.running && snap.run.sessionId === sess.id)
@@ -140,6 +143,7 @@ export function GroupChatPanel(): ReactNode {
140
143
  setMention(null)
141
144
  setErr('')
142
145
  setAtBottom(true)
146
+ clearComposerDraft(sess.id)
143
147
  syncFromDOM()
144
148
  }
145
149
  } finally {
@@ -174,6 +178,16 @@ export function GroupChatPanel(): ReactNode {
174
178
  setPartsSel(has ? participants.filter((x) => x !== rid) : participants.concat([rid]))
175
179
  }
176
180
 
181
+ const retrySpeak = async (messageId: string): Promise<void> => {
182
+ if (!sess) return
183
+ if (busyNow || (snap && snap.run.running)) {
184
+ setToast({ text: '已有对话进行中,请先停止', seq: Date.now() })
185
+ return
186
+ }
187
+ const res = await action({ kind: 'retrySpeak', sessionId: sess.id, messageId }) as ActionOk | null
188
+ if (res && !res.ok && res.error) setToast({ text: res.error, seq: Date.now() })
189
+ }
190
+
177
191
  // 清空确认:不可清空(对话进行中)走 toast 提示,不再落到输入框上方的红字
178
192
  const doClear = async (): Promise<void> => {
179
193
  if (!sess) return
@@ -318,6 +332,7 @@ export function GroupChatPanel(): ReactNode {
318
332
  action={action}
319
333
  mutate={mutate}
320
334
  setMention={setMention}
335
+ onRetrySpeak={(messageId) => { void retrySpeak(messageId) }}
321
336
  />
322
337
  <AsidePanel
323
338
  snap={snap}
@@ -348,7 +363,7 @@ export function GroupChatPanel(): ReactNode {
348
363
  )}
349
364
  >
350
365
  <ul className="dsgc-clearnotes">
351
- <li>删除内容:本会话的用户消息与角色发言(含思考、工具调用记录),确认后立即落盘</li>
366
+ <li>删除内容:本会话的用户消息与角色发言(含思考、工具调用记录)以及本会话结论/约束备忘,确认后立即落盘</li>
352
367
  <li>不可恢复:此操作没有回收站,也没有撤销</li>
353
368
  <li>不受影响:会话本身与主题、群成员角色、工作区目录、权限档位</li>
354
369
  </ul>
@@ -78,15 +78,17 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
78
78
  </div>
79
79
  {r.persona ? <div className="dsgc-rolepersona" title={r.persona}>{r.persona}</div> : null}
80
80
  <div className="dsgc-rolemenu">
81
- <span
82
- className="dsgc-rolemodel"
83
- title={r.provider + ' / ' + r.model + (r.thinking ? ' · 深度思考' + (r.reasoningEffort && r.reasoningEffort !== 'default' ? '(' + r.reasoningEffort + ')' : '') : '')}
84
- >
85
- {r.provider} / {r.model}
81
+ <span className="dsgc-rolemeta">
82
+ <span
83
+ className="dsgc-rolemodel"
84
+ title={r.provider + ' / ' + r.model + (r.thinking ? ' · 深度思考' + (r.reasoningEffort && r.reasoningEffort !== 'default' ? '(' + r.reasoningEffort + ')' : '') : '')}
85
+ >
86
+ {r.provider} / {r.model}
87
+ </span>
88
+ {r.thinking
89
+ ? <span className="dsgc-rolethink" title="深度思考">{Icon(P.IconThinkOutline14, 14)}</span>
90
+ : null}
86
91
  </span>
87
- {r.thinking
88
- ? <span title="深度思考" style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--dsw-alias-label-tertiary,inherit)' }}>{Icon(P.IconThinkOutline14, 14)}</span>
89
- : null}
90
92
  <span className="dsgc-roleops">
91
93
  <button
92
94
  className="dsgc-opbtn"