Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
8323358
docs: add moonbridge console frontend design
KasumiNova May 24, 2026
5e7e6d0
docs: expand console config workflows
KasumiNova May 24, 2026
7299bea
docs: add console implementation plan
KasumiNova May 24, 2026
1cb845e
chore: ignore local worktrees
KasumiNova May 24, 2026
12ef200
feat: serve embedded console assets
KasumiNova May 24, 2026
31d01a6
feat: scaffold console web app
KasumiNova May 24, 2026
9b8a901
fix: harden console theme storage fallback
KasumiNova May 24, 2026
14fa74e
feat: add console rpc client and auth gate
KasumiNova May 24, 2026
fbc54c6
feat: build moonbridge console workflows
KasumiNova May 24, 2026
15b67e5
feat: localize console config workflows
KasumiNova May 24, 2026
970cada
feat: refine console apply UX and md3 layout
KasumiNova May 24, 2026
20d5846
feat: optimize UI/UX for MD3 — pill buttons, filled fields, nav rail …
KasumiNova May 24, 2026
f890734
feat(console): merge moonbridge-console — MD3 UI/UX optimization
KasumiNova May 24, 2026
4f69195
docs: design config graph console refactor
KasumiNova Jun 7, 2026
f55cc8e
docs: address config graph spec review
KasumiNova Jun 7, 2026
32f93b3
docs: plan config graph console implementation
KasumiNova Jun 7, 2026
6af5238
docs: keep webui dist untracked in plan
KasumiNova Jun 7, 2026
282410b
feat: define config graph schema
KasumiNova Jun 7, 2026
719f665
docs: clarify webui dist stays untracked
KasumiNova Jun 7, 2026
a4f2e89
feat: build config graph from runtime config
KasumiNova Jun 7, 2026
17e136d
feat: apply typed config graph patches
KasumiNova Jun 7, 2026
df7bb60
feat: validate runtime config candidates
KasumiNova Jun 7, 2026
8f663f4
feat: save config graph directly
KasumiNova Jun 7, 2026
ced1a29
feat: orchestrate config graph patches
KasumiNova Jun 7, 2026
10712b2
feat: expose config graph api
KasumiNova Jun 7, 2026
c1d6136
feat: expose backend log stream
KasumiNova Jun 7, 2026
de25e1d
fix: load console under auth and create sqlite parent dirs
KasumiNova Jun 7, 2026
4267d9f
feat: add config graph frontend client
KasumiNova Jun 7, 2026
f5ac66a
feat: switch console navigation to config graph
KasumiNova Jun 7, 2026
fb02a20
feat: rebuild core config graph pages
KasumiNova Jun 7, 2026
f7dea93
feat: add remaining config graph console pages
KasumiNova Jun 7, 2026
c8ae800
feat: remove primary apply and yaml console ux
KasumiNova Jun 7, 2026
0797902
docs: document config graph console
KasumiNova Jun 7, 2026
2433c6f
docs: plan console ui ux depth pass
KasumiNova Jun 7, 2026
b37de2e
feat: polish config graph resource cards
KasumiNova Jun 7, 2026
23c0c73
feat: refine realtime field controls
KasumiNova Jun 7, 2026
d72c2fc
feat: improve logs inspection ux
KasumiNova Jun 7, 2026
9af19ad
feat: unify config resource editing surfaces
KasumiNova Jun 7, 2026
ed4263b
feat: refine console shell ux
KasumiNova Jun 7, 2026
ec4f89f
test: make logs download test type-safe
KasumiNova Jun 7, 2026
f7f3265
build: refresh embedded console ui
KasumiNova Jun 7, 2026
73019ac
feat: deepen console resource ux polish
KasumiNova Jun 7, 2026
92f1f51
build: add arch package support
KasumiNova Jun 7, 2026
293e885
fix: seed config graph store on first run
KasumiNova Jun 7, 2026
ece4bbe
feat: refine console usage ui
KasumiNova Jun 8, 2026
e291955
feat(console): redesign config editor and add cross-session usage ana…
KasumiNova Jun 8, 2026
bbf8013
feat(webui): MD3 component overhaul for config editor (round 3)
KasumiNova Jun 8, 2026
3dc670d
refactor(webui): split app shell styles
KasumiNova Jun 8, 2026
bdd1812
fix(webui): use material switch component
KasumiNova Jun 8, 2026
07923c4
docs(webui): track material component debt
KasumiNova Jun 8, 2026
2b0c4f1
refactor(webui): migrate controls to material web
KasumiNova Jun 8, 2026
8758197
fix: align webui material control polish
KasumiNova Jun 8, 2026
0dc8652
fix(webui): expand route advanced feature fields
KasumiNova Jun 8, 2026
16fedb8
fix(webui): remove card surface shadows
KasumiNova Jun 8, 2026
1698bb4
fix(webui): add tonal backing to nav rail
KasumiNova Jun 8, 2026
3dc6973
fix(webui): align log export actions
KasumiNova Jun 8, 2026
6d595f1
fix(webui): tighten single-line text fields
KasumiNova Jun 8, 2026
1cf7b56
fix(webui): keep usage dashboard stable while loading ranges
KasumiNova Jun 8, 2026
bed041d
fix(webui): update usage duration live
KasumiNova Jun 8, 2026
9e2c125
fix(webui): align create subpanel controls
KasumiNova Jun 8, 2026
1c2d4b8
fix(webui): balance help tooltip spacing
KasumiNova Jun 8, 2026
5b7cdec
fix(webui): complete localized console text
KasumiNova Jun 9, 2026
b16c89e
fix(webui): refine model editor controls
KasumiNova Jun 9, 2026
6b19b64
fix(webui): sync embedded console bundle
KasumiNova Jun 9, 2026
7ab843a
fix(webui): replace raw json feature editors
KasumiNova Jun 9, 2026
d0859ff
fix(webui): unify resource config panels
KasumiNova Jun 9, 2026
4ee3807
webui: add model provider icons
KasumiNova Jun 9, 2026
7d5ab6a
feat: refactor model provider configuration
KasumiNova Jun 11, 2026
493da96
fix(packaging): install moonbridge executable
KasumiNova Jun 12, 2026
0a9ba36
Merge remote-tracking branch 'upstream/main'
KasumiNova Jun 12, 2026
3acfa25
fix: clean pr branch and console startup output
KasumiNova Jun 12, 2026
c9938ae
feat: add first-run starter config
KasumiNova Jun 13, 2026
b4df2f9
fix: preserve secret drafts during autosave
KasumiNova Jun 13, 2026
6106193
feat(webui): dialog-based editing, layout/UX overhaul, copy refinement
KasumiNova Jun 14, 2026
1cad3cb
feat(webui): wire up console login gate for auth_token
KasumiNova Jun 14, 2026
2cefb50
chore(packaging): bump pkgrel to 4 for console auth gate
KasumiNova Jun 14, 2026
3220ee5
feat(webui): outlined token field with plaintext toggle on login
KasumiNova Jun 14, 2026
6ed2056
feat(webui): secret fields hidden by default with smaller reveal togg…
KasumiNova Jun 14, 2026
9b08277
fix(webui): secret field trailing holds only the visibility toggle (n…
KasumiNova Jun 14, 2026
01e225e
chore(packaging): bump pkgrel to 5 for token field UX refinements
KasumiNova Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .codex/skills/moonbridge-webui-design/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
name: moonbridge-webui-design
description: Moon Bridge webui design and component rules. Use whenever changing files under webui/src that affect UI structure, controls, styling, theme tokens, layouts, or interaction behavior.
---

# Moon Bridge Webui Design

## Core Rule

Prefer official Material Web components from `@material/web` for all common controls.

Do not hand-roll controls such as switches, buttons, icon buttons, checkboxes, radio buttons, menus, tabs, dialogs, sliders, text fields, or progress indicators unless the user explicitly approves a custom control for that specific case. If a custom control is approved, document why Material Web is insufficient and keep the custom surface isolated.

Before changing webui UI, read `docs/webui/material-component-debt.md` when it exists. Treat it as the ordered migration backlog for known violations.

Native `<button>`, `<input>`, `<select>`, and `<textarea>` elements are not acceptable for app controls under `webui/src` unless they are inside tests, non-interactive generated examples, or an explicitly approved exception. Route links may remain anchors when they are navigation, not button controls.

## Existing Stack

- React renders Material Web custom elements with `createElement(...)` or a small typed wrapper component.
- Material Web imports belong near the component/wrapper that uses the element, for example `@material/web/switch/switch.js`.
- Theme integration should use Material Web public CSS custom properties such as `--md-switch-selected-track-color`; do not style internal shadow DOM classes.
- Project theme tokens live under `webui/src/theme/` and app CSS chunks live under `webui/src/app/styles/`.

## Component Practice

- Use a wrapper when React needs to bridge custom-element properties or events, especially boolean properties such as `selected` and events such as `change`.
- Prefer existing wrappers in `webui/src/components/Material*.tsx` before adding direct `md-*` elements in feature code. Direct `md-chip-set`, `md-icon`, and `md-ripple` use is acceptable when no property/event bridge is needed and the element is an official Material Web primitive.
- Boolean ARIA attributes on Material custom elements must be serialized intentionally when tests or accessibility depend on exact string values, for example `aria-expanded="true"` and `aria-pressed="false"`.
- Wrapper components must fail fast for impossible setup states. Do not add fallback markup that recreates the control.
- Keep wrappers narrow: map props to the official element and expose only app-needed behavior.
- Remove obsolete handcrafted CSS selectors when replacing custom controls. Styling classes may target Material Web host elements for layout or public CSS custom properties only; they must not keep styling native control implementations alive.

### Outlined Text Fields And Selects

`md-outlined-text-field` and `md-outlined-select` are not correctly migrated merely because the official host element is present. The Material component must own the whole visible field surface.

- The Material element must own the visible field label through a non-empty `label` property or attribute. Do not pass `label=""` while rendering an adjacent custom visual label for an ordinary visible form field.
- Text field leading and trailing icons must be slotted children of `md-outlined-text-field` using `slot="leading-icon"` or `slot="trailing-icon"`. Do not render external icons, help buttons, clear buttons, or manually aligned wrappers beside the field when they visually belong inside the field.
- `md-outlined-select` must use its official floating label and built-in trailing affordance. Do not add a custom external caret, trailing icon, or overlay button for the select.
- Wrapper APIs must expose app-needed Material properties directly, including `label`, `value`, `type`, `supportingText`, `errorText`, `disabled`, `required`, and `spellCheck`. Bridge both custom-element properties and host attributes when tests, accessibility, or browser behavior depend on exact serialization.
- Text fields must serialize `spellcheck` intentionally. Configuration identifiers, URLs, secrets, JSON, search filters, numeric fields, and other non-prose fields should default to `spellcheck="false"`; natural-language prose fields may opt in explicitly.
- Do not recreate Material label, outline notch, icon spacing, trailing affordance, focus, filled, or error animations in app CSS. Use Material host layout and public CSS custom properties only.

## Verification

For UI control changes:

1. Add or update tests that render the actual UI and assert the official element is used.
2. Cover the interaction path that changes app state.
3. Run targeted tests first, then `npm run build`, `npm test`, and `git diff --check` from `webui` or repo root as appropriate.
4. For visual changes, use browser or screenshot verification when the risk is layout/spacing drift.

Visual verification is required for migrated controls. Use browser-rendered screenshots or an equivalent visual inspection path for each touched page/state. Passing tests alone is not enough when control geometry, density, or popover behavior changed. Treat major visual drift as a failed task.

## Reviewer Agent Requirements

When acting as a reviewer agent for Moon Bridge webui changes, enforce this skill strictly:

- Block approval if a common control is still hand-rolled and there is no explicit user-approved exception recorded in code or in `docs/webui/material-component-debt.md`.
- Block approval if `webui/src` production code introduces native `<button>`, `<input>`, `<select>`, or `<textarea>` controls without an explicit approved exception.
- Block approval if feature code directly creates `md-*` controls that should go through an existing Material wrapper, unless the direct element is a simple official grouping/decorative primitive such as `md-chip-set`, `md-icon`, or `md-ripple`.
- Block approval if a Material Web replacement includes fallback markup that recreates the old custom control.
- Block approval if an outlined text field or select renders `md-outlined-*` but still uses an external custom label as the visible field label, including `label=""` plus adjacent label markup for a normal visible form field.
- Block approval if text-field icons, help icons, clear buttons, or trailing actions that visually belong to the field are rendered outside `md-outlined-text-field` instead of using official slots.
- Block approval if an outlined select adds a custom external trailing icon, caret, or overlay control instead of relying on the Material select affordance.
- Block approval if code styles Material Web shadow DOM internals or private classes instead of public CSS custom properties.
- Block approval if tests do not assert that the migrated official Material Web element is rendered.
- Block approval if interaction tests do not cover the migrated control's state-change path.
- Block approval if tests only assert that `md-outlined-text-field` or `md-outlined-select` exists without checking correct Material API use: non-empty `label`, slotted icons where applicable, intentional `spellcheck`, and absence of replacement external label or icon markup.
- Block approval if CSS manually rebuilds label position, outline notch behavior, icon alignment, trailing affordances, or focus/error animations for Material outlined fields/selects.
- Block approval if visual verification is missing for changed pages or if screenshots show major layout, density, alignment, or overflow drift.
- Review obsolete CSS selectors as code debt: if a selector only exists for a removed handwritten control, require its removal or a documented follow-up.
- Check staged files and diffs directly. Do not rely on implementer summaries.
4 changes: 4 additions & 0 deletions .codex/skills/moonbridge-webui-design/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "Moonbridge Webui Design"
short_description: "Apply Moon Bridge webui Material design rules."
default_prompt: "Use $moonbridge-webui-design before changing Moon Bridge webui UI components."
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
internal/service/webui/dist/assets/*.js whitespace=-blank-at-eol
20 changes: 19 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ __pycache__/

# Agent / AI assistant local files
.planning/
.superpowers/
.worktrees/
docs/superpowers/
helloagents/
AGENTS.md
CLAUDE.md
.codex
.codex/
!.codex/
.codex/*
!.codex/skills/
.codex/skills/*
!.codex/skills/moonbridge-webui-design/
!.codex/skills/moonbridge-webui-design/**
.claude
.claude/
.agents
Expand Down Expand Up @@ -51,9 +59,19 @@ node_modules/
build/
.wrangler/
# WebUI build artifacts (source in webui/)
webui/node_modules
webui/node_modules/
webui/dist/
webui/coverage/
webui/.vite/
webui/.vitest/
webui/*.tsbuildinfo
webui/test-results/
webui/playwright-report/

# Wrangler dev vars
.dev.vars
/moonbridge
/logs/
logs/
*.log
17 changes: 16 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
.PHONY: test cover cover-html cover-check build
.PHONY: test cover cover-html cover-check build webui-install webui-test webui-build build-with-webui

COVERAGE_THRESHOLD := 95
COVER_PROFILE := /tmp/moonbridge-coverage.out

build:
CGO_ENABLED=0 go build ./...

webui-install:
npm --prefix webui install

webui-test:
npm --prefix webui test

webui-build:
npm --prefix webui run build
rm -rf internal/service/webui/dist
mkdir -p internal/service/webui/dist
cp -R webui/dist/. internal/service/webui/dist/

build-with-webui: webui-build
CGO_ENABLED=0 go build ./...

test:
CGO_ENABLED=0 go test ./...

Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@ Moon Bridge 是一个用 Go 编写的协议转换与模型路由代理。对外
## 快速开始

```bash
# 复制配置并编辑
cp config.example.yml config.yml
# 修改 config.yml 中的 api_key
# pacman 或二进制安装后直接启动
moonbridge

# 启动
go run ./cmd/moonbridge -config config.yml
# 首次无配置启动会创建 $HOME/moonbridge/config.yml
# 打开 http://127.0.0.1:38440/console/
# 在 Web Console 中配置 Provider、Model 和 API Key

# 源码开发也可以直接运行
go run ./cmd/moonbridge

# 另见 CookBook.md 中的详细使用场景
```

要求 Go 1.25+。
源码开发要求 Go 1.25+。

## 核心能力

Expand Down Expand Up @@ -74,7 +77,7 @@ docker run -p 38440:38440 -v $(pwd)/config.yml:/config/config.yml moonbridge

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `-config` | `${XDG_CONFIG_HOME}/moonbridge/config.yml` | 配置文件路径 |
| `-config` | `$HOME/moonbridge/config.yml` | 配置文件路径 |
| `-addr` | 来自配置文件 | 覆盖监听地址 |
| `-mode` | 来自配置文件 | 覆盖运行模式(Transform/CaptureAnthropic/CaptureResponse) |
| `-print-addr` | — | 打印配置的监听地址后退出 |
Expand All @@ -92,6 +95,7 @@ docker run -p 38440:38440 -v $(pwd)/config.yml:/config/config.yml moonbridge
| `/responses` | POST | 同上(无 `/v1` 前缀) |
| `/v1/models` | GET | 列出可用模型 |
| `/models` | GET | 同上 |
| `/console/` | GET | 嵌入式 Web Console |
| `/api/v1/` | — | 管理 API(需启用持久化) |

详细 API 文档见 [API.md](docs/api.md)。
Expand Down
2 changes: 1 addition & 1 deletion cmd/cloudflare/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func main() {
defer plugins.ShutdownAll()

// Wire plugin LogConsumer into the slog consume pipeline.
logger.SetConsumeFunc(func(entries []logger.LogEntry) []logger.LogEntry {
logger.AddConsumeFunc(func(entries []logger.LogEntry) []logger.LogEntry {
return plugins.ConsumeGlobalLog(entries)
})

Expand Down
154 changes: 152 additions & 2 deletions cmd/moonbridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"os/signal"
"path/filepath"
"syscall"

"log/slog"
Expand Down Expand Up @@ -47,14 +48,15 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int {
if err := flags.Parse(args); err != nil {
return exitStartupErr
}
configFlagSet := flagWasSet(flags, "config")

var cfg config.Config
var err error
extensions := app.BuiltinExtensions()
resolvedConfigPath, err := config.ResolveConfigPath(*configPath)
if err != nil {
writeStartupError(stderr, "配置文件路径解析失败", "", err,
"设置 XDG_CONFIG_HOME,或使用 -config 明确指定配置文件路径。")
"设置 HOME,或使用 -config 明确指定配置文件路径。")
return exitStartupErr
}
if *dumpConfigSchema {
Expand All @@ -66,12 +68,26 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int {
return exitOK
}

if !configFlagSet {
created, err := createStarterConfigIfMissing(resolvedConfigPath, config.LoadOptions{
ExtensionSpecs: extensions.ConfigSpecs(),
})
if err != nil {
writeStartupError(stderr, "默认配置创建失败", resolvedConfigPath, err,
"确认 HOME 目录可写,或使用 -config 指向已有配置文件。")
return exitStartupErr
}
if created {
fmt.Fprintf(stderr, "已创建默认配置: %s\n", resolvedConfigPath)
}
}

cfg, err = config.LoadFromFileWithOptions(resolvedConfigPath, config.LoadOptions{
ExtensionSpecs: extensions.ConfigSpecs(),
})
if err != nil {
writeStartupError(stderr, "配置文件加载失败", resolvedConfigPath, err,
"未传 -config 时默认读取 ${XDG_CONFIG_HOME:-$HOME/.config}/moonbridge/config.yml。",
"未传 -config 时默认读取 $HOME/moonbridge/config.yml。",
"检查 YAML 语法、字段拼写和缩进。",
"确认 provider、routes、developer.proxy 等必填配置都已补齐。",
"如果是 protocol 字段,Responses 直通请使用 openai-response。")
Expand Down Expand Up @@ -136,6 +152,140 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int {
return exitOK
}

func flagWasSet(flags *flag.FlagSet, name string) bool {
wasSet := false
flags.Visit(func(f *flag.Flag) {
if f.Name == name {
wasSet = true
}
})
return wasSet
}

func createStarterConfigIfMissing(configPath string, opts config.LoadOptions) (bool, error) {
if _, err := os.Stat(configPath); err == nil {
return false, nil
} else if !os.IsNotExist(err) {
return false, fmt.Errorf("stat config %s: %w", configPath, err)
}

dbPath, err := config.StarterSQLiteDBPath(configPath)
if err != nil {
return false, err
}
data, err := config.StarterConfigYAML(dbPath, opts)
if err != nil {
return false, err
}
configDir := filepath.Dir(configPath)
if err := os.MkdirAll(configDir, 0o700); err != nil {
return false, fmt.Errorf("create config directory %s: %w", configDir, err)
}
if err := os.Chmod(configDir, 0o700); err != nil {
return false, fmt.Errorf("chmod config directory %s: %w", configDir, err)
}
created, err := writeFileExclusive(configPath, data, 0o600)
if err != nil {
return false, err
}
return created, nil
}

func writeFileExclusive(path string, data []byte, perm os.FileMode) (bool, error) {
configDir := filepath.Dir(path)
tempFile, err := os.CreateTemp(configDir, "."+filepath.Base(path)+".*.tmp")
if err != nil {
return false, fmt.Errorf("create temp config file in %s: %w", configDir, err)
}
tempPath := tempFile.Name()
if err := tempFile.Chmod(perm); err != nil {
return false, cleanupTempConfigFile(tempFile, tempPath, fmt.Errorf("chmod temp config file %s: %w", tempPath, err))
}
written, err := tempFile.Write(data)
if err == nil && written != len(data) {
err = io.ErrShortWrite
}
if err != nil {
return false, cleanupTempConfigFile(tempFile, tempPath, fmt.Errorf("write temp config file %s: %w", tempPath, err))
}
if err := tempFile.Sync(); err != nil {
return false, cleanupTempConfigFile(tempFile, tempPath, fmt.Errorf("sync temp config file %s: %w", tempPath, err))
}
if err := tempFile.Close(); err != nil {
return false, cleanupTempPath(tempPath, fmt.Errorf("close temp config file %s: %w", tempPath, err))
}
return publishConfigFile(tempPath, path)
}

func publishConfigFile(tempPath string, finalPath string) (bool, error) {
if err := os.Link(tempPath, finalPath); err != nil {
cleanupErr := cleanupTempPath(tempPath, nil)
if os.IsExist(err) {
if cleanupErr != nil {
return false, cleanupErr
}
return false, nil
}
if cleanupErr != nil {
return false, errors.Join(fmt.Errorf("publish config file %s from %s: %w", finalPath, tempPath, err), cleanupErr)
}
return false, fmt.Errorf("publish config file %s from %s: %w", finalPath, tempPath, err)
}
if err := syncParentDir(finalPath); err != nil {
return false, cleanupTempPath(tempPath, fmt.Errorf("sync config directory after publishing %s: %w", finalPath, err))
}
if err := os.Remove(tempPath); err != nil {
return false, fmt.Errorf("remove published temp config file %s: %w", tempPath, err)
}
if err := syncParentDir(finalPath); err != nil {
return false, fmt.Errorf("sync config directory after removing temp config %s: %w", tempPath, err)
}
return true, nil
}

func syncParentDir(path string) error {
dirPath := filepath.Dir(path)
dir, err := os.Open(dirPath)
if err != nil {
return fmt.Errorf("open config directory %s: %w", dirPath, err)
}
if err := dir.Sync(); err != nil {
closeErr := dir.Close()
if closeErr != nil {
return errors.Join(fmt.Errorf("sync config directory %s: %w", dirPath, err), fmt.Errorf("close config directory %s: %w", dirPath, closeErr))
}
return fmt.Errorf("sync config directory %s: %w", dirPath, err)
}
if err := dir.Close(); err != nil {
return fmt.Errorf("close config directory %s: %w", dirPath, err)
}
return nil
}

func cleanupTempConfigFile(file *os.File, path string, cause error) error {
var errs []error
errs = append(errs, cause)
if err := file.Close(); err != nil {
errs = append(errs, fmt.Errorf("close temp config file %s: %w", path, err))
}
return cleanupTempPath(path, errors.Join(errs...))
}

func cleanupTempPath(path string, cause error) error {
var errs []error
if cause != nil {
errs = append(errs, cause)
}
if err := os.Remove(path); err != nil {
if !os.IsNotExist(err) {
errs = append(errs, fmt.Errorf("remove temp config file %s: %w", path, err))
}
} else if err := syncParentDir(path); err != nil {
errs = append(errs, fmt.Errorf("sync config directory after removing temp config %s: %w", path, err))
}
return errors.Join(errs...)
}

func writeStartupError(output io.Writer, title string, configPath string, err error, hints ...string) {
fmt.Fprintf(output, "Moon Bridge 启动失败:%s\n", title)
if configPath != "" {
Expand Down
Loading