feat(page-data): complete Phase 4-5, ops UI, and publish integration
Add visitor roles, row-level scope, owner ops APIs, MySQL policy index, Turnstile captcha, browser client SDK, publish-panel dataset binding, acceptance tests, and usage documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
import { useCallback, useEffect, useState, type ReactNode } from 'react';
|
||||
import {
|
||||
buildPageDataExportUrl,
|
||||
closePageDataDataset,
|
||||
getPageDataOpsOverview,
|
||||
resetPageDataPassword,
|
||||
restorePageDataRow,
|
||||
revokePageDataTokens,
|
||||
} from '../api/client';
|
||||
import type { MindSpacePublishCheck, PageDataOpsOverview } from '../types';
|
||||
|
||||
type Props = {
|
||||
pageId: string;
|
||||
pageTitle: string;
|
||||
accessMode: MindSpacePublishCheck['accessMode'];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function formatLogTime(value: string) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
export function MindSpacePageDataOpsPanel({ pageId, pageTitle, accessMode, onClose }: Props) {
|
||||
const [overview, setOverview] = useState<PageDataOpsOverview | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busyAction, setBusyAction] = useState<string | null>(null);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const loadOverview = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const next = await getPageDataOpsOverview(pageId);
|
||||
setOverview(next);
|
||||
} catch (err) {
|
||||
const text = err instanceof Error ? err.message : '加载页面数据运维信息失败';
|
||||
if (text.includes('policy_not_found') || text.includes('策略未配置')) {
|
||||
setOverview(null);
|
||||
setError('当前页面尚未配置 Page Data 策略。请让 Agent 注册 dataset 并调用 private_data_set_page_policy。');
|
||||
} else {
|
||||
setError(text);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pageId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadOverview();
|
||||
}, [loadOverview]);
|
||||
|
||||
const runAction = async (key: string, fn: () => Promise<void>) => {
|
||||
setBusyAction(key);
|
||||
setMessage(null);
|
||||
try {
|
||||
await fn();
|
||||
await loadOverview();
|
||||
} catch (err) {
|
||||
setMessage(err instanceof Error ? err.message : '操作失败');
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const recentDeletions =
|
||||
overview?.recentLogs.filter(
|
||||
(entry) => entry.action === 'soft_delete' && entry.dataset && entry.rowId != null,
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
<section className="mindspace-page-data-ops-panel">
|
||||
<div className="mindspace-page-data-ops-header">
|
||||
<div>
|
||||
<p className="mindspace-eyebrow">PAGE DATA</p>
|
||||
<h3>页面数据运维</h3>
|
||||
<small>
|
||||
{pageTitle} · 页面 ID {pageId}
|
||||
</small>
|
||||
</div>
|
||||
<button type="button" onClick={onClose}>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? <p className="mindspace-page-data-ops-muted">加载中…</p> : null}
|
||||
{error ? <p className="mindspace-page-data-ops-error">{error}</p> : null}
|
||||
{message ? <p className="mindspace-page-data-ops-message">{message}</p> : null}
|
||||
|
||||
{overview ? (
|
||||
<>
|
||||
<div className="mindspace-page-data-ops-summary">
|
||||
<div>
|
||||
<span>访问模式</span>
|
||||
<strong>{overview.publication.accessMode}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>策略索引</span>
|
||||
<strong>{overview.index?.scopeHash ?? '—'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>活跃会话</span>
|
||||
<strong>{overview.activeSessionCount}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Dataset 数</span>
|
||||
<strong>{overview.datasets.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>最近提交</span>
|
||||
<strong>{overview.recentSubmissions.length}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{overview.unconfiguredDatasets && overview.unconfiguredDatasets.length > 0 ? (
|
||||
<div className="mindspace-page-data-ops-section">
|
||||
<h4>已注册但未纳入策略的 Dataset</h4>
|
||||
<p className="mindspace-page-data-ops-muted">
|
||||
{overview.unconfiguredDatasets.map((name) => (
|
||||
<code key={name}>{name}</code>
|
||||
)).reduce<ReactNode[]>((acc, node, index) => (
|
||||
index === 0 ? [node] : [...acc, '、', node]
|
||||
), [])}
|
||||
{' '}— 需通过 Agent 更新 Page Data 策略后才能公开访问。
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mindspace-page-data-ops-section">
|
||||
<h4>Dataset 状态</h4>
|
||||
{overview.datasets.length === 0 ? (
|
||||
<p className="mindspace-page-data-ops-muted">策略中尚未声明 dataset。</p>
|
||||
) : (
|
||||
<div className="mindspace-page-data-ops-table-wrap">
|
||||
<table className="mindspace-page-data-ops-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>读</th>
|
||||
<th>写</th>
|
||||
<th>更新</th>
|
||||
<th>删除</th>
|
||||
<th>行数</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{overview.datasets.map((dataset) => (
|
||||
<tr key={dataset.name}>
|
||||
<td>
|
||||
<code>{dataset.name}</code>
|
||||
{dataset.closed ? <span className="mindspace-page-data-ops-badge">已关闭</span> : null}
|
||||
</td>
|
||||
<td>{dataset.read ? '是' : '否'}</td>
|
||||
<td>{dataset.insert ? '是' : '否'}</td>
|
||||
<td>{dataset.update ? '是' : '否'}</td>
|
||||
<td>{dataset.softDelete ? '是' : '否'}</td>
|
||||
<td>{dataset.stats?.total ?? '—'}</td>
|
||||
<td>
|
||||
<div className="mindspace-page-data-ops-row-actions">
|
||||
{!dataset.closed ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyAction != null}
|
||||
onClick={() =>
|
||||
void runAction(`close-${dataset.name}`, async () => {
|
||||
await closePageDataDataset(pageId, dataset.name);
|
||||
setMessage(`已关闭 dataset:${dataset.name}`);
|
||||
})
|
||||
}
|
||||
>
|
||||
{busyAction === `close-${dataset.name}` ? '处理中…' : '关闭'}
|
||||
</button>
|
||||
) : null}
|
||||
<a
|
||||
href={buildPageDataExportUrl(dataset.name, 'json')}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
导出 JSON
|
||||
</a>
|
||||
<a
|
||||
href={buildPageDataExportUrl(dataset.name, 'csv')}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
导出 CSV
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mindspace-page-data-ops-section">
|
||||
<h4>最近提交</h4>
|
||||
{overview.recentSubmissions.length === 0 ? (
|
||||
<p className="mindspace-page-data-ops-muted">暂无公开写入记录。</p>
|
||||
) : (
|
||||
<ul className="mindspace-page-data-ops-log-list">
|
||||
{overview.recentSubmissions.map((entry, index) => (
|
||||
<li key={`${entry.at}-${entry.rowId ?? index}`}>
|
||||
<span>{formatLogTime(entry.at)}</span>
|
||||
<span>{entry.dataset}</span>
|
||||
<span>#{entry.rowId ?? '—'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mindspace-page-data-ops-section">
|
||||
<h4>最近操作日志</h4>
|
||||
{overview.recentLogs.length === 0 ? (
|
||||
<p className="mindspace-page-data-ops-muted">暂无操作日志。</p>
|
||||
) : (
|
||||
<ul className="mindspace-page-data-ops-log-list">
|
||||
{overview.recentLogs.map((entry, index) => (
|
||||
<li key={`${entry.at}-${entry.action}-${index}`}>
|
||||
<span>{formatLogTime(entry.at)}</span>
|
||||
<span>{entry.action}</span>
|
||||
<span>{entry.dataset ?? '—'}</span>
|
||||
<span>{entry.actor?.role ?? entry.actor?.type ?? '—'}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mindspace-page-data-ops-actions">
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyAction != null || overview.activeSessionCount === 0}
|
||||
onClick={() =>
|
||||
void runAction('revoke-all', async () => {
|
||||
const result = await revokePageDataTokens(pageId, { revokeAll: true });
|
||||
setMessage(`已撤销 ${result.revokedCount} 个活跃会话令牌。`);
|
||||
})
|
||||
}
|
||||
>
|
||||
{busyAction === 'revoke-all' ? '撤销中…' : '撤销全部数据令牌'}
|
||||
</button>
|
||||
|
||||
{accessMode === 'password' ? (
|
||||
<div className="mindspace-page-data-ops-password">
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(event) => setNewPassword(event.target.value)}
|
||||
placeholder="新页面口令(至少 8 位)"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyAction != null || newPassword.trim().length < 8}
|
||||
onClick={() =>
|
||||
void runAction('reset-password', async () => {
|
||||
const result = await resetPageDataPassword(pageId, newPassword.trim());
|
||||
setNewPassword('');
|
||||
setMessage(
|
||||
`口令已重置,并撤销 ${result.revokedSessions} 个旧会话。`,
|
||||
);
|
||||
})
|
||||
}
|
||||
>
|
||||
{busyAction === 'reset-password' ? '重置中…' : '重置页面口令'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mindspace-page-data-ops-section">
|
||||
<h4>可恢复的软删除记录</h4>
|
||||
{recentDeletions.length === 0 ? (
|
||||
<p className="mindspace-page-data-ops-muted">最近日志中暂无可恢复记录。</p>
|
||||
) : (
|
||||
<ul className="mindspace-page-data-ops-log-list">
|
||||
{recentDeletions.map((entry, index) => (
|
||||
<li key={`${entry.at}-restore-${index}`}>
|
||||
<span>{formatLogTime(entry.at)}</span>
|
||||
<span>{entry.dataset}</span>
|
||||
<span>#{entry.rowId}</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busyAction != null}
|
||||
onClick={() =>
|
||||
void runAction(`restore-${entry.dataset}-${entry.rowId}`, async () => {
|
||||
if (!entry.dataset || entry.rowId == null) return;
|
||||
await restorePageDataRow(entry.dataset, entry.rowId);
|
||||
setMessage(`已恢复 ${entry.dataset} #${entry.rowId}`);
|
||||
})
|
||||
}
|
||||
>
|
||||
{busyAction === `restore-${entry.dataset}-${entry.rowId}` ? '恢复中…' : '恢复'}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mindspace-page-data-ops-hint">
|
||||
HTML 页面可引入 <code>/assets/page-data-client.js</code>,通过{' '}
|
||||
<code>MindSpacePageData.createClient({'{'} pageId {'}'})</code> 调用公开数据 API。
|
||||
公开表单提交可配合 Cloudflare Turnstile,调用{' '}
|
||||
<code>insertRow(dataset, row, {'{'} turnstileToken {'}'})</code> 传入验证码。
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { usePageDraftHistory, type PageDraftSnapshot } from '../hooks/usePageDraftHistory';
|
||||
import {
|
||||
applyPageDataPublishPolicy,
|
||||
checkMindSpacePagePublication,
|
||||
deleteMindSpacePage,
|
||||
fixMindSpacePagePublication,
|
||||
getMindSpacePageDeletePreview,
|
||||
getMindSpacePage,
|
||||
getMindSpacePublicationStats,
|
||||
getPageDataPolicy,
|
||||
listPageDataDatasets,
|
||||
listPlazaCategories,
|
||||
offlineMindSpacePublication,
|
||||
publishMindSpacePage,
|
||||
@@ -23,6 +26,7 @@ import type {
|
||||
MindSpacePublishCheck,
|
||||
MindSpaceQuota,
|
||||
MindSpaceRedactionChange,
|
||||
PageDataDatasetSummary,
|
||||
PlazaCategory,
|
||||
PlazaPostBrief,
|
||||
} from '../types';
|
||||
@@ -35,6 +39,7 @@ import { MindSpacePagePreviewPanel } from './MindSpacePagePreviewPanel';
|
||||
import { MindSpaceModal } from './MindSpaceModal';
|
||||
import { MindSpaceDeletePlazaOption } from './MindSpaceDeletePlazaOption';
|
||||
import { MindSpacePublishSuccess } from './MindSpacePublishSuccess';
|
||||
import { MindSpacePageDataOpsPanel } from './MindSpacePageDataOpsPanel';
|
||||
import { prepareHtmlPageBrandMarkers } from '../../mindspace-page-tag.mjs';
|
||||
|
||||
type AccessMode = MindSpacePublishCheck['accessMode'];
|
||||
@@ -67,6 +72,21 @@ const ACCESS_MODE_LABELS: Record<AccessMode, string> = {
|
||||
owner_only: '仅自己可见',
|
||||
};
|
||||
|
||||
const PAGE_DATA_PUBLISH_MODES = new Set<AccessMode>(['public', 'password', 'login_required']);
|
||||
|
||||
function defaultPageDataCapabilities(mode: AccessMode) {
|
||||
if (mode === 'public') {
|
||||
return { read: false, insert: true, update: false, softDelete: false };
|
||||
}
|
||||
if (mode === 'password') {
|
||||
return { read: true, insert: true, update: false, softDelete: false };
|
||||
}
|
||||
if (mode === 'login_required') {
|
||||
return { read: true, insert: true, update: true, softDelete: false };
|
||||
}
|
||||
return { read: false, insert: false, update: false, softDelete: false };
|
||||
}
|
||||
|
||||
function slugFromTitle(title: string, pageId?: string) {
|
||||
const ascii = title
|
||||
.normalize('NFKC')
|
||||
@@ -274,6 +294,16 @@ export function MindSpacePageDetail({
|
||||
const [isExportingImage, setIsExportingImage] = useState(false);
|
||||
const [exportFeedback, setExportFeedback] = useState<string | null>(null);
|
||||
const [plazaOpen, setPlazaOpen] = useState(false);
|
||||
const [pageDataOpsOpen, setPageDataOpsOpen] = useState(false);
|
||||
const [pageDataEnabled, setPageDataEnabled] = useState(false);
|
||||
const [pageDataDataset, setPageDataDataset] = useState('');
|
||||
const [pageDataRead, setPageDataRead] = useState(false);
|
||||
const [pageDataInsert, setPageDataInsert] = useState(true);
|
||||
const [pageDataUpdate, setPageDataUpdate] = useState(false);
|
||||
const [pageDataSoftDelete, setPageDataSoftDelete] = useState(false);
|
||||
const [pageDataDatasets, setPageDataDatasets] = useState<PageDataDatasetSummary[]>([]);
|
||||
const [pageDataDatasetsLoading, setPageDataDatasetsLoading] = useState(false);
|
||||
const [pageDataDatasetsError, setPageDataDatasetsError] = useState<string | null>(null);
|
||||
const [plazaCategories, setPlazaCategories] = useState<PlazaCategory[]>([]);
|
||||
const [plazaCategoryId, setPlazaCategoryId] = useState('');
|
||||
const [plazaTags, setPlazaTags] = useState('');
|
||||
@@ -292,6 +322,67 @@ export function MindSpacePageDetail({
|
||||
const [confirmPublicationStatusOpen, setConfirmPublicationStatusOpen] = useState(false);
|
||||
const [statusConfirming, setStatusConfirming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publishOpen) return;
|
||||
let cancelled = false;
|
||||
setPageDataDatasetsLoading(true);
|
||||
setPageDataDatasetsError(null);
|
||||
void listPageDataDatasets()
|
||||
.then((datasets) => {
|
||||
if (cancelled) return;
|
||||
setPageDataDatasets(datasets);
|
||||
setPageDataDataset((current) => current || datasets[0]?.name || '');
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
setPageDataDatasetsError(err instanceof Error ? err.message : '加载 dataset 失败');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setPageDataDatasetsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [publishOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publishOpen || !page?.id) return;
|
||||
let cancelled = false;
|
||||
void getPageDataPolicy(page.id)
|
||||
.then((policy) => {
|
||||
if (cancelled || !policy) return;
|
||||
const datasetNames = Object.keys(policy.datasets ?? {});
|
||||
if (datasetNames.length === 0) return;
|
||||
const primaryDataset = datasetNames[0];
|
||||
const datasetPolicy = policy.datasets[primaryDataset];
|
||||
if (!datasetPolicy) return;
|
||||
setPageDataEnabled(true);
|
||||
setPageDataDataset(primaryDataset);
|
||||
setPageDataRead(Boolean(datasetPolicy.read));
|
||||
setPageDataInsert(Boolean(datasetPolicy.insert));
|
||||
setPageDataUpdate(Boolean(datasetPolicy.update));
|
||||
setPageDataSoftDelete(Boolean(datasetPolicy.softDelete));
|
||||
})
|
||||
.catch(() => {
|
||||
// 无策略时保持默认关闭
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [publishOpen, page?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!PAGE_DATA_PUBLISH_MODES.has(accessMode)) {
|
||||
setPageDataEnabled(false);
|
||||
return;
|
||||
}
|
||||
const defaults = defaultPageDataCapabilities(accessMode);
|
||||
setPageDataRead(defaults.read);
|
||||
setPageDataInsert(defaults.insert);
|
||||
setPageDataUpdate(defaults.update);
|
||||
setPageDataSoftDelete(defaults.softDelete);
|
||||
}, [accessMode]);
|
||||
|
||||
const applyPageRecord = useCallback(
|
||||
(next: MindSpacePage, options?: { recordHistory?: boolean }) => {
|
||||
setPage(next);
|
||||
@@ -427,6 +518,20 @@ export function MindSpacePageDetail({
|
||||
password: accessPassword || undefined,
|
||||
expiresAt: expiresAt ? new Date(expiresAt).getTime() : null,
|
||||
});
|
||||
if (pageDataEnabled && PAGE_DATA_PUBLISH_MODES.has(accessMode)) {
|
||||
if (!pageDataDataset) {
|
||||
throw new Error('已启用页面数据,请先选择 dataset');
|
||||
}
|
||||
await applyPageDataPublishPolicy(page.id, {
|
||||
datasetName: pageDataDataset,
|
||||
capabilities: {
|
||||
read: pageDataRead,
|
||||
insert: pageDataInsert,
|
||||
update: pageDataUpdate,
|
||||
softDelete: pageDataSoftDelete,
|
||||
},
|
||||
});
|
||||
}
|
||||
let nextPlazaPost: PlazaPostBrief | null = plazaPost;
|
||||
if (pushToPlaza) {
|
||||
nextPlazaPost = await publishToPlazaWithDefaults(publication.id);
|
||||
@@ -604,6 +709,11 @@ export function MindSpacePageDetail({
|
||||
const publishBlockedBySecurity = Boolean(
|
||||
publishCheck && publishCheck.findings.some((finding) => finding.blocking),
|
||||
);
|
||||
const publishPageDataBlocked = Boolean(
|
||||
pageDataEnabled &&
|
||||
PAGE_DATA_PUBLISH_MODES.has(accessMode) &&
|
||||
(pageDataDatasetsLoading || !pageDataDataset),
|
||||
);
|
||||
const publishWarningFindings =
|
||||
publishCheck?.findings.filter((finding) => !finding.blocking) ?? [];
|
||||
|
||||
@@ -1006,6 +1116,16 @@ export function MindSpacePageDetail({
|
||||
发布到广场
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPageDataOpsOpen((open) => !open);
|
||||
setPlazaOpen(false);
|
||||
}}
|
||||
disabled={publishing}
|
||||
>
|
||||
页面数据
|
||||
</button>
|
||||
<button type="button" onClick={() => void offline()} disabled={publishing}>
|
||||
立即下线
|
||||
</button>
|
||||
@@ -1032,6 +1152,15 @@ export function MindSpacePageDetail({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{pageDataOpsOpen && page.publication ? (
|
||||
<MindSpacePageDataOpsPanel
|
||||
pageId={page.id}
|
||||
pageTitle={page.title}
|
||||
accessMode={page.publication.accessMode}
|
||||
onClose={() => setPageDataOpsOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{plazaOpen ? (
|
||||
<section className="mindspace-publish-panel">
|
||||
<div>
|
||||
@@ -1130,6 +1259,103 @@ export function MindSpacePageDetail({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{PAGE_DATA_PUBLISH_MODES.has(accessMode) ? (
|
||||
<div className="mindspace-page-data-publish-panel">
|
||||
<label className="mindspace-page-data-publish-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pageDataEnabled}
|
||||
disabled={publishing}
|
||||
onChange={(event) => setPageDataEnabled(event.target.checked)}
|
||||
/>
|
||||
启用页面数据(Page Data API)
|
||||
</label>
|
||||
{pageDataEnabled ? (
|
||||
<>
|
||||
<label>
|
||||
绑定 dataset
|
||||
<select
|
||||
value={pageDataDataset}
|
||||
disabled={publishing || pageDataDatasetsLoading}
|
||||
onChange={(event) => setPageDataDataset(event.target.value)}
|
||||
>
|
||||
{pageDataDatasets.length === 0 ? (
|
||||
<option value="">暂无可用 dataset</option>
|
||||
) : (
|
||||
pageDataDatasets.map((dataset) => (
|
||||
<option value={dataset.name} key={dataset.name}>
|
||||
{dataset.name}
|
||||
{dataset.description ? ` · ${dataset.description}` : ''}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
{pageDataDatasetsLoading ? (
|
||||
<p className="mindspace-page-data-publish-hint">正在加载 dataset…</p>
|
||||
) : null}
|
||||
{pageDataDatasetsError ? (
|
||||
<p className="mindspace-page-data-ops-error">{pageDataDatasetsError}</p>
|
||||
) : null}
|
||||
{!pageDataDatasetsLoading && pageDataDatasets.length === 0 ? (
|
||||
<p className="mindspace-page-data-publish-hint">
|
||||
请先在 Agent 中注册 dataset,再发布并绑定页面数据。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="mindspace-page-data-publish-capabilities">
|
||||
<span>公开能力</span>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pageDataRead}
|
||||
disabled={publishing}
|
||||
onChange={(event) => setPageDataRead(event.target.checked)}
|
||||
/>
|
||||
读取
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pageDataInsert}
|
||||
disabled={publishing}
|
||||
onChange={(event) => setPageDataInsert(event.target.checked)}
|
||||
/>
|
||||
提交
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pageDataUpdate}
|
||||
disabled={publishing}
|
||||
onChange={(event) => setPageDataUpdate(event.target.checked)}
|
||||
/>
|
||||
更新
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={pageDataSoftDelete}
|
||||
disabled={publishing}
|
||||
onChange={(event) => setPageDataSoftDelete(event.target.checked)}
|
||||
/>
|
||||
软删除
|
||||
</label>
|
||||
</div>
|
||||
<p className="mindspace-page-data-publish-hint">
|
||||
发布成功后会自动写入 Page Data 策略;后续可在「页面数据」面板运维、导出与撤销访问。
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="mindspace-page-data-publish-hint">
|
||||
若页面需要读写后台数据,可勾选启用;Agent 需先注册 dataset。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mindspace-page-data-publish-hint">
|
||||
当前访问方式不支持在发布时绑定页面数据;若页面已发布,可在「页面数据」面板单独配置。
|
||||
</p>
|
||||
)}
|
||||
{accessMode === 'password' && (
|
||||
<label>
|
||||
访问密码
|
||||
@@ -1276,7 +1502,7 @@ export function MindSpacePageDetail({
|
||||
type="button"
|
||||
className="mindspace-primary"
|
||||
onClick={() => void publish()}
|
||||
disabled={publishing || !publishCheck?.allowed}
|
||||
disabled={publishing || !publishCheck?.allowed || publishPageDataBlocked}
|
||||
>
|
||||
{publishing ? (pushToPlaza ? '发布并推送…' : '发布中…') : '确认发布'}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user