mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
ci: add go lint and race test workflow (#187)
This commit is contained in:
@@ -148,7 +148,7 @@ type gatewayModelConfig struct {
|
||||
ModelID string `json:"modelId"`
|
||||
ClientType string `json:"clientType"`
|
||||
Input []string `json:"input"`
|
||||
APIKey string `json:"apiKey"`
|
||||
APIKey string `json:"apiKey"` //nolint:gosec // intentional: forwarded to agent gateway for model authentication
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Reasoning *gatewayReasoningConfig `json:"reasoning,omitempty"`
|
||||
}
|
||||
@@ -160,7 +160,7 @@ type gatewayIdentity struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
CurrentPlatform string `json:"currentPlatform,omitempty"`
|
||||
ConversationType string `json:"conversationType,omitempty"`
|
||||
SessionToken string `json:"sessionToken,omitempty"`
|
||||
SessionToken string `json:"sessionToken,omitempty"` //nolint:gosec // intentional: session token forwarded to agent gateway for channel reply routing
|
||||
}
|
||||
|
||||
type gatewaySkill struct {
|
||||
@@ -286,13 +286,13 @@ type resolvedContext struct {
|
||||
|
||||
func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (resolvedContext, error) {
|
||||
if strings.TrimSpace(req.Query) == "" && len(req.Attachments) == 0 {
|
||||
return resolvedContext{}, fmt.Errorf("query or attachments is required")
|
||||
return resolvedContext{}, errors.New("query or attachments is required")
|
||||
}
|
||||
if strings.TrimSpace(req.BotID) == "" {
|
||||
return resolvedContext{}, fmt.Errorf("bot id is required")
|
||||
return resolvedContext{}, errors.New("bot id is required")
|
||||
}
|
||||
if strings.TrimSpace(req.ChatID) == "" {
|
||||
return resolvedContext{}, fmt.Errorf("chat id is required")
|
||||
return resolvedContext{}, errors.New("chat id is required")
|
||||
}
|
||||
|
||||
skipHistory := req.MaxContextLoadTime < 0
|
||||
@@ -360,7 +360,7 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
|
||||
return resolvedContext{}, loadErr
|
||||
}
|
||||
loaded = pruneHistoryForGateway(loaded)
|
||||
messages = trimMessagesByTokens(loaded, historyBudget)
|
||||
messages = trimMessagesByTokens(r.logger, loaded, historyBudget)
|
||||
r.logger.Debug("context trim result",
|
||||
slog.Int("loaded_messages", len(loaded)),
|
||||
slog.Int("kept_messages", len(messages)),
|
||||
@@ -508,10 +508,10 @@ func (r *Resolver) Chat(ctx context.Context, req conversation.ChatRequest) (conv
|
||||
// TriggerSchedule executes a scheduled command through the agent gateway trigger-schedule endpoint.
|
||||
func (r *Resolver) TriggerSchedule(ctx context.Context, botID string, payload schedule.TriggerPayload, token string) error {
|
||||
if strings.TrimSpace(botID) == "" {
|
||||
return fmt.Errorf("bot id is required")
|
||||
return errors.New("bot id is required")
|
||||
}
|
||||
if strings.TrimSpace(payload.Command) == "" {
|
||||
return fmt.Errorf("schedule command is required")
|
||||
return errors.New("schedule command is required")
|
||||
}
|
||||
|
||||
req := conversation.ChatRequest{
|
||||
@@ -554,7 +554,7 @@ func (r *Resolver) TriggerSchedule(ctx context.Context, botID string, payload sc
|
||||
// TriggerHeartbeat executes a heartbeat check through the agent gateway trigger-heartbeat endpoint.
|
||||
func (r *Resolver) TriggerHeartbeat(ctx context.Context, botID string, payload heartbeat.TriggerPayload, token string) (heartbeat.TriggerResult, error) {
|
||||
if strings.TrimSpace(botID) == "" {
|
||||
return heartbeat.TriggerResult{}, fmt.Errorf("bot id is required")
|
||||
return heartbeat.TriggerResult{}, errors.New("bot id is required")
|
||||
}
|
||||
|
||||
// If a dedicated heartbeat model is configured, use it instead of the
|
||||
@@ -691,11 +691,11 @@ func (r *Resolver) postChat(ctx context.Context, payload gatewayRequest, token s
|
||||
httpReq.Header.Set("Authorization", token)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(httpReq)
|
||||
resp, err := r.httpClient.Do(httpReq) //nolint:gosec // G704: URL is from operator-configured agent gateway, not user input
|
||||
if err != nil {
|
||||
return gatewayResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -727,11 +727,11 @@ func (r *Resolver) postTriggerSchedule(ctx context.Context, payload triggerSched
|
||||
httpReq.Header.Set("Authorization", token)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(httpReq)
|
||||
resp, err := r.httpClient.Do(httpReq) //nolint:gosec // G704: URL is from operator-configured agent gateway, not user input
|
||||
if err != nil {
|
||||
return gatewayResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -763,11 +763,11 @@ func (r *Resolver) postTriggerHeartbeat(ctx context.Context, payload triggerHear
|
||||
httpReq.Header.Set("Authorization", token)
|
||||
}
|
||||
|
||||
resp, err := r.httpClient.Do(httpReq)
|
||||
resp, err := r.httpClient.Do(httpReq) //nolint:gosec // G704: URL is from operator-configured agent gateway, not user input
|
||||
if err != nil {
|
||||
return gatewayResponse{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -803,12 +803,12 @@ func (r *Resolver) streamChat(ctx context.Context, payload gatewayRequest, req c
|
||||
httpReq.Header.Set("Authorization", req.Token)
|
||||
}
|
||||
|
||||
resp, err := r.streamingClient.Do(httpReq)
|
||||
resp, err := r.streamingClient.Do(httpReq) //nolint:gosec // G704: URL is from operator-configured agent gateway, not user input
|
||||
if err != nil {
|
||||
r.logger.Error("gateway stream connect failed", slog.String("url", url), slog.Any("error", err))
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
errBody, _ := io.ReadAll(resp.Body)
|
||||
@@ -982,13 +982,14 @@ func (r *Resolver) prepareGatewayAttachments(ctx context.Context, req conversati
|
||||
transport = gatewayTransportInlineDataURL
|
||||
} else {
|
||||
rawURL := strings.TrimSpace(raw.URL)
|
||||
if isDataURL(rawURL) {
|
||||
switch {
|
||||
case isDataURL(rawURL):
|
||||
payload = rawURL
|
||||
transport = gatewayTransportInlineDataURL
|
||||
} else if isLikelyPublicURL(rawURL) {
|
||||
case isLikelyPublicURL(rawURL):
|
||||
payload = rawURL
|
||||
transport = gatewayTransportPublicURL
|
||||
} else if rawURL != "" && fallbackPath == "" {
|
||||
case rawURL != "" && fallbackPath == "":
|
||||
fallbackPath = rawURL
|
||||
}
|
||||
}
|
||||
@@ -1080,7 +1081,7 @@ func (r *Resolver) inlineImageAttachmentAssetIfNeeded(ctx context.Context, botID
|
||||
|
||||
func (r *Resolver) inlineAssetAsDataURL(ctx context.Context, botID, contentHash, attachmentType, fallbackMime string) (string, string, error) {
|
||||
if r == nil || r.assetLoader == nil {
|
||||
return "", "", fmt.Errorf("gateway asset loader not configured")
|
||||
return "", "", errors.New("gateway asset loader not configured")
|
||||
}
|
||||
reader, assetMime, err := r.assetLoader.OpenForGateway(ctx, botID, contentHash)
|
||||
if err != nil {
|
||||
@@ -1102,15 +1103,15 @@ func (r *Resolver) inlineAssetAsDataURL(ctx context.Context, botID, contentHash,
|
||||
|
||||
func encodeReaderAsDataURL(reader io.Reader, maxBytes int64, attachmentType, fallbackMime string) (string, string, error) {
|
||||
if reader == nil {
|
||||
return "", "", fmt.Errorf("reader is required")
|
||||
return "", "", errors.New("reader is required")
|
||||
}
|
||||
if maxBytes <= 0 {
|
||||
return "", "", fmt.Errorf("max bytes must be greater than 0")
|
||||
return "", "", errors.New("max bytes must be greater than 0")
|
||||
}
|
||||
limited := &io.LimitedReader{R: reader, N: maxBytes + 1}
|
||||
head := make([]byte, 512)
|
||||
n, err := limited.Read(head)
|
||||
if err != nil && err != io.EOF {
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return "", "", fmt.Errorf("read asset: %w", err)
|
||||
}
|
||||
head = head[:n]
|
||||
@@ -1229,7 +1230,7 @@ func estimateMessageTokens(msg conversation.ModelMessage) int {
|
||||
return len(text) / 4
|
||||
}
|
||||
|
||||
func trimMessagesByTokens(messages []messageWithUsage, maxTokens int) []conversation.ModelMessage {
|
||||
func trimMessagesByTokens(log *slog.Logger, messages []messageWithUsage, maxTokens int) []conversation.ModelMessage {
|
||||
if maxTokens <= 0 || len(messages) == 0 {
|
||||
result := make([]conversation.ModelMessage, len(messages))
|
||||
for i, m := range messages {
|
||||
@@ -1263,14 +1264,16 @@ func trimMessagesByTokens(messages []messageWithUsage, maxTokens int) []conversa
|
||||
cutoff++
|
||||
}
|
||||
|
||||
slog.Debug("trimMessagesByTokens",
|
||||
slog.Int("total_messages", len(messages)),
|
||||
slog.Int("messages_with_usage", messagesWithUsage),
|
||||
slog.Int("accumulated_output_tokens", totalTokens),
|
||||
slog.Int("max_tokens", maxTokens),
|
||||
slog.Int("cutoff_index", cutoff),
|
||||
slog.Int("kept_messages", len(messages)-cutoff),
|
||||
)
|
||||
if log != nil {
|
||||
log.Debug("trimMessagesByTokens",
|
||||
slog.Int("total_messages", len(messages)),
|
||||
slog.Int("messages_with_usage", messagesWithUsage),
|
||||
slog.Int("accumulated_output_tokens", totalTokens),
|
||||
slog.Int("max_tokens", maxTokens),
|
||||
slog.Int("cutoff_index", cutoff),
|
||||
slog.Int("kept_messages", len(messages)-cutoff),
|
||||
)
|
||||
}
|
||||
|
||||
result := make([]conversation.ModelMessage, 0, len(messages)-cutoff)
|
||||
for _, m := range messages[cutoff:] {
|
||||
@@ -1332,7 +1335,7 @@ func (r *Resolver) persistUserMessage(ctx context.Context, req conversation.Chat
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(req.BotID) == "" {
|
||||
return fmt.Errorf("bot id is required for persistence")
|
||||
return errors.New("bot id is required for persistence")
|
||||
}
|
||||
text := strings.TrimSpace(req.Query)
|
||||
if text == "" && len(req.Attachments) == 0 {
|
||||
@@ -1676,7 +1679,7 @@ func toProviderMessages(messages []conversation.ModelMessage) []memprovider.Mess
|
||||
|
||||
func (r *Resolver) selectChatModel(ctx context.Context, req conversation.ChatRequest, botSettings settings.Settings, cs conversation.Settings) (models.GetResponse, sqlc.LlmProvider, error) {
|
||||
if r.modelsService == nil {
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, fmt.Errorf("models service not configured")
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, errors.New("models service not configured")
|
||||
}
|
||||
modelID := strings.TrimSpace(req.Model)
|
||||
providerFilter := strings.TrimSpace(req.Provider)
|
||||
@@ -1691,7 +1694,7 @@ func (r *Resolver) selectChatModel(ctx context.Context, req conversation.ChatReq
|
||||
}
|
||||
|
||||
if modelID == "" {
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, fmt.Errorf("chat model not configured: specify model in request or bot settings")
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, errors.New("chat model not configured: specify model in request or bot settings")
|
||||
}
|
||||
|
||||
if providerFilter == "" {
|
||||
@@ -1717,7 +1720,7 @@ func (r *Resolver) selectChatModel(ctx context.Context, req conversation.ChatReq
|
||||
func (r *Resolver) fetchChatModel(ctx context.Context, modelID string) (models.GetResponse, sqlc.LlmProvider, error) {
|
||||
modelRef := strings.TrimSpace(modelID)
|
||||
if modelRef == "" {
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, fmt.Errorf("model id is required")
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, errors.New("model id is required")
|
||||
}
|
||||
|
||||
// Support both model UUID and model_id slug. UUID-formatted slugs still
|
||||
@@ -1740,7 +1743,7 @@ func (r *Resolver) fetchChatModel(ctx context.Context, modelID string) (models.G
|
||||
|
||||
resolved:
|
||||
if model.Type != models.ModelTypeChat {
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, fmt.Errorf("model is not a chat model")
|
||||
return models.GetResponse{}, sqlc.LlmProvider{}, errors.New("model is not a chat model")
|
||||
}
|
||||
prov, err := models.FetchProviderByID(ctx, r.queries, model.LlmProviderID)
|
||||
if err != nil {
|
||||
@@ -1792,7 +1795,7 @@ func (r *Resolver) markInboxRead(ctx context.Context, botID string, ids []string
|
||||
|
||||
func (r *Resolver) loadBotSettings(ctx context.Context, botID string) (settings.Settings, error) {
|
||||
if r.settingsService == nil {
|
||||
return settings.Settings{}, fmt.Errorf("settings service not configured")
|
||||
return settings.Settings{}, errors.New("settings service not configured")
|
||||
}
|
||||
return r.settingsService.GetBot(ctx, botID)
|
||||
}
|
||||
@@ -1841,16 +1844,6 @@ func parseLoopDetectionEnabledFromMetadata(payload []byte) bool {
|
||||
|
||||
// --- utility ---
|
||||
|
||||
func normalizeClientType(clientType string) (string, error) {
|
||||
ct := strings.ToLower(strings.TrimSpace(clientType))
|
||||
switch ct {
|
||||
case "openai-responses", "openai-completions", "anthropic-messages", "google-generative-ai":
|
||||
return ct, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported agent gateway client type: %s", clientType)
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeMessages(messages []conversation.ModelMessage) []conversation.ModelMessage {
|
||||
cleaned := make([]conversation.ModelMessage, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
@@ -1903,15 +1896,6 @@ func dedup(items []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, v := range values {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func coalescePositiveInt(values ...int) int {
|
||||
for _, v := range values {
|
||||
if v > 0 {
|
||||
@@ -1944,7 +1928,7 @@ func truncate(s string, n int) string {
|
||||
|
||||
func parseResolverUUID(id string) (pgtype.UUID, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return pgtype.UUID{}, fmt.Errorf("empty id")
|
||||
return pgtype.UUID{}, errors.New("empty id")
|
||||
}
|
||||
return db.ParseUUID(id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user