按协议而不是按厂商做适配,与现有短信、对象存储的做法一致: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>
45 lines
2.3 KiB
SQL
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;
|