Skip to content

fix(streaming): add heartbeat keepalive + connection pooling#27

Open
garywhat wants to merge 16 commits into
vibe-coding-labs:mainfrom
garywhat:fix/streaming-heartbeat
Open

fix(streaming): add heartbeat keepalive + connection pooling#27
garywhat wants to merge 16 commits into
vibe-coding-labs:mainfrom
garywhat:fix/streaming-heartbeat

Conversation

@garywhat

Copy link
Copy Markdown

Summary

Upstream JoyCode API buffers the entire response before sending anything (TTFB 10-30s for reasoning models). Without keepalive, downstream clients (Claude Code, OpenAI clients) time out or show "no response" during this gap.

  • Add SSE heartbeat goroutines for OpenAI (pkg/openai/chat.go) and Anthropic (pkg/anthropic/handler.go) streaming paths that send comment lines (": ...") every 15s while waiting for upstream
  • Parse usage from SSE data lines in OpenAI streaming so token consumption is logged correctly (previously missing for streaming requests)
  • Add shared http.Transport with connection pooling in pkg/joycode/client.go so clients reuse TCP connections instead of dialing anew for every request
  • Add graceful shutdown via stopCh for background goroutines in cmd/JoyCode2Api/serve.go
  • Add pkg/common.IsTimeoutError helper for timeout detection across packages

Background

This addresses the streaming stability issues reported in #2 and #12 (the "stream failed / read first line: EOF" symptom that occurs when the upstream TTFB exceeds client timeouts).

Test plan

  • Go build passes
  • Started server locally, verified OpenAI streaming endpoint sends heartbeat comments during upstream wait
  • Verified Anthropic streaming endpoint sends ping events during upstream wait
  • Verified token usage is logged for streaming requests (checked dashboard stats)
  • Verified graceful shutdown exits background goroutines cleanly

Generated with Devin

TuTouPower and others added 16 commits June 19, 2026 01:27
reset-password only sets auth_password_hash but does not ensure
auth_jwt_secret exists. When the database is wiped and password is
reset via CLI, the user cannot log in because JWT token generation
fails with 'JWT secret not configured'.

Check for missing auth_jwt_secret during reset-password and generate
one if needed, matching the behavior of the /api/auth/setup flow.

Closes vibe-coding-labs#14
…word-jwt-secret

fix(reset-password): generate JWT secret if missing
…-labs#21)

扫码登录的 fetchUserInfoWithPtKey 此前裸调 v1 userInfo + 写死
clientVersion 2.4.5,与 7fcf9df 适配 JoyCode 2.7 时引入的 color
gateway 鉴权(HMAC 签名 + v2 端点 + 2.7.5)脱节。二维码生成与扫码
轮询(qr.m.jd.com)均正常,但确认后用 pt_key 换用户信息这步走的
是 2.7 已弃用的旧路径,导致即使拿到有效 pt_key 也换不出用户信息。

改为复用 joycode.Client.UserInfo(),与 OAuth 登录走同一套鉴权,
消除两条登录链路的分叉。

Closes vibe-coding-labs#21

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ogin-color-gateway

fix(auth): 扫码登录走 color gateway 鉴权,修复 vibe-coding-labs#21 京东登录失败
仓库已从 vibe-coding-labs/JoyCodeProxy 改名为 JoyCode2Api,本次把代码侧
全面对齐到新名:

- go module: github.com/vibe-coding-labs/JoyCodeProxy → JoyCode2Api
- 23 个 .go 文件的 import 路径同步更新
- cmd/JoyCodeProxy/ 目录改名 cmd/JoyCode2Api/(git mv 保留历史)
- 构建配置跟随: Dockerfile / scripts/release.sh / web/vite.config.ts
- 二进制名与发布产物名 JoyCodeProxy → JoyCode2Api
- 前端 GitHub 链接、README、dashboard ghRepo 常量对齐新仓库
- 重新构建前端 embed 资源

不改: docs/superpowers/plans/ 历史规划文档(保持历史原貌);
JWT issuer "joycode-proxy"(改会使现有 dashboard 会话全部失效,不在范围);
.gitignore 中 snake_case/kebab 产物防御规则(保留以防本地旧二进制误提交)。

验证: go build/vet/test 全过;前端构建产物无旧链接残留;
启动重构后二进制 /health、/api/qr-login/init、/v1/models 路由均正常。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…/module-path-joycode2api

refactor: module 路径与二进制名对齐 JoyCode2Api
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Switch sqlite driver from mattn/go-sqlite3 (CGO) to modernc.org/sqlite
(pure Go). With CGO disabled the binary cross-compiles to darwin without
a macOS SDK, restoring the darwin-arm64 release artifact that v0.6.0 had
to ship without. Driver name sqlite3 -> sqlite; DSNs adapted (proxy.db
WAL via _pragma, read-only JoyCode IDE db via file: URI mode=ro).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
新增「从源码构建」章节:git clone + CGO_ENABLED=0 编译当前平台的说明,
以及一段可原样复制粘贴给 AI Agent(Claude Code / Cursor 等)的一键指令,
由 Agent 自动完成 clone / build / 后台启动 / 默认浏览器打开 Dashboard。
顺带把构建章节的 Go 版本要求从 1.22+ 修正为 1.25+(与 go.mod 一致)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refactor base URLs to be configurable via environment variables.
Make BaseURL and SaasBaseURL configurable
跟进 PR vibe-coding-labs#24 的格式规范化:var 块与 envOr 改用 tab 缩进、const 块等号重新对齐。功能改动由 @WslzGmzs 贡献,本提交仅做 gofmt,无逻辑变更。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upstream JoyCode API buffers the entire response before sending anything
(TTFB 10-30s for reasoning models). Without keepalive, downstream clients
(Claude Code, OpenAI clients) time out or show "no response" during this
gap.

- Add SSE heartbeat goroutines for OpenAI and Anthropic streaming paths
  that send comment lines (": ...") every 15s while waiting for upstream
- Parse usage from SSE data lines in OpenAI streaming for token logging
- Add shared http.Transport with connection pooling in joycode client
- Add graceful shutdown via stopCh for background goroutines in serve
- Add pkg/common.IsTimeoutError helper for timeout detection

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Resolve conflict in pkg/joycode/client.go: keep both defaultTransport
(connection pooling from this PR) and envOr (configurable BaseURL from
upstream vibe-coding-labs#24).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
CherryStudio and other OpenAI-compatible clients hang in "generating"
state after the upstream finishes because:

1. The OpenAI streaming path only forwards upstream lines — if the
   upstream omits `data: [DONE]` (premature close, certain error paths),
   the client never receives the stream terminator. Now we track
   `sawDone` and always send a final `data: [DONE]\n\n` if it wasn't
   seen.

2. The heartbeat goroutine can race with the main goroutine writing to
   the same http.ResponseWriter. `close(stopHeartbeat)` signals exit but
   doesn't wait for the goroutine to actually stop — if it's mid-write
   when the main goroutine starts forwarding upstream data, the writes
   interleave and corrupt the SSE stream. Now we use a `heartbeatDone`
   channel and `<-heartbeatDone` to ensure the heartbeat goroutine has
   fully exited before any downstream writes begin.

Applied to all three streaming paths: OpenAI, Anthropic (translated),
and native Anthropic.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants