Split chat message and group references by task, update intent routing and context budget, distinguish accepted card updates from verified writes, and explain the ambiguous chat --from flag.
214 lines
7.1 KiB
Go
214 lines
7.1 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package chatmsg
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"unicode"
|
|
)
|
|
|
|
var (
|
|
ErrCardUpdateNotApplied = errors.New("streaming card update was not applied")
|
|
ErrCardUpdateUnverified = errors.New("streaming card update could not be verified")
|
|
ErrCardUpdateBizIDDrift = errors.New("streaming card update returned a different bizId")
|
|
)
|
|
|
|
// CardUpdateVerification distinguishes an accepted write from an independently
|
|
// verified write. Some server versions return success=true without an updated
|
|
// flag or affected count; that is sufficient to report acceptance, but not to
|
|
// claim that the card's visible content was observed after the write.
|
|
type CardUpdateVerification struct {
|
|
Accepted bool
|
|
Verified bool
|
|
Evidence string
|
|
}
|
|
|
|
// NormalizeCardBizID performs only format-independent checks. bizId is an
|
|
// opaque server-issued identifier; a stricter character or prefix contract
|
|
// must not be invented by the CLI without an authoritative API declaration.
|
|
func NormalizeCardBizID(raw string) (string, error) {
|
|
bizID := strings.TrimSpace(raw)
|
|
if bizID == "" {
|
|
return "", fmt.Errorf("--biz-id 不能为空")
|
|
}
|
|
for _, r := range bizID {
|
|
if unicode.IsControl(r) || unicode.IsSpace(r) {
|
|
return "", fmt.Errorf("--biz-id 必须是 send-card 返回的单个不透明 ID,不能包含空白或控制字符")
|
|
}
|
|
}
|
|
if isCardBizIDPlaceholder(bizID) {
|
|
return "", fmt.Errorf("--biz-id 仍是占位符 %q;请传入 send-card 实际返回的 bizId", bizID)
|
|
}
|
|
return bizID, nil
|
|
}
|
|
|
|
func isCardBizIDPlaceholder(value string) bool {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
switch normalized {
|
|
case "bizid", "biz-id", "your-biz-id", "your_biz_id",
|
|
"<bizid>", "<biz-id>", "<your-biz-id>",
|
|
"{bizid}", "{biz-id}", "${bizid}", "${biz-id}":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// VerifyStreamingCardUpdate separates server acceptance from affirmative
|
|
// evidence that the requested write took effect.
|
|
func VerifyStreamingCardUpdate(requestedBizID string, response map[string]any) (CardUpdateVerification, error) {
|
|
requestedBizID = strings.TrimSpace(requestedBizID)
|
|
observation := cardUpdateObservation{bizIDs: map[string]struct{}{}}
|
|
observeCardUpdate(response, &observation)
|
|
|
|
for responseBizID := range observation.bizIDs {
|
|
if requestedBizID != "" && responseBizID != requestedBizID {
|
|
return CardUpdateVerification{}, fmt.Errorf("%w: requested %q, response %q", ErrCardUpdateBizIDDrift, requestedBizID, responseBizID)
|
|
}
|
|
}
|
|
if observation.negativeEvidence != "" && (observation.positiveEvidence != "" || observation.acceptedEvidence != "") {
|
|
positive := observation.positiveEvidence
|
|
if positive == "" {
|
|
positive = observation.acceptedEvidence
|
|
}
|
|
return CardUpdateVerification{}, fmt.Errorf("%w: conflicting evidence %s and %s", ErrCardUpdateUnverified, positive, observation.negativeEvidence)
|
|
}
|
|
if observation.positiveEvidence != "" {
|
|
return CardUpdateVerification{Accepted: true, Verified: true, Evidence: observation.positiveEvidence}, nil
|
|
}
|
|
if observation.negativeEvidence != "" {
|
|
return CardUpdateVerification{}, fmt.Errorf("%w: %s", ErrCardUpdateNotApplied, observation.negativeEvidence)
|
|
}
|
|
if observation.acceptedEvidence != "" {
|
|
return CardUpdateVerification{Accepted: true, Verified: false, Evidence: observation.acceptedEvidence}, nil
|
|
}
|
|
return CardUpdateVerification{}, ErrCardUpdateUnverified
|
|
}
|
|
|
|
type cardUpdateObservation struct {
|
|
bizIDs map[string]struct{}
|
|
acceptedEvidence string
|
|
positiveEvidence string
|
|
negativeEvidence string
|
|
}
|
|
|
|
func observeCardUpdate(value any, observation *cardUpdateObservation) {
|
|
switch typed := value.(type) {
|
|
case map[string]any:
|
|
observeCardUpdateMap(typed, observation)
|
|
case []any:
|
|
for _, child := range typed {
|
|
observeCardUpdate(child, observation)
|
|
}
|
|
case bool:
|
|
if typed {
|
|
setPositiveCardUpdateEvidence(observation, "result=true")
|
|
} else {
|
|
setNegativeCardUpdateEvidence(observation, "result=false")
|
|
}
|
|
}
|
|
}
|
|
|
|
func observeCardUpdateMap(value map[string]any, observation *cardUpdateObservation) {
|
|
if accepted, ok := value["success"].(bool); ok && accepted && observation.acceptedEvidence == "" {
|
|
observation.acceptedEvidence = "success=true"
|
|
}
|
|
for _, key := range []string{"bizId", "bizID", "biz_id"} {
|
|
if candidate, ok := value[key].(string); ok && strings.TrimSpace(candidate) != "" {
|
|
observation.bizIDs[strings.TrimSpace(candidate)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, key := range []string{"updated", "applied"} {
|
|
if applied, ok := value[key].(bool); ok {
|
|
if applied {
|
|
setPositiveCardUpdateEvidence(observation, key+"=true")
|
|
} else {
|
|
setNegativeCardUpdateEvidence(observation, key+"=false")
|
|
}
|
|
}
|
|
}
|
|
for _, key := range []string{"affectedCount", "updatedCount", "modifiedCount"} {
|
|
if count, ok := cardUpdateCount(value[key]); ok {
|
|
if count > 0 {
|
|
setPositiveCardUpdateEvidence(observation, fmt.Sprintf("%s=%d", key, count))
|
|
} else if count == 0 {
|
|
setNegativeCardUpdateEvidence(observation, fmt.Sprintf("%s=%d", key, count))
|
|
}
|
|
}
|
|
}
|
|
errorCode, hasErrorCode := value["errorCode"]
|
|
errorCodeEmpty := hasErrorCode && cardUpdateErrorCodeEmpty(errorCode)
|
|
if hasErrorCode && !errorCodeEmpty {
|
|
setNegativeCardUpdateEvidence(observation, "errorCode=non-empty")
|
|
}
|
|
if success, ok := value["success"].(bool); ok {
|
|
if !success {
|
|
setNegativeCardUpdateEvidence(observation, "success=false")
|
|
}
|
|
}
|
|
|
|
// Only documented response envelopes are traversed. This prevents an
|
|
// unrelated extension field containing "updated":true from proving the
|
|
// business write.
|
|
for _, key := range []string{"result", "data", "response", "card"} {
|
|
if child, exists := value[key]; exists {
|
|
observeCardUpdate(child, observation)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cardUpdateErrorCodeEmpty(value any) bool {
|
|
if value == nil {
|
|
return true
|
|
}
|
|
code, ok := value.(string)
|
|
return ok && strings.TrimSpace(code) == ""
|
|
}
|
|
|
|
func setPositiveCardUpdateEvidence(observation *cardUpdateObservation, evidence string) {
|
|
if observation.positiveEvidence == "" {
|
|
observation.positiveEvidence = evidence
|
|
}
|
|
}
|
|
|
|
func setNegativeCardUpdateEvidence(observation *cardUpdateObservation, evidence string) {
|
|
if observation.negativeEvidence == "" {
|
|
observation.negativeEvidence = evidence
|
|
}
|
|
}
|
|
|
|
func cardUpdateCount(value any) (int64, bool) {
|
|
switch typed := value.(type) {
|
|
case int:
|
|
return int64(typed), true
|
|
case int32:
|
|
return int64(typed), true
|
|
case int64:
|
|
return typed, true
|
|
case float32:
|
|
return int64(typed), float32(int64(typed)) == typed
|
|
case float64:
|
|
return int64(typed), float64(int64(typed)) == typed
|
|
case json.Number:
|
|
count, err := typed.Int64()
|
|
return count, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|