@tbox.cn/app-template-agent 0.7.4 → 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.
- package/.gitignore +5 -0
- package/.tbox/app.json +9 -1
- package/AGENTS.md +12 -4
- package/ENV.md +102 -17
- package/README.md +36 -34
- package/apps/client/VISUAL_RESTORE.md +11 -11
- package/apps/client/index.html +3 -1
- package/apps/client/package.json +9 -8
- package/apps/client/src/business/App.tsx +4 -4
- package/apps/client/src/business/README.md +5 -11
- package/apps/client/src/cards/registry.ts +23 -8
- package/apps/client/src/main.tsx +36 -2
- package/apps/client/src/modules.ts +7 -4
- package/apps/client/src/pages/index/index.tsx +18 -162
- package/apps/client/src/platform/AgentChatApp.tsx +3 -3
- package/apps/client/src/platform/agent-runtime.ts +5 -0
- package/apps/client/src/store/chat.ts +47 -185
- package/apps/client/src/test/setup.ts +7 -0
- package/apps/client/vite.config.ts +16 -1
- package/apps/server/package.json +0 -1
- package/apps/server/src/app-tools.ts +2 -1
- package/apps/server/src/app.ts +178 -110
- package/apps/server/src/config/integration.ts +2 -6
- package/apps/server/src/dev.ts +0 -1
- package/apps/server/src/intent-routes.ts +3 -3
- package/apps/server/src/modules.ts +4 -4
- package/apps/server/src/pre-tools.ts +4 -3
- package/apps/server/tests/agent-prompt.test.ts +81 -0
- package/apps/server/tests/cards-registry.test.ts +1 -3
- package/apps/server/tests/deployment-loader.test.ts +35 -56
- package/apps/server/tests/tool-runtime.test.ts +25 -61
- package/package.json +5 -4
- package/packages/agent-ui-kit/LICENSE +21 -0
- package/packages/agent-ui-kit/README.md +17 -0
- package/packages/agent-ui-kit/package.json +58 -0
- package/packages/agent-ui-kit/src/ChatPage.less +62 -0
- package/packages/agent-ui-kit/src/ChatPage.tsx +305 -0
- package/packages/agent-ui-kit/src/chat-config.test.ts +82 -0
- package/packages/agent-ui-kit/src/chat-config.ts +62 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/DebugButton.tsx +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.less +19 -16
- package/{apps/client → packages/agent-ui-kit}/src/components/ActionBar/index.tsx +10 -13
- package/packages/agent-ui-kit/src/components/CardRenderer.tsx +36 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.less +148 -0
- package/packages/agent-ui-kit/src/components/ChatHeader/index.test.tsx +74 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatHeader/index.tsx +27 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.less +47 -39
- package/packages/agent-ui-kit/src/components/ChatInput/index.test.tsx +156 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/index.tsx +13 -4
- package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/useVoicePress.ts +2 -2
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.less +49 -0
- package/packages/agent-ui-kit/src/components/HistoryEntry/index.tsx +42 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.less +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/components/MediaList/index.tsx +6 -6
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.less +25 -20
- package/packages/agent-ui-kit/src/components/MessageBubble/index.test.tsx +112 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/MessageBubble/index.tsx +22 -15
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.less +2 -2
- package/{apps/client → packages/agent-ui-kit}/src/components/PullDownIndicator/index.tsx +2 -2
- package/packages/agent-ui-kit/src/components/QuickCommands/index.less +63 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.test.tsx +99 -0
- package/packages/agent-ui-kit/src/components/QuickCommands/index.tsx +52 -0
- package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.less +1 -1
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.test.ts +346 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/detail.ts +204 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.less +312 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.test.tsx +261 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/index.tsx +212 -0
- package/packages/agent-ui-kit/src/components/ToolCallBlock/titles.ts +102 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.test.ts +66 -0
- package/packages/agent-ui-kit/src/hooks/use-drag-gesture.ts +129 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-history.ts +241 -0
- package/packages/agent-ui-kit/src/hooks/use-pull-down-wheel-native.test.ts +64 -0
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useMediaUpload.ts +3 -3
- package/{apps/client → packages/agent-ui-kit}/src/hooks/useTTSAutoPlay.ts +7 -5
- package/packages/agent-ui-kit/src/index.ts +69 -0
- package/packages/agent-ui-kit/src/store-context.tsx +92 -0
- package/packages/agent-ui-kit/src/test/setup.ts +7 -0
- package/packages/agent-ui-kit/src/theme/contract.json +72 -0
- package/packages/agent-ui-kit/src/theme/global.less +99 -0
- package/packages/agent-ui-kit/src/theme/motion.less +19 -0
- package/packages/agent-ui-kit/src/theme/theme-contract.test.ts +69 -0
- package/packages/agent-ui-kit/src/theme/theme.ts +17 -0
- package/packages/agent-ui-kit/src/theme/tokens.less +79 -0
- package/packages/agent-ui-kit/src/utils/haptics.test.ts +51 -0
- package/packages/agent-ui-kit/src/utils/haptics.ts +44 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.test.ts +188 -0
- package/packages/agent-ui-kit/src/utils/pull-down-machine.ts +157 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.test.ts +240 -0
- package/packages/agent-ui-kit/src/utils/tool-call-display.ts +146 -0
- package/packages/agent-ui-kit/src/vite-env.d.ts +1 -0
- package/packages/agent-ui-kit/tsconfig.json +8 -0
- package/packages/agent-ui-kit/vitest.config.ts +7 -0
- package/pnpm-lock.template.yaml +177 -83
- package/pnpm-workspace.template.yaml +12 -4
- package/profiles/mall/config/agent.jsonc +33 -0
- package/profiles/mall/config/chat.json +21 -0
- package/profiles/mall/config/knowledge.json +5 -0
- package/profiles/mall/config/llm_provider.json +5 -0
- package/profiles/mall/config/plugins.json +3 -0
- package/profiles/mall.json +16 -0
- package/scripts/check-workspace.mjs +3 -2
- package/apps/client/assets/copy@2x.png +0 -0
- package/apps/client/assets/debug.png +0 -0
- package/apps/client/assets/dislike@2x.png +0 -0
- package/apps/client/assets/like@2x.png +0 -0
- package/apps/client/assets/speaker@2x.png +0 -0
- package/apps/client/src/cards/CardRenderer.tsx +0 -20
- package/apps/client/src/components/ChatHeader/index.less +0 -104
- package/apps/client/src/components/ErrorBoundary.tsx +0 -2
- package/apps/client/src/components/QuickCommands/index.less +0 -48
- package/apps/client/src/components/QuickCommands/index.tsx +0 -29
- package/apps/client/src/components/ToolCallBlock/index.less +0 -131
- package/apps/client/src/components/ToolCallBlock/index.tsx +0 -125
- package/apps/client/src/examples/tourism/TourismLanding.tsx +0 -96
- package/apps/client/src/examples/tourism/index.less +0 -192
- package/apps/client/src/pages/index/index.less +0 -43
- package/apps/client/src/store/chat-config.ts +0 -40
- package/apps/client/src/styles/global.less +0 -60
- package/apps/client/src/styles/tokens.less +0 -60
- package/apps/server/src/config/deployment.ts +0 -47
- package/packages/module-weather/package.json +0 -36
- package/packages/module-weather/skills/weather-guide/SKILL.md +0 -22
- package/packages/module-weather/skills/weather-guide/references/dressing-tips.md +0 -5
- package/packages/module-weather/src/client/cards/weather/index.less +0 -22
- package/packages/module-weather/src/client/cards/weather/index.tsx +0 -14
- package/packages/module-weather/src/client/index.ts +0 -19
- package/packages/module-weather/src/index.ts +0 -2
- package/packages/module-weather/src/server/cards/weather/meta.ts +0 -27
- package/packages/module-weather/src/server/index.ts +0 -45
- package/packages/module-weather/src/server/pre-tools.ts +0 -20
- package/packages/module-weather/src/server/tool.ts +0 -29
- package/packages/module-weather/tbox.module.json +0 -27
- package/packages/module-weather/tsconfig.json +0 -14
- /package/{apps/client → packages/agent-ui-kit}/assets/keyboard-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/more-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/new-chat.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/quick-command-star.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/send-gradient.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/stop@2x.png +0 -0
- /package/{apps/client → packages/agent-ui-kit}/assets/voice-circle.svg +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoicePressField.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/VoiceRecordingOverlay.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ChatInput/voice/machine.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.less +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ReasoningBlock/index.tsx +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/components/ScrollToBottomButton/index.tsx +0 -0
- /package/{apps/client/src/types/chat.ts → packages/agent-ui-kit/src/types.ts} +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/copyText.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/imagePreview.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/session-error.ts +0 -0
- /package/{apps/client → packages/agent-ui-kit}/src/utils/toast.ts +0 -0
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 卡片注册表 + 客户端组装上下文(模块加载期快照:registerAll → ctx 注册表)。
|
|
3
3
|
*
|
|
4
4
|
* 卡片注册发生在各模块 clientModule.register(ctx) 内(模块内自注册闭环),
|
|
5
5
|
* 本文件仅消费组装端上下文;CardType 由 apps/client/src/modules.ts 从装配数组自动推导。
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* W3-2(薄装配):三通道全经 ctx 读取——
|
|
8
|
+
* - CHAT_REGISTRY = 原始注册 + 覆盖通道(聊天视图;覆盖优先);
|
|
9
|
+
* - CARD_REGISTRY = 原始注册(RawCardRenderer 详情弹层消费);
|
|
10
|
+
* - INLINE_CARD_FILTER = 内联流过滤复合(ChatPage filterInlineCards 注入值);
|
|
11
|
+
* - CLIENT_SLOTS = 槽注册表(home 槽由模块注册)。
|
|
7
12
|
*/
|
|
8
|
-
import type { CardComponent } from '@tbox.cn/app-contracts';
|
|
13
|
+
import type { CardComponent, TboxCardPayload } from '@tbox.cn/app-contracts';
|
|
9
14
|
import { createClientContext } from '@tbox.cn/app-sdk/client';
|
|
10
15
|
import { registerAll, type CardType } from '../modules';
|
|
11
16
|
|
|
12
|
-
/**
|
|
17
|
+
/** 组装端单例:注册全部模块客户端能力(模块加载期执行一次;注册幂等告警) */
|
|
13
18
|
const ctx = createClientContext();
|
|
14
19
|
registerAll(ctx);
|
|
15
20
|
|
|
16
|
-
/**
|
|
21
|
+
/** 运行时卡片注册表(原始形态;由各模块 clientModule.register 填充) */
|
|
17
22
|
export const CARD_REGISTRY = ctx.cards.getAll() as Record<CardType, CardComponent>;
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
/** 聊天入口 registry:模块注册 + 覆盖通道(同 cardType 覆盖优先) */
|
|
25
|
+
export const CHAT_REGISTRY: Record<string, CardComponent> = {
|
|
26
|
+
...CARD_REGISTRY,
|
|
27
|
+
...ctx.cards.getOverrides(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** 内联流过滤(域注册复合;无注册 = 恒等) */
|
|
31
|
+
export const INLINE_CARD_FILTER = (cards: readonly TboxCardPayload[]): TboxCardPayload[] =>
|
|
32
|
+
ctx.cards.applyInlineFilters(cards);
|
|
20
33
|
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
34
|
+
/** 客户端槽注册表(home 槽由模块 register 注册——W3-2 收口完成) */
|
|
35
|
+
export const CLIENT_SLOTS = ctx.slots;
|
|
36
|
+
|
|
37
|
+
export type { CardType } from '../modules';
|
package/apps/client/src/main.tsx
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import { StrictMode } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { configureRuntime, installGlobalErrorReporting } from '@tbox.cn/app-sdk/client';
|
|
3
|
+
import { configureRuntime, createLogger, installGlobalErrorReporting } from '@tbox.cn/app-sdk/client';
|
|
4
4
|
import App from './app';
|
|
5
|
-
import '
|
|
5
|
+
import { configureChatConfigDefaults } from '@app/agent-ui-kit';
|
|
6
|
+
import '@app/agent-ui-kit/theme/global.less';
|
|
7
|
+
// 品牌主题(出厂接线——theme-tbox 平台品牌资产包:素材/标题经本包承载,模块零品牌资产)。
|
|
8
|
+
// 换主题 = 替换三行(index.css + brand.js + applyTheme;叠加会致旧主题残留死重);
|
|
9
|
+
// 部署者轨迹见 cookbook·build-app-image「主题接线」。CLI 自由区,module sync 永不触碰。
|
|
10
|
+
import '@tbox.cn/app-theme-tbox/index.css';
|
|
11
|
+
import '@tbox.cn/app-theme-tbox/brand.js';
|
|
12
|
+
import { applyTheme } from '@app/agent-ui-kit/theme';
|
|
13
|
+
applyTheme('tbox');
|
|
14
|
+
// chat.json 构建期快照 → kit 静态配置 store(createRoot 前注入,首帧即快照值;
|
|
15
|
+
// HELLO_ACK settings 达后经 ChatPage 内水合覆盖)
|
|
16
|
+
import chatConfigJson from '../../../config/chat.json';
|
|
17
|
+
|
|
18
|
+
const bootLogger = createLogger('BOOT');
|
|
19
|
+
|
|
20
|
+
// 壳→H5 接入契约观测:落页即打印 URL 参数(search + hash 双读源,全量加载仅一次)。
|
|
21
|
+
// 壳侧若把参数移进 hash,mall-scope-url 的 search-only 读源会静默失效——这里第一时间暴露。
|
|
22
|
+
// code/token 类值部分脱敏(前缀 + 长度,对齐 provider log-redact 惯例)。
|
|
23
|
+
const SENSITIVE_PARAM = /^(code|token|auth_code|visitortoken|session)$/i;
|
|
24
|
+
const collectParams = (source: string): Record<string, string> => {
|
|
25
|
+
const out: Record<string, string> = {};
|
|
26
|
+
new URLSearchParams(source).forEach((value, key) => {
|
|
27
|
+
out[key] = SENSITIVE_PARAM.test(key) ? `${value.slice(0, 6)}…(len=${value.length})` : value;
|
|
28
|
+
});
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
const hashQuery = window.location.hash.includes('?')
|
|
32
|
+
? window.location.hash.slice(window.location.hash.indexOf('?') + 1)
|
|
33
|
+
: '';
|
|
34
|
+
bootLogger.info(
|
|
35
|
+
`url params: ${JSON.stringify({ search: collectParams(window.location.search), hash: collectParams(hashQuery) })}`,
|
|
36
|
+
);
|
|
6
37
|
|
|
7
38
|
// H1:环境值注入点(SDK 零 Vite 依赖)——VITE_API_URL 空串 = 同源相对路径
|
|
8
39
|
configureRuntime({ apiBaseUrl: import.meta.env.VITE_API_URL as string | undefined });
|
|
@@ -10,7 +41,10 @@ configureRuntime({ apiBaseUrl: import.meta.env.VITE_API_URL as string | undefine
|
|
|
10
41
|
// 全局错误兜底:未捕获异常/rejection → 统一日志行(本地落地;上报扩展点见 SDK client logger)
|
|
11
42
|
installGlobalErrorReporting();
|
|
12
43
|
|
|
44
|
+
configureChatConfigDefaults(chatConfigJson as Record<string, unknown>);
|
|
45
|
+
|
|
13
46
|
const el = document.getElementById('root');
|
|
47
|
+
|
|
14
48
|
if (el) {
|
|
15
49
|
createRoot(el).render(
|
|
16
50
|
<StrictMode>
|
|
@@ -8,20 +8,23 @@
|
|
|
8
8
|
import type { ClientContext, InferModuleCards } from '@tbox.cn/app-sdk/client';
|
|
9
9
|
|
|
10
10
|
// @tbox:imports-begin
|
|
11
|
-
|
|
11
|
+
// 其余能力经 `tbox-app module add` 扩展(shopping-guide/member 等商圈模块;mall-home 经 profiles/mall)
|
|
12
12
|
// @tbox:imports-end
|
|
13
13
|
|
|
14
14
|
// @tbox:register-begin
|
|
15
|
+
// W4 单一模板统一化:缺省出厂 = 空装配(chat 流;home 槽缺席 → chat 形态派生)——
|
|
16
|
+
// mall-home(home 槽/覆盖卡/内联过滤)经 profiles/mall 加装(W5 接线)。
|
|
17
|
+
// 形态约定:数组恒多行(register 锚区行式插入依赖换行结构;单行 [] 会让 add 只插 import 不插注册行)
|
|
15
18
|
const modules = [
|
|
16
|
-
|
|
17
|
-
];
|
|
19
|
+
] as const;
|
|
18
20
|
// @tbox:register-end
|
|
19
21
|
|
|
20
22
|
// 自由区:可做装配级定制——条件注册、顺序调整
|
|
21
23
|
|
|
22
24
|
/** 注册所有已安装模块(客户端装配——勿与 apps/server/src/modules.ts 混淆) */
|
|
23
25
|
export function registerAll(ctx: ClientContext): void {
|
|
24
|
-
|
|
26
|
+
// 空装配下 (typeof modules)[number] = never:经最小注册面断言遍历(add 模块后自动收缩为具体类型)
|
|
27
|
+
for (const m of modules as readonly { register(ctx: ClientContext): void }[]) {
|
|
25
28
|
m.register(ctx);
|
|
26
29
|
}
|
|
27
30
|
}
|
|
@@ -1,167 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 首页(薄页,单一模板统一化 W2-5/W3-2):kit ChatPage 单点装配——零域 import。
|
|
3
|
+
*
|
|
4
|
+
* - store = 应用薄壳 store(会话泳道 deps 注入 SDK 默认实现);
|
|
5
|
+
* - cards/rawCards = 覆盖通道组装 / 原始形态(registry.ts 经 ctx 读取);
|
|
6
|
+
* - slots = 客户端槽注册表(home 槽由 module-mall-home register 注册);
|
|
7
|
+
* - filterInlineCards = 内联流过滤复合(域模块注册;无注册恒等)。
|
|
8
|
+
*/
|
|
9
|
+
import { ChatPage } from '@app/agent-ui-kit';
|
|
2
10
|
import { useChatStore } from '@/store/chat';
|
|
3
|
-
import {
|
|
4
|
-
import { useTTSAutoPlay } from '@/hooks/useTTSAutoPlay';
|
|
5
|
-
import { MessageBubble } from '@/components/MessageBubble';
|
|
6
|
-
import { ChatInput } from '@/components/ChatInput';
|
|
7
|
-
import { QuickCommands } from '@/components/QuickCommands';
|
|
8
|
-
import ChatHeader from '@/components/ChatHeader';
|
|
9
|
-
import ScrollToBottomButton from '@/components/ScrollToBottomButton';
|
|
10
|
-
import PullDownIndicator from '@/components/PullDownIndicator';
|
|
11
|
-
import { usePullDownHistory, consumeAutoSendQuery, runAutoSend } from '@tbox.cn/app-sdk/client';
|
|
12
|
-
import { CardRenderer } from '@/cards/CardRenderer';
|
|
13
|
-
import type { ChatInputMessage } from '@/types/chat';
|
|
14
|
-
import './index.less';
|
|
15
|
-
|
|
16
|
-
export interface ChatLandingProps {
|
|
17
|
-
onSend: (input: string | ChatInputMessage) => void;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type ChatLandingComponent = ComponentType<ChatLandingProps>;
|
|
21
|
-
|
|
22
|
-
interface IndexPageProps {
|
|
23
|
-
landing?: ChatLandingComponent;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default function IndexPage({ landing: Landing }: IndexPageProps) {
|
|
27
|
-
const messages = useChatStore((s) => s.messages);
|
|
28
|
-
const historyMessages = useChatStore((s) => s.historyMessages);
|
|
29
|
-
const historyHasMore = useChatStore((s) => s.historyHasMore);
|
|
30
|
-
const isLoadingHistory = useChatStore((s) => s.isLoadingHistory);
|
|
31
|
-
const loadMoreHistory = useChatStore((s) => s.loadMoreHistory);
|
|
32
|
-
const isGenerating = useChatStore((s) => s.isGenerating);
|
|
33
|
-
const orphanCards = useChatStore((s) => s.orphanCards);
|
|
34
|
-
const cardsByMessageId = useChatStore((s) => s.cardsByMessageId);
|
|
35
|
-
const initConversation = useChatStore((s) => s.initConversation);
|
|
36
|
-
const destroy = useChatStore((s) => s.destroy);
|
|
37
|
-
const sendMessage = useChatStore((s) => s.sendMessage);
|
|
38
|
-
const cancelGeneration = useChatStore((s) => s.cancelGeneration);
|
|
39
|
-
const newConversation = useChatStore((s) => s.newConversation);
|
|
40
|
-
const authBlocked = useChatStore((s) => s.authBlocked);
|
|
41
|
-
const retryBootstrap = useChatStore((s) => s.retryBootstrap);
|
|
42
|
-
// chat settings 水合:HELLO_ACK 服务端真源 → 静态 store(ack 未达/键缺席保留构建期快照;
|
|
43
|
-
// 重连/新会话产生新 helloAck 再水合——applyServerSettings 幂等)
|
|
44
|
-
const serverSettings = useChatStore((s) => s.helloAck?.chatConfig?.settings);
|
|
45
|
-
const applyServerSettings = useChatConfigStore((s) => s.applyServerSettings);
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
applyServerSettings(serverSettings);
|
|
48
|
-
}, [serverSettings, applyServerSettings]);
|
|
49
|
-
|
|
50
|
-
const [ttsAutoPlay, setTtsAutoPlay] = useState(false);
|
|
51
|
-
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
|
52
|
-
const bodyRef = useRef<HTMLDivElement>(null);
|
|
53
|
-
// 初始 true:全新会话(未滚动)也可直接下拉加载历史(onScroll 才是真源更新)
|
|
54
|
-
const isAtTopRef = useRef(true);
|
|
55
|
-
const isNearBottomRef = useRef(true);
|
|
56
|
-
|
|
57
|
-
// TTS 自动播放:开启后边生成边流式朗读(服务端关态经水合强制停——防按钮隐藏仍朗读的僵尸路径)
|
|
58
|
-
const enableTTS = useChatConfigStore((s) => s.enableTTS);
|
|
59
|
-
useTTSAutoPlay(ttsAutoPlay && enableTTS);
|
|
60
|
-
|
|
61
|
-
const send = useCallback((input: string | ChatInputMessage) => sendMessage(input), [sendMessage]);
|
|
62
|
-
|
|
63
|
-
// 连接终态阻断(登录失败/scope 拒绝):禁用输入 + 提示条 + 重试(authBlocked 非空 ⇒ socket 已关 ⇒ 发送必失败)
|
|
64
|
-
const blocked = authBlocked
|
|
65
|
-
? { message: authBlocked, onRetry: () => void retryBootstrap() }
|
|
66
|
-
: undefined;
|
|
67
|
-
|
|
68
|
-
const scrollToBottom = useCallback(() => {
|
|
69
|
-
const el = bodyRef.current;
|
|
70
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
71
|
-
isNearBottomRef.current = true;
|
|
72
|
-
setShowScrollToBottom(false);
|
|
73
|
-
}, []);
|
|
74
|
-
|
|
75
|
-
const NEAR_BOTTOM = 150;
|
|
76
|
-
const onScroll = useCallback(() => {
|
|
77
|
-
const el = bodyRef.current;
|
|
78
|
-
if (!el) return;
|
|
79
|
-
isAtTopRef.current = el.scrollTop <= 5;
|
|
80
|
-
const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
|
|
81
|
-
isNearBottomRef.current = distanceToBottom < NEAR_BOTTOM;
|
|
82
|
-
setShowScrollToBottom(!isNearBottomRef.current);
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
// 下拉加载历史:加载时保持滚动位置(记录加载前高度,加载后补偿)
|
|
86
|
-
const onLoadHistory = useCallback(async () => {
|
|
87
|
-
const el = bodyRef.current;
|
|
88
|
-
const prevHeight = el?.scrollHeight ?? 0;
|
|
89
|
-
await loadMoreHistory();
|
|
90
|
-
requestAnimationFrame(() => {
|
|
91
|
-
const cur = bodyRef.current;
|
|
92
|
-
if (cur) cur.scrollTop += cur.scrollHeight - prevHeight;
|
|
93
|
-
});
|
|
94
|
-
}, [loadMoreHistory]);
|
|
95
|
-
|
|
96
|
-
const pull = usePullDownHistory({
|
|
97
|
-
isAtTopRef,
|
|
98
|
-
isLoadingHistory,
|
|
99
|
-
hasMore: historyHasMore,
|
|
100
|
-
onLoad: onLoadHistory,
|
|
101
|
-
// touchmove 走容器上的非 passive 原生监听(web-view 内合成事件 passive,阻止不了原生滚动)
|
|
102
|
-
scrollRef: bodyRef,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
// 深链自动发送:同步消费(StrictMode 双挂载/刷新安全),引导完成后等连接就绪发送
|
|
107
|
-
const autoQuery = consumeAutoSendQuery();
|
|
108
|
-
void initConversation().then(async () => {
|
|
109
|
-
if (autoQuery) await runAutoSend(autoQuery, sendMessage);
|
|
110
|
-
});
|
|
111
|
-
return () => destroy();
|
|
112
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
-
}, []);
|
|
114
|
-
|
|
115
|
-
// 仅在贴近底部时自动滚动,避免打断用户上翻查看历史
|
|
116
|
-
// 依赖含 cardsByMessageId:012 主卡在文字完成后挂载、消息行增高,需补滚避免卡片截出视口
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
if (isNearBottomRef.current) {
|
|
119
|
-
const el = bodyRef.current;
|
|
120
|
-
if (el) el.scrollTop = el.scrollHeight;
|
|
121
|
-
}
|
|
122
|
-
}, [messages, orphanCards, cardsByMessageId]);
|
|
11
|
+
import { CARD_REGISTRY, CHAT_REGISTRY, CLIENT_SLOTS, INLINE_CARD_FILTER } from '@/cards/registry';
|
|
123
12
|
|
|
13
|
+
export default function IndexPage() {
|
|
124
14
|
return (
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<div className="chat-page__body-wrapper">
|
|
134
|
-
<div
|
|
135
|
-
className="chat-page__body"
|
|
136
|
-
ref={bodyRef}
|
|
137
|
-
onScroll={onScroll}
|
|
138
|
-
>
|
|
139
|
-
{/* 下拉加载历史指示器 */}
|
|
140
|
-
<PullDownIndicator status={pull.pullStatus} distance={pull.pullDistance} />
|
|
141
|
-
|
|
142
|
-
{/* 历史消息(下拉加载) */}
|
|
143
|
-
{historyMessages.map((msg) => (
|
|
144
|
-
<MessageBubble key={msg.id} message={msg} />
|
|
145
|
-
))}
|
|
146
|
-
|
|
147
|
-
{Landing ? <Landing onSend={send} /> : null}
|
|
148
|
-
|
|
149
|
-
{/* 业务旁路 emit 的卡片(如欢迎卡) */}
|
|
150
|
-
{orphanCards.map((card) => (
|
|
151
|
-
<CardRenderer key={card.id} card={card} />
|
|
152
|
-
))}
|
|
153
|
-
|
|
154
|
-
{/* 消息(每条消息下的卡片由 MessageBubble 内部渲染) */}
|
|
155
|
-
{messages.map((msg) => (
|
|
156
|
-
<MessageBubble key={msg.id} message={msg} />
|
|
157
|
-
))}
|
|
158
|
-
</div>
|
|
159
|
-
|
|
160
|
-
<ScrollToBottomButton visible={showScrollToBottom && isGenerating} onClick={scrollToBottom} />
|
|
161
|
-
</div>
|
|
162
|
-
|
|
163
|
-
{!blocked && <QuickCommands onCommand={send} />}
|
|
164
|
-
<ChatInput generating={isGenerating} blocked={blocked} onSend={send} onCancel={cancelGeneration} onNewChat={newConversation} />
|
|
165
|
-
</div>
|
|
15
|
+
<ChatPage
|
|
16
|
+
store={useChatStore}
|
|
17
|
+
cards={CHAT_REGISTRY}
|
|
18
|
+
rawCards={CARD_REGISTRY}
|
|
19
|
+
slots={CLIENT_SLOTS}
|
|
20
|
+
filterInlineCards={INLINE_CARD_FILTER}
|
|
21
|
+
/>
|
|
166
22
|
);
|
|
167
23
|
}
|
|
@@ -5,11 +5,11 @@ const appLogger = createLogger('AgentChatApp');
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { AdapterProvider, createPlatformAdapters, type AdapterContextValue } from '@tbox.cn/app-sdk/client';
|
|
7
7
|
import type { TboxSessionData } from '@tbox.cn/app-contracts';
|
|
8
|
-
import IndexPage
|
|
8
|
+
import IndexPage from '@/pages/index/index';
|
|
9
9
|
import { authClient, platformApi } from './agent-runtime';
|
|
10
10
|
|
|
11
11
|
/** 平台默认的全栈对话运行时。普通本地应用无需读取或修改。 */
|
|
12
|
-
export default function AgentChatApp(
|
|
12
|
+
export default function AgentChatApp() {
|
|
13
13
|
const [session, setSession] = useState<TboxSessionData | null>(null);
|
|
14
14
|
// TTS 不依赖 session,恒可用且保持同一实例(播放状态不因会话刷新丢失)
|
|
15
15
|
const tts = useMemo(() => createPlatformAdapters().tts, []);
|
|
@@ -37,7 +37,7 @@ export default function AgentChatApp({ landing }: { landing?: ChatLandingCompone
|
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<AdapterProvider value={adapters}>
|
|
40
|
-
<IndexPage
|
|
40
|
+
<IndexPage />
|
|
41
41
|
</AdapterProvider>
|
|
42
42
|
);
|
|
43
43
|
}
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* 环境值(VITE_API_URL / VITE_AUTH_PROVIDER)由 main.tsx 经 configureRuntime / 选项注入(H1)。
|
|
5
5
|
*/
|
|
6
6
|
import { createAuthClient, createPlatformApi } from '@tbox.cn/app-sdk/client';
|
|
7
|
+
// 读源归位 TIER1(W1-①):mall-scope-url 契约助手(search-only 语义;壳 URL 改写层契约见包注释)
|
|
8
|
+
import { readMallIdFromUrl } from '@app/contracts-mall';
|
|
7
9
|
|
|
8
10
|
/** VITE_AUTH_PROVIDER=external 时壳内换码登录转发外部(服务端需同步配置 AUTH_EXTERNAL_*) */
|
|
9
11
|
const AUTH_PROVIDER = import.meta.env.VITE_AUTH_PROVIDER as string | undefined;
|
|
10
12
|
|
|
11
13
|
export const authClient = createAuthClient({
|
|
12
14
|
...(AUTH_PROVIDER === 'external' ? { loginPlatform: 'external' } : {}),
|
|
15
|
+
// SPEZ-017:token 缓存域键——URL mallId 变更即失效旧 token 重登(跨 mall 防串域)。
|
|
16
|
+
// 读源与 chat.ts configureHelloContext 逐字符同源(mall-scope-url.ts 唯一真源,search-only)
|
|
17
|
+
scopeKey: () => readMallIdFromUrl() ?? '',
|
|
13
18
|
});
|
|
14
19
|
|
|
15
20
|
export const platformApi = createPlatformApi();
|
|
@@ -1,202 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 对话状态(Zustand
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 对话状态(Zustand)薄壳(单一模板统一化 W2-5):
|
|
3
|
+
* 会话泳道/历史分页/凭证升级调度全部下沉 SDK createCoreChatState——
|
|
4
|
+
* 本壳只做域参数注入(HELLO context 采集)+ 排水订阅装配(应用侧一行)。
|
|
5
5
|
*/
|
|
6
6
|
import { create } from 'zustand';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import {
|
|
8
|
+
createCoreChatState,
|
|
9
|
+
getWsUrl,
|
|
10
|
+
getLocation,
|
|
11
|
+
configureHelloContext,
|
|
12
|
+
subscribeSessionUpgradeDrain,
|
|
13
|
+
} from '@tbox.cn/app-sdk/client';
|
|
13
14
|
import type { CoreChatState } from '@tbox.cn/app-sdk/client';
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import type { ChatMessage } from '@/types/chat';
|
|
17
|
-
import type { IFeedbackAdapter } from '@tbox.cn/app-sdk/client';
|
|
18
|
-
import type { AdapterContextValue } from '@tbox.cn/app-sdk/client';
|
|
19
|
-
import { isSessionExpiredError } from '@/utils/session-error';
|
|
20
|
-
import { toast } from '@/utils/toast';
|
|
15
|
+
import { toast } from '@app/agent-ui-kit';
|
|
16
|
+
import { readMallIdFromUrl, readMiniAppIdFromUrl } from '@app/contracts-mall';
|
|
21
17
|
import { authClient, platformApi } from '@/platform/agent-runtime';
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
let newConversationInFlight = false;
|
|
27
|
-
// ── 终态阻断重试 in-flight 标志(模块级):防并发重试 ──
|
|
28
|
-
let retryBootstrapInFlight: Promise<void> | null = null;
|
|
29
|
-
|
|
30
|
-
interface ChatState extends CoreChatState {
|
|
31
|
-
historyMessages: ChatMessage[];
|
|
32
|
-
historyHasMore: boolean;
|
|
33
|
-
isLoadingHistory: boolean;
|
|
34
|
-
initConversation: () => Promise<void>;
|
|
35
|
-
/** 新建会话:清会话态(连接复用)+ 重置历史态 + create 新会话 */
|
|
36
|
-
newConversation: () => Promise<void>;
|
|
37
|
-
loadMoreHistory: () => Promise<void>;
|
|
38
|
-
/** 终态阻断后重试:清匿名降级缓存 → 拆除连接 → 重走 initConversation(重复点击防抖) */
|
|
39
|
-
retryBootstrap: () => Promise<void>;
|
|
40
|
-
submitFeedback: (
|
|
41
|
-
messageId: string,
|
|
42
|
-
type: 'like' | 'dislike' | 'cancel',
|
|
43
|
-
adapter: IFeedbackAdapter,
|
|
44
|
-
refreshSession?: () => Promise<AdapterContextValue>,
|
|
45
|
-
) => Promise<void>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const useChatStore = create<ChatState>((set, get) => ({
|
|
49
|
-
// 核心状态:所有 set/get 闭包绑定到此 store(006:不再创建客户端 sessionId)
|
|
19
|
+
export const useChatStore = create<CoreChatState>()((set, get) => ({
|
|
20
|
+
// 核心状态:所有 set/get 闭包绑定到此 store(006:不再创建客户端 sessionId);
|
|
21
|
+
// 会话泳道 deps(platformApi/authClient)注入 SDK 默认实现
|
|
50
22
|
...createCoreChatState(
|
|
51
|
-
{
|
|
23
|
+
{
|
|
24
|
+
getWsUrl: () => getWsUrl(),
|
|
25
|
+
toast,
|
|
26
|
+
platformApi,
|
|
27
|
+
authClient,
|
|
28
|
+
// session.credentials-refreshed effect → 升级调度(清缓存冷登 + 幂等重连;守卫在 scheduleSessionUpgrade 内)
|
|
29
|
+
onSessionCredentialsRefreshed: () => void get().scheduleSessionUpgrade(),
|
|
30
|
+
},
|
|
52
31
|
set as (partial: any) => void,
|
|
53
32
|
get as () => any,
|
|
54
|
-
),
|
|
55
|
-
historyMessages: [],
|
|
56
|
-
historyHasMore: true,
|
|
57
|
-
isLoadingHistory: false,
|
|
58
|
-
|
|
59
|
-
initConversation: async () => {
|
|
60
|
-
const userId = await authClient.resolveUserId();
|
|
61
|
-
set({ userId });
|
|
62
|
-
try {
|
|
63
|
-
let conversationId: string | null = null;
|
|
64
|
-
// 006:create 的 userId 只来自 token 身份(body 废弃);匿名(无 token)不可建会话 → 直接走 no-conv 分支
|
|
65
|
-
if (!authClient.getAuthState()?.token) {
|
|
66
|
-
conversationLogger.warn('anonymous: skip create conversation (no-conv)');
|
|
67
|
-
} else {
|
|
68
|
-
for (let i = 0; i < 2 && !conversationId; i++) {
|
|
69
|
-
try {
|
|
70
|
-
const { status, data } = await platformApi.createConversation();
|
|
71
|
-
if (status === 200 && data?.conversationId) conversationId = data.conversationId;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
conversationLogger.warn(`createConversation 失败(重试 ${i + 1}/2):err=${(e as Error).message}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (conversationId) set({ conversationId });
|
|
78
|
-
get().init();
|
|
79
|
-
} catch (err) {
|
|
80
|
-
conversationLogger.error('init error:', err);
|
|
81
|
-
get().init();
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
// 终态阻断(登录失败/scope 拒绝)后重试:清匿名降级缓存(不清则必复败)→ 拆连接 → 重走完整引导
|
|
86
|
-
retryBootstrap: () => {
|
|
87
|
-
if (retryBootstrapInFlight) return retryBootstrapInFlight; // 并发重试防抖
|
|
88
|
-
const task = (async () => {
|
|
89
|
-
authClient.clearAuth(); // 匿名降级结果被缓存,不清则重试必然复败
|
|
90
|
-
get().destroy();
|
|
91
|
-
await get().initConversation();
|
|
92
|
-
})();
|
|
93
|
-
const tracked = task.finally(() => {
|
|
94
|
-
if (retryBootstrapInFlight === tracked) retryBootstrapInFlight = null;
|
|
95
|
-
});
|
|
96
|
-
retryBootstrapInFlight = tracked;
|
|
97
|
-
return tracked;
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// 新建会话:连接复用(clearChat 不断连——连接/身份是身份域),create 拿新会话键
|
|
101
|
-
newConversation: async () => {
|
|
102
|
-
if (newConversationInFlight) return; // 双击防双建会话
|
|
103
|
-
newConversationInFlight = true;
|
|
104
|
-
try {
|
|
105
|
-
historyEpoch++; // 作废在途历史加载(stale 响应整段丢弃,含 isLoadingHistory 复位)
|
|
106
|
-
get().clearChat(); // SDK:内聚取消运行中 run + 清会话态
|
|
107
|
-
set({ historyMessages: [], historyHasMore: true, isLoadingHistory: false });
|
|
108
|
-
await get().initConversation(); // create 新会话;init 对 OPEN 连接幂等早返
|
|
109
|
-
} finally {
|
|
110
|
-
newConversationInFlight = false;
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
loadMoreHistory: async () => {
|
|
115
|
-
const { isLoadingHistory, historyHasMore, historyMessages } = get();
|
|
116
|
-
if (isLoadingHistory || !historyHasMore) return;
|
|
117
|
-
const epoch = historyEpoch; // 快照:newConversation 递增后本响应作废
|
|
118
|
-
set({ isLoadingHistory: true });
|
|
119
|
-
|
|
120
|
-
const pageSize = 5;
|
|
121
|
-
const earliestRequestId =
|
|
122
|
-
historyMessages.length > 0
|
|
123
|
-
? historyMessages[0].requestId
|
|
124
|
-
: get().messages.find((m) => m.requestId)?.requestId;
|
|
125
|
-
|
|
126
|
-
// 006:有 token 身份时服务端按 token 取归属;无 token(匿名/兼容)保留 query userId
|
|
127
|
-
try {
|
|
128
|
-
const { status, data } = await platformApi.listMessages({
|
|
129
|
-
pageSize,
|
|
130
|
-
...(authClient.getAuthState()?.token
|
|
131
|
-
? {}
|
|
132
|
-
: { userId: get().userId }),
|
|
133
|
-
...(earliestRequestId ? { beforeId: earliestRequestId } : {}),
|
|
134
|
-
});
|
|
135
|
-
// stale 响应(newConversation 已重置态):整段丢弃——含 isLoadingHistory 复位也不落
|
|
136
|
-
if (epoch !== historyEpoch) return;
|
|
137
|
-
if (status === 200 && data) {
|
|
138
|
-
const rawList: PlatformHistoryMessage[] = (data.data as PlatformHistoryMessage[]) || [];
|
|
139
|
-
const currentRequestIds = new Set(get().messages.map((m) => m.requestId).filter(Boolean));
|
|
140
|
-
const filtered = rawList.filter(
|
|
141
|
-
(item) => !item.outerBusinessId || !currentRequestIds.has(item.outerBusinessId),
|
|
142
|
-
);
|
|
143
|
-
// SDK 单真源:消息对 + 历史卡片桶同 key 派生(平台 id 字段归一;isHistory 已由服务端标记)
|
|
144
|
-
const { messages: newMessages, cardsByMessageId } = convertHistoryMessages(filtered.reverse());
|
|
145
|
-
set((s) => ({
|
|
146
|
-
historyMessages: [...newMessages, ...s.historyMessages],
|
|
147
|
-
historyHasMore: rawList.length >= pageSize,
|
|
148
|
-
isLoadingHistory: false,
|
|
149
|
-
cardsByMessageId: { ...s.cardsByMessageId, ...cardsByMessageId },
|
|
150
|
-
}));
|
|
151
|
-
} else {
|
|
152
|
-
set({ isLoadingHistory: false });
|
|
153
|
-
}
|
|
154
|
-
} catch (err) {
|
|
155
|
-
historyLogger.error('load more error:', err);
|
|
156
|
-
if (epoch !== historyEpoch) return; // stale:newConversation 已重置 isLoadingHistory
|
|
157
|
-
set({ isLoadingHistory: false });
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
|
|
161
|
-
submitFeedback: async (messageId, type, adapter, refreshSession) => {
|
|
162
|
-
const oldMsg = get().messages.find((m) => m.id === messageId);
|
|
163
|
-
const oldFeedbackState = oldMsg?.feedbackState ?? 'none';
|
|
164
|
-
const feedbackRequestId = oldMsg?.requestId;
|
|
165
|
-
if (!feedbackRequestId) {
|
|
166
|
-
feedbackLogger.warn('no requestId for message', messageId);
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const newFeedbackState = type === 'cancel' ? 'none' : type;
|
|
170
|
-
get().updateMessage(messageId, { feedbackState: newFeedbackState as 'none' | 'like' | 'dislike' });
|
|
171
|
-
|
|
172
|
-
const feedbackMap = { like: 'LIKE' as const, dislike: 'DISLIKE' as const, cancel: 'CANCEL' as const };
|
|
173
|
-
try {
|
|
174
|
-
const result = await adapter.addTag(feedbackRequestId, feedbackMap[type]);
|
|
175
|
-
if (!result.success) get().updateMessage(messageId, { feedbackState: oldFeedbackState });
|
|
176
|
-
} catch (err) {
|
|
177
|
-
if (isSessionExpiredError(err) && refreshSession) {
|
|
178
|
-
try {
|
|
179
|
-
const newAdapters = await refreshSession();
|
|
180
|
-
if (newAdapters.feedback) {
|
|
181
|
-
const retry = await newAdapters.feedback.addTag(feedbackRequestId, feedbackMap[type]);
|
|
182
|
-
if (!retry.success) get().updateMessage(messageId, { feedbackState: oldFeedbackState });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
} catch (retryErr) {
|
|
186
|
-
feedbackLogger.error('retry failed:', retryErr);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
get().updateMessage(messageId, { feedbackState: oldFeedbackState });
|
|
190
|
-
}
|
|
191
|
-
},
|
|
33
|
+
),
|
|
192
34
|
}));
|
|
193
35
|
|
|
194
36
|
// 011 L4:HELLO context 采集——mallId 经 URL query 采集(mall 部署;无 mallId 时不携带 context)
|
|
37
|
+
// 停车缴费按用户就近车场查(服务端 #ensureParkingId 用 lbs 反查 cityId→最近车场),
|
|
38
|
+
// 故伴采集实时 lbs(best-effort:H5/无定位权限时静默丢弃,服务端自行降级 plaza)。
|
|
39
|
+
// getter 同步签名,不能现 await getLocation() → 装配期先发起,缓存供 getter 同步读取。
|
|
40
|
+
let cachedLbs: { latitude: string; longitude: string } | undefined;
|
|
41
|
+
void getLocation()
|
|
42
|
+
.then((loc) => {
|
|
43
|
+
if (Number.isFinite(loc.latitude) && Number.isFinite(loc.longitude)) {
|
|
44
|
+
cachedLbs = { latitude: String(loc.latitude), longitude: String(loc.longitude) };
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
// 定位不可用(H5 沙箱/未授权)→ 不带 location,服务端走 plaza 降级
|
|
49
|
+
});
|
|
195
50
|
configureHelloContext(() => {
|
|
196
|
-
|
|
197
|
-
const
|
|
51
|
+
// 读源收敛(CS-8):mallId ?? plazaId 单一 helper(contracts-mall runtime 唯一真源)
|
|
52
|
+
const mallId = readMallIdFromUrl();
|
|
53
|
+
const miniAppId = readMiniAppIdFromUrl();
|
|
198
54
|
if (!mallId) return undefined;
|
|
199
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
mall: { mallId, ...(miniAppId ? { miniAppId } : {}) },
|
|
57
|
+
...(cachedLbs ? { location: cachedLbs } : {}),
|
|
58
|
+
};
|
|
200
59
|
});
|
|
201
60
|
|
|
202
61
|
// 4002 scope 拒绝 / 4001 鉴权耗尽的终态提示与禁用由 SDK 统一出站(chat-store configureWsFatalCloseHandler)
|
|
62
|
+
|
|
63
|
+
// 凭证升级排水(W2-2 SDK 装配位):生成中排队的升级在 isGenerating true→false 跃迁时执行
|
|
64
|
+
subscribeSessionUpgradeDrain(useChatStore);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
1
2
|
import { defineConfig, type Plugin } from 'vite';
|
|
2
3
|
import react from '@vitejs/plugin-react';
|
|
3
4
|
import pxToRem from 'postcss-pxtorem';
|
|
@@ -31,7 +32,9 @@ export default defineConfig(async ({ command }) => {
|
|
|
31
32
|
// __dirname 基准 = apps/client:构件区在模板根(上一级×2)
|
|
32
33
|
const devOverridePath = resolve(__dirname, '../../.tbox-dev.local/vite-override.mjs');
|
|
33
34
|
const devPlugins: Plugin[] = [];
|
|
34
|
-
|
|
35
|
+
// 作用域门:override 仅 dev-serve 生效——build/vitest 不受 dev 会话残留影响
|
|
36
|
+
// (G2 防线;门与探针标记同文件,guard 另断言门条件在场)
|
|
37
|
+
if (command === 'serve' && !process.env.VITEST && existsSync(devOverridePath)) {
|
|
35
38
|
const { default: tboxDevOverride } = await import(pathToFileURL(devOverridePath).href);
|
|
36
39
|
devPlugins.push(tboxDevOverride());
|
|
37
40
|
}
|
|
@@ -48,6 +51,18 @@ export default defineConfig(async ({ command }) => {
|
|
|
48
51
|
fs: resolve(__dirname, 'src/shims/node-stub.ts'),
|
|
49
52
|
path: resolve(__dirname, 'src/shims/node-stub.ts'),
|
|
50
53
|
},
|
|
54
|
+
// dev 域 React 单实例(G1/G2 防线):双 dev 入口经本文件合并生效(middleware
|
|
55
|
+
// dev 加载本文件且 command==='serve');build 恒不继承——产物位稳定性。
|
|
56
|
+
...(command === 'serve' ? { dedupe: ['react', 'react-dom'] } : {}),
|
|
57
|
+
},
|
|
58
|
+
// vitest 并入同一 config(vitest 无专属 config 文件时本就加载本文件):
|
|
59
|
+
// act() 环境声明统一入口——此前散落各用例内联、多数漏声明 → act 告警刷屏 CI 日志。
|
|
60
|
+
// environment 保持每文件 @vitest-environment pragma(纯逻辑用例不付 DOM 开销)。
|
|
61
|
+
test: {
|
|
62
|
+
setupFiles: ['src/test/setup.ts'],
|
|
63
|
+
// W3-2:client 测试随域迁 module-mall-home(页级集成测试 = 模块 tests/index.test.tsx);
|
|
64
|
+
// 模板仅留薄页装配,零测试文件合法(新增客户端测试时移除本旗标)
|
|
65
|
+
passWithNoTests: true,
|
|
51
66
|
},
|
|
52
67
|
css: {
|
|
53
68
|
postcss: { plugins: [pxToRemPlugin] },
|