feat: enhance API and session management with Nacos and Redis integration

- Add Nacos registry for service registration and deregistration.
- Implement Redis registry for session management with heartbeat and session claiming.
- Improve completion service with session handling and request validation.
- Enhance WebSocket handling for completion requests with JSON-RPC support.
- Add tests for new registry implementations and completion manager functionalities.
- Refactor existing code for better readability and maintainability.
This commit is contained in:
2026-02-15 17:46:34 +08:00
parent 57afb90bc0
commit 3284ce07c7
22 changed files with 1863 additions and 87 deletions

View File

@@ -12,17 +12,18 @@ import (
)
type RedisRegistryConfig struct {
Addr string
Password string
DB int
KeyPrefix string
InstanceID string
InstanceEndpoint string
SessionTTL time.Duration
InstanceTTL time.Duration
HeartbeatInterval time.Duration
Addr string // Redis 地址,例如 127.0.0.1:6379。
Password string // Redis 密码,可为空。
DB int // Redis 数据库编号。
KeyPrefix string // 键前缀,隔离不同环境/业务。
InstanceID string // 当前实例唯一 ID。
InstanceEndpoint string // 当前实例对外访问地址。
SessionTTL time.Duration // 会话键的过期时间。
InstanceTTL time.Duration // 实例元数据过期时间。
HeartbeatInterval time.Duration // 实例心跳刷新周期。
}
// RedisRegistry 负责在 Redis 中维护实例心跳与会话归属。
type RedisRegistry struct {
client *redis.Client
@@ -37,6 +38,7 @@ type RedisRegistry struct {
stopOnce sync.Once
}
// claimSessionScript 原子地抢占/续租会话,返回当前 owner。
var claimSessionScript = redis.NewScript(`
local sessionKey = KEYS[1]
local owner = ARGV[1]
@@ -54,6 +56,7 @@ redis.call('PEXPIRE', sessionKey, ttl)
return existing
`)
// releaseSessionScript 仅允许 owner 主动释放自己的会话。
var releaseSessionScript = redis.NewScript(`
local sessionKey = KEYS[1]
local owner = ARGV[1]
@@ -65,6 +68,7 @@ end
return 0
`)
// NewRedisRegistry 初始化 Redis 客户端并启动实例心跳。
func NewRedisRegistry(ctx context.Context, cfg RedisRegistryConfig) (*RedisRegistry, error) {
if strings.TrimSpace(cfg.Addr) == "" {
return nil, errors.New("redis addr is required")
@@ -113,6 +117,7 @@ func NewRedisRegistry(ctx context.Context, cfg RedisRegistryConfig) (*RedisRegis
return registry, nil
}
// ClaimSession 尝试声明会话归属,并解析 owner 对应的实例地址。
func (r *RedisRegistry) ClaimSession(
ctx context.Context,
language string,
@@ -147,6 +152,7 @@ func (r *RedisRegistry) ClaimSession(
return owner, endpoint, nil
}
// ReleaseSession 释放当前实例持有的会话键。
func (r *RedisRegistry) ReleaseSession(ctx context.Context, language, sessionID string) error {
key := r.sessionKey(language, sessionID)
if _, err := releaseSessionScript.Run(ctx, r.client, []string{key}, r.instanceID).Result(); err != nil {
@@ -155,6 +161,7 @@ func (r *RedisRegistry) ReleaseSession(ctx context.Context, language, sessionID
return nil
}
// Close 停止心跳并关闭 Redis 连接。
func (r *RedisRegistry) Close() error {
r.stopOnce.Do(func() {
close(r.stopCh)
@@ -162,6 +169,7 @@ func (r *RedisRegistry) Close() error {
return r.client.Close()
}
// heartbeatLoop 周期性刷新实例元数据,维持实例在线状态。
func (r *RedisRegistry) heartbeatLoop() {
ticker := time.NewTicker(r.heartbeatInterval)
defer ticker.Stop()
@@ -178,6 +186,7 @@ func (r *RedisRegistry) heartbeatLoop() {
}
}
// refreshInstance 更新实例 endpoint 和 TTL。
func (r *RedisRegistry) refreshInstance(ctx context.Context) error {
key := r.instanceKey(r.instanceID)
now := time.Now().UTC().Format(time.RFC3339Nano)
@@ -193,6 +202,7 @@ func (r *RedisRegistry) refreshInstance(ctx context.Context) error {
return nil
}
// resolveInstanceEndpoint 根据实例 ID 读取其对外地址。
func (r *RedisRegistry) resolveInstanceEndpoint(ctx context.Context, ownerID string) (string, error) {
key := r.instanceKey(ownerID)
endpoint, err := r.client.HGet(ctx, key, "endpoint").Result()
@@ -213,6 +223,7 @@ func (r *RedisRegistry) instanceKey(instanceID string) string {
return fmt.Sprintf("%s:instances:%s", r.keyPrefix, normalizePart(instanceID))
}
// normalizePart 规范化 Redis key 片段,避免空字符串破坏 key 结构。
func normalizePart(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {