feat(admin): embed slot management with code snippet
P4b Task 5:/system/embed-slots 管理页(Task 8/9 页面模式:useCursorList
游标累积 + 对话框治理)。列表(名称/绑定相册/启停/configVersion/更新时间,
启停开关行内 PATCH → configVersion+1 toast 提示)+ 新建/编辑对话框
(设置表单位置/尺寸/间隔/语言/默认收起,intervalMs 契约区间夹取;编辑按
整体替换语义提交完整 settings)+ 绑定相册对话框(相册标题/ID 搜索选中 →
确认框携带 A22 文案「configVersion 将递增,官网无需改动」;清空即解绑)+
嵌入代码卡({EMBED_ORIGIN}/embed/v1/loader.js + data-slot 令牌,一键复制;
EMBED_ORIGIN=VITE_EMBED_ORIGIN 缺省本机 viewer dev,.env.example 登记)。
- API 层 src/api/embedSlots.ts + types/api/embedSlot.d.ts(契约镜像)
- vitest 9 例:表单→载荷(缺省+夹取)、编辑载荷收敛、snippet 拼接、
环境基址兜底、绑定变更分类、zh/en 键一致 + A22 短语断言
- Playwright e2e(真实 cn+ov 后端):新建→绑定(gov-photos 直种 +
seed-fixture 新增 sync-flush 模式经 bootstrap/真实发送管线投影 photos_ov)
→嵌入代码卡含 token→ov 公开 feed 返回相册照片→换绑 configVersion+1 +
feed 切换(A22)→停用 feed 404;admin playwright 配置补 ov webServer
- i18n zh/en(embedSlotsManage.* + menus.system.embedSlots)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -23,13 +23,25 @@
|
||||
// 常驻 worker,同 services/
|
||||
// api/test/watermark.e2e-
|
||||
// spec.ts 直驱模式)
|
||||
// sync-flush [albumId] cn→ov 种子同步(P4b Task 5
|
||||
// 展示位 e2e):albumId 提供
|
||||
// 时先打 cn bootstrap(当前
|
||||
// 态快照写回 outbox,直种
|
||||
// photos 行入公开 feed 的
|
||||
// 必经通道),再经 services/
|
||||
// sync dist 的
|
||||
// deliverPendingOnce 排空
|
||||
// outbox(真实 HMAC 发送
|
||||
// 管线;slot.updated 事件不
|
||||
// 随 bootstrap 走,须此排空;
|
||||
// albumId 省略或 '-' = 仅排空)
|
||||
// 幂等:账号存在即跳过;照片按确定性 photoId/objectKey upsert(同相册重复种
|
||||
// 不重复建行);watermark-run 对已 done 的任务返回 skipped 视为成功。
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const path = require('node:path')
|
||||
const { execSync } = require('node:child_process')
|
||||
const { existsSync, statSync } = require('node:fs')
|
||||
const { createHash } = require('node:crypto')
|
||||
const { createHash, createHmac } = require('node:crypto')
|
||||
|
||||
const dbCn = process.env.PHOTO_DB_CN
|
||||
const databaseUrl = process.env.DATABASE_URL_CN
|
||||
@@ -303,6 +315,56 @@ async function runWatermark(albumId) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sync-flush:cn→ov 种子同步(apps/viewer e2e helpers flushSync/flushOutbox
|
||||
* 的 execSync 版——apps/admin 不依赖 @photo/db-cn/@photo/contracts,经
|
||||
* services/{api,sync} 的 node_modules/dist 借道)。bootstrap 为 cn 内部
|
||||
* HMAC 端点(签名 = HMAC-SHA256(key, rawBody).hex,同 contracts
|
||||
* signSyncBody);发送器 dist 过期则先 tsc 构建(同 watermark-run 口径)。
|
||||
*/
|
||||
async function syncFlush(albumId) {
|
||||
const internalKey = process.env.SYNC_INTERNAL_KEY ?? 'dev-sync-secret-change-me'
|
||||
const cnBase = (process.env.CN_API_URL ?? 'http://127.0.0.1:8080').replace(/\/+$/, '')
|
||||
|
||||
if (albumId && albumId !== '-') {
|
||||
const rawBody = JSON.stringify({ albumId })
|
||||
const bootstrap = await fetch(`${cnBase}/v1/internal/sync/bootstrap`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-sync-timestamp': String(Math.floor(Date.now() / 1000)),
|
||||
'x-sync-signature': createHmac('sha256', internalKey).update(rawBody).digest('hex')
|
||||
},
|
||||
body: rawBody
|
||||
})
|
||||
if (!bootstrap.ok) {
|
||||
throw new Error(
|
||||
`seed-fixture: bootstrap 失败(${bootstrap.status}):${(await bootstrap.text()).slice(0, 300)}`
|
||||
)
|
||||
}
|
||||
console.log(`seed-fixture: bootstrap ok for album ${albumId}`)
|
||||
}
|
||||
|
||||
const syncRoot = path.resolve(__dirname, '..', '..', '..', 'services', 'sync')
|
||||
const distSender = path.join(syncRoot, 'dist', 'sender.js')
|
||||
const srcSender = path.join(syncRoot, 'src', 'sender.ts')
|
||||
const stale =
|
||||
!existsSync(distSender) ||
|
||||
(existsSync(srcSender) && statSync(srcSender).mtimeMs > statSync(distSender).mtimeMs)
|
||||
if (stale) {
|
||||
console.log('seed-fixture: building services/sync dist (stale or missing)...')
|
||||
execSync('npx tsc -p tsconfig.build.json', { cwd: syncRoot, stdio: 'inherit' })
|
||||
}
|
||||
|
||||
const { deliverPendingOnce } = require(distSender)
|
||||
await deliverPendingOnce({
|
||||
prisma,
|
||||
ovApiUrl: process.env.OV_API_URL ?? 'http://127.0.0.1:8081',
|
||||
internalKey
|
||||
})
|
||||
console.log('seed-fixture: outbox drained to ov')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [mode, argA, argB, argC, argD] = process.argv.slice(2)
|
||||
if (mode === 'staff') {
|
||||
@@ -315,6 +377,8 @@ async function main() {
|
||||
await seedWmPhotos(argA, argB, argC)
|
||||
} else if (mode === 'watermark-run') {
|
||||
await runWatermark(argA)
|
||||
} else if (mode === 'sync-flush') {
|
||||
await syncFlush(argA)
|
||||
} else {
|
||||
throw new Error(`seed-fixture: unknown mode "${mode}"`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user