新增功能

This commit is contained in:
Your Name
2026-07-28 09:46:53 +08:00
parent 45b3bc0852
commit 980795b4da
57 changed files with 18421 additions and 1051 deletions
+130 -34
View File
@@ -2,12 +2,14 @@
AI 大模型配置文件
=================
支持所有 OpenAI 兼容接口(DeepSeek、通义千问、Moonshot、OpenAI 等)。
本文件中的值是【默认值】;在 GUI 的「AI 高级配置」中修改并保存后,
会写入 ai_settings.json,下次启动自动加载覆盖这里的默认值。
本文件中的值是【默认值】;在 GUI 或管理后台修改并保存后,会写入已忽略
Git 的 ai_settings.local.json,下次启动自动加载覆盖这里的默认值。
"""
import copy
import json
import os
import threading
# ── 是否启用 AI 回复(False 时使用固定文本回复)──
AI_ENABLED = True
@@ -17,9 +19,33 @@ AI_ENABLED = True
# /v1/ 后已有路径时不再自动拼接 /chat/completions
# Dify:填 .../v1/chat-messagesAI_API_KEY 用应用「访问 API」里的 Key(通常 app- 开头)
AI_API_BASE = "https://api.deepseek.com"
AI_API_KEY = "sk-992b66aec315400d92848a676acb0e99" # 你的 API Key
AI_API_KEY = "" # 从后台或 ai_settings.local.json 注入
AI_MODEL = "deepseek-chat" # 模型名称(Dify 应用侧选模型时此项可忽略)
# ── Grok Build 本地客服 Agent ──
# 客服回复由本机 Grok Build Agent 生成。Agent 只能调度项目内置的受控客服 MCP,
# 不再登录或访问外部客服 API,也没有企业微信发送能力。
GROK_CUSTOMER_SERVICE_ENABLED = True
GROK_CUSTOMER_SERVICE_TIMEOUT = 180
GROK_CUSTOMER_SERVICE_MAX_TURNS = 8
GROK_CUSTOMER_SERVICE_EFFORT = "low" # low / medium / high
# ── Grok Build Agent 自有模型 ──
# Grok Build 只提供 Agent 调度能力,实际推理由这里配置的模型完成。原生支持
# OpenAI Chat Completions / Responses、Anthropic MessagesDify Chat Messages
# 会由项目内置的 loopback 适配器转换成 Grok 工具调用协议。未配置或不兼容时
# Agent 直接停用,绝不会回退到 Grok/xAI 模型。
GROK_MODEL_ENABLED = False
GROK_API_BASE = ""
GROK_API_KEY = ""
GROK_MODEL = ""
GROK_API_BACKEND = "chat_completions" # 也可填 responses/messages/dify
GROK_AUTH_SCHEME = "auto"
GROK_DIFY_INPUTS: dict = {} # Dify 应用必填 inputs;其他协议忽略
GROK_CONTEXT_WINDOW = 128000
GROK_MAX_TOKENS = 8192
GROK_TEMPERATURE = 0.3
# ── 聊天上下文记忆 ──
# True → 按会话维护多轮上下文:把同一聊天框中之前提取到的聊天记录和我方历史回复
# 一起发给 AI,使回答能衔接上下文(如客户分多条消息描述一件事)
@@ -200,34 +226,87 @@ def build_system_prompt() -> str:
AI_SYSTEM_PROMPT = build_system_prompt()
# ──────────────────────────────────────────────────────────────────────────────
# GUI 配置持久化(ai_settings.json
# GUI 配置持久化(ai_settings.local.json
# ──────────────────────────────────────────────────────────────────────────────
_SETTINGS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ai_settings.json")
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
_SETTINGS_FILE = os.path.join(_BASE_DIR, "ai_settings.local.json")
_LEGACY_SETTINGS_FILE = os.path.join(_BASE_DIR, "ai_settings.json")
# 允许通过 GUI 修改并持久化的配置项
CONFIGURABLE_KEYS = [
"AI_ENABLED", "AI_API_BASE", "AI_API_KEY", "AI_MODEL",
"GROK_CUSTOMER_SERVICE_ENABLED", "GROK_CUSTOMER_SERVICE_TIMEOUT",
"GROK_CUSTOMER_SERVICE_MAX_TURNS", "GROK_CUSTOMER_SERVICE_EFFORT",
"GROK_MODEL_ENABLED", "GROK_API_BASE", "GROK_API_KEY", "GROK_MODEL",
"GROK_API_BACKEND", "GROK_AUTH_SCHEME", "GROK_CONTEXT_WINDOW", "GROK_MAX_TOKENS",
"GROK_TEMPERATURE", "GROK_DIFY_INPUTS",
"AI_USE_VISION", "AI_CONTEXT_ENABLED", "AI_CONTEXT_MAX_ROUNDS",
"AI_COUNTER_INSULT_ENABLED", "AI_AGENT_NAME", "AI_HOSPITAL_NAME",
"AI_MAX_TOKENS", "AI_TEMPERATURE", "AI_TIMEOUT",
"AI_MCP_ENABLED", "AI_MCP_MAX_ROUNDS", "AI_MCP_SERVERS",
]
_SETTINGS_LOCK = threading.RLock()
_SETTINGS_REVISION = 0
_REVISION_SNAPSHOT: dict = {}
def _settings_snapshot_unlocked(keys=CONFIGURABLE_KEYS) -> dict:
g = globals()
return {key: copy.deepcopy(g[key]) for key in keys}
def _record_revision_unlocked(snapshot: dict) -> None:
"""Advance the process-local revision when effective settings changed."""
global _SETTINGS_REVISION, _REVISION_SNAPSHOT
if snapshot != _REVISION_SNAPSHOT:
_SETTINGS_REVISION += 1
_REVISION_SNAPSHOT = copy.deepcopy(snapshot)
def _write_settings_unlocked(data: dict) -> None:
tmp = (
f"{_SETTINGS_FILE}.{os.getpid()}.{threading.get_ident()}.tmp"
)
try:
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, _SETTINGS_FILE)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
raise
# The module defaults are revision zero's baseline. Loading a local override
# below advances the revision only when it actually changes an effective value.
_REVISION_SNAPSHOT = _settings_snapshot_unlocked()
def save_settings():
"""将当前配置原子写入 ai_settings.jsonGUI 保存时调用)。"""
g = globals()
data = {k: g[k] for k in CONFIGURABLE_KEYS}
tmp = _SETTINGS_FILE + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp, _SETTINGS_FILE)
"""将当前配置原子写入本机私密配置(GUI/后台同步时调用)。"""
with _SETTINGS_LOCK:
data = _settings_snapshot_unlocked()
_write_settings_unlocked(data)
# This also detects legacy callers which assigned globals immediately
# before calling save_settings().
_record_revision_unlocked(data)
def export_settings() -> dict:
"""返回可下发给桌面端的 AI 配置副本。"""
g = globals()
return {key: g[key] for key in CONFIGURABLE_KEYS}
with _SETTINGS_LOCK:
return _settings_snapshot_unlocked()
def get_settings_revision() -> int:
"""Return the process-local revision of the effective AI configuration."""
with _SETTINGS_LOCK:
return _SETTINGS_REVISION
def apply_settings(settings: dict, *, persist: bool = True) -> dict:
@@ -239,34 +318,51 @@ def apply_settings(settings: dict, *, persist: bool = True) -> dict:
global AI_SYSTEM_PROMPT
if not isinstance(settings, dict):
raise TypeError("AI 配置必须是 JSON 对象")
g = globals()
applied = {}
for key in CONFIGURABLE_KEYS:
if key in settings:
g[key] = settings[key]
applied[key] = settings[key]
AI_SYSTEM_PROMPT = build_system_prompt()
if persist:
save_settings()
return applied
with _SETTINGS_LOCK:
g = globals()
next_snapshot = _settings_snapshot_unlocked()
applied = {}
for key in CONFIGURABLE_KEYS:
if key in settings:
value = copy.deepcopy(settings[key])
next_snapshot[key] = value
applied[key] = copy.deepcopy(value)
# Persist the complete prospective snapshot before publishing it to
# other threads. A failed write therefore leaves runtime values and
# the revision untouched.
if persist:
_write_settings_unlocked(next_snapshot)
for key, value in next_snapshot.items():
g[key] = value
AI_SYSTEM_PROMPT = build_system_prompt()
_record_revision_unlocked(next_snapshot)
return applied
def load_settings():
"""从 ai_settings.json 加载已保存的配置,覆盖本文件中的默认值"""
global AI_SYSTEM_PROMPT
if not os.path.exists(_SETTINGS_FILE):
"""加载本机私密配置,并一次性迁移旧版的 tracked JSON"""
source = (
_SETTINGS_FILE
if os.path.exists(_SETTINGS_FILE)
else _LEGACY_SETTINGS_FILE
)
if not os.path.exists(source):
return
try:
with open(_SETTINGS_FILE, encoding="utf-8") as f:
with open(source, encoding="utf-8") as f:
data = json.load(f)
except Exception:
return
g = globals()
for k in CONFIGURABLE_KEYS:
if k in data:
g[k] = data[k]
# 昵称/医院名可能被覆盖,重新渲染提示词
AI_SYSTEM_PROMPT = build_system_prompt()
if not isinstance(data, dict):
return
apply_settings(data, persist=False)
if source == _LEGACY_SETTINGS_FILE and not os.path.exists(_SETTINGS_FILE):
try:
save_settings()
except OSError:
# 只读安装仍可使用模板默认值;后台同步时会再次尝试持久化。
pass
load_settings()