zizi a7076871e4 feat(B1): 黄金闭环前端联调(step③) — 真连 staging + 有效遥测信封
step③ game-studio 联调(编译验证待 mini-desktop):
- request.ts 注入 tenant-id:1;user.ts token 取 VITE_STUDIO_TOKEN(=test1)
- vite.config.ts: VITE_API_BASE 非空则关 mock + 启动打印 baseURL/mock 状态
- runtimeApi.ts: 取包改委托 src/api/runtime.ts(带 base+token+tenant,修 Codex C3 裸 axios 同源)
- inject.ts: manifestUrl resolve 到 VITE_API_BASE + 带鉴权头(修 Codex C4)
- telemetry: 信封补 eventId(randomUUID)+ traceId 兜底非空;sendBeacon→fetch keepalive 带头(sendBeacon 不能带 Authorization/tenant)
- Play.vue onTelemetry 白名单原样转发 like/share/play_end(修 Codex H8 吞事件)
- 新增 game-studio/.env.staging + vite-env.d.ts;.gitignore 豁免 .env.staging(防 gitignore-swallows-frontend-source 同类坑)
- 注:Feed.vue 互动经 buildEnvelope traceId 兜底覆盖,免改

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:40:53 +00:00

111 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { fileURLToPath, URL } from 'node:url'
import type { IncomingMessage } from 'node:http'
import { defineConfig, loadEnv, type Connect, type PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
// 自写 mock 调度器DOM-freedev/preview 两端复用)。严格按 contracts/api-schemas 返回数据。
import { dispatch, MOCK_PREFIXES } from './src/mock/index.ts'
/**
* 读取请求体POST/PUT 等),解析为 JSON。
* connect 中间件层面手动收 chunk避免依赖 body-parser零额外依赖
*/
function readBody(req: IncomingMessage): Promise<unknown> {
return new Promise((resolve) => {
const chunks: Buffer[] = []
req.on('data', (c: Buffer) => chunks.push(c))
req.on('end', () => {
if (chunks.length === 0) {
resolve(undefined)
return
}
const raw = Buffer.concat(chunks).toString('utf-8')
try {
resolve(raw ? JSON.parse(raw) : undefined)
} catch {
// 非 JSON 体(如 sendBeacon 偶发直接透传原文handler 自行兜底
resolve(raw)
}
})
req.on('error', () => resolve(undefined))
})
}
/** 判断 url 是否命中 mock 前缀(/app-api、/admin-api */
function isMockUrl(url: string | undefined): boolean {
if (!url) return false
return MOCK_PREFIXES.some((p) => url === p || url.startsWith(p + '/') || url.startsWith(p + '?'))
}
/**
* connect 中间件:拦截 API 请求 → 调 dispatch → 写 JSON 包络。
* 未命中 mock 路由则调用 next() 放行(不误吞静态资源/HMR 等)。
*/
const mockMiddleware: Connect.NextHandleFunction = (req, res, next) => {
const url = req.url
if (!isMockUrl(url)) {
next()
return
}
void (async () => {
try {
const body = await readBody(req)
const result = await dispatch(req.method || 'GET', url as string, body)
if (result === null) {
// 命中前缀但无对应路由:返回 404 包络,便于排查“契约里没有的端点”被误调
res.statusCode = 404
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ code: 404, data: null, msg: `mock 未匹配: ${req.method} ${url}` }))
return
}
res.statusCode = 200
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(JSON.stringify(result))
} catch (err) {
// 兜底mock 自身异常也返回包络,不让请求悬挂
res.statusCode = 200
res.setHeader('Content-Type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ code: 1, data: null, msg: (err as Error)?.message || 'mock error' }))
}
})()
}
/**
* 自写 mock vite 插件。
* 关键:同时 hook configureServer(dev) 与 configurePreviewServer(preview)
* 两端都把 mockMiddleware 前置注入 connect 栈 → `vite` 与 `vite preview` 都能命中 mock验收要求
*/
function mockPlugin(): PluginOption {
return {
name: 'wanxiang-local-mock',
// 开发服务器vite dev
configureServer(server: { middlewares: Connect.Server }) {
server.middlewares.use(mockMiddleware)
},
// 预览服务器vite preview——验收闭环在 preview 跑,必须生效
configurePreviewServer(server: { middlewares: Connect.Server }) {
server.middlewares.use(mockMiddleware)
},
}
}
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// 读 env第三参 '' = 不限前缀,确保拿到 VITE_API_BASE
// VITE_API_BASE 非空 = 联调真连后端(如 staging此时关本地 mock空 = 同源 mock§2.3)。
const env = loadEnv(mode, process.cwd(), '')
const apiBase = env.VITE_API_BASE ?? ''
const mockEnabled = !apiBase
// 启动打印:一眼确认"真连后端"还是"mock"
console.log(`[wanxiang] mode=${mode} VITE_API_BASE=${apiBase || '(空=同源/mock)'} mock=${mockEnabled ? 'ON' : 'OFF(真连后端)'}`)
return {
// mockEnabled=false 时不注册 mock 插件,请求直达 VITE_API_BASE
plugins: [vue(), ...(mockEnabled ? [mockPlugin()] : [])],
resolve: {
alias: {
// '@' → src供 B 工位用 @/ 绝对导入A1 自身用相对导入,避免依赖 tsconfig paths
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
}
})