Merge remote-tracking branch 'origin/master' into style/polish-taonier-publish
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

This commit is contained in:
2026-10-08 17:06:27 +08:00
6 changed files with 152 additions and 7 deletions
@@ -71,6 +71,10 @@ const eventCapabilityPath = new URL(
'../src-tauri/capabilities/events.json',
import.meta.url,
);
const windowChromeCapabilityPath = new URL(
'../src-tauri/capabilities/window-chrome.json',
import.meta.url,
);
const defaultAppConfig = JSON.parse(
fs.readFileSync(
new URL('../game-creator.config.json', import.meta.url),
@@ -1489,6 +1493,41 @@ if (
);
}
// 自绘标题栏的窗口手势由两条不同命令承担:三个窗口按钮走 JS API 的
// toggle_maximize / minimize / close,拖拽区双击由 Tauri 注入的 drag.js 派发
// internal_toggle_maximize。少授其中任何一条,用户手势都会在 ACL 处被拒,
// 且拒绝通过全局 unhandledrejection 冒到错误池,而不是表现为按钮无反应。
if (!fs.existsSync(windowChromeCapabilityPath)) {
throw new Error(
'AI game creator shell must declare a Tauri window-chrome capability',
);
}
const windowChromeCapability = JSON.parse(
fs.readFileSync(windowChromeCapabilityPath, 'utf8'),
);
const windowChromeWindows = new Set(windowChromeCapability.windows ?? []);
const windowChromePermissions = new Set(
windowChromeCapability.permissions ?? [],
);
for (const windowLabel of ['client', 'main', 'launcher']) {
if (!windowChromeWindows.has(windowLabel)) {
throw new Error(
`AI game creator shell window-chrome capability missing window: ${windowLabel}`,
);
}
}
for (const permission of [
'core:window:allow-start-dragging',
'core:window:allow-toggle-maximize',
'core:window:allow-internal-toggle-maximize',
]) {
if (!windowChromePermissions.has(permission)) {
throw new Error(
`AI game creator shell window-chrome capability missing permission: ${permission}`,
);
}
}
if (
!Array.isArray(tauriConfig.app?.windows) ||
tauriConfig.app.windows.length !== 1 ||
@@ -5,6 +5,7 @@
"windows": ["client", "main", "launcher"],
"permissions": [
"core:window:allow-close",
"core:window:allow-internal-toggle-maximize",
"core:window:allow-is-maximized",
"core:window:allow-minimize",
"core:window:allow-start-dragging",
@@ -318,6 +318,7 @@ export interface GameCreatorAppConfig {
};
selectedModelId?: string;
selectedModelIsDefault?: boolean;
selectedModelProtocol?: GameCreatorLlmApiKind;
planning?: {
capabilityEnabled: boolean;
};
@@ -262,19 +262,30 @@ export function ConversationModelSelect({
);
}
}
if (next && (next !== saved || nextIsDefault !== followsDefault)) {
const nextModel = catalog.models.find((model) => model.id === next);
const nextProtocol = nextModel
? resolveModelProtocol(nextModel)
: undefined;
// 后台可能只改同一目录项的 agentMode/protocol,而不改 id。旧配置若仍保留
// Responses,会把 Claude/Anthropic 模型发到 /v1/responses 并在上游才失败。
// 缺省协议按 Responses 兼容旧配置;非 Responses 或已有显式协议发生漂移时必须写回。
const protocolChanged =
Boolean(nextModel) &&
config.config.selectedModelProtocol !== nextProtocol &&
(config.config.selectedModelProtocol != null ||
nextProtocol !== 'openai_responses');
if (
next &&
(next !== saved || nextIsDefault !== followsDefault || protocolChanged)
) {
let persisted: GameCreatorAppConfigView;
try {
persisted = await queueConfigWrite(() =>
invoke<GameCreatorAppConfigView>('select_game_creator_model', {
modelId: next,
isDefault: nextIsDefault,
agentMode:
catalog.models.find((model) => model.id === next)?.agentMode ??
'codex',
protocol: resolveModelProtocol(
catalog.models.find((model) => model.id === next),
),
agentMode: nextModel?.agentMode ?? 'codex',
protocol: nextProtocol,
}),
);
} catch (error) {
@@ -36,6 +36,8 @@ const nativeInvoke = vi.fn((command: string, input: unknown) =>
);
let savedModelId = 'quality';
let savedModelIsDefault = true;
let savedModelProtocol: string | undefined = 'openai_responses';
let savedAgentMode = 'codex_app_server';
beforeEach(() => {
vi.clearAllMocks();
@@ -51,15 +53,22 @@ beforeEach(() => {
});
savedModelId = 'quality';
savedModelIsDefault = true;
savedModelProtocol = 'openai_responses';
savedAgentMode = 'codex_app_server';
invoke.mockImplementation(async (command, input) => {
if (command === 'select_game_creator_model') {
savedModelId = String(input.modelId);
savedModelIsDefault = Boolean(input.isDefault);
savedModelProtocol = input.protocol;
savedAgentMode =
input.agentMode === 'cc' ? 'claude_code_cli' : 'codex_app_server';
}
return {
config: {
agentMode: savedAgentMode,
selectedModelId: savedModelId,
selectedModelIsDefault: savedModelIsDefault,
selectedModelProtocol: savedModelProtocol,
},
};
});
@@ -622,6 +631,70 @@ test('keeps an explicit selection when the server default changes', async () =>
expect(savedModelIsDefault).toBe(false);
});
test('rewrites the saved protocol when the selected model routing changes', async () => {
let selectedModelId = 'claude-opus-5-5';
let selectedModelIsDefault = false;
let selectedModelProtocol = 'openai_responses';
let agentMode = 'codex_app_server';
vi.mocked(loadClientLlmModels).mockResolvedValue({
defaultModelId: 'claude-opus-5-5',
models: [
{
id: 'claude-opus-5-5',
displayName: 'claude-opus-5-5',
agentMode: 'codex',
protocol: 'openai_responses',
},
],
unavailableModels: [],
revision: 7,
});
invoke.mockImplementation(async (command, input) => {
if (command === 'select_game_creator_model') {
selectedModelId = input.modelId;
selectedModelIsDefault = Boolean(input.isDefault);
selectedModelProtocol = input.protocol;
agentMode =
input.agentMode === 'cc' ? 'claude_code_cli' : 'codex_app_server';
}
return {
config: {
agentMode,
selectedModelId,
selectedModelIsDefault,
selectedModelProtocol,
},
};
});
render(<ConversationModelSelect disabled={false} />);
await screen.findByRole('button', { name: '对话模型' });
vi.mocked(loadClientLlmModels).mockResolvedValue({
defaultModelId: 'claude-opus-5-5',
models: [
{
id: 'claude-opus-5-5',
displayName: 'claude-opus-5-5',
agentMode: 'cc',
protocol: 'anthropic',
},
],
unavailableModels: [],
revision: 8,
});
fireEvent(window, new Event('focus'));
await waitFor(() => expect(selectedModelProtocol).toBe('anthropic'));
expect(agentMode).toBe('claude_code_cli');
expect(invoke).toHaveBeenCalledWith('select_game_creator_model', {
modelId: 'claude-opus-5-5',
isDefault: false,
agentMode: 'cc',
protocol: 'anthropic',
});
});
test('recovers the selector when reading the native config fails', async () => {
invoke.mockRejectedValueOnce(new Error('config unreadable'));
const onReady = vi.fn();
@@ -649,14 +722,19 @@ test('pre-send validation waits for an in-flight selection save', async () => {
resolveSave = () => {
savedModelId = String(input.modelId);
savedModelIsDefault = Boolean(input.isDefault);
savedModelProtocol = input.protocol;
savedAgentMode =
input.agentMode === 'cc' ? 'claude_code_cli' : 'codex_app_server';
resolve();
};
});
}
return {
config: {
agentMode: savedAgentMode,
selectedModelId: savedModelId,
selectedModelIsDefault: savedModelIsDefault,
selectedModelProtocol: savedModelProtocol,
},
};
});
@@ -1176,6 +1176,14 @@ AGC macOS 发布入口一度传入 `--no-sign`(目的是绕过没有 Apple 证
- **当前网络边界**:登录与素材直传均已由 Rust facade 承担,渲染层不再授予 `http:default`;旧“登录 HTTP ACL 拒绝”症状不作为现役诊断依据。窗口 label 漂移仍会影响原生对话框、剪贴板、opener/updater 等权限。
- **关联**:`apps/ai-game-creator-shell/scripts/build-release.mjs`、`build-release.test.mjs`、`check-config.mjs`、`src-tauri/tauri.conf.json`、`src-tauri/capabilities/main.json`。
### 自绘标题栏双击最大化走的是另一条命令,只授 `toggle-maximize` 会以未捕获拒绝冒到错误池
- **现象**:双击 AGC 自绘标题栏的拖拽区,窗口不最大化,错误池多一条 `unhandledrejection`:`window.internal_toggle_maximize not allowed. Permissions associated with this command: core:window:allow-internal-toggle-maximize, core:window:default`;同一根标题栏上的最小化 / 最大化按钮却都正常。
- **原因**:同一个用户动作对应两个不同命令。按钮走 JS API `getCurrentWindow().toggleMaximize()` → `plugin:window|toggle_maximize`(权限 `core:window:allow-toggle-maximize`);拖拽区双击由 Tauri 注入的 `drag.js` 派发 → `plugin:window|internal_toggle_maximize`(权限 `core:window:allow-internal-toggle-maximize`)。capability 只授了前者,ACL 拒绝后 `drag.js` 里那个没人接管的 `invoke` 直接变成全局 `unhandledrejection`,于是症状看起来像「窗口按钮正常、双击没反应还多一条错误上报」。
- **处理(现行口径)**:带 `data-tauri-drag-region` 且要保留「双击最大化」的窗口,capability 必须显式列出 `core:window:allow-internal-toggle-maximize`。`core:window:default` 虽然包含这条,但 capability 不引用 default 集合时不会生效;新增窗口或新 capability 时按同一口径核对全部窗口手势。
- **验证判据**:改 capability 后必须重新编译并重启客户端(编译期生成 ACL,运行中的窗口不会继承),再双击标题栏:窗口应最大化,且错误池不再新增该条。
- **关联**:`apps/ai-game-creator-shell/src-tauri/capabilities/window-chrome.json`、`apps/ai-game-creator-shell/src/components/WindowChrome.tsx`。
### 按目录扫描更新产物时须核对包内版本与渠道身份
- **风险**:复用构建工作区里残留的其它版本或渠道更新包,会被扫描式清单生成器选中;对象存在且签名有效仍可能发错包。
@@ -3529,6 +3537,13 @@ Phaser `Scale.FIT` 与 `autoCenter: CENTER_BOTH` 会给 canvas 计算定位外
当前 Router 用户保持 `LLM_ROUTER_USER_GROUP = "taonier"`,Key 创建与修复使用 `LLM_ROUTER_TOKEN_GROUP = "auto"`。排查时分别核对用户、Key 和渠道,不因用户组正确就排除路由问题。模型目录启动同步使用用户分组 `LLM_ROUTER_USER_GROUP`(`taonier`),不能复用 Token 的 `auto` 路由分组,否则调整 Key 路由会意外改变在售目录;目录请求测试须固定断言 `group=taonier`。合法的存量目录不会自动重建,不能把 Key 修复成功当作目录已更新。入口为 `external_api_keys.rs` 与 `agc_models.rs`。
### 模型目录协议变更必须同步回写客户端选中项
- **现象**:后台把同一个模型目录项从 Codex/Responses 改成 Claude Code/Anthropic 后,客户端仍把该模型发到 `/v1/responses`,Router 最终以 `500 not implemented` 失败;模型列表本身已经显示新协议。
- **根因**:模型选择器原先只在 `selectedModelId` 或默认项变化时写回配置,没有比较同一 ID 的 `protocol`。已选模型保留旧的 `selectedModelProtocol` 和 `agentMode`,直到用户重新点选。
- **现行口径**:目录同步发现协议漂移时,按目录中的 `agentMode` / `protocol` 重写选中项;旧配置缺省协议仍按 `openai_responses` 兼容,非 Responses 或已有显式协议不一致才触发写入。回归测试覆盖同一模型 ID 从 Codex/Responses 切到 Claude Code/Anthropic。
- **关联**:`ConversationModelSelect.tsx`、`apps/ai-game-creator-shell/tests/conversationModelSelect.test.tsx`。
## 跨领域与待归类
### 资源预览的 scope、容量与队列边界不能靠浅层文本测试验证