@tbox.cn/app-template-agent 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.gitignore +5 -0
  2. package/.tbox/app.json +9 -1
  3. package/AGENTS.md +12 -4
  4. package/ENV.md +102 -17
  5. package/README.md +36 -34
  6. package/apps/client/VISUAL_RESTORE.md +11 -11
  7. package/apps/client/index.html +3 -1
  8. package/apps/client/package.json +9 -8
  9. package/apps/client/src/business/App.tsx +4 -4
  10. package/apps/client/src/business/README.md +5 -11
  11. package/apps/client/src/cards/registry.ts +23 -8
  12. package/apps/client/src/main.tsx +36 -2
  13. package/apps/client/src/modules.ts +7 -4
  14. package/apps/client/src/pages/index/index.tsx +18 -162
  15. package/apps/client/src/platform/AgentChatApp.tsx +3 -3
  16. package/apps/client/src/platform/agent-runtime.ts +5 -0
  17. package/apps/client/src/store/chat.ts +47 -185
  18. package/apps/client/src/test/setup.ts +7 -0
  19. package/apps/client/vite.config.ts +16 -1
  20. package/apps/server/package.json +0 -1
  21. package/apps/server/src/app-tools.ts +2 -1
  22. package/apps/server/src/app.ts +178 -110
  23. package/apps/server/src/config/integration.ts +2 -6
  24. package/apps/server/src/dev.ts +0 -1
  25. package/apps/server/src/intent-routes.ts +3 -3
  26. package/apps/server/src/modules.ts +4 -4
  27. package/apps/server/src/pre-tools.ts +4 -3
  28. package/apps/server/tests/agent-prompt.test.ts +81 -0
  29. package/apps/server/tests/cards-registry.test.ts +1 -3
  30. package/apps/server/tests/deployment-loader.test.ts +35 -56
  31. package/apps/server/tests/tool-runtime.test.ts +25 -61
  32. package/package.json +5 -4
  33. package/packages/agent-ui-kit/LICENSE +21 -0
  34. package/packages/agent-ui-kit/README.md +17 -0
  35. package/packages/agent-ui-kit/package.json +58 -0
  36. package/packages/agent-ui-kit/src/ChatPage.less +62 -0
  37. package/packages/agent-ui-kit/src/ChatPage.tsx +305 -0
  38. package/packages/agent-ui-kit/src/chat-config.test.ts +82 -0
  39. package/packages/agent-ui-kit/src/chat-config.ts +62 -0
  40. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/DebugButton.tsx +2 -2
  41. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.less +19 -16
  42. package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.tsx +10 -13
  43. package/packages/agent-ui-kit/src/components/CardRenderer.tsx +36 -0
  44. package/packages/agent-ui-kit/src/components/ChatHeader/index.less +148 -0
  45. package/packages/agent-ui-kit/src/components/ChatHeader/index.test.tsx +74 -0
  46. package/{apps/client → packages/agent-ui-kit}/src/components/ChatHeader/index.tsx +27 -15
  47. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.less +47 -39
  48. package/packages/agent-ui-kit/src/components/ChatInput/index.test.tsx +156 -0
  49. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.tsx +13 -4
  50. package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts +2 -2
  51. package/packages/agent-ui-kit/src/components/HistoryEntry/index.less +49 -0
  52. package/packages/agent-ui-kit/src/components/HistoryEntry/index.tsx +42 -0
  53. package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.less +3 -3
  54. package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.tsx +6 -6
  55. package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.less +25 -20
  56. package/packages/agent-ui-kit/src/components/MessageBubble/index.test.tsx +112 -0
  57. package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.tsx +22 -15
  58. package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.less +2 -2
  59. package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.tsx +2 -2
  60. package/packages/agent-ui-kit/src/components/QuickCommands/index.less +63 -0
  61. package/packages/agent-ui-kit/src/components/QuickCommands/index.test.tsx +99 -0
  62. package/packages/agent-ui-kit/src/components/QuickCommands/index.tsx +52 -0
  63. package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less +1 -1
  64. package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.test.ts +346 -0
  65. package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.ts +204 -0
  66. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.less +312 -0
  67. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.test.tsx +261 -0
  68. package/packages/agent-ui-kit/src/components/ToolCallBlock/index.tsx +212 -0
  69. package/packages/agent-ui-kit/src/components/ToolCallBlock/titles.ts +102 -0
  70. package/packages/agent-ui-kit/src/hooks/use-drag-gesture.test.ts +66 -0
  71. package/packages/agent-ui-kit/src/hooks/use-drag-gesture.ts +129 -0
  72. package/packages/agent-ui-kit/src/hooks/use-pull-down-history.ts +241 -0
  73. package/packages/agent-ui-kit/src/hooks/use-pull-down-wheel-native.test.ts +64 -0
  74. package/{apps/client → packages/agent-ui-kit}/src/hooks/useMediaUpload.ts +3 -3
  75. package/{apps/client → packages/agent-ui-kit}/src/hooks/useTTSAutoPlay.ts +7 -5
  76. package/packages/agent-ui-kit/src/index.ts +69 -0
  77. package/packages/agent-ui-kit/src/store-context.tsx +92 -0
  78. package/packages/agent-ui-kit/src/test/setup.ts +7 -0
  79. package/packages/agent-ui-kit/src/theme/contract.json +72 -0
  80. package/packages/agent-ui-kit/src/theme/global.less +99 -0
  81. package/packages/agent-ui-kit/src/theme/motion.less +19 -0
  82. package/packages/agent-ui-kit/src/theme/theme-contract.test.ts +69 -0
  83. package/packages/agent-ui-kit/src/theme/theme.ts +17 -0
  84. package/packages/agent-ui-kit/src/theme/tokens.less +79 -0
  85. package/packages/agent-ui-kit/src/utils/haptics.test.ts +51 -0
  86. package/packages/agent-ui-kit/src/utils/haptics.ts +44 -0
  87. package/packages/agent-ui-kit/src/utils/pull-down-machine.test.ts +188 -0
  88. package/packages/agent-ui-kit/src/utils/pull-down-machine.ts +157 -0
  89. package/packages/agent-ui-kit/src/utils/tool-call-display.test.ts +240 -0
  90. package/packages/agent-ui-kit/src/utils/tool-call-display.ts +146 -0
  91. package/packages/agent-ui-kit/src/vite-env.d.ts +1 -0
  92. package/packages/agent-ui-kit/tsconfig.json +8 -0
  93. package/packages/agent-ui-kit/vitest.config.ts +7 -0
  94. package/pnpm-lock.template.yaml +177 -83
  95. package/pnpm-workspace.template.yaml +12 -4
  96. package/profiles/mall/config/agent.jsonc +33 -0
  97. package/profiles/mall/config/chat.json +21 -0
  98. package/profiles/mall/config/knowledge.json +5 -0
  99. package/profiles/mall/config/llm_provider.json +5 -0
  100. package/profiles/mall/config/plugins.json +3 -0
  101. package/profiles/mall.json +16 -0
  102. package/scripts/check-workspace.mjs +3 -2
  103. package/apps/client/assets/copy@2x.png +0 -0
  104. package/apps/client/assets/debug.png +0 -0
  105. package/apps/client/assets/dislike@2x.png +0 -0
  106. package/apps/client/assets/like@2x.png +0 -0
  107. package/apps/client/assets/speaker@2x.png +0 -0
  108. package/apps/client/src/cards/CardRenderer.tsx +0 -20
  109. package/apps/client/src/components/ChatHeader/index.less +0 -104
  110. package/apps/client/src/components/ErrorBoundary.tsx +0 -2
  111. package/apps/client/src/components/QuickCommands/index.less +0 -48
  112. package/apps/client/src/components/QuickCommands/index.tsx +0 -29
  113. package/apps/client/src/components/ToolCallBlock/index.less +0 -131
  114. package/apps/client/src/components/ToolCallBlock/index.tsx +0 -125
  115. package/apps/client/src/examples/tourism/TourismLanding.tsx +0 -96
  116. package/apps/client/src/examples/tourism/index.less +0 -192
  117. package/apps/client/src/pages/index/index.less +0 -43
  118. package/apps/client/src/store/chat-config.ts +0 -40
  119. package/apps/client/src/styles/global.less +0 -60
  120. package/apps/client/src/styles/tokens.less +0 -60
  121. package/apps/server/src/config/deployment.ts +0 -47
  122. package/packages/module-weather/package.json +0 -36
  123. package/packages/module-weather/skills/weather-guide/SKILL.md +0 -22
  124. package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +0 -5
  125. package/packages/module-weather/src/client/cards/weather/index.less +0 -22
  126. package/packages/module-weather/src/client/cards/weather/index.tsx +0 -14
  127. package/packages/module-weather/src/client/index.ts +0 -19
  128. package/packages/module-weather/src/index.ts +0 -2
  129. package/packages/module-weather/src/server/cards/weather/meta.ts +0 -27
  130. package/packages/module-weather/src/server/index.ts +0 -45
  131. package/packages/module-weather/src/server/pre-tools.ts +0 -20
  132. package/packages/module-weather/src/server/tool.ts +0 -29
  133. package/packages/module-weather/tbox.module.json +0 -27
  134. package/packages/module-weather/tsconfig.json +0 -14
  135. /package/{apps/client → packages/agent-ui-kit}/assets/keyboard-circle.svg +0 -0
  136. /package/{apps/client → packages/agent-ui-kit}/assets/more-circle.svg +0 -0
  137. /package/{apps/client → packages/agent-ui-kit}/assets/new-chat.svg +0 -0
  138. /package/{apps/client → packages/agent-ui-kit}/assets/quick-command-star.svg +0 -0
  139. /package/{apps/client → packages/agent-ui-kit}/assets/send-gradient.svg +0 -0
  140. /package/{apps/client → packages/agent-ui-kit}/assets/stop@2x.png +0 -0
  141. /package/{apps/client → packages/agent-ui-kit}/assets/voice-circle.svg +0 -0
  142. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoicePressField.tsx +0 -0
  143. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +0 -0
  144. /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/machine.ts +0 -0
  145. /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.less +0 -0
  146. /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.tsx +0 -0
  147. /package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.tsx +0 -0
  148. /package/{apps/client/src/types/chat.ts → packages/agent-ui-kit/src/types.ts} +0 -0
  149. /package/{apps/client → packages/agent-ui-kit}/src/utils/copyText.ts +0 -0
  150. /package/{apps/client → packages/agent-ui-kit}/src/utils/imagePreview.ts +0 -0
  151. /package/{apps/client → packages/agent-ui-kit}/src/utils/session-error.ts +0 -0
  152. /package/{apps/client → packages/agent-ui-kit}/src/utils/toast.ts +0 -0
@@ -1,10 +1,10 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
- import { useMediaUpload } from '@/hooks/useMediaUpload';
2
+ import { useMediaUpload } from '../../hooks/useMediaUpload';
3
3
  import { useASRAdapter, isInAlipayShell } from '@tbox.cn/app-sdk/client';
4
- import { useChatConfigStore } from '@/store/chat-config';
4
+ import { useChatConfigStore } from '../../chat-config';
5
5
 
6
- import type { ChatInputMessage, InputMethod } from '@/types/chat';
7
- import { MediaList } from '@/components/MediaList';
6
+ import type { ChatInputMessage, InputMethod } from '../../types';
7
+ import { MediaList } from '../../components/MediaList';
8
8
  import { useVoicePress } from './voice/useVoicePress';
9
9
  import { VoicePressField } from './voice/VoicePressField';
10
10
  import { VoiceRecordingOverlay } from './voice/VoiceRecordingOverlay';
@@ -48,6 +48,7 @@ export function ChatInput({
48
48
  onSend,
49
49
  onCancel,
50
50
  onNewChat,
51
+ onVoiceActivityChange,
51
52
  disclaimer = '以上内容由AI生成,请谨慎甄别。',
52
53
  }: {
53
54
  generating?: boolean;
@@ -55,6 +56,7 @@ export function ChatInput({
55
56
  onSend: (msg: ChatInputMessage) => void;
56
57
  onCancel?: () => void;
57
58
  onNewChat?: () => void;
59
+ onVoiceActivityChange?: (active: boolean) => void;
58
60
  disclaimer?: string;
59
61
  }) {
60
62
  const [text, setText] = useState('');
@@ -85,6 +87,13 @@ export function ChatInput({
85
87
  asr,
86
88
  onResult: (t) => onSend({ text: t, mediaItems: [], inputMethod: 'voice' }),
87
89
  });
90
+ const voiceActive = voice.interactionState !== 'idle';
91
+
92
+ useEffect(() => {
93
+ onVoiceActivityChange?.(voiceActive);
94
+ }, [onVoiceActivityChange, voiceActive]);
95
+
96
+ useEffect(() => () => onVoiceActivityChange?.(false), [onVoiceActivityChange]);
88
97
 
89
98
  const isWebPc = !inShell && typeof window !== 'undefined' && window.innerWidth > 450;
90
99
 
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type { IASRAdapter } from '@tbox.cn/app-sdk/client';
3
3
  import { useRefreshSession } from '@tbox.cn/app-sdk/client';
4
- import { isSessionExpiredError } from '@/utils/session-error';
5
- import { toast } from '@/utils/toast';
4
+ import { isSessionExpiredError } from '../../../utils/session-error';
5
+ import { toast } from '../../../utils/toast';
6
6
  import {
7
7
  getMoveInteractionState,
8
8
  getReleaseAction,
@@ -0,0 +1,49 @@
1
+ /* 顶部历史入口:参考稿的浅灰胶囊,居中跟随内容滚动;历史拉出后由页面撤下。 */
2
+ .history-entry {
3
+ display: flex;
4
+ justify-content: center;
5
+ /* 自身不留边距;与欢迎区的 4PX 间距由页面 index.less 的 `.history-entry ~ .mall-home` 统一编排。 */
6
+ padding: 0;
7
+
8
+ &__button {
9
+ appearance: none;
10
+ padding: 3PX 12PX;
11
+ border: none;
12
+ border-radius: 25PX;
13
+ background: rgba(15, 64, 113, 0.06);
14
+ color: var(--tbox-text-muted);
15
+ font-size: 11.5PX;
16
+ font-weight: 400;
17
+ line-height: 16PX;
18
+ transition: background var(--tbox-transition-fast);
19
+
20
+ &:active {
21
+ background: rgba(15, 64, 113, 0.1);
22
+ }
23
+
24
+ /* 加载态小转子:与文案同处胶囊内,替代原独立指示器 */
25
+ display: inline-flex;
26
+ align-items: center;
27
+ gap: 5PX;
28
+
29
+ &:disabled {
30
+ /* 未就绪只是短暂不可点,保持视觉常态;加载态由文案表达。 */
31
+ cursor: default;
32
+ }
33
+ }
34
+
35
+ &__spinner {
36
+ width: 10PX;
37
+ height: 10PX;
38
+ border: 1.5PX solid rgba(15, 64, 113, 0.18);
39
+ border-top-color: var(--tbox-primary);
40
+ border-radius: 50%;
41
+ animation: history-entry-spin 0.6s linear infinite;
42
+ }
43
+ }
44
+
45
+ @keyframes history-entry-spin {
46
+ to {
47
+ transform: rotate(360deg);
48
+ }
49
+ }
@@ -0,0 +1,42 @@
1
+ // 顶部历史入口胶囊:同时承载点击入口和轻量下拉状态反馈。
2
+ // 常驻渲染是刻意的:运行时未就绪时只禁用,不隐藏——否则就绪后插入 DOM 会把下方欢迎区顶开。
3
+ import type { PullStatus } from '../../utils/pull-down-machine';
4
+ import './index.less';
5
+
6
+ function labelOf(status: PullStatus, loading: boolean): string {
7
+ if (loading || status === 'loading') return '加载中...';
8
+ if (status === 'pulling') return '下拉查看历史';
9
+ if (status === 'ready') return '松手查看历史';
10
+ if (status === 'noMore') return '没有更多历史记录';
11
+ return '历史记录';
12
+ }
13
+
14
+ export default function HistoryEntry({
15
+ visible,
16
+ loading,
17
+ disabled,
18
+ pullStatus,
19
+ onLoad,
20
+ }: {
21
+ visible: boolean;
22
+ loading: boolean;
23
+ disabled: boolean;
24
+ pullStatus: PullStatus;
25
+ onLoad: () => void;
26
+ }) {
27
+ if (!visible) return null;
28
+
29
+ return (
30
+ <div className="history-entry">
31
+ <button
32
+ type="button"
33
+ className="history-entry__button"
34
+ disabled={disabled}
35
+ onClick={onLoad}
36
+ >
37
+ {(loading || pullStatus === 'loading') && <span className="history-entry__spinner" />}
38
+ {labelOf(pullStatus, loading)}
39
+ </button>
40
+ </div>
41
+ );
42
+ }
@@ -58,7 +58,7 @@
58
58
  display: flex;
59
59
  align-items: center;
60
60
  justify-content: center;
61
- transition: background 0.3s ease;
61
+ transition: background var(--tbox-motion-duration-base) ease;
62
62
  }
63
63
 
64
64
  &__progress-inner {
@@ -157,7 +157,7 @@
157
157
  white-space: nowrap;
158
158
 
159
159
  &--failed {
160
- color: #ff4d4f;
160
+ color: var(--tbox-error);
161
161
  }
162
162
  }
163
163
 
@@ -195,7 +195,7 @@
195
195
  z-index: 10;
196
196
  opacity: 0;
197
197
  transform: translateY(8px);
198
- transition: opacity 0.2s ease-out, transform 0.2s ease-out;
198
+ transition: opacity var(--tbox-motion-duration-fast) ease-out, transform var(--tbox-motion-duration-fast) ease-out;
199
199
  pointer-events: none;
200
200
 
201
201
  &--visible {
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { X, Warning, Prohibit, Plus, VideoCamera } from '@phosphor-icons/react';
3
- import { previewImages } from '@/utils/imagePreview';
4
- import { toast } from '@/utils/toast';
5
- import type { MediaItem } from '@/types/chat';
3
+ import { previewImages } from '../../utils/imagePreview';
4
+ import { toast } from '../../utils/toast';
5
+ import type { MediaItem } from '../../types';
6
6
  import './index.less';
7
7
 
8
8
  /** 媒体选项图标(远程素材,对齐原模板)。 */
@@ -15,12 +15,12 @@ const POPOVER_ICONS: Record<string, { icon: string; label: string; bg: string }>
15
15
  camera: {
16
16
  icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
17
17
  label: '拍摄',
18
- bg: '#4f58ec26',
18
+ bg: '#096bfa26',
19
19
  },
20
20
  video: {
21
- icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*xdKLSabxqmQAAAAAOnAAAAgAel-hAQ/original',
21
+ icon: 'https://mdn.alipayobjects.com/huamei_nzq4hr/afts/img/A*DfDhSYAMzoQAAAAAHiAAAAgAel-hAQ/original',
22
22
  label: '视频',
23
- bg: '#4f58ec26',
23
+ bg: '#096bfa26',
24
24
  },
25
25
  };
26
26
 
@@ -2,7 +2,7 @@
2
2
  display: flex;
3
3
  align-items: flex-start;
4
4
  margin-bottom: 20px;
5
- padding: 0 24px;
5
+ padding: 0 var(--tbox-page-gutter);
6
6
 
7
7
  &--user {
8
8
  flex-direction: row-reverse;
@@ -26,7 +26,7 @@
26
26
 
27
27
  &--user {
28
28
  justify-content: flex-end;
29
- margin-right: 12px;
29
+ margin-right: 0;
30
30
  }
31
31
  &--bot {
32
32
  justify-content: flex-start;
@@ -34,12 +34,12 @@
34
34
 
35
35
  &__role {
36
36
  font-size: 19px;
37
- color: #70788d;
37
+ color: var(--tbox-text-secondary);
38
38
  margin-right: 8px;
39
39
  }
40
40
  &__time {
41
41
  font-size: 18px;
42
- color: #a0a7b8;
42
+ color: var(--tbox-text-muted);
43
43
  }
44
44
  }
45
45
 
@@ -49,19 +49,24 @@
49
49
  margin-bottom: 12px;
50
50
  }
51
51
 
52
+ /* 多卡纵向铺开 wrapper(决策出卡 >1 张时 SDK CardCarousel 外层;单卡直出无此层) */
53
+ .msg-cards {
54
+ margin-top: 8px;
55
+ }
56
+
52
57
  .msg-bubble {
53
58
  padding: 18px 22px;
54
- border-radius: 20px;
55
59
  line-height: 1.6;
60
+ /* mall-vis 气泡影(demo .msg-bubble 基座影,用户气泡沿用;bot 气泡自持双层影) */
56
61
  box-shadow: 0 4px 16px rgba(50, 65, 92, 0.08);
57
62
 
58
63
  &--user {
59
- background-color: rgba(255, 255, 255, 0.9);
60
- border-radius: 48px 48px 8px;
64
+ background: rgba(255, 255, 255, 0.9);
65
+ border-radius: 48px 8px 48px 48px;
61
66
  width: fit-content;
62
67
  max-width: 100%;
63
68
  margin-left: auto;
64
- margin-right: 12px;
69
+ margin-right: 0;
65
70
  margin-bottom: 8px;
66
71
  display: flex;
67
72
  align-items: center;
@@ -73,10 +78,10 @@
73
78
  font-size: 30px;
74
79
  line-height: 1.6;
75
80
  padding: 22px;
76
- color: #1b1212;
77
- background-color: rgba(255, 255, 255, 0.4);
81
+ color: var(--tbox-text-primary);
82
+ background: rgba(255, 255, 255, 0.4);
78
83
  border: 1px solid #edf0f6;
79
- border-radius: 48px 8px 48px 48px;
84
+ border-radius: 8px 48px 48px;
80
85
  box-shadow:
81
86
  0 3px 8px 5px rgba(1, 112, 254, 0.0392),
82
87
  0 2px 4px rgba(72, 78, 86, 0.0706);
@@ -104,7 +109,7 @@
104
109
  }
105
110
 
106
111
  &--user &__text {
107
- color: #1f3b63;
112
+ color: var(--tbox-text-primary);
108
113
  font-weight: 500;
109
114
  overflow-wrap: break-word;
110
115
  }
@@ -180,7 +185,7 @@
180
185
  width: 10px;
181
186
  height: 10px;
182
187
  border-radius: 50%;
183
- background: #8a9ab8;
188
+ background: #aab3c2; /* mall-vis thinking 点灰(非主色) */
184
189
  animation: dot-fade 1.4s ease-in-out infinite;
185
190
 
186
191
  &--2 {
@@ -208,7 +213,7 @@
208
213
  display: inline-block;
209
214
  width: 4px;
210
215
  height: 24px;
211
- background: #5874ff;
216
+ background: #5874ff; /* mall-vis 流式光标蓝 */
212
217
  margin-left: 4px;
213
218
  vertical-align: middle;
214
219
  animation: blink 1s step-end infinite;
@@ -227,7 +232,7 @@
227
232
  /* Markdown */
228
233
  .msg-markdown {
229
234
  font-size: 25px;
230
- color: #2a3142;
235
+ color: var(--tbox-text-primary);
231
236
  overflow-wrap: anywhere;
232
237
 
233
238
  h1,
@@ -263,7 +268,7 @@
263
268
  font-style: italic;
264
269
  }
265
270
  a {
266
- color: #1677ff;
271
+ color: var(--tbox-primary);
267
272
  text-decoration: underline;
268
273
  }
269
274
  ul,
@@ -275,10 +280,10 @@
275
280
  margin: 4px 0;
276
281
  }
277
282
  blockquote {
278
- border-left: 4px solid #667eea;
283
+ border-left: 4px solid #667eea; /* mall-vis 引用竖条(ReasoningBlock 折叠族同源) */
279
284
  padding-left: 16px;
280
285
  margin: 12px 0;
281
- color: #666;
286
+ color: var(--tbox-text-secondary);
282
287
  }
283
288
  code {
284
289
  background: rgba(0, 0, 0, 0.06);
@@ -308,13 +313,13 @@
308
313
  }
309
314
  th,
310
315
  td {
311
- border: 1px solid #ddd;
316
+ border: 1px solid var(--tbox-border);
312
317
  padding: 8px 12px;
313
318
  text-align: left;
314
319
  font-size: 26px;
315
320
  }
316
321
  th {
317
- background: #f0f0f5;
322
+ background: var(--tbox-bg-subtle);
318
323
  font-weight: 600;
319
324
  }
320
325
  }
@@ -0,0 +1,112 @@
1
+ // @vitest-environment happy-dom
2
+ import { act } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import type { TboxCardPayload } from '@tbox.cn/app-contracts';
6
+
7
+ const mocks = vi.hoisted(() => ({
8
+ state: {
9
+ cardsByMessageId: {} as Record<string, TboxCardPayload[]>,
10
+ conversationId: null,
11
+ },
12
+ }));
13
+
14
+ vi.mock('../../store-context', () => ({
15
+ useChatStore: (selector: (state: typeof mocks.state) => unknown) => selector(mocks.state),
16
+ // W2-1:内联卡过滤经 context 注入(域语义保持原 witness:会员门禁卡系剥离,
17
+ // 原直连 '@/components/MemberGateSheet/model' 的 MEMBER_GATE_CARD_TYPES 语义)
18
+ useInlineCardFilter: () =>
19
+ (cards: Array<{ cardType: string }>) =>
20
+ cards.filter((c) => !['member-login-cta', 'member-enrollment-cta'].includes(c.cardType)),
21
+ }));
22
+ vi.mock('../CardRenderer', () => ({
23
+ CardRenderer: ({ card }: { card: TboxCardPayload }) => <div data-card-id={card.id} />,
24
+ }));
25
+ vi.mock('../../components/ReasoningBlock', () => ({ default: () => null }));
26
+ vi.mock('../../components/ToolCallBlock', () => ({ default: () => null }));
27
+ vi.mock('../../components/ActionBar', () => ({
28
+ ActionBar: () => null,
29
+ CopyButton: () => null,
30
+ getMessageCopyText: () => '',
31
+ }));
32
+ vi.mock('../../components/ActionBar/DebugButton', () => ({ DebugButton: () => null }));
33
+
34
+ import { MessageBubble } from './index';
35
+
36
+ function card(id: string, cardType: string): TboxCardPayload {
37
+ return { id, cardType, data: {}, status: 'ready', meta: { createdAt: 1 } };
38
+ }
39
+
40
+ const emptyAssistantMessage = {
41
+ id: 'assistant-1',
42
+ role: 'assistant' as const,
43
+ content: '',
44
+ status: 'done' as const,
45
+ createdAt: 1,
46
+ };
47
+
48
+ describe('MessageBubble member gate card partition', () => {
49
+ let host: HTMLDivElement;
50
+ let root: ReturnType<typeof createRoot>;
51
+
52
+ beforeEach(() => {
53
+ host = document.createElement('div');
54
+ document.body.appendChild(host);
55
+ root = createRoot(host);
56
+ });
57
+
58
+ afterEach(() => {
59
+ act(() => root.unmount());
60
+ host.remove();
61
+ });
62
+
63
+ it('does not leave an empty message row for a login-only card', () => {
64
+ mocks.state.cardsByMessageId = {
65
+ 'assistant-1': [card('login-1', 'member-login-cta')],
66
+ };
67
+ act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
68
+ expect(host.innerHTML).toBe('');
69
+ });
70
+
71
+ it('keeps ordinary cards while removing the login card from a mixed carousel', () => {
72
+ mocks.state.cardsByMessageId = {
73
+ 'assistant-1': [
74
+ card('login-1', 'member-login-cta'),
75
+ card('coupon-1', 'coupon-detail'),
76
+ ],
77
+ };
78
+ act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
79
+ expect(host.querySelector('[data-card-id="login-1"]')).toBeNull();
80
+ expect(host.querySelector('[data-card-id="coupon-1"]')).not.toBeNull();
81
+ expect(host.querySelector('[aria-label^="第 "]')).toBeNull();
82
+ });
83
+
84
+ it('strips enrollment-cta from inline carousel (module button is a bypassing openLink jump)', () => {
85
+ mocks.state.cardsByMessageId = {
86
+ 'assistant-1': [
87
+ card('enroll-1', 'member-enrollment-cta'),
88
+ card('member-card-1', 'member-card-summary'),
89
+ ],
90
+ };
91
+ act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
92
+ // 入会卡内联渲染会旁路 gate 闭环(openLink 裸外跳)——必须剥离,由 MemberGateSheet 投影
93
+ expect(host.querySelector('[data-card-id="enroll-1"]')).toBeNull();
94
+ expect(host.querySelector('[data-card-id="member-card-1"]')).not.toBeNull();
95
+ });
96
+
97
+ it('多卡决策出卡 → 纵向按序渲染,无轮播圆点', () => {
98
+ mocks.state.cardsByMessageId = {
99
+ 'assistant-1': [
100
+ card('coupon-1', 'coupon-detail'),
101
+ card('member-1', 'member-card-summary'),
102
+ ],
103
+ };
104
+ act(() => root.render(<MessageBubble message={emptyAssistantMessage} />));
105
+ const all = host.querySelectorAll('[data-card-id]');
106
+ expect(all.length).toBe(2);
107
+ expect(all[0].getAttribute('data-card-id')).toBe('coupon-1');
108
+ expect(all[1].getAttribute('data-card-id')).toBe('member-1');
109
+ // 纵向铺开:不再有轮播圆点指示器
110
+ expect(host.querySelector('[aria-label^="第 "]')).toBeNull();
111
+ });
112
+ });
@@ -2,17 +2,17 @@ import { useMemo } from 'react';
2
2
  import ReactMarkdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import { Copy } from '@phosphor-icons/react';
5
- import { previewImages } from '@/utils/imagePreview';
6
- import type { ChatMessage } from '@/types/chat';
7
- import ReasoningBlock from '@/components/ReasoningBlock';
8
- import ToolCallBlock from '@/components/ToolCallBlock';
9
- import { ActionBar, CopyButton, getMessageCopyText } from '@/components/ActionBar';
10
- import { DebugButton } from '@/components/ActionBar/DebugButton';
11
- import { CardRenderer } from '@/cards/CardRenderer';
5
+ import { previewImages } from '../../utils/imagePreview';
6
+ import type { ChatMessage } from '../../types';
7
+ import ReasoningBlock from '../../components/ReasoningBlock';
8
+ import ToolCallBlock from '../../components/ToolCallBlock';
9
+ import { ActionBar, CopyButton, getMessageCopyText } from '../../components/ActionBar';
10
+ import { DebugButton } from '../../components/ActionBar/DebugButton';
11
+ import { CardRenderer } from '../CardRenderer';
12
12
  import { CardCarousel } from '@tbox.cn/app-sdk/client';
13
- import { useChatStore } from '@/store/chat';
14
- import { copyText } from '@/utils/copyText';
15
- import { toast } from '@/utils/toast';
13
+ import { useChatStore, useInlineCardFilter } from '../../store-context';
14
+ import { copyText } from '../../utils/copyText';
15
+ import { toast } from '../../utils/toast';
16
16
  import './index.less';
17
17
 
18
18
  const TBOX_FILE_URL = 'https://mdn.alipayobjects.com/tcmarket/afts/file/';
@@ -21,6 +21,8 @@ const TBOX_FILE_URL = 'https://mdn.alipayobjects.com/tcmarket/afts/file/';
21
21
  export function MessageBubble({ message }: { message: ChatMessage }) {
22
22
  const cardsByMessageId = useChatStore((s) => s.cardsByMessageId);
23
23
  const conversationId = useChatStore((s) => s.conversationId);
24
+ // 018 tool-debug:服务端旗标单源(HELLO_ACK;无 URL 逃逸——数据面信任锚点在服务端)
25
+ const toolDebug = useChatStore((s) => s.helloAck?.toolDebug === true);
24
26
  const isUser = message.role === 'user';
25
27
  const roleName = isUser ? '你' : 'AI 助手';
26
28
  const timeLabel = new Date(message.createdAt).toLocaleTimeString('zh-CN', {
@@ -32,8 +34,6 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
32
34
  const isThinking = message.status === 'thinking';
33
35
  const isStreaming = message.status === 'streaming';
34
36
  const hasReasoning = !!message.reasoning;
35
- // 018 tool-debug:服务端旗标单源(HELLO_ACK;无 URL 逃逸——数据面信任锚点在服务端)
36
- const toolDebug = useChatStore((s) => s.helloAck?.toolDebug === true);
37
37
  // 018:formSubmit 伪工具 prod 过滤不变;dev 放行(防御性——若事件到达则可见)
38
38
  const visibleToolCalls = useMemo(
39
39
  () => (message.toolCalls || []).filter((tc) => toolDebug || tc.name !== '__a2uiFormSubmit'),
@@ -41,9 +41,10 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
41
41
  );
42
42
  const hasToolCalls = visibleToolCalls.length > 0;
43
43
 
44
+ const filterInlineCards = useInlineCardFilter();
44
45
  const messageCards = useMemo(
45
- () => (isUser ? [] : cardsByMessageId[message.id] || []),
46
- [cardsByMessageId, message.id, isUser],
46
+ () => (isUser ? [] : filterInlineCards(cardsByMessageId[message.id] || [])),
47
+ [cardsByMessageId, message.id, isUser, filterInlineCards],
47
48
  );
48
49
  const imageItems = isUser
49
50
  ? message.mediaItems?.filter((i) => i.type === 'image' && (i.src || i.fileId))
@@ -60,6 +61,11 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
60
61
 
61
62
  const imageUrls = (imageItems || []).map((it) => it.src || `${TBOX_FILE_URL}${it.fileId}`);
62
63
 
64
+ // 登录入口由页面级半屏浮层承载;仅含登录卡的空 assistant 行不进入聊天流。
65
+ if (!isUser && !isPureThinking && !hasReasoning && !hasToolCalls && !showBubble && messageCards.length === 0) {
66
+ return null;
67
+ }
68
+
63
69
  return (
64
70
  <div className={`msg-row ${isUser ? 'msg-row--user' : 'msg-row--bot'}`}>
65
71
  <div className="msg-main">
@@ -117,6 +123,7 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
117
123
 
118
124
  {/* 卡片在对话气泡之后显示(决策出卡多张/意图卡/历史卡统一经 cardsByMessageId 挂载,多卡纵向铺开) */}
119
125
  <CardCarousel
126
+ className="msg-cards"
120
127
  cards={messageCards}
121
128
  renderItem={(card) => <CardRenderer key={card.id} card={card} />}
122
129
  />
@@ -138,7 +145,7 @@ export function MessageBubble({ message }: { message: ChatMessage }) {
138
145
  {isUser && !!message.content && (
139
146
  <div className="msg-toolbar msg-toolbar--user">
140
147
  <div className="msg-toolbar__action" onClick={copy}>
141
- <Copy size={12} color="#5f7ec6" />
148
+ <Copy size={12} color="#535b6c" />
142
149
  </div>
143
150
  </div>
144
151
  )}
@@ -6,13 +6,13 @@
6
6
  gap: 12px;
7
7
 
8
8
  &--animated {
9
- transition: margin-top 0.3s ease-out;
9
+ transition: margin-top var(--tbox-motion-duration-base) ease-out;
10
10
  }
11
11
 
12
12
  &__spinner {
13
13
  width: 32px;
14
14
  height: 32px;
15
- border: 3px solid #d0d5dd;
15
+ border: 3px solid var(--tbox-border-strong);
16
16
  border-top-color: var(--tbox-primary);
17
17
  border-radius: 50%;
18
18
  animation: pull-spin 0.6s linear infinite;
@@ -1,5 +1,5 @@
1
- import type { PullStatus } from '@tbox.cn/app-sdk/client';
2
- import { PULL_DOWN_THRESHOLD } from '@tbox.cn/app-sdk/client';
1
+ import type { PullStatus } from '../../utils/pull-down-machine';
2
+ import { PULL_DOWN_THRESHOLD } from '../../utils/pull-down-machine';
3
3
  import './index.less';
4
4
 
5
5
  /** 下拉加载历史的顶部指示器。 */
@@ -0,0 +1,63 @@
1
+ .quick-commands {
2
+ position: relative;
3
+ z-index: 1;
4
+ width: 100%;
5
+ max-width: var(--tbox-content-max-width);
6
+ margin: 0 auto;
7
+ box-sizing: border-box;
8
+ display: flex;
9
+ gap: 8PX;
10
+ overflow-x: auto;
11
+ padding: 0 var(--tbox-page-gutter) 9PX;
12
+ -ms-overflow-style: none;
13
+ scrollbar-width: none;
14
+
15
+ &::-webkit-scrollbar {
16
+ display: none;
17
+ }
18
+
19
+ /* 语音交互时只隐藏胶囊,保留原高度和页面灰底,继续遮挡聊天内容。 */
20
+ &--hidden {
21
+ visibility: hidden;
22
+ pointer-events: none;
23
+ }
24
+
25
+ &__item {
26
+ flex: 1 0 auto;
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ gap: 5PX;
31
+ height: 34PX;
32
+ padding: 0 13PX;
33
+ appearance: none;
34
+ /* mall-vis 快捷指令胶囊:白底 + 2px 描边 + 50px 胶囊圆角(数据仍服务端驱动) */
35
+ background: rgba(255, 255, 255, 0.92);
36
+ border: 1PX solid rgba(15, 64, 113, 0.14);
37
+ border-radius: 25PX;
38
+ box-shadow: 0 4PX 16PX rgba(38, 59, 92, 0.05);
39
+ transition: background var(--tbox-transition-fast), border-color var(--tbox-transition-fast);
40
+
41
+ &:active {
42
+ border-color: var(--tbox-primary);
43
+ }
44
+ }
45
+
46
+ &__icon {
47
+ width: 16PX;
48
+ height: 16PX;
49
+ object-fit: contain;
50
+ }
51
+
52
+ &__icon-svg {
53
+ color: var(--tbox-primary);
54
+ }
55
+
56
+ &__label {
57
+ color: var(--tbox-text-primary);
58
+ font-size: 13PX;
59
+ font-weight: 500;
60
+ line-height: 18PX;
61
+ white-space: nowrap;
62
+ }
63
+ }