diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index fa617a0c1..c38f161ff 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -45,7 +45,7 @@ server-rs + Axum + SpacetimeDB - `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。 - `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。 - `platform-*`:OSS、LLM、认证、语音等外部平台能力。 -- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。 +- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。 - 前端:表现、交互、临时 UI 状态和后端结果渲染。 明确废弃:旧 `server-node`、Express、PostgreSQL、Go 服务端、`maincloud`、人工 `spacetime --root-dir` 口径,以及前端承接正式业务真相的路线。 diff --git a/docs/project-memory/shared-memory/team-conventions.md b/docs/project-memory/shared-memory/team-conventions.md index ef7364769..d2fe77662 100644 --- a/docs/project-memory/shared-memory/team-conventions.md +++ b/docs/project-memory/shared-memory/team-conventions.md @@ -47,6 +47,7 @@ - 新增 Markdown 文档时,文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;只在任务需要时重命名历史文档,避免无关大 diff。 - 涉及中文文本时注意 UTF-8 编码和乱码排查。 - 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。 +- `packages/shared` 用于前后端 DTO、公开契约及跨页面复用的无业务真相 UI 组件和纯工具;不得把领域规则、后端副作用或正式状态放入其中。 - `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run dev:api-server` 与 `/healthz`。 - 涉及 SpacetimeDB 表结构、发布或迁移时,先看 `SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md` 和 `SPACETIMEDB_TABLE_CATALOG.md`。 - 涉及生产发布、服务器配置、Jenkins Job 重建或回滚时,先看 `PRODUCTION_DEPLOYMENT_PLAN_2026-05-02.md`。 diff --git a/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md b/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md index a2d523728..d93111563 100644 --- a/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md +++ b/docs/【协作规范】Agent工作入口与执行准则-2026-06-22.md @@ -66,7 +66,7 @@ RAG 默认不安装运行时依赖,也不把 LanceDB、Transformers.js 或本 - 领域规则沉到 `module-*`;SpacetimeDB 表、reducer、procedure、事务 adapter 和 row mapper 留在 `spacetime-module`。 - 后端访问 SpacetimeDB 统一经 `spacetime-client` facade。 - HTTP / SSE / BFF 和外部副作用编排留在 `api-server`;OSS、LLM、认证、语音等外部平台能力留在 `platform-*`。 -- 前后端 DTO 和公开契约留在 `shared-contracts` / `packages/shared`。 +- 前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码留在 `shared-contracts` / `packages/shared`;共享 UI 组件与纯工具可留在 `packages/shared`,但领域规则、后端副作用和正式状态不得下沉。 - 契约、路由、DTO 去留和 breaking change 以当前后端架构文档、`api-server/src/app.rs`、`shared-contracts` 和 `packages/shared` 为准。 后端修改后按当前 DDD 文档执行验收。涉及 API smoke 时,使用 `npm run dev:api-server` 重新拉起后端并检查 `/healthz`;不要使用旧 `maincloud` 启动口径。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 29b660887..cf34190b6 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -34,7 +34,7 @@ SpacetimeDB 版本口径:当前 Rust crate `spacetimedb`、`spacetimedb-sdk` 3. SpacetimeDB 表结构、reducer、procedure 和持久化 row shape 留在 `spacetime-module`。 4. 后端访问 SpacetimeDB 必须经 `spacetime-client` facade。 5. HTTP 鉴权、BFF 聚合、SSE、外部模型编排、OSS 上传和第三方回调在 `api-server`。 -6. 前端共享 DTO 通过 `shared-contracts` 和 `packages/shared` 对齐,不在页面内重新发明旧接口。 +6. 前端共享 DTO 通过 `shared-contracts` 和 `packages/shared` 对齐,不在页面内重新发明旧接口;`packages/shared` 还可复用无业务真相的 UI 组件和纯工具,领域规则、后端副作用与正式状态仍留在各自分层。 7. 微信能力按两层收口:`server-rs/crates/platform-wechat` 承载微信协议 client、订阅消息 `stable_token` / `subscribeMessage.send`、微信支付 V3 / 虚拟支付消息推送的 HTTP header、签名、验签、解密、mock 响应和协议 payload 解析;`server-rs/crates/api-server/src/wechat.rs` 与 `wechat/*` 承载 Axum handler、AppConfig 到平台配置的映射、Genarrative 用户 / 订单 / 钱包 / SSE / 错误 envelope 编排。`platform-auth` 当前仍承载微信 OAuth / 小程序登录 provider 协议,`api-server::wechat::provider` 只作为组合根 adapter,不在业务 handler 内散落 provider 构造。 验证: diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 36ff0a5d0..fdd615560 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -81,7 +81,7 @@ server-rs + Axum + SpacetimeDB - `spacetime-client`:后端访问 SpacetimeDB 的 typed facade。 - `module-*`:纯领域模型、命令、应用规则、领域事件和领域错误。 - `platform-*`:OSS、LLM、认证、语音等外部平台能力。 -- `shared-contracts` / `packages/shared`:前后端 DTO 和公开契约。 +- `shared-contracts` / `packages/shared`:前后端 DTO、公开契约和跨页面复用的无业务真相 TypeScript 代码;`packages/shared` 可承载共享 UI 组件与纯工具,但不承载领域规则、后端副作用或正式状态。 - 前端:表现、交互、临时 UI 状态和后端结果渲染。 明确废弃: diff --git a/src/components/common/PlatformMudPointWalletEntry.test.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx similarity index 98% rename from src/components/common/PlatformMudPointWalletEntry.test.tsx rename to packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx index 1156a20d4..e6aca12a9 100644 --- a/src/components/common/PlatformMudPointWalletEntry.test.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.test.tsx @@ -4,8 +4,8 @@ import { act, fireEvent, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { expect, test, vi } from 'vitest'; -import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry'; -import { formatMudPointCount } from './platformMudPointWalletModel'; +import { formatMudPointCount } from '../utils/format'; +import { PlatformMudPointWalletEntry } from './PlatformMudPointWalletEntry.tsx'; const breakdown = { totalPoints: 207, diff --git a/src/components/common/PlatformMudPointWalletEntry.tsx b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx similarity index 97% rename from src/components/common/PlatformMudPointWalletEntry.tsx rename to packages/shared/src/components/PlatformMudPointWalletEntry.tsx index 6f2b0f218..41d017a5d 100644 --- a/src/components/common/PlatformMudPointWalletEntry.tsx +++ b/packages/shared/src/components/PlatformMudPointWalletEntry.tsx @@ -8,10 +8,9 @@ import { useState, } from 'react'; -import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { formatMudPointCount } from './platformMudPointWalletModel'; - -const MUD_POINT_ICON_SRC = '/creation-home/topbar-wallet.png'; +import type { ProfileMudPointBalance } from '../contracts/runtime'; +import MUD_POINT_ICON_SRC from '../icons/topbar-wallet.png'; +import { formatMudPointCount } from '../utils/format'; export type PlatformMudPointWalletEntryProps = { balance: number | null; diff --git a/packages/shared/src/icons/topbar-wallet.png b/packages/shared/src/icons/topbar-wallet.png new file mode 100644 index 000000000..37043028d Binary files /dev/null and b/packages/shared/src/icons/topbar-wallet.png differ diff --git a/src/components/common/platformMudPointWalletModel.ts b/packages/shared/src/utils/format.ts similarity index 100% rename from src/components/common/platformMudPointWalletModel.ts rename to packages/shared/src/utils/format.ts diff --git a/src/components/image-editor/ImageCanvasTopbarView.test.tsx b/src/components/image-editor/ImageCanvasTopbarView.test.tsx index 1bbf80ec6..c9eb10d4b 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.test.tsx @@ -108,9 +108,6 @@ describe('ImageCanvasTopbarView', () => { }); expect(walletChip.textContent).toBe('泥点 1.2万'); - expect(walletChip.querySelector('img')?.getAttribute('src')).toBe( - '/creation-home/topbar-wallet.png', - ); await user.hover(walletChip); const details = screen.getByRole('dialog', { name: '泥点账户详情' }); diff --git a/src/components/image-editor/ImageCanvasTopbarView.tsx b/src/components/image-editor/ImageCanvasTopbarView.tsx index 72fa0e4ab..ba20c2e4c 100644 --- a/src/components/image-editor/ImageCanvasTopbarView.tsx +++ b/src/components/image-editor/ImageCanvasTopbarView.tsx @@ -7,8 +7,9 @@ import { X, } from 'lucide-react'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; + import type { ProfileMudPointBalance } from '../../../packages/shared/src/contracts/runtime'; -import { PlatformMudPointWalletEntry } from '../common/PlatformMudPointWalletEntry'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; diff --git a/src/components/platform-entry/PlatformProfileRechargeModal.tsx b/src/components/platform-entry/PlatformProfileRechargeModal.tsx index 26964cc30..f5acb0023 100644 --- a/src/components/platform-entry/PlatformProfileRechargeModal.tsx +++ b/src/components/platform-entry/PlatformProfileRechargeModal.tsx @@ -1,6 +1,8 @@ import QRCode from 'qrcode'; import { useCallback, useEffect, useState } from 'react'; +import { formatMudPointCount } from '@/packages/shared/src/utils/format.ts'; + import type { ProfileRechargeCenterResponse, ProfileRechargeProduct, @@ -8,7 +10,6 @@ import type { import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; -import { formatMudPointCount } from '../common/platformMudPointWalletModel'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformProfileSkeletonList } from '../common/PlatformProfileSkeletonList'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 336dded05..d9873356c 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -4338,9 +4338,6 @@ test('logged in create tab shows real wallet balance beside the brand', () => { expect(walletEntry?.className).toContain( 'platform-mobile-create-wallet-chip', ); - expect(walletButton.querySelector('img')?.getAttribute('src')).toBe( - '/creation-home/topbar-wallet.png', - ); expect( within(topbar as HTMLElement).getByRole('button', { name: '充值' }), ).toBeTruthy(); diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 702c6367e..505f99193 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -40,6 +40,8 @@ import { useState, } from 'react'; +import { PlatformMudPointWalletEntry } from '@/packages/shared/src/components/PlatformMudPointWalletEntry.tsx'; + import profileClockImage from '../../../media/profile/_Image (1).png'; import profileGamepadImage from '../../../media/profile/_Image (2).png'; import profileStillLifeImage from '../../../media/profile/_Image (3).png'; @@ -89,7 +91,6 @@ import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFilterToolbar } from '../common/PlatformFilterToolbar'; import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformMudPointWalletEntry } from '../common/PlatformMudPointWalletEntry'; import { PlatformNavigableListItem } from '../common/PlatformNavigableListItem'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { diff --git a/vitest.config.ts b/vitest.config.ts index 87ae74b8b..30f506d44 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,14 @@ +import path from 'node:path'; + import {defineConfig} from 'vitest/config'; export default defineConfig({ + resolve: { + alias: { + // Keep shared components' application-root imports resolvable in tests. + '@': path.resolve(__dirname, '.'), + }, + }, test: { environment: 'node', globals: true, @@ -12,6 +20,7 @@ export default defineConfig({ 'miniprogram/**/*.test.js', 'scripts/**/*.test.ts', 'packages/shared/src/**/*.test.ts', + 'packages/shared/src/**/*.test.tsx', ], }, });