后端:资料距离、语音图片会员限制、免短信注册、AI 托管回复修复
- 单用户资料接口补上距离:此前只有推荐/附近列表会算距离,资料页和 聊天头部因此无内容可显示。沿用同一套 haversine 与隐私开关。 - 语音/图片消息可限定会员发送,两个开关在管理端「运营配置」中修改 (迁移 032)。校验放在 persistMessageContext,HTTP 与 WebSocket 两条发送路径都覆盖;文本消息永不受限。 - 短信服务关闭时注册不再要求验证码:关掉之后没人能拿到验证码,继续 要求就等于关闭注册通道。重置密码不做同样放宽,那里缺验证码等于 凭手机号夺号。app/config 增加 smsVerification 供客户端决定表单形态。 - 修复 AI 托管账号之间不回复:原规则按「发送方是否托管账号」拦截, 把真人操作测试号的正常对话也挡了。改为标记 worker 自己写入的回复, 只对 AI 生成的消息跳过入队。 - ai.default_model_id 同时接受模型 ID 与名称,填名称时不再被 MySQL 静默转成 0 而使配置失效。 - 聊天媒体留存管理与清理任务(迁移 033,两台线上均已应用)。 新增集成测试均针对真实 MySQL:会员限制、免短信注册、AI 入队规则、 默认模型解析、资料距离。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
842990b0e7
commit
acd8933dbb
@@ -43,10 +43,28 @@ type aiReplyJob struct {
|
||||
// Called right after a message is committed, from every send path. It must stay
|
||||
// cheap and must never fail the send: a missing reply is better than a failed
|
||||
// message.
|
||||
// aiGeneratedKey marks the context in which the worker writes its own reply.
|
||||
// The loop it prevents is "a reply triggers another reply", which is a property
|
||||
// of the message, not of the account: a person operating a managed test account
|
||||
// is having a real conversation and deserves an answer.
|
||||
type aiGeneratedKey struct{}
|
||||
|
||||
func markAIGenerated(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, aiGeneratedKey{}, true)
|
||||
}
|
||||
|
||||
func isAIGenerated(ctx context.Context) bool {
|
||||
value, _ := ctx.Value(aiGeneratedKey{}).(bool)
|
||||
return value
|
||||
}
|
||||
|
||||
func (a *App) enqueueAIReply(ctx context.Context, conversationID, senderID int64, members []int64, messageID int64) {
|
||||
if !a.configBool(ctx, "ai.enabled", false) {
|
||||
return
|
||||
}
|
||||
if isAIGenerated(ctx) {
|
||||
return // The worker's own reply must never trigger the next one.
|
||||
}
|
||||
peerID := int64(0)
|
||||
for _, member := range members {
|
||||
if member != senderID {
|
||||
@@ -61,13 +79,14 @@ func (a *App) enqueueAIReply(ctx context.Context, conversationID, senderID int64
|
||||
}
|
||||
var agentUserID int64
|
||||
var testBatch string
|
||||
// The sender must not itself be managed, otherwise two agents would keep
|
||||
// each other talking forever.
|
||||
// Note there is no condition on the sender: whoever typed this, the reply is
|
||||
// owed by the recipient. Runaway agent-to-agent chatter is prevented above,
|
||||
// where the worker's own writes are recognised and skipped, and bounded by
|
||||
// the per-agent daily reply limits.
|
||||
err := a.db.QueryRowContext(ctx, `SELECT g.user_id,u.test_batch
|
||||
FROM ai_agents g JOIN users u ON u.id=g.user_id
|
||||
WHERE g.user_id=? AND g.enabled=1 AND u.is_test=1 AND u.status=1 AND u.deleted_at IS NULL
|
||||
AND NOT EXISTS(SELECT 1 FROM ai_agents s WHERE s.user_id=? AND s.enabled=1)`,
|
||||
peerID, senderID).Scan(&agentUserID, &testBatch)
|
||||
WHERE g.user_id=? AND g.enabled=1 AND u.is_test=1 AND u.status=1 AND u.deleted_at IS NULL`,
|
||||
peerID).Scan(&agentUserID, &testBatch)
|
||||
if err != nil || agentUserID == 0 {
|
||||
return
|
||||
}
|
||||
@@ -260,7 +279,7 @@ func (a *App) processAIReplyJob(ctx context.Context, job aiReplyJob) error {
|
||||
}
|
||||
|
||||
func (a *App) sendAIReply(ctx context.Context, job aiReplyJob, text string) error {
|
||||
item, members, err := a.persistMessageContext(ctx, job.ConversationID, job.AgentUserID,
|
||||
item, members, err := a.persistMessageContext(markAIGenerated(ctx), job.ConversationID, job.AgentUserID,
|
||||
"ai-"+randomToken()[:20], 1, map[string]any{"text": text})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user