Skip to content

Commit 78bd3a1

Browse files
committed
Release 0.4.2: MiniMax x-api-key auth, Feishu pairing file watcher
- MiniMax: use Anthropic-style x-api-key (authHeader false); migrate configs; exclude MiniMax from blanket Bearer migration - Feishu: replace 12s polling with fs.watch on credentials dir; dedupe by openId and throttle without openId - Version bump and docs (CHANGELOG, README, bundle manifest) Made-with: Cursor
1 parent 8332ad2 commit 78bd3a1

10 files changed

Lines changed: 173 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
All notable changes to OpenClaw Desktop will be documented in this file.
44

5+
## [0.4.2] - 2026-03-26
6+
7+
### Fixed
8+
9+
- **MiniMax HTTP 401 `invalid api key`:** MiniMax’s Anthropic-compatible API (`https://api.minimax.io/anthropic`) expects **Anthropic-style `x-api-key`** (official docs: `ANTHROPIC_API_KEY` + Anthropic SDK). The shell had set `authHeader: true` (**Bearer**), which MiniMax rejects even when the key is valid. The MiniMax seed no longer sets Bearer; existing `openclaw.json` entries are migrated to **`authHeader: false`** on load. The blanket third-party `anthropic-messages` migration **excludes** MiniMax; OpenCode Zen, Kimi Coding, Synthetic, Cloudflare AI Gateway, etc. are unchanged.
10+
11+
### Changed
12+
13+
- **Feishu DM pairing notifications:** Replaced the 12s polling loop with **`fs.watch` on `~/.openclaw/credentials`** (debounced) so pairing JSON updates drive notifications; **dedupe by `openId`** and a short throttle when `openId` is missing to avoid repeated toasts when the pairing code rotates.
14+
515
## [0.4.1] - 2026-03-26
616

717
### Changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ If you've been searching for *how to install OpenClaw on Windows*, *how to run O
5454

5555
**System:** Windows 10/11 x64 · ~350 MB free space · Internet for API calls
5656

57+
## Latest: MiniMax `invalid api key` (401)
58+
59+
MiniMax’s Anthropic-compatible base URL (`https://api.minimax.io/anthropic`) expects **Anthropic-style `x-api-key`** ([MiniMax platform docs](https://platform.minimax.io/docs/guides/text-generation): `ANTHROPIC_API_KEY` + Anthropic SDK). Older shell builds set `authHeader: true` (**Bearer**), which MiniMax rejects. Current builds omit Bearer for MiniMax and migrate existing `openclaw.json` to `authHeader: false` on startup. Other third-party `anthropic-messages` hosts (OpenCode Zen, Kimi Coding, Synthetic, Cloudflare AI Gateway, …) may still use `authHeader: true` where required.
60+
5761
## What's New in v0.3.4
5862

59-
- **MiniMax / Anthropic-compatible 401:** `models.providers.*` now sets **`authHeader: true`** for third-party `anthropic-messages` hosts (same as OpenClaw `extensions/minimax/onboard.ts`). Existing configs are migrated on startup—fixes invalid API key errors when the key is correct but auth headers did not match the host.
63+
- **Third-party Anthropic-compatible 401 (excluding MiniMax):** `authHeader: true` for hosts that expect Bearer. MiniMax is handled separately (see above).
6064

6165
Earlier highlights (v0.3.3): `minimax:global` profile id + auth-profiles migration.
6266

README.zh-CN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@
5454

5555
**系统要求:** Windows 10/11 x64 · 约 350 MB 可用空间 · 网络连接(用于 API 调用)
5656

57+
## 最新:MiniMax `invalid api key`(401)
58+
59+
MiniMax 的 Anthropic 兼容地址(`https://api.minimax.io/anthropic`)使用与 Anthropic 相同的 **`x-api-key`**(见 [MiniMax 平台文档](https://platform.minimax.io/docs/guides/text-generation)`ANTHROPIC_API_KEY` + Anthropic SDK)。旧版 Shell 曾为 MiniMax 设置 `authHeader: true`**Bearer**),会导致密钥正确仍被服务端拒绝。当前版本对 MiniMax 不再使用 Bearer,并在启动时将已有 `openclaw.json` 迁移为 `authHeader: false`。其他第三方 `anthropic-messages` 端点(OpenCode Zen、Kimi Coding、Synthetic、Cloudflare AI Gateway 等)仍按需在适当时机使用 `authHeader: true`
60+
5761
## v0.3.4 更新亮点
5862

59-
- **MiniMax / Anthropic 兼容 401:** 与 OpenClaw 上游一致,为第三方 **`anthropic-messages`** 端点设置 **`authHeader: true`**;启动时自动迁移已有 `openclaw.json`。密钥正确但仍报 invalid api key 时多属此类问题
63+
- **第三方 Anthropic 兼容 401(不含 MiniMax)** 对需要 Bearer 的端点设置 `authHeader: true`;MiniMax 单独处理(见上文)
6064

6165
更早版本(v0.3.3)要点:`minimax:global` 与 auth-profiles 迁移 — 详见 [CHANGELOG.md](CHANGELOG.md)
6266

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openclaw-desktop",
3-
"version": "0.4.1+openclaw.2026.3.24",
3+
"version": "0.4.2+openclaw.2026.3.24",
44
"openclawBundleVersion": "2026.3.24",
55
"description": "Community-maintained Windows desktop app and installer for OpenClaw.",
66
"type": "module",

resources/bundle-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"shellVersion": "0.4.1+openclaw.2026.3.24",
2+
"shellVersion": "0.4.2+openclaw.2026.3.24",
33
"bundledOpenClawVersion": "2026.3.24"
44
}

src/main/config/openclaw-config.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,17 @@ function migrateAuthOrderFullProfileIds(
171171
return { config: next, changed: true }
172172
}
173173

174+
/** MiniMax Anthropic-compatible hosts use the same credential transport as Anthropic (`x-api-key`), per platform docs (ANTHROPIC_API_KEY + Anthropic SDK). Bearer (`authHeader: true`) yields HTTP 401 invalid api key. */
175+
function isMinimaxAnthropicProvider(providerId: string, baseUrl: string): boolean {
176+
if (providerId === 'minimax') return true
177+
const u = baseUrl.toLowerCase()
178+
return u.includes('api.minimax.io') || u.includes('minimaxi.com')
179+
}
180+
174181
/**
175-
* OpenClaw `extensions/minimax/onboard.ts` sets `authHeader: true` for third-party Anthropic-compatible
176-
* APIs (MiniMax, Synthetic, etc.). Without it, gateways can get HTTP 401 from hosts that expect
177-
* Bearer-style auth (see upstream issue #29169). Only set when missing; do not override explicit values.
182+
* Third-party Anthropic-compatible APIs that are not Anthropic official often need `authHeader: true`
183+
* (Bearer) — e.g. OpenCode Zen, Kimi Coding, Synthetic (see upstream issue #29169).
184+
* MiniMax is excluded: it expects Anthropic-style `x-api-key`, not Bearer.
178185
*/
179186
function migrateAnthropicThirdPartyAuthHeader(
180187
config: OpenClawConfig,
@@ -190,7 +197,7 @@ function migrateAnthropicThirdPartyAuthHeader(
190197
return { config, changed: false }
191198
}
192199

193-
for (const [, raw] of Object.entries(nextProviders)) {
200+
for (const [providerId, raw] of Object.entries(nextProviders)) {
194201
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue
195202
const p = raw as Record<string, unknown>
196203
if (p.authHeader !== undefined) continue
@@ -199,6 +206,7 @@ function migrateAnthropicThirdPartyAuthHeader(
199206
const baseUrl = typeof p.baseUrl === 'string' ? p.baseUrl : ''
200207
if (!baseUrl.trim()) continue
201208
if (baseUrl.includes('api.anthropic.com')) continue
209+
if (isMinimaxAnthropicProvider(providerId, baseUrl)) continue
202210
p.authHeader = true
203211
changed = true
204212
}
@@ -207,6 +215,40 @@ function migrateAnthropicThirdPartyAuthHeader(
207215
return { config: next, changed: true }
208216
}
209217

218+
/**
219+
* Earlier desktop migrations set `authHeader: true` for all third-party anthropic-messages hosts including MiniMax.
220+
* MiniMax requires Anthropic-style `x-api-key` (docs: ANTHROPIC_API_KEY + base https://api.minimax.io/anthropic). Flip to false.
221+
*/
222+
function migrateMinimaxAuthHeaderToXApiKey(
223+
config: OpenClawConfig,
224+
): { config: OpenClawConfig; changed: boolean } {
225+
const providers = config.models?.providers
226+
if (!providers || typeof providers !== 'object') {
227+
return { config, changed: false }
228+
}
229+
let changed = false
230+
const next = JSON.parse(JSON.stringify(config)) as OpenClawConfig
231+
const nextProviders = next.models?.providers
232+
if (!nextProviders || typeof nextProviders !== 'object') {
233+
return { config, changed: false }
234+
}
235+
236+
for (const [providerId, raw] of Object.entries(nextProviders)) {
237+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) continue
238+
const p = raw as Record<string, unknown>
239+
if (p.authHeader !== true) continue
240+
const api = typeof p.api === 'string' ? p.api : ''
241+
if (api !== 'anthropic-messages') continue
242+
const baseUrl = typeof p.baseUrl === 'string' ? p.baseUrl : ''
243+
if (!isMinimaxAnthropicProvider(providerId, baseUrl)) continue
244+
p.authHeader = false
245+
changed = true
246+
}
247+
248+
if (!changed) return { config, changed: false }
249+
return { config: next, changed: true }
250+
}
251+
210252
/**
211253
* OpenClaw 2026.1.29+: gateway auth mode `none` removed — gateway must use token or password.
212254
* Migrate legacy `mode: "none"` using whichever credential field is present.
@@ -369,14 +411,17 @@ export function readOpenClawConfig(): OpenClawConfig {
369411
cfg = migratedAuthOrder.config
370412
const migratedAuthHeader = migrateAnthropicThirdPartyAuthHeader(cfg)
371413
cfg = migratedAuthHeader.config
414+
const migratedMinimaxAuthHeader = migrateMinimaxAuthHeaderToXApiKey(cfg)
415+
cfg = migratedMinimaxAuthHeader.config
372416
if (
373417
migratedProviders.changed ||
374418
migratedFeishu.changed ||
375419
migratedControlUiRoot.changed ||
376420
migratedControlUi.changed ||
377421
migratedAuthNone.changed ||
378422
migratedAuthOrder.changed ||
379-
migratedAuthHeader.changed
423+
migratedAuthHeader.changed ||
424+
migratedMinimaxAuthHeader.changed
380425
) {
381426
try {
382427
writeOpenClawConfig(cfg)
@@ -404,7 +449,12 @@ export function readOpenClawConfig(): OpenClawConfig {
404449
}
405450
if (migratedAuthHeader.changed) {
406451
console.info(
407-
'[config] Set models.providers.*.authHeader=true for third-party anthropic-messages hosts in openclaw.json',
452+
'[config] Set models.providers.*.authHeader=true for third-party anthropic-messages hosts (excluding MiniMax) in openclaw.json',
453+
)
454+
}
455+
if (migratedMinimaxAuthHeader.changed) {
456+
console.info(
457+
'[config] Set models.providers.minimax.authHeader=false (Anthropic x-api-key) for MiniMax in openclaw.json',
408458
)
409459
}
410460
} catch (err) {

src/main/index.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { syncLoginItemToSystem, getLoginItemOpenAtLogin, clearLoginItem } from '
2727
import { patchGatewayResponseHeaders } from './security/gateway-response-headers.js'
2828
import { rewriteGatewayRequestUrlWithToken } from './security/gateway-request-auth.js'
2929
import { listPendingFeishuPairing } from './pairing/index.js'
30+
import { watchFeishuPairingCredentialsDir } from './pairing/feishu-pairing-credentials-watcher.js'
3031
import { resolveTrayLocale, getFeishuPairingNotificationStrings, formatFeishuPairingBody } from './tray/tray-i18n.js'
3132
import { registerShellFileProtocol, registerShellPrivileges } from './shell-protocol.js'
3233

@@ -39,7 +40,7 @@ process.on('uncaughtException', (error) => {
3940
dialog.showErrorBox('Unexpected Error', error.stack ?? error.message)
4041
})
4142
let isQuitting = false
42-
let feishuPairingNotifyTimer: ReturnType<typeof setInterval> | null = null
43+
let stopFeishuPairingNotifyWatcher: (() => void) | null = null
4344
const windowManager = new WindowManager({
4445
defaultGatewayPort: DEFAULT_GATEWAY_PORT,
4546
readShellConfig,
@@ -102,9 +103,9 @@ async function cleanupBeforeQuit(): Promise<void> {
102103
// 5. Stop background update polling
103104
stopBackgroundUpdateCheck()
104105

105-
if (feishuPairingNotifyTimer) {
106-
clearInterval(feishuPairingNotifyTimer)
107-
feishuPairingNotifyTimer = null
106+
if (stopFeishuPairingNotifyWatcher) {
107+
stopFeishuPairingNotifyWatcher()
108+
stopFeishuPairingNotifyWatcher = null
108109
}
109110
}
110111

@@ -307,14 +308,34 @@ app.whenReady().then(() => {
307308
trayManager.create()
308309
trayManager.setGatewayStatus(gatewayManager.getStatus().status)
309310

311+
/** Pairing codes we already surfaced via system notification (session). */
310312
const notifiedFeishuPairingCodes = new Set<string>()
311-
const pollFeishuPairingNotifications = () => {
313+
/**
314+
* When the gateway refreshes the pairing challenge frequently, `code` changes while `openId`
315+
* stays the same — dedupe by openId so we do not spam duplicate system notifications.
316+
*/
317+
const notifiedFeishuPairingOpenIds = new Set<string>()
318+
/** When pending rows have no openId yet, only one notification per interval (rotating code / noisy state). */
319+
let lastFeishuPairingNotifyWithoutOpenIdAt = 0
320+
const FEISHU_PAIRING_NOTIFY_MIN_INTERVAL_MS = 120_000
321+
322+
const processFeishuPairingNotifications = () => {
312323
void listPendingFeishuPairing()
313324
.then(({ requests }) => {
325+
const now = Date.now()
314326
for (const row of requests) {
315327
const code = row.code.trim().toUpperCase()
316-
if (!code || notifiedFeishuPairingCodes.has(code)) continue
328+
if (!code) continue
329+
const openId = row.openId?.trim()
330+
if (openId) {
331+
if (notifiedFeishuPairingOpenIds.has(openId)) continue
332+
} else {
333+
if (notifiedFeishuPairingCodes.has(code)) continue
334+
if (now - lastFeishuPairingNotifyWithoutOpenIdAt < FEISHU_PAIRING_NOTIFY_MIN_INTERVAL_MS) continue
335+
lastFeishuPairingNotifyWithoutOpenIdAt = now
336+
}
317337
notifiedFeishuPairingCodes.add(code)
338+
if (openId) notifiedFeishuPairingOpenIds.add(openId)
318339
if (!Notification.isSupported()) continue
319340
const loc = resolveTrayLocale(readShellConfig)
320341
const notifCopy = getFeishuPairingNotificationStrings(loc)
@@ -330,8 +351,8 @@ app.whenReady().then(() => {
330351
})
331352
.catch(() => {})
332353
}
333-
feishuPairingNotifyTimer = setInterval(pollFeishuPairingNotifications, 12_000)
334-
pollFeishuPairingNotifications()
354+
processFeishuPairingNotifications()
355+
stopFeishuPairingNotifyWatcher = watchFeishuPairingCredentialsDir(processFeishuPairingNotifications)
335356

336357
// 5. Pre-start checks (bundle + config)
337358
const prestartCheck = runPrestartCheck()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { getUserDataDir } from '../utils/paths.js'
4+
5+
const CREDENTIALS_SUBDIR = 'credentials'
6+
/** Debounce rapid writes (save + rename) and multi-platform double events */
7+
const DEBOUNCE_MS = 400
8+
9+
function isFeishuPairingJsonFilename(filename: string | null): boolean {
10+
if (!filename) return false
11+
const lower = filename.toLowerCase()
12+
if (!lower.endsWith('.json')) return false
13+
return lower.includes('pairing')
14+
}
15+
16+
/**
17+
* Watch `~/.openclaw/credentials` for Feishu pairing store files (`*pairing*.json`).
18+
* Invokes `onChange` after writes settle — no polling loop.
19+
*/
20+
export function watchFeishuPairingCredentialsDir(onChange: () => void): () => void {
21+
const dir = path.join(getUserDataDir(), CREDENTIALS_SUBDIR)
22+
try {
23+
fs.mkdirSync(dir, { recursive: true })
24+
} catch {
25+
// If mkdir fails, still try watch (caller may handle empty pending)
26+
}
27+
28+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
29+
const schedule = () => {
30+
if (debounceTimer) clearTimeout(debounceTimer)
31+
debounceTimer = setTimeout(() => {
32+
debounceTimer = null
33+
onChange()
34+
}, DEBOUNCE_MS)
35+
}
36+
37+
let watcher: fs.FSWatcher | null = null
38+
try {
39+
watcher = fs.watch(dir, (_event, filename) => {
40+
// Some platforms emit with null filename — still worth one debounced read
41+
if (filename != null && !isFeishuPairingJsonFilename(filename)) return
42+
schedule()
43+
})
44+
} catch {
45+
return () => {
46+
if (debounceTimer) clearTimeout(debounceTimer)
47+
}
48+
}
49+
50+
return () => {
51+
if (debounceTimer) {
52+
clearTimeout(debounceTimer)
53+
debounceTimer = null
54+
}
55+
try {
56+
watcher?.close()
57+
} catch {
58+
// ignore
59+
}
60+
watcher = null
61+
}
62+
}

src/main/wizard/setup-handler.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ type ProviderSeed = {
6868
/** OpenClaw `models.providers.*.api`; omit for plugin-native providers (e.g. Google Gemini). */
6969
api?: string
7070
/**
71-
* Third-party Anthropic-compatible hosts need `authHeader: true` so the gateway sends the same
72-
* auth as upstream `applyMinimaxApiConfig` / OpenClaw issue #29169 (MiniMax 401 without Bearer).
71+
* Some third-party Anthropic-compatible hosts need `authHeader: true` (Bearer). MiniMax uses
72+
* default Anthropic `x-api-key` — do not set here.
7373
*/
7474
authHeader?: boolean
7575
}
@@ -126,7 +126,7 @@ const PROVIDER_SEEDS: Partial<Record<ModelProvider, ProviderSeed>> = {
126126
providerId: 'minimax',
127127
baseUrl: 'https://api.minimax.io/anthropic',
128128
api: 'anthropic-messages',
129-
authHeader: true,
129+
/** Omit authHeader (default): MiniMax uses Anthropic-style `x-api-key`; Bearer breaks with 401 invalid api key. */
130130
},
131131
xai: {
132132
providerId: 'xai',

src/shared/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,7 @@ export interface ModelProviderConfig {
213213
/** Custom HTTP headers */
214214
headers?: Record<string, string>
215215
/**
216-
* Third-party `anthropic-messages` hosts (MiniMax, Synthetic, Cloudflare gateway, etc.) need `true`
217-
* so the gateway matches OpenClaw upstream onboard configs. Use `false` for local proxies (e.g. copilot-proxy).
216+
* Third-party `anthropic-messages` hosts that expect Bearer credentials (Synthetic, OpenCode Zen, Kimi Coding, Cloudflare gateway, etc.) use `true`. MiniMax (`api.minimax.io`) uses Anthropic-style `x-api-key` — omit or `false`. Use `false` for local proxies (e.g. copilot-proxy).
218217
*/
219218
authHeader?: boolean
220219
models?: Array<Record<string, unknown> & { id: string; name?: string }>

0 commit comments

Comments
 (0)