34b1ad5d5c
- 新增 4 个仓库内手写 Godot 4.7 模板源:godot-empty-2d、godot-empty-3d、godot-hello-world、godot-platformer-2d,entry 统一为 project.godot,各自带 meta.json 与 960×540 封面 - template_library.rs:复制模板后按 project.godot 识别 Godot 模板,改写工程显示名后走既有 Godot 导入并写入 godotProjectRoot,不再落到 Web 占位入口 - manifest.rs:新增 apply_godot_project_display_name,只改 [application] 段的 config/name 一行,其余字节逐字保留 - manifest.rs:放宽 Godot 工作区发现,一层子目录多命中按目录名排序取第一个,project.godot 允许是符号链接 / reparse point / 硬链接 - commands.rs:同步 Godot 导入错误文案,去掉「普通文件」限定 - RuntimeConfigDialog.tsx:内置插件行去掉手动启动 / 停止按钮,启动由前端自动启动承担、停止走该行启用开关 - import_tests.rs 与 tests/project.rs:补显示名改写、多命中确定性、链接标记与错误文案的定向回归 - 同步更新 PRD、模板包组织指南、模板库与模板建项技术方案、AGC Godot 编辑器插件接入方案、实施计划与共享决策记录
1464 lines
54 KiB
TypeScript
1464 lines
54 KiB
TypeScript
import {
|
|
Bot,
|
|
CheckCircle2,
|
|
CircleAlert,
|
|
FolderOpen,
|
|
Info,
|
|
LoaderCircle,
|
|
Pencil,
|
|
RotateCcw,
|
|
Save,
|
|
Settings2,
|
|
SlidersHorizontal,
|
|
Trash2,
|
|
Upload,
|
|
X,
|
|
} from 'lucide-react';
|
|
import { type FormEvent, useEffect, useRef, useState } from 'react';
|
|
|
|
import BRAND_ICON from '../../../../../packages/shared/src/icons/taonier-product-ip.png';
|
|
import { APP_NAME, APP_VERSION } from '../../app/appMetadata';
|
|
import {
|
|
closeDialogOnBackdropMouseDown,
|
|
closeDialogOnEscape,
|
|
useEscapeToClose,
|
|
} from '../../app/dialogs';
|
|
import { appUpdateCheckEnabled } from '../../app/featureFlags';
|
|
import { resolveTauriInvoke } from '../../app/tauri';
|
|
import {
|
|
type AgcPluginPanel,
|
|
type AgcPluginSummary,
|
|
type ClientExtensionImportResult,
|
|
type ClientExtensionItem,
|
|
type GameCreatorAppConfig,
|
|
type GameCreatorAppConfigView,
|
|
type GameCreatorLlmReasoningEffort,
|
|
gameCreatorLlmReasoningEfforts,
|
|
} from '../../app/types';
|
|
import { checkForAppUpdate } from '../../services/appUpdate';
|
|
import { notifyLlmConfigChanged } from '../../services/llmModelCatalog';
|
|
import {
|
|
listAgcExtensions,
|
|
reloadAgcPlugin,
|
|
setAgcPluginEnabled,
|
|
setAgcPluginProjectPath,
|
|
startAgcPlugin,
|
|
stopAgcPlugin,
|
|
} from '../../services/pluginHost';
|
|
import { useProjectCreationDirectory } from '../app-shell/useProjectCreationDirectory';
|
|
import { PluginPanelHost } from '../plugins/PluginPanelHost';
|
|
import { reasoningEffortLabel } from '../project-workspace/composerReasoningEffort';
|
|
import { CustomLlmSettings } from './CustomLlmSettings';
|
|
|
|
const defaultRuntimeConfigDraft: GameCreatorAppConfig = {
|
|
schemaVersion: 'game-creator-config.v2',
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
customEnabled: false,
|
|
visibleModels: [],
|
|
apiKey: '',
|
|
baseUrl: '',
|
|
model: '',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: true,
|
|
webSearchEnabled: true,
|
|
contextWindowTokens: 128000,
|
|
autoCompactTokenLimit: 64000,
|
|
toolOutputTokenLimit: 12000,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 10,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'https://dev.genarrative.world',
|
|
apiKey: '',
|
|
},
|
|
};
|
|
|
|
type RuntimeSettingsSection =
|
|
| 'general'
|
|
| 'workspace'
|
|
| 'agents'
|
|
| 'extensions'
|
|
| 'advanced'
|
|
| 'about';
|
|
|
|
type RuntimeConfigToast = {
|
|
tone: 'success' | 'error';
|
|
message: string;
|
|
};
|
|
|
|
type ClientExtensionsLoadState = 'loading' | 'ready' | 'error';
|
|
|
|
const runtimeSettingsSections = [
|
|
{
|
|
id: 'general',
|
|
label: '常用设置',
|
|
description: '运行方式与输出偏好',
|
|
icon: Settings2,
|
|
},
|
|
{
|
|
id: 'workspace',
|
|
label: '工作区',
|
|
description: '项目创建目录',
|
|
icon: FolderOpen,
|
|
},
|
|
{
|
|
id: 'agents',
|
|
label: 'Agent 分工',
|
|
description: '查看各角色运行状态',
|
|
icon: Bot,
|
|
},
|
|
{
|
|
id: 'extensions',
|
|
label: '扩展',
|
|
description: 'Skill 与 MCP',
|
|
icon: Upload,
|
|
},
|
|
{
|
|
id: 'advanced',
|
|
label: '高级参数',
|
|
description: '上下文、超时与重试',
|
|
icon: SlidersHorizontal,
|
|
},
|
|
{
|
|
id: 'about',
|
|
label: '关于',
|
|
description: '客户端信息',
|
|
icon: Info,
|
|
},
|
|
] as const satisfies ReadonlyArray<{
|
|
id: RuntimeSettingsSection;
|
|
label: string;
|
|
description: string;
|
|
icon: typeof Settings2;
|
|
}>;
|
|
|
|
function clampRuntimeConfigNumber(value: number, minimum: number) {
|
|
const numericValue = Number(value);
|
|
return Number.isFinite(numericValue) && numericValue >= minimum
|
|
? numericValue
|
|
: minimum;
|
|
}
|
|
|
|
function isGameCreatorLlmReasoningEffort(
|
|
value: unknown,
|
|
): value is GameCreatorLlmReasoningEffort {
|
|
return gameCreatorLlmReasoningEfforts.some((effort) => effort === value);
|
|
}
|
|
|
|
function normalizeRuntimeConfigDraft(
|
|
config: GameCreatorAppConfig,
|
|
allowAdvancedExternalEditorConfig: boolean,
|
|
): GameCreatorAppConfig {
|
|
const reasoningEffort = isGameCreatorLlmReasoningEffort(
|
|
config.llm.reasoningEffort,
|
|
)
|
|
? config.llm.reasoningEffort
|
|
: defaultRuntimeConfigDraft.llm.reasoningEffort;
|
|
return {
|
|
...config,
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
...config.llm,
|
|
apiKey: config.llm.customEnabled ? config.llm.apiKey : '',
|
|
baseUrl: config.llm.customEnabled ? config.llm.baseUrl : '',
|
|
model: config.llm.customEnabled ? config.llm.model : '',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort,
|
|
webSearchEnabled:
|
|
typeof config.llm.webSearchEnabled === 'boolean'
|
|
? config.llm.webSearchEnabled
|
|
: defaultRuntimeConfigDraft.llm.webSearchEnabled,
|
|
contextWindowTokens: clampRuntimeConfigNumber(
|
|
typeof config.llm.contextWindowTokens === 'number'
|
|
? config.llm.contextWindowTokens
|
|
: defaultRuntimeConfigDraft.llm.contextWindowTokens,
|
|
1,
|
|
),
|
|
autoCompactTokenLimit: clampRuntimeConfigNumber(
|
|
typeof config.llm.autoCompactTokenLimit === 'number'
|
|
? config.llm.autoCompactTokenLimit
|
|
: defaultRuntimeConfigDraft.llm.autoCompactTokenLimit,
|
|
1,
|
|
),
|
|
toolOutputTokenLimit: clampRuntimeConfigNumber(
|
|
typeof config.llm.toolOutputTokenLimit === 'number'
|
|
? config.llm.toolOutputTokenLimit
|
|
: defaultRuntimeConfigDraft.llm.toolOutputTokenLimit,
|
|
1,
|
|
),
|
|
requestTimeoutMs: clampRuntimeConfigNumber(
|
|
config.llm.requestTimeoutMs,
|
|
1000,
|
|
),
|
|
maxRetries: clampRuntimeConfigNumber(config.llm.maxRetries, 0),
|
|
retryBackoffMs: clampRuntimeConfigNumber(config.llm.retryBackoffMs, 1),
|
|
},
|
|
// 客户端统一使用全局连接,设置不保留独立的 Agent 路由覆盖。
|
|
agentLlm: {},
|
|
editorApi: allowAdvancedExternalEditorConfig
|
|
? { ...defaultRuntimeConfigDraft.editorApi, ...config.editorApi }
|
|
: { ...defaultRuntimeConfigDraft.editorApi },
|
|
};
|
|
}
|
|
|
|
export function RuntimeConfigDialog({
|
|
projectPath,
|
|
allowAdvancedExternalEditorConfig = false,
|
|
onClose,
|
|
onLog,
|
|
}: {
|
|
projectPath?: string;
|
|
allowAdvancedExternalEditorConfig?: boolean;
|
|
onClose: () => void;
|
|
onLog?: (entry: string) => void;
|
|
}) {
|
|
// The prop is intentionally not sufficient to expose developer credentials:
|
|
// only an explicitly opted-in Vite development build may render the legacy
|
|
// External Editor fields. Release bundles therefore ignore accidental or
|
|
// stale callers that pass the prop.
|
|
const advancedExternalEditorConfigEnabled =
|
|
allowAdvancedExternalEditorConfig &&
|
|
import.meta.env.DEV &&
|
|
import.meta.env.VITE_AGC_ENABLE_ADVANCED_EXTERNAL_EDITOR_CONFIG === '1';
|
|
const [runtimeConfigPath, setRuntimeConfigPath] = useState('');
|
|
const [runtimeConfigStatus, setRuntimeConfigStatus] = useState('未读取');
|
|
const [runtimeConfigToast, setRuntimeConfigToast] =
|
|
useState<RuntimeConfigToast | null>(null);
|
|
const [runtimeConfigDraft, setRuntimeConfigDraft] =
|
|
useState<GameCreatorAppConfig>(defaultRuntimeConfigDraft);
|
|
const [runtimeConfigBusy, setRuntimeConfigBusy] = useState(false);
|
|
const [activeSection, setActiveSection] =
|
|
useState<RuntimeSettingsSection>('general');
|
|
/**
|
|
* 「项目创建目录」是客户端本地偏好(localStorage),不随下面的配置文件一起保存:
|
|
* 选择目录当场生效,做游戏 / 做方案与模板建项下一次建项就落在该目录下。
|
|
*/
|
|
const projectCreationDirectory = useProjectCreationDirectory();
|
|
const [clientExtensions, setClientExtensions] = useState<
|
|
ClientExtensionItem[]
|
|
>([]);
|
|
const [clientExtensionsBusy, setClientExtensionsBusy] = useState(false);
|
|
const [clientExtensionsLoadState, setClientExtensionsLoadState] =
|
|
useState<ClientExtensionsLoadState>('loading');
|
|
const [clientExtensionsStatus, setClientExtensionsStatus] = useState('');
|
|
const [agcPlugins, setAgcPlugins] = useState<AgcPluginSummary[]>([]);
|
|
const [agcPluginsBusy, setAgcPluginsBusy] = useState(false);
|
|
const [agcPluginsStatus, setAgcPluginsStatus] = useState('');
|
|
const [mountedPluginPanel, setMountedPluginPanel] = useState<{
|
|
pluginId: string;
|
|
panel: AgcPluginPanel;
|
|
} | null>(null);
|
|
const [editingExtensionId, setEditingExtensionId] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [editingExtensionName, setEditingExtensionName] = useState('');
|
|
const [appUpdateStatus, setAppUpdateStatus] = useState('');
|
|
const [appUpdateChecking, setAppUpdateChecking] = useState(false);
|
|
const runtimeConfigBusyRef = useRef(false);
|
|
|
|
useEscapeToClose(
|
|
mountedPluginPanel ? () => setMountedPluginPanel(null) : onClose,
|
|
);
|
|
|
|
useEffect(() => {
|
|
void setAgcPluginProjectPath(projectPath ?? null).catch(() => undefined);
|
|
}, [projectPath]);
|
|
|
|
useEffect(() => {
|
|
if (activeSection !== 'extensions') return;
|
|
const timer = window.setInterval(() => void readAgcPlugins(), 1000);
|
|
return () => window.clearInterval(timer);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [activeSection]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
mountedPluginPanel &&
|
|
!agcPlugins.some(
|
|
(plugin) =>
|
|
plugin.id === mountedPluginPanel.pluginId &&
|
|
plugin.status === 'running',
|
|
)
|
|
) {
|
|
setMountedPluginPanel(null);
|
|
}
|
|
}, [agcPlugins, mountedPluginPanel]);
|
|
|
|
useEffect(() => {
|
|
const htmlOverflow = document.documentElement.style.overflow;
|
|
const bodyOverflow = document.body.style.overflow;
|
|
document.documentElement.style.overflow = 'hidden';
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
return () => {
|
|
document.documentElement.style.overflow = htmlOverflow;
|
|
document.body.style.overflow = bodyOverflow;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void readRuntimeConfig();
|
|
void readClientExtensions();
|
|
void readAgcPlugins();
|
|
// The dialog reads once on mount; subsequent reads are explicit user actions.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
async function readAgcPlugins() {
|
|
try {
|
|
const extensions = await listAgcExtensions();
|
|
setAgcPlugins(
|
|
extensions.flatMap((extension) =>
|
|
extension.kind === 'plugin' && extension.plugin
|
|
? [extension.plugin]
|
|
: [],
|
|
),
|
|
);
|
|
setAgcPluginsStatus('');
|
|
} catch (error) {
|
|
setAgcPluginsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
}
|
|
}
|
|
|
|
async function toggleAgcPlugin(plugin: AgcPluginSummary) {
|
|
if (agcPluginsBusy) return;
|
|
setAgcPluginsBusy(true);
|
|
try {
|
|
const updated =
|
|
plugin.status === 'running'
|
|
? await stopAgcPlugin(plugin.id)
|
|
: await startAgcPlugin(plugin.id);
|
|
setAgcPlugins((current) =>
|
|
current.map((item) => (item.id === updated.id ? updated : item)),
|
|
);
|
|
setAgcPluginsStatus('');
|
|
} catch (error) {
|
|
setAgcPluginsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setAgcPluginsBusy(false);
|
|
}
|
|
}
|
|
|
|
async function reloadPlugin(plugin: AgcPluginSummary) {
|
|
if (agcPluginsBusy) return;
|
|
setAgcPluginsBusy(true);
|
|
try {
|
|
const updated = await reloadAgcPlugin(plugin.id);
|
|
setAgcPlugins((current) =>
|
|
current.map((item) => (item.id === updated.id ? updated : item)),
|
|
);
|
|
setAgcPluginsStatus('插件已重新加载');
|
|
} catch (error) {
|
|
setAgcPluginsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setAgcPluginsBusy(false);
|
|
}
|
|
}
|
|
|
|
/** 内置插件没有卸载入口,只用可用开关控制是否对客户端和 Agent 生效。 */
|
|
async function setBuiltinPluginEnabled(
|
|
plugin: AgcPluginSummary,
|
|
enabled: boolean,
|
|
) {
|
|
if (agcPluginsBusy) return;
|
|
setAgcPluginsBusy(true);
|
|
try {
|
|
const plugins = await setAgcPluginEnabled(plugin.id, enabled);
|
|
setAgcPlugins(plugins);
|
|
await readClientExtensions();
|
|
setAgcPluginsStatus(enabled ? '内置插件已启用' : '内置插件已禁用');
|
|
} catch (error) {
|
|
setAgcPluginsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setAgcPluginsBusy(false);
|
|
}
|
|
}
|
|
|
|
async function readClientExtensions() {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
setClientExtensionsLoadState('error');
|
|
setClientExtensionsStatus('需要在 Tauri App 内运行');
|
|
return;
|
|
}
|
|
setClientExtensionsLoadState('loading');
|
|
setClientExtensionsBusy(true);
|
|
try {
|
|
const result = await invoke<ClientExtensionItem[]>(
|
|
'list_client_extensions',
|
|
);
|
|
setClientExtensions(result);
|
|
setClientExtensionsLoadState('ready');
|
|
setClientExtensionsStatus('');
|
|
} catch (error) {
|
|
setClientExtensionsLoadState('error');
|
|
setClientExtensionsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setClientExtensionsBusy(false);
|
|
}
|
|
}
|
|
|
|
async function importClientExtension(kind: 'file' | 'directory') {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
setClientExtensionsStatus('需要在 Tauri App 内运行');
|
|
return;
|
|
}
|
|
if (clientExtensionsBusy) {
|
|
return;
|
|
}
|
|
setClientExtensionsBusy(true);
|
|
setClientExtensionsStatus('正在导入');
|
|
try {
|
|
const selectedPath =
|
|
kind === 'file'
|
|
? await invoke<string | null>('pick_client_extension_file')
|
|
: await invoke<string | null>('pick_client_extension_directory');
|
|
if (!selectedPath) {
|
|
setClientExtensionsStatus('');
|
|
return;
|
|
}
|
|
const result = await invoke<ClientExtensionImportResult>(
|
|
'import_client_extension',
|
|
{ sourcePath: selectedPath },
|
|
);
|
|
const refreshed = await invoke<ClientExtensionItem[]>(
|
|
'list_client_extensions',
|
|
);
|
|
setClientExtensions(refreshed);
|
|
await readAgcPlugins();
|
|
setClientExtensionsLoadState('ready');
|
|
const importedCount = result.imported.length;
|
|
setClientExtensionsStatus(
|
|
importedCount > 0
|
|
? `已导入 ${importedCount} 个扩展${result.renamed ? ',同名项已自动追加编号' : ''}`
|
|
: '未发现可管理的扩展',
|
|
);
|
|
} catch (error) {
|
|
setClientExtensionsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setClientExtensionsBusy(false);
|
|
}
|
|
}
|
|
|
|
function beginRenameClientExtension(item: ClientExtensionItem) {
|
|
setEditingExtensionId(item.id);
|
|
setEditingExtensionName(item.name);
|
|
setClientExtensionsStatus('');
|
|
}
|
|
|
|
function cancelRenameClientExtension() {
|
|
setEditingExtensionId(null);
|
|
setEditingExtensionName('');
|
|
}
|
|
|
|
async function saveClientExtensionName(item: ClientExtensionItem) {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke || clientExtensionsBusy) {
|
|
return;
|
|
}
|
|
setClientExtensionsBusy(true);
|
|
try {
|
|
const updated = await invoke<ClientExtensionItem>(
|
|
'rename_client_extension',
|
|
{ id: item.id, name: editingExtensionName },
|
|
);
|
|
setClientExtensions((current) =>
|
|
current.map((candidate) =>
|
|
candidate.id === updated.id ? updated : candidate,
|
|
),
|
|
);
|
|
await readAgcPlugins();
|
|
const requestedName = editingExtensionName.trim();
|
|
setClientExtensionsStatus(
|
|
updated.name === requestedName
|
|
? '扩展名称已更新'
|
|
: `扩展名称已调整为“${updated.name}”`,
|
|
);
|
|
cancelRenameClientExtension();
|
|
} catch (error) {
|
|
setClientExtensionsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setClientExtensionsBusy(false);
|
|
}
|
|
}
|
|
|
|
async function setClientExtensionEnabled(
|
|
item: ClientExtensionItem,
|
|
enabled: boolean,
|
|
) {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke || clientExtensionsBusy || item.extensionType === 'unknown') {
|
|
return;
|
|
}
|
|
setClientExtensionsBusy(true);
|
|
try {
|
|
const updated = await invoke<ClientExtensionItem>(
|
|
'set_client_extension_enabled',
|
|
{ id: item.id, enabled },
|
|
);
|
|
setClientExtensions((current) =>
|
|
current.map((candidate) =>
|
|
candidate.id === updated.id ? updated : candidate,
|
|
),
|
|
);
|
|
await readClientExtensions();
|
|
await readAgcPlugins();
|
|
} catch (error) {
|
|
setClientExtensionsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setClientExtensionsBusy(false);
|
|
}
|
|
}
|
|
|
|
async function removeClientExtension(item: ClientExtensionItem) {
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke || clientExtensionsBusy) {
|
|
return;
|
|
}
|
|
setClientExtensionsBusy(true);
|
|
try {
|
|
await invoke('remove_client_extension', { id: item.id });
|
|
await readClientExtensions();
|
|
await readAgcPlugins();
|
|
if (editingExtensionId === item.id) {
|
|
cancelRenameClientExtension();
|
|
}
|
|
setClientExtensionsStatus('扩展已移除');
|
|
} catch (error) {
|
|
setClientExtensionsStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setClientExtensionsBusy(false);
|
|
}
|
|
}
|
|
|
|
function updateRuntimeLlmConfig<K extends keyof GameCreatorAppConfig['llm']>(
|
|
key: K,
|
|
value: GameCreatorAppConfig['llm'][K],
|
|
) {
|
|
setRuntimeConfigDraft((current) => ({
|
|
...current,
|
|
llm: {
|
|
...current.llm,
|
|
[key]: value,
|
|
},
|
|
}));
|
|
}
|
|
|
|
// 常用设置仅调整运行参数,读写时无需外部诊断,避免进程探测拖慢页面加载和保存反馈。
|
|
async function readRuntimeConfig() {
|
|
if (runtimeConfigBusyRef.current) {
|
|
return;
|
|
}
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
setRuntimeConfigStatus('需要在 Tauri App 内运行');
|
|
return;
|
|
}
|
|
|
|
runtimeConfigBusyRef.current = true;
|
|
setRuntimeConfigBusy(true);
|
|
setRuntimeConfigStatus('正在读取');
|
|
try {
|
|
const result = await invoke<GameCreatorAppConfigView>(
|
|
'read_game_creator_app_config',
|
|
);
|
|
setRuntimeConfigPath(result.path);
|
|
const config = normalizeRuntimeConfigDraft(
|
|
result.config,
|
|
advancedExternalEditorConfigEnabled,
|
|
);
|
|
setRuntimeConfigDraft(config);
|
|
setRuntimeConfigStatus(`已读取:${result.path}`);
|
|
onLog?.('runtime_config.read');
|
|
} catch (error) {
|
|
setRuntimeConfigStatus(
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
runtimeConfigBusyRef.current = false;
|
|
setRuntimeConfigBusy(false);
|
|
}
|
|
}
|
|
|
|
async function handleRuntimeConfigSave(event: FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (runtimeConfigBusyRef.current) {
|
|
return;
|
|
}
|
|
const invoke = resolveTauriInvoke();
|
|
if (!invoke) {
|
|
setRuntimeConfigStatus('需要在 Tauri App 内运行');
|
|
return;
|
|
}
|
|
|
|
runtimeConfigBusyRef.current = true;
|
|
setRuntimeConfigBusy(true);
|
|
setRuntimeConfigToast(null);
|
|
setRuntimeConfigStatus('正在保存');
|
|
try {
|
|
const config = normalizeRuntimeConfigDraft(
|
|
{
|
|
...runtimeConfigDraft,
|
|
agentMode: 'codex_app_server',
|
|
editorApi: advancedExternalEditorConfigEnabled
|
|
? runtimeConfigDraft.editorApi
|
|
: defaultRuntimeConfigDraft.editorApi,
|
|
},
|
|
advancedExternalEditorConfigEnabled,
|
|
);
|
|
const result = await invoke<GameCreatorAppConfigView>(
|
|
'write_game_creator_app_config',
|
|
{ config },
|
|
);
|
|
setRuntimeConfigPath(result.path);
|
|
const savedConfig = normalizeRuntimeConfigDraft(
|
|
result.config,
|
|
advancedExternalEditorConfigEnabled,
|
|
);
|
|
setRuntimeConfigDraft(savedConfig);
|
|
notifyLlmConfigChanged();
|
|
setRuntimeConfigStatus(`已保存:${result.path}`);
|
|
setRuntimeConfigToast({
|
|
tone: 'success',
|
|
message: '保存成功,新的运行时配置已生效',
|
|
});
|
|
onLog?.('runtime_config.save');
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
setRuntimeConfigStatus(message);
|
|
setRuntimeConfigToast({
|
|
tone: 'error',
|
|
message: `保存失败:${message}`,
|
|
});
|
|
} finally {
|
|
runtimeConfigBusyRef.current = false;
|
|
setRuntimeConfigBusy(false);
|
|
}
|
|
}
|
|
|
|
function resetRuntimeConfigDraft() {
|
|
setRuntimeConfigDraft({
|
|
...defaultRuntimeConfigDraft,
|
|
llm: {
|
|
...defaultRuntimeConfigDraft.llm,
|
|
customEnabled: runtimeConfigDraft.llm.customEnabled,
|
|
},
|
|
});
|
|
setRuntimeConfigStatus('已恢复默认配置,保存后生效');
|
|
}
|
|
|
|
async function checkAppUpdateManually() {
|
|
if (appUpdateChecking) return;
|
|
setAppUpdateChecking(true);
|
|
setAppUpdateStatus('正在检查更新…');
|
|
try {
|
|
const update = await checkForAppUpdate({ force: true });
|
|
setAppUpdateStatus(
|
|
update
|
|
? `发现新版本 v${update.version},可在右上角下载`
|
|
: '当前已是最新版本',
|
|
);
|
|
} catch {
|
|
setAppUpdateStatus('检查更新失败,请稍后重试');
|
|
} finally {
|
|
setAppUpdateChecking(false);
|
|
}
|
|
}
|
|
|
|
const selectedSection =
|
|
runtimeSettingsSections.find((section) => section.id === activeSection) ??
|
|
runtimeSettingsSections[0];
|
|
const runtimeConfigStatusTone = runtimeConfigBusy
|
|
? 'busy'
|
|
: /^(已保存|已读取|已恢复默认)/.test(runtimeConfigStatus)
|
|
? 'success'
|
|
: runtimeConfigStatus === '未读取'
|
|
? 'neutral'
|
|
: 'warning';
|
|
|
|
return (
|
|
<div
|
|
className="settings-overlay"
|
|
role="presentation"
|
|
onMouseDown={(event) => closeDialogOnBackdropMouseDown(event, onClose)}
|
|
>
|
|
{runtimeConfigToast ? (
|
|
<div
|
|
className="runtime-settings-toast"
|
|
data-tone={runtimeConfigToast.tone}
|
|
role={runtimeConfigToast.tone === 'error' ? 'alert' : 'status'}
|
|
aria-label="运行时配置提示"
|
|
aria-live={
|
|
runtimeConfigToast.tone === 'error' ? 'assertive' : 'polite'
|
|
}
|
|
>
|
|
{runtimeConfigToast.tone === 'error' ? (
|
|
<CircleAlert size={16} aria-hidden="true" />
|
|
) : (
|
|
<CheckCircle2 size={16} aria-hidden="true" />
|
|
)}
|
|
<span>{runtimeConfigToast.message}</span>
|
|
</div>
|
|
) : null}
|
|
<form
|
|
className="settings-panel runtime-settings-panel"
|
|
role="dialog"
|
|
aria-label="运行时配置"
|
|
aria-modal="true"
|
|
onSubmit={handleRuntimeConfigSave}
|
|
onKeyDown={(event) => closeDialogOnEscape(event, onClose)}
|
|
>
|
|
<header className="runtime-settings-header">
|
|
<div>
|
|
<span className="runtime-settings-eyebrow">AI GAME CREATOR</span>
|
|
<h2>Agent 设置</h2>
|
|
</div>
|
|
<button
|
|
className="runtime-settings-close"
|
|
type="button"
|
|
aria-label="关闭 Agent 设置"
|
|
onClick={onClose}
|
|
>
|
|
<X size={18} aria-hidden="true" />
|
|
</button>
|
|
</header>
|
|
<div className="runtime-settings-layout">
|
|
<aside className="runtime-settings-nav" aria-label="设置分类">
|
|
<nav>
|
|
{runtimeSettingsSections.map((section) => {
|
|
const Icon = section.icon;
|
|
return (
|
|
<button
|
|
key={section.id}
|
|
type="button"
|
|
className={activeSection === section.id ? 'is-active' : ''}
|
|
aria-current={
|
|
activeSection === section.id ? 'page' : undefined
|
|
}
|
|
onClick={() => setActiveSection(section.id)}
|
|
>
|
|
<Icon size={17} aria-hidden="true" />
|
|
<span>
|
|
<strong>{section.label}</strong>
|
|
<small>{section.description}</small>
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
<div className="runtime-settings-config-state">
|
|
<span>配置文件</span>
|
|
<strong>{runtimeConfigPath ? '已载入' : '等待载入'}</strong>
|
|
{runtimeConfigPath ? (
|
|
<small title={runtimeConfigPath}>{runtimeConfigPath}</small>
|
|
) : null}
|
|
</div>
|
|
</aside>
|
|
<main className="runtime-settings-content">
|
|
<header className="runtime-settings-section-header">
|
|
<div>
|
|
<h3>{selectedSection.label}</h3>
|
|
<p>{selectedSection.description}</p>
|
|
</div>
|
|
{activeSection === 'extensions' ? (
|
|
<div className="runtime-settings-section-actions">
|
|
<button
|
|
type="button"
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() => void importClientExtension('file')}
|
|
>
|
|
<Upload size={14} aria-hidden="true" />
|
|
导入文件或 zip
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() => void importClientExtension('directory')}
|
|
>
|
|
<Upload size={14} aria-hidden="true" />
|
|
导入目录
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</header>
|
|
<div className="settings-grid runtime-settings-fields">
|
|
{activeSection === 'general' ? (
|
|
<>
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>工作方式</span>
|
|
<strong>陶泥儿智能创作(固定)</strong>
|
|
{!runtimeConfigDraft.llm.customEnabled ? (
|
|
<small>需求将由官方智能服务执行</small>
|
|
) : null}
|
|
</div>
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>智能服务</span>
|
|
<strong>
|
|
{runtimeConfigDraft.llm.customEnabled
|
|
? '自定义 LLM'
|
|
: '官方账号服务(固定)'}
|
|
</strong>
|
|
{!runtimeConfigDraft.llm.customEnabled ? (
|
|
<small>登录后自动使用当前账号权限。</small>
|
|
) : null}
|
|
</div>
|
|
{runtimeConfigDraft.llm.customEnabled ? (
|
|
<>
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>协议</span>
|
|
<strong>OpenAI Responses</strong>
|
|
<small>自定义端点需兼容该协议。</small>
|
|
</div>
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>推理档</span>
|
|
<strong>
|
|
{reasoningEffortLabel(
|
|
runtimeConfigDraft.llm.reasoningEffort,
|
|
)}
|
|
</strong>
|
|
<small>在对话输入盒的模型旁按回合调整。</small>
|
|
</div>
|
|
<CustomLlmSettings
|
|
llm={runtimeConfigDraft.llm}
|
|
disabled={runtimeConfigBusy}
|
|
onChange={(llm) =>
|
|
setRuntimeConfigDraft((current) => ({
|
|
...current,
|
|
llm,
|
|
}))
|
|
}
|
|
/>
|
|
</>
|
|
) : null}
|
|
{runtimeConfigDraft.agentMode !== 'codex_cli' ? (
|
|
<>
|
|
{/* 推理档已下移到对话输入盒的模型选择器旁(按回合生效),
|
|
设置里不再重复一份。 */}
|
|
<label className="settings-checkbox">
|
|
<input
|
|
aria-label="流式输出"
|
|
type="checkbox"
|
|
checked={runtimeConfigDraft.llm.stream}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'stream',
|
|
event.currentTarget.checked,
|
|
)
|
|
}
|
|
/>
|
|
流式输出
|
|
</label>
|
|
<label className="settings-checkbox">
|
|
<input
|
|
aria-label="联网检索"
|
|
type="checkbox"
|
|
checked={runtimeConfigDraft.llm.webSearchEnabled}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'webSearchEnabled',
|
|
event.currentTarget.checked,
|
|
)
|
|
}
|
|
/>
|
|
联网检索
|
|
</label>
|
|
</>
|
|
) : null}
|
|
</>
|
|
) : null}
|
|
{activeSection === 'workspace' ? (
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>项目创建目录</span>
|
|
<strong
|
|
title={
|
|
projectCreationDirectory.projectCreationDirectory ||
|
|
undefined
|
|
}
|
|
>
|
|
{projectCreationDirectory.projectCreationDirectory ||
|
|
'默认位置'}
|
|
</strong>
|
|
<div className="runtime-settings-field-actions">
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
projectCreationDirectory.projectCreationDirectoryBusy
|
|
}
|
|
onClick={() =>
|
|
void projectCreationDirectory.pickProjectCreationDirectory()
|
|
}
|
|
>
|
|
<FolderOpen size={14} aria-hidden="true" />
|
|
选择目录
|
|
</button>
|
|
{projectCreationDirectory.projectCreationDirectory ? (
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
projectCreationDirectory.projectCreationDirectoryBusy
|
|
}
|
|
onClick={
|
|
projectCreationDirectory.resetProjectCreationDirectory
|
|
}
|
|
>
|
|
<RotateCcw size={14} aria-hidden="true" />
|
|
恢复默认位置
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
{projectCreationDirectory.projectCreationDirectoryStatus ? (
|
|
<small>
|
|
{projectCreationDirectory.projectCreationDirectoryStatus}
|
|
</small>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
{activeSection === 'advanced' &&
|
|
runtimeConfigDraft.agentMode !== 'codex_cli' ? (
|
|
<>
|
|
<label>
|
|
上下文窗口 tokens
|
|
<input
|
|
aria-label="上下文窗口 tokens"
|
|
type="number"
|
|
min="1"
|
|
value={runtimeConfigDraft.llm.contextWindowTokens}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'contextWindowTokens',
|
|
Math.max(1, Number(event.currentTarget.value) || 1),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
自动压缩阈值 tokens
|
|
<input
|
|
aria-label="自动压缩阈值 tokens"
|
|
type="number"
|
|
min="1"
|
|
value={runtimeConfigDraft.llm.autoCompactTokenLimit}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'autoCompactTokenLimit',
|
|
Math.max(1, Number(event.currentTarget.value) || 1),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
工具输出上限 tokens
|
|
<input
|
|
aria-label="工具输出上限 tokens"
|
|
type="number"
|
|
min="1"
|
|
value={runtimeConfigDraft.llm.toolOutputTokenLimit}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'toolOutputTokenLimit',
|
|
Math.max(1, Number(event.currentTarget.value) || 1),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
请求超时 ms
|
|
<input
|
|
aria-label="请求超时 ms"
|
|
type="number"
|
|
min="1000"
|
|
value={runtimeConfigDraft.llm.requestTimeoutMs}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'requestTimeoutMs',
|
|
Math.max(
|
|
1000,
|
|
Number(event.currentTarget.value) || 1000,
|
|
),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
重试次数
|
|
<input
|
|
aria-label="重试次数"
|
|
type="number"
|
|
min="0"
|
|
value={runtimeConfigDraft.llm.maxRetries}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'maxRetries',
|
|
Math.max(0, Number(event.currentTarget.value) || 0),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
<label>
|
|
重试退避 ms
|
|
<input
|
|
aria-label="重试退避 ms"
|
|
type="number"
|
|
min="1"
|
|
value={runtimeConfigDraft.llm.retryBackoffMs}
|
|
onChange={(event) =>
|
|
updateRuntimeLlmConfig(
|
|
'retryBackoffMs',
|
|
Math.max(1, Number(event.currentTarget.value) || 1),
|
|
)
|
|
}
|
|
/>
|
|
</label>
|
|
</>
|
|
) : null}
|
|
{activeSection === 'agents' ? (
|
|
<div className="runtime-settings-readonly-field">
|
|
<span>角色服务</span>
|
|
<strong>所有角色使用统一账号服务</strong>
|
|
<small>普通用户无需配置服务连接信息。</small>
|
|
</div>
|
|
) : null}
|
|
{activeSection === 'extensions' ? (
|
|
<section
|
|
className="runtime-settings-section runtime-settings-extensions"
|
|
aria-label="客户端扩展"
|
|
>
|
|
{clientExtensionsStatus &&
|
|
clientExtensionsLoadState !== 'error' ? (
|
|
<p className="runtime-settings-inline-status" role="status">
|
|
{clientExtensionsStatus}
|
|
</p>
|
|
) : null}
|
|
{agcPlugins.some((plugin) => plugin.builtin) ? (
|
|
<div className="runtime-settings-extension-list">
|
|
{agcPlugins
|
|
.filter((plugin) => plugin.builtin)
|
|
.map((plugin) => (
|
|
<article
|
|
className="runtime-settings-extension-item"
|
|
key={plugin.id}
|
|
>
|
|
<div className="runtime-settings-extension-main">
|
|
<strong title={plugin.name}>{plugin.name}</strong>
|
|
<span>内置 Plugin · 不可卸载</span>
|
|
{plugin.lastError ? (
|
|
<small title={plugin.lastError}>
|
|
{plugin.lastError}
|
|
</small>
|
|
) : null}
|
|
</div>
|
|
<div className="runtime-settings-extension-actions">
|
|
<span>
|
|
{!plugin.enabled
|
|
? '已禁用'
|
|
: plugin.status === 'running'
|
|
? '运行中'
|
|
: plugin.status === 'invalid'
|
|
? '当前不可用'
|
|
: '已启用'}
|
|
</span>
|
|
{plugin.enabled && plugin.hasRuntime ? (
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
agcPluginsBusy ||
|
|
plugin.status !== 'running'
|
|
}
|
|
onClick={() => void reloadPlugin(plugin)}
|
|
>
|
|
重载
|
|
</button>
|
|
) : null}
|
|
{plugin.status === 'running'
|
|
? plugin.panels.map((panel) => (
|
|
<button
|
|
type="button"
|
|
key={panel.id}
|
|
onClick={() =>
|
|
setMountedPluginPanel({
|
|
pluginId: plugin.id,
|
|
panel,
|
|
})
|
|
}
|
|
>
|
|
{panel.title}
|
|
</button>
|
|
))
|
|
: null}
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={plugin.enabled}
|
|
aria-label={`${plugin.name} ${
|
|
plugin.enabled ? '禁用' : '启用'
|
|
}`}
|
|
disabled={agcPluginsBusy}
|
|
onClick={() =>
|
|
void setBuiltinPluginEnabled(
|
|
plugin,
|
|
!plugin.enabled,
|
|
)
|
|
}
|
|
>
|
|
{plugin.enabled ? '禁用' : '启用'}
|
|
</button>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{clientExtensionsLoadState === 'loading' ? (
|
|
<div className="runtime-settings-empty-state" role="status">
|
|
<strong>正在加载扩展</strong>
|
|
<span>正在读取已导入的 Skill 和 MCP。</span>
|
|
</div>
|
|
) : clientExtensionsLoadState === 'error' ? (
|
|
<div className="runtime-settings-empty-state" role="alert">
|
|
<strong>扩展列表加载失败</strong>
|
|
<span>
|
|
{clientExtensionsStatus || '暂时无法读取扩展列表。'}
|
|
</span>
|
|
</div>
|
|
) : clientExtensions.length > 0 ? (
|
|
<div className="runtime-settings-extension-list">
|
|
{clientExtensions
|
|
.filter(
|
|
(item) =>
|
|
!(
|
|
item.extensionType === 'plugin' &&
|
|
agcPlugins.some(
|
|
(plugin) =>
|
|
plugin.builtin && plugin.id === item.id,
|
|
)
|
|
),
|
|
)
|
|
.map((item) => {
|
|
const editing = editingExtensionId === item.id;
|
|
const plugin = agcPlugins.find(
|
|
(plugin) => plugin.id === item.id,
|
|
);
|
|
const typeLabel =
|
|
item.extensionType === 'plugin'
|
|
? 'Plugin'
|
|
: item.extensionType === 'skill'
|
|
? 'Skill'
|
|
: item.extensionType === 'mcp'
|
|
? 'MCP'
|
|
: '未识别';
|
|
const statusLabel =
|
|
item.status === 'enabled'
|
|
? '已启用'
|
|
: item.status === 'disabled'
|
|
? '已禁用'
|
|
: item.status === 'startup-failed'
|
|
? '启动失败'
|
|
: '当前不可用';
|
|
return (
|
|
<article
|
|
className="runtime-settings-extension-item"
|
|
key={item.id}
|
|
>
|
|
<div className="runtime-settings-extension-main">
|
|
{editing ? (
|
|
<input
|
|
aria-label={`${item.name} 新名称`}
|
|
autoFocus
|
|
value={editingExtensionName}
|
|
onChange={(event) =>
|
|
setEditingExtensionName(
|
|
event.currentTarget.value,
|
|
)
|
|
}
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
void saveClientExtensionName(item);
|
|
} else if (event.key === 'Escape') {
|
|
event.stopPropagation();
|
|
cancelRenameClientExtension();
|
|
}
|
|
}}
|
|
/>
|
|
) : (
|
|
<strong title={item.name}>{item.name}</strong>
|
|
)}
|
|
<span>
|
|
{typeLabel} · 来自 {item.sourceName}
|
|
</span>
|
|
{item.lastError ? (
|
|
<small title={item.lastError}>
|
|
{item.lastError}
|
|
</small>
|
|
) : null}
|
|
</div>
|
|
<div className="runtime-settings-extension-actions">
|
|
<span>
|
|
{plugin?.status === 'running'
|
|
? '运行中'
|
|
: statusLabel}
|
|
</span>
|
|
{plugin?.enabled && plugin.hasRuntime ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
agcPluginsBusy ||
|
|
clientExtensionsBusy ||
|
|
plugin.status === 'invalid'
|
|
}
|
|
onClick={() =>
|
|
void toggleAgcPlugin(plugin)
|
|
}
|
|
>
|
|
{plugin.status === 'running'
|
|
? '停止'
|
|
: '启动'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={
|
|
agcPluginsBusy ||
|
|
plugin.status !== 'running'
|
|
}
|
|
onClick={() => void reloadPlugin(plugin)}
|
|
>
|
|
重载
|
|
</button>
|
|
</>
|
|
) : null}
|
|
{plugin?.status === 'running'
|
|
? plugin.panels.map((panel) => (
|
|
<button
|
|
type="button"
|
|
key={panel.id}
|
|
onClick={() =>
|
|
setMountedPluginPanel({
|
|
pluginId: plugin.id,
|
|
panel,
|
|
})
|
|
}
|
|
>
|
|
{panel.title}
|
|
</button>
|
|
))
|
|
: null}
|
|
{editing ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() =>
|
|
void saveClientExtensionName(item)
|
|
}
|
|
>
|
|
保存
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={clientExtensionsBusy}
|
|
onClick={cancelRenameClientExtension}
|
|
>
|
|
取消
|
|
</button>
|
|
</>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
aria-label={`重命名 ${item.name}`}
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() =>
|
|
beginRenameClientExtension(item)
|
|
}
|
|
>
|
|
<Pencil size={14} aria-hidden="true" />
|
|
</button>
|
|
)}
|
|
{item.extensionType === 'unknown' ? null : (
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={item.enabled}
|
|
aria-label={`${item.name} ${item.enabled ? '禁用' : '启用'}`}
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() =>
|
|
void setClientExtensionEnabled(
|
|
item,
|
|
!item.enabled,
|
|
)
|
|
}
|
|
>
|
|
{item.enabled ? '禁用' : '启用'}
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
aria-label={`删除 ${item.name}`}
|
|
disabled={clientExtensionsBusy}
|
|
onClick={() =>
|
|
void removeClientExtension(item)
|
|
}
|
|
>
|
|
<Trash2 size={14} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="runtime-settings-empty-state">
|
|
<strong>还没有导入扩展</strong>
|
|
<span>
|
|
导入 Skill、MCP 配置或标准 Plugin 后会显示在这里。
|
|
</span>
|
|
</div>
|
|
)}
|
|
{agcPluginsStatus ? (
|
|
<p className="runtime-settings-inline-status" role="status">
|
|
{agcPluginsStatus}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
) : null}
|
|
{activeSection === 'about' ? (
|
|
<section
|
|
className="runtime-settings-about"
|
|
aria-label="关于客户端"
|
|
>
|
|
<div className="runtime-settings-about-hero">
|
|
<div
|
|
className="runtime-settings-about-mark"
|
|
aria-hidden="true"
|
|
>
|
|
<img
|
|
src={BRAND_ICON}
|
|
alt=""
|
|
data-testid="runtime-settings-app-logo"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<span className="runtime-settings-about-kicker">
|
|
GENARRATIVE
|
|
</span>
|
|
<h4>{APP_NAME}</h4>
|
|
<p>面向游戏创作者的智能创作客户端</p>
|
|
</div>
|
|
</div>
|
|
<dl className="runtime-settings-about-meta">
|
|
<div>
|
|
<dt>客户端版本</dt>
|
|
<dd data-testid="runtime-settings-app-version">
|
|
v{APP_VERSION}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt>运行环境</dt>
|
|
<dd>桌面客户端</dd>
|
|
</div>
|
|
</dl>
|
|
{appUpdateCheckEnabled ? (
|
|
<div className="runtime-settings-about-update">
|
|
<button
|
|
type="button"
|
|
onClick={() => void checkAppUpdateManually()}
|
|
disabled={appUpdateChecking}
|
|
>
|
|
{appUpdateChecking ? '正在检查…' : '检查更新'}
|
|
</button>
|
|
<span role="status" aria-live="polite">
|
|
{appUpdateStatus}
|
|
</span>
|
|
</div>
|
|
) : null}
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<footer className="runtime-settings-footer">
|
|
<p
|
|
className="status-line"
|
|
data-tone={runtimeConfigStatusTone}
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
{runtimeConfigStatusTone === 'busy' ? (
|
|
<LoaderCircle
|
|
className="is-spinning"
|
|
size={15}
|
|
aria-hidden="true"
|
|
/>
|
|
) : runtimeConfigStatusTone === 'success' ? (
|
|
<CheckCircle2 size={15} aria-hidden="true" />
|
|
) : runtimeConfigStatusTone === 'warning' ? (
|
|
<CircleAlert size={15} aria-hidden="true" />
|
|
) : null}
|
|
{runtimeConfigStatus}
|
|
</p>
|
|
<div className="runtime-settings-footer-actions">
|
|
<button
|
|
type="button"
|
|
aria-label="读取"
|
|
disabled={runtimeConfigBusy}
|
|
onClick={readRuntimeConfig}
|
|
>
|
|
重新读取
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={runtimeConfigBusy}
|
|
onClick={resetRuntimeConfigDraft}
|
|
>
|
|
<RotateCcw size={15} aria-hidden="true" />
|
|
恢复默认
|
|
</button>
|
|
<button
|
|
className="runtime-settings-save"
|
|
type="submit"
|
|
aria-label="保存"
|
|
disabled={runtimeConfigBusy}
|
|
>
|
|
<Save size={15} aria-hidden="true" />
|
|
{runtimeConfigStatus === '正在保存'
|
|
? '正在保存'
|
|
: runtimeConfigBusy
|
|
? '请稍候'
|
|
: '保存设置'}
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
</form>
|
|
{mountedPluginPanel ? (
|
|
<PluginPanelHost
|
|
{...mountedPluginPanel}
|
|
onClose={() => setMountedPluginPanel(null)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|