diff --git a/mindspace-public-share-widget.mjs b/mindspace-public-share-widget.mjs index cb0cf9a..a6a059b 100644 --- a/mindspace-public-share-widget.mjs +++ b/mindspace-public-share-widget.mjs @@ -40,6 +40,13 @@ function withParam(url,key,value){var next=new URL(url);next.searchParams.set(ke function fallbackCopy(text){var ta=document.createElement('textarea');ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';ta.style.opacity='0';document.body.appendChild(ta);ta.focus();ta.select();var ok=document.execCommand('copy');ta.remove();if(!ok)throw new Error('复制失败');} async function copyText(text){if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(text);return;}catch(e){}}fallbackCopy(text);} function workspaceRelativePath(){var parts=location.pathname.split('/').filter(Boolean);if(parts[0]==='MindSpace'&&parts.length>=3)return parts.slice(2).join('/');return'';} +function redirectToMindSpaceLogin(){ +var returnTo=encodeURIComponent(cleanUrl()); +var inWechat=/MicroMessenger/i.test(navigator.userAgent||''); +if(inWechat){location.href='/auth/wechat/authorize?intent=login&return_to='+returnTo;return;} +location.href='/?return_to='+returnTo; +} +function handleUnauthorizedResponse(){redirectToMindSpaceLogin();return new Error('正在跳转登录…');} function showPlazaView(name){Object.keys(plazaViews).forEach(function(key){var view=plazaViews[key];if(view)view.hidden=key!==name;});} function openPlazaDialog(){if(!dialog)return;dialog.hidden=false;showPlazaView('loading');void refreshPlazaDialog();} function closePlazaDialog(){if(dialog)dialog.hidden=true;showPlazaView('confirm');} @@ -55,7 +62,7 @@ try{ var res=await fetch('/api/mindspace/v1/pages/quick-plaza-from-public-html/status?relative_path='+encodeURIComponent(relativePath),{credentials:'same-origin'}); var payload=await res.json().catch(function(){return{};}); if(!res.ok){ -if(res.status===401)throw new Error('请先登录 MindSpace'); +if(res.status===401)throw handleUnauthorizedResponse(); var errBody=apiErrorBody(payload); throw new Error(errBody.message||'无法检查发布状态'); } @@ -87,7 +94,7 @@ try{ var res=await fetch('/api/mindspace/v1/pages/quick-plaza-from-public-html',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({relative_path:relativePath})}); var payload=await res.json().catch(function(){return{};}); if(!res.ok){ -if(res.status===401)throw new Error('请先登录 MindSpace'); +if(res.status===401)throw handleUnauthorizedResponse(); var errBody=apiErrorBody(payload); var plazaUrl=resolvePlazaUrl(payload); if(errBody.code==='ALREADY_PUBLISHED'){showAlreadyPublished(plazaUrl);setStatus('该内容已在广场发布',false,true);return;} @@ -143,7 +150,7 @@ try{ var res=await fetch('/api/mindspace/v1/pages/quick-wechat-draft-from-public-html/status?relative_path='+encodeURIComponent(relativePath),{credentials:'same-origin'}); var payload=await res.json().catch(function(){return{};}); if(!res.ok){ -if(res.status===401)throw new Error('请先登录 MindSpace'); +if(res.status===401)throw handleUnauthorizedResponse(); var errBody=apiErrorBody(payload); throw new Error(errBody.message||'无法检查公众号草稿状态'); } @@ -177,7 +184,7 @@ try{ var res=await fetch('/api/mindspace/v1/pages/quick-wechat-draft-from-public-html',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({relative_path:relativePath})}); var payload=await res.json().catch(function(){return{};}); if(!res.ok){ -if(res.status===401)throw new Error('请先登录 MindSpace'); +if(res.status===401)throw handleUnauthorizedResponse(); var errBody=apiErrorBody(payload); throw new Error(errBody.message||'推送失败'); } diff --git a/mindspace-public-share-widget.test.mjs b/mindspace-public-share-widget.test.mjs index b580753..feb8a89 100644 --- a/mindspace-public-share-widget.test.mjs +++ b/mindspace-public-share-widget.test.mjs @@ -18,6 +18,8 @@ test('injectPublicFileShareButton adds plaza entry and confirm dialog', () => { assert.doesNotMatch(result.html, /data-mindspace-public-share-dialog-panel"/); assert.match(result.html, /color:#fff !important/); assert.match(result.html, /apiErrorBody/); + assert.match(result.html, /redirectToMindSpaceLogin/); + assert.match(result.html, /auth\/wechat\/authorize\?intent=login/); assert.match(result.html, /ALREADY_PUBLISHED/); assert.equal(result.scriptHashes.length, 1); }); diff --git a/scripts/repair-icyxu-wechat-pages-103.mjs b/scripts/repair-icyxu-wechat-pages-103.mjs new file mode 100644 index 0000000..4189035 --- /dev/null +++ b/scripts/repair-icyxu-wechat-pages-103.mjs @@ -0,0 +1,119 @@ +#!/usr/bin/env node +/** + * Repair icyxu (清风徐徐女士) WeChat pages missing MindSpace records/publications. + * + * Usage: + * node scripts/repair-icyxu-wechat-pages-103.mjs [--apply] + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createDbPool } from '../db.mjs'; +import { createAssetService } from '../mindspace-assets.mjs'; +import { createPageService } from '../mindspace-pages.mjs'; +import { createPageSyncService } from '../mindspace-page-sync-service.mjs'; +import { createPublicationService } from '../mindspace-publications.mjs'; +import { createWorkspacePageDeliverService } from '../mindspace-workspace-page-deliver.mjs'; +import { resolveMindSpaceStorageRoot } from '../mindspace-runtime-config.mjs'; + +const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) return; + for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq < 0) continue; + const key = trimmed.slice(0, eq).trim(); + const value = trimmed.slice(eq + 1).trim(); + if (!process.env[key]) process.env[key] = value; + } +} + +loadEnvFile(path.join(repoRoot, '.env')); +loadEnvFile(path.join(repoRoot, '.env.local')); +loadEnvFile(path.join(repoRoot, '../../.env.local')); + +const USER_ID = '7a650359-5f2e-4911-85ad-cbc74c0cdb22'; +const TARGET_PATHS = ['public/poem.html', 'public/mountain-trip.html']; + +const apply = process.argv.includes('--apply'); +const h5Root = process.env.H5_ROOT || repoRoot; +const storageRoot = resolveMindSpaceStorageRoot(h5Root, process.env); + +async function main() { + const pool = createDbPool(); + const pageService = createPageService(pool, { h5Root, storageRoot }); + const assetService = createAssetService(pool, { h5Root, storageRoot }); + const pageSyncService = createPageSyncService({ + pool, + pageService, + assetService, + h5Root, + syncWorkspaceAssets: assetService.syncWorkspaceAssets?.bind(assetService), + }); + const publicationService = createPublicationService(pool, { h5Root, storageRoot }); + const workspacePageDeliveryService = createWorkspacePageDeliverService({ + pool, + pageService, + publicationService, + pageSyncService, + h5Root, + storageRoot, + findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService), + }); + + const actions = []; + for (const relativePath of TARGET_PATHS) { + const absPath = path.join(h5Root, 'MindSpace', USER_ID, relativePath); + actions.push({ + relativePath, + exists: fs.existsSync(absPath), + size: fs.existsSync(absPath) ? fs.statSync(absPath).size : 0, + }); + } + + console.log(JSON.stringify({ userId: USER_ID, dryRun: !apply, targets: actions }, null, 2)); + + if (!apply) { + console.log('\nDry run only. Re-run with --apply to sync and publish.'); + await pool.end(); + return; + } + + const syncResult = await pageSyncService.syncUserGeneratedPages(USER_ID, { + onlyRelativePaths: TARGET_PATHS, + }); + console.log('sync:', JSON.stringify(syncResult)); + + const publishResult = await workspacePageDeliveryService.ensureWorkspaceHtmlPublications( + USER_ID, + { onlyRelativePaths: TARGET_PATHS }, + ); + console.log('publish:', JSON.stringify(publishResult)); + + for (const relativePath of TARGET_PATHS) { + const page = await pageService.findPageByRelativePath(USER_ID, relativePath); + const publication = page?.id + ? await publicationService.getCurrent(USER_ID, page.id) + : null; + console.log( + JSON.stringify({ + relativePath, + pageId: page?.id ?? null, + pageStatus: page?.status ?? null, + publicationId: publication?.id ?? null, + publicationStatus: publication?.status ?? null, + publicUrl: publication?.publicUrl ?? publication?.public_url ?? null, + }), + ); + } + + await pool.end(); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.stack ?? error.message : error); + process.exit(1); +}); diff --git a/server.mjs b/server.mjs index 620a2f6..2185cb2 100644 --- a/server.mjs +++ b/server.mjs @@ -637,6 +637,7 @@ async function bootstrapUserAuth() { apiSecret: API_SECRET, mindSpacePages, mindSpacePageLiveEdit, + syncUserGeneratedPages, healthChannelStore, healthObservationStore: healthDataRuntime.observationStore, healthObservationService: healthDataRuntime.observationService, @@ -1288,11 +1289,21 @@ async function listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs = .filter((relativePath) => relativePath?.startsWith('public/') && relativePath.toLowerCase().endsWith('.html')))]; } -async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null } = {}) { +async function syncUserGeneratedPages( + userId, + { sessionId = null, sinceMs = null, onlyRelativePaths = null } = {}, +) { if (!userId) return; // Agent-run/Finish delivery must stay scoped to the current conversation. // A stale Page Data page elsewhere in the user's workspace must not turn a // successfully completed current task into a failed run. + const explicitRelativePaths = Array.isArray(onlyRelativePaths) + ? [...new Set( + onlyRelativePaths + .map((relativePath) => normalizeWorkspaceRelativePath(relativePath)) + .filter(Boolean), + )] + : null; const discoveredRelativePaths = sessionId ? await listSessionPublicHtmlRelativePaths(userId, sessionId, { sinceMs }) : null; @@ -1302,6 +1313,7 @@ async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null // block an unrelated delivery. const recentWorkspaceRelativePaths = sessionId && + !explicitRelativePaths?.length && !discoveredRelativePaths?.length && mindSpaceWorkspacePublicationDelivery ? ( @@ -1312,9 +1324,11 @@ async function syncUserGeneratedPages(userId, { sessionId = null, sinceMs = null }) )?.relativePaths ?? [] : []; - const pageDataRelativePaths = sessionId - ? (discoveredRelativePaths?.length ? discoveredRelativePaths : recentWorkspaceRelativePaths) - : null; + const pageDataRelativePaths = explicitRelativePaths?.length + ? explicitRelativePaths + : sessionId + ? (discoveredRelativePaths?.length ? discoveredRelativePaths : recentWorkspaceRelativePaths) + : null; if (workspacePageDeliver?.syncAndDeliver) { return await workspacePageDeliver.syncAndDeliver(userId, { pageDataRelativePaths }); } diff --git a/server/portal-integration-services-bootstrap.mjs b/server/portal-integration-services-bootstrap.mjs index c388285..f22b6d6 100644 --- a/server/portal-integration-services-bootstrap.mjs +++ b/server/portal-integration-services-bootstrap.mjs @@ -51,6 +51,7 @@ export async function bootstrapPortalIntegrationServices({ apiSecret, mindSpacePages, mindSpacePageLiveEdit, + syncUserGeneratedPages = null, logger = console, healthChannelStore = null, healthObservationStore = null, @@ -212,6 +213,35 @@ export async function bootstrapPortalIntegrationServices({ sessionId, artifacts = [], }) => { + const relativePaths = [ + ...new Set( + artifacts + .map((artifact) => + String( + artifact?.relativePath ?? + artifact?.relative_path ?? + '', + ).trim(), + ) + .filter(Boolean), + ), + ]; + if ( + typeof syncUserGeneratedPages === 'function' && + userId + ) { + void syncUserGeneratedPages(userId, { + sessionId, + sinceMs: Date.now() - 10 * 60 * 1000, + onlyRelativePaths: + relativePaths.length > 0 ? relativePaths : null, + }).catch((error) => { + logger.warn?.( + '[WeChat MP] page sync after generation failed:', + error instanceof Error ? error.message : error, + ); + }); + } for (const artifact of artifacts) { const pageOwner = (await userAuth diff --git a/server/portal-integration-services-bootstrap.test.mjs b/server/portal-integration-services-bootstrap.test.mjs index a998ca2..a5df754 100644 --- a/server/portal-integration-services-bootstrap.test.mjs +++ b/server/portal-integration-services-bootstrap.test.mjs @@ -129,6 +129,10 @@ function createSetup(overrides = {}) { apiSecret: 'secret', mindSpacePages: { id: 'pages' }, mindSpacePageLiveEdit: { id: 'live-edit' }, + syncUserGeneratedPages(userId, options) { + calls.push(['sync-user-pages', userId, options]); + return Promise.resolve({ created: 1, updated: 0, skipped: 0 }); + }, logger: { log(...args) { calls.push(['log', ...args]); @@ -407,6 +411,13 @@ test('preserves generated-page analytics projection', async () => { .length, 1, ); + const syncCall = setup.calls.find( + ([name]) => name === 'sync-user-pages', + ); + assert.ok(syncCall); + assert.equal(syncCall[1], 'user-1'); + assert.equal(syncCall[2].sessionId, 'session-1'); + assert.deepEqual(syncCall[2].onlyRelativePaths, ['public/page.html']); const analyticsCall = setup.calls.find( ([name]) => name === 'analytics', );