refactor: schema 收敛到 ORM metadata——5 张裸表升模型,方言无关的建表/补列/唯一索引
原来建表有三套来源(ORM create_all + 自建 SCHEMA_MIGRATIONS + agent_api 里的裸
CREATE TABLE)。这在 SQLite 下能活是因为 SQLite 的 DDL 极宽容;换 MySQL 后
agent_* 四张表的 `INTEGER PRIMARY KEY AUTOINCREMENT`、`TEXT DEFAULT ''`、
`TEXT PRIMARY KEY` 会全部建不出来,而失败被 `except: pass` 吞掉 —— 表现为
「经验库/动作库/会话功能静默失灵、日志里什么都看不到」。本期把它收敛成一套。
- core/models.py:
* 新增 5 个 ORM 模型:AppMeta / AgentExperience / ExperienceAudit /
AgentAction / AgentConversation(列名沿用历史,`app_meta.key` 在 MySQL 里是
保留字,ORM 属性名用 k,读写统一走 db_config.meta_get/meta_set)
* 新增 _long_text()(MySQL 用 MEDIUMTEXT,裸 TEXT 只有 64KB)与 _DOUBLE()
(评分别用单精度 FLOAT);_migrate_schema 不再执行 DDL,只维护版本账本,
SCHEMA_MIGRATIONS 的历史建表/加列条目 SQL 置 None
* 新增 _sync_columns():用 sqlalchemy.inspect 比对模型与实表补缺列,取代原来
靠 "duplicate column name" 报错文本判断的写法(换方言就失效)
* _ensure_unique_indexes() 按方言分叉:MySQL 5.7 没有过滤索引,改用
「虚拟生成列 + 唯一索引」复刻"空值不参与唯一约束"的语义
- web/agent_api.py: 删 4 段裸 CREATE TABLE;_ensure_* 收敛为 _ensure_tables()
(db.create_all 薄封装,失败必记日志);app_meta 读写与 INSERT OR IGNORE
改为方言中立
- core/device_discovery.py: app_meta 读写改用同一助手
- core/system_backup.py: SUMMARY_TABLES 由 db.metadata 派生 —— 备份覆盖红线
从"靠人记"变成结构上不可能漏;CURRENT_SCHEMA_VERSION 改从 models 导入
验证(均在 users.db 的一致快照副本上,不碰真实库):
空库 → 自建 12 张表 + 默认管理员 + 唯一索引 + schema_version=6;
老库 → 逐表行数与真实库完全一致,app_meta 键未变;
接口 → health/devices/jobs/groups/agent(经验库/动作库/会话) 全 200,
会话增删与备份导出(12 张表、无覆盖缺失)正常。
This commit is contained in:
+31
-71
@@ -60,14 +60,14 @@ def set_app(app):
|
||||
|
||||
|
||||
def _meta_get(key):
|
||||
return db.session.execute(
|
||||
db.text("SELECT value FROM app_meta WHERE key=:k"), {"k": key}).scalar() or ""
|
||||
# 走方言中立助手(app_meta 的列名 key 在 MySQL 里是保留字,裸 SQL 会语法错)
|
||||
from core.db_config import meta_get
|
||||
return meta_get(key) or ""
|
||||
|
||||
|
||||
def _meta_put(key, value):
|
||||
db.session.execute(
|
||||
db.text("INSERT OR REPLACE INTO app_meta(key,value) VALUES(:k,:v)"),
|
||||
{"k": key, "v": str(value)})
|
||||
from core.db_config import meta_set
|
||||
meta_set(key, str(value))
|
||||
|
||||
|
||||
def _read_cfg():
|
||||
@@ -75,29 +75,10 @@ def _read_cfg():
|
||||
|
||||
|
||||
# ================== 经验记忆(自进化) ==================
|
||||
# agent_experience:任务成功后的操作配方,下次相似任务检索注入 system prompt。
|
||||
# 原始 SQLite(CREATE IF NOT EXISTS 幂等),不进模型层迁移。
|
||||
_EXP_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS agent_experience (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_prompt TEXT DEFAULT '',
|
||||
recipe TEXT DEFAULT '',
|
||||
tool_seq TEXT DEFAULT '',
|
||||
hits INTEGER DEFAULT 0,
|
||||
created_at VARCHAR(20) DEFAULT '')"""
|
||||
|
||||
# 经验巡检(AI 质检):每日定时把经验交给模型评审,疑似问题标 pending 待人工确认。
|
||||
# 删除只允许人工(前端 confirm / 本 API),巡检绝不自动删。
|
||||
_AUDIT_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS experience_audit (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
exp_id INTEGER NOT NULL,
|
||||
verdict TEXT DEFAULT '', -- keep 保留 / delete 建议删除
|
||||
score REAL DEFAULT 0, -- 0-10 可用性评分
|
||||
reason TEXT DEFAULT '', -- 模型给出的理由
|
||||
hits INTEGER DEFAULT 0, -- 巡检时的引用次数(hits>0 被删要更谨慎)
|
||||
action TEXT DEFAULT 'pending', -- pending 待人工确认 / kept 已保留 / deleted 已删除
|
||||
audited_at VARCHAR(20) DEFAULT '')"""
|
||||
# agent_experience / experience_audit / agent_action / agent_conversation 四张表
|
||||
# 已在 core/models.py 里定义为 ORM 模型(唯一真相),建表统一由 db.create_all() 完成。
|
||||
# 以前这里是各写一段 SQLite 的 CREATE TABLE + try/except: pass —— 换 MySQL 后
|
||||
# AUTOINCREMENT / TEXT DEFAULT 全都建不出来,而异常被吞掉,表现为"功能静默失灵"。""
|
||||
|
||||
# 巡检评审提示词:模型只判「保留/建议删除」+ 评分 + 理由,不直接删。
|
||||
_AUDIT_PROMPT = """你是「手机自动化经验库」质检员。经验库存放 AI 成功操作手机后提炼的
|
||||
@@ -119,12 +100,20 @@ _AUDIT_PROMPT = """你是「手机自动化经验库」质检员。经验库存
|
||||
只输出 JSON:{{"keep": true或false, "score": 0到10的整数, "reason": "一句话中文理由"}}"""
|
||||
|
||||
|
||||
def _ensure_exp_table():
|
||||
def _ensure_tables():
|
||||
"""确保 agent 系四张表存在(幂等)。create_all 只建缺失的表,开销可以忽略。
|
||||
|
||||
失败必须落日志:以前是 except: pass,MySQL 下建表失败会被完全吞掉,
|
||||
表不存在 → 后续查询全报错,而日志里什么都看不到。
|
||||
"""
|
||||
try:
|
||||
db.session.execute(db.text(_EXP_TABLE))
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
db.create_all()
|
||||
except Exception as e:
|
||||
_log.error(f"agent 表创建失败: {e}")
|
||||
|
||||
|
||||
def _ensure_exp_table():
|
||||
_ensure_tables()
|
||||
|
||||
|
||||
def _bigrams(text):
|
||||
@@ -366,19 +355,7 @@ def _save_experience(prompt, recipe, tool_seq):
|
||||
# steps 直接用编辑器 schema(open_app/click/input_text…),带**元素定位**
|
||||
# (selector_type/selector_value),**不含坐标**(分辨率/旋转/改版即失效)。
|
||||
# 复用:执行前按 name/别名/App 召回并注入 system prompt,模型可跳过重新探索。
|
||||
_ACTION_TABLE = (
|
||||
"CREATE TABLE IF NOT EXISTS agent_action ("
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
|
||||
"name TEXT NOT NULL,"
|
||||
"app TEXT DEFAULT '',"
|
||||
"aliases TEXT DEFAULT '[]',"
|
||||
"params TEXT DEFAULT '[]',"
|
||||
"steps TEXT NOT NULL,"
|
||||
"preconditions TEXT DEFAULT '',"
|
||||
"hits INTEGER DEFAULT 0,"
|
||||
"source_prompt TEXT DEFAULT '',"
|
||||
"created_at TEXT DEFAULT '',"
|
||||
"updated_at TEXT DEFAULT '')")
|
||||
# 建表见 core/models.py 的 AgentAction
|
||||
|
||||
# 允许沉淀的步骤类型(编辑器 STEP_TYPES 子集;显式排除 click_xy 等坐标类)
|
||||
_ACTION_STEP_TYPES = {
|
||||
@@ -398,11 +375,7 @@ _ACTION_TOOLS = {
|
||||
|
||||
|
||||
def _ensure_action_table():
|
||||
try:
|
||||
db.session.execute(db.text(_ACTION_TABLE))
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
_ensure_tables()
|
||||
|
||||
|
||||
def _brief_result(result):
|
||||
@@ -731,11 +704,7 @@ _audit_state = {"running": False, "last": "", "last_summary": ""}
|
||||
|
||||
|
||||
def _ensure_audit_table():
|
||||
try:
|
||||
db.session.execute(db.text(_AUDIT_TABLE))
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
_ensure_tables()
|
||||
|
||||
|
||||
def _review_one_exp(cfg, prompt, recipe, tool_seq, hits):
|
||||
@@ -1002,22 +971,11 @@ def agent_actions_save():
|
||||
|
||||
# ================== 历史会话(DeepSeek 式:多会话持久化) ==================
|
||||
# 会话 = 消息序列(JSON),一轮 run 的 user/assistant 文本完成后追加落库。
|
||||
# 单表 JSON 存储(每会话几十 KB 内,无需独立消息表)。
|
||||
_CONV_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS agent_conversation (
|
||||
id VARCHAR(20) PRIMARY KEY,
|
||||
title VARCHAR(100) DEFAULT '',
|
||||
messages TEXT DEFAULT '[]',
|
||||
created_at VARCHAR(20) DEFAULT '',
|
||||
updated_at VARCHAR(20) DEFAULT '')"""
|
||||
# 单表 JSON 存储(每会话几十 KB 内,无需独立消息表)。建表见 core/models.py。
|
||||
|
||||
|
||||
def _ensure_conv_table():
|
||||
try:
|
||||
db.session.execute(db.text(_CONV_TABLE))
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
_ensure_tables()
|
||||
|
||||
|
||||
def _conv_msgs(conv_id):
|
||||
@@ -1181,13 +1139,15 @@ def agent_run():
|
||||
try:
|
||||
from datetime import datetime as _dt2
|
||||
now = _dt2.now().strftime("%Y-%m-%d %H:%M")
|
||||
# 上面刚确认过会话不存在,普通 INSERT 即可;
|
||||
# 并发下撞主键就回滚忽略(原来用 SQLite 专有的 INSERT OR IGNORE)
|
||||
db.session.execute(db.text(
|
||||
"INSERT OR IGNORE INTO agent_conversation(id, messages, "
|
||||
"INSERT INTO agent_conversation(id, messages, "
|
||||
"created_at, updated_at) VALUES(:i, '[]', :c, :c)"),
|
||||
{"i": conv_id, "c": now})
|
||||
db.session.commit()
|
||||
except Exception:
|
||||
pass
|
||||
db.session.rollback()
|
||||
with _lock:
|
||||
if _run["state"] == "running":
|
||||
return jsonify({"ok": False, "error":
|
||||
|
||||
Reference in New Issue
Block a user