coding-tool-x 3.4.9 → 3.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/web/assets/{Analytics-0PgPv5qO.js → Analytics-BT-pLYj8.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-pBGoYbCP.js → ConfigTemplates-BGH9N-xf.js} +1 -1
  3. package/dist/web/assets/{Home-BRN882om.js → Home-C_YwC-4M.js} +1 -1
  4. package/dist/web/assets/{PluginManager-am97Huts.js → PluginManager-BTb28q0R.js} +1 -1
  5. package/dist/web/assets/{ProjectList-CXS9KJN1.js → ProjectList-BYm3jQ3S.js} +1 -1
  6. package/dist/web/assets/{SessionList-BZyrzH7J.js → SessionList-C6EIsN9j.js} +1 -1
  7. package/dist/web/assets/{SkillManager-p1CI0tYa.js → SkillManager-B2VKu6_J.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-CUPvLoba.js → WorkspaceManager-LHBQcZIV.js} +1 -1
  9. package/dist/web/assets/index-D547X48u.js +2 -0
  10. package/dist/web/assets/index-NC-fbfg8.css +1 -0
  11. package/dist/web/index.html +2 -2
  12. package/package.json +1 -1
  13. package/src/commands/toggle-proxy.js +1 -1
  14. package/src/server/api/claude-hooks.js +36 -2
  15. package/src/server/api/codex-proxy.js +1 -1
  16. package/src/server/api/opencode-proxy.js +92 -7
  17. package/src/server/api/proxy.js +2 -5
  18. package/src/server/services/channels.js +12 -7
  19. package/src/server/services/codex-channels.js +1 -1
  20. package/src/server/services/codex-env-manager.js +25 -14
  21. package/src/server/services/gemini-channels.js +52 -31
  22. package/src/server/services/native-oauth-adapters.js +152 -5
  23. package/src/server/services/notification-hooks.js +70 -2
  24. package/src/server/services/opencode-channels.js +11 -2
  25. package/dist/web/assets/index-B4Wl3JfR.js +0 -2
  26. package/dist/web/assets/index-Bgt_oqoE.css +0 -1
@@ -44,18 +44,22 @@ function extractApiKeyFromHelper(apiKeyHelper) {
44
44
  return '';
45
45
  }
46
46
  const helper = apiKeyHelper.trim();
47
- let match = helper.match(/^echo\s+["']([^"']+)["']$/);
47
+ let match = helper.match(/^echo\s+["']([^"']+)["']$/i);
48
48
  if (match && match[1]) return match[1];
49
- match = helper.match(/^printf\s+["'][^"']*["']\s+["']([^"']+)["']$/);
49
+ match = helper.match(/^echo\s+([^\s].*)$/i);
50
+ if (match && match[1]) return match[1].trim();
51
+ match = helper.match(/^cmd(?:\.exe)?\s*\/c\s+echo\s+([^\s].*)$/i);
52
+ if (match && match[1]) return match[1].trim();
53
+ match = helper.match(/^printf\s+["'][^"']*["']\s+["']([^"']+)["']$/i);
50
54
  if (match && match[1]) return match[1];
51
55
  return '';
52
56
  }
53
57
 
54
- function buildApiKeyHelperCommand() {
58
+ function buildApiKeyHelperCommand(value) {
55
59
  if (isWindowsLikePlatform(process.platform, process.env)) {
56
- return 'cmd /c echo ctx-managed';
60
+ return `cmd /c echo ${value}`;
57
61
  }
58
- return "echo 'ctx-managed'";
62
+ return `echo '${value}'`;
59
63
  }
60
64
 
61
65
  // ── Claude 原生设置写入 ──
@@ -114,7 +118,7 @@ function updateClaudeSettingsWithModelConfig(channel) {
114
118
  delete settings.env;
115
119
  }
116
120
 
117
- settings.apiKeyHelper = buildApiKeyHelperCommand();
121
+ settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
118
122
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
119
123
  }
120
124
 
@@ -147,7 +151,7 @@ function updateClaudeSettings(baseUrl, apiKey) {
147
151
  delete settings.env;
148
152
  }
149
153
 
150
- settings.apiKeyHelper = buildApiKeyHelperCommand();
154
+ settings.apiKeyHelper = buildApiKeyHelperCommand(apiKey);
151
155
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
152
156
  }
153
157
 
@@ -326,4 +330,5 @@ module.exports = {
326
330
  updateClaudeSettingsWithModelConfig,
327
331
  getEffectiveApiKey,
328
332
  disableAllChannels,
333
+ extractApiKeyFromHelper,
329
334
  };
@@ -73,7 +73,7 @@ function writeAnnotatedCodexConfig(configPath, config, comments = []) {
73
73
  function pruneManagedProviders(existingProviders, currentProviderKey, allChannels) {
74
74
  const knownKeys = new Set(allChannels.map(ch => ch.providerKey).filter(Boolean));
75
75
  for (const key of Object.keys(existingProviders)) {
76
- if (key !== currentProviderKey && !knownKeys.has(key)) {
76
+ if (key === 'cc-proxy' || (key !== currentProviderKey && knownKeys.has(key))) {
77
77
  delete existingProviders[key];
78
78
  }
79
79
  }
@@ -5,6 +5,9 @@ const { PATHS, HOME_DIR } = require('../../config/paths');
5
5
 
6
6
  const PROFILE_MARKER_START = '# >>> coding-tool codex env >>>';
7
7
  const PROFILE_MARKER_END = '# <<< coding-tool codex env <<<';
8
+ const WINDOWS_ENV_COMMAND_TIMEOUT_MS = 15000;
9
+ const WINDOWS_SETTING_CHANGE_TIMEOUT_MS = 1000;
10
+ const WINDOWS_SETTING_CHANGE_COMMAND_TIMEOUT_MS = 7000;
8
11
 
9
12
  function defaultEnvFilePath(configDir) {
10
13
  return path.join(configDir, 'codex-env.sh');
@@ -32,7 +35,7 @@ function powershellQuote(value) {
32
35
  return `'${String(value).replace(/'/g, "''")}'`;
33
36
  }
34
37
 
35
- function buildWindowsSettingChangeScript() {
38
+ function buildWindowsSettingChangeScript(timeoutMs = WINDOWS_SETTING_CHANGE_TIMEOUT_MS) {
36
39
  return [
37
40
  'Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @"',
38
41
  '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]',
@@ -45,11 +48,11 @@ function buildWindowsSettingChangeScript() {
45
48
  '$SMTO_ABORTIFHUNG = 0x0002',
46
49
  '$result = [UIntPtr]::Zero',
47
50
  '[Win32.NativeMethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE,',
48
- ' [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, 5000, [ref]$result) | Out-Null'
51
+ ` [UIntPtr]::Zero, "Environment", $SMTO_ABORTIFHUNG, ${timeoutMs}, [ref]$result) | Out-Null`
49
52
  ].join('\n');
50
53
  }
51
54
 
52
- function buildWindowsEnvBatchScript(operations = [], { includeSettingChangeBroadcast = false } = {}) {
55
+ function buildWindowsEnvBatchScript(operations = [], { includeSettingChangeBroadcast = true } = {}) {
53
56
  const normalizedOperations = Array.isArray(operations) ? operations.filter(Boolean) : [];
54
57
  const lines = normalizedOperations.map((operation) => {
55
58
  const key = powershellQuote(operation.key || '');
@@ -375,7 +378,12 @@ function runLaunchctlCommand(args, execSync) {
375
378
  }
376
379
 
377
380
  function broadcastWindowsSettingChange(execSync) {
378
- runWindowsEnvCommand(buildWindowsSettingChangeScript(), execSync);
381
+ // WM_SETTINGCHANGE 只是帮助已运行的 GUI/终端尽快感知环境变化。
382
+ // 用户级环境变量已经写入注册表,即使这里超时,新开进程仍然能读到。
383
+ runWindowsEnvCommand(buildWindowsSettingChangeScript(), execSync, {
384
+ timeout: WINDOWS_SETTING_CHANGE_COMMAND_TIMEOUT_MS,
385
+ ignoreErrors: true
386
+ });
379
387
  }
380
388
 
381
389
  function syncWindowsEnvironment(nextValues, previousState, options) {
@@ -398,14 +406,10 @@ function syncWindowsEnvironment(nextValues, previousState, options) {
398
406
  if (changed) {
399
407
  runWindowsEnvCommand(
400
408
  buildWindowsEnvBatchScript(operations, { includeSettingChangeBroadcast: false }),
401
- execSync
409
+ execSync,
410
+ { timeout: WINDOWS_ENV_COMMAND_TIMEOUT_MS }
402
411
  );
403
- try {
404
- // WM_SETTINGCHANGE 只是帮助已打开的应用刷新环境变量,失败不应让主流程报错。
405
- broadcastWindowsSettingChange(execSync);
406
- } catch {
407
- // ignore broadcast failures; registry writes are already durable
408
- }
412
+ broadcastWindowsSettingChange(execSync);
409
413
  }
410
414
 
411
415
  if (nextKeys.length > 0) {
@@ -430,21 +434,28 @@ function syncWindowsEnvironment(nextValues, previousState, options) {
430
434
  };
431
435
  }
432
436
 
433
- function runWindowsEnvCommand(script, execSync) {
437
+ function runWindowsEnvCommand(script, execSync, options = {}) {
438
+ const timeout = Number(options.timeout) > 0
439
+ ? Number(options.timeout)
440
+ : WINDOWS_ENV_COMMAND_TIMEOUT_MS;
441
+ const ignoreErrors = options.ignoreErrors === true;
434
442
  const candidates = ['powershell', 'pwsh'];
435
443
  let lastError = null;
436
444
  for (const command of candidates) {
437
445
  try {
438
446
  execSync(command, ['-NoProfile', '-NonInteractive', '-Command', script], {
439
447
  stdio: ['ignore', 'ignore', 'ignore'],
440
- timeout: 5000,
448
+ timeout,
441
449
  windowsHide: true
442
450
  });
443
- return;
451
+ return true;
444
452
  } catch (error) {
445
453
  lastError = error;
446
454
  }
447
455
  }
456
+ if (ignoreErrors) {
457
+ return false;
458
+ }
448
459
  throw lastError || new Error('No PowerShell executable available');
449
460
  }
450
461
 
@@ -34,6 +34,43 @@ function getChannelsFilePath() {
34
34
  return PATHS.channels.gemini;
35
35
  }
36
36
 
37
+ function readExistingGeminiEnv() {
38
+ const envPath = path.join(getGeminiDir(), '.env');
39
+ if (!fs.existsSync(envPath)) {
40
+ return {};
41
+ }
42
+
43
+ const env = {};
44
+ try {
45
+ const content = fs.readFileSync(envPath, 'utf8');
46
+ content.split('\n').forEach((line) => {
47
+ const trimmed = line.trim();
48
+ if (!trimmed || trimmed.startsWith('#')) return;
49
+
50
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
51
+ if (match) {
52
+ env[match[1].trim()] = match[2].trim();
53
+ }
54
+ });
55
+ } catch (err) {
56
+ return {};
57
+ }
58
+
59
+ return env;
60
+ }
61
+
62
+ function writeGeminiEnv(env = {}) {
63
+ const envPath = path.join(getGeminiDir(), '.env');
64
+ const content = Object.entries(env)
65
+ .map(([key, value]) => `${key}=${value}`)
66
+ .join('\n');
67
+
68
+ fs.writeFileSync(envPath, content ? `${content}\n` : '', 'utf8');
69
+ if (process.platform !== 'win32') {
70
+ fs.chmodSync(envPath, 0o600);
71
+ }
72
+ }
73
+
37
74
  // 检查是否在代理模式
38
75
  function isProxyConfig() {
39
76
  const envPath = path.join(getGeminiDir(), '.env');
@@ -295,21 +332,12 @@ function applyChannelToSettings(channelId, channels = null) {
295
332
  fs.mkdirSync(geminiDir, { recursive: true });
296
333
  }
297
334
 
298
- const envPath = path.join(geminiDir, '.env');
299
-
300
- // 构建 .env 内容
335
+ const env = readExistingGeminiEnv();
301
336
  const effectiveApiKey = getEffectiveApiKey(channel) || '';
302
- const envContent = `GOOGLE_GEMINI_BASE_URL=${channel.baseUrl}
303
- GEMINI_API_KEY=${effectiveApiKey}
304
- GEMINI_MODEL=${channel.model}
305
- `;
306
-
307
- fs.writeFileSync(envPath, envContent, 'utf8');
308
-
309
- // 设置 .env 文件权限为 600 (仅所有者可读写)
310
- if (process.platform !== 'win32') {
311
- fs.chmodSync(envPath, 0o600);
312
- }
337
+ env.GOOGLE_GEMINI_BASE_URL = channel.baseUrl;
338
+ env.GEMINI_API_KEY = effectiveApiKey;
339
+ env.GEMINI_MODEL = channel.model;
340
+ writeGeminiEnv(env);
313
341
 
314
342
  // 确保 settings.json 存在并配置正确的认证模式
315
343
  const settingsPath = path.join(geminiDir, 'settings.json');
@@ -364,32 +392,25 @@ function writeGeminiConfigForMultiChannel(allChannels) {
364
392
  fs.mkdirSync(geminiDir, { recursive: true });
365
393
  }
366
394
 
367
- const envPath = path.join(geminiDir, '.env');
368
-
369
395
  // 获取第一个启用的渠道作为默认配置
370
396
  const enabledChannels = allChannels.filter(c => c.enabled !== false);
371
397
  const defaultChannel = enabledChannels[0] || allChannels[0];
372
398
 
399
+ const env = readExistingGeminiEnv();
400
+
373
401
  if (!defaultChannel) {
374
- // 没有渠道,写入空配置
375
- const envContent = `# Gemini Configuration\n# No channels configured\n`;
376
- fs.writeFileSync(envPath, envContent, 'utf8');
402
+ delete env.GOOGLE_GEMINI_BASE_URL;
403
+ delete env.GEMINI_API_KEY;
404
+ delete env.GEMINI_MODEL;
405
+ writeGeminiEnv(env);
377
406
  return;
378
407
  }
379
408
 
380
- // 构建 .env 内容
381
409
  const effectiveApiKey = getEffectiveApiKey(defaultChannel) || '';
382
- const envContent = `GOOGLE_GEMINI_BASE_URL=${defaultChannel.baseUrl}
383
- GEMINI_API_KEY=${effectiveApiKey}
384
- GEMINI_MODEL=${defaultChannel.model}
385
- `;
386
-
387
- fs.writeFileSync(envPath, envContent, 'utf8');
388
-
389
- // 设置 .env 文件权限为 600 (仅所有者可读写)
390
- if (process.platform !== 'win32') {
391
- fs.chmodSync(envPath, 0o600);
392
- }
410
+ env.GOOGLE_GEMINI_BASE_URL = defaultChannel.baseUrl;
411
+ env.GEMINI_API_KEY = effectiveApiKey;
412
+ env.GEMINI_MODEL = defaultChannel.model;
413
+ writeGeminiEnv(env);
393
414
 
394
415
  // 确保 settings.json 存在并配置正确的认证模式
395
416
  const settingsPath = path.join(geminiDir, 'settings.json');
@@ -723,18 +723,21 @@ function clearOpenCodeOAuth() {
723
723
  return;
724
724
  }
725
725
 
726
+ const removedProviderIds = [];
726
727
  Object.keys(payload).forEach((providerId) => {
727
728
  if (payload[providerId]?.type === 'oauth') {
729
+ removedProviderIds.push(providerId);
728
730
  delete payload[providerId];
729
731
  }
730
732
  });
731
733
 
732
734
  if (Object.keys(payload).length === 0) {
733
735
  removeFileIfExists(NATIVE_PATHS.opencode.auth);
734
- return;
736
+ } else {
737
+ writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
735
738
  }
736
739
 
737
- writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
740
+ syncOpenCodeConfigAfterOAuthRemoval(removedProviderIds);
738
741
  }
739
742
 
740
743
  function disableOpenCodeOAuthCredential(credential = {}) {
@@ -745,6 +748,7 @@ function disableOpenCodeOAuthCredential(credential = {}) {
745
748
  return;
746
749
  }
747
750
 
751
+ const removedProviderIds = [];
748
752
  Object.keys(payload).forEach((key) => {
749
753
  const target = payload[key];
750
754
  if (!target || target.type !== 'oauth') {
@@ -754,16 +758,18 @@ function disableOpenCodeOAuthCredential(credential = {}) {
754
758
  const providerMatched = providerId && key === providerId;
755
759
  const tokenMatched = accessToken && String(target.access || '').trim() === accessToken;
756
760
  if (providerMatched || tokenMatched) {
761
+ removedProviderIds.push(key);
757
762
  delete payload[key];
758
763
  }
759
764
  });
760
765
 
761
766
  if (Object.keys(payload).length === 0) {
762
767
  removeFileIfExists(NATIVE_PATHS.opencode.auth);
763
- return;
768
+ } else {
769
+ writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
764
770
  }
765
771
 
766
- writeJsonFile(NATIVE_PATHS.opencode.auth, payload);
772
+ syncOpenCodeConfigAfterOAuthRemoval(removedProviderIds);
767
773
  }
768
774
 
769
775
  function isManagedOpenCodeProvider(provider) {
@@ -780,6 +786,44 @@ function isManagedOpenCodeProvider(provider) {
780
786
  return apiKey === 'PROXY_KEY' && (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost'));
781
787
  }
782
788
 
789
+ function isProxyBackedOpenCodeProvider(provider) {
790
+ if (!provider || typeof provider !== 'object') {
791
+ return false;
792
+ }
793
+
794
+ const apiKey = String(provider?.options?.apiKey || '').trim();
795
+ const baseUrl = String(provider?.options?.baseURL || '').trim();
796
+ return apiKey === 'PROXY_KEY' && (baseUrl.includes('127.0.0.1') || baseUrl.includes('localhost'));
797
+ }
798
+
799
+ function isMeaningfulOpenCodeProvider(provider) {
800
+ if (!provider || typeof provider !== 'object') {
801
+ return false;
802
+ }
803
+
804
+ if (provider.__ctx_managed__ === true) {
805
+ return true;
806
+ }
807
+
808
+ if (typeof provider.npm === 'string' && provider.npm.trim()) {
809
+ return true;
810
+ }
811
+
812
+ if (typeof provider.name === 'string' && provider.name.trim()) {
813
+ return true;
814
+ }
815
+
816
+ if (provider.options && typeof provider.options === 'object' && Object.keys(provider.options).length > 0) {
817
+ return true;
818
+ }
819
+
820
+ if (provider.models && typeof provider.models === 'object' && Object.keys(provider.models).length > 0) {
821
+ return true;
822
+ }
823
+
824
+ return false;
825
+ }
826
+
783
827
  function clearOpenCodeManagedModelSelection(config) {
784
828
  const modelRef = String(config?.model || '').trim();
785
829
  if (!modelRef || !modelRef.includes('/')) {
@@ -797,6 +841,107 @@ function clearOpenCodeManagedModelSelection(config) {
797
841
  }
798
842
  }
799
843
 
844
+ function getConfiguredOpenCodeProviderId(config) {
845
+ const modelRef = String(config?.model || '').trim();
846
+ if (modelRef.includes('/')) {
847
+ return modelRef.split('/')[0].trim();
848
+ }
849
+
850
+ const providerIds = config?.provider && typeof config.provider === 'object'
851
+ ? Object.keys(config.provider).filter(Boolean)
852
+ : [];
853
+ return providerIds.length === 1 ? providerIds[0] : '';
854
+ }
855
+
856
+ function buildOpenCodeModelRef(providerId, provider) {
857
+ if (!providerId || !provider || typeof provider !== 'object') {
858
+ return '';
859
+ }
860
+
861
+ const modelIds = provider.models && typeof provider.models === 'object'
862
+ ? Object.keys(provider.models).filter(Boolean)
863
+ : [];
864
+
865
+ if (modelIds.length === 0) {
866
+ return '';
867
+ }
868
+
869
+ return `${providerId}/${modelIds[0]}`;
870
+ }
871
+
872
+ function pickFallbackOpenCodeModel(config, excludedProviderIds = new Set()) {
873
+ const providers = config?.provider && typeof config.provider === 'object'
874
+ ? Object.entries(config.provider)
875
+ : [];
876
+
877
+ for (const [providerId, provider] of providers) {
878
+ if (excludedProviderIds.has(providerId)) {
879
+ continue;
880
+ }
881
+
882
+ const modelRef = buildOpenCodeModelRef(providerId, provider);
883
+ if (modelRef) {
884
+ return modelRef;
885
+ }
886
+ }
887
+
888
+ return '';
889
+ }
890
+
891
+ function syncOpenCodeConfigAfterOAuthRemoval(removedProviderIds = []) {
892
+ const removedIds = new Set((removedProviderIds || []).filter(Boolean));
893
+ if (removedIds.size === 0) {
894
+ return;
895
+ }
896
+
897
+ const configPath = opencodeSettingsManager.selectConfigPath();
898
+ if (!configPath || !fs.existsSync(configPath)) {
899
+ return;
900
+ }
901
+
902
+ let config = {};
903
+ try {
904
+ config = opencodeSettingsManager.readConfig(configPath);
905
+ } catch {
906
+ return;
907
+ }
908
+
909
+ config = config && typeof config === 'object' ? config : {};
910
+ config.provider = config.provider && typeof config.provider === 'object' ? config.provider : {};
911
+
912
+ let changed = false;
913
+ for (const providerId of removedIds) {
914
+ const provider = config.provider[providerId];
915
+ if (provider && !isMeaningfulOpenCodeProvider(provider)) {
916
+ delete config.provider[providerId];
917
+ changed = true;
918
+ }
919
+ }
920
+
921
+ const activeProviderId = getConfiguredOpenCodeProviderId(config);
922
+ if (activeProviderId && removedIds.has(activeProviderId)) {
923
+ const activeProvider = config.provider[activeProviderId];
924
+ if (!isMeaningfulOpenCodeProvider(activeProvider)) {
925
+ const fallbackModel = pickFallbackOpenCodeModel(config, removedIds);
926
+ if (fallbackModel) {
927
+ config.model = fallbackModel;
928
+ } else {
929
+ delete config.model;
930
+ }
931
+ changed = true;
932
+ }
933
+ }
934
+
935
+ if (Object.keys(config.provider).length === 0) {
936
+ delete config.provider;
937
+ changed = true;
938
+ }
939
+
940
+ if (changed) {
941
+ opencodeSettingsManager.writeConfig(configPath, config);
942
+ }
943
+ }
944
+
800
945
  function applyOpenCodeOAuth(credential) {
801
946
  const providerId = String(credential.providerId || 'openai').trim() || 'openai';
802
947
  const payload = readJsonFile(NATIVE_PATHS.opencode.auth, {});
@@ -840,7 +985,9 @@ function inspectOpenCodeState() {
840
985
  const providers = config?.provider && typeof config.provider === 'object'
841
986
  ? Object.values(config.provider)
842
987
  : [];
843
- channelConfigured = providers.some(provider => provider?.__ctx_managed__ === true);
988
+ channelConfigured = providers.some(provider => (
989
+ isMeaningfulOpenCodeProvider(provider) && !isProxyBackedOpenCodeProvider(provider)
990
+ ));
844
991
  } catch {
845
992
  channelConfigured = false;
846
993
  }
@@ -238,7 +238,41 @@ function escapeForXml(value) {
238
238
  }
239
239
 
240
240
  function buildWindowsPopupCommand(title, message) {
241
- return `powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('${escapeForPowerShellSingleQuote(message)}', 5, '${escapeForPowerShellSingleQuote(title)}', 0x40)"`;
241
+ const script = [
242
+ 'Add-Type -AssemblyName System.Windows.Forms',
243
+ 'Add-Type -AssemblyName System.Drawing',
244
+ '$form = New-Object System.Windows.Forms.Form',
245
+ `$form.Text = '${escapeForPowerShellSingleQuote(title)}'`,
246
+ '$form.Width = 360',
247
+ '$form.Height = 120',
248
+ '$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
249
+ '$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow',
250
+ '$form.ShowInTaskbar = $false',
251
+ '$form.TopMost = $true',
252
+ '$form.MaximizeBox = $false',
253
+ '$form.MinimizeBox = $false',
254
+ '$workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
255
+ '$form.Location = New-Object System.Drawing.Point(($workingArea.Right - $form.Width - 16), ($workingArea.Top + 16))',
256
+ '$titleLabel = New-Object System.Windows.Forms.Label',
257
+ `$titleLabel.Text = '${escapeForPowerShellSingleQuote(title)}'`,
258
+ "$titleLabel.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)",
259
+ '$titleLabel.AutoSize = $true',
260
+ '$titleLabel.Location = New-Object System.Drawing.Point(14, 12)',
261
+ '$messageLabel = New-Object System.Windows.Forms.Label',
262
+ `$messageLabel.Text = '${escapeForPowerShellSingleQuote(message)}'`,
263
+ "$messageLabel.Font = New-Object System.Drawing.Font('Segoe UI', 9)",
264
+ '$messageLabel.MaximumSize = New-Object System.Drawing.Size(332, 0)',
265
+ '$messageLabel.AutoSize = $true',
266
+ '$messageLabel.Location = New-Object System.Drawing.Point(14, 40)',
267
+ '$form.Controls.Add($titleLabel)',
268
+ '$form.Controls.Add($messageLabel)',
269
+ '$timer = New-Object System.Windows.Forms.Timer',
270
+ '$timer.Interval = 5000',
271
+ '$timer.Add_Tick({ $timer.Stop(); $form.Close() })',
272
+ '$timer.Start()',
273
+ '[void]$form.ShowDialog()'
274
+ ].join('; ');
275
+ return `powershell -NoProfile -Command ${JSON.stringify(script)}`;
242
276
  }
243
277
 
244
278
  function generateNotifyScript(feishu = {}) {
@@ -464,7 +498,41 @@ function escapeForXml(value) {
464
498
  }
465
499
 
466
500
  function buildWindowsPopupCommand(title, message) {
467
- return \`powershell -NoProfile -Command "$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('\${escapeForPowerShellSingleQuote(message)}', 5, '\${escapeForPowerShellSingleQuote(title)}', 0x40)"\`
501
+ const script = [
502
+ 'Add-Type -AssemblyName System.Windows.Forms',
503
+ 'Add-Type -AssemblyName System.Drawing',
504
+ '$form = New-Object System.Windows.Forms.Form',
505
+ \`$form.Text = '\${escapeForPowerShellSingleQuote(title)}'\`,
506
+ '$form.Width = 360',
507
+ '$form.Height = 120',
508
+ '$form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
509
+ '$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow',
510
+ '$form.ShowInTaskbar = $false',
511
+ '$form.TopMost = $true',
512
+ '$form.MaximizeBox = $false',
513
+ '$form.MinimizeBox = $false',
514
+ '$workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
515
+ '$form.Location = New-Object System.Drawing.Point(($workingArea.Right - $form.Width - 16), ($workingArea.Top + 16))',
516
+ '$titleLabel = New-Object System.Windows.Forms.Label',
517
+ \`$titleLabel.Text = '\${escapeForPowerShellSingleQuote(title)}'\`,
518
+ " $titleLabel.Font = New-Object System.Drawing.Font('Segoe UI', 10, [System.Drawing.FontStyle]::Bold)".trim(),
519
+ '$titleLabel.AutoSize = $true',
520
+ '$titleLabel.Location = New-Object System.Drawing.Point(14, 12)',
521
+ '$messageLabel = New-Object System.Windows.Forms.Label',
522
+ \`$messageLabel.Text = '\${escapeForPowerShellSingleQuote(message)}'\`,
523
+ " $messageLabel.Font = New-Object System.Drawing.Font('Segoe UI', 9)".trim(),
524
+ '$messageLabel.MaximumSize = New-Object System.Drawing.Size(332, 0)',
525
+ '$messageLabel.AutoSize = $true',
526
+ '$messageLabel.Location = New-Object System.Drawing.Point(14, 40)',
527
+ '$form.Controls.Add($titleLabel)',
528
+ '$form.Controls.Add($messageLabel)',
529
+ '$timer = New-Object System.Windows.Forms.Timer',
530
+ '$timer.Interval = 5000',
531
+ '$timer.Add_Tick({ $timer.Stop(); $form.Close() })',
532
+ '$timer.Start()',
533
+ '[void]$form.ShowDialog()'
534
+ ].join('; ')
535
+ return 'powershell -NoProfile -Command ' + JSON.stringify(script)
468
536
  }
469
537
  `;
470
538
  }
@@ -117,7 +117,7 @@ function syncManagedChannelConfig(channels = [], preferredChannel = null) {
117
117
  : resolveCurrentManagedChannel(channels);
118
118
 
119
119
  if (targetChannel) {
120
- setChannelConfig(targetChannel);
120
+ setChannelConfig(buildNativeConfigChannel(targetChannel));
121
121
  return targetChannel;
122
122
  }
123
123
 
@@ -305,11 +305,20 @@ function applyChannelToSettings(channelId) {
305
305
  });
306
306
  saveChannels(data);
307
307
 
308
- setChannelConfig(channel);
308
+ setChannelConfig(buildNativeConfigChannel(channel));
309
309
 
310
310
  return channel;
311
311
  }
312
312
 
313
+ function buildNativeConfigChannel(channel = {}) {
314
+ const candidates = getEffectiveApiKeyCandidates(channel);
315
+ const effectiveApiKey = candidates[0] || normalizeApiKey(channel.apiKey || channel.key || '');
316
+ return {
317
+ ...channel,
318
+ apiKey: effectiveApiKey
319
+ };
320
+ }
321
+
313
322
  function loadCodexChannels() {
314
323
  const filePath = getCodexChannelsFilePath();
315
324
  if (!fs.existsSync(filePath)) {