refactor: Business logic and dependencies updates
- 核心服务代码更新 (db, server, auth, proxy) - Agent 相关模块更新 (mindspace, experience) - 前端组件和 hooks 更新 - 数据库 schema 更新 - 依赖版本更新
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { checkAuth, logout, setUnauthorizedHandler } from './api/client';
|
||||
import { clearAllStoredSessionIds } from './utils/sessionStorage';
|
||||
import { loadBlockedWords } from './utils/wordFilter';
|
||||
import { AuthView } from './components/AuthView';
|
||||
import { ChatView } from './components/ChatView';
|
||||
@@ -51,6 +52,7 @@ function AuthenticatedApp({
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
clearAllStoredSessionIds();
|
||||
void logout().finally(() => {
|
||||
onLogout();
|
||||
navigate('/', { replace: true });
|
||||
@@ -191,6 +193,7 @@ export function App() {
|
||||
grantedSkills={grantedSkills}
|
||||
onUserUpdate={setUser}
|
||||
onLogout={() => {
|
||||
clearAllStoredSessionIds();
|
||||
setAuthed(false);
|
||||
setUser(null);
|
||||
}}
|
||||
|
||||
+30
-6
@@ -995,6 +995,26 @@ export async function publishMindSpacePage(
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function updatePublicationStatus(
|
||||
publicationId: string,
|
||||
input: {
|
||||
accessMode: MindSpacePublishCheck['accessMode'];
|
||||
expiresAt?: number | null;
|
||||
},
|
||||
): Promise<MindSpacePublication> {
|
||||
const result = await apiFetch<{ data: MindSpacePublication }>(
|
||||
`/mindspace/v1/publications/${encodeURIComponent(publicationId)}/update-status`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
access_mode: input.accessMode,
|
||||
expires_at: input.expiresAt,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function redactMindSpacePage(
|
||||
pageId: string,
|
||||
input: {
|
||||
@@ -1967,12 +1987,16 @@ export async function listSessions(): Promise<Session[]> {
|
||||
return result.sessions ?? [];
|
||||
}
|
||||
|
||||
function sessionPath(sessionId: string, suffix = '') {
|
||||
return `/sessions/${encodeURIComponent(sessionId)}${suffix}`;
|
||||
}
|
||||
|
||||
export async function getSession(sessionId: string): Promise<Session> {
|
||||
return apiFetch<Session>(`/sessions/${sessionId}`);
|
||||
return apiFetch<Session>(sessionPath(sessionId));
|
||||
}
|
||||
|
||||
export async function deleteChatSession(sessionId: string): Promise<void> {
|
||||
await apiFetch(`/sessions/${sessionId}`, { method: 'DELETE' });
|
||||
await apiFetch(sessionPath(sessionId), { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function loadSessionDetail(
|
||||
@@ -1986,7 +2010,7 @@ export async function loadSessionDetail(
|
||||
if (hints?.messageCount != null) params.set('hint_mc', String(hints.messageCount));
|
||||
if (hints?.updatedAt) params.set('hint_ua', hints.updatedAt);
|
||||
const qs = params.size ? `?${params.toString()}` : '';
|
||||
const detail = await apiFetch<Session>(`/sessions/${sessionId}${qs}`);
|
||||
const detail = await apiFetch<Session>(`${sessionPath(sessionId)}${qs}`);
|
||||
const messages = normalizeConversationMessages(
|
||||
(detail.conversation ?? []).filter((m) => m.metadata?.userVisible),
|
||||
);
|
||||
@@ -1998,7 +2022,7 @@ export async function sendReply(
|
||||
requestId: string,
|
||||
userMessage: Message,
|
||||
): Promise<void> {
|
||||
await apiFetch(`/sessions/${sessionId}/reply`, {
|
||||
await apiFetch(`${sessionPath(sessionId)}/reply`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
@@ -2008,7 +2032,7 @@ export async function sendReply(
|
||||
}
|
||||
|
||||
export async function cancelRequest(sessionId: string, requestId: string): Promise<void> {
|
||||
await apiFetch(`/sessions/${sessionId}/cancel`, {
|
||||
await apiFetch(`${sessionPath(sessionId)}/cancel`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ request_id: requestId }),
|
||||
});
|
||||
@@ -2053,7 +2077,7 @@ export function subscribeSessionEvents(
|
||||
const headers: Record<string, string> = { Accept: 'text/event-stream' };
|
||||
if (lastEventId) headers['Last-Event-ID'] = lastEventId;
|
||||
|
||||
const res = await fetch(`${API}/sessions/${sessionId}/events`, {
|
||||
const res = await fetch(`${API}${sessionPath(sessionId)}/events`, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
publishPageToPlaza,
|
||||
redactMindSpacePage,
|
||||
updateMindSpacePage,
|
||||
updatePublicationStatus,
|
||||
} from '../api/client';
|
||||
import type {
|
||||
MindSpacePage,
|
||||
@@ -288,6 +289,8 @@ export function MindSpacePageDetail({
|
||||
pageTitle: string;
|
||||
pushedToPlaza: boolean;
|
||||
} | null>(null);
|
||||
const [confirmPublicationStatusOpen, setConfirmPublicationStatusOpen] = useState(false);
|
||||
const [statusConfirming, setStatusConfirming] = useState(false);
|
||||
|
||||
const applyPageRecord = useCallback(
|
||||
(next: MindSpacePage, options?: { recordHistory?: boolean }) => {
|
||||
@@ -334,6 +337,13 @@ export function MindSpacePageDetail({
|
||||
? await getMindSpacePublicationStats(next.publication.id).catch(() => null)
|
||||
: null,
|
||||
);
|
||||
if (
|
||||
next.publication &&
|
||||
next.publication.accessMode === 'public' &&
|
||||
next.publication.userConfirmedAt === null
|
||||
) {
|
||||
setConfirmPublicationStatusOpen(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '页面加载失败');
|
||||
} finally {
|
||||
@@ -434,6 +444,34 @@ export function MindSpacePageDetail({
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPublicationStatus = async (nextAccessMode: AccessMode) => {
|
||||
if (!page || !page.publication) return;
|
||||
setStatusConfirming(true);
|
||||
try {
|
||||
const nextExpiresAt =
|
||||
nextAccessMode === 'public'
|
||||
? null
|
||||
: nextAccessMode === 'time_limited'
|
||||
? Date.now() + 30 * 60 * 1000
|
||||
: null;
|
||||
const updated = await updatePublicationStatus(page.publication.id, {
|
||||
accessMode: nextAccessMode,
|
||||
expiresAt: nextExpiresAt,
|
||||
});
|
||||
setPage({ ...page, publication: updated });
|
||||
setAccessMode(updated.accessMode);
|
||||
setExpiresAt(
|
||||
updated.expiresAt ? localDateTimeValue(updated.expiresAt) : '',
|
||||
);
|
||||
setConfirmPublicationStatusOpen(false);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '更新发布状态失败');
|
||||
} finally {
|
||||
setStatusConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const offlineFromSuccess = async () => {
|
||||
if (!publishSuccess) return;
|
||||
setPublishing(true);
|
||||
@@ -1395,7 +1433,13 @@ export function MindSpacePageDetail({
|
||||
))}
|
||||
</section>
|
||||
</section>
|
||||
{sharePayload ? <ShareSheet payload={sharePayload} onClose={() => setSharePayload(null)} /> : null}
|
||||
{sharePayload ? (
|
||||
<ShareSheet
|
||||
payload={sharePayload}
|
||||
publication={page?.publication}
|
||||
onClose={() => setSharePayload(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{fullscreenPreviewOpen && page && page.contentFormat === 'html' ? (
|
||||
<MindSpacePageFullscreenPreview
|
||||
@@ -1414,6 +1458,38 @@ export function MindSpacePageDetail({
|
||||
onContentChange={handlePreviewContentChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{confirmPublicationStatusOpen && page?.publication ? (
|
||||
<MindSpaceModal className="mindspace-confirm-publication-status-modal">
|
||||
<div className="mindspace-modal-content">
|
||||
<h3>页面预览期确认</h3>
|
||||
<p>你的页面当前处于 30 分钟预览期。请选择后续状态:</p>
|
||||
<div className="mindspace-modal-actions">
|
||||
<button
|
||||
className="mindspace-secondary"
|
||||
disabled={statusConfirming}
|
||||
onClick={() => confirmPublicationStatus('private')}
|
||||
>
|
||||
{statusConfirming ? '处理中...' : '改为私有'}
|
||||
</button>
|
||||
<button
|
||||
className="mindspace-secondary"
|
||||
disabled={statusConfirming}
|
||||
onClick={() => confirmPublicationStatus('time_limited')}
|
||||
>
|
||||
{statusConfirming ? '处理中...' : '再预览 30 分钟'}
|
||||
</button>
|
||||
<button
|
||||
className="mindspace-primary"
|
||||
disabled={statusConfirming}
|
||||
onClick={() => confirmPublicationStatus('public')}
|
||||
>
|
||||
{statusConfirming ? '处理中...' : '发布为永久公开'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</MindSpaceModal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { MindSpacePublication } from '../types';
|
||||
import {
|
||||
SHARE_CHANNELS,
|
||||
canUseNativeShare,
|
||||
@@ -10,13 +11,37 @@ import {
|
||||
|
||||
export function ShareSheet({
|
||||
payload,
|
||||
publication,
|
||||
onClose,
|
||||
}: {
|
||||
payload: SharePayload;
|
||||
publication?: MindSpacePublication | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [expiresInMinutes, setExpiresInMinutes] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!publication || publication.accessMode !== 'public' || publication.userConfirmedAt !== null) {
|
||||
setExpiresInMinutes(null);
|
||||
return;
|
||||
}
|
||||
if (!publication.expiresAt) {
|
||||
setExpiresInMinutes(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
const now = Date.now();
|
||||
const remaining = Math.ceil((publication.expiresAt! - now) / (1000 * 60));
|
||||
setExpiresInMinutes(Math.max(0, remaining));
|
||||
};
|
||||
update();
|
||||
|
||||
const interval = setInterval(update, 10000);
|
||||
return () => clearInterval(interval);
|
||||
}, [publication]);
|
||||
|
||||
const handleChannel = async (channel: ShareChannel) => {
|
||||
setBusy(true);
|
||||
@@ -60,6 +85,15 @@ export function ShareSheet({
|
||||
<p className="share-sheet-eyebrow">SHARE</p>
|
||||
<h2 id="share-sheet-title">分享到</h2>
|
||||
<p className="share-sheet-subtitle">{payload.title}</p>
|
||||
{expiresInMinutes !== null && (
|
||||
<p className="share-sheet-preview-notice">
|
||||
⏱️ 预览还剩{' '}
|
||||
<strong>
|
||||
{expiresInMinutes > 0 ? `${expiresInMinutes} 分钟` : '不到 1 分钟'}
|
||||
</strong>
|
||||
,之后仅你能看到。确认发布为永久公开?
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="share-sheet-close" onClick={onClose} aria-label="关闭">
|
||||
×
|
||||
|
||||
@@ -24,6 +24,11 @@ import {
|
||||
updateProvider,
|
||||
} from '../api/client';
|
||||
import { appConfig } from '../config';
|
||||
import {
|
||||
clearStoredSessionId,
|
||||
readStoredSessionId,
|
||||
writeStoredSessionId,
|
||||
} from '../utils/sessionStorage';
|
||||
import type {
|
||||
CapabilityMap,
|
||||
ChatState,
|
||||
@@ -57,36 +62,6 @@ import {
|
||||
} from '../utils/message';
|
||||
import { prependUnique, shouldShowNewChatTitle, sortAndTrim, touchSession } from '../utils/sessions';
|
||||
|
||||
const LEGACY_SESSION_KEY = 'tkmind-h5-session-id';
|
||||
|
||||
function resolveSessionStorageKey(userId?: string | null): string {
|
||||
return userId ? `${LEGACY_SESSION_KEY}:${userId}` : LEGACY_SESSION_KEY;
|
||||
}
|
||||
|
||||
function readStoredSessionId(userId?: string | null): string | null {
|
||||
const scopedKey = resolveSessionStorageKey(userId);
|
||||
const scopedValue = localStorage.getItem(scopedKey);
|
||||
if (scopedValue) return scopedValue;
|
||||
const legacyValue = localStorage.getItem(LEGACY_SESSION_KEY);
|
||||
if (legacyValue && userId) {
|
||||
localStorage.setItem(scopedKey, legacyValue);
|
||||
localStorage.removeItem(LEGACY_SESSION_KEY);
|
||||
return legacyValue;
|
||||
}
|
||||
return legacyValue;
|
||||
}
|
||||
|
||||
function writeStoredSessionId(userId: string | null | undefined, sessionId: string) {
|
||||
localStorage.setItem(resolveSessionStorageKey(userId), sessionId);
|
||||
}
|
||||
|
||||
function clearStoredSessionId(userId?: string | null) {
|
||||
localStorage.removeItem(resolveSessionStorageKey(userId));
|
||||
if (!userId) {
|
||||
localStorage.removeItem(LEGACY_SESSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
const INSUFFICIENT_BALANCE_NOTICE = '余额不足,请充值后继续使用';
|
||||
|
||||
export { INSUFFICIENT_BALANCE_NOTICE };
|
||||
|
||||
@@ -407,6 +407,7 @@ export type MindSpacePublication = {
|
||||
publicUrl: string;
|
||||
accessMode: MindSpacePublishCheck['accessMode'];
|
||||
expiresAt: number | null;
|
||||
userConfirmedAt: number | null;
|
||||
status: 'online' | 'offline' | 'expired' | 'blocked';
|
||||
viewCount: number;
|
||||
publishedAt: number;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
const LEGACY_SESSION_KEY = 'tkmind-h5-session-id';
|
||||
|
||||
export function resolveSessionStorageKey(userId?: string | null): string {
|
||||
return userId ? `${LEGACY_SESSION_KEY}:${userId}` : LEGACY_SESSION_KEY;
|
||||
}
|
||||
|
||||
export function readStoredSessionId(userId?: string | null): string | null {
|
||||
if (userId) {
|
||||
// Only use per-user scoped storage. Do not migrate the legacy global key,
|
||||
// which may belong to another account on the same browser profile.
|
||||
return localStorage.getItem(resolveSessionStorageKey(userId));
|
||||
}
|
||||
return localStorage.getItem(LEGACY_SESSION_KEY);
|
||||
}
|
||||
|
||||
export function writeStoredSessionId(userId: string | null | undefined, sessionId: string) {
|
||||
localStorage.setItem(resolveSessionStorageKey(userId), sessionId);
|
||||
}
|
||||
|
||||
export function clearStoredSessionId(userId?: string | null) {
|
||||
localStorage.removeItem(resolveSessionStorageKey(userId));
|
||||
if (!userId) {
|
||||
localStorage.removeItem(LEGACY_SESSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAllStoredSessionIds() {
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i += 1) {
|
||||
const key = localStorage.key(i);
|
||||
if (!key) continue;
|
||||
if (key === LEGACY_SESSION_KEY || key.startsWith(`${LEGACY_SESSION_KEY}:`)) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
for (const key of keysToRemove) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user