diff --git a/.env.example b/.env.example index 996df4a..18651f4 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,7 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:8081 DATABASE_URL=mysql://boot:password@localhost:3306/tkmind # 或分别设置 MYSQL_HOST / MYSQL_PORT / MYSQL_USER / MYSQL_PASSWORD / MYSQL_DATABASE H5_USERS_ROOT=/root/tkmind_go/users +# 新用户赠送余额(人民币分,500 = ¥5.00;勿与美元 cents 混淆) H5_SIGNUP_BALANCE_CENTS=500 H5_ADMIN_USERNAME=admin H5_ADMIN_PASSWORD=change-me-admin @@ -47,12 +48,13 @@ H5_ADMIN_PASSWORD=change-me-admin # H5_LOCAL_LLM_NAME=Local Ollama 7B H5_ACCESS_PASSWORD=change-me -# 计费(Phase 2) -# H5_USE_BACKEND_COST=1 # 优先用后端 accumulatedCost(USD)计费 -# H5_USD_CNY_RATE=7.2 -# H5_BILL_INPUT_CENTS_PER_1K=2 -# H5_BILL_OUTPUT_CENTS_PER_1K=6 +# 计费(Phase 2,金额单位均为人民币分) +# 默认按 Token 单价扣费(人民币结算);勿开启美元成本换算除非明确需要 +# H5_USE_BACKEND_COST=0 +# H5_BILL_INPUT_CENTS_PER_1K=2 # 输入 Token:2 分/1k(¥0.02/1k) +# H5_BILL_OUTPUT_CENTS_PER_1K=6 # 输出 Token:6 分/1k(¥0.06/1k) # H5_MIN_BILL_CENTS=1 +# 仅调试:H5_USE_BACKEND_COST=1 + H5_USD_CNY_RATE=7.2(上游 USD × 汇率 → 人民币分) # 用户自助充值(微信支付) # H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000 diff --git a/billing.mjs b/billing.mjs index a0fe0c7..1a0572f 100644 --- a/billing.mjs +++ b/billing.mjs @@ -1,5 +1,6 @@ export function loadBillingConfig() { - const useBackendCost = process.env.H5_USE_BACKEND_COST !== '0'; + // 默认按人民币分(CNY cents)计费;仅当 H5_USE_BACKEND_COST=1 时才用上游 USD 成本换算。 + const useBackendCost = process.env.H5_USE_BACKEND_COST === '1'; return { useBackendCost, usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2), @@ -53,6 +54,8 @@ export function computeDeltaCostCents(previous, current, config = loadBillingCon return Math.max(config.minBillCents, Math.ceil(currCost * config.usdCnyRate * 100)); } + // 默认路径:按 Token 增量 × 人民币单价(分/1k tokens)扣费。 + const prevIn = Number(previous?.lastInputTokens ?? 0); const prevOut = Number(previous?.lastOutputTokens ?? 0); const deltaIn = Math.max(0, current.accumulatedInputTokens - prevIn); diff --git a/billing.test.mjs b/billing.test.mjs index 8cd0da6..5279949 100644 --- a/billing.test.mjs +++ b/billing.test.mjs @@ -39,3 +39,16 @@ test('computeDeltaCostCents returns zero when no usage delta', () => { }); assert.equal(computeDeltaCostCents(previous, current, config), 0); }); + +test('computeDeltaCostCents ignores backend USD cost unless explicitly enabled', () => { + const previous = { lastInputTokens: 0, lastOutputTokens: 0, lastAccumulatedCost: 0.01 }; + const current = normalizeTokenState({ + accumulatedInputTokens: 1000, + accumulatedOutputTokens: 500, + accumulatedCost: 0.02, + }); + const rmbConfig = { ...config, useBackendCost: false }; + assert.equal(computeDeltaCostCents(previous, current, rmbConfig), 5); + const usdConfig = { ...config, useBackendCost: true }; + assert.equal(computeDeltaCostCents(previous, current, usdConfig), 8); +}); diff --git a/mindspace-visual-editor.test.mjs b/mindspace-visual-editor.test.mjs index 67b3877..5fb7ab2 100644 --- a/mindspace-visual-editor.test.mjs +++ b/mindspace-visual-editor.test.mjs @@ -26,3 +26,14 @@ test('buildEditablePreviewDocument provides blank fallback', () => { const result = buildEditablePreviewDocument(''); assert.match(result, /空白页面/); }); + +test('buildEditablePreviewDocument strips CSP meta so editor script can run', () => { + const html = ` + +

探索世界

`; + const result = buildEditablePreviewDocument(html); + + assert.doesNotMatch(result, /Content-Security-Policy/); + assert.match(result, /mindspace-visual-editor-script/); + assert.match(result, /

探索世界<\/h1>/); +}); diff --git a/src/components/MindSpacePageDetail.tsx b/src/components/MindSpacePageDetail.tsx index 64cd0a6..eae733e 100644 --- a/src/components/MindSpacePageDetail.tsx +++ b/src/components/MindSpacePageDetail.tsx @@ -27,7 +27,6 @@ import { resolvePlazaPostUrl, resolvePublicPageUrl } from '../utils/publicUrl'; import { MindSpacePageDraftPreviewFrame, type MindSpacePreviewChangeHighlight } from './MindSpacePageDraftPreviewFrame'; import { MindSpacePageFullscreenPreview, - type MindSpacePageFullscreenPreviewChat, } from './MindSpacePageFullscreenPreview'; import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel'; import { MindSpaceModal } from './MindSpaceModal'; @@ -96,7 +95,6 @@ export function MindSpacePageDetail({ onContextUpdate, autoOpenPlaza = false, refreshTrigger = 0, - overlayChat = null, onFullscreenPreviewChange, }: { pageId: string; @@ -110,7 +108,6 @@ export function MindSpacePageDetail({ }) => void; autoOpenPlaza?: boolean; refreshTrigger?: number; - overlayChat?: MindSpacePageFullscreenPreviewChat | null; onFullscreenPreviewChange?: (open: boolean) => void; }) { const [page, setPage] = useState(null); @@ -550,6 +547,9 @@ export function MindSpacePageDetail({ const openFullscreenPreview = () => { if (!page || page.contentFormat !== 'html' || !content.trim()) return; + setPreviewContent(content); + setPreviewRefreshPending(false); + setPreviewKey((value) => value + 1); setFullscreenPreviewOpen(true); onFullscreenPreviewChange?.(true); }; @@ -1071,7 +1071,7 @@ export function MindSpacePageDetail({ type="button" onClick={openFullscreenPreview} disabled={page.contentFormat !== 'html' || !content.trim()} - title={page.contentFormat === 'html' ? '全屏可视化编辑,可配合 Agent 改页' : '仅 HTML 页面支持全屏编辑'} + title={page.contentFormat === 'html' ? '进入沉浸式可视化编辑' : '仅 HTML 页面支持全屏编辑'} > 全屏预览编辑 @@ -1156,12 +1156,9 @@ export function MindSpacePageDetail({ {fullscreenPreviewOpen && page && page.contentFormat === 'html' ? ( handleManualPreviewRefresh()} refreshPending={previewRefreshPending} onContentChange={handlePreviewContentChange} - chat={overlayChat} /> ) : null} diff --git a/src/components/MindSpacePageEditablePreviewFrame.tsx b/src/components/MindSpacePageEditablePreviewFrame.tsx index 9051ee3..0e4e4f3 100644 --- a/src/components/MindSpacePageEditablePreviewFrame.tsx +++ b/src/components/MindSpacePageEditablePreviewFrame.tsx @@ -11,7 +11,9 @@ export function MindSpacePageEditablePreviewFrame({ reloadKey, minHeight = 680, maxHeight = 12000, - className = 'mindspace-page-preview-frame', + className, + compact = false, + fillViewport = false, showHint = true, onContentChange, }: { @@ -21,9 +23,14 @@ export function MindSpacePageEditablePreviewFrame({ minHeight?: number; maxHeight?: number; className?: string; + compact?: boolean; + fillViewport?: boolean; showHint?: boolean; onContentChange: (html: string) => void; }) { + const scrollInsideFrame = compact || fillViewport; + const frameClassName = + className ?? (compact ? 'page-save-mini-page-frame' : 'mindspace-page-preview-frame'); const [previewFailed, setPreviewFailed] = useState(false); const [srcdoc, setSrcdoc] = useState(() => buildEditablePreviewDocument(content)); const [height, setHeight] = useState(minHeight); @@ -62,6 +69,11 @@ export function MindSpacePageEditablePreviewFrame({ }, [onContentChange]); useEffect(() => { + if (scrollInsideFrame) { + syncHeightRef.current = null; + return; + } + const iframe = iframeRef.current; if (!iframe || !srcdoc) return; @@ -85,7 +97,7 @@ export function MindSpacePageEditablePreviewFrame({ syncHeightRef.current = null; unbind(); }; - }, [maxHeight, minHeight, srcdoc]); + }, [maxHeight, minHeight, scrollInsideFrame, srcdoc]); if (previewFailed) { return
页面预览加载失败
; @@ -95,7 +107,14 @@ export function MindSpacePageEditablePreviewFrame({ <> {showHint ? (

- 文字直接改 · 图片/音视频点击换 · 背景点空白区 · 链接双击改地址 + + 文字 + + 图片/媒体 + + 链接 + + 背景 — 预览内左上角有完整图例,虚线框即可直接修改

) : null}