feat(chat): improve error handling for chat operations
- Added centralized error handling for "not found" scenarios in GetChat, UpdateChat, DeleteChat, and GetMessages functions, enhancing user feedback for missing resources. - Introduced isNotFoundError and respondNotFound utility functions to streamline error responses across chat-related operations. - Updated the setting package to include new endpoints for setup status and user preferences, improving API functionality and user experience.
This commit is contained in:
parent
cf0ebd5601
commit
01daa783e0
5 changed files with 785 additions and 8 deletions
|
|
@ -101,6 +101,10 @@ func GetChat(c *gin.Context) {
|
|||
// Check permission
|
||||
hasPermission, err := checkChatPermission(chatStore, authInfo, chatID, true)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -121,16 +125,10 @@ func GetChat(c *gin.Context) {
|
|||
// Get chat
|
||||
chat, err := chatStore.GetChat(chatID)
|
||||
if err != nil {
|
||||
// Check if it's a "not found" error
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Chat not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -184,6 +182,10 @@ func UpdateChat(c *gin.Context) {
|
|||
// Check permission (write access)
|
||||
hasPermission, err := checkChatPermission(chatStore, authInfo, chatID, false)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -229,6 +231,10 @@ func UpdateChat(c *gin.Context) {
|
|||
|
||||
// Update chat
|
||||
if err := chatStore.UpdateChat(chatID, updates); err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -274,6 +280,10 @@ func DeleteChat(c *gin.Context) {
|
|||
// Check permission (write access)
|
||||
hasPermission, err := checkChatPermission(chatStore, authInfo, chatID, false)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -293,6 +303,10 @@ func DeleteChat(c *gin.Context) {
|
|||
|
||||
// Delete chat
|
||||
if err := chatStore.DeleteChat(chatID); err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -342,6 +356,10 @@ func GetMessages(c *gin.Context) {
|
|||
// Check permission (read access)
|
||||
hasPermission, err := checkChatPermission(chatStore, authInfo, chatID, true)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -365,6 +383,10 @@ func GetMessages(c *gin.Context) {
|
|||
// Get messages
|
||||
messages, err := chatStore.GetMessages(chatID, filter)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
respondNotFound(c, chatID)
|
||||
return
|
||||
}
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: err.Error(),
|
||||
|
|
@ -551,6 +573,18 @@ func buildMessageFilter(c *gin.Context) storetypes.MessageFilter {
|
|||
return filter
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "not found")
|
||||
}
|
||||
|
||||
func respondNotFound(c *gin.Context, chatID string) {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: "resource_not_found",
|
||||
ErrorDescription: "Chat " + chatID + " not found",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusNotFound, errorResp)
|
||||
}
|
||||
|
||||
// checkChatPermission checks if the user has permission to access the chat
|
||||
// readable: true for read access, false for write access
|
||||
func checkChatPermission(chatStore storetypes.ChatStore, authInfo *oauthtypes.AuthorizedInfo, chatID string, readable bool) (bool, error) {
|
||||
|
|
|
|||
92
openapi/setting/preference.go
Normal file
92
openapi/setting/preference.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/setting"
|
||||
)
|
||||
|
||||
const preferenceNS = "preference"
|
||||
|
||||
func preferenceScope(info *oauthTypes.AuthorizedInfo) setting.ScopeID {
|
||||
return setting.ScopeID{Scope: setting.ScopeUser, UserID: info.UserID}
|
||||
}
|
||||
|
||||
// handlePreferenceGet returns the current user's preference.
|
||||
// GET /setting/preference
|
||||
func handlePreferenceGet(c *gin.Context) {
|
||||
info := authorized.GetInfo(c)
|
||||
|
||||
if setting.Global == nil {
|
||||
response.RespondWithSuccess(c, http.StatusOK, PreferenceData{})
|
||||
return
|
||||
}
|
||||
|
||||
merged, _ := setting.Global.GetMerged(info.UserID, info.TeamID, preferenceNS)
|
||||
data := preferenceFromMap(merged)
|
||||
response.RespondWithSuccess(c, http.StatusOK, data)
|
||||
}
|
||||
|
||||
// handlePreferenceUpdate partially updates the current user's preference.
|
||||
// PUT /setting/preference
|
||||
func handlePreferenceUpdate(c *gin.Context) {
|
||||
info := authorized.GetInfo(c)
|
||||
|
||||
var body PreferenceData
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
respondError(c, http.StatusInternalServerError, "setting registry not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
scope := preferenceScope(info)
|
||||
existing, _ := setting.Global.Get(scope, preferenceNS)
|
||||
|
||||
m := make(map[string]interface{})
|
||||
for k, v := range existing {
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
// Marshal the body to a map so only non-nil fields are included
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
var bodyMap map[string]interface{}
|
||||
json.Unmarshal(bodyBytes, &bodyMap)
|
||||
for k, v := range bodyMap {
|
||||
m[k] = v
|
||||
}
|
||||
|
||||
if _, err := setting.Global.Set(scope, preferenceNS, m); err != nil {
|
||||
respondError(c, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
merged, _ := setting.Global.GetMerged(info.UserID, info.TeamID, preferenceNS)
|
||||
result := preferenceFromMap(merged)
|
||||
response.RespondWithSuccess(c, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func preferenceFromMap(m map[string]interface{}) PreferenceData {
|
||||
data := PreferenceData{}
|
||||
if m == nil {
|
||||
return data
|
||||
}
|
||||
if v, ok := m["email_notification"].(bool); ok {
|
||||
data.EmailNotification = &v
|
||||
}
|
||||
if v, ok := m["banner_dismissed"].(bool); ok {
|
||||
data.BannerDismissed = &v
|
||||
}
|
||||
if v, ok := m["onboarding_completed"].(bool); ok {
|
||||
data.OnboardingCompleted = &v
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
|
@ -74,6 +74,13 @@ func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) {
|
|||
sb.POST("/nodes/:nodeId/images/pull-all", handleSandboxPullAll)
|
||||
sb.DELETE("/nodes/:nodeId/images/:imageId", handleSandboxImageDelete)
|
||||
sb.POST("/nodes/:nodeId/check-docker", handleSandboxCheckDocker)
|
||||
|
||||
group.GET("/setup-status", handleSetupStatus)
|
||||
group.GET("/setup-status/assistant/:id", handleAssistantSetupStatus)
|
||||
|
||||
pref := group.Group("/preference")
|
||||
pref.GET("", handlePreferenceGet)
|
||||
pref.PUT("", handlePreferenceUpdate)
|
||||
}
|
||||
|
||||
// requireOwner checks that the current user is the team owner.
|
||||
|
|
|
|||
608
openapi/setting/setup_status.go
Normal file
608
openapi/setting/setup_status.go
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/agent/assistant"
|
||||
"github.com/yaoapp/yao/llmprovider"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/setting"
|
||||
"github.com/yaoapp/yao/tai"
|
||||
"github.com/yaoapp/yao/tai/registry"
|
||||
)
|
||||
|
||||
// handleSetupStatus aggregates all system-level configuration checkpoints.
|
||||
// GET /setting/setup-status
|
||||
func handleSetupStatus(c *gin.Context) {
|
||||
info := authorized.GetInfo(c)
|
||||
locale := strings.ToLower(c.DefaultQuery("locale", "en-us"))
|
||||
isCN := strings.HasPrefix(locale, "zh")
|
||||
|
||||
checkpoints := make(map[string]Checkpoint, 6)
|
||||
|
||||
checkpoints["llm_default"] = checkLLMDefault(info, isCN)
|
||||
checkpoints["llm_vision"] = checkLLMVision(info, isCN)
|
||||
checkpoints["sandbox_node"] = checkSandboxNode(info, isCN)
|
||||
checkpoints["sandbox_image"] = checkSandboxImage(info, locale, isCN)
|
||||
checkpoints["search"] = checkSearch(info, isCN)
|
||||
checkpoints["smtp"] = checkSMTP(info, isCN)
|
||||
|
||||
completed := true
|
||||
for _, cp := range checkpoints {
|
||||
if cp.Required && cp.Status == "fail" {
|
||||
completed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
bannerDismissed := false
|
||||
onboardingCompleted := false
|
||||
if setting.Global != nil {
|
||||
prefs, _ := setting.Global.GetMerged(info.UserID, info.TeamID, preferenceNS)
|
||||
if prefs != nil {
|
||||
if v, ok := prefs["banner_dismissed"].(bool); ok {
|
||||
bannerDismissed = v
|
||||
}
|
||||
if v, ok := prefs["onboarding_completed"].(bool); ok {
|
||||
onboardingCompleted = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, SetupStatus{
|
||||
Completed: completed,
|
||||
Checkpoints: checkpoints,
|
||||
OnboardingCompleted: onboardingCompleted,
|
||||
BannerDismissed: bannerDismissed,
|
||||
})
|
||||
}
|
||||
|
||||
// handleAssistantSetupStatus checks configuration readiness for a specific assistant.
|
||||
// GET /setting/setup-status/assistant/:id
|
||||
func handleAssistantSetupStatus(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
info := authorized.GetInfo(c)
|
||||
locale := strings.ToLower(c.DefaultQuery("locale", "en-us"))
|
||||
isCN := strings.HasPrefix(locale, "zh")
|
||||
|
||||
cache := assistant.GetCache()
|
||||
var ast *assistant.Assistant
|
||||
if cache != nil {
|
||||
ast, _ = cache.Get(id)
|
||||
}
|
||||
if ast == nil {
|
||||
var err error
|
||||
ast, err = assistant.Get(id)
|
||||
if err != nil || ast == nil {
|
||||
respondError(c, http.StatusNotFound, fmt.Sprintf("assistant %q not found", id))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
checkpoints := make(map[string]Checkpoint)
|
||||
allReady := true
|
||||
|
||||
// connector check
|
||||
cp := checkAssistantConnector(ast, info, isCN)
|
||||
checkpoints["connector"] = cp
|
||||
if cp.Status == "fail" {
|
||||
allReady = false
|
||||
}
|
||||
|
||||
// sandbox check (only if V2 sandbox configured)
|
||||
if ast.HasSandboxV2() {
|
||||
cp := checkAssistantSandbox(ast, info, locale, isCN)
|
||||
checkpoints["sandbox_ready"] = cp
|
||||
if cp.Status == "fail" {
|
||||
allReady = false
|
||||
}
|
||||
}
|
||||
|
||||
// search check (only if uses.search is configured and not disabled)
|
||||
if ast.Uses != nil && ast.Uses.Search != "" && ast.Uses.Search != "disabled" {
|
||||
cp := checkAssistantSearch(ast, info, isCN)
|
||||
checkpoints["search"] = cp
|
||||
if cp.Status == "fail" {
|
||||
allReady = false
|
||||
}
|
||||
}
|
||||
|
||||
name := ast.GetName(locale)
|
||||
if name == "" {
|
||||
name = ast.ID
|
||||
}
|
||||
|
||||
response.RespondWithSuccess(c, http.StatusOK, AssistantSetupStatus{
|
||||
AssistantID: id,
|
||||
AssistantName: name,
|
||||
Ready: allReady,
|
||||
Checkpoints: checkpoints,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System-level checkpoint helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// parseRoleTarget extracts provider key and model ID from a role value.
|
||||
// Supports both map format {"provider":"x","model":"y"} and legacy string "provider::model".
|
||||
func parseRoleTarget(val interface{}) (providerKey, modelID string) {
|
||||
switch v := val.(type) {
|
||||
case map[string]interface{}:
|
||||
providerKey, _ = v["provider"].(string)
|
||||
modelID, _ = v["model"].(string)
|
||||
case string:
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(v, "::", 2)
|
||||
providerKey = parts[0]
|
||||
if len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkLLMDefault(info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: true,
|
||||
Label: "Default Model",
|
||||
Path: "/settings/models",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "默认模型"
|
||||
}
|
||||
|
||||
if setting.Global == nil || llmprovider.Global == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
roles, _ := setting.Global.GetMerged(info.UserID, info.TeamID, llmprovider.RolesNamespace)
|
||||
if roles == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
providerKey, _ := parseRoleTarget(roles["default"])
|
||||
if providerKey == "" {
|
||||
return cp
|
||||
}
|
||||
|
||||
p, err := llmprovider.Global.Get(providerKey)
|
||||
if err != nil || p == nil || !p.Enabled {
|
||||
return cp
|
||||
}
|
||||
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkLLMVision(info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: false,
|
||||
Label: "Vision Model",
|
||||
Path: "/settings/models",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "视觉模型"
|
||||
}
|
||||
|
||||
if setting.Global == nil || llmprovider.Global == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
roles, _ := setting.Global.GetMerged(info.UserID, info.TeamID, llmprovider.RolesNamespace)
|
||||
if roles == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
// Check dedicated vision role first
|
||||
if providerKey, modelID := parseRoleTarget(roles["vision"]); providerKey != "" {
|
||||
if p, err := llmprovider.Global.Get(providerKey); err == nil && p != nil && p.Enabled {
|
||||
for _, m := range p.Models {
|
||||
if m.Enabled && hasCapability(m.Capabilities, "vision") {
|
||||
if modelID == "" || m.ID == modelID {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check if default role has vision capability
|
||||
if providerKey, _ := parseRoleTarget(roles["default"]); providerKey != "" {
|
||||
if p, err := llmprovider.Global.Get(providerKey); err == nil && p != nil && p.Enabled {
|
||||
for _, m := range p.Models {
|
||||
if m.Enabled && hasCapability(m.Capabilities, "vision") {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkSandboxNode(info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: true,
|
||||
Label: "Sandbox Node",
|
||||
Path: "/settings/sandbox",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "沙箱节点"
|
||||
}
|
||||
|
||||
reg := registry.Global()
|
||||
if reg == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
for _, snap := range reg.List() {
|
||||
if snap.Mode != "local" && !sandboxNodeOwnedBy(&snap, info) {
|
||||
continue
|
||||
}
|
||||
if snap.Status == "online" && snap.Capabilities.Docker {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkSandboxImage(info *oauthTypes.AuthorizedInfo, locale string, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: true,
|
||||
Label: "Sandbox Images",
|
||||
Path: "/settings/sandbox",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "沙箱镜像"
|
||||
}
|
||||
|
||||
needed := collectAssistantImages(locale)
|
||||
if len(needed) == 0 {
|
||||
cp.Status = "pass"
|
||||
if isCN {
|
||||
cp.Detail = "无需镜像"
|
||||
} else {
|
||||
cp.Detail = "No images needed"
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
reg := registry.Global()
|
||||
if reg == nil {
|
||||
cp.Detail = fmt.Sprintf("0/%d", len(needed))
|
||||
return cp
|
||||
}
|
||||
|
||||
downloaded := 0
|
||||
for _, snap := range reg.List() {
|
||||
if snap.Mode != "local" && !sandboxNodeOwnedBy(&snap, info) {
|
||||
continue
|
||||
}
|
||||
if snap.Status != "online" || !snap.Capabilities.Docker {
|
||||
continue
|
||||
}
|
||||
|
||||
res, ok := tai.GetResources(snap.TaiID)
|
||||
if !ok || res.Image == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
images, err := res.Image.List(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
tagIndex := make(map[string]bool)
|
||||
for _, img := range images {
|
||||
for _, tag := range img.Tags {
|
||||
tagIndex[tag] = true
|
||||
}
|
||||
}
|
||||
|
||||
for imageRef := range needed {
|
||||
if tagIndex[imageRef] {
|
||||
downloaded++
|
||||
}
|
||||
}
|
||||
break // only check the first usable node
|
||||
}
|
||||
|
||||
if isCN {
|
||||
cp.Detail = fmt.Sprintf("%d/%d 镜像已下载", downloaded, len(needed))
|
||||
} else {
|
||||
cp.Detail = fmt.Sprintf("%d/%d images downloaded", downloaded, len(needed))
|
||||
}
|
||||
if downloaded > 0 {
|
||||
cp.Status = "pass"
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkSearch(info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: false,
|
||||
Label: "Search Provider",
|
||||
Path: "/settings/search",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "搜索服务"
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
for _, preset := range searchPresets {
|
||||
if preset.IsCloud {
|
||||
saved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, cloudNS)
|
||||
if saved != nil {
|
||||
if v, ok := saved["status"].(string); ok && v == "connected" {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
} else {
|
||||
saved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, searchProviderNS(preset.Key))
|
||||
if saved != nil {
|
||||
if v, ok := saved["status"].(string); ok && v == "connected" {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkSMTP(info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: false,
|
||||
Label: "SMTP Email",
|
||||
Path: "/settings/smtp",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "邮件服务"
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
saved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, smtpNS)
|
||||
if saved == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
status, _ := saved["status"].(string)
|
||||
if status == "connected" {
|
||||
if enabled, ok := saved["enabled"].(bool); ok && !enabled {
|
||||
return cp
|
||||
}
|
||||
cp.Status = "pass"
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assistant-level checkpoint helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func checkAssistantConnector(ast *assistant.Assistant, info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: true,
|
||||
Label: "Connector",
|
||||
Path: "/settings/models",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "模型连接"
|
||||
}
|
||||
|
||||
connID := ast.Connector
|
||||
if connID == "" {
|
||||
connID = "default"
|
||||
}
|
||||
|
||||
// Role-based connector: "use::vision", "use::heavy", etc.
|
||||
if strings.HasPrefix(connID, "use::") {
|
||||
roleName := strings.TrimPrefix(connID, "use::")
|
||||
if setting.Global == nil {
|
||||
return cp
|
||||
}
|
||||
roles, _ := setting.Global.GetMerged(info.UserID, info.TeamID, llmprovider.RolesNamespace)
|
||||
if roles == nil {
|
||||
return cp
|
||||
}
|
||||
pk, mid := parseRoleTarget(roles[roleName])
|
||||
if pk == "" {
|
||||
return cp
|
||||
}
|
||||
connID = pk
|
||||
if mid != "" {
|
||||
connID = pk + "::" + mid
|
||||
}
|
||||
}
|
||||
|
||||
// "default" means use the default role
|
||||
if connID == "default" {
|
||||
if setting.Global == nil {
|
||||
return cp
|
||||
}
|
||||
roles, _ := setting.Global.GetMerged(info.UserID, info.TeamID, llmprovider.RolesNamespace)
|
||||
if roles == nil {
|
||||
return cp
|
||||
}
|
||||
pk, mid := parseRoleTarget(roles["default"])
|
||||
if pk == "" {
|
||||
return cp
|
||||
}
|
||||
connID = pk
|
||||
if mid != "" {
|
||||
connID = pk + "::" + mid
|
||||
}
|
||||
}
|
||||
|
||||
parts := strings.SplitN(connID, "::", 2)
|
||||
if llmprovider.Global == nil {
|
||||
return cp
|
||||
}
|
||||
p, err := llmprovider.Global.Get(parts[0])
|
||||
if err != nil || p == nil || !p.Enabled {
|
||||
return cp
|
||||
}
|
||||
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkAssistantSandbox(ast *assistant.Assistant, info *oauthTypes.AuthorizedInfo, locale string, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: true,
|
||||
Label: "Sandbox Ready",
|
||||
Path: "/settings/sandbox",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "沙箱就绪"
|
||||
}
|
||||
|
||||
imageRef := ""
|
||||
if ast.SandboxV2 != nil && ast.SandboxV2.Computer.Image != "" {
|
||||
imageRef = ast.SandboxV2.Computer.Image
|
||||
}
|
||||
if imageRef == "" {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
|
||||
reg := registry.Global()
|
||||
if reg == nil {
|
||||
if isCN {
|
||||
cp.Detail = "沙箱节点未配置"
|
||||
} else {
|
||||
cp.Detail = "No sandbox node configured"
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
dockerNodeFound := false
|
||||
for _, snap := range reg.List() {
|
||||
if snap.Mode != "local" && !sandboxNodeOwnedBy(&snap, info) {
|
||||
continue
|
||||
}
|
||||
if snap.Status != "online" || !snap.Capabilities.Docker {
|
||||
continue
|
||||
}
|
||||
dockerNodeFound = true
|
||||
|
||||
res, ok := tai.GetResources(snap.TaiID)
|
||||
if !ok || res.Image == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
images, err := res.Image.List(ctx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
for _, tag := range img.Tags {
|
||||
if tag == imageRef {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !dockerNodeFound {
|
||||
if isCN {
|
||||
cp.Detail = "Docker 未安装或节点离线"
|
||||
} else {
|
||||
cp.Detail = "Docker not installed or node offline"
|
||||
}
|
||||
} else {
|
||||
if isCN {
|
||||
cp.Detail = "镜像未下载"
|
||||
} else {
|
||||
cp.Detail = "Image not downloaded"
|
||||
}
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func checkAssistantSearch(ast *assistant.Assistant, info *oauthTypes.AuthorizedInfo, isCN bool) Checkpoint {
|
||||
cp := Checkpoint{
|
||||
Required: false,
|
||||
Label: "Search",
|
||||
Path: "/settings/search",
|
||||
Status: "fail",
|
||||
}
|
||||
if isCN {
|
||||
cp.Label = "搜索"
|
||||
}
|
||||
|
||||
if setting.Global == nil {
|
||||
return cp
|
||||
}
|
||||
|
||||
// Check cloud search
|
||||
cloudSaved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, cloudNS)
|
||||
if cloudSaved != nil {
|
||||
if v, ok := cloudSaved["status"].(string); ok && v == "connected" {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
|
||||
// Check any standalone search provider
|
||||
for _, preset := range searchPresets {
|
||||
if preset.IsCloud {
|
||||
continue
|
||||
}
|
||||
saved, _ := setting.Global.GetMerged(info.UserID, info.TeamID, searchProviderNS(preset.Key))
|
||||
if saved != nil {
|
||||
if v, ok := saved["status"].(string); ok && v == "connected" {
|
||||
cp.Status = "pass"
|
||||
return cp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cp
|
||||
}
|
||||
|
||||
func hasCapability(caps []string, target string) bool {
|
||||
for _, c := range caps {
|
||||
if c == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -219,3 +219,39 @@ type SandboxPageData struct {
|
|||
Registry SandboxRegistryConfig `json:"registry"`
|
||||
Images map[string][]SandboxImage `json:"images"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup Status
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type Checkpoint struct {
|
||||
Status string `json:"status"`
|
||||
Required bool `json:"required"`
|
||||
Label string `json:"label"`
|
||||
Path string `json:"path"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type SetupStatus struct {
|
||||
Completed bool `json:"completed"`
|
||||
Checkpoints map[string]Checkpoint `json:"checkpoints"`
|
||||
OnboardingCompleted bool `json:"onboarding_completed"`
|
||||
BannerDismissed bool `json:"banner_dismissed"`
|
||||
}
|
||||
|
||||
type AssistantSetupStatus struct {
|
||||
AssistantID string `json:"assistant_id"`
|
||||
AssistantName string `json:"assistant_name"`
|
||||
Ready bool `json:"ready"`
|
||||
Checkpoints map[string]Checkpoint `json:"checkpoints"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// User Preference
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type PreferenceData struct {
|
||||
EmailNotification *bool `json:"email_notification,omitempty"`
|
||||
BannerDismissed *bool `json:"banner_dismissed,omitempty"`
|
||||
OnboardingCompleted *bool `json:"onboarding_completed,omitempty"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue