@roaming-ai/dsh-group-chat 0.2.1 → 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.
- package/README.md +3 -2
- package/lib/client.js +986 -298
- package/lib/client.js.map +1 -1
- package/lib/index.js +524 -52
- package/lib/types/client/components/Bubble.d.ts +6 -3
- package/lib/types/client/components/ChatPanel.d.ts +1 -0
- package/lib/types/client/components/ConstraintList.d.ts +11 -0
- package/lib/types/client/components/FailCard.d.ts +9 -0
- package/lib/types/client/components/Fold.d.ts +19 -0
- package/lib/types/client/components/HoverTip.d.ts +22 -0
- package/lib/types/client/components/MessageFlow.d.ts +1 -0
- package/lib/types/client/components/MsgActions.d.ts +14 -0
- package/lib/types/client/hooks/useComposer.d.ts +6 -1
- package/lib/types/client/lib/composer-draft.d.ts +17 -0
- package/lib/types/core/constraints.d.ts +61 -0
- package/lib/types/core/errors.d.ts +54 -0
- package/lib/types/core/types.d.ts +22 -0
- package/lib/types/host/api/actions.d.ts +1 -1
- package/lib/types/host/engine/conversation.d.ts +5 -3
- package/lib/types/host/engine/fold.d.ts +17 -0
- package/lib/types/host/engine/index.d.ts +1 -0
- package/lib/types/host/engine/retitle.d.ts +5 -5
- package/lib/types/host/service.d.ts +1 -1
- package/lib/types/host/state.d.ts +2 -0
- package/lib/types/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/GroupChatPanel.tsx +18 -3
- package/src/client/components/AsidePanel.tsx +10 -8
- package/src/client/components/Bubble.tsx +45 -18
- package/src/client/components/ChatPanel.tsx +17 -12
- package/src/client/components/Composer.tsx +26 -20
- package/src/client/components/ConstraintList.tsx +69 -0
- package/src/client/components/FailCard.tsx +49 -0
- package/src/client/components/Fold.tsx +72 -0
- package/src/client/components/HoverTip.tsx +134 -0
- package/src/client/components/MessageFlow.tsx +77 -54
- package/src/client/components/MsgActions.tsx +85 -0
- package/src/client/components/NavPanel.tsx +6 -1
- package/src/client/components/ThinkRow.tsx +7 -3
- package/src/client/components/ToolRow.tsx +7 -3
- package/src/client/hooks/useComposer.ts +40 -3
- package/src/client/hooks/useGroupChatState.ts +5 -4
- package/src/client/lib/composer-draft.ts +47 -0
- package/src/client/lib/styles.ts +74 -13
- package/src/client/react-dom-shim.d.ts +2 -0
- package/src/core/constraints.ts +210 -0
- package/src/core/errors.ts +184 -0
- package/src/core/json.ts +1 -0
- package/src/core/types.ts +28 -3
- package/src/host/api/actions.ts +35 -1
- package/src/host/broadcast.ts +3 -3
- package/src/host/engine/conversation.ts +103 -38
- package/src/host/engine/fold.ts +114 -0
- package/src/host/engine/index.ts +1 -0
- package/src/host/engine/retitle.ts +16 -11
- package/src/host/persistence/persistence.ts +18 -1
- package/src/host/service.ts +1 -1
- package/src/host/state.ts +5 -4
- 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
|
|
11
|
+
import { type SnapshotRole } from '../lib/model.ts';
|
|
12
|
+
import type { SnapshotMessage } from '../lib/model.ts';
|
|
12
13
|
export interface BubbleProps {
|
|
13
|
-
m:
|
|
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,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
|
|
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
|
|
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>;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 会话标题/主题自动整理(参照 oil-codex-title):每轮结束后用 DSH 默认模型
|
|
3
|
-
*
|
|
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
|
|
8
|
+
import { type HostState } from '../state.ts';
|
|
9
9
|
/**
|
|
10
10
|
* 每轮结束后根据聊天内容整理会话名称与主题:后台 fire-and-forget、不产生
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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"
|