Skip to content

Commit a3cc918

Browse files
authored
Merge pull request #1 from LeoLin990405/feat/mimo-provider
feat(mimo): add MiMo provider
2 parents 94d7bd6 + 8dda6ce commit a3cc918

83 files changed

Lines changed: 6783 additions & 81 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-app.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Build CodexBar App
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: ["feat/qwen-doubao-providers"]
7+
8+
jobs:
9+
build-macos-app:
10+
runs-on: macos-latest
11+
steps:
12+
- uses: actions/checkout@v6
13+
14+
- name: Select Xcode
15+
run: |
16+
set -euo pipefail
17+
for candidate in /Applications/Xcode_26.1.1.app /Applications/Xcode_26.1.app /Applications/Xcode.app; do
18+
if [[ -d "$candidate" ]]; then
19+
sudo xcode-select -s "${candidate}/Contents/Developer"
20+
echo "DEVELOPER_DIR=${candidate}/Contents/Developer" >> "$GITHUB_ENV"
21+
break
22+
fi
23+
done
24+
xcodebuild -version
25+
swift --version
26+
27+
- name: Resolve dependencies
28+
run: swift package resolve
29+
30+
- name: Build release
31+
run: swift build -c release 2>&1
32+
33+
- name: Run tests
34+
run: swift test --no-parallel
35+
36+
- name: Fix rpath and package
37+
run: |
38+
set -euo pipefail
39+
BIN_DIR="$(swift build -c release --show-bin-path)"
40+
echo "Binary directory: $BIN_DIR"
41+
42+
# Add @executable_path/../Frameworks rpath so Sparkle.framework loads from .app bundle
43+
install_name_tool -add_rpath @executable_path/../Frameworks "$BIN_DIR/CodexBar" || true
44+
install_name_tool -add_rpath @executable_path/../Frameworks "$BIN_DIR/CodexBarCLI" || true
45+
46+
# Create a zip of the built products
47+
cd "$BIN_DIR"
48+
zip -r "$GITHUB_WORKSPACE/CodexBar-custom-build.zip" \
49+
CodexBar CodexBarCLI CodexBarClaudeWatchdog CodexBarClaudeWebProbe \
50+
CodexBar_CodexBar.bundle KeyboardShortcuts_KeyboardShortcuts.bundle \
51+
Sparkle.framework
52+
53+
- name: Upload build artifact
54+
uses: actions/upload-artifact@v4
55+
with:
56+
name: CodexBar-custom-build
57+
path: CodexBar-custom-build.zip
58+
retention-days: 30

Sources/CodexBar/MenuDescriptor.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,14 @@ struct MenuDescriptor {
247247
entries.append(.text("Activity: \(detail)", .secondary))
248248
}
249249
} else if let loginMethodText, !loginMethodText.isEmpty {
250-
entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText))", .secondary))
250+
let formatted = AccountFormatter.plan(loginMethodText)
251+
// Balance-style providers (openrouter, mimo) already emit "Balance: $X.XX";
252+
// don't double-prefix with "Plan: " in that case.
253+
if provider == .openrouter || provider == .mimo, formatted.hasPrefix("Balance:") {
254+
entries.append(.text(formatted, .secondary))
255+
} else {
256+
entries.append(.text("Plan: \(formatted)", .secondary))
257+
}
251258
}
252259

253260
if metadata.usesAccountFallback {

Sources/CodexBar/PreferencesProviderDetailView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct ProviderDetailView: View {
2727
else {
2828
return nil
2929
}
30-
guard provider == .openrouter else {
30+
guard provider == .openrouter || provider == .mimo else {
3131
return (label: "Plan", value: rawPlan)
3232
}
3333

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import AppKit
2+
import CodexBarCore
3+
import CodexBarMacroSupport
4+
import Foundation
5+
6+
@ProviderImplementationRegistration
7+
struct AigoCodeProviderImplementation: ProviderImplementation {
8+
let id: UsageProvider = .aigocode
9+
10+
@MainActor
11+
func observeSettings(_ settings: SettingsStore) {
12+
_ = settings.aigocodeAPIToken
13+
}
14+
15+
@MainActor
16+
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
17+
[
18+
ProviderSettingsFieldDescriptor(
19+
id: "aigocode-api-token",
20+
title: "API key",
21+
subtitle: "Optional when using web dashboard mode. "
22+
+ "Stored in ~/.codexbar/config.json.",
23+
kind: .secure,
24+
placeholder: "sk-...",
25+
binding: context.stringBinding(\.aigocodeAPIToken),
26+
actions: [
27+
ProviderSettingsActionDescriptor(
28+
id: "aigocode-open-dashboard",
29+
title: "Open AigoCode Dashboard",
30+
style: .link,
31+
isVisible: nil,
32+
perform: {
33+
if let url = URL(string: "https://www.aigocode.com/dashboard/console") {
34+
NSWorkspace.shared.open(url)
35+
}
36+
}),
37+
],
38+
isVisible: nil,
39+
onActivate: nil),
40+
]
41+
}
42+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import CodexBarCore
2+
import Foundation
3+
4+
extension SettingsStore {
5+
var aigocodeAPIToken: String {
6+
get { self.configSnapshot.providerConfig(for: .aigocode)?.sanitizedAPIKey ?? "" }
7+
set {
8+
self.updateProviderConfig(provider: .aigocode) { entry in
9+
entry.apiKey = self.normalizedConfigValue(newValue)
10+
}
11+
self.logSecretUpdate(provider: .aigocode, field: "apiKey", value: newValue)
12+
}
13+
}
14+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import AppKit
2+
import CodexBarCore
3+
import CodexBarMacroSupport
4+
import Foundation
5+
6+
@ProviderImplementationRegistration
7+
struct DoubaoProviderImplementation: ProviderImplementation {
8+
let id: UsageProvider = .doubao
9+
10+
@MainActor
11+
func observeSettings(_ settings: SettingsStore) {
12+
_ = settings.doubaoAPIToken
13+
}
14+
15+
@MainActor
16+
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
17+
[
18+
ProviderSettingsFieldDescriptor(
19+
id: "doubao-api-token",
20+
title: "API key",
21+
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Volcengine "
22+
+ "Ark console.",
23+
kind: .secure,
24+
placeholder: "ark-...",
25+
binding: context.stringBinding(\.doubaoAPIToken),
26+
actions: [
27+
ProviderSettingsActionDescriptor(
28+
id: "doubao-open-dashboard",
29+
title: "Open Volcengine Ark Console",
30+
style: .link,
31+
isVisible: nil,
32+
perform: {
33+
if let url = URL(string: "https://console.volcengine.com/ark/") {
34+
NSWorkspace.shared.open(url)
35+
}
36+
}),
37+
],
38+
isVisible: nil,
39+
onActivate: nil),
40+
]
41+
}
42+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import CodexBarCore
2+
import Foundation
3+
4+
extension SettingsStore {
5+
var doubaoAPIToken: String {
6+
get { self.configSnapshot.providerConfig(for: .doubao)?.sanitizedAPIKey ?? "" }
7+
set {
8+
self.updateProviderConfig(provider: .doubao) { entry in
9+
entry.apiKey = self.normalizedConfigValue(newValue)
10+
}
11+
self.logSecretUpdate(provider: .doubao, field: "apiKey", value: newValue)
12+
}
13+
}
14+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import AppKit
2+
import CodexBarCore
3+
import CodexBarMacroSupport
4+
import Foundation
5+
import SwiftUI
6+
7+
@ProviderImplementationRegistration
8+
struct MiMoProviderImplementation: ProviderImplementation {
9+
let id: UsageProvider = .mimo
10+
let supportsLoginFlow: Bool = true
11+
12+
@MainActor
13+
func presentation(context _: ProviderPresentationContext) -> ProviderPresentation {
14+
ProviderPresentation { _ in "web" }
15+
}
16+
17+
@MainActor
18+
func observeSettings(_ settings: SettingsStore) {
19+
_ = settings.miMoCookieSource
20+
_ = settings.miMoCookieHeader
21+
}
22+
23+
@MainActor
24+
func settingsSnapshot(context: ProviderSettingsSnapshotContext) -> ProviderSettingsSnapshotContribution? {
25+
.mimo(context.settings.miMoSettingsSnapshot(tokenOverride: context.tokenOverride))
26+
}
27+
28+
@MainActor
29+
func settingsPickers(context: ProviderSettingsContext) -> [ProviderSettingsPickerDescriptor] {
30+
let cookieBinding = Binding(
31+
get: { context.settings.miMoCookieSource.rawValue },
32+
set: { raw in
33+
context.settings.miMoCookieSource = ProviderCookieSource(rawValue: raw) ?? .auto
34+
})
35+
let cookieOptions = ProviderCookieSourceUI.options(
36+
allowsOff: false,
37+
keychainDisabled: context.settings.debugDisableKeychainAccess)
38+
let cookieSubtitle: () -> String? = {
39+
ProviderCookieSourceUI.subtitle(
40+
source: context.settings.miMoCookieSource,
41+
keychainDisabled: context.settings.debugDisableKeychainAccess,
42+
auto: "Automatic imports Chrome browser cookies from Xiaomi MiMo.",
43+
manual: "Paste a Cookie header from platform.xiaomimimo.com.",
44+
off: "Xiaomi MiMo cookies are disabled.")
45+
}
46+
47+
return [
48+
ProviderSettingsPickerDescriptor(
49+
id: "mimo-cookie-source",
50+
title: "Cookie source",
51+
subtitle: "Automatic imports Chrome browser cookies from Xiaomi MiMo.",
52+
dynamicSubtitle: cookieSubtitle,
53+
binding: cookieBinding,
54+
options: cookieOptions,
55+
isVisible: nil,
56+
onChange: nil,
57+
trailingText: {
58+
guard let entry = CookieHeaderCache.load(provider: .mimo) else { return nil }
59+
let when = entry.storedAt.relativeDescription()
60+
return "Cached: \(entry.sourceLabel)\(when)"
61+
}),
62+
]
63+
}
64+
65+
@MainActor
66+
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
67+
[
68+
ProviderSettingsFieldDescriptor(
69+
id: "mimo-cookie",
70+
title: "",
71+
subtitle: "",
72+
kind: .secure,
73+
placeholder: "Cookie: ...",
74+
binding: context.stringBinding(\.miMoCookieHeader),
75+
actions: [
76+
ProviderSettingsActionDescriptor(
77+
id: "mimo-open-balance",
78+
title: "Open MiMo Balance",
79+
style: .link,
80+
isVisible: nil,
81+
perform: {
82+
guard let url = URL(string: "https://platform.xiaomimimo.com/#/console/balance") else {
83+
return
84+
}
85+
NSWorkspace.shared.open(url)
86+
}),
87+
],
88+
isVisible: { context.settings.miMoCookieSource == .manual },
89+
onActivate: { context.settings.ensureMiMoCookieLoaded() }),
90+
]
91+
}
92+
93+
@MainActor
94+
func runLoginFlow(context _: ProviderLoginContext) async -> Bool {
95+
let loginURL = "https://platform.xiaomimimo.com/api/v1/genLoginUrl?currentPath=%2F%23%2Fconsole%2Fbalance"
96+
guard let url = URL(string: loginURL) else {
97+
return false
98+
}
99+
NSWorkspace.shared.open(url)
100+
return false
101+
}
102+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import CodexBarCore
2+
import Foundation
3+
4+
extension SettingsStore {
5+
var miMoCookieHeader: String {
6+
get { self.configSnapshot.providerConfig(for: .mimo)?.sanitizedCookieHeader ?? "" }
7+
set {
8+
self.updateProviderConfig(provider: .mimo) { entry in
9+
entry.cookieHeader = self.normalizedConfigValue(newValue)
10+
}
11+
self.logSecretUpdate(provider: .mimo, field: "cookieHeader", value: newValue)
12+
}
13+
}
14+
15+
var miMoCookieSource: ProviderCookieSource {
16+
get { self.resolvedCookieSource(provider: .mimo, fallback: .auto) }
17+
set {
18+
self.updateProviderConfig(provider: .mimo) { entry in
19+
entry.cookieSource = newValue
20+
}
21+
self.logProviderModeChange(provider: .mimo, field: "cookieSource", value: newValue.rawValue)
22+
}
23+
}
24+
25+
func ensureMiMoCookieLoaded() {}
26+
}
27+
28+
extension SettingsStore {
29+
func miMoSettingsSnapshot(tokenOverride: TokenAccountOverride?) -> ProviderSettingsSnapshot.MiMoProviderSettings {
30+
_ = tokenOverride
31+
return ProviderSettingsSnapshot.MiMoProviderSettings(
32+
cookieSource: self.miMoCookieSource,
33+
manualCookieHeader: self.miMoCookieHeader)
34+
}
35+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import AppKit
2+
import CodexBarCore
3+
import CodexBarMacroSupport
4+
import Foundation
5+
6+
@ProviderImplementationRegistration
7+
struct QwenProviderImplementation: ProviderImplementation {
8+
let id: UsageProvider = .qwen
9+
10+
@MainActor
11+
func observeSettings(_ settings: SettingsStore) {
12+
_ = settings.qwenAPIToken
13+
}
14+
15+
@MainActor
16+
func settingsFields(context: ProviderSettingsContext) -> [ProviderSettingsFieldDescriptor] {
17+
[
18+
ProviderSettingsFieldDescriptor(
19+
id: "qwen-api-token",
20+
title: "API key",
21+
subtitle: "Stored in ~/.codexbar/config.json. Get your API key from the Alibaba Cloud "
22+
+ "Bailian console (DashScope).",
23+
kind: .secure,
24+
placeholder: "sk-...",
25+
binding: context.stringBinding(\.qwenAPIToken),
26+
actions: [
27+
ProviderSettingsActionDescriptor(
28+
id: "qwen-open-dashboard",
29+
title: "Open Bailian Console",
30+
style: .link,
31+
isVisible: nil,
32+
perform: {
33+
if let url = URL(string: "https://bailian.console.aliyun.com/") {
34+
NSWorkspace.shared.open(url)
35+
}
36+
}),
37+
],
38+
isVisible: nil,
39+
onActivate: nil),
40+
]
41+
}
42+
}

0 commit comments

Comments
 (0)