@roaming-ai/dsh-group-chat 0.3.0 → 0.3.1
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 +89 -26
- package/lib/client.js +289 -321
- package/lib/client.js.map +1 -1
- package/lib/index.js +124 -132
- package/lib/types/client/components/AsidePanel.d.ts +3 -19
- package/lib/types/client/components/NavPanel.d.ts +9 -11
- package/lib/types/client/components/RoleDrawer.d.ts +3 -2
- package/lib/types/client/components/ToolRow.d.ts +2 -2
- package/lib/types/client/hooks/useGroupChatState.d.ts +0 -16
- package/lib/types/client/lib/model.d.ts +1 -7
- package/lib/types/client/utils/utils.d.ts +4 -4
- package/lib/types/core/json.d.ts +6 -0
- package/lib/types/core/types.d.ts +57 -63
- package/lib/types/host/engine/defaults.d.ts +13 -0
- package/lib/types/shared/file-mention-grammar.d.ts +3 -12
- package/package.json +1 -1
- package/src/client/GroupChatPanel.tsx +7 -56
- package/src/client/components/AsidePanel.tsx +40 -15
- package/src/client/components/ConstraintList.tsx +1 -6
- package/src/client/components/MessageFlow.tsx +2 -1
- package/src/client/components/NavPanel.tsx +96 -78
- package/src/client/components/RoleDrawer.tsx +29 -7
- package/src/client/components/ToolRow.tsx +2 -2
- package/src/client/hooks/useComposer.ts +14 -53
- package/src/client/hooks/useGroupChatState.ts +2 -13
- package/src/client/lib/model.ts +1 -14
- package/src/client/utils/utils.ts +6 -6
- package/src/core/constraints.ts +6 -12
- package/src/core/errors.ts +2 -12
- package/src/core/json.ts +17 -0
- package/src/core/types.ts +27 -19
- package/src/host/api/actions.ts +14 -13
- package/src/host/engine/conversation.ts +49 -50
- package/src/host/engine/defaults.ts +28 -0
- package/src/host/engine/fold.ts +2 -17
- package/src/host/engine/retitle.ts +9 -31
- package/src/host/materials/materials.ts +2 -0
- package/src/host/tools/tools.ts +2 -2
- package/src/shared/file-mention-grammar.test.ts +1 -38
- package/src/shared/file-mention-grammar.ts +7 -33
- package/lib/types/client/Bubble.d.ts +0 -10
- package/lib/types/client/Glyph.d.ts +0 -12
- package/lib/types/client/GroupChatPanel.old.d.ts +0 -10
- package/lib/types/client/GroupChatSettingsSection.d.ts +0 -15
- package/lib/types/client/PermissionSelect.d.ts +0 -13
- package/lib/types/client/RoleDrawer.d.ts +0 -17
- package/lib/types/client/ThinkRow.d.ts +0 -9
- package/lib/types/client/ToolRow.d.ts +0 -9
- package/lib/types/client/api.d.ts +0 -8
- package/lib/types/client/components.d.ts +0 -22
- package/lib/types/client/drawer.d.ts +0 -17
- package/lib/types/client/glyph.d.ts +0 -12
- package/lib/types/client/model.d.ts +0 -78
- package/lib/types/client/panel.d.ts +0 -10
- package/lib/types/client/settings.d.ts +0 -15
- package/lib/types/client/styles.d.ts +0 -10
- package/lib/types/client/ui.d.ts +0 -17
- package/lib/types/client/utils.d.ts +0 -22
- package/lib/types/host/actions.d.ts +0 -34
- package/lib/types/host/conversation.d.ts +0 -25
- package/lib/types/host/http.d.ts +0 -16
- package/lib/types/host/materials.d.ts +0 -32
- package/lib/types/host/persistence.d.ts +0 -31
- package/lib/types/host/routes.d.ts +0 -11
- package/lib/types/host/store.d.ts +0 -65
- package/lib/types/host/tools.d.ts +0 -28
|
@@ -193,78 +193,72 @@ export interface FileSearchResult {
|
|
|
193
193
|
}[];
|
|
194
194
|
error?: string;
|
|
195
195
|
}
|
|
196
|
-
/**
|
|
196
|
+
/** 快照内的会话行(wire 形态:不含 hydrate 用的固定标记与折入水位)。 */
|
|
197
|
+
export type SnapshotSession = Omit<SessionRecord, 'namePinned' | 'topicPinned' | 'constraintsUpToSeq'>;
|
|
198
|
+
/** 快照内的消息行(wire 形态:思考全文与摘要不外发)。 */
|
|
199
|
+
export type SnapshotMessage = Omit<MessageRecord, 'reasoningFull' | 'thinkingSummary'>;
|
|
200
|
+
/** 发到客户端的全量快照(wire 形态;各表行由领域记录派生,字段增删由编译器同步)。 */
|
|
197
201
|
export interface Snapshot {
|
|
198
202
|
revision: number;
|
|
199
|
-
run:
|
|
200
|
-
running: boolean;
|
|
201
|
-
sessionId: string | null;
|
|
202
|
-
currentRoleId: string | null;
|
|
203
|
-
partial: string;
|
|
204
|
-
partialReasoning: string;
|
|
205
|
-
pendingConfirm: PendingConfirm | null;
|
|
206
|
-
finished: RunFinished | null;
|
|
207
|
-
replaceMessageId: string | null;
|
|
208
|
-
};
|
|
203
|
+
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | 'childProc'>;
|
|
209
204
|
lastCreated: LastCreated | null;
|
|
210
|
-
groups:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
permissionTier: PermissionTier;
|
|
215
|
-
roleIds: string[];
|
|
216
|
-
sessionIds: string[];
|
|
217
|
-
}[];
|
|
218
|
-
sessions: {
|
|
219
|
-
id: string;
|
|
220
|
-
groupId: string;
|
|
221
|
-
name: string;
|
|
222
|
-
topic: string;
|
|
223
|
-
constraints?: SessionConstraint[];
|
|
224
|
-
messageIds: string[];
|
|
225
|
-
createdAt: number;
|
|
226
|
-
}[];
|
|
227
|
-
roles: {
|
|
228
|
-
id: string;
|
|
229
|
-
groupId: string;
|
|
230
|
-
name: string;
|
|
231
|
-
color?: string;
|
|
232
|
-
persona: string;
|
|
233
|
-
provider: string;
|
|
234
|
-
model: string;
|
|
235
|
-
temperature?: number;
|
|
236
|
-
reasoningEffort?: string;
|
|
237
|
-
enabled: boolean;
|
|
238
|
-
thinking: boolean;
|
|
239
|
-
}[];
|
|
240
|
-
messages: {
|
|
241
|
-
id: string;
|
|
242
|
-
sessionId: string;
|
|
243
|
-
seq: number;
|
|
244
|
-
speaker: string;
|
|
245
|
-
text: string;
|
|
246
|
-
reasoning?: string;
|
|
247
|
-
model?: string;
|
|
248
|
-
error?: boolean;
|
|
249
|
-
failedRoleId?: string;
|
|
250
|
-
toolCalls?: ToolCallRecord[];
|
|
251
|
-
ts: number;
|
|
252
|
-
}[];
|
|
205
|
+
groups: GroupRecord[];
|
|
206
|
+
sessions: SnapshotSession[];
|
|
207
|
+
roles: RoleRecord[];
|
|
208
|
+
messages: SnapshotMessage[];
|
|
253
209
|
error?: string;
|
|
254
210
|
}
|
|
255
|
-
/** mutate
|
|
256
|
-
export
|
|
257
|
-
op:
|
|
258
|
-
groupId?: string;
|
|
259
|
-
sessionId?: string;
|
|
260
|
-
roleId?: string;
|
|
261
|
-
role?: Partial<RoleRecord>;
|
|
211
|
+
/** mutate 动作的参数形态(判别联合:按 op 收窄各分支字段;wire 上字段可缺,消费方各自守卫)。 */
|
|
212
|
+
export type MutateArgs = {
|
|
213
|
+
op: 'createGroup';
|
|
262
214
|
name?: string;
|
|
215
|
+
} | {
|
|
216
|
+
op: 'renameGroup';
|
|
217
|
+
groupId: string;
|
|
218
|
+
name?: string;
|
|
219
|
+
} | {
|
|
220
|
+
op: 'deleteGroup';
|
|
221
|
+
groupId: string;
|
|
222
|
+
} | {
|
|
223
|
+
op: 'createSession';
|
|
224
|
+
groupId: string;
|
|
225
|
+
name?: string;
|
|
226
|
+
} | {
|
|
227
|
+
op: 'renameSession';
|
|
228
|
+
sessionId: string;
|
|
229
|
+
name?: string;
|
|
230
|
+
} | {
|
|
231
|
+
op: 'deleteSession';
|
|
232
|
+
sessionId: string;
|
|
233
|
+
} | {
|
|
234
|
+
op: 'setTopic';
|
|
235
|
+
sessionId: string;
|
|
263
236
|
topic?: string;
|
|
237
|
+
} | {
|
|
238
|
+
op: 'upsertRole';
|
|
239
|
+
groupId: string;
|
|
240
|
+
role?: Partial<RoleRecord>;
|
|
241
|
+
} | {
|
|
242
|
+
op: 'deleteRole';
|
|
243
|
+
roleId: string;
|
|
244
|
+
} | {
|
|
245
|
+
op: 'setRoleEnabled';
|
|
246
|
+
roleId: string;
|
|
247
|
+
enabled?: boolean;
|
|
248
|
+
} | {
|
|
249
|
+
op: 'setWorkspaceDir';
|
|
250
|
+
groupId: string;
|
|
264
251
|
path?: string;
|
|
252
|
+
} | {
|
|
253
|
+
op: 'setPermissionTier';
|
|
254
|
+
groupId: string;
|
|
265
255
|
tier?: string;
|
|
266
|
-
|
|
267
|
-
|
|
256
|
+
} | {
|
|
257
|
+
op: 'ackFinish';
|
|
258
|
+
} | {
|
|
259
|
+
op: 'clearMessages';
|
|
260
|
+
sessionId: string;
|
|
261
|
+
};
|
|
268
262
|
/** send 动作的参数形态。 */
|
|
269
263
|
export interface SendArgs {
|
|
270
264
|
sessionId: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎共享小件:DSH 默认模型读取 + 发言人展示名(retitle / fold / conversation 共用)。
|
|
3
|
+
* @module dsh-group-chat/host/engine/defaults
|
|
4
|
+
*/
|
|
5
|
+
import type { RoleRecord } from '../../core/types.ts';
|
|
6
|
+
import type { HostState } from '../state.ts';
|
|
7
|
+
/** DSH 默认模型(agentDefaultModel 服务缺位或未配置时返回 null,调用方静默跳过)。 */
|
|
8
|
+
export declare const defaultModel: (core: HostState) => {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
} | null;
|
|
12
|
+
/** 发言人展示名(roles 表内查角色名;user/system 固定文案,见 speakerLabel)。 */
|
|
13
|
+
export declare const speakerNameOf: (roles: Map<string, RoleRecord>, speaker: string) => string;
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Sourced from @deepseek-ai/dsh-file-reference/lib/types/grammar.js
|
|
5
5
|
* Cannot import directly due to client bundle purity gate (dsh-client-bundle-purity).
|
|
6
6
|
*
|
|
7
|
+
* 只保留输入侧的 @token 识别(activeAtToken);候选的文本化(formatFileMention)
|
|
8
|
+
* 未被本插件采用——目录钻入走「插入整颗芯片」语义,见 docs/DESIGN.md Composer 契约。
|
|
9
|
+
*
|
|
7
10
|
* @module dsh-group-chat/shared
|
|
8
11
|
*/
|
|
9
12
|
export interface AtToken {
|
|
@@ -14,10 +17,6 @@ export interface AtToken {
|
|
|
14
17
|
/** Whether the token uses quoted form @"..." */
|
|
15
18
|
quoted: boolean;
|
|
16
19
|
}
|
|
17
|
-
export interface FileCandidate {
|
|
18
|
-
kind: 'file' | 'directory';
|
|
19
|
-
path: string;
|
|
20
|
-
}
|
|
21
20
|
/**
|
|
22
21
|
* Extract active @token at cursor position.
|
|
23
22
|
* Matches @ at word boundary (line start or after whitespace).
|
|
@@ -27,11 +26,3 @@ export interface FileCandidate {
|
|
|
27
26
|
* @returns Token or undefined if no active @token at cursor
|
|
28
27
|
*/
|
|
29
28
|
export declare function activeAtToken(line: string, cursor: number): AtToken | undefined;
|
|
30
|
-
/**
|
|
31
|
-
* Format file/directory candidate as @mention text.
|
|
32
|
-
*
|
|
33
|
-
* @param candidate - File or directory to format
|
|
34
|
-
* @param preserveQuote - Keep quotes even when unnecessary (for drill-down continuity)
|
|
35
|
-
* @returns Formatted @path or undefined for invalid paths
|
|
36
|
-
*/
|
|
37
|
-
export declare function formatFileMention(candidate: FileCandidate, preserveQuote: boolean): string | undefined;
|
package/package.json
CHANGED
|
@@ -41,16 +41,10 @@ export function GroupChatPanel(): ReactNode {
|
|
|
41
41
|
setConfirmClear,
|
|
42
42
|
roleDraft,
|
|
43
43
|
setRoleDraft,
|
|
44
|
-
roleFormError,
|
|
45
|
-
setRoleFormError,
|
|
46
44
|
models,
|
|
47
45
|
setModels,
|
|
48
46
|
modelsError,
|
|
49
47
|
setModelsError,
|
|
50
|
-
fileBrowser,
|
|
51
|
-
setFileBrowser,
|
|
52
|
-
wsDraft,
|
|
53
|
-
setWsDraft,
|
|
54
48
|
partsSel,
|
|
55
49
|
setPartsSel,
|
|
56
50
|
rounds,
|
|
@@ -129,11 +123,11 @@ export function GroupChatPanel(): ReactNode {
|
|
|
129
123
|
if (busyNow || !sess || sendingRef.current) return
|
|
130
124
|
sendingRef.current = true
|
|
131
125
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!mentionedRoles.length) return
|
|
126
|
+
if (!participants.length && !mentionedRoles.length) {
|
|
127
|
+
setErr('请至少选择一个参与角色(或在消息中 @成员)')
|
|
128
|
+
return
|
|
136
129
|
}
|
|
130
|
+
const parts = mentionedRoles.length ? mentionedRoles.map((r) => r.id) : participants
|
|
137
131
|
const res = await action({ kind: 'send', sessionId: sess.id, text: input, participantRoleIds: parts, rounds }) as ActionOk | null
|
|
138
132
|
if (res && !res.ok && res.error) setErr(res.error)
|
|
139
133
|
else if (res && res.ok) {
|
|
@@ -220,51 +214,14 @@ export function GroupChatPanel(): ReactNode {
|
|
|
220
214
|
|
|
221
215
|
const openRoleEditor = async (role: SnapshotRole | null): Promise<void> => {
|
|
222
216
|
setErr('')
|
|
223
|
-
setRoleFormError('')
|
|
224
217
|
if (!models || modelsError) await fetchModels()
|
|
225
218
|
setRoleDraft(role ? draftFromRole(role) : blankDraft())
|
|
226
219
|
}
|
|
227
220
|
|
|
228
|
-
const saveRole = async (): Promise<void> => {
|
|
229
|
-
if (!roleDraft) return
|
|
230
|
-
if (!roleDraft.name.trim()) {
|
|
231
|
-
setRoleFormError('角色名称不能为空')
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
if (!roleDraft.provider || !roleDraft.model) {
|
|
235
|
-
setRoleFormError('请选择角色绑定的模型')
|
|
236
|
-
return
|
|
237
|
-
}
|
|
238
|
-
const res = await mutate({ op: 'upsertRole', groupId: group.id, role: roleDraft })
|
|
239
|
-
if (res && res.ok && res.snapshot && !res.snapshot.error) {
|
|
240
|
-
setRoleDraft(null)
|
|
241
|
-
setRoleFormError('')
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
221
|
const stopRun = async (): Promise<void> => {
|
|
246
222
|
await action({ kind: 'stop', sessionId: sess!.id })
|
|
247
223
|
}
|
|
248
224
|
|
|
249
|
-
const openBrowser = async (path: string | undefined): Promise<void> => {
|
|
250
|
-
setFileBrowser({ open: true, loading: true, list: null, error: '' })
|
|
251
|
-
try {
|
|
252
|
-
const res = await api.action({ kind: 'browse', path: path || '' }) as import('../core/types.ts').BrowseResult
|
|
253
|
-
if (res && res.ok) setFileBrowser({ open: true, loading: false, list: res, error: '' })
|
|
254
|
-
else setFileBrowser({ open: true, loading: false, list: null, error: (res && res.error) || '浏览失败' })
|
|
255
|
-
} catch (e) {
|
|
256
|
-
setFileBrowser({ open: true, loading: false, list: null, error: String((e && (e as Error).message) || e) })
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const selectCurrentDir = (): void => {
|
|
261
|
-
if (!fileBrowser || !fileBrowser.list || !group) return
|
|
262
|
-
const path = fileBrowser.list.path!
|
|
263
|
-
void mutate({ op: 'setWorkspaceDir', groupId: group.id, path })
|
|
264
|
-
setWsDraft(null)
|
|
265
|
-
setFileBrowser(null)
|
|
266
|
-
}
|
|
267
|
-
|
|
268
225
|
return (
|
|
269
226
|
<div className="dsgc-root">
|
|
270
227
|
<NavPanel
|
|
@@ -338,14 +295,8 @@ export function GroupChatPanel(): ReactNode {
|
|
|
338
295
|
snap={snap}
|
|
339
296
|
group={group}
|
|
340
297
|
asideOpen={asideOpen}
|
|
341
|
-
wsDraft={wsDraft}
|
|
342
|
-
setWsDraft={setWsDraft}
|
|
343
|
-
fileBrowser={fileBrowser}
|
|
344
|
-
setFileBrowser={setFileBrowser}
|
|
345
298
|
mutate={mutate}
|
|
346
299
|
openRoleEditor={openRoleEditor}
|
|
347
|
-
openBrowser={openBrowser}
|
|
348
|
-
selectCurrentDir={selectCurrentDir}
|
|
349
300
|
/>
|
|
350
301
|
{sess && confirmClear
|
|
351
302
|
? (
|
|
@@ -386,12 +337,12 @@ export function GroupChatPanel(): ReactNode {
|
|
|
386
337
|
<RoleDrawer
|
|
387
338
|
draft={roleDraft}
|
|
388
339
|
set={setRoleDraft}
|
|
340
|
+
groupId={group.id}
|
|
389
341
|
models={models}
|
|
390
342
|
modelsError={modelsError}
|
|
391
343
|
onRetryModels={() => { void fetchModels() }}
|
|
392
|
-
|
|
393
|
-
onCancel={() => { setRoleDraft(null)
|
|
394
|
-
formError={roleFormError}
|
|
344
|
+
mutate={mutate}
|
|
345
|
+
onCancel={() => { setRoleDraft(null) }}
|
|
395
346
|
/>
|
|
396
347
|
)
|
|
397
348
|
: null}
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 右侧成员与工作区栏组件。工作区目录编辑态(草稿、文件浏览器)为栏内自有状态。
|
|
3
3
|
* @module dsh-group-chat/client/components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type
|
|
6
|
+
import { useState, type ReactNode } from 'react'
|
|
7
7
|
import { Icon, P } from '../lib/ui.ts'
|
|
8
|
-
import {
|
|
8
|
+
import { api } from '../lib/api.ts'
|
|
9
|
+
import { roleById, type ClientSnapshot, type SnapshotRole } from '../lib/model.ts'
|
|
10
|
+
import type { BrowseResult } from '../../core/types.ts'
|
|
9
11
|
|
|
10
12
|
interface AsidePanelProps {
|
|
11
13
|
snap: ClientSnapshot
|
|
12
14
|
group: ClientSnapshot['groups'][number]
|
|
13
15
|
asideOpen: boolean
|
|
14
|
-
wsDraft: string | null
|
|
15
|
-
setWsDraft: (val: string | null) => void
|
|
16
|
-
fileBrowser: { open: boolean, loading: boolean, list: import('../../core/types.ts').BrowseResult | null, error: string } | null
|
|
17
|
-
setFileBrowser: (val: { open: boolean, loading: boolean, list: import('../../core/types.ts').BrowseResult | null, error: string } | null) => void
|
|
18
16
|
mutate: (args: Record<string, unknown>) => Promise<unknown>
|
|
19
|
-
openRoleEditor: (role: SnapshotRole | null) =>
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
openRoleEditor: (role: SnapshotRole | null) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 文件浏览器态(栏内自有)。 */
|
|
21
|
+
interface FileBrowserState {
|
|
22
|
+
open: boolean
|
|
23
|
+
loading: boolean
|
|
24
|
+
list: BrowseResult | null
|
|
25
|
+
error: string
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function AsidePanel(props: AsidePanelProps): ReactNode {
|
|
25
|
-
const { snap, group, asideOpen,
|
|
29
|
+
const { snap, group, asideOpen, mutate, openRoleEditor } = props
|
|
30
|
+
const [wsDraft, setWsDraft] = useState<string | null>(null)
|
|
31
|
+
const [fileBrowser, setFileBrowser] = useState<FileBrowserState | null>(null)
|
|
26
32
|
|
|
27
33
|
const commitWsDir = (): void => {
|
|
28
34
|
if (wsDraft !== null && group && wsDraft !== (group.workspaceDir || '')) {
|
|
@@ -31,6 +37,25 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
|
|
|
31
37
|
setWsDraft(null)
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
const openBrowser = async (path: string | undefined): Promise<void> => {
|
|
41
|
+
setFileBrowser({ open: true, loading: true, list: null, error: '' })
|
|
42
|
+
try {
|
|
43
|
+
const res = await api.action({ kind: 'browse', path: path || '' }) as BrowseResult
|
|
44
|
+
if (res && res.ok) setFileBrowser({ open: true, loading: false, list: res, error: '' })
|
|
45
|
+
else setFileBrowser({ open: true, loading: false, list: null, error: (res && res.error) || '浏览失败' })
|
|
46
|
+
} catch (e) {
|
|
47
|
+
setFileBrowser({ open: true, loading: false, list: null, error: String((e && (e as Error).message) || e) })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const selectCurrentDir = (): void => {
|
|
52
|
+
if (!fileBrowser || !fileBrowser.list || !group) return
|
|
53
|
+
const path = fileBrowser.list.path!
|
|
54
|
+
void mutate({ op: 'setWorkspaceDir', groupId: group.id, path })
|
|
55
|
+
setWsDraft(null)
|
|
56
|
+
setFileBrowser(null)
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
return (
|
|
35
60
|
<div className={'dsgc-aside' + (asideOpen ? '' : ' closed')} aria-hidden={asideOpen ? undefined : 'true'}>
|
|
36
61
|
<div className="dsgc-sec">
|
|
@@ -38,7 +63,7 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
|
|
|
38
63
|
群成员
|
|
39
64
|
<span className="dsgc-secspacer" />
|
|
40
65
|
<span className="dsgc-seccount">{group.roleIds.length ? group.roleIds.length + ' 个' : ''}</span>
|
|
41
|
-
<P.Button variant="ghost" size="sm" onClick={() => {
|
|
66
|
+
<P.Button variant="ghost" size="sm" onClick={() => { openRoleEditor(null) }} aria-label="添加角色">
|
|
42
67
|
{Icon(P.IconPlusOutline16, 14)}添加
|
|
43
68
|
</P.Button>
|
|
44
69
|
</div>
|
|
@@ -54,8 +79,8 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
|
|
|
54
79
|
className={'dsgc-role' + (r.enabled ? '' : ' off')}
|
|
55
80
|
role="button"
|
|
56
81
|
tabIndex={0}
|
|
57
|
-
onClick={() => {
|
|
58
|
-
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault();
|
|
82
|
+
onClick={() => { openRoleEditor(r) }}
|
|
83
|
+
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); openRoleEditor(r) } }}
|
|
59
84
|
title="点击编辑角色"
|
|
60
85
|
>
|
|
61
86
|
<div className="dsgc-rolehead">
|
|
@@ -94,7 +119,7 @@ export function AsidePanel(props: AsidePanelProps): ReactNode {
|
|
|
94
119
|
className="dsgc-opbtn"
|
|
95
120
|
title="编辑角色"
|
|
96
121
|
aria-label="编辑角色"
|
|
97
|
-
onClick={(e) => { e.stopPropagation();
|
|
122
|
+
onClick={(e) => { e.stopPropagation(); openRoleEditor(r) }}
|
|
98
123
|
>
|
|
99
124
|
{Icon(P.IconEditOutline16, 14)}
|
|
100
125
|
</button>
|
|
@@ -5,14 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { useState, type ReactNode } from 'react'
|
|
7
7
|
import type { SessionConstraint } from '../../core/types.ts'
|
|
8
|
+
import { KIND_LABEL } from '../../core/constraints.ts'
|
|
8
9
|
import { ClipWell, Fold } from './Fold.tsx'
|
|
9
10
|
|
|
10
|
-
const KIND_LABEL: Record<SessionConstraint['kind'], string> = {
|
|
11
|
-
decided: '已定',
|
|
12
|
-
rejected: '否决',
|
|
13
|
-
open: '未决',
|
|
14
|
-
}
|
|
15
|
-
|
|
16
11
|
interface ConstraintListProps {
|
|
17
12
|
items: SessionConstraint[]
|
|
18
13
|
}
|
|
@@ -87,6 +87,8 @@ export function MessageFlow(props: MessageFlowProps): ReactNode {
|
|
|
87
87
|
let livePlaced = false
|
|
88
88
|
|
|
89
89
|
if (sess) {
|
|
90
|
+
// 循环不变量:本群角色表只过滤一次(流式期间每帧执行的热路径)
|
|
91
|
+
const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId)
|
|
90
92
|
const ids = sess.messageIds
|
|
91
93
|
const memo = sess.constraints && sess.constraints.length
|
|
92
94
|
? <ConstraintList key={sess.id + '-constraints'} items={sess.constraints} />
|
|
@@ -100,7 +102,6 @@ export function MessageFlow(props: MessageFlowProps): ReactNode {
|
|
|
100
102
|
if (!m) continue
|
|
101
103
|
// 角色在父级解析:Bubble 按值 memo(不依赖 snap identity),流式帧
|
|
102
104
|
// 不再触发已完成消息的全量重渲(每帧仅 live 行与派生列表变化)
|
|
103
|
-
const groupRoles = snap.roles.filter((r) => r.groupId === sess.groupId)
|
|
104
105
|
const role = resolveFailedRole(m, groupRoles) || (m.speaker !== 'user' && m.speaker !== 'system' ? roleById(snap, m.speaker) : null)
|
|
105
106
|
// 原地重试:该槽位换成 live(确认卡紧随其后),不要钉在列表末尾
|
|
106
107
|
if (replaceId && m.id === replaceId) {
|