diff --git a/apps/ai-game-creator-shell/src/view/template-library/index.tsx b/apps/ai-game-creator-shell/src/view/template-library/index.tsx
index 154e2ddc0..7d93b1c2c 100644
--- a/apps/ai-game-creator-shell/src/view/template-library/index.tsx
+++ b/apps/ai-game-creator-shell/src/view/template-library/index.tsx
@@ -1,3 +1,4 @@
+import { PlatformRuntimeStatusToast } from '@genarrative/shared/components';
import {
ArrowLeft,
BadgeCheck,
@@ -9,10 +10,13 @@ import {
SearchX,
SlidersHorizontal,
} from 'lucide-react';
+import { useEffect } from 'react';
+import { createPortal } from 'react-dom';
import type { GameTemplateEntry } from '../../features/template-library/templateLibraryModel';
import {
formatGameTemplateSize,
+ needsTemplateDownload,
templateRuntimeLabel,
} from '../../features/template-library/templateLibraryModel';
import type { TemplateLibraryController } from '../../features/template-library/useTemplateLibrary';
@@ -22,6 +26,51 @@ type TemplateLibraryViewProps = {
onBack: () => void;
};
+const TEMPLATE_LIBRARY_TOAST_MILLIS = 2600;
+
+/**
+ * 模板库提示统一走浮层 toast:下载完成、开始建项目这类过程提示不再占用页面内位置,
+ * 页面里只保留可操作的错误与空态。
+ */
+function TemplateLibraryToast({
+ message,
+ onDismiss,
+}: {
+ message: string;
+ onDismiss: () => void;
+}) {
+ useEffect(() => {
+ if (!message) {
+ return;
+ }
+ const timer = window.setTimeout(onDismiss, TEMPLATE_LIBRARY_TOAST_MILLIS);
+ return () => {
+ window.clearTimeout(timer);
+ };
+ }, [message, onDismiss]);
+
+ if (!message) {
+ return null;
+ }
+ return createPortal(
+
,
+ document.body,
+ );
+}
+
const chipClass =
'cursor-pointer rounded-full border border-(--platform-subpanel-border) bg-transparent px-2.5 py-1 text-[11px] text-(--platform-text-soft) transition hover:border-(--platform-warm-text) hover:text-(--platform-warm-text)';
const activeChipClass =
@@ -41,6 +90,7 @@ function TemplateCard({
onUse: (template: GameTemplateEntry) => void;
}) {
const busy = busyTemplateId === template.id;
+ const needsDownload = needsTemplateDownload(template);
const busyLabel =
busy && busyKind === 'download'
? '正在下载模板'
@@ -118,19 +168,26 @@ function TemplateCard({
)}
使用模板
-
+ {/* 已下载且版本一致时不再提供下载入口;只有缺包或版本落后才显示(落后时按「更新」)。 */}
+ {needsDownload ? (
+
+ ) : null}
{busyLabel ? (
{busyLabel}
@@ -167,6 +224,7 @@ export default function TemplateLibraryView({
refresh,
downloadTemplate,
createProjectFromTemplate,
+ clearNotice,
} = controller;
const showEmptyLibrary = status === 'ready' && templates.length === 0;
@@ -286,14 +344,7 @@ export default function TemplateLibraryView({
) : null}
- {notice ? (
-
- {notice}
-
- ) : null}
+
{error ? (
{
expect(blankWebCard.textContent).toContain('网页 · none · v0.1.0 · 2.0 KB');
expect(blankWebCard.textContent).toContain('空白');
expect(blankWebCard.textContent).toContain('零依赖最小网页工程');
+ // 已下载且版本一致:不再显示下载入口,只留「使用模板」。
+ expect(
+ within(blankWebCard).queryByRole('button', { name: /下载/ }),
+ ).toBeNull();
+ expect(
+ within(blankWebCard).getByRole('button', { name: /使用模板/ }),
+ ).toBeTruthy();
const blankCanvasCard = cardFor('空白二维画布工程');
expect(blankCanvasCard.textContent).not.toContain('已下载');
expect(blankCanvasCard.querySelector('img')?.getAttribute('src')).toBe(
`${OSS_BASE}/templates/v1/blank-2d-canvas/cover.svg`,
);
+ expect(
+ within(blankCanvasCard).getByRole('button', { name: /下载/ }),
+ ).toBeTruthy();
+ });
+
+ it('offers 更新 instead of 下载 when the installed version is stale', () => {
+ const stale = template({
+ id: 'blank-web',
+ title: '空白网页工程',
+ summary: '零依赖最小网页工程',
+ tags: ['空白', '网页'],
+ installed: true,
+ installedVersion: '0.0.9',
+ installedAtMillis: 1789000000000,
+ });
+ render(
+ {}}
+ />,
+ );
+
+ const card = cardFor('空白网页工程');
+ expect(within(card).getByRole('button', { name: /更新/ })).toBeTruthy();
+ expect(within(card).queryByRole('button', { name: /^下载/ })).toBeNull();
});
it('routes search, tag, runtime and installed-only controls through the controller', () => {
@@ -245,6 +280,40 @@ describe('TemplateLibraryView', () => {
screen.getByText('远端清单暂时读不到,当前展示本机缓存'),
).toBeTruthy();
});
+
+ it('renders process notices as a floating toast outside the page and auto-dismisses it', () => {
+ vi.useFakeTimers();
+ const clearNotice = vi.fn();
+ try {
+ const { container } = render(
+ {}}
+ />,
+ );
+
+ const toast = document.body.querySelector(
+ '[data-template-library-toast="true"]',
+ );
+ expect(toast).not.toBeNull();
+ expect(toast?.textContent).toContain('已下载模板「空白网页工程」');
+ expect(toast?.querySelector('[role="status"]')?.textContent).toContain(
+ '已下载模板「空白网页工程」',
+ );
+ // 提示不再占用页面内位置。
+ expect(
+ container.querySelector('[data-template-library-toast]'),
+ ).toBeNull();
+
+ vi.advanceTimersByTime(2600);
+ expect(clearNotice).toHaveBeenCalledTimes(1);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
});
describe('TemplateRecommendations', () => {
diff --git a/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md
index 9059a5348..f6483d226 100644
--- a/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md
+++ b/docs/project-memory/plans/【里程碑】AGC模板库客户端接入-2026-09-17.md
@@ -29,7 +29,7 @@ AGC 客户端能读取公共 OSS 上的游戏模板库,并把「浏览 → 筛
2. `download_game_template` 对字节数与 SHA-256 不一致、越界对象键、非 `templates/` 前缀的包一律拒绝,且不落半成品目录。
3. zip 解压拒绝绝对路径、`..`、盘符、符号链接;安装完成后才写 `installed.json` 作为已下载判据。
4. `create_automatic_local_game_project_from_template` 建出的项目同时具备模板文件、`.agent` 清单与标准目录;失败时不留项目目录。
-5. 模板库页可按关键词、标签、运行时与「仅看已下载」筛选;卡片显示封面与已下载徽标;「使用模板」在版本落后时先重下再建项。
+5. 模板库页可按关键词、标签、运行时与「仅看已下载」筛选;卡片显示封面与已下载徽标;已下载且版本一致时不再显示下载入口(落后显示「更新」);过程提示以浮层 toast 呈现,不占页面内位置;「使用模板」在版本落后时先重下再建项。
6. 首页推荐位展示模板库内容并可进入模板库页;左侧导航有模板库入口且为独立全屏页。
7. 定向 Rust 单测 9 项(含线上清单 fixture)、前端模型 9 项 + 页面/推荐位 8 项、`tsc` 类型检查、`npm run check:encoding`、`git diff --check` 全部通过;可选真连检查能读线上清单、下载安装线上模板并据此建项目。
diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md
index a22a96145..2435b532e 100644
--- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md
+++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md
@@ -66,6 +66,8 @@ templates/
- `src/features/template-library/templateLibraryModel.ts`:清单类型、搜索(空白分隔多关键词「与」)、标签/运行时/已下载筛选、标签选项聚合、体积格式化等纯函数。
- `src/features/template-library/useTemplateLibrary.ts`:一次拉清单,暴露筛选状态、下载与「用模板建项目」;下载成功后只就地更新该条目的已下载状态。
- `src/view/template-library/index.tsx`:模板库全屏页(返回、刷新、搜索、运行时/标签筛选、仅看已下载、卡片显示封面与已下载徽标、下载/使用模板)。
+ - 卡片动作按安装状态收口:已下载且版本一致时**不再显示下载入口**,只留「使用模板」;版本落后才显示「更新」;缺包显示「下载」。
+ - 过程提示(下载完成、开始建项目)走浮层 toast(复用 `packages/shared` 的 `PlatformRuntimeStatusToast`,`document.body` 浮层 + 2.6 秒自动消失),不再占用页面内位置;页面内只保留可操作的错误与空态。
- 首页「灵感推荐」替换为「模板库」推荐位(`src/view/home/TemplateRecommendations.tsx`):只展示封面、标题、运行时与已下载徽标,点击进入模板库页面;首页不再直接触发建项目。
- 左侧导航新增模板库入口(`LauncherView = 'template-library'`)。
- `src-tauri/tauri.conf.json` 的 `csp` / `devCsp` 在 `img-src` 放行 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com`,用于封面图;`connect-src` 原本已放行同一域名。