Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 29 additions & 16 deletions docs/build.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ WARN account@example.com 账号切换 | 模型=gemini-3.7-flash
| `账户添加 | 1/2`、`2/2`、`账户添加完成` | 隔离登录、保存认证状态、同步模型目录 |
| `账户登录 | 1/2`、`2/2`、`账户登录完成` | 更新当前账户认证材料与 Worker 实例 |
| `账户配置已更新`、`账户已删除` | 账户配置与运行对象已更新 |
| `账户写代码助手应用删除失败` | 删除账户前未能删除该账户的写代码助手应用,账户照常删除,原始错误保留 |
| `账户模型目录同步完成` | 新目录已写入当前模型目录和账户调度状态 |
| `账户模型目录等待重试` | 当前同步失败,运行期重试继续处理 |

Expand Down
209 changes: 157 additions & 52 deletions internal/aistudio/assistant.go

Large diffs are not rendered by default.

118 changes: 88 additions & 30 deletions internal/aistudio/assistant_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const (
assistantAppletKind = "assistant-applet"
// assistantAppletIdle 是写代码助手应用闲置后被巡检删除的时长
assistantAppletIdle = time.Hour
// assistantAppletDeleteAttempts 是应用服务以 HTTP 500 拒绝删除写代码助手应用时的最多尝试次数
assistantAppletDeleteAttempts = 10
)

// assistantLink 是对话中一组连续的模型轮或其余轮次结束处的累积摘要
Expand All @@ -41,10 +43,10 @@ type assistantSession struct {
applet string
// key 是可继续时在 tips 中的键
key assistantSessionKey
// transcribed 表示应用中有以 user 轮转录的模型历史轮
transcribed bool
busy bool
usedAt time.Time
// history 是新建应用时转录在自定义指令中的开头分组数
history int
busy bool
usedAt time.Time
}

// assistantSessions 是本进程创建的写代码助手应用与其中可继续的对话
Expand Down Expand Up @@ -116,16 +118,18 @@ func canonicalJSON(raw json.RawMessage) string {
return string(encoded)
}

// findLocked 在 links 中从后往前找到之后还有新轮次、且本进程保存过的模型回复,返回所在会话、键与新轮次的开头
// findLocked 在 links 最后一组之前最近的模型轮是本进程保存过的回复时,返回所在会话、键与之后新轮次的开头
func (store *assistantSessions) findLocked(apiKey string, links []assistantLink) (*assistantSession, assistantSessionKey, int) {
for index := len(links) - 2; index >= 0; index-- {
if !links[index].model {
continue
}
key := assistantSessionKey{apiKey: apiKey, digest: links[index].digest}
if session := store.tips[key]; session != nil {
return session, key, links[index].end
}
index := len(links) - 2
if index >= 0 && !links[index].model {
index--
}
if index < 0 {
return nil, assistantSessionKey{}, 0
}
key := assistantSessionKey{apiKey: apiKey, digest: links[index].digest}
if session := store.tips[key]; session != nil {
return session, key, links[index].end
}
return nil, assistantSessionKey{}, 0
}
Expand Down Expand Up @@ -157,17 +161,17 @@ func (store *assistantSessions) claim(apiKey string, accountID string, links []a
return session, start
}

// open 记录新建的应用,会话为进行中
func (store *assistantSessions) open(accountID string, applet string) *assistantSession {
// open 记录新建的应用与转录在自定义指令中的开头分组数,会话为进行中
func (store *assistantSessions) open(accountID string, applet string, history int) *assistantSession {
store.mu.Lock()
defer store.mu.Unlock()
session := &assistantSession{accountID: accountID, applet: applet, busy: true}
session := &assistantSession{accountID: accountID, applet: applet, history: history, busy: true}
store.applets[applet] = session
return session
}

// finish 结束会话上的请求:key 不为空时保存为可继续,否则放弃会话,应用留待巡检删除
func (store *assistantSessions) finish(session *assistantSession, key *assistantSessionKey, transcribed bool) {
func (store *assistantSessions) finish(session *assistantSession, key *assistantSessionKey) {
store.mu.Lock()
defer store.mu.Unlock()
session.busy = false
Expand All @@ -176,7 +180,6 @@ func (store *assistantSessions) finish(session *assistantSession, key *assistant
return
}
session.key = *key
session.transcribed = transcribed
session.usedAt = time.Now()
store.tips[*key] = session
}
Expand All @@ -192,14 +195,30 @@ func (store *assistantSessions) expire(applets []string, now time.Time) []string
continue
}
if session != nil {
delete(store.tips, session.key)
delete(store.applets, applet)
store.dropLocked(session)
}
idle = append(idle, applet)
}
return idle
}

// forget 放弃应用上的会话
func (store *assistantSessions) forget(applet string) {
store.mu.Lock()
defer store.mu.Unlock()
if session := store.applets[applet]; session != nil {
store.dropLocked(session)
}
}

// dropLocked 移除会话的应用记录与仍指向该会话的可继续位置
func (store *assistantSessions) dropLocked(session *assistantSession) {
if store.tips[session.key] == session {
delete(store.tips, session.key)
}
delete(store.applets, session.applet)
}

// AssistantService 是可以查询写代码助手对话所在账户的生成服务
type AssistantService interface {
AssistantAccount(context.Context, GenerateRequest) string
Expand Down Expand Up @@ -233,17 +252,56 @@ func (s *PooledService) ReclaimIdleAssistantApplets(ctx context.Context) error {
return reclaimErr
}

// deleteAssistantApplet 删除写代码助手应用并解除账户绑定,应用已不存在时同样解除绑定
func (s *PooledService) deleteAssistantApplet(ctx context.Context, accountID string, applet string) error {
body, _ := json.Marshal([]any{applet})
response, err := s.client.do(ctx, "DeleteApplet", accountID, "", body, false)
if err == nil {
_, err = io.Copy(io.Discard, response.Body)
err = errors.Join(err, response.Body.Close())
// DeleteAccountAssistantApplets 等账户上进行中的请求结束后独占账户,删除账户记录的全部写代码助手应用并解除绑定
func (s *PooledService) DeleteAccountAssistantApplets(ctx context.Context, accountID string) error {
if len(s.pool.resourcesOfKind(assistantAppletKind)[accountID]) == 0 {
return nil
}
lease, err := s.pool.AcquireAccount(ctx, accountID)
if err != nil {
return err
}
var rpcErr *RPCError
if err != nil && !(errors.As(err, &rpcErr) && rpcErr.StatusCode == http.StatusNotFound) {
return fmt.Errorf("删除写代码助手应用 %s: %w", applet, err)
leaseCtx := ContextWithAccountLease(ctx, lease)
var deleteErr error
for _, applet := range s.pool.resourcesOfKind(assistantAppletKind)[accountID] {
deleteErr = errors.Join(deleteErr, s.deleteAssistantApplet(leaseCtx, accountID, applet))
}
return errors.Join(deleteErr, lease.Release())
}

// deleteAssistantApplet 删除写代码助手应用,放弃其上的会话并解除账户绑定
func (s *PooledService) deleteAssistantApplet(ctx context.Context, accountID string, applet string) error {
if err := s.client.deleteAssistantApplet(ctx, accountID, applet); err != nil {
return err
}
s.client.assistants.forget(applet)
return s.pool.UnbindResource(applet)
}

// deleteAssistantApplet 经应用服务删除写代码助手应用,应用已不存在时同样返回 nil;生成刚结束的几秒内应用服务以 HTTP 500 拒绝删除,期间每秒重试
func (c *Client) deleteAssistantApplet(ctx context.Context, accountID string, applet string) error {
body, _ := json.Marshal([]any{applet})
for attempt := 1; ; attempt++ {
rpc := newRPCRequest("DeleteApplet", accountID, "", body, false)
rpc.URL = appletServiceRPCBase + rpc.Method
response, err := c.send(ctx, rpc)
if err == nil {
_, err = io.Copy(io.Discard, response.Body)
err = errors.Join(err, response.Body.Close())
}
var rpcErr *RPCError
if err == nil || errors.As(err, &rpcErr) && rpcErr.StatusCode == http.StatusNotFound {
return nil
}
if rpcErr == nil || rpcErr.StatusCode != http.StatusInternalServerError || attempt == assistantAppletDeleteAttempts {
return fmt.Errorf("删除写代码助手应用 %s: %w", applet, err)
}
timer := time.NewTimer(time.Second)
select {
case <-ctx.Done():
timer.Stop()
return fmt.Errorf("删除写代码助手应用 %s: %w", applet, errors.Join(err, ctx.Err()))
case <-timer.C:
}
}
}
14 changes: 10 additions & 4 deletions internal/aistudio/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
const (
// MakerSuiteRPCBase 是 AI Studio RPC 根地址
MakerSuiteRPCBase = "https://alkalimakersuite-pa.clients6.google.com/$rpc/google.internal.alkali.applications.makersuite.v1.MakerSuiteService/"
// appletServiceRPCBase 是 AI Studio 应用服务 RPC 根地址
appletServiceRPCBase = "https://alkalimakersuiteapplets.pa.clients6.google.com/$rpc/google.internal.alkali.applications.makersuite.v1.applets.AppletService/"
// JSONProtobufContentType 是 MakerSuite 使用的数组协议媒体类型
JSONProtobufContentType = "application/json+protobuf"
)
Expand Down Expand Up @@ -154,13 +156,17 @@ func (e *RPCError) RetryAt(now time.Time) time.Time {
}

func (c *Client) do(ctx context.Context, method string, accountID string, requestID string, body []byte, streaming bool) (*RPCResponse, error) {
rpc := newRPCRequest(method, accountID, requestID, body, streaming)
c.applyBenefitTier(method, accountID, rpc.Header)
return c.send(ctx, newRPCRequest(method, accountID, requestID, body, streaming))
}

// send 发送 RPC 请求并校验响应状态与媒体类型
func (c *Client) send(ctx context.Context, rpc RPCRequest) (*RPCResponse, error) {
c.applyBenefitTier(rpc.Method, rpc.AccountID, rpc.Header)
response, err := c.transport.Do(ctx, rpc)
if err != nil {
return nil, fmt.Errorf("发送 AI Studio %s: %w", method, err)
return nil, fmt.Errorf("发送 AI Studio %s: %w", rpc.Method, err)
}
return validateRPCResponse(method, response)
return validateRPCResponse(rpc.Method, response)
}

func (c *Client) doProtected(ctx context.Context, request GenerateRequest, body []byte) (*RPCResponse, error) {
Expand Down
5 changes: 4 additions & 1 deletion internal/app/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,11 +522,14 @@ func (admin *runtimeAdmin) UpdateAccount(ctx context.Context, accountID string,
return admin.account(account.ID)
}

func (admin *runtimeAdmin) DeleteAccount(_ context.Context, accountID string) error {
func (admin *runtimeAdmin) DeleteAccount(ctx context.Context, accountID string) error {
account, err := admin.account(accountID)
if err != nil {
return err
}
if err := admin.service.catalog.DeleteAccountAssistantApplets(ctx, accountID); err != nil {
admin.requests.log("auth", "WARN", fmt.Sprintf("账户写代码助手应用删除失败 | 账户=%s | 原因: %v", account.Label, err))
}
defer admin.syncModelCache()
err = admin.service.changeModels(func() error {
_, removeErr := admin.pool.Remove(accountID, func(account *aistudio.Account) error {
Expand Down
1 change: 1 addition & 0 deletions internal/app/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ type modelCatalogService interface {
RefreshAccountState(context.Context, string, bool) (aistudio.AccountRefresh, error)
ReclaimExpiredResources(context.Context) error
ReclaimIdleAssistantApplets(context.Context) error
DeleteAccountAssistantApplets(context.Context, string) error
CachedModels() []aistudio.Model
}

Expand Down