sipeed/picoclaw · warning

feishu placeholder: card build failed: %w

Error message

feishu placeholder: card build failed: %w

What it means

FeishuChannel.SendPlaceholder builds a placeholder interactive card via buildMarkdownCard before any API call; this error means that local conversion failed and the underlying build error is wrapped. The placeholder is a cosmetic UX affordance (random 'thinking...' text), so this failure does not affect the real reply path.

Source

Thrown at pkg/channels/feishu/feishu_64.go:307

	}
	return nil
}

// SendPlaceholder implements channels.PlaceholderCapable.
// Sends an interactive card with placeholder text and returns its message ID.
func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
	if !c.bc.Placeholder.Enabled {
		logger.DebugCF("feishu", "Placeholder disabled, skipping", map[string]any{
			"chat_id": chatID,
		})
		return "", nil
	}

	text := c.bc.Placeholder.GetRandomText()

	cardContent, err := buildMarkdownCard(text)
	if err != nil {
		return "", fmt.Errorf("feishu placeholder: card build failed: %w", err)
	}

	req := larkim.NewCreateMessageReqBuilder().
		ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
		Body(larkim.NewCreateMessageReqBodyBuilder().
			ReceiveId(chatID).
			MsgType(larkim.MsgTypeInteractive).
			Content(cardContent).
			Build()).
		Build()

	resp, err := c.client.Im.V1.Message.Create(ctx, req)
	if err != nil {
		return "", fmt.Errorf("feishu placeholder send: %w", err)
	}
	if !resp.Success() {
		c.invalidateTokenOnAuthError(resp.Code)
		return "", fmt.Errorf("feishu placeholder api error (code=%d msg=%s)", resp.Code, resp.Msg)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Inspect the wrapped cause to see which card constraint fired
  2. Simplify custom placeholder texts to plain strings
  3. Treat the failure as non-fatal: proceed without a placeholder rather than aborting the reply flow

Example fix

// before
phID, err := ch.SendPlaceholder(ctx, chatID)
if err != nil {
	return err // cosmetic feature aborts the whole reply
}

// after
phID, err := ch.SendPlaceholder(ctx, chatID)
if err != nil {
	logger.Warn("placeholder skipped", "err", err)
	phID = "" // reply flow continues without placeholder
}
Defensive patterns

Strategy: fallback

Validate before calling

if !placeholderEnabled { // mirrors c.bc.Placeholder.Enabled check
	return // avoids the call entirely
}

Type guard

func isPlaceholderBuildFailure(err error) bool {
	return err != nil && strings.Contains(err.Error(), "placeholder: card build failed")
}

Try / catch

phID, err := ch.SendPlaceholder(ctx, chatID)
if err != nil {
	logger.Warn("placeholder unavailable, continuing without it", "err", err)
	phID = "" // proceed with the normal send path
}

Prevention

When it happens

Trigger: SendPlaceholder is called with placeholder text enabled, and buildMarkdownCard fails on the randomly chosen placeholder string (marshal error or card schema violation).

Common situations: A custom placeholder text containing markdown the card builder rejects; library upgrade changing card schema validation; essentially rare - the built-in placeholder strings are known-good.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/0dedf0b3fe857689. Report an issue: GitHub.