@roaming-ai/dsh-group-chat 0.2.2 → 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 +91 -27
- package/lib/client.js +1254 -598
- package/lib/client.js.map +1 -1
- package/lib/index.js +573 -109
- package/lib/types/client/components/AsidePanel.d.ts +3 -19
- 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/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/useComposer.d.ts +6 -1
- package/lib/types/client/hooks/useGroupChatState.d.ts +0 -16
- package/lib/types/client/lib/composer-draft.d.ts +17 -0
- package/lib/types/client/lib/model.d.ts +1 -7
- package/lib/types/client/utils/utils.d.ts +4 -4
- package/lib/types/core/constraints.d.ts +61 -0
- package/lib/types/core/errors.d.ts +54 -0
- package/lib/types/core/json.d.ts +6 -0
- package/lib/types/core/types.d.ts +76 -60
- 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/defaults.d.ts +13 -0
- 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/lib/types/shared/file-mention-grammar.d.ts +3 -12
- package/package.json +1 -1
- package/src/client/GroupChatPanel.tsx +25 -59
- package/src/client/components/AsidePanel.tsx +50 -23
- 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 +64 -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 +78 -54
- package/src/client/components/MsgActions.tsx +85 -0
- package/src/client/components/NavPanel.tsx +102 -79
- package/src/client/components/RoleDrawer.tsx +29 -7
- package/src/client/components/ThinkRow.tsx +7 -3
- package/src/client/components/ToolRow.tsx +9 -5
- package/src/client/hooks/useComposer.ts +54 -56
- package/src/client/hooks/useGroupChatState.ts +7 -17
- package/src/client/lib/composer-draft.ts +47 -0
- package/src/client/lib/model.ts +1 -14
- package/src/client/lib/styles.ts +73 -12
- package/src/client/react-dom-shim.d.ts +2 -0
- package/src/client/utils/utils.ts +6 -6
- package/src/core/constraints.ts +204 -0
- package/src/core/errors.ts +174 -0
- package/src/core/json.ts +18 -0
- package/src/core/types.ts +52 -19
- package/src/host/api/actions.ts +49 -14
- package/src/host/broadcast.ts +3 -3
- package/src/host/engine/conversation.ts +104 -40
- package/src/host/engine/defaults.ts +28 -0
- package/src/host/engine/fold.ts +99 -0
- package/src/host/engine/index.ts +1 -0
- package/src/host/engine/retitle.ts +24 -41
- package/src/host/materials/materials.ts +2 -0
- package/src/host/persistence/persistence.ts +18 -1
- package/src/host/service.ts +1 -1
- package/src/host/state.ts +5 -4
- package/src/host/tools/tools.ts +2 -2
- package/src/index.ts +2 -0
- 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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色发言失败:供应商原文 → 人话标题/原因(core 纯函数,无 React)。
|
|
3
|
+
* 原始 JSON 仍落盘在消息 text 里,UI 默认只展示短因,展开才看原文。
|
|
4
|
+
* @module dsh-group-chat/core/errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SpeakFailureView {
|
|
8
|
+
/** 一行标题,例如「额度已用尽」。 */
|
|
9
|
+
title: string
|
|
10
|
+
/** 可选短因:重置时间、HTTP 状态等。 */
|
|
11
|
+
detail?: string
|
|
12
|
+
/** 复制/排障用的供应商原文(已剥「模型输出异常终止: 」前缀)。 */
|
|
13
|
+
raw: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PREFIX = '模型输出异常终止: '
|
|
17
|
+
const LEGACY_ROLE = /^角色「([^」]+)」发言失败[::]\s*/
|
|
18
|
+
|
|
19
|
+
/** 剥旧系统胶囊与引擎包装前缀,保留供应商原文。 */
|
|
20
|
+
export function unwrapSpeakFailure(raw: string): string {
|
|
21
|
+
const text = String(raw || '').trim()
|
|
22
|
+
const legacy = parseLegacyRoleFailure(text)
|
|
23
|
+
const body = legacy ? legacy.rest : text
|
|
24
|
+
return body.startsWith(PREFIX) ? body.slice(PREFIX.length).trim() : body
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 旧系统胶囊文案:角色「名」发言失败:原文。对不上则 null。 */
|
|
28
|
+
export function parseLegacyRoleFailure(text: string): { roleName: string, rest: string } | null {
|
|
29
|
+
const m = LEGACY_ROLE.exec(String(text || ''))
|
|
30
|
+
if (!m) return null
|
|
31
|
+
return { roleName: m[1], rest: String(text).slice(m[0].length) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RoleRef {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
provider?: string
|
|
38
|
+
model?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FailedMessageRef {
|
|
42
|
+
speaker: string
|
|
43
|
+
text: string
|
|
44
|
+
error?: boolean
|
|
45
|
+
failedRoleId?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 发言失败卡:error 标记,或旧系统胶囊文案。 */
|
|
49
|
+
export function isSpeakFailure(m: FailedMessageRef): boolean {
|
|
50
|
+
return !!m.error || !!parseLegacyRoleFailure(m.text)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 失败回合对应角色:failedRoleId / speaker / 旧文案里的角色名(恰好一名才命中)。 */
|
|
54
|
+
export function resolveFailedRole<T extends RoleRef>(m: FailedMessageRef, roles: T[]): T | null {
|
|
55
|
+
const byId = (id: string | undefined): T | null => {
|
|
56
|
+
if (!id) return null
|
|
57
|
+
for (const r of roles) if (r.id === id) return r
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
const direct = byId(m.failedRoleId) || (m.speaker !== 'user' && m.speaker !== 'system' ? byId(m.speaker) : null)
|
|
61
|
+
if (direct) return direct
|
|
62
|
+
const parsed = parseLegacyRoleFailure(m.text)
|
|
63
|
+
if (!parsed) return null
|
|
64
|
+
const hits = roles.filter((r) => r.name === parsed.roleName)
|
|
65
|
+
return hits.length === 1 ? hits[0] : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 把旧系统失败行挂到对应角色(恰好一名命中才迁)。
|
|
70
|
+
* 返回是否改写了记录。
|
|
71
|
+
*/
|
|
72
|
+
export function repairFailedMessage(
|
|
73
|
+
m: { speaker: string, text: string, error?: boolean, failedRoleId?: string, model?: string },
|
|
74
|
+
roles: RoleRef[],
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!isSpeakFailure(m)) return false
|
|
77
|
+
let changed = false
|
|
78
|
+
if (!m.error) {
|
|
79
|
+
m.error = true
|
|
80
|
+
changed = true
|
|
81
|
+
}
|
|
82
|
+
if (!m.failedRoleId && m.speaker !== 'user' && m.speaker !== 'system') {
|
|
83
|
+
m.failedRoleId = m.speaker
|
|
84
|
+
changed = true
|
|
85
|
+
}
|
|
86
|
+
if (!m.failedRoleId && m.speaker === 'system') {
|
|
87
|
+
const parsed = parseLegacyRoleFailure(m.text)
|
|
88
|
+
if (parsed) {
|
|
89
|
+
const hits = roles.filter((r) => r.name === parsed.roleName)
|
|
90
|
+
if (hits.length === 1) {
|
|
91
|
+
const role = hits[0]
|
|
92
|
+
m.speaker = role.id
|
|
93
|
+
m.failedRoleId = role.id
|
|
94
|
+
m.text = unwrapSpeakFailure(parsed.rest)
|
|
95
|
+
if (!m.model && (role.provider || role.model)) m.model = (role.provider || '') + ' / ' + (role.model || '')
|
|
96
|
+
changed = true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} else if (m.failedRoleId && m.speaker === 'system') {
|
|
100
|
+
m.speaker = m.failedRoleId
|
|
101
|
+
changed = true
|
|
102
|
+
}
|
|
103
|
+
// 对不上角色时保留「角色「名」发言失败」原文,客户端才能再解析。
|
|
104
|
+
if (m.failedRoleId || (m.speaker !== 'user' && m.speaker !== 'system')) {
|
|
105
|
+
const unwrapped = unwrapSpeakFailure(m.text)
|
|
106
|
+
if (unwrapped !== m.text) {
|
|
107
|
+
m.text = unwrapped
|
|
108
|
+
changed = true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return changed
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
import { looseJson } from './json.ts'
|
|
115
|
+
|
|
116
|
+
function httpStatus(text: string): number | null {
|
|
117
|
+
const m = text.match(/\b([1-5]\d{2})\b/)
|
|
118
|
+
if (!m) return null
|
|
119
|
+
const n = Number(m[1])
|
|
120
|
+
return n >= 100 && n <= 599 ? n : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resetHint(message: string): string | undefined {
|
|
124
|
+
const m = message.match(/reset at ([^.]+\S)/i) || message.match(/将在\s*([^\s。]+)\s*重置/)
|
|
125
|
+
return m ? '将在 ' + m[1] + ' 重置' : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 把供应商错误压成可扫描的标题 + 短因。
|
|
130
|
+
* 未知形态回退为「发言失败」,原文仍可展开。
|
|
131
|
+
*/
|
|
132
|
+
export function classifySpeakFailure(raw: string): SpeakFailureView {
|
|
133
|
+
const source = unwrapSpeakFailure(raw)
|
|
134
|
+
const blob = looseJson(source)
|
|
135
|
+
const code = blob && typeof blob.code === 'string' ? blob.code : ''
|
|
136
|
+
const type = blob && typeof blob.type === 'string' ? blob.type : ''
|
|
137
|
+
const message = blob && typeof blob.message === 'string' ? blob.message : source
|
|
138
|
+
const status = httpStatus(source)
|
|
139
|
+
const joined = (code + ' ' + type + ' ' + message + ' ' + source).toLowerCase()
|
|
140
|
+
|
|
141
|
+
if (/quota|accountquotaexceeded|exceeded the .*quota|额度|配额/.test(joined) || code === 'AccountQuotaExceeded') {
|
|
142
|
+
return { title: '额度已用尽', detail: resetHint(message), raw: source }
|
|
143
|
+
}
|
|
144
|
+
if (status === 429 || /too.?many.?requests|rate.?limit|限流/.test(joined) || type === 'TooManyRequests') {
|
|
145
|
+
return { title: '请求过于频繁', detail: '稍后再试,或降低并发', raw: source }
|
|
146
|
+
}
|
|
147
|
+
if (status === 401 || status === 403 || /unauthorized|forbidden|invalid.?api.?key|鉴权|未授权/.test(joined)) {
|
|
148
|
+
return { title: '模型鉴权失败', detail: '检查该角色绑定的提供方密钥', raw: source }
|
|
149
|
+
}
|
|
150
|
+
if (status === 404 || /model.?not.?found|unknown.?model|模型不存在/.test(joined)) {
|
|
151
|
+
return { title: '模型不可用', detail: '该角色绑定的模型可能已下线', raw: source }
|
|
152
|
+
}
|
|
153
|
+
if (/timeout|timed out|etimedout|超时/.test(joined)) {
|
|
154
|
+
return { title: '模型响应超时', raw: source }
|
|
155
|
+
}
|
|
156
|
+
if (/network|econnreset|econnrefused|enotfound|fetch failed|网络/.test(joined)) {
|
|
157
|
+
return { title: '网络异常', detail: '检查网络后重试', raw: source }
|
|
158
|
+
}
|
|
159
|
+
if (status !== null && status >= 500) {
|
|
160
|
+
return { title: '模型服务暂时不可用', detail: 'HTTP ' + status, raw: source }
|
|
161
|
+
}
|
|
162
|
+
if (status !== null) {
|
|
163
|
+
return { title: '发言失败', detail: 'HTTP ' + status, raw: source }
|
|
164
|
+
}
|
|
165
|
+
return { title: '发言失败', raw: source }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 失败卡默认复制内容:标题 + 短因 + 原文。 */
|
|
169
|
+
export function formatSpeakFailureCopy(view: SpeakFailureView): string {
|
|
170
|
+
const lines = [view.title]
|
|
171
|
+
if (view.detail) lines.push(view.detail)
|
|
172
|
+
if (view.raw && view.raw !== view.title) lines.push(view.raw)
|
|
173
|
+
return lines.join('\n')
|
|
174
|
+
}
|
package/src/core/json.ts
CHANGED
|
@@ -14,6 +14,7 @@ export function messageJson(m: Partial<MessageRecord> | undefined): Record<strin
|
|
|
14
14
|
if (m.reasoningFull !== undefined) o.reasoningFull = m.reasoningFull
|
|
15
15
|
if (m.thinkingSummary !== undefined) o.thinkingSummary = m.thinkingSummary
|
|
16
16
|
if (m.error !== undefined) o.error = m.error
|
|
17
|
+
if (typeof m.failedRoleId === 'string' && m.failedRoleId) o.failedRoleId = m.failedRoleId
|
|
17
18
|
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) o.toolCalls = m.toolCalls
|
|
18
19
|
return o
|
|
19
20
|
}
|
|
@@ -26,3 +27,20 @@ export function roleJson(r: Partial<RoleRecord>): Record<string, unknown> {
|
|
|
26
27
|
if (typeof r.reasoningEffort === 'string' && r.reasoningEffort && r.reasoningEffort !== 'default') o.reasoningEffort = r.reasoningEffort
|
|
27
28
|
return o
|
|
28
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 宽容 JSON 提取(模型输出/供应商错误原文的统一解析入口):
|
|
33
|
+
* 取首个 { 至末个 } 的片段解析,仅接受普通对象;失败返回 null。
|
|
34
|
+
* 代码围栏剥离由调用方按需先行处理(供应商错误原文不剥)。
|
|
35
|
+
*/
|
|
36
|
+
export function looseJson(raw: string): Record<string, unknown> | null {
|
|
37
|
+
const start = raw.indexOf('{')
|
|
38
|
+
const end = raw.lastIndexOf('}')
|
|
39
|
+
if (start < 0 || end <= start) return null
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(raw.slice(start, end + 1)) as unknown
|
|
42
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/core/types.ts
CHANGED
|
@@ -41,6 +41,23 @@ export interface GroupRecord {
|
|
|
41
41
|
sessionIds: string[]
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** 会话约束条目类型(窗口外结论/约束备忘)。 */
|
|
45
|
+
export type ConstraintKind = 'decided' | 'rejected' | 'open'
|
|
46
|
+
|
|
47
|
+
/** 全部合法约束类型。 */
|
|
48
|
+
export const CONSTRAINT_KINDS: readonly ConstraintKind[] = ['decided', 'rejected', 'open']
|
|
49
|
+
|
|
50
|
+
/** 合法 kind 原样,其余 undefined。 */
|
|
51
|
+
export function asConstraintKind(value: unknown): ConstraintKind | undefined {
|
|
52
|
+
return typeof value === 'string' && (CONSTRAINT_KINDS as readonly string[]).includes(value) ? value as ConstraintKind : undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 一条无主结论/约束(已定 / 否决 / 未决)。 */
|
|
56
|
+
export interface SessionConstraint {
|
|
57
|
+
kind: ConstraintKind
|
|
58
|
+
text: string
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
/** 会话:消息挂在会话上。 */
|
|
45
62
|
export interface SessionRecord {
|
|
46
63
|
id: string
|
|
@@ -51,6 +68,10 @@ export interface SessionRecord {
|
|
|
51
68
|
namePinned?: boolean
|
|
52
69
|
/** 主题已被手动编辑:自动整理永久跳过(隐式固定)。 */
|
|
53
70
|
topicPinned?: boolean
|
|
71
|
+
/** 窗口外结论/约束备忘;空则省略。 */
|
|
72
|
+
constraints?: SessionConstraint[]
|
|
73
|
+
/** 已折入备忘的最大消息 seq;0/缺省 = 尚未折过。 */
|
|
74
|
+
constraintsUpToSeq?: number
|
|
54
75
|
messageIds: string[]
|
|
55
76
|
createdAt: number
|
|
56
77
|
}
|
|
@@ -91,6 +112,8 @@ export interface MessageRecord {
|
|
|
91
112
|
thinkingSummary?: string
|
|
92
113
|
model?: string
|
|
93
114
|
error?: boolean
|
|
115
|
+
/** 发言失败时的角色 id;刷新后仍可对该条点重试。角色消息 speaker 即角色 id,此字段冗余兼容旧系统错误行。 */
|
|
116
|
+
failedRoleId?: string
|
|
94
117
|
toolCalls?: ToolCallRecord[]
|
|
95
118
|
ts: number
|
|
96
119
|
}
|
|
@@ -129,6 +152,8 @@ export interface RunState {
|
|
|
129
152
|
childProc: import('node:child_process').ChildProcess | null
|
|
130
153
|
/** 最近一次 run 的结束标记:会话列表「已完成/已出错」状态的数据源。 */
|
|
131
154
|
finished: RunFinished | null
|
|
155
|
+
/** 原地重试时被覆盖的失败消息 id;普通 send 为 null。 */
|
|
156
|
+
replaceMessageId: string | null
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
/** 角色发言的引擎产物。 */
|
|
@@ -177,31 +202,39 @@ export interface FileSearchResult {
|
|
|
177
202
|
error?: string
|
|
178
203
|
}
|
|
179
204
|
|
|
180
|
-
/**
|
|
205
|
+
/** 快照内的会话行(wire 形态:不含 hydrate 用的固定标记与折入水位)。 */
|
|
206
|
+
export type SnapshotSession = Omit<SessionRecord, 'namePinned' | 'topicPinned' | 'constraintsUpToSeq'>
|
|
207
|
+
/** 快照内的消息行(wire 形态:思考全文与摘要不外发)。 */
|
|
208
|
+
export type SnapshotMessage = Omit<MessageRecord, 'reasoningFull' | 'thinkingSummary'>
|
|
209
|
+
|
|
210
|
+
/** 发到客户端的全量快照(wire 形态;各表行由领域记录派生,字段增删由编译器同步)。 */
|
|
181
211
|
export interface Snapshot {
|
|
182
212
|
revision: number
|
|
183
|
-
run:
|
|
213
|
+
run: Omit<RunState, 'stopping' | 'queue' | 'confirmSignal' | 'childProc'>
|
|
184
214
|
lastCreated: LastCreated | null
|
|
185
|
-
groups:
|
|
186
|
-
sessions:
|
|
187
|
-
roles:
|
|
188
|
-
messages:
|
|
215
|
+
groups: GroupRecord[]
|
|
216
|
+
sessions: SnapshotSession[]
|
|
217
|
+
roles: RoleRecord[]
|
|
218
|
+
messages: SnapshotMessage[]
|
|
189
219
|
error?: string
|
|
190
220
|
}
|
|
191
221
|
|
|
192
|
-
/** mutate
|
|
193
|
-
export
|
|
194
|
-
op: string
|
|
195
|
-
groupId?: string
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
topic?: string
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
enabled?: boolean
|
|
204
|
-
}
|
|
222
|
+
/** mutate 动作的参数形态(判别联合:按 op 收窄各分支字段;wire 上字段可缺,消费方各自守卫)。 */
|
|
223
|
+
export type MutateArgs =
|
|
224
|
+
| { op: 'createGroup', name?: string }
|
|
225
|
+
| { op: 'renameGroup', groupId: string, name?: string }
|
|
226
|
+
| { op: 'deleteGroup', groupId: string }
|
|
227
|
+
| { op: 'createSession', groupId: string, name?: string }
|
|
228
|
+
| { op: 'renameSession', sessionId: string, name?: string }
|
|
229
|
+
| { op: 'deleteSession', sessionId: string }
|
|
230
|
+
| { op: 'setTopic', sessionId: string, topic?: string }
|
|
231
|
+
| { op: 'upsertRole', groupId: string, role?: Partial<RoleRecord> }
|
|
232
|
+
| { op: 'deleteRole', roleId: string }
|
|
233
|
+
| { op: 'setRoleEnabled', roleId: string, enabled?: boolean }
|
|
234
|
+
| { op: 'setWorkspaceDir', groupId: string, path?: string }
|
|
235
|
+
| { op: 'setPermissionTier', groupId: string, tier?: string }
|
|
236
|
+
| { op: 'ackFinish' }
|
|
237
|
+
| { op: 'clearMessages', sessionId: string }
|
|
205
238
|
|
|
206
239
|
/** send 动作的参数形态。 */
|
|
207
240
|
export interface SendArgs {
|
package/src/host/api/actions.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
|
|
|
7
7
|
import { rmSync, unlinkSync } from 'node:fs'
|
|
8
|
+
import { isSpeakFailure, repairFailedMessage } from '../../core/errors.ts'
|
|
8
9
|
import type { BrowseResult, EffortOptions, GroupRecord, ModelCatalog, MutateArgs, RoleRecord, SendArgs, Snapshot } from '../../core/types.ts'
|
|
9
10
|
import { asEffort, asNumber, asPermissionTier } from '../../core/types.ts'
|
|
10
11
|
import { PALETTE } from '../state.ts'
|
|
@@ -35,7 +36,8 @@ export function createActions(core: HostState, deps: {
|
|
|
35
36
|
const { touch, snapshot, schedulePersist, dropDirty, appendMessage, runLoop, wakeConfirm, killChild, browse, fileSearch, disposeFileSearch } = deps
|
|
36
37
|
|
|
37
38
|
const mutate = (args: MutateArgs): Snapshot => {
|
|
38
|
-
|
|
39
|
+
// 直接属性别名保持判别联合收窄(调用方 handleAction 已保证 body 为对象)
|
|
40
|
+
const op = args.op
|
|
39
41
|
if (op === 'createGroup') {
|
|
40
42
|
const g: GroupRecord = { id: core.nid('grp'), name: String(args.name || '').trim() || '群组 ' + (groups.size + 1), workspaceDir: '', permissionTier: 'view_only', roleIds: [], sessionIds: [] }
|
|
41
43
|
groups.set(g.id, g)
|
|
@@ -45,14 +47,14 @@ export function createActions(core: HostState, deps: {
|
|
|
45
47
|
schedulePersist({ ledger: true, session: sess.id })
|
|
46
48
|
touch()
|
|
47
49
|
} else if (op === 'renameGroup') {
|
|
48
|
-
const g = groups.get(args.groupId
|
|
50
|
+
const g = groups.get(args.groupId)
|
|
49
51
|
if (g && String(args.name || '').trim()) {
|
|
50
52
|
g.name = String(args.name).trim()
|
|
51
53
|
schedulePersist({ ledger: true })
|
|
52
54
|
touch()
|
|
53
55
|
}
|
|
54
56
|
} else if (op === 'deleteGroup') {
|
|
55
|
-
const g = groups.get(args.groupId
|
|
57
|
+
const g = groups.get(args.groupId)
|
|
56
58
|
if (!g) return { ...snapshot(), error: '群组不存在' }
|
|
57
59
|
if (groups.size <= 1) return { ...snapshot(), error: '至少保留一个群组' }
|
|
58
60
|
if (run.running) {
|
|
@@ -79,7 +81,7 @@ export function createActions(core: HostState, deps: {
|
|
|
79
81
|
schedulePersist({ ledger: true })
|
|
80
82
|
touch()
|
|
81
83
|
} else if (op === 'createSession') {
|
|
82
|
-
const g = groups.get(args.groupId
|
|
84
|
+
const g = groups.get(args.groupId)
|
|
83
85
|
if (!g) return { ...snapshot(), error: '群组不存在' }
|
|
84
86
|
const sess = core.newSession(g.id, String(args.name || '').trim() || undefined)
|
|
85
87
|
g.sessionIds.push(sess.id)
|
|
@@ -87,7 +89,7 @@ export function createActions(core: HostState, deps: {
|
|
|
87
89
|
schedulePersist({ ledger: true, session: sess.id })
|
|
88
90
|
touch()
|
|
89
91
|
} else if (op === 'renameSession') {
|
|
90
|
-
const sess = sessions.get(args.sessionId
|
|
92
|
+
const sess = sessions.get(args.sessionId)
|
|
91
93
|
if (sess && String(args.name || '').trim()) {
|
|
92
94
|
sess.name = String(args.name).trim()
|
|
93
95
|
sess.namePinned = true // 手动编辑 = 隐式固定:自动命名此后跳过名称
|
|
@@ -95,7 +97,7 @@ export function createActions(core: HostState, deps: {
|
|
|
95
97
|
touch()
|
|
96
98
|
}
|
|
97
99
|
} else if (op === 'deleteSession') {
|
|
98
|
-
const sess = sessions.get(args.sessionId
|
|
100
|
+
const sess = sessions.get(args.sessionId)
|
|
99
101
|
if (!sess) return { ...snapshot(), error: '会话不存在' }
|
|
100
102
|
const g = groups.get(sess.groupId)
|
|
101
103
|
if (g && g.sessionIds.length <= 1) return { ...snapshot(), error: '每个群组至少保留一个会话' }
|
|
@@ -114,7 +116,7 @@ export function createActions(core: HostState, deps: {
|
|
|
114
116
|
schedulePersist({ ledger: true })
|
|
115
117
|
touch()
|
|
116
118
|
} else if (op === 'setTopic') {
|
|
117
|
-
const sess = sessions.get(args.sessionId
|
|
119
|
+
const sess = sessions.get(args.sessionId)
|
|
118
120
|
if (sess) {
|
|
119
121
|
sess.topic = String(args.topic || '')
|
|
120
122
|
sess.topicPinned = true // 手动编辑 = 隐式固定:自动整理此后跳过主题
|
|
@@ -122,7 +124,7 @@ export function createActions(core: HostState, deps: {
|
|
|
122
124
|
touch()
|
|
123
125
|
}
|
|
124
126
|
} else if (op === 'upsertRole') {
|
|
125
|
-
const g = groups.get(args.groupId
|
|
127
|
+
const g = groups.get(args.groupId)
|
|
126
128
|
const r = args.role || {}
|
|
127
129
|
if (!g) return { ...snapshot(), error: '群组不存在' }
|
|
128
130
|
if (!r.name || !String(r.name).trim()) return { ...snapshot(), error: '角色名称不能为空' }
|
|
@@ -160,7 +162,7 @@ export function createActions(core: HostState, deps: {
|
|
|
160
162
|
touch()
|
|
161
163
|
}
|
|
162
164
|
} else if (op === 'deleteRole') {
|
|
163
|
-
const r = roles.get(args.roleId
|
|
165
|
+
const r = roles.get(args.roleId)
|
|
164
166
|
if (r) {
|
|
165
167
|
const gid = r.groupId
|
|
166
168
|
const g = groups.get(gid)
|
|
@@ -170,14 +172,14 @@ export function createActions(core: HostState, deps: {
|
|
|
170
172
|
touch()
|
|
171
173
|
}
|
|
172
174
|
} else if (op === 'setRoleEnabled') {
|
|
173
|
-
const r = roles.get(args.roleId
|
|
175
|
+
const r = roles.get(args.roleId)
|
|
174
176
|
if (r) {
|
|
175
177
|
r.enabled = !!args.enabled
|
|
176
178
|
schedulePersist({ roles: r.groupId })
|
|
177
179
|
touch()
|
|
178
180
|
}
|
|
179
181
|
} else if (op === 'setWorkspaceDir') {
|
|
180
|
-
const g = groups.get(args.groupId
|
|
182
|
+
const g = groups.get(args.groupId)
|
|
181
183
|
if (g) {
|
|
182
184
|
g.workspaceDir = String(args.path || '').trim()
|
|
183
185
|
schedulePersist({ workspace: g.id })
|
|
@@ -186,7 +188,7 @@ export function createActions(core: HostState, deps: {
|
|
|
186
188
|
} else if (op === 'setPermissionTier') {
|
|
187
189
|
const tier = asPermissionTier(args.tier)
|
|
188
190
|
if (!tier) return { ...snapshot(), error: '未知权限档位' }
|
|
189
|
-
const g = groups.get(args.groupId
|
|
191
|
+
const g = groups.get(args.groupId)
|
|
190
192
|
if (g) {
|
|
191
193
|
g.permissionTier = tier
|
|
192
194
|
// 降到仅可查看时若该群挂着待确认命令,自动拒绝(安全侧倾斜);
|
|
@@ -205,11 +207,13 @@ export function createActions(core: HostState, deps: {
|
|
|
205
207
|
touch()
|
|
206
208
|
}
|
|
207
209
|
} else if (op === 'clearMessages') {
|
|
208
|
-
const sess = sessions.get(args.sessionId
|
|
210
|
+
const sess = sessions.get(args.sessionId)
|
|
209
211
|
if (sess) {
|
|
210
212
|
if (run.running && run.sessionId === sess.id) return { ...snapshot(), error: '对话进行中,无法清空' }
|
|
211
213
|
for (const mid of sess.messageIds) messages.delete(mid)
|
|
212
214
|
sess.messageIds = []
|
|
215
|
+
sess.constraints = undefined
|
|
216
|
+
sess.constraintsUpToSeq = undefined
|
|
213
217
|
schedulePersist({ session: sess.id })
|
|
214
218
|
touch()
|
|
215
219
|
}
|
|
@@ -243,11 +247,41 @@ export function createActions(core: HostState, deps: {
|
|
|
243
247
|
run.queue = queue
|
|
244
248
|
run.stopping = false
|
|
245
249
|
run.finished = null // 新 run 覆盖旧的「输出完毕」未读标记
|
|
250
|
+
run.replaceMessageId = null
|
|
246
251
|
touch()
|
|
247
252
|
void runLoop(sess).catch((e) => console.error('group-chat run failed', e))
|
|
248
253
|
return { ok: true }
|
|
249
254
|
}
|
|
250
255
|
|
|
256
|
+
/** 对失败卡原地重试:只让该角色再讲一次,成功后覆盖同一条消息。 */
|
|
257
|
+
const retrySpeak = (args: { sessionId?: string, messageId?: string }): { ok: boolean, error?: string } => {
|
|
258
|
+
const sess = sessions.get(String(args && args.sessionId || ''))
|
|
259
|
+
if (!sess) return { ok: false, error: '会话不存在' }
|
|
260
|
+
if (run.running) return { ok: false, error: '已有对话进行中,请先停止' }
|
|
261
|
+
const msg = messages.get(String(args && args.messageId || ''))
|
|
262
|
+
if (!msg || msg.sessionId !== sess.id || !isSpeakFailure(msg)) return { ok: false, error: '没有可重试的失败发言' }
|
|
263
|
+
const g = groups.get(sess.groupId)
|
|
264
|
+
if (!g) return { ok: false, error: '群组不存在' }
|
|
265
|
+
const groupRoles = g.roleIds.map((id) => roles.get(id)).filter((r): r is RoleRecord => Boolean(r))
|
|
266
|
+
if (repairFailedMessage(msg, groupRoles)) {
|
|
267
|
+
schedulePersist({ session: sess.id })
|
|
268
|
+
touch()
|
|
269
|
+
}
|
|
270
|
+
const roleId = msg.failedRoleId || (msg.speaker !== 'user' && msg.speaker !== 'system' ? msg.speaker : '')
|
|
271
|
+
const role = roleId ? roles.get(roleId) : undefined
|
|
272
|
+
if (!role || role.groupId !== sess.groupId) return { ok: false, error: '失败角色已不存在,无法重试' }
|
|
273
|
+
if (!role.enabled) return { ok: false, error: '该角色已停用,无法重试' }
|
|
274
|
+
run.running = true
|
|
275
|
+
run.sessionId = sess.id
|
|
276
|
+
run.queue = [role.id]
|
|
277
|
+
run.stopping = false
|
|
278
|
+
run.finished = null
|
|
279
|
+
run.replaceMessageId = msg.id
|
|
280
|
+
touch()
|
|
281
|
+
void runLoop(sess, { replaceMessageId: msg.id }).catch((e) => console.error('group-chat retry failed', e))
|
|
282
|
+
return { ok: true }
|
|
283
|
+
}
|
|
284
|
+
|
|
251
285
|
const stop = (args: { sessionId?: string }): { ok: boolean } => {
|
|
252
286
|
if (run.running && (!args || !args.sessionId || run.sessionId === args.sessionId)) {
|
|
253
287
|
run.stopping = true
|
|
@@ -322,6 +356,7 @@ export function createActions(core: HostState, deps: {
|
|
|
322
356
|
const kind = body && body.kind
|
|
323
357
|
if (kind === 'mutate') return { ok: true, snapshot: mutate(body as unknown as MutateArgs), lastCreated: core.lastCreated }
|
|
324
358
|
if (kind === 'send') return send(body as unknown as SendArgs)
|
|
359
|
+
if (kind === 'retrySpeak') return retrySpeak(body as { sessionId?: string, messageId?: string })
|
|
325
360
|
if (kind === 'stop') return stop(body as { sessionId?: string })
|
|
326
361
|
if (kind === 'confirmCommand') return confirmCommand(body as { toolCallId?: string, allow?: boolean })
|
|
327
362
|
if (kind === 'models') return { ok: true, ...(await models()) }
|
package/src/host/broadcast.ts
CHANGED
|
@@ -41,12 +41,12 @@ export function createBroadcast(core: HostState): Broadcast {
|
|
|
41
41
|
|
|
42
42
|
const snapshot = (): Snapshot => ({
|
|
43
43
|
revision: core.revision,
|
|
44
|
-
run: { running: core.run.running, sessionId: core.run.sessionId, currentRoleId: core.run.currentRoleId, partial: core.run.partial, partialReasoning: core.run.partialReasoning, pendingConfirm: core.run.pendingConfirm, finished: core.run.finished },
|
|
44
|
+
run: { running: core.run.running, sessionId: core.run.sessionId, currentRoleId: core.run.currentRoleId, partial: core.run.partial, partialReasoning: core.run.partialReasoning, pendingConfirm: core.run.pendingConfirm, finished: core.run.finished, replaceMessageId: core.run.replaceMessageId },
|
|
45
45
|
lastCreated: core.lastCreated,
|
|
46
46
|
groups: [...core.groups.values()].map((g) => ({ id: g.id, name: g.name, workspaceDir: g.workspaceDir, permissionTier: g.permissionTier, roleIds: g.roleIds.slice(), sessionIds: g.sessionIds.slice() })),
|
|
47
|
-
sessions: [...core.sessions.values()].map((s) => ({ id: s.id, groupId: s.groupId, name: s.name, topic: s.topic, messageIds: s.messageIds.slice(), createdAt: s.createdAt })),
|
|
47
|
+
sessions: [...core.sessions.values()].map((s) => ({ id: s.id, groupId: s.groupId, name: s.name, topic: s.topic, ...(s.constraints && s.constraints.length ? { constraints: s.constraints } : {}), messageIds: s.messageIds.slice(), createdAt: s.createdAt })),
|
|
48
48
|
roles: [...core.roles.values()].map((r) => ({ id: r.id, groupId: r.groupId, name: r.name, color: r.color, persona: r.persona, provider: r.provider, model: r.model, temperature: r.temperature, reasoningEffort: r.reasoningEffort, enabled: r.enabled, thinking: r.thinking === true })),
|
|
49
|
-
messages: [...core.messages.values()].map((m) => ({ id: m.id, sessionId: m.sessionId, seq: m.seq, speaker: m.speaker, text: m.text, reasoning: m.reasoning, model: m.model, error: m.error, toolCalls: m.toolCalls, ts: m.ts })),
|
|
49
|
+
messages: [...core.messages.values()].map((m) => ({ id: m.id, sessionId: m.sessionId, seq: m.seq, speaker: m.speaker, text: m.text, reasoning: m.reasoning, model: m.model, error: m.error, failedRoleId: m.failedRoleId, toolCalls: m.toolCalls, ts: m.ts })),
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
return {
|