Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6b46bdf
feat(TUI):增加图片输入入口
phantom5099 Apr 17, 2026
20a5312
fix(tui,session,config): address review findings and add regression c…
xgopilot Apr 17, 2026
5d5e51c
Merge pull request #27 from phantom5099/fork-pr-337-1776410957
phantom5099 Apr 17, 2026
56fb8c9
fix(runtime/session/tui): harden multimodal input safety and event is…
xgopilot Apr 17, 2026
6d9f3e8
Merge pull request #28 from phantom5099/fork-pr-337-1776410957
phantom5099 Apr 17, 2026
dd7e3b9
test(coverage): 补充多模态输入与事件分支测试
xgopilot Apr 17, 2026
a2d685e
Merge pull request #29 from phantom5099/fork-pr-337-1776410957
phantom5099 Apr 17, 2026
07e26c0
feat(cli): 支持应用启动静默检测新版本与平滑自动升级
pionxe Apr 17, 2026
2b90cf9
fix: resolve residual multimodal/session/provider review risks
xgopilot Apr 17, 2026
4edc502
fix(cli): 修复自动更新并发冲突、终端注入风险及网络阻塞问题
pionxe Apr 17, 2026
fbc5f8a
test: improve updater and workspace coverage
xgopilot Apr 17, 2026
07f999c
fix(runtime/session/config): avoid prepare-event deadlock and improve…
xgopilot Apr 17, 2026
100ad8a
Merge branch 'fork-pr-337-1776410957' of https://github.com/phantom50…
xgopilot Apr 17, 2026
a7e61a1
Merge pull request #30 from phantom5099/fork-pr-337-1776410957
phantom5099 Apr 17, 2026
6c0338a
fix(cli): harden update notice/output and simplify logic
xgopilot Apr 17, 2026
6a6e7eb
Merge pull request #340 from pionxe/main
phantom5099 Apr 17, 2026
4d49d5e
Merge pull request #337 from phantom5099/main
phantom5099 Apr 17, 2026
66ec304
merge: resolve conflicts with origin/main for sqlite persistence PR
xgopilot Apr 17, 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
3 changes: 2 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ before:
builds:
- env:
- CGO_ENABLED=0 # 禁用 CGO,确保生成纯静态链接的二进制文件
ldflags:
- -s -w -X 'neo-code/internal/version.Version={{.Version}}'
goos:
- linux
- windows
Expand Down Expand Up @@ -46,4 +48,3 @@ changelog:
exclude:
- '^docs:'
- '^test:'

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ go run ./cmd/neocode --workdir /path/to/workspace
- [Context Compact 说明](docs/context-compact.md)
- [Tools 与 TUI 集成](docs/tools-and-tui-integration.md)
- [MCP 配置指南](docs/guides/mcp-configuration.md)
- [更新与升级](docs/guides/update.md)

## 如何参与

Expand Down
3 changes: 3 additions & 0 deletions cmd/neocode/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ func main() {
fmt.Fprintf(os.Stderr, "neocode: %v\n", err)
os.Exit(1)
}
if notice := cli.ConsumeUpdateNotice(); notice != "" {
fmt.Fprintln(os.Stdout, notice)
}
}
26 changes: 26 additions & 0 deletions docs/guides/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 更新与升级

## 自动检测

- `neocode` 启动时会在后台静默检测最新版本(默认 3 秒超时)。
- 为避免干扰 Bubble Tea TUI 交互,更新提示会在应用退出、终端屏幕恢复后输出。
- `url-dispatch` 与 `update` 子命令会跳过该检测流程。

## 手动升级

使用以下命令升级到最新稳定版:

```bash
neocode update
```

如需包含预发布版本:

```bash
neocode update --prerelease
```

## 版本来源

- 发布构建会通过 `ldflags` 注入版本号到 `internal/version.Version`。
- 本地开发构建默认版本为 `dev`。
1 change: 1 addition & 0 deletions internal/app/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ func BuildRuntime(ctx context.Context, opts BootstrapOptions) (RuntimeBundle, er
contextBuilder,
)
runtimeSvc.SetSessionAssetStore(sessionStore)
runtimeSvc.SetUserInputPreparer(agentruntime.NewSessionInputPreparer(sessionStore, sessionStore))
runtimeSvc.SetSkillsRegistry(buildSkillsRegistry(ctx, loader.BaseDir()))
runtimeSvc.SetAutoCompactThresholdResolver(runtimeAutoCompactThresholdResolverFunc(
func(ctx context.Context, cfg config.Config) (int, error) {
Expand Down
128 changes: 124 additions & 4 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,37 @@ package cli
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"neo-code/internal/app"
"neo-code/internal/config"
"neo-code/internal/updater"
"neo-code/internal/version"
)

var launchRootProgram = defaultRootProgramLauncher
var newRootProgram = app.NewProgram
var runGlobalPreload = defaultGlobalPreload
var runSilentUpdateCheck = defaultSilentUpdateCheck
var readCurrentVersion = version.Current
var checkLatestRelease = updater.CheckLatest

const silentUpdateCheckTimeout = 3 * time.Second
const silentUpdateCheckDrainTimeout = 300 * time.Millisecond

var ansiEscapeSequencePattern = regexp.MustCompile(`\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\)|[@-Z\\-_])`)

var (
silentUpdateCheckMu sync.Mutex
silentUpdateCheckDone <-chan struct{}
)

// GlobalFlags 描述 CLI 根命令当前支持的全局参数。
type GlobalFlags struct {
Expand All @@ -24,7 +43,12 @@ type GlobalFlags struct {
// Execute 负责执行 NeoCode 的 CLI 根命令。
func Execute(ctx context.Context) error {
app.EnsureConsoleUTF8()
return NewRootCommand().ExecuteContext(ctx)
_ = ConsumeUpdateNotice()
setSilentUpdateCheckDone(nil)

err := NewRootCommand().ExecuteContext(ctx)
waitSilentUpdateCheckDone(silentUpdateCheckDrainTimeout)
return err
}

// NewRootCommand 创建 NeoCode 的 CLI 根命令。
Expand All @@ -41,7 +65,13 @@ func NewRootCommand() *cobra.Command {
if shouldSkipGlobalPreload(cmd) {
return nil
}
return runGlobalPreload(cmd.Context())
if err := runGlobalPreload(cmd.Context()); err != nil {
return err
}
if !shouldSkipSilentUpdateCheck(cmd) {
runSilentUpdateCheck(cmd.Context())
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
flags.Workdir = strings.TrimSpace(settings.GetString("workdir"))
Expand All @@ -56,6 +86,7 @@ func NewRootCommand() *cobra.Command {
cmd.AddCommand(
newGatewayCommand(),
newURLDispatchCommand(),
newUpdateCommand(),
)

return cmd
Expand Down Expand Up @@ -92,10 +123,99 @@ func defaultGlobalPreload(ctx context.Context) error {
return config.LoadPersistedEnv("")
}

// defaultSilentUpdateCheck 在后台异步检查新版本并缓存退出后提示文案。
func defaultSilentUpdateCheck(ctx context.Context) {
currentVersion := readCurrentVersion()
if !version.IsSemverRelease(currentVersion) {
setSilentUpdateCheckDone(nil)
return
}
parentCtx := context.WithoutCancel(ctx)
done := make(chan struct{})
setSilentUpdateCheckDone(done)

go func(parent context.Context, currentVersion string, done chan struct{}) {
defer close(done)

checkCtx, cancel := context.WithTimeout(parent, silentUpdateCheckTimeout)
defer cancel()

result, err := checkLatestRelease(checkCtx, updater.CheckOptions{
CurrentVersion: currentVersion,
IncludePrerelease: false,
})
if err != nil || !result.HasUpdate {
return
}

latestVersion := sanitizeVersionForTerminal(result.LatestVersion)
if latestVersion == "" {
return
}
setUpdateNotice(fmt.Sprintf("\u53d1\u73b0\u65b0\u7248\u672c: %s\uff0c\u8fd0\u884c neocode update \u5373\u53ef\u5347\u7ea7", latestVersion))
}(parentCtx, currentVersion, done)
}

// shouldSkipGlobalPreload 判断当前命令是否应跳过全局预加载逻辑。
func shouldSkipGlobalPreload(cmd *cobra.Command) bool {
if cmd == nil {
return normalizedCommandName(cmd) == "url-dispatch"
}

// shouldSkipSilentUpdateCheck 判断当前命令是否应跳过静默更新检测。
func shouldSkipSilentUpdateCheck(cmd *cobra.Command) bool {
switch normalizedCommandName(cmd) {
case "url-dispatch", "update":
return true
default:
return false
}
return strings.EqualFold(strings.TrimSpace(cmd.Name()), "url-dispatch")
}

// sanitizeVersionForTerminal 清洗远端版本字符串,避免 ANSI 控制序列或不可见字符污染终端输出。
func sanitizeVersionForTerminal(version string) string {
cleaned := ansiEscapeSequencePattern.ReplaceAllString(version, "")
var builder strings.Builder
builder.Grow(len(cleaned))
for _, ch := range cleaned {
if ch >= 0x20 && ch <= 0x7e {
builder.WriteRune(ch)
}
}
return strings.TrimSpace(builder.String())
}

// normalizedCommandName 返回标准化后的命令名,统一处理空命令与大小写。
func normalizedCommandName(cmd *cobra.Command) string {
if cmd == nil {
return ""
}
return strings.ToLower(strings.TrimSpace(cmd.Name()))
}

// setSilentUpdateCheckDone 保存当前静默检测任务的完成信号通道。
func setSilentUpdateCheckDone(done <-chan struct{}) {
silentUpdateCheckMu.Lock()
silentUpdateCheckDone = done
silentUpdateCheckMu.Unlock()
}

// waitSilentUpdateCheckDone 在命令退出阶段等待静默检测短暂收口,降低提示丢失概率。
func waitSilentUpdateCheckDone(timeout time.Duration) {
if timeout <= 0 {
return
}

silentUpdateCheckMu.Lock()
done := silentUpdateCheckDone
silentUpdateCheckMu.Unlock()
if done == nil {
return
}

timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-done:
case <-timer.C:
}
}
Loading
Loading