Files
kefu/im/backend/migrations/031_ai_agents.sql
T
Your NameandClaude Opus 5 a024d59827 后端接入 AI 模型并支持测试账号托管回复
按协议而不是按厂商做适配,与现有短信、对象存储的做法一致:openai 兼容格式
一个适配器即可覆盖 DeepSeek、通义、智谱、火山方舟、Ollama 等,新增厂商通常
只需在后台加一条模型记录;anthropic、gemini、webhook、debug 各一个适配器。
密钥沿用 integration.go 的 AES-GCM 加密存储。

测试账号托管的回复走 persistMessage 同一条落库和推送路径,因此未读数、
WebSocket 推送和会话排序全部复用现有逻辑,客户端无需改动。为此把
persistMessage 抽出 persistMessageContext,因为 worker 没有 *http.Request。

任务队列用数据库表而非内存:重启不丢回复,多实例不重复消费。领取用 UPDATE
打 claim_token 再回读,没有用 SELECT ... FOR UPDATE SKIP LOCKED——生产存在
MySQL 5.7 环境,那里该语法无法解析。同一会话同时只允许一个待处理任务
(pending_key 生成列 + 唯一键),所以用户连发多条消息只会得到一条回复。

闸门:仅 is_test=1 且在允许批次内的账号生效,托管账号之间不互相触发,三层
配额,调用失败默认静默。会话内是否标注 AI 身份由 ai.disclose_in_chat 控制
并默认开启,关闭前需确认所在地区的监管要求。

app.go、integration.go、im.go 三个文件同时包含本次改动之前工作区里就已存在
的未提交修改,一并带入。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:31:04 +08:00

45 lines
2.3 KiB
SQL

SET NAMES utf8mb4;
-- One row per managed test account. model_id 0 falls back to the configured
-- default model so operations can switch every agent at once.
CREATE TABLE IF NOT EXISTS ai_agents (
user_id BIGINT UNSIGNED NOT NULL,
model_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
persona VARCHAR(1000) NOT NULL DEFAULT '',
enabled TINYINT(1) NOT NULL DEFAULT 1,
daily_reply_limit INT UNSIGNED NOT NULL DEFAULT 0,
reply_count INT UNSIGNED NOT NULL DEFAULT 0,
last_reply_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id),
KEY idx_ai_agents_enabled (enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- A durable queue rather than an in-memory one: replies survive a restart and
-- several instances can share it. Claiming is an UPDATE that stamps claim_token
-- instead of SELECT ... FOR UPDATE SKIP LOCKED, because deployments still run
-- MySQL 5.7 where SKIP LOCKED does not parse.
-- pending_key is NULL once a job leaves the pending state, so the unique index
-- allows exactly one waiting job per conversation and collapses a burst of
-- incoming messages into a single reply.
CREATE TABLE IF NOT EXISTS ai_reply_jobs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
conversation_id BIGINT UNSIGNED NOT NULL,
agent_user_id BIGINT UNSIGNED NOT NULL,
trigger_message_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
status TINYINT UNSIGNED NOT NULL DEFAULT 0,
attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
run_after DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
claim_token CHAR(32) NOT NULL DEFAULT '',
last_error VARCHAR(500) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
pending_key BIGINT UNSIGNED GENERATED ALWAYS AS (IF(status = 0, conversation_id, NULL)) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_ai_reply_jobs_pending (pending_key),
KEY idx_ai_reply_jobs_due (status, run_after),
KEY idx_ai_reply_jobs_claim (claim_token),
KEY idx_ai_reply_jobs_agent (agent_user_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;