修复策划项目重开运行态恢复
持久化策划项目 design 运行模式并在重开时恢复 design/game 工作台。 补充旧策划会话兼容判断、前端夹具和恢复回归测试。
This commit is contained in:
@@ -845,6 +845,26 @@ pub(crate) fn hydrate_design_agent_session(
|
||||
Ok(Some(design_view(&session, active.is_none())))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn get_design_agent_runtime_mode(
|
||||
project_path: String,
|
||||
) -> Result<Option<DesignRuntimeMode>, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
design_project_id(root)?;
|
||||
resolve_design_runtime_mode(root)
|
||||
}
|
||||
|
||||
fn resolve_design_runtime_mode(root: &Path) -> Result<Option<DesignRuntimeMode>, String> {
|
||||
if let Some(mode) = read_design_runtime_mode(root)? {
|
||||
return Ok(Some(mode));
|
||||
}
|
||||
// 已有策划会话可能早于运行模式文件;明确的 game 标记始终优先。
|
||||
Ok(read_design_session(root)?.map(|_| DesignRuntimeMode {
|
||||
active_runtime: "design".into(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn set_design_agent_runtime_mode(
|
||||
project_path: String,
|
||||
@@ -1054,6 +1074,37 @@ mod fake_provider {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_mode_restore_prefers_explicit_mode_over_existing_design_session() {
|
||||
let temporary = tempfile::tempdir().expect("tempdir");
|
||||
let root = temporary.path();
|
||||
assert_eq!(resolve_design_runtime_mode(root).unwrap(), None);
|
||||
write_design_session(root, &new_design_session("project", "")).unwrap();
|
||||
assert_eq!(
|
||||
resolve_design_runtime_mode(root)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.active_runtime,
|
||||
"design"
|
||||
);
|
||||
write_design_runtime_mode(root, "game").unwrap();
|
||||
assert_eq!(
|
||||
resolve_design_runtime_mode(root)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.active_runtime,
|
||||
"game"
|
||||
);
|
||||
write_design_runtime_mode(root, "design").unwrap();
|
||||
assert_eq!(
|
||||
resolve_design_runtime_mode(root)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.active_runtime,
|
||||
"design"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_runtime_rejects_design_execution_in_game_mode() {
|
||||
let temporary = tempfile::tempdir().expect("create runtime mode root");
|
||||
|
||||
@@ -2583,6 +2583,7 @@ fn main() {
|
||||
decide_planning_artifact_v2,
|
||||
hydrate_planning_session_v2,
|
||||
hydrate_design_agent_session,
|
||||
get_design_agent_runtime_mode,
|
||||
set_design_agent_runtime_mode,
|
||||
debug_fast_forward_design_session,
|
||||
continue_design_agent_session,
|
||||
|
||||
@@ -252,6 +252,12 @@ export function useHomeProjectCreation({
|
||||
attachments: HomeAttachmentDraft[],
|
||||
startMode: ProjectStartMode,
|
||||
) {
|
||||
if (startMode === 'planning') {
|
||||
await invoke('set_design_agent_runtime_mode', {
|
||||
projectPath: result.projectPath,
|
||||
activeRuntime: 'design',
|
||||
});
|
||||
}
|
||||
const importedAttachments = await importHomeAttachments(
|
||||
invoke,
|
||||
result.projectPath,
|
||||
@@ -477,6 +483,11 @@ export function useHomeProjectCreation({
|
||||
setStatus('这不是已初始化的 AI 游戏项目,请使用新建项目。');
|
||||
return;
|
||||
}
|
||||
const runtimeMode = await invoke<{
|
||||
activeRuntime: 'design' | 'game';
|
||||
} | null>('get_design_agent_runtime_mode', {
|
||||
projectPath: trimmedProjectPath,
|
||||
});
|
||||
setStatus('已打开项目');
|
||||
enterProjectDevelopment({
|
||||
projectPath: trimmedProjectPath,
|
||||
@@ -494,7 +505,7 @@ export function useHomeProjectCreation({
|
||||
trimmedProjectPath,
|
||||
),
|
||||
creationType: null,
|
||||
startMode: null,
|
||||
startMode: runtimeMode?.activeRuntime === 'design' ? 'planning' : null,
|
||||
initialPrompt: '',
|
||||
attachments: [],
|
||||
recentRunStatus: directoryStatus.recentRunStatus,
|
||||
|
||||
@@ -186,6 +186,7 @@ export function DesignWorkspacePanel({
|
||||
() => new Set(),
|
||||
);
|
||||
const refreshTimerRef = useRef<number | null>(null);
|
||||
const refreshHydrateSessionRef = useRef(false);
|
||||
|
||||
const loadWorkspace = useCallback(
|
||||
async ({
|
||||
@@ -266,20 +267,27 @@ export function DesignWorkspacePanel({
|
||||
}
|
||||
let disposed = false;
|
||||
let cleanup: (() => void) | null = null;
|
||||
const scheduleRefresh = () => {
|
||||
const scheduleRefresh = (hydrateSession: boolean) => {
|
||||
refreshHydrateSessionRef.current =
|
||||
refreshHydrateSessionRef.current || hydrateSession;
|
||||
if (refreshTimerRef.current !== null) {
|
||||
window.clearTimeout(refreshTimerRef.current);
|
||||
}
|
||||
refreshTimerRef.current = window.setTimeout(() => {
|
||||
refreshTimerRef.current = null;
|
||||
void loadWorkspace({ showLoading: false, hydrateSession: false });
|
||||
const shouldHydrateSession = refreshHydrateSessionRef.current;
|
||||
refreshHydrateSessionRef.current = false;
|
||||
void loadWorkspace({
|
||||
showLoading: false,
|
||||
hydrateSession: shouldHydrateSession,
|
||||
});
|
||||
}, 250);
|
||||
};
|
||||
void listen<{ projectPath: string; kind: string }>(
|
||||
'design-agent-update',
|
||||
(event) => {
|
||||
if (!disposed && event.payload.projectPath === projectPath) {
|
||||
scheduleRefresh();
|
||||
scheduleRefresh(event.payload.kind === 'state');
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -297,6 +305,7 @@ export function DesignWorkspacePanel({
|
||||
window.clearTimeout(refreshTimerRef.current);
|
||||
refreshTimerRef.current = null;
|
||||
}
|
||||
refreshHydrateSessionRef.current = false;
|
||||
cleanup?.();
|
||||
};
|
||||
}, [loadWorkspace, projectPath]);
|
||||
|
||||
@@ -853,6 +853,12 @@ function createProjectSupervisorRuntimeHarness({
|
||||
if (command === 'hydrate_design_agent_session') {
|
||||
return currentDesignAgentView;
|
||||
}
|
||||
if (command === 'get_design_agent_runtime_mode') {
|
||||
return currentDesignAgentView ? { activeRuntime: 'design' } : null;
|
||||
}
|
||||
if (command === 'set_design_agent_runtime_mode') {
|
||||
return { activeRuntime: args?.activeRuntime };
|
||||
}
|
||||
if (command === 'continue_design_agent_session') {
|
||||
const next = currentDesignContinueView ?? currentDesignAgentView;
|
||||
if (!next) {
|
||||
|
||||
@@ -222,6 +222,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -368,6 +369,7 @@ export function registerClientHomeTests() {
|
||||
let manifestChanged = false;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -537,6 +539,7 @@ export function registerClientHomeTests() {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
const requestedPath = String(args?.projectPath ?? '');
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: requestedPath,
|
||||
@@ -714,6 +717,7 @@ export function registerClientHomeTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: String(args?.projectPath ?? ''),
|
||||
@@ -1176,6 +1180,7 @@ export function registerHomeProjectCreationTests() {
|
||||
let inspectCount = 0;
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
inspectCount += 1;
|
||||
return {
|
||||
@@ -1241,6 +1246,7 @@ export function registerHomeProjectCreationTests() {
|
||||
it('does not open a missing or non-directory path from the open action', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
if (projectPath === '/tmp/broken-status') {
|
||||
@@ -1338,6 +1344,7 @@ export function registerHomeProjectCreationTests() {
|
||||
expect(args).toBeUndefined();
|
||||
return '/tmp/picked-game';
|
||||
}
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: '/tmp/picked-game',
|
||||
@@ -1368,6 +1375,7 @@ export function registerHomeProjectCreationTests() {
|
||||
if (command === 'pick_local_project_directory') {
|
||||
return '/tmp/broken-project';
|
||||
}
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: '/tmp/broken-project',
|
||||
@@ -2459,6 +2467,7 @@ export function registerRecentProjectsTests() {
|
||||
];
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
if (projectPath === paths[0]) {
|
||||
@@ -2605,6 +2614,7 @@ export function registerRecentProjectsTests() {
|
||||
);
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const renamed =
|
||||
invoke.mock.calls.filter(
|
||||
@@ -2659,6 +2669,7 @@ export function registerRecentProjectsTests() {
|
||||
const projectPath = '/tmp/rename-failed-game';
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -2705,6 +2716,7 @@ export function registerRecentProjectsTests() {
|
||||
it('opens a recent launcher project directory in the system file manager', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath: String(args?.projectPath ?? ''),
|
||||
@@ -2758,6 +2770,7 @@ export function registerRecentProjectsTests() {
|
||||
|
||||
it('does not remember a launcher project when project inspection fails', async () => {
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
throw new Error('inspect failed');
|
||||
}
|
||||
@@ -2775,6 +2788,7 @@ export function registerRecentProjectsTests() {
|
||||
it('marks missing recent launcher projects and does not reopen them', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
if (projectPath === '/tmp/broken-status') {
|
||||
@@ -2919,6 +2933,7 @@ export function registerRecentProjectsTests() {
|
||||
it('checks recent launcher project status automatically without changing the list', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
return {
|
||||
@@ -2977,6 +2992,7 @@ export function registerRecentProjectsTests() {
|
||||
}) => void)
|
||||
| null = null;
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return await new Promise((resolve) => {
|
||||
finishRefresh = resolve;
|
||||
|
||||
@@ -4749,6 +4749,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -4869,6 +4870,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -4933,6 +4935,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -5007,6 +5010,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -5108,6 +5112,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -5543,6 +5548,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
);
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -6031,6 +6037,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -6108,6 +6115,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
});
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
@@ -6182,6 +6190,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
if (command === 'pick_local_project_directory') {
|
||||
return projectPath;
|
||||
}
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
return {
|
||||
projectPath,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act, cleanup, renderHook } from '@testing-library/react';
|
||||
import { afterEach, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { useHomeProjectCreation } from '../src/features/app-shell/useHomeProjectCreation';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
delete window.__TAURI__;
|
||||
});
|
||||
|
||||
const projectPath = 'C:/test/design-project';
|
||||
const manifest = createGameCreationAppManifest('test-project', '策划项目');
|
||||
|
||||
function setup(invoke: ReturnType<typeof vi.fn>) {
|
||||
window.__TAURI__ = { core: { invoke } } as unknown as typeof window.__TAURI__;
|
||||
return renderHook(() =>
|
||||
useHomeProjectCreation({
|
||||
setStatus: vi.fn(),
|
||||
setLauncherView: vi.fn(),
|
||||
setAgentChatProjectPath: vi.fn(),
|
||||
rememberRecentWorkspace: vi.fn(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
it.each(['design', 'game', null] as const)(
|
||||
'restores %s before publishing the project context',
|
||||
async (mode) => {
|
||||
let resolveMode!: (value: { activeRuntime: string } | null) => void;
|
||||
const modePromise = new Promise<{ activeRuntime: string } | null>(
|
||||
(resolve) => {
|
||||
resolveMode = resolve;
|
||||
},
|
||||
);
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'inspect_local_project_directory')
|
||||
return { exists: true, isDirectory: true, isGameCreatorProject: true };
|
||||
if (command === 'get_local_game_manifest') return manifest;
|
||||
if (command === 'get_local_game_project_revision') return { revision: 1 };
|
||||
if (command === 'get_design_agent_runtime_mode') return modePromise;
|
||||
throw new Error(command);
|
||||
});
|
||||
const { result } = setup(invoke);
|
||||
let opening!: Promise<void>;
|
||||
await act(async () => {
|
||||
opening = result.current.openProject(projectPath, 'open');
|
||||
});
|
||||
expect(result.current.currentProjectContext).toBeNull();
|
||||
await act(async () => {
|
||||
resolveMode(mode ? { activeRuntime: mode } : null);
|
||||
await opening;
|
||||
});
|
||||
expect(result.current.currentProjectContext?.startMode).toBe(
|
||||
mode === 'design' ? 'planning' : null,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('persists design mode before entering a newly created planning project', async () => {
|
||||
const invoke = vi.fn(async (command: string) => {
|
||||
if (command === 'create_automatic_local_game_project')
|
||||
return { projectPath, manifest };
|
||||
if (command === 'set_design_agent_runtime_mode')
|
||||
return { activeRuntime: 'design' };
|
||||
if (command === 'get_local_game_project_revision') return { revision: 1 };
|
||||
throw new Error(command);
|
||||
});
|
||||
const { result } = setup(invoke);
|
||||
await act(async () => {
|
||||
await result.current.createHomeDraftAutomatically(
|
||||
{ creationType: 'game', prompt: '测试', attachments: [] },
|
||||
'planning',
|
||||
);
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('set_design_agent_runtime_mode', {
|
||||
projectPath,
|
||||
activeRuntime: 'design',
|
||||
});
|
||||
expect(result.current.currentProjectContext?.startMode).toBe('planning');
|
||||
});
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
> 当前口径:本文件保留可复用的排障经验;历史条目的旧路由、旧版本和已删除文档仅作根因背景,不得据此恢复退役入口。当前命令、路由和 schema 以代码与 `docs/README.md` 为准。
|
||||
|
||||
## 2026-09-12 策划项目重开前必须恢复运行模式
|
||||
|
||||
- 工作台不能只依赖创建时的内存 `startMode`:重开时丢失该值会挂载游戏资源画布,而对话恢复后又进入策划状态,造成左右区域不一致。
|
||||
- 新建策划项目进入工作台前持久化 `.agent/runtime-mode.json`;重开先读取模式再发布项目上下文。无模式文件的旧项目用策划会话恢复为 design,明确的 game 标记优先于旧会话。
|
||||
|
||||
## 2026-09-10 设计 Agent 需要流式 Responses 的原生 output[],不能只靠 tool_calls
|
||||
|
||||
- **现象**:新策划报 `Provider 返回工具调用但未提供完整 Responses output`。模型已经在调工具,`.debug/design-agent` 里 `tool_calls` 有值但 `output` 是 `[]`。
|
||||
|
||||
@@ -342,6 +342,8 @@ UI 使用“批准”和“继续修改”两个文字按钮,分别配 Lucide
|
||||
|
||||
## 14. 开发调试入口
|
||||
|
||||
项目运行模式通过 `.agent/runtime-mode.json` 持久化。新建策划项目在进入工作台前写入 `design`;“做成游戏”写入 `game`。重新打开项目时通过 `get_design_agent_runtime_mode` 读取模式,完成后一次性挂载对应工作台,不能先挂载 GameAgent 再切回策划。旧项目缺少模式文件但存在策划会话时按 `design` 恢复;明确的 `game` 标记优先于残留策划会话。无模式也无策划会话的项目仍使用游戏工作台。
|
||||
|
||||
开发构建的策划工作区页头在“刷新”旁提供“快速准备做成游戏测试”按钮。该入口与策划 Debug 日志共用 `GENARRATIVE_AGC_DESIGN_DEBUG=1` 开关:开关未启用时按钮不显示,命令也不可执行。入口仅进行本地 fixture 和会话状态写入,不调用 Provider;完成后自动刷新文件树与阶段,通过 `design-agent-update` 状态事件同步右侧审批/阶段操作区。随后仍需点击正常的“做成游戏”按钮执行资产登记与运行时切换。
|
||||
|
||||
## 15. 策划 Agent reasoning 展示现状
|
||||
|
||||
Reference in New Issue
Block a user