合并master并保留双侧决策记录

合并 origin/master 的 UI 编辑器、GDD 审批与前端修复。

保留本分支 LLM Router、Direct 过程卡与私有路径相关实现和决策记录。

解决 decision-log 文档冲突,完整保留双方新增决策条目。
This commit is contained in:
2026-09-02 19:31:12 +08:00
39 changed files with 1824 additions and 97 deletions
@@ -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>
);