oclaw/runtime/operations/scripts/smoke_admin_chat_postgres.py
oliver d14e9d3596 feat(persistence): PostgreSQL assistant store, chat persist fixes, gateway scripts
- Add SQLAlchemy Core repos, pg adapter/compat, assistant_store factory, Alembic bootstrap and migration/cutover scripts.

- Harden chat_message writes (NUL scrub for PG), turn_uuid on attempt failure, WS turn_runner fallbacks and gateway executed_turn_uuid init.

- start_gateway: log paths, PS7 stderr handling via cmd, background stdout/stderr redirect; runtime assistant_runtime_log_dir export.

- Ops: clear_all_chat_sessions with PG-only --postgresql and env-gated wipe; clear_postgres_chat_sessions.ps1.

- Tests: SA repos, pg compat, persist fallback, smoke env isolation; CI and docs touch-ups.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:33:50 +08:00

178 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""HTTP 冒烟:加载 _local/system.env → 用当前配置的 PG → 登录 → 建会话 → POST 一条消息 → 拉历史。
不依赖外网 LLM:强制 ``AIA_ASSISTANT_MODE=rule``(本地规则回复)。
用法(仓库根)::
python runtime/operations/scripts/smoke_admin_chat_postgres.py
退出码 0 表示全流程成功且 ``messages`` 里同时有 user 与 assistant。
"""
from __future__ import annotations
import hashlib
import os
import shutil
import sys
import tempfile
import uuid
from pathlib import Path
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
def main() -> int:
root = _repo_root()
os.chdir(root)
if str(root) not in sys.path:
sys.path.insert(0, str(root))
from svc.config.bootstrap_env import load_system_env
load_system_env(force=True)
if (os.getenv("AIA_ASSISTANT_DB_BACKEND") or "").strip().lower() not in (
"postgresql",
"pg",
"postgres",
):
print("AIA_ASSISTANT_DB_BACKEND 不是 postgresql,本脚本用于测 PG。", file=sys.stderr)
return 2
# 不覆盖已有 shell 变量;仅保证规则模式(免外网)
os.environ.setdefault("AIA_ASSISTANT_MODE", "rule")
os.environ["AIA_ASSISTANT_MODE"] = "rule"
tmp = tempfile.mkdtemp(prefix="oclaw-smoke-ws-")
os.environ["OPS_WORKSPACE_ROOT"] = tmp
from svc.config import database as db_cfg
from svc.persistence.assistant_store import get_assistant_store, reset_assistant_store_singleton
from svc.persistence.db.engine import clear_assistant_engine_cache
clear_assistant_engine_cache()
reset_assistant_store_singleton()
if db_cfg.assistant_db_backend() != "postgresql":
print("assistant_db_backend() 不是 postgresql", file=sys.stderr)
return 2
from svc.persistence.sqlite_store import (
LLM_BUILTIN_RULE_PROFILE_ID,
SqliteStore,
active_llm_profile_setting_key,
)
store = get_assistant_store()
assert isinstance(store, SqliteStore) and store._use_pg
tag = uuid.uuid4().hex[:10]
pw = f"smoke-{tag}"
t = store.create_tenant(f"pg-smoke-{tag}")
tenant_id = str(t["id"])
# 不用 administrator:共享库里会走全局模型池与已导入的 OpenAI profile,易在无 Key 时得到空 reply。
u = store.create_user_account(
tenant_id=tenant_id,
username=f"smoke_{tag}",
display_name="Smoke",
role="owner",
password_hash=hashlib.sha256(pw.encode("utf-8")).hexdigest(),
is_active=True,
)
user_id = str(u.get("id") or "").strip()
if not user_id:
print("create_user_account returned no id", u, file=sys.stderr)
return 1
store.grant_llm_profile_to_user(
tenant_id=tenant_id,
profile_id=LLM_BUILTIN_RULE_PROFILE_ID,
user_id=user_id,
)
store.set_setting(
active_llm_profile_setting_key(user_id, f"smoke_{tag}"),
LLM_BUILTIN_RULE_PROFILE_ID,
)
from fastapi.testclient import TestClient
from interfaces.http.fastapi_app import create_app
client = TestClient(create_app())
try:
client.post("/admin/api/auth/bootstrap", json={})
lr = client.post(
"/admin/api/auth/login",
json={
"tenant_id": tenant_id,
"username": f"smoke_{tag}",
"password": pw,
"purpose": "console",
},
)
lj = lr.json()
if not lj.get("ok"):
print("login failed:", lr.status_code, lj, file=sys.stderr)
return 1
token = str(lj.get("token") or "")
h = {"authorization": f"Bearer {token}"}
cr = client.post("/admin/api/chat/sessions", json={"title": f"smoke-{tag}"}, headers=h)
if cr.status_code != 200:
print("create session:", cr.status_code, cr.text, file=sys.stderr)
return 1
cj = cr.json()
if not cj.get("ok"):
print("create session body:", cj, file=sys.stderr)
return 1
sid = str((cj.get("session") or {}).get("id") or "")
if not sid:
print("no session id", cj, file=sys.stderr)
return 1
mr = client.post(
f"/admin/api/chat/sessions/{sid}/messages",
json={"text": "ping-smoke-pg"},
headers=h,
)
if mr.status_code != 200:
print("send message:", mr.status_code, mr.text, file=sys.stderr)
return 1
mj = mr.json()
if not mj.get("ok"):
print("send message body:", mj, file=sys.stderr)
return 1
reply = str(mj.get("reply") or "")
if not reply.strip():
print("empty reply (unexpected for rule mode)", mj, file=sys.stderr)
return 1
gr = client.get(f"/admin/api/chat/sessions/{sid}/messages", headers=h)
if gr.status_code != 200:
print("list messages:", gr.status_code, gr.text, file=sys.stderr)
return 1
gj = gr.json()
msgs = gj.get("messages") or []
roles = [str(m.get("role") or "") for m in msgs if isinstance(m, dict)]
if "user" not in roles or "assistant" not in roles:
print("roles mismatch:", roles, "full ok=", gj.get("ok"), file=sys.stderr)
return 1
print("OK smoke_admin_chat_postgres")
print(" tenant_id=", tenant_id)
print(" session_id=", sid)
print(" reply_len=", len(reply))
print(" message_count=", len(msgs), "roles=", roles)
return 0
finally:
try:
shutil.rmtree(tmp, ignore_errors=True)
except Exception:
pass
if __name__ == "__main__":
raise SystemExit(main())