This commit is contained in:
2026-03-24 18:07:22 +08:00
parent e062368ef2
commit 9a16f738d8
121 changed files with 8904 additions and 3940 deletions
+21
View File
@@ -0,0 +1,21 @@
# Scripts 模块
## 目录说明
`scripts` 提供本地调试与数据同步脚本。
## 文件清单
- `console_chat.py`:命令行多轮问答调试
- `demo_chat.py`:示例交互脚本
- `sync_ragflow_templates.py`:同步表检索模板到 RAGFlow
- `sync_sql_gen_prompts.py`:同步 SQL 提示词到 RAGFlow
## 常用命令
```powershell
python scripts\console_chat.py --skip-sr-api --show-sql
python scripts\sync_ragflow_templates.py
python scripts\sync_sql_gen_prompts.py
```
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""控制台问数脚本:支持交互式多轮问答和单次执行。"""
from __future__ import annotations
import argparse
import json
import sys
import traceback
import uuid
from pathlib import Path
from typing import Any, Dict, Iterable, Optional, Sequence
# 允许直接使用 `python scripts/console_chat.py` 运行
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from agent.agents.conversation import ConversationAgent
from config import Config
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="在控制台中直接输入自然语言问题,调用 ConversationAgent 进行问数。"
)
parser.add_argument("--model-section", default=None, help="可选:指定 config.ini 中的模型配置段")
parser.add_argument("--conversation-id", default=None, help="可选:会话 ID,仅用于展示和上下文标识")
parser.add_argument("--user", default="console-user", help="可选:用户标识")
parser.add_argument("--query", "-q", default=None, help="单次执行模式:直接执行一条问题后退出")
parser.add_argument("--skip-sr-api", action="store_true", help="仅生成 SQL,不执行 SR API")
parser.add_argument("--show-sql", action="store_true", help="额外打印生成的 SQL")
parser.add_argument("--show-context", action="store_true", help="额外打印完整上下文 JSON")
parser.add_argument("--show-plan", action="store_true", help="额外打印 SQL 规划 JSON")
parser.add_argument("--no-banner", action="store_true", help="不显示启动横幅")
return parser
def print_banner(model_section: Optional[str], conversation_id: str, skip_sr_api: bool) -> None:
print("=" * 72)
print("APBO Console Chat")
print(f"Model Section : {model_section or 'default'}")
print(f"Conversation : {conversation_id}")
print(f"Execution : {'SQL only' if skip_sr_api else 'SQL + SR API'}")
print("Commands : /quit /exit /sql /context /plan /exec")
print("=" * 72)
def _safe_json(data: Any) -> str:
try:
return json.dumps(data, ensure_ascii=False, indent=2, default=str)
except Exception:
return str(data)
def _looks_like_json(text: str) -> bool:
stripped = (text or "").strip()
return stripped.startswith("{") or stripped.startswith("[")
def _try_json_loads(value: Any) -> Any:
if isinstance(value, (dict, list)):
return value
if isinstance(value, str) and _looks_like_json(value):
try:
return json.loads(value)
except Exception:
return value
return value
def parse_sr_api_result(raw_result: Any) -> Any:
"""解析 SR API 返回值,兼容外层 envelope 和内层 text JSON。"""
parsed = _try_json_loads(raw_result)
if isinstance(parsed, dict) and "text" in parsed:
text_payload = _try_json_loads(parsed.get("text"))
parsed = {**parsed, "text": text_payload}
return parsed
def _find_table_candidate(value: Any) -> Any:
if isinstance(value, list):
return value
if isinstance(value, dict):
if "columns" in value and any(key in value for key in ("rows", "data", "values")):
return value
for key in ("data", "rows", "records", "record", "items", "list", "result", "text"):
nested = value.get(key)
if isinstance(nested, (list, dict)):
found = _find_table_candidate(nested)
if found is not None:
return found
return None
def extract_table_rows(parsed_result: Any) -> tuple[list[str], list[list[str]]]:
"""从常见查询结果结构中提取表头和二维行数据。"""
candidate = _find_table_candidate(parsed_result)
if candidate is None:
return [], []
if isinstance(candidate, dict) and isinstance(candidate.get("columns"), list):
headers = [str(col) for col in candidate.get("columns") or []]
raw_rows = candidate.get("rows") or candidate.get("data") or candidate.get("values") or []
if raw_rows and all(isinstance(row, dict) for row in raw_rows):
return headers or list(raw_rows[0].keys()), [
[str((row or {}).get(header, "")) for header in (headers or list(raw_rows[0].keys()))]
for row in raw_rows
]
return headers, [[str(cell) for cell in row] for row in raw_rows if isinstance(row, (list, tuple))]
if isinstance(candidate, list) and candidate:
if all(isinstance(row, dict) for row in candidate):
headers: list[str] = []
for row in candidate:
for key in row.keys():
if key not in headers:
headers.append(str(key))
return headers, [[str((row or {}).get(header, "")) for header in headers] for row in candidate]
if all(isinstance(row, (list, tuple)) for row in candidate):
width = max(len(row) for row in candidate)
headers = [f"col_{idx + 1}" for idx in range(width)]
return headers, [[str(row[idx]) if idx < len(row) else "" for idx in range(width)] for row in candidate]
return [], []
def render_text_table(headers: Sequence[str], rows: Sequence[Sequence[str]], *, max_width: int = 28, max_rows: int = 20) -> str:
"""将二维数据渲染成纯文本表格。"""
if not headers or not rows:
return "<empty table>"
def clip(value: Any) -> str:
text = str(value).replace("\r", " ").replace("\n", " ")
return text if len(text) <= max_width else text[: max_width - 3] + "..."
display_rows = list(rows[:max_rows])
str_rows = [[clip(cell) for cell in row] for row in display_rows]
clipped_headers = [clip(header) for header in headers]
widths = []
for idx, header in enumerate(clipped_headers):
col_values = [row[idx] if idx < len(row) else "" for row in str_rows]
widths.append(max(len(header), *(len(value) for value in col_values)) if col_values else len(header))
def render_row(values: Sequence[str]) -> str:
padded = []
for idx, width in enumerate(widths):
value = values[idx] if idx < len(values) else ""
padded.append(value.ljust(width))
return "| " + " | ".join(padded) + " |"
separator = "+-" + "-+-".join("-" * width for width in widths) + "-+"
lines = [separator, render_row(clipped_headers), separator]
lines.extend(render_row(row) for row in str_rows)
lines.append(separator)
if len(rows) > max_rows:
lines.append(f"... showing first {max_rows} of {len(rows)} rows")
return "\n".join(lines)
def _format_sr_api_table(sr_api_result: Any) -> Optional[str]:
parsed = parse_sr_api_result(sr_api_result)
headers, rows = extract_table_rows(parsed)
if not headers or not rows:
return None
summary = f"Query Result: {len(rows)} row(s)"
if isinstance(parsed, dict) and parsed.get("status_code") is not None:
summary += f" | status={parsed.get('status_code')}"
return summary + "\n" + render_text_table(headers, rows)
def format_result(
result: Dict[str, Any],
*,
show_sql: bool = False,
show_context: bool = False,
show_plan: bool = False,
) -> str:
context = (result or {}).get("context") or {}
messages = (result or {}).get("messages") or []
sr_api_result = context.get("sr_api_result")
answer = ""
if messages:
last = messages[-1]
answer = getattr(last, "content", "") or str(last)
if not answer:
answer = str(sr_api_result or context.get("final_sql") or "<empty response>")
table_block = _format_sr_api_table(sr_api_result) if sr_api_result else None
if table_block and (_looks_like_json(answer) or answer.strip().lower() in {"ok", "success"}):
answer = "查询成功,结果已按二维表格展示如下。"
blocks = [f"Answer:\n{answer}"]
if table_block:
blocks.append(table_block)
if show_sql and context.get("final_sql"):
blocks.append(f"SQL:\n{context['final_sql']}")
if show_plan and context.get("sql_plan"):
blocks.append(f"SQL Plan:\n{_safe_json(context['sql_plan'])}")
if show_context:
blocks.append(f"Context:\n{_safe_json(context)}")
return "\n\n".join(blocks)
def run_turn(
agent: ConversationAgent,
query: str,
*,
user: str,
conversation_id: str,
skip_sr_api: bool,
show_sql: bool,
show_context: bool,
show_plan: bool,
) -> Dict[str, Any]:
result = agent.run(
query,
user=user,
conversation_id=conversation_id,
skip_sr_api=skip_sr_api,
debug_node_trace=True,
)
print(format_result(result, show_sql=show_sql, show_context=show_context, show_plan=show_plan))
return result
def interactive_loop(args: argparse.Namespace) -> int:
conversation_id = args.conversation_id or f"console_{uuid.uuid4().hex[:8]}"
agent = ConversationAgent(model_section=args.model_section)
show_sql = bool(args.show_sql)
show_context = bool(args.show_context)
show_plan = bool(args.show_plan)
skip_sr_api = bool(args.skip_sr_api)
if not args.no_banner:
print_banner(args.model_section, conversation_id, skip_sr_api)
while True:
try:
user_input = input("\n问数> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
return 0
if not user_input:
continue
lowered = user_input.lower()
if lowered in {"/quit", "/exit", "quit", "exit"}:
print("Bye.")
return 0
if lowered == "/sql":
show_sql = not show_sql
print(f"show_sql = {show_sql}")
continue
if lowered == "/context":
show_context = not show_context
print(f"show_context = {show_context}")
continue
if lowered == "/plan":
show_plan = not show_plan
print(f"show_plan = {show_plan}")
continue
if lowered == "/exec":
skip_sr_api = not skip_sr_api
print(f"skip_sr_api = {skip_sr_api}")
continue
try:
run_turn(
agent,
user_input,
user=args.user,
conversation_id=conversation_id,
skip_sr_api=skip_sr_api,
show_sql=show_sql,
show_context=show_context,
show_plan=show_plan,
)
except Exception as exc:
print(f"\n[ERROR] {exc}")
traceback.print_exc()
return 0
def one_shot(args: argparse.Namespace) -> int:
conversation_id = args.conversation_id or f"console_{uuid.uuid4().hex[:8]}"
agent = ConversationAgent(model_section=args.model_section)
try:
run_turn(
agent,
args.query,
user=args.user,
conversation_id=conversation_id,
skip_sr_api=bool(args.skip_sr_api),
show_sql=bool(args.show_sql),
show_context=bool(args.show_context),
show_plan=bool(args.show_plan),
)
return 0
except Exception as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
traceback.print_exc()
return 1
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(list(argv) if argv is not None else None)
try:
Config.validate_config()
except Exception as exc:
print(f"Configuration error: {exc}", file=sys.stderr)
print("Please check `config/config.ini` and your model/API settings.", file=sys.stderr)
return 1
if args.query:
return one_shot(args)
return interactive_loop(args)
if __name__ == "__main__":
raise SystemExit(main())
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""简洁演示问数脚本:只保留问题输入与结果输出。"""
from __future__ import annotations
import argparse
import sys
import time
import traceback
import uuid
from pathlib import Path
from typing import Any, Dict, Iterable, Optional
# 允许直接使用 `python scripts/demo_chat.py` 运行
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from agent.agents.conversation import ConversationAgent
from config import Config
from scripts.console_chat import extract_table_rows, parse_sr_api_result, render_text_table
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="演示版控制台问数:仅展示耗时、SQL、结果表和数据行数。"
)
parser.add_argument("--model-section", default=None, help="可选:指定 config.ini 中的模型配置段")
parser.add_argument("--conversation-id", default=None, help="可选:会话 ID")
parser.add_argument("--user", default="demo-user", help="可选:用户标识")
parser.add_argument("--query", "-q", default=None, help="单次执行模式:直接执行一条问题后退出")
return parser
def _last_answer_text(result: Dict[str, Any]) -> str:
messages = (result or {}).get("messages") or []
if not messages:
return ""
last = messages[-1]
return getattr(last, "content", "") or str(last)
def format_demo_result(result: Dict[str, Any], elapsed_seconds: float) -> str:
context = (result or {}).get("context") or {}
final_sql = str(context.get("final_sql") or "")
parsed_result = parse_sr_api_result(context.get("sr_api_result"))
headers, rows = extract_table_rows(parsed_result)
row_count = len(rows)
has_structured_result = context.get("sr_api_result") is not None and isinstance(parsed_result, (dict, list))
is_empty_result = bool(context.get("is_empty_result"))
response_source = str(context.get("response_source") or "")
answer = _last_answer_text(result)
blocks = [f"耗时: {elapsed_seconds:.2f}s"]
if final_sql:
blocks.append(f"SQL:\n{final_sql}")
else:
blocks.append("SQL:\n<未生成 SQL>")
blocks.append(f"数据行数: {row_count}")
if is_empty_result and response_source in {"model_empty_result_fallback", "empty_result_fixed_fallback"}:
blocks.append(f"结果说明:\n{answer or '未查询到符合条件的数据,请尝试调整筛选条件后再查询。'}")
elif has_structured_result:
table_text = render_text_table(headers or ['result'], rows)
blocks.append(f"SQL执行结果表:\n{table_text}")
else:
blocks.append(f"SQL执行结果:\n{answer or str(parsed_result or '<无结果>')}")
return "\n\n".join(blocks)
def run_turn(
agent: ConversationAgent,
query: str,
*,
user: str,
conversation_id: str,
) -> Dict[str, Any]:
started_at = time.perf_counter()
result = agent.run(
query,
user=user,
conversation_id=conversation_id,
skip_sr_api=False,
debug_node_trace=False,
)
elapsed_seconds = time.perf_counter() - started_at
print(format_demo_result(result, elapsed_seconds))
return result
def interactive_loop(args: argparse.Namespace) -> int:
conversation_id = args.conversation_id or f"demo_{uuid.uuid4().hex[:8]}"
agent = ConversationAgent(model_section=args.model_section)
while True:
try:
user_input = input("\n问题> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
return 0
if not user_input:
continue
if user_input.lower() in {"/quit", "/exit", "quit", "exit"}:
print("Bye.")
return 0
try:
run_turn(
agent,
user_input,
user=args.user,
conversation_id=conversation_id,
)
except Exception as exc:
print(f"\n[ERROR] {exc}")
traceback.print_exc()
return 0
def one_shot(args: argparse.Namespace) -> int:
conversation_id = args.conversation_id or f"demo_{uuid.uuid4().hex[:8]}"
agent = ConversationAgent(model_section=args.model_section)
query = str(args.query or "")
try:
run_turn(
agent,
query,
user=args.user,
conversation_id=conversation_id,
)
return 0
except Exception as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
traceback.print_exc()
return 1
def main(argv: Optional[Iterable[str]] = None) -> int:
parser = build_parser()
args = parser.parse_args(list(argv) if argv is not None else None)
try:
Config.validate_config()
except Exception as exc:
print(f"Configuration error: {exc}", file=sys.stderr)
print("Please check `config/config.ini` and your model/API settings.", file=sys.stderr)
return 1
if args.query:
return one_shot(args)
return interactive_loop(args)
if __name__ == "__main__":
raise SystemExit(main())
+12 -2
View File
@@ -1,8 +1,18 @@
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
def main():
from services.ragflow_sync import RagflowSync
from services.integrations.ragflow_sync import RagflowSync
syncer = RagflowSync()
syncer.sync_table_retrieval()
result = syncer.sync_table_retrieval()
print("表名检索模板同步完成")
print(result)
if __name__ == "__main__":
+11 -2
View File
@@ -1,10 +1,19 @@
from services.ragflow_sync import RagflowSync
from pathlib import Path
import sys
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from services.integrations.ragflow_sync import RagflowSync
def main():
syncer = RagflowSync()
syncer.sync_sql_gen_prompts()
result = syncer.sync_sql_gen_prompts()
print("SQL 生成提示词同步完成")
print(result)
if __name__ == "__main__":
-148
View File
@@ -1,148 +0,0 @@
import json
import sys
from typing import Any, Dict, List
import httpx
from config import Config
def _base_url() -> str:
app = Config.get_section("app")
host = app.get("host", "127.0.0.1")
port = app.get("port", "8000")
if host in ("0.0.0.0", "::"):
host = "127.0.0.1"
return f"http://{host}:{port}"
def _post(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
resp = client.post(url, json=payload)
print(f"POST {url} -> {resp.status_code}")
print(resp.text)
def _put(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
resp = client.put(url, json=payload)
print(f"PUT {url} -> {resp.status_code}")
print(resp.text)
def _get(client: httpx.Client, url: str) -> None:
resp = client.get(url)
print(f"GET {url} -> {resp.status_code}")
print(resp.text)
def _stream_sse(client: httpx.Client, url: str, payload: Dict[str, Any]) -> None:
with client.stream("POST", url, json=payload) as resp:
print(f"POST {url} -> {resp.status_code}")
current_event = "message"
for raw in resp.iter_lines():
if raw is None:
continue
line = raw.strip()
if not line:
continue
if line.startswith("event:"):
current_event = line.split(":", 1)[1].strip() or "message"
continue
if line.startswith("data:"):
data = line.split(":", 1)[1].strip()
print(f"[{current_event}] {data}")
def main() -> None:
base = _base_url()
menu: List[str] = [
"1) GET /health",
"2) GET /nacos/status",
"3) POST /api/workflows (conversation)",
"4) POST /api/workflows/stream (conversation)",
"5) POST /api/sql/generate",
"6) POST /api/tools/execute",
"7) POST /api/prompts/reload",
"8) POST /api/ragflow/table-retrieval/upload",
"9) PUT /api/ragflow/table-retrieval/update",
"10) POST /api/ragflow/sql-gen/upload",
"11) PUT /api/ragflow/sql-gen/update",
"0) Exit",
]
with httpx.Client(timeout=60) as client:
while True:
print("\n可用接口:")
for line in menu:
print(line)
choice = input("\n请选择编号: ").strip()
if choice == "0":
break
if choice == "1":
_get(client, f"{base}/health")
elif choice == "2":
_get(client, f"{base}/nacos/status")
elif choice == "3":
payload = {
"query": "查询 SO 4020438779 的 eta 信息",
"conversation_id": None,
"workflow_type": "conversation",
"response_mode": "blocking",
"user": "tester",
"inputs": {},
}
_post(client, f"{base}/api/workflows", payload)
elif choice == "4":
payload = {
"query": "查询 SO 4016769041 的 eta 信息",
"conversation_id": None,
"response_mode": "streaming",
"user": "tester",
"inputs": {},
"files": [],
}
_stream_sse(client, f"{base}/api/workflows/stream", payload)
elif choice == "5":
payload = {
"query": "查询 SO 4020438779 的 eta 信息",
"conversation_id": None,
"workflow_type": "conversation",
"response_mode": "blocking",
"user": "tester",
"inputs": {},
}
_post(client, f"{base}/api/sql/generate", payload)
elif choice == "6":
payload = {
"tool_name": "sr_api_query",
"payload": {
"sql": "SELECT 1",
"page": 1,
"rows": 1,
"orderBySelect": True,
"timeout": 30,
},
}
_post(client, f"{base}/api/tools/execute", payload)
elif choice == "7":
_post(client, f"{base}/api/prompts/reload", {})
elif choice == "8":
_post(client, f"{base}/api/ragflow/table-retrieval/upload", {})
elif choice == "9":
cfg = {"name": "table_retrieval_dataset"}
_put(client, f"{base}/api/ragflow/table-retrieval/update", cfg)
elif choice == "10":
_post(client, f"{base}/api/ragflow/sql-gen/upload", {})
elif choice == "11":
cfg = {"name": "sql_gen_dataset"}
_put(client, f"{base}/api/ragflow/sql-gen/update", cfg)
else:
print("无效选择")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
sys.exit(0)
+188
View File
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
"""
SQL 提示词 Redis 热更新脚本
用法:
python scripts/update_sql_prompts.py # 同步所有本地文件到 Redis
python scripts/update_sql_prompts.py --tables apbo_eta_ful apbo_eta_milestone # 同步指定表
python scripts/update_sql_prompts.py --list # 列出 Redis 中的所有表
python scripts/update_sql_prompts.py --delete apbo_eta_ful # 删除指定表
python scripts/update_sql_prompts.py --from-file path/to/file.json --table apbo_eta_ful # 从指定文件更新
"""
import argparse
import json
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from config import Config
from services.storage.cache import RedisCache
def _first_line(value) -> str:
if value is None:
return ""
text = str(value)
return text.splitlines()[0].strip() if text else ""
def _to_bool(value, default: bool = False) -> bool:
text = _first_line(value).lower()
if not text:
return default
return text in ("1", "true", "yes", "y", "on")
def _to_int(value, default: int = 0) -> int:
text = _first_line(value)
if not text:
return default
try:
return int(text)
except Exception:
return default
def get_redis_cache() -> RedisCache:
redis_cfg = Config.get_section("redis")
enabled = _to_bool(redis_cfg.get("enabled", "false"))
if not enabled:
raise RuntimeError("Redis 未启用,请检查配置 redis.enabled")
url = _first_line(redis_cfg.get("url"))
db = _to_int(redis_cfg.get("db", redis_cfg.get("database", 0)), default=0)
if not url:
host = _first_line(redis_cfg.get("host"))
port = _first_line(redis_cfg.get("port", "6379")) or "6379"
password = _first_line(redis_cfg.get("password", ""))
username = _first_line(redis_cfg.get("username", ""))
database = _first_line(redis_cfg.get("database", str(db))) or str(db)
if host:
from urllib.parse import quote_plus
if username and password:
auth = f"{quote_plus(username)}:{quote_plus(password)}@"
elif password:
auth = f":{quote_plus(password)}@"
else:
auth = ""
url = f"redis://{auth}{host}:{port}/{database}"
if not url:
raise RuntimeError("Redis 配置不完整,请检查 redis.url 或 redis.host")
return RedisCache(url=url, db=db)
def get_ttl() -> int:
redis_cfg = Config.get_section("redis")
return _to_int(redis_cfg.get("sql_prompt_ttl", 0), default=0)
def sync_from_local_files(cache: RedisCache, tables: list = None, ttl: int = None):
prompts_dir = PROJECT_ROOT / "config" / "sql_gen_prompts"
if tables:
files = [prompts_dir / f"{t}.json" for t in tables]
else:
files = list(prompts_dir.glob("*.json"))
results = {}
for file_path in files:
if not file_path.exists():
print(f"[跳过] 文件不存在: {file_path}")
continue
table_name = file_path.stem
try:
with open(file_path, "r", encoding="utf-8") as f:
prompt = json.load(f)
key = f"sql_prompt:{table_name}"
cache.set(key, json.dumps(prompt, ensure_ascii=False), ttl)
results[table_name] = "success"
print(f"[成功] {table_name}")
except Exception as e:
results[table_name] = f"failed: {e}"
print(f"[失败] {table_name}: {e}")
return results
def update_from_file(cache: RedisCache, file_path: str, table_name: str, ttl: int = None):
path = Path(file_path)
if not path.exists():
print(f"[错误] 文件不存在: {file_path}")
return False
try:
with open(path, "r", encoding="utf-8") as f:
prompt = json.load(f)
key = f"sql_prompt:{table_name}"
cache.set(key, json.dumps(prompt, ensure_ascii=False), ttl)
print(f"[成功] 已更新 {table_name}")
return True
except Exception as e:
print(f"[失败] {table_name}: {e}")
return False
def list_tables(cache: RedisCache):
keys = cache.keys("sql_prompt:*")
tables = []
for key in keys:
parts = key.split(":", 1)
if len(parts) == 2 and parts[1] != "table_list":
tables.append(parts[1])
if tables:
print("Redis 中的 SQL 提示词表:")
for t in sorted(tables):
print(f" - {t}")
else:
print("Redis 中没有 SQL 提示词")
return tables
def delete_table(cache: RedisCache, table_name: str):
key = f"sql_prompt:{table_name}"
cache.delete(key)
print(f"[成功] 已删除 {table_name}")
def main():
parser = argparse.ArgumentParser(description="SQL 提示词 Redis 热更新工具")
parser.add_argument("--tables", nargs="*", help="指定要同步的表名列表")
parser.add_argument("--list", action="store_true", help="列出 Redis 中的所有表")
parser.add_argument("--delete", type=str, help="删除指定表")
parser.add_argument("--from-file", type=str, help="从指定文件更新")
parser.add_argument("--table", type=str, help="目标表名(与 --from-file 配合使用)")
args = parser.parse_args()
try:
cache = get_redis_cache()
ttl = get_ttl()
except Exception as e:
print(f"[错误] {e}")
sys.exit(1)
if args.list:
list_tables(cache)
elif args.delete:
delete_table(cache, args.delete)
elif args.from_file:
if not args.table:
print("[错误] 使用 --from-file 时必须指定 --table")
sys.exit(1)
update_from_file(cache, args.from_file, args.table, ttl)
else:
sync_from_local_files(cache, args.tables, ttl)
if __name__ == "__main__":
main()