合并master并保留双侧决策记录
合并 origin/master 的 UI 编辑器、GDD 审批与前端修复。 保留本分支 LLM Router、Direct 过程卡与私有路径相关实现和决策记录。 解决 decision-log 文档冲突,完整保留双方新增决策条目。
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
type FormEvent,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@@ -49,6 +50,36 @@ type ClientRuntimeErrorBoundaryState = {
|
||||
errorMessage: string;
|
||||
};
|
||||
|
||||
type AuthCheckStage = 'token' | 'refresh' | 'me' | 'runner';
|
||||
|
||||
const AUTH_CHECK_STAGE_LABELS: Record<AuthCheckStage, string> = {
|
||||
token: '读取本地登录凭据',
|
||||
refresh: '刷新登录状态',
|
||||
me: '确认当前用户',
|
||||
runner: '连接本地运行时',
|
||||
};
|
||||
|
||||
const AUTH_CHECK_REQUEST_TIMEOUT_MS = 15_000;
|
||||
// Existing endpoint probes can consume the 10s IPC budget before Runner's
|
||||
// 30s startup deadline. Keep the UI fence slightly above that worst case.
|
||||
const AUTH_CHECK_RUNNER_TIMEOUT_MS = 45_000;
|
||||
|
||||
function withAuthCheckTimeout<T>(
|
||||
promise: Promise<T>,
|
||||
timeoutMs: number,
|
||||
message: string,
|
||||
) {
|
||||
let timeoutId: number | undefined;
|
||||
const timeout = new Promise<T>((_, reject) => {
|
||||
timeoutId = window.setTimeout(() => reject(new Error(message)), timeoutMs);
|
||||
});
|
||||
return Promise.race([promise, timeout]).finally(() => {
|
||||
if (timeoutId !== undefined) {
|
||||
window.clearTimeout(timeoutId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class ClientRuntimeErrorBoundary extends Component<
|
||||
ClientRuntimeErrorBoundaryProps,
|
||||
ClientRuntimeErrorBoundaryState
|
||||
@@ -101,6 +132,11 @@ export function AuthenticatedClient({
|
||||
'checking' | 'authenticated' | 'unauthenticated'
|
||||
>('checking');
|
||||
const [authUser, setAuthUser] = useState<AuthUser | null>(null);
|
||||
const [authCheckStage, setAuthCheckStage] = useState<AuthCheckStage>('token');
|
||||
const [authCheckElapsedSeconds, setAuthCheckElapsedSeconds] = useState(0);
|
||||
const [authCheckError, setAuthCheckError] = useState('');
|
||||
const [authCheckRetryKey, setAuthCheckRetryKey] = useState(0);
|
||||
const authCheckRunRef = useRef(0);
|
||||
const [loginMode, setLoginMode] = useState<'code' | 'password'>('code');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
@@ -147,100 +183,162 @@ export function AuthenticatedClient({
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
async function hydrateAuth() {
|
||||
const runId = ++authCheckRunRef.current;
|
||||
const isActiveRun = () => !disposed && authCheckRunRef.current === runId;
|
||||
const hydrationGeneration = currentPlatformSessionGeneration();
|
||||
const hydrationApiBaseUrl = getClientServerBaseUrl();
|
||||
setAuthCheckError('');
|
||||
setAuthCheckStage('token');
|
||||
try {
|
||||
if (!getStoredAuthAccessToken()) {
|
||||
const refreshed = await refreshPlatformSessionForGeneration(
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
setAuthCheckStage('refresh');
|
||||
const refreshed = await withAuthCheckTimeout(
|
||||
refreshPlatformSessionForGeneration(
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
),
|
||||
AUTH_CHECK_REQUEST_TIMEOUT_MS,
|
||||
'刷新登录状态超时,请检查服务器地址和网络后重试',
|
||||
);
|
||||
if (!refreshed) {
|
||||
if (isActiveRun()) {
|
||||
setAuthStatus('unauthenticated');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const user = await getCurrentClientAuthUser(hydrationApiBaseUrl);
|
||||
if (disposed) {
|
||||
if (!isActiveRun()) return;
|
||||
setAuthCheckStage('me');
|
||||
const user = await withAuthCheckTimeout(
|
||||
getCurrentClientAuthUser(hydrationApiBaseUrl),
|
||||
AUTH_CHECK_REQUEST_TIMEOUT_MS,
|
||||
'读取当前用户超时,请检查服务器地址和网络后重试',
|
||||
);
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
if (user) {
|
||||
const committedGeneration = await commitAuthenticatedPlatformSession(
|
||||
user,
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
setAuthCheckStage('runner');
|
||||
const committedGeneration = await withAuthCheckTimeout(
|
||||
commitAuthenticatedPlatformSession(
|
||||
user,
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
),
|
||||
AUTH_CHECK_RUNNER_TIMEOUT_MS,
|
||||
'连接本地运行时超时,请重试或重启客户端',
|
||||
);
|
||||
if (committedGeneration === null) {
|
||||
return;
|
||||
}
|
||||
if (disposed) {
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
setAuthUser(user);
|
||||
setAuthCheckError('');
|
||||
setAuthStatus('authenticated');
|
||||
return;
|
||||
}
|
||||
clearStoredAuthAccessToken();
|
||||
setAuthStatus('unauthenticated');
|
||||
} catch (error) {
|
||||
if (disposed) {
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getStoredAuthAccessToken() &&
|
||||
isClientAuthRecoverableCheckError(error)
|
||||
) {
|
||||
setLoginStatus(
|
||||
getClientAuthErrorMessage(error, '登录服务暂时不可用,请稍后重试'),
|
||||
const message = getClientAuthErrorMessage(
|
||||
error,
|
||||
'登录服务暂时不可用,请稍后重试',
|
||||
);
|
||||
setAuthCheckError(message);
|
||||
setLoginStatus(message);
|
||||
setAuthStatus('unauthenticated');
|
||||
return;
|
||||
}
|
||||
if (getStoredAuthAccessToken()) {
|
||||
try {
|
||||
const refreshed = await refreshPlatformSessionForGeneration(
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
if (!isActiveRun()) return;
|
||||
setAuthCheckStage('refresh');
|
||||
const refreshed = await withAuthCheckTimeout(
|
||||
refreshPlatformSessionForGeneration(
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
),
|
||||
AUTH_CHECK_REQUEST_TIMEOUT_MS,
|
||||
'刷新登录状态超时,请检查服务器地址和网络后重试',
|
||||
);
|
||||
if (!refreshed) {
|
||||
if (isActiveRun()) {
|
||||
setAuthStatus('unauthenticated');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const user = await getCurrentClientAuthUser(hydrationApiBaseUrl);
|
||||
if (disposed) {
|
||||
if (!isActiveRun()) return;
|
||||
setAuthCheckStage('me');
|
||||
const user = await withAuthCheckTimeout(
|
||||
getCurrentClientAuthUser(hydrationApiBaseUrl),
|
||||
AUTH_CHECK_REQUEST_TIMEOUT_MS,
|
||||
'读取当前用户超时,请检查服务器地址和网络后重试',
|
||||
);
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
if (user) {
|
||||
const committedGeneration =
|
||||
await commitAuthenticatedPlatformSession(
|
||||
if (!isActiveRun()) return;
|
||||
setAuthCheckStage('runner');
|
||||
const committedGeneration = await withAuthCheckTimeout(
|
||||
commitAuthenticatedPlatformSession(
|
||||
user,
|
||||
hydrationGeneration,
|
||||
hydrationApiBaseUrl,
|
||||
);
|
||||
),
|
||||
AUTH_CHECK_RUNNER_TIMEOUT_MS,
|
||||
'连接本地运行时超时,请重试或重启客户端',
|
||||
);
|
||||
if (committedGeneration === null) {
|
||||
return;
|
||||
}
|
||||
if (disposed) {
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
setAuthCheckError('');
|
||||
setAuthUser(user);
|
||||
setAuthStatus('authenticated');
|
||||
return;
|
||||
}
|
||||
} catch (retryError) {
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getStoredAuthAccessToken() &&
|
||||
isClientAuthRecoverableCheckError(retryError)
|
||||
) {
|
||||
setLoginStatus(
|
||||
getClientAuthErrorMessage(
|
||||
retryError,
|
||||
'登录服务暂时不可用,请稍后重试',
|
||||
),
|
||||
const message = getClientAuthErrorMessage(
|
||||
retryError,
|
||||
'登录服务暂时不可用,请稍后重试',
|
||||
);
|
||||
setAuthCheckError(message);
|
||||
setLoginStatus(message);
|
||||
setAuthStatus('unauthenticated');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isActiveRun()) {
|
||||
return;
|
||||
}
|
||||
if (isClientAuthRecoverableCheckError(error)) {
|
||||
const message = getClientAuthErrorMessage(
|
||||
error,
|
||||
'登录服务暂时不可用,请稍后重试',
|
||||
);
|
||||
setAuthCheckError(message);
|
||||
setLoginStatus(message);
|
||||
}
|
||||
clearStoredAuthAccessToken();
|
||||
setAuthStatus('unauthenticated');
|
||||
}
|
||||
@@ -249,7 +347,18 @@ export function AuthenticatedClient({
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, []);
|
||||
}, [authCheckRetryKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authStatus !== 'checking') {
|
||||
return;
|
||||
}
|
||||
setAuthCheckElapsedSeconds(0);
|
||||
const timer = window.setInterval(() => {
|
||||
setAuthCheckElapsedSeconds((current) => current + 1);
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [authStatus, authCheckRetryKey]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -358,6 +467,7 @@ export function AuthenticatedClient({
|
||||
return;
|
||||
}
|
||||
setAuthUser(user);
|
||||
setAuthCheckError('');
|
||||
setAuthStatus('authenticated');
|
||||
setCode('');
|
||||
setPassword('');
|
||||
@@ -383,6 +493,7 @@ export function AuthenticatedClient({
|
||||
nativeClearError = error;
|
||||
}
|
||||
setAuthUser(null);
|
||||
setAuthCheckError('');
|
||||
setAuthStatus('unauthenticated');
|
||||
setLoginStatus(
|
||||
nativeClearError
|
||||
@@ -392,6 +503,7 @@ export function AuthenticatedClient({
|
||||
}
|
||||
|
||||
if (authStatus === 'checking') {
|
||||
const stageLabel = AUTH_CHECK_STAGE_LABELS[authCheckStage];
|
||||
return (
|
||||
<main
|
||||
className="client-auth-shell platform-theme platform-theme--light"
|
||||
@@ -400,6 +512,14 @@ export function AuthenticatedClient({
|
||||
<section className="client-auth-panel">
|
||||
<img className="client-auth-logo" src={brandIcon} alt="陶泥儿" />
|
||||
<h1>正在检查登录状态</h1>
|
||||
<p className="client-auth-status" aria-live="polite">
|
||||
{stageLabel} · 已等待 {authCheckElapsedSeconds} 秒
|
||||
</p>
|
||||
<p className="client-auth-status" aria-live="polite">
|
||||
{authCheckElapsedSeconds >= 8
|
||||
? '检查时间较长,请确认服务器可访问;若持续无响应可稍后重试'
|
||||
: '正在恢复会话,请稍候'}
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
@@ -417,6 +537,23 @@ export function AuthenticatedClient({
|
||||
<h1>登录陶泥儿 GameAgent</h1>
|
||||
<p>登录后进入首页和本地项目工作区</p>
|
||||
</div>
|
||||
{authCheckError ? (
|
||||
<div role="alert" className="client-auth-status">
|
||||
<p>{authCheckError}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
beginPlatformSessionTransition();
|
||||
setAuthCheckError('');
|
||||
setAuthStatus('checking');
|
||||
setAuthCheckRetryKey((current) => current + 1);
|
||||
}}
|
||||
disabled={loginBusy || codeBusy}
|
||||
>
|
||||
重试登录状态检查
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<label>
|
||||
服务器
|
||||
<select
|
||||
@@ -523,7 +660,9 @@ export function AuthenticatedClient({
|
||||
<button type="submit" disabled={loginBusy}>
|
||||
{loginBusy ? '登录中' : '登录'}
|
||||
</button>
|
||||
<p className="client-auth-status">{loginStatus}</p>
|
||||
{!authCheckError ? (
|
||||
<p className="client-auth-status">{loginStatus}</p>
|
||||
) : null}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user