@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
package/src/client/lib/styles.ts
CHANGED
|
@@ -62,7 +62,32 @@ export const CSS = [
|
|
|
62
62
|
'.dsgc-seambtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l3,rgba(128,128,128,.4))}',
|
|
63
63
|
'.dsgc-seambtn.left{left:0;border-left:none;border-radius:0 8px 8px 0;box-shadow:3px 0 10px rgba(0,0,0,.07)}',
|
|
64
64
|
'.dsgc-seambtn.right{right:0;border-right:none;border-radius:8px 0 0 8px;box-shadow:-3px 0 10px rgba(0,0,0,.07)}',
|
|
65
|
-
'.dsgc-chathead{display:flex;
|
|
65
|
+
'.dsgc-chathead{display:flex;flex-direction:column;gap:6px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(128,128,128,.2))}',
|
|
66
|
+
'.dsgc-chathead-row{display:flex;gap:10px;align-items:center;min-width:0}',
|
|
67
|
+
'.dsgc-constraints{align-self:center;box-sizing:border-box;width:76%;max-width:76%;display:flex;flex-direction:column;gap:8px;min-width:0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));border-radius:12px;animation:dsgc-fade-in .18s ease-out}',
|
|
68
|
+
'.dsgc-chead{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
69
|
+
'.dsgc-ctitle{margin:0;font:inherit;font-size:12px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,inherit)}',
|
|
70
|
+
'.dsgc-cdesc{margin:0;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
71
|
+
'.dsgc-fold{display:grid;grid-template-rows:0fr;transition:grid-template-rows .16s ease-in}',
|
|
72
|
+
'.dsgc-fold.open{grid-template-rows:1fr;transition:grid-template-rows .22s cubic-bezier(.16,1,.3,1)}',
|
|
73
|
+
'.dsgc-fold-inner{min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:2px;opacity:0;transition:opacity .12s ease-in}',
|
|
74
|
+
'.dsgc-fold.open .dsgc-fold-inner{opacity:1;transition:opacity .18s cubic-bezier(.16,1,.3,1)}',
|
|
75
|
+
'.dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.18));position:relative;min-width:0}',
|
|
76
|
+
'.dsgc-clip::before,.dsgc-clip::after{content:"";position:absolute;left:0;right:0;height:14px;pointer-events:none;z-index:1;opacity:0;transition:opacity .12s}',
|
|
77
|
+
'.dsgc-clip::before{top:0;background:linear-gradient(to bottom,var(--dsgc-clip-from),transparent)}',
|
|
78
|
+
'.dsgc-clip::after{bottom:0;background:linear-gradient(to top,var(--dsgc-clip-from),transparent)}',
|
|
79
|
+
'.dsgc-clip.can-up::before,.dsgc-clip.can-down::after{opacity:1}',
|
|
80
|
+
'.dsgc-clip-scroll{min-width:0;overflow-y:auto;overscroll-behavior:contain;overflow-anchor:none;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
81
|
+
'.dsgc-clist{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
82
|
+
'.dsgc-constraint{display:flex;gap:8px;align-items:flex-start;min-width:0;font-size:11.5px;line-height:1.5}',
|
|
83
|
+
'.dsgc-ckind{flex:none;font-weight:500}',
|
|
84
|
+
'.dsgc-ckind.decided{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
85
|
+
'.dsgc-ckind.rejected{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
86
|
+
'.dsgc-ckind.open{color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
87
|
+
'.dsgc-ctext{min-width:0;color:var(--dsw-alias-label-primary,inherit);overflow-wrap:break-word}',
|
|
88
|
+
'.dsgc-cmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}',
|
|
89
|
+
'.dsgc-cmore:hover{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
90
|
+
'.dsgc-cmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
66
91
|
'.dsgc-sess-title{flex:none;font-weight:600;font-size:14px;color:var(--dsw-alias-label-primary,inherit);max-width:30%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
67
92
|
'.dsgc-topic{flex:1;background:transparent;border:none;border-bottom:1px solid transparent;padding:2px;min-height:20px;color:var(--dsw-alias-label-secondary,inherit);font:inherit;outline:none;min-width:0;transition:color .12s,border-color .12s}',
|
|
68
93
|
'.dsgc-topic:hover{border-bottom-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}',
|
|
@@ -99,7 +124,8 @@ export const CSS = [
|
|
|
99
124
|
'.dsgc-think .dsgc-thinkrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}',
|
|
100
125
|
'.dsgc-think .dsgc-thinkrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}',
|
|
101
126
|
'.dsgc-think .dsgc-thinksummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:44ch;font-size:11.5px;margin-left:6px}',
|
|
102
|
-
'.dsgc-think .dsgc-
|
|
127
|
+
'.dsgc-think .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}',
|
|
128
|
+
'.dsgc-think .dsgc-thinkbody{font-size:12.5px;line-height:1.7;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}',
|
|
103
129
|
/* 首 delta 前的「思考中」占位行:ThinkRow 同语言(思考图标 + 次要色 12px 小字)
|
|
104
130
|
+ 三点交错呼吸动画——深度思考模型首字节可能等数秒到数十秒,空白气泡会被
|
|
105
131
|
感知为卡死;delta 到达后被真实思考行/正文自然替换 */
|
|
@@ -111,17 +137,42 @@ export const CSS = [
|
|
|
111
137
|
'.dsgc-pendingdots i:nth-child(3){animation-delay:.4s}',
|
|
112
138
|
'@keyframes dsgc-dot-breathe{0%,100%{opacity:.25}50%{opacity:1}}',
|
|
113
139
|
'.dsgc-sysmsg{align-self:center;font-size:11.5px;color:var(--dsw-alias-label-secondary,inherit);background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.1));border-radius:999px;padding:2px 10px;text-align:center;max-width:90%;animation:dsgc-sys-in .24s ease-out}',
|
|
140
|
+
/* 消息操作条:气泡外下方;角色左齐、自己右齐。占位常在,悬停显现;失败卡 always 常驻 */
|
|
141
|
+
'.dsgc-msgops{display:inline-flex;align-items:center;gap:2px;margin-top:2px;min-height:24px;visibility:hidden;opacity:0;transition:opacity .12s,visibility .12s}',
|
|
142
|
+
'.dsgc-msg:hover .dsgc-msgops,.dsgc-msg:focus-within .dsgc-msgops,.dsgc-msgops.always{visibility:visible;opacity:1}',
|
|
143
|
+
'@media (hover:none){.dsgc-msg .dsgc-msgops{visibility:visible;opacity:1}}',
|
|
144
|
+
'.dsgc-msg.mine .dsgc-msgops{justify-content:flex-end}',
|
|
145
|
+
'.dsgc-msgop{border:none;background:none;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer;padding:3px 6px;border-radius:6px;display:inline-flex;align-items:center;gap:4px;font:inherit;font-size:11.5px;line-height:16px;transition:background-color .12s,color .12s}',
|
|
146
|
+
'.dsgc-msgop svg{flex:none}',
|
|
147
|
+
'.dsgc-msgop:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}',
|
|
148
|
+
'.dsgc-msgop:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
149
|
+
'.dsgc-msgop.done{color:var(--dsw-alias-state-success-primary,#30a46c)}',
|
|
150
|
+
'.dsgc-msgop.retry{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
151
|
+
'.dsgc-msgop.retry:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 10%,transparent);color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
152
|
+
'.dsgc-msgop:disabled{opacity:.4;cursor:default}',
|
|
153
|
+
/* 失败卡:角色消息形态,人话标题 + 可展开原文;不用左侧色条,避免 craft-floor 禁令 */
|
|
154
|
+
'.dsgc-fail{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 8%,var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06)));border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 28%,var(--dsw-alias-border-l1,rgba(128,128,128,.15)));border-radius:12px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0;animation:dsgc-fade-in .18s ease-out}',
|
|
155
|
+
'.dsgc-failhead{display:flex;align-items:flex-start;gap:8px;min-width:0}',
|
|
156
|
+
'.dsgc-failicon{flex:none;width:22px;height:22px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-state-error-primary,#e5484d);background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 12%,transparent);margin-top:1px}',
|
|
157
|
+
'.dsgc-failcopy{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}',
|
|
158
|
+
'.dsgc-failtitle{font-size:13px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary,inherit)}',
|
|
159
|
+
'.dsgc-faildetail{font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,inherit);overflow-wrap:anywhere}',
|
|
160
|
+
'.dsgc-failmore{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary,inherit);cursor:pointer}',
|
|
161
|
+
'.dsgc-failmore:hover{color:var(--dsw-alias-label-secondary,inherit)}',
|
|
162
|
+
'.dsgc-failmore:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
163
|
+
'.dsgc-fail .dsgc-clip{--dsgc-clip-from:var(--dsw-alias-bg-layer-2,rgba(128,128,128,.12));border-radius:8px}',
|
|
164
|
+
'.dsgc-failraw{margin:0;font-size:11.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-secondary,inherit)}',
|
|
114
165
|
/* ===== 工具调用行 / 确认卡片(对标思考折叠语言) ===== */
|
|
115
166
|
'.dsgc-tool{margin:2px 0}',
|
|
116
167
|
'.dsgc-tool .dsgc-toolrow{font-size:12px;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:3px 8px;transition:background-color .12s}',
|
|
117
168
|
'.dsgc-tool .dsgc-toolrow:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}',
|
|
118
169
|
'.dsgc-tool .dsgc-toolsummary{color:var(--dsw-alias-label-tertiary,inherit);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:40ch;font-size:11.5px;margin-left:6px}',
|
|
119
|
-
'.dsgc-tool .dsgc-
|
|
170
|
+
'.dsgc-tool .dsgc-clip{margin:2px 0 4px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l2,rgba(128,128,128,.25))}',
|
|
171
|
+
'.dsgc-tool .dsgc-toolbody{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,inherit);white-space:pre-wrap;word-break:break-word}',
|
|
120
172
|
'.dsgc-confirm{border:1px solid var(--dsw-alias-state-business-primary,#4f6ef7);border-radius:12px;background:var(--dsw-alias-bg-layer-3,rgba(128,128,128,.06));padding:10px 12px;display:flex;flex-direction:column;gap:8px;max-width:76%;animation:dsgc-fade-in .18s ease-out}',
|
|
121
173
|
'.dsgc-confirmtitle{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,inherit);display:flex;align-items:center;gap:6px}',
|
|
122
174
|
'.dsgc-confirmcmd{font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:8px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);max-height:240px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
123
175
|
'.dsgc-confirmops{display:flex;gap:8px;justify-content:flex-end}',
|
|
124
|
-
'.dsgc-sysmsg.err{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
125
176
|
/* 清空确认弹窗内的说明列表 */
|
|
126
177
|
'.dsgc-clearnotes{margin:0;padding-left:18px;font-size:13px;line-height:1.8;color:var(--dsw-alias-label-secondary,inherit);display:flex;flex-direction:column;gap:2px}',
|
|
127
178
|
'.dsgc-err{font-size:12px;color:var(--dsw-alias-state-error-primary,#e5484d);animation:dsgc-fade-in .18s ease-out}',
|
|
@@ -152,8 +203,11 @@ export const CSS = [
|
|
|
152
203
|
'.dsgc-card .dsgc-sendrow{padding:0 12px 10px}',
|
|
153
204
|
'.dsgc-stopbtn{background:var(--dsw-alias-state-error-primary,#e5484d);border-color:transparent;color:#fff;font-weight:600}',
|
|
154
205
|
'.dsgc-stopbtn:hover:not(:disabled){filter:brightness(1.08);color:#fff}',
|
|
155
|
-
|
|
156
|
-
|
|
206
|
+
/* 标签固定在左;芯片在右侧独立换行,后续行与首行芯片左缘对齐(不折到「参与角色」下面)。
|
|
207
|
+
单行 24px;角色过多时随内容增高,避免固定高度把后续行溢出叠到下方输入卡上 */
|
|
208
|
+
'.dsgc-parts{display:flex;align-items:flex-start;gap:6px;min-height:24px}',
|
|
209
|
+
'.dsgc-partslabel{font-size:12px;line-height:24px;color:var(--dsw-alias-label-tertiary,inherit);flex:none}',
|
|
210
|
+
'.dsgc-partlist{display:flex;flex-wrap:wrap;align-items:center;gap:6px;row-gap:4px;flex:1;min-width:0}',
|
|
157
211
|
'.dsgc-partchip{display:inline-flex;gap:6px;align-items:center;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:999px;padding:2px 10px;font-size:12px;line-height:1.5;cursor:pointer;transition:background-color .12s,color .12s,border-color .12s;font-family:inherit}',
|
|
158
212
|
'.dsgc-partchip:hover{color:var(--dsw-alias-label-primary,inherit)}',
|
|
159
213
|
'.dsgc-partchip.on{background:var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.18));color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,rgba(128,128,128,.3))}',
|
|
@@ -176,6 +230,12 @@ export const CSS = [
|
|
|
176
230
|
'.dsgc-rounds .dsgc-roundbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.15));color:var(--dsw-alias-label-primary,inherit)}',
|
|
177
231
|
'.dsgc-rounds .dsgc-roundbtn:disabled{opacity:.4;cursor:default}',
|
|
178
232
|
'.dsgc-rounds .dsgc-roundnum{width:26px;text-align:center;font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
|
|
233
|
+
/* hover 气泡:对齐宿主 Tooltip.module.css,portal 到 body 以躲开 container-type */
|
|
234
|
+
'.dsgc-hovertip{position:fixed;z-index:100;width:max-content;max-width:50vw;padding:3px 7px;border-radius:8px;background:var(--dsw-alias-tooltip-bg,#1f1f1f);color:var(--dsw-static-neutral-bluish-00,#fff);font-size:13px;line-height:20px;white-space:pre-line;overflow-wrap:break-word;pointer-events:none;animation:dsgc-hovertip-in 150ms var(--ds-ease-in-out,ease)}',
|
|
235
|
+
'.dsgc-hovertip[data-side="right"]{transform:translateY(-50%)}',
|
|
236
|
+
'.dsgc-hovertip[data-side="top"]{transform:translate(-50%,-100%)}',
|
|
237
|
+
'.dsgc-hovertip[data-side="bottom"]{transform:translateX(-50%)}',
|
|
238
|
+
'@keyframes dsgc-hovertip-in{from{opacity:0}}',
|
|
179
239
|
/* ===== @成员 / @文件弹层 ===== */
|
|
180
240
|
'.dsgc-mentionwrap{position:relative}',
|
|
181
241
|
'.dsgc-mention{animation:dsgc-pop-in .16s cubic-bezier(.16,1,.3,1);position:absolute;left:0;right:0;bottom:calc(100% + 6px);background:var(--dsw-alias-bg-layer-3,#fff);border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));border-radius:10px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.18));max-height:220px;overflow-y:auto;padding:4px;z-index:30;display:flex;flex-direction:column;gap:2px;scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,rgba(128,128,128,.35)) transparent}',
|
|
@@ -214,10 +274,12 @@ export const CSS = [
|
|
|
214
274
|
'.dsgc-rolename{font-weight:600;font-size:13px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary,inherit)}',
|
|
215
275
|
'.dsgc-role.off .dsgc-rolename{color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
216
276
|
'.dsgc-rolepersona{font-size:11.5px;color:var(--dsw-alias-label-tertiary,inherit);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:0}',
|
|
217
|
-
'.dsgc-
|
|
218
|
-
'.dsgc-
|
|
277
|
+
'.dsgc-rolemeta{display:flex;align-items:center;gap:6px;min-width:0;flex:1}',
|
|
278
|
+
'.dsgc-rolemodel{min-width:0;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,.12));border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px;color:var(--dsw-alias-label-secondary,inherit)}',
|
|
279
|
+
'.dsgc-rolethink{display:inline-flex;align-items:center;flex:none;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
280
|
+
'.dsgc-roleops{display:flex;gap:2px;flex:none;margin-left:auto;opacity:0;transition:opacity .12s}',
|
|
219
281
|
'.dsgc-role:hover .dsgc-roleops,.dsgc-role:focus-within .dsgc-roleops{opacity:1}',
|
|
220
|
-
'.dsgc-rolemenu{display:flex;align-items:center;
|
|
282
|
+
'.dsgc-rolemenu{display:flex;align-items:center;gap:8px}',
|
|
221
283
|
/* ===== 表单与控件(角色抽屉内) ===== */
|
|
222
284
|
'.dsgc-field{display:flex;flex-direction:column;gap:5px}',
|
|
223
285
|
'.dsgc-field>label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary,inherit)}',
|
|
@@ -239,7 +301,6 @@ export const CSS = [
|
|
|
239
301
|
'@keyframes dsgc-drawer-out{to{transform:translateX(32px);opacity:0}}',
|
|
240
302
|
'@keyframes dsgc-msg-in{from{opacity:0;transform:translateY(6px)}}',
|
|
241
303
|
'@keyframes dsgc-sys-in{from{opacity:0;transform:scale(.96)}}',
|
|
242
|
-
'@keyframes dsgc-think-in{from{opacity:0;transform:translateY(-3px)}}',
|
|
243
304
|
'@keyframes dsgc-pop-in{from{opacity:0;transform:translateY(4px)}}',
|
|
244
305
|
'@keyframes dsgc-rise-in{from{opacity:0;transform:translate(-50%,6px)}}',
|
|
245
306
|
'@keyframes dsgc-fade-in{from{opacity:0}}',
|
|
@@ -269,7 +330,7 @@ export const CSS = [
|
|
|
269
330
|
'.dsgc-partchip:focus-visible,.dsgc-mentionitem:focus-visible,.dsgc-dot:focus-visible,.dsgc-opbtn:focus-visible,.dsgc-twist:focus-visible,.dsgc-addsess:focus-visible,.dsgc-grow-row:focus-visible,.dsgc-sess-row:focus-visible,.dsgc-rename:focus-visible,.dsgc-fbrow:focus-visible,.dsgc-role:focus-visible,.dsgc-roundbtn:focus-visible,.dsgc-tobtn:focus-visible,.dsgc-seambtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary,#4f6ef7));outline-offset:1px}',
|
|
270
331
|
/* ===== 窄容器:右栏覆盖式呈现 ===== */
|
|
271
332
|
'@container (max-width: 880px){.dsgc-aside{position:absolute;top:0;right:0;bottom:0;z-index:15;width:min(304px,88%);box-shadow:-12px 0 32px rgba(0,0,0,.16);border-left:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.3));transition:transform .3s cubic-bezier(.16,1,.3,1),opacity .2s ease}.dsgc-aside.closed{width:min(304px,88%);padding-left:12px;padding-right:12px;border-left-width:1px;transform:translateX(calc(100% + 14px));opacity:0;transition:transform .26s ease-in,opacity .18s ease-in,visibility 0s .24s}.dsgc-seambtn.right:not(.closed){right:min(304px,88%)}}',
|
|
272
|
-
'@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}}',
|
|
333
|
+
'@container (max-width: 640px){.dsgc-nav{width:200px}.dsgc-msgbody{max-width:88%}.dsgc-constraints{width:88%;max-width:88%}}',
|
|
273
334
|
/* ===== 侧边栏入口对齐(外壳将插槽内容包在 panelGlyph span 内,必须用后代 :has) ===== */
|
|
274
335
|
'[class*="panelList"]:has(.dsgc-entryOverlay){margin-top:4px}',
|
|
275
336
|
'[class*="panelRow"]:has(.dsgc-entryOverlay){position:relative;box-sizing:border-box;height:36px;min-height:36px;padding:0 10px;font-size:13px;transition:background-color .12s,color .12s}',
|
|
@@ -292,7 +353,7 @@ export const CSS = [
|
|
|
292
353
|
'.dsgc-entryIcon svg{width:18px;height:18px;display:block}',
|
|
293
354
|
'[data-sidebar-collapsed] .dsgc-entryOverlay,[class*="collapsed"] .dsgc-entryOverlay{justify-content:center;padding:0}',
|
|
294
355
|
'[data-sidebar-collapsed] [class*="panelRow"]:has(.dsgc-entryOverlay),[class*="collapsed"] [class*="panelRow"]:has(.dsgc-entryOverlay){margin:0 auto}',
|
|
295
|
-
'@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,[class*="panelRow"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-
|
|
356
|
+
'@media (prefers-reduced-motion:reduce){.dsgc-dot,.dsgc-partchip,.dsgc-mentionitem,.dsgc-grow-row,.dsgc-sess-row,.dsgc-opbtn,.dsgc-addsess,.dsgc-topic,.dsgc-role,.dsgc-rename,.dsgc-input,.dsgc-select,.dsgc-textarea,.dsgc-fbrow,.dsgc-twist svg,.dsgc-roleops,.dsgc-roundbtn,.dsgc-permtrigger,.dsgc-permchevron,.dsgc-tobtn,.dsgc-seambtn,.dsgc-aside,.dsgc-aside.closed,.dsgc-nav,.dsgc-nav.closed,.dsgc-clearbtn,.dsgc-cmore,.dsgc-msgops,.dsgc-msgop,.dsgc-failmore,.dsgc-clip::before,.dsgc-clip::after,.dsgc-fold,.dsgc-fold-inner,.dsgc-fold.open,.dsgc-fold.open .dsgc-fold-inner,[class*="panelRow"]:has(.dsgc-entryOverlay){transition:none}.dsgc-dot:hover{transform:none}.dsgc-drawer,.dsgc-msg,.dsgc-sysmsg,.dsgc-mention,.dsgc-tobottom,.dsgc-err,.dsgc-empty,.dsgc-fb,.dsgc-msg.live .dsgc-avatar,.dsgc-loading svg,.dsgc-pendingdots i,.dsgc-typing,.dsgc-constraints,.dsgc-hovertip,.dsgc-fail{animation:none}.dsgc-typing{background-position:0 0;background-size:100% 100%}}',
|
|
296
357
|
/* ===== 设置页(同一卡片语言) ===== */
|
|
297
358
|
'.dgcs-page{display:flex;flex-direction:column;gap:14px;padding:4px 0}',
|
|
298
359
|
'.dgcs-head{display:flex;flex-direction:column;gap:6px}',
|
|
@@ -309,7 +370,7 @@ export function injectStyles(): void {
|
|
|
309
370
|
if (typeof document === 'undefined') return
|
|
310
371
|
if (document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']') === null) {
|
|
311
372
|
const tag = document.createElement('style')
|
|
312
|
-
tag.dataset.plugin = 'dsh-group-chat'
|
|
373
|
+
tag.dataset.plugin = '@roaming-ai/dsh-group-chat'
|
|
313
374
|
tag.dataset.pluginCss = TAG_ID
|
|
314
375
|
tag.textContent = CSS
|
|
315
376
|
document.head.appendChild(tag)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Platform modules (react-dom / client) have no @types in this plugin. */
|
|
2
2
|
declare module 'react-dom' {
|
|
3
|
+
import type { ReactNode } from 'react'
|
|
3
4
|
export function flushSync(fn: () => void): void
|
|
5
|
+
export function createPortal(children: ReactNode, container: Element | DocumentFragment): ReactNode
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
declare module 'react-dom/client' {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话约束备忘纯逻辑:滑动 40 条窗口、挤出集、压行、临时原文、解析与截断。
|
|
3
|
+
* @module dsh-group-chat/core/constraints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TRANSCRIPT_TOOL_SUMMARY } from './tools.ts'
|
|
7
|
+
import { asConstraintKind, type MessageRecord, type SessionConstraint, type SessionRecord } from './types.ts'
|
|
8
|
+
|
|
9
|
+
/** 当场原文窗口(含系统行)。 */
|
|
10
|
+
export const WINDOW_SIZE = 40
|
|
11
|
+
/** 折叠失败时临时原文条数上限。 */
|
|
12
|
+
export const TEMP_MAX_MESSAGES = 20
|
|
13
|
+
/** 折叠失败时临时原文总长上限。 */
|
|
14
|
+
export const TEMP_MAX_CHARS = 16000
|
|
15
|
+
/** 单轮折叠最多消耗的挤出条数(含系统行;超出留待下一轮)。 */
|
|
16
|
+
export const FOLD_MAX_MESSAGES = 40
|
|
17
|
+
/** 单轮折叠输入总长上限(格式化后)。 */
|
|
18
|
+
export const FOLD_MAX_CHARS = 16000
|
|
19
|
+
/** 备忘条数上限。 */
|
|
20
|
+
export const CONSTRAINT_MAX_ITEMS = 12
|
|
21
|
+
/** 备忘总长上限(条目前缀+正文)。 */
|
|
22
|
+
export const CONSTRAINT_MAX_CHARS = 1200
|
|
23
|
+
/** 单条备忘正文上限。 */
|
|
24
|
+
export const CONSTRAINT_ITEM_MAX_CHARS = 160
|
|
25
|
+
|
|
26
|
+
export const KIND_LABEL: Record<SessionConstraint['kind'], string> = {
|
|
27
|
+
decided: '已定',
|
|
28
|
+
rejected: '否决',
|
|
29
|
+
open: '未决',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 已折入水位(缺省 0)。 */
|
|
33
|
+
export function constraintsWatermark(sess: SessionRecord): number {
|
|
34
|
+
return typeof sess.constraintsUpToSeq === 'number' && sess.constraintsUpToSeq > 0 ? sess.constraintsUpToSeq : 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 重试:只取失败卡之前的时间线;untilId 不在列表则原样。 */
|
|
38
|
+
export function prefixIds(ids: string[], untilId?: string): string[] {
|
|
39
|
+
if (!untilId) return ids
|
|
40
|
+
const i = ids.indexOf(untilId)
|
|
41
|
+
return i >= 0 ? ids.slice(0, i) : ids
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 新挤出:seq > 水位 且不在最近 40 条。只扫窗口外前缀(旧→新)。
|
|
46
|
+
* untilId:重试时把窗口截到该消息之前,不带上后面已经发生的发言。
|
|
47
|
+
*/
|
|
48
|
+
export function squeezedMessages(
|
|
49
|
+
messages: Map<string, MessageRecord>,
|
|
50
|
+
sess: SessionRecord,
|
|
51
|
+
untilId?: string,
|
|
52
|
+
): MessageRecord[] {
|
|
53
|
+
const ids = prefixIds(sess.messageIds, untilId)
|
|
54
|
+
if (ids.length <= WINDOW_SIZE) return []
|
|
55
|
+
const end = ids.length - WINDOW_SIZE
|
|
56
|
+
const upTo = constraintsWatermark(sess)
|
|
57
|
+
const last = messages.get(ids[end - 1])
|
|
58
|
+
if (last && last.seq <= upTo) return []
|
|
59
|
+
const out: MessageRecord[] = []
|
|
60
|
+
for (let i = 0; i < end; i++) {
|
|
61
|
+
const m = messages.get(ids[i])
|
|
62
|
+
if (!m || m.seq <= upTo) continue
|
|
63
|
+
out.push(m)
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 说话人展示名(折叠输入 / transcript 共用)。 */
|
|
69
|
+
export function speakerLabel(speaker: string, roleName?: string): string {
|
|
70
|
+
if (speaker === 'user') return '用户'
|
|
71
|
+
if (speaker === 'system') return '系统'
|
|
72
|
+
return roleName || '成员'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 单条消息压成 transcript 行(正文 8k + 工具一行摘要)。 */
|
|
76
|
+
export function formatTranscriptLine(m: MessageRecord, name: string): string {
|
|
77
|
+
let text = m.text || ''
|
|
78
|
+
if (text.length > 8000) text = text.slice(0, 8000) + '…(已截断)'
|
|
79
|
+
let line = '【' + name + '】' + text
|
|
80
|
+
if (Array.isArray(m.toolCalls)) {
|
|
81
|
+
for (const c of m.toolCalls) {
|
|
82
|
+
if (!c || typeof c.tool !== 'string') continue
|
|
83
|
+
let brief = ''
|
|
84
|
+
try {
|
|
85
|
+
brief = JSON.stringify(c.args) || ''
|
|
86
|
+
} catch {
|
|
87
|
+
brief = ''
|
|
88
|
+
}
|
|
89
|
+
if (brief.length > 60) brief = brief.slice(0, 60) + '…'
|
|
90
|
+
const st = c.status === 'ok' ? '成功' : c.status === 'denied' ? '用户拒绝' : '失败'
|
|
91
|
+
let ob = String(c.output || '')
|
|
92
|
+
if (ob.length > TRANSCRIPT_TOOL_SUMMARY) ob = ob.slice(0, TRANSCRIPT_TOOL_SUMMARY) + '…'
|
|
93
|
+
line += '\n [工具] ' + c.tool + ' ' + brief + ' → ' + st + (ob ? '(' + ob.replace(/\s+/g, ' ') + ')' : '')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return line
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 单轮折叠消耗前缀:从最旧挤出起,最多 40 条 / 16k;系统行与失败卡计入消耗但不进模型。
|
|
101
|
+
* 水位只能推到 consumed 的 max seq,剩余留待下一轮。
|
|
102
|
+
*/
|
|
103
|
+
export function takeFoldBatch(squeezed: MessageRecord[], nameOf: (m: MessageRecord) => string): {
|
|
104
|
+
consumed: MessageRecord[]
|
|
105
|
+
lines: string[]
|
|
106
|
+
hasUser: boolean
|
|
107
|
+
allSystem: boolean
|
|
108
|
+
} {
|
|
109
|
+
const consumed: MessageRecord[] = []
|
|
110
|
+
const lines: string[] = []
|
|
111
|
+
let hasUser = false
|
|
112
|
+
let chars = 0
|
|
113
|
+
for (const m of squeezed) {
|
|
114
|
+
if (m.speaker === 'system' || m.error) {
|
|
115
|
+
consumed.push(m)
|
|
116
|
+
if (consumed.length >= FOLD_MAX_MESSAGES) break
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const line = formatTranscriptLine(m, nameOf(m))
|
|
120
|
+
const extra = line.length + (lines.length ? 2 : 0)
|
|
121
|
+
if (lines.length > 0 && chars + extra > FOLD_MAX_CHARS) break
|
|
122
|
+
if (m.speaker === 'user') hasUser = true
|
|
123
|
+
lines.push(line)
|
|
124
|
+
chars += extra
|
|
125
|
+
consumed.push(m)
|
|
126
|
+
if (consumed.length >= FOLD_MAX_MESSAGES) break
|
|
127
|
+
}
|
|
128
|
+
return { consumed, lines, hasUser, allSystem: lines.length === 0 }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 未折入的挤出原文(失败缓冲):只格式化最近 20 条,再按 16k 从最旧往下丢。
|
|
133
|
+
*/
|
|
134
|
+
export function tempTranscript(squeezed: MessageRecord[], nameOf: (m: MessageRecord) => string): string {
|
|
135
|
+
if (!squeezed.length) return ''
|
|
136
|
+
const usable = squeezed.filter((m) => m.speaker !== 'system' && !m.error)
|
|
137
|
+
if (!usable.length) return ''
|
|
138
|
+
const batch = usable.length > TEMP_MAX_MESSAGES ? usable.slice(-TEMP_MAX_MESSAGES) : usable
|
|
139
|
+
const lines = batch.map((m) => formatTranscriptLine(m, nameOf(m)))
|
|
140
|
+
let start = 0
|
|
141
|
+
let total = lines[0] ? lines[0].length : 0
|
|
142
|
+
for (let i = 1; i < lines.length; i++) {
|
|
143
|
+
total += 2 + lines[i].length
|
|
144
|
+
}
|
|
145
|
+
while (start < lines.length - 1 && total > TEMP_MAX_CHARS) {
|
|
146
|
+
total -= lines[start].length + 2
|
|
147
|
+
start++
|
|
148
|
+
}
|
|
149
|
+
let block = lines.slice(start).join('\n\n')
|
|
150
|
+
if (block.length > TEMP_MAX_CHARS) block = block.slice(0, TEMP_MAX_CHARS) + '…(已截断)'
|
|
151
|
+
return block
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 水位 = 本批挤出的 max(seq);空批为 0。 */
|
|
155
|
+
export function squeezedMaxSeq(squeezed: MessageRecord[]): number {
|
|
156
|
+
let max = 0
|
|
157
|
+
for (const m of squeezed) if (m.seq > max) max = m.seq
|
|
158
|
+
return max
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** hydrate / 模型输出:非法 kind 丢条目;空 text 丢;条数与总长截断。 */
|
|
162
|
+
export function sanitizeConstraints(raw: unknown): SessionConstraint[] {
|
|
163
|
+
if (!Array.isArray(raw)) return []
|
|
164
|
+
const out: SessionConstraint[] = []
|
|
165
|
+
let total = 0
|
|
166
|
+
for (const item of raw) {
|
|
167
|
+
if (out.length >= CONSTRAINT_MAX_ITEMS) break
|
|
168
|
+
if (!item || typeof item !== 'object') continue
|
|
169
|
+
const kind = asConstraintKind((item as { kind?: unknown }).kind)
|
|
170
|
+
const text = typeof (item as { text?: unknown }).text === 'string' ? (item as { text: string }).text.trim() : ''
|
|
171
|
+
if (!kind || !text) continue
|
|
172
|
+
const clipped = text.length > CONSTRAINT_ITEM_MAX_CHARS ? text.slice(0, CONSTRAINT_ITEM_MAX_CHARS) + '…' : text
|
|
173
|
+
const cost = KIND_LABEL[kind].length + clipped.length
|
|
174
|
+
if (total + cost > CONSTRAINT_MAX_CHARS) break
|
|
175
|
+
out.push({ kind, text: clipped })
|
|
176
|
+
total += cost
|
|
177
|
+
}
|
|
178
|
+
return out
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 解析折叠模型输出。null = 解析失败(水位不推);[] = 无新结论(水位推、备忘不动)。
|
|
183
|
+
*/
|
|
184
|
+
export function parseConstraints(raw: string): SessionConstraint[] | null {
|
|
185
|
+
const body = raw.replace(/```(?:json)?/g, '')
|
|
186
|
+
const l = body.indexOf('{')
|
|
187
|
+
const r = body.lastIndexOf('}')
|
|
188
|
+
if (l < 0 || r <= l) return null
|
|
189
|
+
try {
|
|
190
|
+
const o = JSON.parse(body.slice(l, r + 1)) as { constraints?: unknown }
|
|
191
|
+
if (!Object.prototype.hasOwnProperty.call(o, 'constraints')) return null
|
|
192
|
+
if (!Array.isArray(o.constraints)) return null
|
|
193
|
+
return sanitizeConstraints(o.constraints)
|
|
194
|
+
} catch {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 本批无用户消息时,新的已定/否决降为未决。 */
|
|
200
|
+
export function downgradeWithoutUser(list: SessionConstraint[], hasUser: boolean): SessionConstraint[] {
|
|
201
|
+
if (hasUser) return list
|
|
202
|
+
return list.map((c) => c.kind === 'open' ? c : { kind: 'open', text: c.text })
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** system 提示词「已确认约束」块;空则空串。 */
|
|
206
|
+
export function constraintBlock(list: SessionConstraint[] | undefined): string {
|
|
207
|
+
if (!list || !list.length) return ''
|
|
208
|
+
const lines = list.map((c) => '- ' + KIND_LABEL[c.kind] + ':' + c.text)
|
|
209
|
+
return '\n# 已确认约束\n' + lines.join('\n')
|
|
210
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
function pickJsonBlob(text: string): Record<string, unknown> | null {
|
|
115
|
+
const start = text.indexOf('{')
|
|
116
|
+
const end = text.lastIndexOf('}')
|
|
117
|
+
if (start < 0 || end <= start) return null
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(text.slice(start, end + 1)) as unknown
|
|
120
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null
|
|
121
|
+
} catch {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function httpStatus(text: string): number | null {
|
|
127
|
+
const m = text.match(/\b([1-5]\d{2})\b/)
|
|
128
|
+
if (!m) return null
|
|
129
|
+
const n = Number(m[1])
|
|
130
|
+
return n >= 100 && n <= 599 ? n : null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resetHint(message: string): string | undefined {
|
|
134
|
+
const m = message.match(/reset at ([^.]+\S)/i) || message.match(/将在\s*([^\s。]+)\s*重置/)
|
|
135
|
+
return m ? '将在 ' + m[1] + ' 重置' : undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 把供应商错误压成可扫描的标题 + 短因。
|
|
140
|
+
* 未知形态回退为「发言失败」,原文仍可展开。
|
|
141
|
+
*/
|
|
142
|
+
export function classifySpeakFailure(raw: string): SpeakFailureView {
|
|
143
|
+
const source = unwrapSpeakFailure(raw)
|
|
144
|
+
const blob = pickJsonBlob(source)
|
|
145
|
+
const code = blob && typeof blob.code === 'string' ? blob.code : ''
|
|
146
|
+
const type = blob && typeof blob.type === 'string' ? blob.type : ''
|
|
147
|
+
const message = blob && typeof blob.message === 'string' ? blob.message : source
|
|
148
|
+
const status = httpStatus(source)
|
|
149
|
+
const joined = (code + ' ' + type + ' ' + message + ' ' + source).toLowerCase()
|
|
150
|
+
|
|
151
|
+
if (/quota|accountquotaexceeded|exceeded the .*quota|额度|配额/.test(joined) || code === 'AccountQuotaExceeded') {
|
|
152
|
+
return { title: '额度已用尽', detail: resetHint(message), raw: source }
|
|
153
|
+
}
|
|
154
|
+
if (status === 429 || /too.?many.?requests|rate.?limit|限流/.test(joined) || type === 'TooManyRequests') {
|
|
155
|
+
return { title: '请求过于频繁', detail: '稍后再试,或降低并发', raw: source }
|
|
156
|
+
}
|
|
157
|
+
if (status === 401 || status === 403 || /unauthorized|forbidden|invalid.?api.?key|鉴权|未授权/.test(joined)) {
|
|
158
|
+
return { title: '模型鉴权失败', detail: '检查该角色绑定的提供方密钥', raw: source }
|
|
159
|
+
}
|
|
160
|
+
if (status === 404 || /model.?not.?found|unknown.?model|模型不存在/.test(joined)) {
|
|
161
|
+
return { title: '模型不可用', detail: '该角色绑定的模型可能已下线', raw: source }
|
|
162
|
+
}
|
|
163
|
+
if (/timeout|timed out|etimedout|超时/.test(joined)) {
|
|
164
|
+
return { title: '模型响应超时', raw: source }
|
|
165
|
+
}
|
|
166
|
+
if (/network|econnreset|econnrefused|enotfound|fetch failed|网络/.test(joined)) {
|
|
167
|
+
return { title: '网络异常', detail: '检查网络后重试', raw: source }
|
|
168
|
+
}
|
|
169
|
+
if (status !== null && status >= 500) {
|
|
170
|
+
return { title: '模型服务暂时不可用', detail: 'HTTP ' + status, raw: source }
|
|
171
|
+
}
|
|
172
|
+
if (status !== null) {
|
|
173
|
+
return { title: '发言失败', detail: 'HTTP ' + status, raw: source }
|
|
174
|
+
}
|
|
175
|
+
return { title: '发言失败', raw: source }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 失败卡默认复制内容:标题 + 短因 + 原文。 */
|
|
179
|
+
export function formatSpeakFailureCopy(view: SpeakFailureView): string {
|
|
180
|
+
const lines = [view.title]
|
|
181
|
+
if (view.detail) lines.push(view.detail)
|
|
182
|
+
if (view.raw && view.raw !== view.title) lines.push(view.raw)
|
|
183
|
+
return lines.join('\n')
|
|
184
|
+
}
|
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
|
}
|