fix: MindSpace remote sync, thumbnail fallback, and Plaza URL guards
Ensure page sync runs via pageSyncService in remote mode, fall back to workspace HTML when storage assets are missing, and prevent production Portal from linking to loopback Plaza URLs baked in at build time. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4
-3
@@ -167,10 +167,11 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
|
||||
# 历史列表每次展示/加载条数(默认 25,与 desktop 一致)
|
||||
# VITE_SESSION_PAGE_SIZE=25
|
||||
|
||||
# Plaza 发现广场(本机为主,105 已停用)
|
||||
# PLAZA_AUTO_APPROVE=true
|
||||
# Plaza 发现广场(独立域名 plaza.tkmind.cn;Portal 前端链接见 VITE_PLAZA_BASE)
|
||||
# PLAZA_PUBLIC_BASE=https://plaza.tkmind.cn
|
||||
# 本地 dev 脚本会自动注入 VITE_PLAZA_BASE=http://127.0.0.1:3001;勿把该值留进生产构建用的 .env
|
||||
# VITE_PLAZA_BASE=http://127.0.0.1:3001
|
||||
# 文档:docs/plaza-local.md | 本地:http://127.0.0.1:3001/plaza
|
||||
# 文档:docs/plaza-local.md | 本地:http://127.0.0.1:3001/plaza | 生产:https://plaza.tkmind.cn/plaza
|
||||
|
||||
# MindSpace(默认启用;子功能需按需开启)
|
||||
# MINDSPACE_ENABLED=false
|
||||
|
||||
@@ -24,6 +24,7 @@ bash scripts/check-release-ready.sh
|
||||
| 主题 | 文档 |
|
||||
|------|------|
|
||||
| MindSpace 公开页 `edit_file` 落盘 + 聊天 Finish 不丢消息 | [docs/regression-guards/mindspace-publish-and-chat-finish.md](docs/regression-guards/mindspace-publish-and-chat-finish.md) |
|
||||
| MindSpace remote 页面 sync + storage 缺失缩略图 fallback | [docs/regression-guards/mindspace-remote-page-sync-and-thumbnail.md](docs/regression-guards/mindspace-remote-page-sync-and-thumbnail.md) |
|
||||
|
||||
索引:[docs/regression-guards/README.md](docs/regression-guards/README.md)
|
||||
|
||||
@@ -32,6 +33,7 @@ bash scripts/check-release-ready.sh
|
||||
```bash
|
||||
npm run verify:mindspace-publish-guards
|
||||
npm run verify:mindspace-publish-guards:full
|
||||
npm run verify:mindspace-page-sync-guards
|
||||
```
|
||||
|
||||
发版脚本(`scripts/release-portal-runtime-prod.sh`)在未 `--skip-tests` 时也会执行相关 verify。
|
||||
@@ -43,6 +45,8 @@ npm run verify:mindspace-publish-guards:full
|
||||
- `conversation-display.mjs` + `src/utils/message.ts` - 用户消息不得展示 routing/skill 内部前缀
|
||||
- `src/hooks/useTKMindChat.ts` - `syncSessionMessages` merge + 重试
|
||||
- `server.mjs` - session snapshot 需 `hint_mc` 且 `hint_ua` 才走缓存
|
||||
- `mindspace-pages.mjs` - storage 缺失时 HTML 页回退读 workspace `relative_path`
|
||||
- `mindspace-page-sync-service.mjs` + `server.mjs` - remote 模式也必须 sync public HTML
|
||||
|
||||
代码内搜索 `REGRESSION GUARD` 可定位所有内联说明。
|
||||
|
||||
@@ -52,6 +56,11 @@ npm run verify:mindspace-publish-guards:full
|
||||
- `chat-finish-sync.test.mjs`
|
||||
- `conversation-display.test.mjs`
|
||||
|
||||
### MindSpace page sync + thumbnail
|
||||
|
||||
- `mindspace-page-sync-service.test.mjs`
|
||||
- `mindspace-pages.test.mjs`
|
||||
|
||||
## Cursor 专用规则(其它工具请读上文文档)
|
||||
|
||||
Cursor 额外加载:`.cursor/rules/mindspace-publish-chat-finish-guards.mdc`
|
||||
|
||||
@@ -104,6 +104,30 @@ pnpm setup:plaza-tunnel
|
||||
| `H5_PORT` | 8081 | Portal / API 端口 |
|
||||
| `PLAZA_LOCAL_HOST` | plaza.tkmind.cn | 本地域名 |
|
||||
| `PLAZA_PUBLIC_BASE` | https://plaza.tkmind.cn | 公开 URL 前缀 |
|
||||
| `VITE_PLAZA_BASE` | (见下) | Portal 前端「逛 Plaza」链接;**编译期**写入 dist |
|
||||
|
||||
### Portal「逛 Plaza」链接(MindSpace / 发布成功页)
|
||||
|
||||
Plaza 是独立域名 `plaza.tkmind.cn`,与 Portal `m.tkmind.cn` 不同。前端通过 `src/utils/publicUrl.ts` → `public-site-bases.mjs` 解析:
|
||||
|
||||
| 环境 | 期望链接 |
|
||||
|------|----------|
|
||||
| 本机 `127.0.0.1` / `localhost` | `http://127.0.0.1:3001/plaza` |
|
||||
| 生产 `m.tkmind.cn` | `https://plaza.tkmind.cn/plaza` |
|
||||
|
||||
**为何生产曾出现 127?** `VITE_*` 在 `vite build` 时写入 bundle。若本机 `.env` 含 `VITE_PLAZA_BASE=http://127.0.0.1:3001`,未覆盖就直接打生产包,会把 127 带进 `m.tkmind.cn`。
|
||||
|
||||
**彻底修复(三层):**
|
||||
|
||||
1. **运行时守卫**:在 `m.tkmind.cn` 等公网 host 上,即使 bundle 内是 loopback,也强制用 `https://plaza.tkmind.cn`(见 `public-site-bases.mjs`)。
|
||||
2. **构建默认值**:`scripts/build-portal-runtime.mjs` 在未显式设置时注入 `VITE_PLAZA_BASE=https://plaza.tkmind.cn`。
|
||||
3. **勿在生产构建用本地 .env**:发 Portal runtime 前确认 `npm run build` / `build-portal-runtime.mjs` 未读取带 127 的 `VITE_PLAZA_BASE`。
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node --test public-site-bases.test.mjs
|
||||
```
|
||||
|
||||
## 已弃用
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
| 文档 | 保护内容 |
|
||||
|------|----------|
|
||||
| [mindspace-publish-and-chat-finish.md](./mindspace-publish-and-chat-finish.md) | ① `edit_file` 覆盖 `public/*.html` ② Finish 后聊天不清空、不暴露 agent 内部前缀 |
|
||||
| [mindspace-remote-page-sync-and-thumbnail.md](./mindspace-remote-page-sync-and-thumbnail.md) | ① remote 模式 public HTML 入库 sync ② storage 缺失时缩略图/读页回退 workspace HTML |
|
||||
|
||||
## 自动化
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# MindSpace Remote 页面同步与缩略图回归守卫
|
||||
|
||||
日期:2026-07-05
|
||||
|
||||
## 事故症状
|
||||
|
||||
1. **缩略图 500**:`GET /api/mindspace/v1/pages/:pageId/thumbnail` 返回 500;日志/storage 为 `ENOENT`,但 `MindSpace/<userId>/public/*.html` 仍存在。
|
||||
2. **页面列表缺新生成页**:103 生产 `MINDSPACE_SERVER_ADAPTER=remote` 时,Agent 新生成的 `public/*.html` 不出现在「最近页面 / 全部页面」。
|
||||
3. **误判**:容易把缺页当成「MindSpace 路径解析错误」或「改回 local adapter 就能修缩略图」——两者根因不同。
|
||||
|
||||
## 根因
|
||||
|
||||
### 缩略图 500
|
||||
|
||||
- `h5_page_records` / `h5_asset_versions.storage_key` 指向 `data/mindspace/users/.../*.md`。
|
||||
- 磁盘上该 storage 文件可能已丢失或未写入(历史 sync、清理、quota、迁移)。
|
||||
- 页面 snapshot 仍保留 `relative_path: public/index.html`,工作区 HTML 是真实内容来源。
|
||||
- `loadPageThumbnailContext` / `getPage` 若**只读 storage**,会在 remote 8082 上直接抛错 → Portal 返回 500。
|
||||
|
||||
### Remote 下缺页
|
||||
|
||||
- Portal `GET /mindspace/v1/pages` 会调用 `syncUserGeneratedPages()`,将 `public/*.html` 入库。
|
||||
- 曾存在 `if (adapterKind === 'remote') return;`,导致 **8082 独立部署后 sync 永不执行**。
|
||||
- 测试环境多为 `local`,sync 正常 → 生产/测试行为不一致。
|
||||
|
||||
## 必须保留的行为
|
||||
|
||||
### 1. Workspace HTML fallback(读内容)
|
||||
|
||||
在 `mindspace-pages.mjs` 中,读取页面 content / 缩略图 HTML 时:
|
||||
|
||||
1. 优先读 `storage_key` 对应文件;
|
||||
2. 若 `ENOENT` 且 `page_type === 'html'` 且 snapshot 有 `relative_path` → 回退读 `resolvePublishDir(h5Root, userId)` 下对应 HTML;
|
||||
3. `getPage` 与 `renderThumbnail` **共用** `readPageContentWithWorkspaceFallback`,禁止只修一处。
|
||||
|
||||
### 2. Remote 也执行 page sync
|
||||
|
||||
1. 抽出 `mindspace-page-sync-service.mjs` → `pageSyncService.syncUserGeneratedPages(userId)`;
|
||||
2. 写入 `mindspace-server-adapter-contract.mjs`:`pageSyncService` binding;
|
||||
3. **Local**:8082 / Portal 内直接跑 `syncGeneratedPagesFromPublicAssets`;
|
||||
4. **Remote**:Portal 通过 RPC 调 8082 的 `pageSyncService.syncUserGeneratedPages`;
|
||||
5. **`server.mjs` 禁止**在 remote 模式 early-return 跳过 sync。
|
||||
|
||||
触发点(保持不变):
|
||||
|
||||
- `GET /mindspace/v1/pages` 列表前
|
||||
- 聊天 Finish 后(`onAfterFinish`)
|
||||
|
||||
### 3. 103 拓扑约束
|
||||
|
||||
- Portal:`8081`,`MINDSPACE_SERVER_ADAPTER=remote`
|
||||
- MindSpace Service:`8082`,跑 **local adapter**(含 pageSyncService + storage + h5Root)
|
||||
- 用户 HTML:`/Users/john/Project/Memind/MindSpace/<userId>/public/`
|
||||
- Storage:`data/mindspace/users/<userId>/...`
|
||||
|
||||
**禁止**用「改回 Portal local adapter」作为生产长期方案;只可用于本地对比验证。
|
||||
|
||||
## 排查入口
|
||||
|
||||
```bash
|
||||
# 1. 页面 DB 记录 vs storage vs workspace
|
||||
mysql ... # h5_page_records + h5_page_versions.source_snapshot_json + h5_asset_versions.storage_key
|
||||
|
||||
# 2. storage 是否存在
|
||||
ls -la data/mindspace/users/<userId>/pages/<assetId>/versions/
|
||||
|
||||
# 3. workspace HTML 是否存在
|
||||
ls -la MindSpace/<userId>/public/
|
||||
|
||||
# 4. 8082 健康与 adapter
|
||||
curl -s http://127.0.0.1:8082/health
|
||||
|
||||
# 5. 缩略图 RPC(需登录态;或直接在 8082 侧调 pageService.renderThumbnail)
|
||||
curl -I 'https://m.tkmind.cn/api/mindspace/v1/pages/<pageId>/thumbnail'
|
||||
```
|
||||
|
||||
## 改相关文件前必跑
|
||||
|
||||
```bash
|
||||
npm run verify:mindspace-page-sync-guards
|
||||
node --test mindspace-page-sync-service.test.mjs mindspace-pages.test.mjs
|
||||
```
|
||||
|
||||
## Checklist(发 103 前)
|
||||
|
||||
- [ ] `mindspace-pages.mjs` 含 workspace fallback
|
||||
- [ ] `pageSyncService` 在 contract + local runtime + remote proxy 中齐全
|
||||
- [ ] `server.mjs` 无 remote sync skip
|
||||
- [ ] **8082 与 Portal 均发布**(8082 承载 renderThumbnail + sync)
|
||||
- [ ] 用真实 pageId 验证 thumbnail 200 + 列表含新页
|
||||
|
||||
## 相关单测
|
||||
|
||||
- `mindspace-page-sync-service.test.mjs`
|
||||
- `mindspace-pages.test.mjs`(storage 缺失时 thumbnail / getPage fallback)
|
||||
- `mindspace-page-sync.test.mjs`(public dir → DB)
|
||||
|
||||
## 代码内标记
|
||||
|
||||
搜索 `REGRESSION GUARD: mindspace-page-sync-thumbnail` 定位内联说明。
|
||||
@@ -6,6 +6,7 @@ import { createAgentJobService } from './mindspace-agent-jobs.mjs';
|
||||
import { createAssetAgentService } from './mindspace-asset-agent.mjs';
|
||||
import { createPageLiveEditService } from './mindspace-page-live-edit.mjs';
|
||||
import { createPageService } from './mindspace-pages.mjs';
|
||||
import { createPageSyncService } from './mindspace-page-sync-service.mjs';
|
||||
import { createPublicationService } from './mindspace-publications.mjs';
|
||||
import { createMindSpaceLocalRuntime } from './mindspace-runtime-config.mjs';
|
||||
|
||||
@@ -19,6 +20,7 @@ export function createMindSpaceLocalRuntimeServices({
|
||||
conversationPackageBackfill,
|
||||
conversationPackagePublicHtmlHydrator,
|
||||
logger = console,
|
||||
syncWorkspaceAssets = null,
|
||||
} = {}) {
|
||||
let conversationPackageRegistry = null;
|
||||
const runtime = createMindSpaceLocalRuntime({
|
||||
@@ -51,6 +53,14 @@ export function createMindSpaceLocalRuntimeServices({
|
||||
storageRoot: runtime.storageRoot,
|
||||
conversationPackageRegistry,
|
||||
});
|
||||
const pageSyncService = createPageSyncService({
|
||||
pool,
|
||||
pageService,
|
||||
assetService,
|
||||
h5Root,
|
||||
syncWorkspaceAssets,
|
||||
logger,
|
||||
});
|
||||
const publicationService = createPublicationService(pool, {
|
||||
h5Root,
|
||||
storageRoot: runtime.storageRoot,
|
||||
@@ -80,6 +90,7 @@ export function createMindSpaceLocalRuntimeServices({
|
||||
conversationPackageRegistry,
|
||||
assetService,
|
||||
pageService,
|
||||
pageSyncService,
|
||||
publicationService,
|
||||
cleanupService,
|
||||
agentJobService,
|
||||
|
||||
@@ -30,6 +30,7 @@ test('createMindSpaceLocalRuntimeServices assembles local runtime-backed service
|
||||
assert.ok(services.conversationPackageRegistry);
|
||||
assert.ok(services.assetService);
|
||||
assert.ok(services.pageService);
|
||||
assert.ok(services.pageSyncService);
|
||||
assert.ok(services.publicationService);
|
||||
assert.ok(services.cleanupService);
|
||||
assert.ok(services.agentJobService);
|
||||
|
||||
@@ -13,8 +13,10 @@ export function createMindSpaceLocalServerAdapter({
|
||||
registerPublicHtmlArtifactsForConversation,
|
||||
logger = console,
|
||||
setIntervalFn = setInterval,
|
||||
syncWorkspaceAssetsEnabled = true,
|
||||
} = {}) {
|
||||
const services = createMindSpaceLocalRuntimeServices({
|
||||
let services;
|
||||
services = createMindSpaceLocalRuntimeServices({
|
||||
pool,
|
||||
h5Root,
|
||||
env,
|
||||
@@ -36,6 +38,10 @@ export function createMindSpaceLocalServerAdapter({
|
||||
registerPublicHtmlArtifactsForConversation,
|
||||
}),
|
||||
logger,
|
||||
syncWorkspaceAssets: syncWorkspaceAssetsEnabled
|
||||
? (targetUserId, options) =>
|
||||
services?.assetService?.syncWorkspaceAssets?.(targetUserId, options)
|
||||
: null,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs';
|
||||
import { resolvePublishDir } from './user-publish.mjs';
|
||||
|
||||
export function createPageSyncService({
|
||||
pool,
|
||||
pageService,
|
||||
assetService,
|
||||
h5Root,
|
||||
syncWorkspaceAssets = null,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
const syncInFlight = new Map();
|
||||
|
||||
async function syncUserGeneratedPages(userId) {
|
||||
if (!pool || !pageService || !userId) {
|
||||
return { created: 0, skipped: 0, updated: 0 };
|
||||
}
|
||||
|
||||
let inFlight = syncInFlight.get(userId);
|
||||
if (!inFlight) {
|
||||
inFlight = syncGeneratedPagesFromPublicAssets({
|
||||
pool,
|
||||
pageService,
|
||||
assetService,
|
||||
userId,
|
||||
publishDir: h5Root ? resolvePublishDir(h5Root, { id: userId }) : null,
|
||||
syncWorkspaceAssets,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.warn?.('[MindSpace] page sync failed:', error?.message ?? error);
|
||||
return { created: 0, skipped: 0, updated: 0 };
|
||||
})
|
||||
.finally(() => {
|
||||
if (syncInFlight.get(userId) === inFlight) {
|
||||
syncInFlight.delete(userId);
|
||||
}
|
||||
});
|
||||
syncInFlight.set(userId, inFlight);
|
||||
}
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
return { syncUserGeneratedPages };
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { createPageSyncService } from './mindspace-page-sync-service.mjs';
|
||||
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||
|
||||
test('syncUserGeneratedPages registers html files from publish public dir', async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-page-sync-service-'));
|
||||
const h5Root = tmp;
|
||||
const userId = 'user-1';
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
||||
const html =
|
||||
'<!doctype html><html><head><title>Sync Demo</title></head><body><h1>Hi</h1></body></html>';
|
||||
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
|
||||
await fs.writeFile(path.join(publishDir, 'public/index.html'), html);
|
||||
|
||||
const calls = [];
|
||||
const pageService = {
|
||||
createFromChat: async (_userId, input, source) => {
|
||||
calls.push({ input, source });
|
||||
return { id: 'page-1' };
|
||||
},
|
||||
updatePage: async () => assert.fail('should create, not update'),
|
||||
};
|
||||
const pool = {
|
||||
query: async (sql) => {
|
||||
if (String(sql).includes('FROM h5_page_records')) return [[]];
|
||||
return [[]];
|
||||
},
|
||||
};
|
||||
|
||||
const pageSyncService = createPageSyncService({
|
||||
pool,
|
||||
pageService,
|
||||
assetService: null,
|
||||
h5Root,
|
||||
});
|
||||
const result = await pageSyncService.syncUserGeneratedPages(userId);
|
||||
|
||||
assert.equal(result.created, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].input.title, 'Sync Demo');
|
||||
assert.equal(calls[0].source.snapshot.relative_path, 'public/index.html');
|
||||
});
|
||||
|
||||
test('syncUserGeneratedPages deduplicates concurrent sync for the same user', async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-page-sync-dedupe-'));
|
||||
const h5Root = tmp;
|
||||
const userId = 'user-1';
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
||||
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(publishDir, 'public/index.html'),
|
||||
'<html><head><title>Dedupe</title></head><body>Hi</body></html>',
|
||||
);
|
||||
|
||||
let active = 0;
|
||||
let maxActive = 0;
|
||||
const pageService = {
|
||||
createFromChat: async () => {
|
||||
active += 1;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
active -= 1;
|
||||
return { id: 'page-1' };
|
||||
},
|
||||
};
|
||||
const pool = {
|
||||
query: async (sql) => {
|
||||
if (String(sql).includes('FROM h5_page_records')) return [[]];
|
||||
return [[]];
|
||||
},
|
||||
};
|
||||
const pageSyncService = createPageSyncService({
|
||||
pool,
|
||||
pageService,
|
||||
h5Root,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
pageSyncService.syncUserGeneratedPages(userId),
|
||||
pageSyncService.syncUserGeneratedPages(userId),
|
||||
]);
|
||||
|
||||
assert.equal(maxActive, 1);
|
||||
});
|
||||
+52
-5
@@ -309,6 +309,45 @@ export function createPageService(pool, options = {}) {
|
||||
throw lastError ?? Object.assign(new Error('存储文件不存在'), { code: 'storage_not_found' });
|
||||
};
|
||||
|
||||
// REGRESSION GUARD: mindspace-page-sync-thumbnail — storage ENOENT 时回退读 workspace HTML
|
||||
const readPageContentWithWorkspaceFallback = async ({
|
||||
userId,
|
||||
storageKey,
|
||||
sourceSnapshotJson,
|
||||
pageType,
|
||||
}) => {
|
||||
let snapshot = {};
|
||||
try {
|
||||
snapshot = parseJsonColumn(sourceSnapshotJson);
|
||||
} catch {
|
||||
snapshot = {};
|
||||
}
|
||||
const workspaceHtmlRelativePath =
|
||||
pageType === 'html' ? snapshot.relative_path ?? null : null;
|
||||
const workspacePublishDir = workspaceHtmlRelativePath
|
||||
? await resolveWorkspacePublishDir(userId)
|
||||
: null;
|
||||
|
||||
try {
|
||||
const storagePath = await resolveReadableStoragePath(storageKey);
|
||||
return await fs.readFile(storagePath, 'utf8');
|
||||
} catch (error) {
|
||||
const storageMissing = error?.code === 'ENOENT' || error?.code === 'storage_not_found';
|
||||
if (!storageMissing || !workspacePublishDir || !workspaceHtmlRelativePath) {
|
||||
throw error;
|
||||
}
|
||||
const workspaceHtmlPath = path.join(workspacePublishDir, workspaceHtmlRelativePath);
|
||||
try {
|
||||
return await fs.readFile(workspaceHtmlPath, 'utf8');
|
||||
} catch (workspaceError) {
|
||||
if (workspaceError?.code === 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
throw workspaceError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const writeVersionContent = async (userId, assetId, versionId, content) => {
|
||||
const storageKey = path.posix.join(
|
||||
'users',
|
||||
@@ -668,7 +707,7 @@ export function createPageService(pool, options = {}) {
|
||||
|
||||
async function getPage(userId, pageId) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT p.*, c.category_code, pv.version_no, av.storage_key
|
||||
`SELECT p.*, c.category_code, pv.version_no, av.storage_key, pv.source_snapshot_json
|
||||
FROM h5_page_records p
|
||||
JOIN h5_space_categories c ON c.id = p.category_id AND c.user_id = p.user_id
|
||||
JOIN h5_page_versions pv ON pv.id = p.current_version_id
|
||||
@@ -679,8 +718,12 @@ export function createPageService(pool, options = {}) {
|
||||
);
|
||||
const row = rows[0];
|
||||
if (!row) throw pageError('页面不存在', 'page_not_found');
|
||||
const storagePath = await resolveReadableStoragePath(row.storage_key);
|
||||
const content = await fs.readFile(storagePath, 'utf8');
|
||||
const content = await readPageContentWithWorkspaceFallback({
|
||||
userId,
|
||||
storageKey: row.storage_key,
|
||||
sourceSnapshotJson: row.source_snapshot_json,
|
||||
pageType: row.page_type,
|
||||
});
|
||||
return pageResponse({ ...row, content });
|
||||
}
|
||||
|
||||
@@ -1265,8 +1308,12 @@ export function createPageService(pool, options = {}) {
|
||||
if (row.page_type !== 'html') {
|
||||
throw pageError('该页面不支持缩略图', 'thumbnail_not_supported');
|
||||
}
|
||||
const storagePath = await resolveReadableStoragePath(row.storage_key);
|
||||
const content = await fs.readFile(storagePath, 'utf8');
|
||||
const content = await readPageContentWithWorkspaceFallback({
|
||||
userId,
|
||||
storageKey: row.storage_key,
|
||||
sourceSnapshotJson: row.source_snapshot_json,
|
||||
pageType: row.page_type,
|
||||
});
|
||||
let snapshot = {};
|
||||
try {
|
||||
snapshot = parseJsonColumn(row.source_snapshot_json);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||
import { createPageService, pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
|
||||
|
||||
test('parseJsonColumn accepts mysql json objects and strings', () => {
|
||||
const objectValue = { relative_path: 'public/demo.html', auto_synced: true };
|
||||
@@ -208,3 +212,86 @@ test('registerPageArtifactForConversation skips missing registry or session safe
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
test('renderThumbnail falls back to workspace html when storage asset is missing', async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-thumb-'));
|
||||
const storageRoot = path.join(tmp, 'storage');
|
||||
const h5Root = tmp;
|
||||
const userId = 'user-1';
|
||||
const pageId = 'page-1';
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
||||
const html =
|
||||
'<!doctype html><html><head><title>Demo</title></head><body><h1>Hi</h1></body></html>';
|
||||
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
|
||||
await fs.writeFile(path.join(publishDir, 'public/index.html'), html);
|
||||
|
||||
const pool = {
|
||||
query: async (sql) => {
|
||||
if (String(sql).includes('FROM h5_page_records')) {
|
||||
return [[{
|
||||
title: 'Demo',
|
||||
summary: '',
|
||||
page_type: 'html',
|
||||
version_no: 1,
|
||||
storage_key: `users/${userId}/pages/missing/versions/v1.md`,
|
||||
source_snapshot_json: { relative_path: 'public/index.html' },
|
||||
}]];
|
||||
}
|
||||
if (String(sql).includes('FROM h5_users')) {
|
||||
return [[{ username: 'demo' }]];
|
||||
}
|
||||
return [[]];
|
||||
},
|
||||
};
|
||||
|
||||
const pageService = createPageService(pool, { storageRoot, h5Root });
|
||||
const svg = await pageService.renderThumbnail(userId, pageId);
|
||||
assert.match(svg, /<svg/i);
|
||||
assert.match(svg, /Demo/);
|
||||
});
|
||||
|
||||
test('getPage falls back to workspace html when storage asset is missing', async () => {
|
||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-get-page-'));
|
||||
const storageRoot = path.join(tmp, 'storage');
|
||||
const h5Root = tmp;
|
||||
const userId = 'user-1';
|
||||
const pageId = 'page-1';
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, userId);
|
||||
const html =
|
||||
'<!doctype html><html><head><title>Page Demo</title></head><body><h1>Body</h1></body></html>';
|
||||
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
|
||||
await fs.writeFile(path.join(publishDir, 'public/index.html'), html);
|
||||
|
||||
const pool = {
|
||||
query: async (sql) => {
|
||||
if (String(sql).includes('FROM h5_page_records')) {
|
||||
return [[{
|
||||
id: pageId,
|
||||
user_id: userId,
|
||||
title: 'Page Demo',
|
||||
summary: '',
|
||||
page_type: 'html',
|
||||
template_id: 'static-html',
|
||||
current_version_id: 'version-1',
|
||||
status: 'draft',
|
||||
visibility: 'private',
|
||||
category_code: 'draft',
|
||||
version_no: 1,
|
||||
storage_key: `users/${userId}/pages/missing/versions/v1.md`,
|
||||
source_snapshot_json: { relative_path: 'public/index.html' },
|
||||
created_at: 1,
|
||||
updated_at: 1,
|
||||
}]];
|
||||
}
|
||||
if (String(sql).includes('FROM h5_users')) {
|
||||
return [[{ username: 'demo' }]];
|
||||
}
|
||||
return [[]];
|
||||
},
|
||||
};
|
||||
|
||||
const pageService = createPageService(pool, { storageRoot, h5Root });
|
||||
const page = await pageService.getPage(userId, pageId);
|
||||
assert.equal(page.title, 'Page Demo');
|
||||
assert.match(page.content, /<h1>Body<\/h1>/);
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ export const MINDSPACE_SERVER_ADAPTER_BINDINGS = Object.freeze({
|
||||
'updatePage',
|
||||
'uploadThumbnail',
|
||||
]),
|
||||
pageSyncService: Object.freeze(['syncUserGeneratedPages']),
|
||||
pageLiveEditService: Object.freeze(['applyAgentPatch', 'bindSession', 'getRevisionSnapshot']),
|
||||
assetAgentService: Object.freeze(['applyAgentDelete']),
|
||||
publicationService: Object.freeze([
|
||||
|
||||
+2
-1
@@ -51,12 +51,13 @@
|
||||
"smoke:memory-v2-qdrant": "node scripts/smoke-memory-v2-qdrant.mjs",
|
||||
"smoke:memory-v2-external": "node scripts/smoke-memory-v2-external.mjs",
|
||||
"mock:memory-v2-services": "node scripts/mock-memory-v2-services.mjs",
|
||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
||||
"test:mindspace-service": "node --test mindspace-service/mindspace-rpc-server.test.mjs",
|
||||
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
||||
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
||||
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
||||
"verify:mindspace-publish-guards:full": "node scripts/verify-mindspace-publish-guards.mjs --with-runtime",
|
||||
"verify:mindspace-page-sync-guards": "node scripts/verify-mindspace-page-sync-guards.mjs",
|
||||
"verify:wechat-channel-isolation": "node scripts/check-wechat-channel-isolation.mjs",
|
||||
"test:mindspace-e2e": "node scripts/mindspace-e2e.mjs",
|
||||
"test:mindspace-pages-e2e": "node scripts/mindspace-pages-e2e.mjs",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
DEFAULT_PLAZA_PUBLIC_BASE,
|
||||
DEFAULT_LOCAL_PLAZA_PUBLIC_BASE,
|
||||
resolvePlazaHomePath,
|
||||
resolvePlazaPostPath,
|
||||
resolvePlazaPublicBase,
|
||||
} from './src/utils/public-site-bases.mjs';
|
||||
|
||||
test('resolvePlazaPublicBase uses local default on localhost without configured base', () => {
|
||||
assert.equal(
|
||||
resolvePlazaPublicBase({ hostname: '127.0.0.1', dev: false }),
|
||||
DEFAULT_LOCAL_PLAZA_PUBLIC_BASE,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolvePlazaPublicBase uses plaza.tkmind.cn on production host without configured base', () => {
|
||||
assert.equal(
|
||||
resolvePlazaPublicBase({ hostname: 'm.tkmind.cn', dev: false }),
|
||||
DEFAULT_PLAZA_PUBLIC_BASE,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolvePlazaPublicBase ignores loopback configured base on production host', () => {
|
||||
assert.equal(
|
||||
resolvePlazaPublicBase({
|
||||
configuredBase: 'http://127.0.0.1:3001',
|
||||
hostname: 'm.tkmind.cn',
|
||||
dev: false,
|
||||
}),
|
||||
DEFAULT_PLAZA_PUBLIC_BASE,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolvePlazaPublicBase keeps loopback configured base on local host', () => {
|
||||
assert.equal(
|
||||
resolvePlazaPublicBase({
|
||||
configuredBase: 'http://127.0.0.1:3001',
|
||||
hostname: '127.0.0.1',
|
||||
dev: false,
|
||||
}),
|
||||
'http://127.0.0.1:3001',
|
||||
);
|
||||
});
|
||||
|
||||
test('resolvePlazaPublicBase respects explicit production configured base', () => {
|
||||
assert.equal(
|
||||
resolvePlazaPublicBase({
|
||||
configuredBase: 'https://plaza.tkmind.cn',
|
||||
hostname: 'm.tkmind.cn',
|
||||
dev: false,
|
||||
}),
|
||||
'https://plaza.tkmind.cn',
|
||||
);
|
||||
});
|
||||
|
||||
test('resolvePlazaHomePath and resolvePlazaPostPath append plaza routes', () => {
|
||||
assert.equal(resolvePlazaHomePath('https://plaza.tkmind.cn'), 'https://plaza.tkmind.cn/plaza');
|
||||
assert.equal(
|
||||
resolvePlazaPostPath('https://plaza.tkmind.cn', 'post-1'),
|
||||
'https://plaza.tkmind.cn/plaza/p/post-1',
|
||||
);
|
||||
});
|
||||
@@ -81,7 +81,18 @@ async function writeFile(targetPath, content) {
|
||||
async function buildFrontend() {
|
||||
if (skipBuild) return;
|
||||
console.log('==> 构建 Portal 前端 dist');
|
||||
await run('npm', ['run', 'build']);
|
||||
const buildEnv = {
|
||||
...process.env,
|
||||
VITE_PLAZA_BASE:
|
||||
process.env.VITE_PLAZA_BASE ??
|
||||
process.env.PLAZA_PUBLIC_BASE ??
|
||||
'https://plaza.tkmind.cn',
|
||||
VITE_MINDSPACE_BASE:
|
||||
process.env.VITE_MINDSPACE_BASE ??
|
||||
process.env.H5_PUBLIC_BASE_URL ??
|
||||
'https://m.tkmind.cn',
|
||||
};
|
||||
await run('npm', ['run', 'build'], { env: buildEnv });
|
||||
}
|
||||
|
||||
async function bundleServer() {
|
||||
@@ -397,6 +408,11 @@ async function writeMetadata() {
|
||||
' TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1',
|
||||
' H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT',
|
||||
'',
|
||||
'Frontend compile-time public URLs (baked into dist at build):',
|
||||
' VITE_PLAZA_BASE defaults to https://plaza.tkmind.cn in build-portal-runtime.mjs',
|
||||
' VITE_MINDSPACE_BASE defaults to H5_PUBLIC_BASE_URL or https://m.tkmind.cn',
|
||||
' Do not build production dist with VITE_PLAZA_BASE=http://127.0.0.1:* in .env',
|
||||
'',
|
||||
'Deployment and operations transport:',
|
||||
' H5 public domain: m.tkmind.cn',
|
||||
' Current public path: m.tkmind.cn -> 105 nginx -> 103 Portal :8081',
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
||||
|
||||
function read(relativePath) {
|
||||
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
function assertIncludes(label, source, snippet) {
|
||||
if (!source.includes(snippet)) {
|
||||
throw new Error(`${label} is missing required guard snippet:\n${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
const serverSource = read('server.mjs');
|
||||
const pagesSource = read('mindspace-pages.mjs');
|
||||
const contractSource = read('mindspace-server-adapter-contract.mjs');
|
||||
|
||||
assertIncludes(
|
||||
'server.mjs syncUserGeneratedPages',
|
||||
serverSource,
|
||||
'await mindSpacePageSync.syncUserGeneratedPages(userId);',
|
||||
);
|
||||
if (serverSource.includes("mindSpaceServerRuntime.adapterKind === 'remote'")) {
|
||||
throw new Error(
|
||||
'server.mjs must not skip page sync in remote mode; use pageSyncService RPC instead',
|
||||
);
|
||||
}
|
||||
|
||||
assertIncludes(
|
||||
'mindspace-server-adapter-contract.mjs',
|
||||
contractSource,
|
||||
"pageSyncService: Object.freeze(['syncUserGeneratedPages'])",
|
||||
);
|
||||
|
||||
assertIncludes(
|
||||
'mindspace-pages.mjs',
|
||||
pagesSource,
|
||||
'readPageContentWithWorkspaceFallback',
|
||||
);
|
||||
assertIncludes(
|
||||
'mindspace-pages.mjs workspace fallback',
|
||||
pagesSource,
|
||||
"snapshot.relative_path ?? null",
|
||||
);
|
||||
|
||||
const publicSiteBasesSource = read('src/utils/public-site-bases.mjs');
|
||||
const publicUrlSource = read('src/utils/publicUrl.ts');
|
||||
const buildRuntimeSource = read('scripts/build-portal-runtime.mjs');
|
||||
|
||||
assertIncludes(
|
||||
'public-site-bases.mjs loopback guard',
|
||||
publicSiteBasesSource,
|
||||
'isLoopbackPublicBase',
|
||||
);
|
||||
assertIncludes(
|
||||
'publicUrl.ts uses public-site-bases',
|
||||
publicUrlSource,
|
||||
"from './public-site-bases.mjs'",
|
||||
);
|
||||
assertIncludes(
|
||||
'build-portal-runtime.mjs production plaza default',
|
||||
buildRuntimeSource,
|
||||
'https://plaza.tkmind.cn',
|
||||
);
|
||||
|
||||
console.log('mindspace page sync + thumbnail regression guards ok');
|
||||
+6
-30
@@ -122,7 +122,6 @@ import {
|
||||
normalizePublicHtmlRelativePath,
|
||||
syncPublicHtmlAfterFinish,
|
||||
} from './mindspace-public-finish-sync.mjs';
|
||||
import { syncGeneratedPagesFromPublicAssets } from './mindspace-page-sync.mjs';
|
||||
import { extractCoverSignals, generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
|
||||
import {
|
||||
extractSharePreviewMeta,
|
||||
@@ -299,6 +298,7 @@ let mindSpaceAudit = null;
|
||||
let mindSpaceServiceFacade = null;
|
||||
let mindSpaceConversationPackageRegistry = null;
|
||||
let mindSpacePages = null;
|
||||
let mindSpacePageSync = null;
|
||||
let mindSpacePageLiveEdit = null;
|
||||
let mindSpaceAssetAgent = null;
|
||||
let mindSpacePageEditSession = null;
|
||||
@@ -363,6 +363,7 @@ async function bootstrapUserAuth() {
|
||||
resolveUserIdForAgentSession,
|
||||
resolveSessionSnapshot: (sessionId) => sessionSnapshotService?.get?.(sessionId),
|
||||
registerPublicHtmlArtifactsForConversation,
|
||||
syncWorkspaceAssetsEnabled: WORKSPACE_MAINTENANCE_ENABLED,
|
||||
remote: mindSpaceServerRuntime.remote,
|
||||
logger: console,
|
||||
}),
|
||||
@@ -372,6 +373,7 @@ async function bootstrapUserAuth() {
|
||||
mindSpaceConversationPackageRegistry = mindSpaceRuntimeAdapter.conversationPackageRegistry;
|
||||
mindSpaceAssets = mindSpaceRuntimeAdapter.assetService;
|
||||
mindSpacePages = mindSpaceRuntimeAdapter.pageService;
|
||||
mindSpacePageSync = mindSpaceRuntimeAdapter.pageSyncService;
|
||||
mindSpacePageLiveEdit = mindSpaceRuntimeAdapter.pageLiveEditService;
|
||||
mindSpaceAssetAgent = mindSpaceRuntimeAdapter.assetAgentService;
|
||||
mindSpacePublications = mindSpaceRuntimeAdapter.publicationService;
|
||||
@@ -2942,36 +2944,10 @@ async function resolveExistingSavedPage(userId, { sessionId, messageId, relative
|
||||
|
||||
const SAVE_TARGET_CATEGORIES = new Set(['draft', 'oa', 'public']);
|
||||
|
||||
const pageSyncInFlight = new Map();
|
||||
|
||||
// REGRESSION GUARD: mindspace-page-sync-thumbnail — remote 也经 pageSyncService RPC 同步 public HTML
|
||||
async function syncUserGeneratedPages(userId) {
|
||||
if (!mindSpacePages || !authPool || !userId) return;
|
||||
if (mindSpaceServerRuntime.adapterKind === 'remote') return;
|
||||
|
||||
let inFlight = pageSyncInFlight.get(userId);
|
||||
if (!inFlight) {
|
||||
inFlight = syncGeneratedPagesFromPublicAssets({
|
||||
pool: authPool,
|
||||
pageService: mindSpacePages,
|
||||
assetService: mindSpaceAssets,
|
||||
userId,
|
||||
publishDir: resolveMindSpaceUserPublishDir(__dirname, { id: userId }),
|
||||
syncWorkspaceAssets:
|
||||
mindSpaceAssets && WORKSPACE_MAINTENANCE_ENABLED
|
||||
? (targetUserId, options) => mindSpaceAssets.syncWorkspaceAssets(targetUserId, options)
|
||||
: null,
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[MindSpace] page sync failed:', error?.message ?? error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (pageSyncInFlight.get(userId) === inFlight) {
|
||||
pageSyncInFlight.delete(userId);
|
||||
}
|
||||
});
|
||||
pageSyncInFlight.set(userId, inFlight);
|
||||
}
|
||||
await inFlight;
|
||||
if (!mindSpacePageSync || !userId) return;
|
||||
await mindSpacePageSync.syncUserGeneratedPages(userId);
|
||||
}
|
||||
|
||||
async function resolveChatSaveBundle(user, h5Root, input = {}) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export const DEFAULT_PLAZA_PUBLIC_BASE = 'https://plaza.tkmind.cn';
|
||||
export const DEFAULT_LOCAL_PLAZA_PUBLIC_BASE = 'http://127.0.0.1:3001';
|
||||
|
||||
export function isLocalSiteHost(hostname) {
|
||||
const host = String(hostname ?? '').trim().toLowerCase();
|
||||
return !host || host === 'localhost' || host === '127.0.0.1' || host.endsWith('.localhost');
|
||||
}
|
||||
|
||||
export function isLoopbackPublicBase(base) {
|
||||
const value = String(base ?? '').trim();
|
||||
if (!value) return false;
|
||||
return (
|
||||
/^https?:\/\/127\.0\.0\.1(?::\d+)?(?:\/|$)/i.test(value) ||
|
||||
/^https?:\/\/localhost(?::\d+)?(?:\/|$)/i.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
export function resolvePlazaPublicBase({
|
||||
configuredBase = '',
|
||||
dev = false,
|
||||
hostname = '',
|
||||
} = {}) {
|
||||
const configured = String(configuredBase ?? '').trim().replace(/\/$/, '');
|
||||
const localHost = isLocalSiteHost(hostname);
|
||||
if (configured) {
|
||||
if (!localHost && !dev && isLoopbackPublicBase(configured)) {
|
||||
return DEFAULT_PLAZA_PUBLIC_BASE;
|
||||
}
|
||||
return configured;
|
||||
}
|
||||
if (dev || localHost) {
|
||||
return DEFAULT_LOCAL_PLAZA_PUBLIC_BASE;
|
||||
}
|
||||
return DEFAULT_PLAZA_PUBLIC_BASE;
|
||||
}
|
||||
|
||||
export function resolvePlazaHomePath(base) {
|
||||
return `${String(base ?? '').replace(/\/$/, '')}/plaza`;
|
||||
}
|
||||
|
||||
export function resolvePlazaPostPath(base, postId) {
|
||||
return `${String(base ?? '').replace(/\/$/, '')}/plaza/p/${encodeURIComponent(postId)}`;
|
||||
}
|
||||
+16
-16
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
resolvePlazaHomePath,
|
||||
resolvePlazaPostPath,
|
||||
resolvePlazaPublicBase,
|
||||
} from './public-site-bases.mjs';
|
||||
|
||||
export function resolvePublicPageUrl(publicUrl: string): string {
|
||||
const value = String(publicUrl ?? '').trim();
|
||||
if (!value) return value;
|
||||
@@ -7,26 +13,20 @@ export function resolvePublicPageUrl(publicUrl: string): string {
|
||||
return `${window.location.origin}/${value.replace(/^\/+/, '')}`;
|
||||
}
|
||||
|
||||
function resolvedPlazaBase(): string {
|
||||
return resolvePlazaPublicBase({
|
||||
configuredBase: import.meta.env.VITE_PLAZA_BASE,
|
||||
dev: import.meta.env.DEV,
|
||||
hostname: typeof window !== 'undefined' ? window.location.hostname : '',
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePlazaPostUrl(postId: string): string {
|
||||
const configured = String(import.meta.env.VITE_PLAZA_BASE ?? '').trim();
|
||||
if (configured) {
|
||||
return `${configured.replace(/\/$/, '')}/plaza/p/${encodeURIComponent(postId)}`;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
return `http://127.0.0.1:3001/plaza/p/${encodeURIComponent(postId)}`;
|
||||
}
|
||||
return `${window.location.origin}/plaza/p/${encodeURIComponent(postId)}`;
|
||||
return resolvePlazaPostPath(resolvedPlazaBase(), postId);
|
||||
}
|
||||
|
||||
export function resolvePlazaHomeUrl(): string {
|
||||
const configured = String(import.meta.env.VITE_PLAZA_BASE ?? '').trim();
|
||||
if (configured) {
|
||||
return `${configured.replace(/\/$/, '')}/plaza`;
|
||||
}
|
||||
if (import.meta.env.DEV) {
|
||||
return 'http://127.0.0.1:3001/plaza';
|
||||
}
|
||||
return `${window.location.origin}/plaza`;
|
||||
return resolvePlazaHomePath(resolvedPlazaBase());
|
||||
}
|
||||
|
||||
export function resolveMindSpaceHomeUrl(): string {
|
||||
|
||||
Reference in New Issue
Block a user