refactor: 收口公开码搜索计划
This commit is contained in:
@@ -1355,6 +1355,14 @@
|
|||||||
- 验证方式:`npm run test -- src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts`、`npm run test -- src/services/creationUrlState.test.ts`、`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
- 验证方式:`npm run test -- src/components/platform-entry/platformCreationUrlStateModel.test.ts src/components/platform-entry/platformPuzzleIdentityModel.test.ts`、`npm run test -- src/services/creationUrlState.test.ts`、`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对新 Module 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md`。
|
||||||
|
|
||||||
|
## 2026-06-04 Platform Public Code Search Model 收口
|
||||||
|
|
||||||
|
- 背景:`PlatformEntryFlowShellImpl.tsx` 的公开搜索回调内联判断内部用户 ID、陶泥号、RPG 作品号、各玩法公开作品号前缀和 fallback 顺序,壳层同时承担纯搜索计划与网络 / 打开副作用。
|
||||||
|
- 决策:新增 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,以 `resolvePlatformPublicCodeSearchPlan(keyword)` 返回 `normalizedKeyword` 与 `steps`。`user_` / `user-` 只查用户 ID;玩法前缀直达对应作品;`CW` / 纯数字先查 RPG 作品再查陶泥号;普通关键词和 `SY` 保持既有用户号、RPG 作品、汪汪声浪、用户号兜底顺序。壳层只按 step 执行既有查找、详情打开、Bark Battle runtime 特例和 missing work 归航。
|
||||||
|
- 影响范围:发现页 / 推荐页公开搜索、作品详情深链初始搜索、陶泥号命中面板、各玩法公开作品号直达。
|
||||||
|
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts`、针对新 Module 和 `PlatformEntryFlowShellImpl.tsx` 执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||||
|
- 关联文档:`docs/technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||||
|
|
||||||
## 2026-06-03 Public Work Presentation 收口
|
## 2026-06-03 Public Work Presentation 收口
|
||||||
|
|
||||||
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
|||||||
|
|
||||||
创作入口点击的占位、隐藏模板拦截、未知入口 no-op 与工作台启动目标收口到 `src/components/platform-entry/platformCreationLaunchModel.ts`,壳层只执行启动前准备、错误提示和受保护动作,规则见 [【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md)。
|
创作入口点击的占位、隐藏模板拦截、未知入口 no-op 与工作台启动目标收口到 `src/components/platform-entry/platformCreationLaunchModel.ts`,壳层只执行启动前准备、错误提示和受保护动作,规则见 [【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformCreationLaunchModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
|
平台入口公开码搜索的用户 ID、陶泥号、RPG 作品号、各玩法作品号前缀和失败回退顺序收口到 `src/components/platform-entry/platformPublicCodeSearchModel.ts`,壳层只按计划执行网络读取、详情打开和错误归航副作用,规则见 [【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md](./technical/【前端架构】PlatformPublicCodeSearchModel收口计划-2026-06-04.md)。
|
||||||
|
|
||||||
平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||||
|
|
||||||
平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、恢复目标身份匹配、跳一跳 / 敲木鱼恢复阶段落点、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。
|
平台入口创作恢复 URL 私有 query、初始恢复判定、创作直达恢复目标解析、恢复目标身份匹配、跳一跳 / 敲木鱼恢复阶段落点、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 【前端架构】Platform Public Code Search Model 收口计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 的公开搜索回调曾直接在壳层内判断 `user_` / `user-`、`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB`、`CW`、纯数字和普通关键词的优先级。壳层因此既要持有搜索输入到查找顺序的纯规则,又要执行各玩法公开详情读取、用户读取、运行态启动和错误归航副作用。
|
||||||
|
|
||||||
|
公开搜索的“先查什么、失败后回退什么”是稳定的分流规则,应有独立测试面;壳层只应作为副作用 Adapter,按计划执行网络读取与打开动作。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
新增 `src/components/platform-entry/platformPublicCodeSearchModel.ts` 作为 Platform Public Code Search **Module**。其公开 **Interface** 为:
|
||||||
|
|
||||||
|
- `resolvePlatformPublicCodeSearchPlan(keyword)`:输入用户搜索词,输出 `{ normalizedKeyword, steps }`;空输入返回 `null`。
|
||||||
|
- `PlatformPublicCodeSearchStep`:枚举壳层可执行的查找步骤,包括 `user-id`、`public-user-code`、`rpg-work`、各玩法公开作品步骤与 `bark-battle-work`。
|
||||||
|
|
||||||
|
`PlatformEntryFlowShellImpl.tsx` 仍作为 **Adapter**:它保留 `getPublicAuthUserByCode`、各玩法 gallery 刷新 / 详情打开、Bark Battle runtime 特例和 missing work 归航副作用,只按 `steps` 顺序执行,前一步失败才尝试下一步。
|
||||||
|
|
||||||
|
## Interface 约束
|
||||||
|
|
||||||
|
- 空白搜索词返回 `null`,壳层不得进入搜索 loading。
|
||||||
|
- `user_` / `user-` 开头的内部用户 ID 只执行 `user-id`,不回退作品号。
|
||||||
|
- `PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀只进入对应玩法公开作品查找;`M3D-*` 继续归入 `M3` / 抓大鹅。
|
||||||
|
- `CW` 与 `1-8` 位纯数字先查 RPG 公开作品,再回退陶泥号。
|
||||||
|
- 普通关键词与 `SY` 陶泥号保持既有顺序:先查陶泥号,再查 RPG 公开作品,再查汪汪声浪作品,最后再以陶泥号兜底。
|
||||||
|
|
||||||
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
- **Depth**:壳层只消费短小的 `steps` Interface,搜索前缀、优先级和回退顺序藏入 Module Implementation。
|
||||||
|
- **Leverage**:新增公开作品前缀时,先扩展 Module 的 step union、前缀表和单测,再在壳层 Adapter 绑定对应执行函数。
|
||||||
|
- **Locality**:搜索计划规则集中在一个纯 Module;UI、网络、详情打开与 runtime 启动副作用继续留在壳层,避免把副作用 setter 变成浅 Interface。
|
||||||
|
|
||||||
|
## 验收
|
||||||
|
|
||||||
|
- `npm run test -- src/components/platform-entry/platformPublicCodeSearchModel.test.ts`
|
||||||
|
- `npx eslint src/components/platform-entry/platformPublicCodeSearchModel.ts src/components/platform-entry/platformPublicCodeSearchModel.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run check:encoding`
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 平台入口与玩法链路
|
# 平台入口与玩法链路
|
||||||
|
|
||||||
更新时间:`2026-06-03`
|
更新时间:`2026-06-04`
|
||||||
|
|
||||||
## 平台创作入口
|
## 平台创作入口
|
||||||
|
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
|
|
||||||
发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。
|
发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。
|
||||||
|
|
||||||
|
平台公开搜索的分流顺序统一由 `platformPublicCodeSearchModel.ts` 判定:`user_` / `user-` 内部用户 ID 只查用户 ID;`PZ`、`BF`、`JH`、`WF`、`BO`、`M3`、`SH`、`VN`、`BB` 前缀分别直达对应玩法公开作品;`CW` 与 1-8 位纯数字先查 RPG 公开作品再回退陶泥号;普通关键词和 `SY` 陶泥号保持先查陶泥号、再查 RPG 作品、再查汪汪声浪作品、最后陶泥号兜底的既有顺序。平台壳只按计划执行网络读取、详情打开、Bark Battle runtime 特例和缺失作品归航,不在壳层重复维护前缀布尔链。
|
||||||
|
|
||||||
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息组织,不保留旧的底部“填邀请码”次级入口;常用功能当前只展示四项常驻入口时必须按四列铺满整行,不保留五列网格导致左对齐空位;每日任务卡必须读取 `/api/profile/tasks` 的当前任务摘要并在领取后同步刷新卡片进度。字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
|
发现 Tab、创作 Tab 与草稿 Tab 的页面根内容区不再套 `platform-page-stage` 外层全局卡片壳,让列表、筛选和玩法卡获得更宽的横向空间;推荐页和我的页仍按各自页面设计保留原有全局卡片口径。移动端“我的”页仍按顶部头像 / 昵称 / 陶泥号、会员横幅、三张统计卡、每日任务、五项常用功能宫格、设置入口和法律信息组织,不保留旧的底部“填邀请码”次级入口;常用功能当前只展示四项常驻入口时必须按四列铺满整行,不保留五列网格导致左对齐空位;每日任务卡必须读取 `/api/profile/tasks` 的当前任务摘要并在领取后同步刷新卡片进度。字号必须维持平台普通 UI 档位,不能因为窄屏把卡片标题、功能 label 或法律信息撑成展示级字号;最后一屏内容必须能在底部 dock 上方完整滚动露出,不得被固定底部导航遮挡。
|
||||||
|
|
||||||
## RPG / 自定义世界
|
## RPG / 自定义世界
|
||||||
|
|||||||
@@ -512,6 +512,10 @@ import {
|
|||||||
resolveMatch3DRuntimeGeneratedBackgroundAsset,
|
resolveMatch3DRuntimeGeneratedBackgroundAsset,
|
||||||
resolveMatch3DRuntimeGeneratedItemAssets,
|
resolveMatch3DRuntimeGeneratedItemAssets,
|
||||||
} from './platformMatch3DRuntimeProfile';
|
} from './platformMatch3DRuntimeProfile';
|
||||||
|
import {
|
||||||
|
type PlatformPublicCodeSearchStep,
|
||||||
|
resolvePlatformPublicCodeSearchPlan,
|
||||||
|
} from './platformPublicCodeSearchModel';
|
||||||
import {
|
import {
|
||||||
getPlatformPublicGalleryEntryKey,
|
getPlatformPublicGalleryEntryKey,
|
||||||
getPlatformRecommendRuntimeKind,
|
getPlatformRecommendRuntimeKind,
|
||||||
@@ -13449,53 +13453,17 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const handlePublicCodeSearch = useCallback(
|
const handlePublicCodeSearch = useCallback(
|
||||||
async (keyword: string) => {
|
async (keyword: string) => {
|
||||||
const normalizedKeyword = keyword.trim();
|
const searchPlan = resolvePlatformPublicCodeSearchPlan(keyword);
|
||||||
if (!normalizedKeyword) {
|
if (!searchPlan) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { normalizedKeyword } = searchPlan;
|
||||||
|
|
||||||
setIsSearchingPublicCode(true);
|
setIsSearchingPublicCode(true);
|
||||||
setPublicSearchError(null);
|
setPublicSearchError(null);
|
||||||
setSearchedPublicUser(null);
|
setSearchedPublicUser(null);
|
||||||
|
|
||||||
const upperKeyword = normalizedKeyword.toUpperCase();
|
|
||||||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(
|
|
||||||
normalizedKeyword,
|
|
||||||
);
|
|
||||||
const shouldSearchBigFishFirst = upperKeyword.startsWith('BF');
|
|
||||||
const shouldSearchBabyObjectFirst = upperKeyword.startsWith('BO');
|
|
||||||
const shouldSearchJumpHopFirst = upperKeyword.startsWith('JH');
|
|
||||||
const shouldSearchWoodenFishFirst = upperKeyword.startsWith('WF');
|
|
||||||
const shouldSearchMatch3DFirst = upperKeyword.startsWith('M3');
|
|
||||||
const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
|
|
||||||
const shouldSearchSquareHoleFirst = upperKeyword.startsWith('SH');
|
|
||||||
const shouldSearchVisualNovelFirst = upperKeyword.startsWith('VN');
|
|
||||||
const shouldSearchBarkBattleFirst = upperKeyword.startsWith('BB');
|
|
||||||
const shouldSearchWorkFirst =
|
|
||||||
!shouldSearchUserIdFirst &&
|
|
||||||
!shouldSearchBabyObjectFirst &&
|
|
||||||
!shouldSearchBigFishFirst &&
|
|
||||||
!shouldSearchJumpHopFirst &&
|
|
||||||
!shouldSearchWoodenFishFirst &&
|
|
||||||
!shouldSearchMatch3DFirst &&
|
|
||||||
!shouldSearchPuzzleFirst &&
|
|
||||||
!shouldSearchSquareHoleFirst &&
|
|
||||||
!shouldSearchVisualNovelFirst &&
|
|
||||||
!shouldSearchBarkBattleFirst &&
|
|
||||||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
|
||||||
const shouldSearchUserFirst =
|
|
||||||
shouldSearchUserIdFirst ||
|
|
||||||
upperKeyword.startsWith('SY') ||
|
|
||||||
(!shouldSearchWorkFirst &&
|
|
||||||
!shouldSearchBigFishFirst &&
|
|
||||||
!shouldSearchBabyObjectFirst &&
|
|
||||||
!shouldSearchJumpHopFirst &&
|
|
||||||
!shouldSearchWoodenFishFirst &&
|
|
||||||
!shouldSearchMatch3DFirst &&
|
|
||||||
!shouldSearchPuzzleFirst &&
|
|
||||||
!shouldSearchSquareHoleFirst &&
|
|
||||||
!shouldSearchVisualNovelFirst);
|
|
||||||
|
|
||||||
const tryOpenGalleryEntry = async () => {
|
const tryOpenGalleryEntry = async () => {
|
||||||
const entry =
|
const entry =
|
||||||
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||||||
@@ -13717,95 +13685,66 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
const runSearchStep = async (step: PlatformPublicCodeSearchStep) => {
|
||||||
if (shouldSearchUserIdFirst) {
|
switch (step) {
|
||||||
const user = await getPublicAuthUserById(normalizedKeyword);
|
case 'user-id': {
|
||||||
setSearchedPublicUser(user);
|
const user = await getPublicAuthUserById(normalizedKeyword);
|
||||||
return;
|
setSearchedPublicUser(user);
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchPuzzleFirst) {
|
|
||||||
await tryOpenPuzzleGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchBigFishFirst) {
|
|
||||||
await tryOpenBigFishGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchJumpHopFirst) {
|
|
||||||
await tryOpenJumpHopGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchWoodenFishFirst) {
|
|
||||||
await tryOpenWoodenFishGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchBabyObjectFirst) {
|
|
||||||
await tryOpenBabyObjectMatchGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchMatch3DFirst) {
|
|
||||||
await tryOpenMatch3DGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchSquareHoleFirst) {
|
|
||||||
await tryOpenSquareHoleGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchVisualNovelFirst) {
|
|
||||||
await tryOpenVisualNovelGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchBarkBattleFirst) {
|
|
||||||
await tryOpenBarkBattleGalleryEntry();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSearchWorkFirst) {
|
|
||||||
try {
|
|
||||||
await tryOpenGalleryEntry();
|
|
||||||
return;
|
return;
|
||||||
} catch {
|
|
||||||
// 作品号优先时允许继续回退到用户号搜索。
|
|
||||||
}
|
}
|
||||||
}
|
case 'public-user-code': {
|
||||||
|
|
||||||
if (shouldSearchUserFirst) {
|
|
||||||
try {
|
|
||||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||||
setSearchedPublicUser(user);
|
setSearchedPublicUser(user);
|
||||||
return;
|
return;
|
||||||
} catch {
|
|
||||||
// 用户号优先时允许继续回退到作品号搜索。
|
|
||||||
}
|
}
|
||||||
}
|
case 'rpg-work':
|
||||||
|
|
||||||
if (!shouldSearchWorkFirst) {
|
|
||||||
try {
|
|
||||||
await tryOpenGalleryEntry();
|
await tryOpenGalleryEntry();
|
||||||
return;
|
return;
|
||||||
} catch {
|
case 'puzzle-work':
|
||||||
// 常规作品未命中时继续尝试汪汪声浪作品号。
|
await tryOpenPuzzleGalleryEntry();
|
||||||
}
|
return;
|
||||||
|
case 'big-fish-work':
|
||||||
try {
|
await tryOpenBigFishGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'jump-hop-work':
|
||||||
|
await tryOpenJumpHopGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'wooden-fish-work':
|
||||||
|
await tryOpenWoodenFishGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'baby-object-match-work':
|
||||||
|
await tryOpenBabyObjectMatchGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'match3d-work':
|
||||||
|
await tryOpenMatch3DGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'square-hole-work':
|
||||||
|
await tryOpenSquareHoleGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'visual-novel-work':
|
||||||
|
await tryOpenVisualNovelGalleryEntry();
|
||||||
|
return;
|
||||||
|
case 'bark-battle-work':
|
||||||
await tryOpenBarkBattleGalleryEntry();
|
await tryOpenBarkBattleGalleryEntry();
|
||||||
return;
|
return;
|
||||||
} catch {
|
default: {
|
||||||
// 汪汪声浪作品未命中时继续回退到陶泥号搜索。
|
const exhaustive: never = step;
|
||||||
|
return exhaustive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
try {
|
||||||
setSearchedPublicUser(user);
|
for (const [index, step] of searchPlan.steps.entries()) {
|
||||||
|
try {
|
||||||
|
await runSearchStep(step);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
if (index === searchPlan.steps.length - 1) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (selectionStage === 'work-detail') {
|
if (selectionStage === 'work-detail') {
|
||||||
setSelectedPublicWorkDetail(null);
|
setSelectedPublicWorkDetail(null);
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type PlatformPublicCodeSearchStep,
|
||||||
|
resolvePlatformPublicCodeSearchPlan,
|
||||||
|
} from './platformPublicCodeSearchModel';
|
||||||
|
|
||||||
|
function expectSearchSteps(
|
||||||
|
keyword: string,
|
||||||
|
steps: readonly PlatformPublicCodeSearchStep[],
|
||||||
|
) {
|
||||||
|
expect(resolvePlatformPublicCodeSearchPlan(keyword)?.steps).toEqual(steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('platformPublicCodeSearchModel', () => {
|
||||||
|
test('ignores empty public code search input', () => {
|
||||||
|
expect(resolvePlatformPublicCodeSearchPlan(' ')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes public code search keyword before planning', () => {
|
||||||
|
expect(resolvePlatformPublicCodeSearchPlan(' PZ-00000001 ')).toEqual({
|
||||||
|
normalizedKeyword: 'PZ-00000001',
|
||||||
|
steps: ['puzzle-work'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('searches internal user ids directly without work fallback', () => {
|
||||||
|
expectSearchSteps('user_00000001', ['user-id']);
|
||||||
|
expectSearchSteps('USER-profile-1', ['user-id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('routes known public work prefixes to their play-specific lookup', () => {
|
||||||
|
const cases: Array<
|
||||||
|
[keyword: string, step: PlatformPublicCodeSearchStep]
|
||||||
|
> = [
|
||||||
|
['PZ-EPUBLIC1', 'puzzle-work'],
|
||||||
|
['BF-NPUBLIC1', 'big-fish-work'],
|
||||||
|
['JH-EPUBLIC1', 'jump-hop-work'],
|
||||||
|
['WF-EPUBLIC1', 'wooden-fish-work'],
|
||||||
|
['BO-EPUBLIC1', 'baby-object-match-work'],
|
||||||
|
['M3-EPUBLIC1', 'match3d-work'],
|
||||||
|
['M3D-LEGACY1', 'match3d-work'],
|
||||||
|
['SH-EPUBLIC1', 'square-hole-work'],
|
||||||
|
['VN-EPUBLIC1', 'visual-novel-work'],
|
||||||
|
['BB-EPUBLIC1', 'bark-battle-work'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [keyword, step] of cases) {
|
||||||
|
expectSearchSteps(keyword, [step]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('searches RPG public works before public user codes for CW and numeric codes', () => {
|
||||||
|
expectSearchSteps('CW-00000001', ['rpg-work', 'public-user-code']);
|
||||||
|
expectSearchSteps('12345678', ['rpg-work', 'public-user-code']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps legacy user-code-first fallback for SY and ordinary keywords', () => {
|
||||||
|
const legacyFallbackSteps = [
|
||||||
|
'public-user-code',
|
||||||
|
'rpg-work',
|
||||||
|
'bark-battle-work',
|
||||||
|
'public-user-code',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
expectSearchSteps('SY-00000001', legacyFallbackSteps);
|
||||||
|
expectSearchSteps('月井守望', legacyFallbackSteps);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
export type PlatformPublicCodeSearchStep =
|
||||||
|
| 'user-id'
|
||||||
|
| 'public-user-code'
|
||||||
|
| 'rpg-work'
|
||||||
|
| 'puzzle-work'
|
||||||
|
| 'big-fish-work'
|
||||||
|
| 'jump-hop-work'
|
||||||
|
| 'wooden-fish-work'
|
||||||
|
| 'baby-object-match-work'
|
||||||
|
| 'match3d-work'
|
||||||
|
| 'square-hole-work'
|
||||||
|
| 'visual-novel-work'
|
||||||
|
| 'bark-battle-work';
|
||||||
|
|
||||||
|
export type PlatformPublicCodeSearchPlan = {
|
||||||
|
normalizedKeyword: string;
|
||||||
|
steps: readonly PlatformPublicCodeSearchStep[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLATFORM_PUBLIC_USER_ID_PATTERN = /^user[_-][a-z0-9_-]+$/iu;
|
||||||
|
const PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN = /^\d{1,8}$/u;
|
||||||
|
|
||||||
|
const DIRECT_WORK_PREFIX_STEPS: ReadonlyArray<
|
||||||
|
readonly [prefix: string, step: PlatformPublicCodeSearchStep]
|
||||||
|
> = [
|
||||||
|
['PZ', 'puzzle-work'],
|
||||||
|
['BF', 'big-fish-work'],
|
||||||
|
['JH', 'jump-hop-work'],
|
||||||
|
['WF', 'wooden-fish-work'],
|
||||||
|
['BO', 'baby-object-match-work'],
|
||||||
|
['M3', 'match3d-work'],
|
||||||
|
['SH', 'square-hole-work'],
|
||||||
|
['VN', 'visual-novel-work'],
|
||||||
|
['BB', 'bark-battle-work'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 收口公开码搜索顺序,壳层只按步骤执行网络读取与打开副作用。 */
|
||||||
|
export function resolvePlatformPublicCodeSearchPlan(
|
||||||
|
keyword: string,
|
||||||
|
): PlatformPublicCodeSearchPlan | null {
|
||||||
|
const normalizedKeyword = keyword.trim();
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PLATFORM_PUBLIC_USER_ID_PATTERN.test(normalizedKeyword)) {
|
||||||
|
return {
|
||||||
|
normalizedKeyword,
|
||||||
|
steps: ['user-id'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const upperKeyword = normalizedKeyword.toUpperCase();
|
||||||
|
const directWorkStep = DIRECT_WORK_PREFIX_STEPS.find(([prefix]) =>
|
||||||
|
upperKeyword.startsWith(prefix),
|
||||||
|
)?.[1];
|
||||||
|
if (directWorkStep) {
|
||||||
|
return {
|
||||||
|
normalizedKeyword,
|
||||||
|
steps: [directWorkStep],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
upperKeyword.startsWith('CW') ||
|
||||||
|
PLATFORM_RPG_WORK_NUMERIC_CODE_PATTERN.test(normalizedKeyword)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
normalizedKeyword,
|
||||||
|
steps: ['rpg-work', 'public-user-code'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
normalizedKeyword,
|
||||||
|
steps: [
|
||||||
|
'public-user-code',
|
||||||
|
'rpg-work',
|
||||||
|
'bark-battle-work',
|
||||||
|
'public-user-code',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user