zlwu 7 ماه پیش
والد
کامیت
59078bdb9b

+ 10 - 2
backend/services/user_service.py

@@ -222,8 +222,16 @@ class UserService:
             "chatbot_layouts",
             "user_chatbot_backdrops",
         )
-        if any(not await self._mysql_table_exists(session, t) for t in required):
-            return
+        # 显式逐个 await,避免把 await 放进生成器表达式导致的潜在迭代异常
+        for t in required:
+            exists = await self._mysql_table_exists(session, t)
+            if not exists:
+                logger.warning(
+                    "⚠️ Chatbot 注册礼包:缺少必需表,跳过 user_id=%s missing_table=%s",
+                    user_id_uuid,
+                    t,
+                )
+                return
 
         gift_codes = ("stump_stool", "french_window", "fresh_green_wallpaper")
         used_qty_by_code: Dict[str, int] = {

+ 2 - 2
backend/websocket_handler.py

@@ -996,8 +996,8 @@ class WebSocketHandler:
                     )
                 )
                 
-                if next_round == 1:
-                    await self._maybe_spawn_mission_stone(request_id)
+                # 首次对话“水晶石”赠送已关闭:不再生成首句奖励(MissionEnergyStone)
+                # 也不再更新 `users.crystal_stone_total`。
 
                 # 注意:总结触发逻辑已移至 message_service.py
                 # 在保存 assistant 消息时,如果 current_round % 15 == 0,会自动触发总结

+ 29 - 23
frontend/src/components/ChatBotStaticShell.tsx

@@ -935,33 +935,35 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
           )}
         </div>
 
-        {/* 中间麦克风按钮(静态) */}
-        <div
-          className="absolute flex items-center justify-center"
-          style={{
-            left: '50%',
-            top: '50%',
-            transform: 'translate(-50%, -50%)',
-          }}
-        >
+        {/* 中间麦克风按钮(静态);step===5 时由蒙层上方的元素1替代 */}
+        {step !== 5 && (
           <div
-            className="chatbot-record-button touch-manipulation flex items-center justify-center cursor-default"
+            className="absolute flex items-center justify-center"
             style={{
-              width: px2vmin(84),
-              height: px2vmin(82),
-              userSelect: 'none',
-              overflow: 'visible',
-              position: 'relative',
+              left: '50%',
+              top: '50%',
+              transform: 'translate(-50%, -50%)',
             }}
           >
-            <img
-              src={luyinDefault}
-              alt={tCommon('a11y.record')}
-              className="pointer-events-none w-full h-full object-contain"
-              draggable={false}
-            />
+            <div
+              className="chatbot-record-button touch-manipulation flex items-center justify-center cursor-default"
+              style={{
+                width: px2vmin(84),
+                height: px2vmin(82),
+                userSelect: 'none',
+                overflow: 'visible',
+                position: 'relative',
+              }}
+            >
+              <img
+                src={luyinDefault}
+                alt={tCommon('a11y.record')}
+                className="pointer-events-none w-full h-full object-contain"
+                draggable={false}
+              />
+            </div>
           </div>
-        </div>
+        )}
 
         {/* 右侧字幕按钮(步骤 3 使用开启态图标),默认在底部容器内;步骤 3 单独在外层再绘制一份 */}
         <div
@@ -1179,7 +1181,11 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
         <div
           className="absolute"
           style={{
-            top: px2vmin(510),
+            // 让元素1中的录音按钮中心点与元素2(底部)的设置/字幕按钮中心点上下居中对齐
+            // 计算逻辑:底部容器 top(622) + 高度(98)/2 = 671
+            // 元素1录音中心 = wrapperTop(?, 取本处 top) + unionHeight(85) + gap(5) + recordHeight(82)/2(41)
+            // => wrapperTop = 671 - 131 = 540
+            top: px2vmin(540),
             left: '50%',
             transform: 'translateX(-50%)',
             display: 'flex',

+ 0 - 48
frontend/src/pages/ChatBotPage.tsx

@@ -3085,54 +3085,6 @@ export const ChatBotPage = () => {
             zIndex: 1,
           }}
         >
-          {/* 顶部 5x4 单元格边框线(调试/展示用) */}
-          <div
-            className="absolute pointer-events-none"
-            style={{
-              left: px2vmin(gridOriginX),
-              top: px2vmin(gridOriginY),
-              width: px2vmin(gridCols * cellWidth),
-              height: px2vmin(gridRows * cellHeight),
-              zIndex: CHATBOT_Z_INDEX.GRID_LINES,
-              border: '1px solid rgba(255, 255, 255, 0.35)',
-              boxSizing: 'border-box',
-            }}
-          >
-            {Array.from({ length: gridCols - 1 }, (_, idx) => {
-              const col = idx + 1;
-              return (
-                <div
-                  key={col}
-                  style={{
-                    position: 'absolute',
-                    top: 0,
-                    left: px2vmin(col * cellWidth),
-                    width: 1,
-                    height: '100%',
-                    backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                  }}
-                />
-              );
-            })}
-
-            {Array.from({ length: gridRows - 1 }, (_, idx) => {
-              const row = idx + 1;
-              return (
-                <div
-                  key={row}
-                  style={{
-                    position: 'absolute',
-                    left: 0,
-                    top: px2vmin(row * cellHeight),
-                    height: 1,
-                    width: '100%',
-                    backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                  }}
-                />
-              );
-            })}
-          </div>
-
           {/* 家具网格:按后端的 start_row/start_col/size_rows/size_cols 渲染 */}
           {activeLayoutItems.map((item, idx) => {
             const slotLeftPx = gridOriginX + (item.start_col - 1) * cellWidth;

+ 0 - 93
frontend/src/pages/LumiOutfitsPage.tsx

@@ -572,54 +572,6 @@ export const LumiOutfitsPage = () => {
               zIndex: 1,
             }}
           >
-            {lumiBagTab === 'furniture' && (
-              <div
-                className="absolute pointer-events-none"
-                style={{
-                  left: px2vmin(gridOriginX),
-                  top: px2vmin(gridOriginY),
-                  width: px2vmin(gridCols * cellWidth),
-                  height: px2vmin(gridRows * cellHeight),
-                  zIndex: CHATBOT_Z_INDEX.GRID_LINES,
-                  border: '1px solid rgba(255, 255, 255, 0.35)',
-                  boxSizing: 'border-box',
-                }}
-              >
-                {Array.from({ length: gridCols - 1 }, (_, idx) => {
-                  const col = idx + 1;
-                  return (
-                    <div
-                      key={col}
-                      style={{
-                        position: 'absolute',
-                        top: 0,
-                        left: px2vmin(col * cellWidth),
-                        width: 1,
-                        height: '100%',
-                        backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                      }}
-                    />
-                  );
-                })}
-                {Array.from({ length: gridRows - 1 }, (_, idx) => {
-                  const row = idx + 1;
-                  return (
-                    <div
-                      key={row}
-                      style={{
-                        position: 'absolute',
-                        left: 0,
-                        top: px2vmin(row * cellHeight),
-                        height: 1,
-                        width: '100%',
-                        backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                      }}
-                    />
-                  );
-                })}
-              </div>
-            )}
-
             {activeLayoutItemsForRender.map((item, idx) => {
               const slotLeftPx = gridOriginX + (item.start_col - 1) * cellWidth;
               const slotTopPx = gridOriginY + (item.start_row - 1) * cellHeight;
@@ -654,56 +606,11 @@ export const LumiOutfitsPage = () => {
               );
             })}
 
-            {lumiBagTab === 'furniture' &&
-              chatbotBackdropSlots.map((slot, idx) => {
-                const slotLeftPx = gridOriginX + (slot.start_col - 1) * cellWidth;
-                const slotTopPx = gridOriginY + (slot.start_row - 1) * cellHeight;
-                const slotWidthPx = slot.size_cols * cellWidth;
-                const slotHeightPx = slot.size_rows * cellHeight;
-                const isPixelSlot = slot.render_mode === 'pixel';
-                const offsetX = slot.offset_x ?? 0;
-                const offsetY = slot.offset_y ?? 0;
-                const widthPx = isPixelSlot ? (slot.pixel_width ?? slotWidthPx) : slotWidthPx;
-                const heightPx = isPixelSlot ? (slot.pixel_height ?? slotHeightPx) : slotHeightPx;
-                const leftPx = isPixelSlot ? slotLeftPx + offsetX : slotLeftPx;
-                const topPx = isPixelSlot ? slotTopPx + offsetY : slotTopPx;
-                const color = SLOT_HIGHLIGHT_COLORS[idx % SLOT_HIGHLIGHT_COLORS.length];
-                return (
-                  <div
-                    key={`slot-highlight-${slot.id}-${idx}`}
-                    className="absolute pointer-events-none"
-                    style={{
-                      left: px2vmin(leftPx),
-                      top: px2vmin(topPx),
-                      width: px2vmin(widthPx),
-                      height: px2vmin(heightPx),
-                      border: `2PX solid ${color}`,
-                      boxSizing: 'border-box',
-                      zIndex: 20,
-                    }}
-                  />
-                );
-              })}
-
             {activeFurnitureModalItem &&
             furnitureModalMode === 'equip' &&
             furnitureEquipPreview &&
             furnitureEquipPreviewRect ? (
               <>
-                <div
-                  className="absolute pointer-events-none"
-                  style={{
-                    left: px2vmin(furnitureEquipPreviewRect.leftPx),
-                    top: px2vmin(furnitureEquipPreviewRect.topPx),
-                    width: px2vmin(furnitureEquipPreviewRect.widthPx),
-                    height: px2vmin(furnitureEquipPreviewRect.heightPx),
-                    border: '2PX dashed rgba(255, 255, 255, 0.7)',
-                    boxSizing: 'border-box',
-                    zIndex: furnitureEquipPreviewRect.zIndex + 1,
-                    borderRadius: px2vmin(6),
-                  }}
-                  aria-hidden
-                />
                 <img
                   src={getOssUrl(furnitureEquipPreview.preview_image_name)}
                   alt=""

+ 0 - 94
frontend/src/pages/LumiShopPage.tsx

@@ -19,7 +19,6 @@ import {
   purchaseChatbotShopItem,
   type ChatbotLayoutBackdrop,
   type ChatbotLayoutItem,
-  type ChatbotBackdropSlot,
   type ChatbotShopCatalogItem,
   type ChatbotShopCartLine,
   unequipChatbotFurniture,
@@ -82,16 +81,6 @@ const CHATBOT_Z_INDEX = {
   GRID_LINES: 1,
   GRID_DYNAMIC_MIN: 3,
 } as const;
-const SLOT_HIGHLIGHT_COLORS = [
-  '#00FF62',
-  '#FFB020',
-  '#4DA3FF',
-  '#FF5FA2',
-  '#A56BFF',
-  '#00D1FF',
-  '#FFD60A',
-  '#FF6B6B',
-] as const;
 
 type FurnitureActionModalMode = 'equip' | 'remove';
 
@@ -183,7 +172,6 @@ export const LumiShopPage = () => {
   /** 首次拉取布局完成前不渲染默认幕布 init3,避免与真实幕布切换时产生「窗户闪一下」 */
   const [chatbotLayoutReady, setChatbotLayoutReady] = useState(false);
   const [chatbotLayoutItems, setChatbotLayoutItems] = useState<ChatbotLayoutItem[]>([]);
-  const [chatbotBackdropSlots, setChatbotBackdropSlots] = useState<ChatbotBackdropSlot[]>([]);
   const [furnitureNotice, setFurnitureNotice] = useState<string>('');
   const [activePurchaseItem, setActivePurchaseItem] = useState<ChatbotShopCatalogItem | null>(null);
   const [purchaseSuccessItem, setPurchaseSuccessItem] = useState<ChatbotShopCatalogItem | null>(null);
@@ -263,7 +251,6 @@ export const LumiShopPage = () => {
       if (!res.success || !res.data) return;
       setChatbotBackdrop(res.data.backdrop ?? null);
       setChatbotLayoutItems(res.data.items ?? []);
-      setChatbotBackdropSlots(res.data.slots ?? []);
       setChatbotGrid({
         grid_rows: res.data.grid_rows,
         grid_cols: res.data.grid_cols,
@@ -290,7 +277,6 @@ export const LumiShopPage = () => {
       if (!mounted) return;
       setChatbotBackdrop(null);
       setChatbotLayoutItems([]);
-      setChatbotBackdropSlots([]);
     });
     return () => {
       mounted = false;
@@ -307,7 +293,6 @@ export const LumiShopPage = () => {
   const activeLayoutItems = chatbotLayoutItems;
   const cellWidth = chatbotGrid.cell_width ?? 96;
   const cellHeight = chatbotGrid.cell_height ?? 96;
-  const gridRows = chatbotGrid.grid_rows ?? 5;
   const gridCols = chatbotGrid.grid_cols ?? 4;
   const gridOriginY = 0;
   /** 5×4 区整体在幕布层水平居中,槽位坐标相对网格左上角 */
@@ -729,54 +714,6 @@ export const LumiShopPage = () => {
               zIndex: 1,
             }}
           >
-          {lumiBagTab === 'furniture' && (
-            <div
-              className="absolute pointer-events-none"
-              style={{
-                left: px2vmin(gridOriginX),
-                top: px2vmin(gridOriginY),
-                width: px2vmin(gridCols * cellWidth),
-                height: px2vmin(gridRows * cellHeight),
-                zIndex: CHATBOT_Z_INDEX.GRID_LINES,
-                border: '1px solid rgba(255, 255, 255, 0.35)',
-                boxSizing: 'border-box',
-              }}
-            >
-              {Array.from({ length: gridCols - 1 }, (_, idx) => {
-                const col = idx + 1;
-                return (
-                  <div
-                    key={col}
-                    style={{
-                      position: 'absolute',
-                      top: 0,
-                      left: px2vmin(col * cellWidth),
-                      width: 1,
-                      height: '100%',
-                      backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                    }}
-                  />
-                );
-              })}
-              {Array.from({ length: gridRows - 1 }, (_, idx) => {
-                const row = idx + 1;
-                return (
-                  <div
-                    key={row}
-                    style={{
-                      position: 'absolute',
-                      left: 0,
-                      top: px2vmin(row * cellHeight),
-                      height: 1,
-                      width: '100%',
-                      backgroundColor: 'rgba(255, 255, 255, 0.35)',
-                    }}
-                  />
-                );
-              })}
-            </div>
-          )}
-
           {activeLayoutItems.map((item, idx) => {
             const slotLeftPx = gridOriginX + (item.start_col - 1) * cellWidth;
             const slotTopPx = gridOriginY + (item.start_row - 1) * cellHeight;
@@ -810,37 +747,6 @@ export const LumiShopPage = () => {
               />
             );
           })}
-
-          {lumiBagTab === 'furniture' &&
-            chatbotBackdropSlots.map((slot, idx) => {
-              const slotLeftPx = gridOriginX + (slot.start_col - 1) * cellWidth;
-              const slotTopPx = gridOriginY + (slot.start_row - 1) * cellHeight;
-              const slotWidthPx = slot.size_cols * cellWidth;
-              const slotHeightPx = slot.size_rows * cellHeight;
-              const isPixelSlot = slot.render_mode === 'pixel';
-              const offsetX = slot.offset_x ?? 0;
-              const offsetY = slot.offset_y ?? 0;
-              const widthPx = isPixelSlot ? (slot.pixel_width ?? slotWidthPx) : slotWidthPx;
-              const heightPx = isPixelSlot ? (slot.pixel_height ?? slotHeightPx) : slotHeightPx;
-              const leftPx = isPixelSlot ? slotLeftPx + offsetX : slotLeftPx;
-              const topPx = isPixelSlot ? slotTopPx + offsetY : slotTopPx;
-              const color = SLOT_HIGHLIGHT_COLORS[idx % SLOT_HIGHLIGHT_COLORS.length];
-              return (
-                <div
-                  key={`slot-highlight-${slot.id}-${idx}`}
-                  className="absolute pointer-events-none"
-                  style={{
-                    left: px2vmin(leftPx),
-                    top: px2vmin(topPx),
-                    width: px2vmin(widthPx),
-                    height: px2vmin(heightPx),
-                    border: `2PX solid ${color}`,
-                    boxSizing: 'border-box',
-                    zIndex: 20,
-                  }}
-                />
-              );
-            })}
           </div>
         </div>