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
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:
@@ -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;
|
||||
};
|
||||
|
||||
+18
-7
@@ -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、容量与队列边界不能靠浅层文本测试验证
|
||||
|
||||
Reference in New Issue
Block a user