feat: 增加 screen_id 参数以支持针对特定屏幕的工具执行,优化大屏控制逻辑
This commit is contained in:
+12
-12
@@ -14,34 +14,34 @@ log = logging.getLogger("dpm.ai_tools")
|
|||||||
|
|
||||||
# ---------------- 工具执行 ----------------
|
# ---------------- 工具执行 ----------------
|
||||||
|
|
||||||
def execute_tool(tool):
|
def execute_tool(tool, screen_id=""):
|
||||||
"""执行一个工具调用:发布 MQTT 控制指令,返回是否发布成功"""
|
"""执行一个工具调用:发布 MQTT 控制指令(带发起屏幕 screen_id,只发到该屏),返回是否发布成功"""
|
||||||
t = tool.get("type")
|
t = tool.get("type")
|
||||||
params = tool.get("params") or {}
|
params = tool.get("params") or {}
|
||||||
if t == "navigate":
|
if t == "navigate":
|
||||||
return hub.publish_command("navigate", {"page": params.get("page", "home")}).get("published", False)
|
return hub.publish_command("navigate", {"page": params.get("page", "home")}, screen_id=screen_id).get("published", False)
|
||||||
if t == "control":
|
if t == "control":
|
||||||
action = params.get("action")
|
action = params.get("action")
|
||||||
if action in ("play", "pause", "next", "prev"):
|
if action in ("play", "pause", "next", "prev"):
|
||||||
return hub.publish_command(action).get("published", False)
|
return hub.publish_command(action, screen_id=screen_id).get("published", False)
|
||||||
if action == "set_mode":
|
if action == "set_mode":
|
||||||
return hub.publish_command("set_mode", {"mode": params.get("mode", "auto")}).get("published", False)
|
return hub.publish_command("set_mode", {"mode": params.get("mode", "auto")}, screen_id=screen_id).get("published", False)
|
||||||
return False
|
return False
|
||||||
if t == "alert":
|
if t == "alert":
|
||||||
return hub.publish_command("alert", {"title": params.get("title", "提示"), "content": params.get("content", "")}).get("published", False)
|
return hub.publish_command("alert", {"title": params.get("title", "提示"), "content": params.get("content", "")}, screen_id=screen_id).get("published", False)
|
||||||
if t == "show_card":
|
if t == "show_card":
|
||||||
return hub.publish_command("show_card", params).get("published", False)
|
return hub.publish_command("show_card", params, screen_id=screen_id).get("published", False)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def run_tools(tools):
|
def run_tools(tools, screen_id=""):
|
||||||
"""执行一组工具,返回 [{type, ok}] 并汇总是否全部发布成功"""
|
"""执行一组工具(带发起屏幕 screen_id),返回 [{type, ok}] 并汇总是否全部发布成功"""
|
||||||
results = []
|
results = []
|
||||||
all_ok = True
|
all_ok = True
|
||||||
for tool in tools or []:
|
for tool in tools or []:
|
||||||
ok = bool(execute_tool(tool))
|
ok = bool(execute_tool(tool, screen_id))
|
||||||
log.info("ai_tools: 执行工具 %s params=%s -> %s", tool.get("type"),
|
log.info("ai_tools: 执行工具 %s params=%s screen=%s -> %s", tool.get("type"),
|
||||||
json.dumps(tool.get("params") or {}, ensure_ascii=False), ok)
|
json.dumps(tool.get("params") or {}, ensure_ascii=False), screen_id or "all", ok)
|
||||||
results.append({"type": tool.get("type"), "ok": ok})
|
results.append({"type": tool.get("type"), "ok": ok})
|
||||||
all_ok = all_ok and ok
|
all_ok = all_ok and ok
|
||||||
return results, all_ok
|
return results, all_ok
|
||||||
|
|||||||
+10
-10
@@ -135,32 +135,32 @@ def _chat_once(messages, with_tools=True):
|
|||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def _fallback(messages):
|
def _fallback(messages, screen_id=""):
|
||||||
"""DashScope 不可用时回退本地规则引擎"""
|
"""DashScope 不可用时回退本地规则引擎"""
|
||||||
from .ai_tools import run_chat as rule_run
|
from .ai_tools import run_chat as rule_run
|
||||||
|
|
||||||
result = rule_run(messages)
|
result = rule_run(messages)
|
||||||
# 规则引擎产出的工具同样执行(MQTT 广播),并汇总发布结果
|
# 规则引擎产出的工具同样执行(MQTT 广播,带发起屏幕),并汇总发布结果
|
||||||
_, all_ok = _exec_all(result.get("tools", []))
|
_, all_ok = _exec_all(result.get("tools", []), screen_id)
|
||||||
result["model"] = "rule-engine"
|
result["model"] = "rule-engine"
|
||||||
result["mqtt_published"] = all_ok
|
result["mqtt_published"] = all_ok
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _exec_all(tools):
|
def _exec_all(tools, screen_id=""):
|
||||||
"""执行工具列表,返回 (results, all_ok)"""
|
"""执行工具列表(带发起屏幕 screen_id,工具结果只发到该屏),返回 (results, all_ok)"""
|
||||||
from .ai_tools import run_tools
|
from .ai_tools import run_tools
|
||||||
|
|
||||||
results, all_ok = run_tools(tools)
|
results, all_ok = run_tools(tools, screen_id)
|
||||||
return results, all_ok
|
return results, all_ok
|
||||||
|
|
||||||
|
|
||||||
def run_chat(messages):
|
def run_chat(messages, screen_id=""):
|
||||||
"""入口:{reply, tools, model}
|
"""入口:{reply, tools, model}
|
||||||
tools 已在后端执行(MQTT 广播),返回值供请求端本地同步执行"""
|
tools 已在后端执行(MQTT 广播,带发起屏幕 screen_id),返回值供请求端本地同步执行"""
|
||||||
if not settings.DASHSCOPE_API_KEY:
|
if not settings.DASHSCOPE_API_KEY:
|
||||||
log.warning("未配置 DASHSCOPE_API_KEY,使用本地规则引擎")
|
log.warning("未配置 DASHSCOPE_API_KEY,使用本地规则引擎")
|
||||||
return _fallback(messages)
|
return _fallback(messages, screen_id)
|
||||||
|
|
||||||
# 注入园区知识库(标准资料口径):按用户 query 检索命中段落动态注入,
|
# 注入园区知识库(标准资料口径):按用户 query 检索命中段落动态注入,
|
||||||
# 替代原 1200 字截断全量注入(避免尾部知识丢失)。无命中时回退 brief()。
|
# 替代原 1200 字截断全量注入(避免尾部知识丢失)。无命中时回退 brief()。
|
||||||
@@ -226,7 +226,7 @@ def run_chat(messages):
|
|||||||
"content": json.dumps({"ok": True}, ensure_ascii=False),
|
"content": json.dumps({"ok": True}, ensure_ascii=False),
|
||||||
})
|
})
|
||||||
# 工具执行(MQTT 广播),并汇总发布结果
|
# 工具执行(MQTT 广播),并汇总发布结果
|
||||||
results, all_ok = _exec_all(executed)
|
results, all_ok = _exec_all(executed, screen_id)
|
||||||
log.info("llm: 工具执行结果 %s", results)
|
log.info("llm: 工具执行结果 %s", results)
|
||||||
for t in executed:
|
for t in executed:
|
||||||
log.info("llm: 执行 %s %s", t["type"], json.dumps(t["params"], ensure_ascii=False))
|
log.info("llm: 执行 %s %s", t["type"], json.dumps(t["params"], ensure_ascii=False))
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class ChatMessage(BaseModel):
|
|||||||
|
|
||||||
class ChatBody(BaseModel):
|
class ChatBody(BaseModel):
|
||||||
messages: list[ChatMessage]
|
messages: list[ChatMessage]
|
||||||
|
screen_id: str = "" # 发起对话的屏幕设备 id(工具结果只发到该屏)
|
||||||
|
|
||||||
|
|
||||||
# ==================== 认证 / 设置 ====================
|
# ==================== 认证 / 设置 ====================
|
||||||
@@ -451,7 +452,7 @@ async def ai_chat(body: ChatBody):
|
|||||||
"""大模型对话:工具调用(切页/控制/卡片/通知)经 MQTT 广播到所有大屏
|
"""大模型对话:工具调用(切页/控制/卡片/通知)经 MQTT 广播到所有大屏
|
||||||
返回 {reply, tools, model};tools 供请求端本地同步执行"""
|
返回 {reply, tools, model};tools 供请求端本地同步执行"""
|
||||||
messages = [m.model_dump() for m in body.messages]
|
messages = [m.model_dump() for m in body.messages]
|
||||||
result = llm_run_chat(messages)
|
result = llm_run_chat(messages, body.screen_id or "")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -604,6 +605,7 @@ def vision_llm(body: VisionLlmBody):
|
|||||||
class ToolsExecBody(BaseModel):
|
class ToolsExecBody(BaseModel):
|
||||||
name: str = ""
|
name: str = ""
|
||||||
args: dict = {}
|
args: dict = {}
|
||||||
|
screen_id: str = "" # 发起语音对话的屏幕设备 id(工具结果只发到该屏)
|
||||||
|
|
||||||
@router.post("/api/tools/exec")
|
@router.post("/api/tools/exec")
|
||||||
def tools_exec(body: ToolsExecBody):
|
def tools_exec(body: ToolsExecBody):
|
||||||
@@ -612,10 +614,10 @@ def tools_exec(body: ToolsExecBody):
|
|||||||
if not name:
|
if not name:
|
||||||
return {"ok": False, "error": "missing tool name"}
|
return {"ok": False, "error": "missing tool name"}
|
||||||
from .tools import exec_tool
|
from .tools import exec_tool
|
||||||
result = exec_tool(name, body.args or {})
|
result = exec_tool(name, body.args or {}, body.screen_id or "")
|
||||||
log.info("api: /api/tools/exec name=%s args=%s result=%s", name,
|
log.info("api: /api/tools/exec name=%s args=%s screen=%s result=%s", name,
|
||||||
json.dumps(body.args or {}, ensure_ascii=False)[:300],
|
json.dumps(body.args or {}, ensure_ascii=False)[:300],
|
||||||
str(result)[:400])
|
body.screen_id or "all", str(result)[:400])
|
||||||
return {"ok": True, "result": result}
|
return {"ok": True, "result": result}
|
||||||
|
|
||||||
@router.get("/api/kb/brief")
|
@router.get("/api/kb/brief")
|
||||||
|
|||||||
+11
-7
@@ -60,15 +60,15 @@ def _query_companies(args):
|
|||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
def _control_display(args):
|
def _control_display(args, screen_id=""):
|
||||||
"""大屏显示控制:切页 / 弹通知 / 媒体播放暂停(经 MQTT 广播到大屏)"""
|
"""大屏显示控制:切页 / 弹通知 / 媒体播放暂停(经 MQTT 按发起屏幕下发)"""
|
||||||
from .mqtt import hub
|
from .mqtt import hub
|
||||||
action = (args.get("action") or "").strip()
|
action = (args.get("action") or "").strip()
|
||||||
target = (args.get("target") or "").strip()
|
target = (args.get("target") or "").strip()
|
||||||
if action not in ("switch_page", "alert", "media_play", "media_pause"):
|
if action not in ("switch_page", "alert", "media_play", "media_pause"):
|
||||||
return json.dumps({"error": f"不支持的 action: {action}"}, ensure_ascii=False)
|
return json.dumps({"error": f"不支持的 action: {action}"}, ensure_ascii=False)
|
||||||
params = {"target": target} if target else {}
|
params = {"target": target} if target else {}
|
||||||
ok = hub.publish_command(action, params).get("published", False)
|
ok = hub.publish_command(action, params, screen_id=screen_id).get("published", False)
|
||||||
return json.dumps({"ok": ok, "action": action, "target": target}, ensure_ascii=False)
|
return json.dumps({"ok": ok, "action": action, "target": target}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -125,15 +125,19 @@ def tool_schemas():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def exec_tool(name: str, args: dict):
|
def exec_tool(name: str, args: dict, screen_id: str = ""):
|
||||||
"""执行工具 → 返回给 LLM 的字符串结果(JSON)"""
|
"""执行工具 → 返回给 LLM 的字符串结果(JSON)。
|
||||||
|
控制类工具(control_display)按发起屏幕 screen_id 只下发到该屏。"""
|
||||||
t = TOOLS.get(name)
|
t = TOOLS.get(name)
|
||||||
if not t:
|
if not t:
|
||||||
log.warning("tools: 未知工具 %s", name)
|
log.warning("tools: 未知工具 %s", name)
|
||||||
return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
|
return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
|
||||||
try:
|
try:
|
||||||
result = t["fn"](args or {})
|
if name == "control_display":
|
||||||
log.info("tools: 执行工具 %s args=%s", name, json.dumps(args, ensure_ascii=False))
|
result = _control_display(args or {}, screen_id)
|
||||||
|
else:
|
||||||
|
result = t["fn"](args or {})
|
||||||
|
log.info("tools: 执行工具 %s args=%s screen=%s", name, json.dumps(args, ensure_ascii=False), screen_id or "all")
|
||||||
log.info("tools: 结果 -> %s", _trunc(result, 1000))
|
log.info("tools: 结果 -> %s", _trunc(result, 1000))
|
||||||
return result
|
return result
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
if (btn.id === 'mqttAiSend') {
|
if (btn.id === 'mqttAiSend') {
|
||||||
var text = (document.getElementById('mqttAiInput').value || '').trim();
|
var text = (document.getElementById('mqttAiInput').value || '').trim();
|
||||||
if (!text) { toast('请输入问题', 'error'); return; }
|
if (!text) { toast('请输入问题', 'error'); return; }
|
||||||
post('/api/display/command', { action: 'ai_input', params: { text: text } }, function () {
|
post('/api/display/command', { action: 'ai_input', params: { text: text }, screen_id: screenId, screen_role: screenRole }, function () {
|
||||||
toast('已发送问题到大屏 AI 页');
|
toast('已发送问题到大屏 AI 页');
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -203,17 +203,20 @@
|
|||||||
if (btn.id === 'mqttAlertSend') {
|
if (btn.id === 'mqttAlertSend') {
|
||||||
var alertText = (document.getElementById('mqttAlertText').value || '').trim();
|
var alertText = (document.getElementById('mqttAlertText').value || '').trim();
|
||||||
if (!alertText) { toast('请输入通知内容', 'error'); return; }
|
if (!alertText) { toast('请输入通知内容', 'error'); return; }
|
||||||
post('/api/display/command', { action: 'alert', params: { text: alertText } }, function () {
|
post('/api/display/command', { action: 'alert', params: { text: alertText }, screen_id: screenId, screen_role: screenRole }, function () {
|
||||||
toast('通知已广播');
|
toast('通知已发送');
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面切换
|
// 页面切换(绑定目标屏幕:screen_id 精确某台;空=全部)
|
||||||
if (btn.hasAttribute('data-nav')) {
|
if (btn.hasAttribute('data-nav')) {
|
||||||
var page = btn.getAttribute('data-nav');
|
var page = btn.getAttribute('data-nav');
|
||||||
post('/api/display/command', { action: 'navigate', params: { page: page } }, function () {
|
var tgtTxt = screenId
|
||||||
toast('已切换大屏至「' + btn.textContent.trim() + '」');
|
? (screenRole === 'main' ? '主屏' : screenRole === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10)
|
||||||
|
: '全部';
|
||||||
|
post('/api/display/command', { action: 'navigate', params: { page: page }, screen_id: screenId, screen_role: screenRole }, function () {
|
||||||
|
toast('已切换大屏至「' + btn.textContent.trim() + '」 → ' + tgtTxt);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Icon from './Icons';
|
|||||||
import ShowCard from './ShowCard';
|
import ShowCard from './ShowCard';
|
||||||
import { getApiBase as API_BASE } from '../config';
|
import { getApiBase as API_BASE } from '../config';
|
||||||
import { Sfx } from '../utils/sounds';
|
import { Sfx } from '../utils/sounds';
|
||||||
import { getMqttStatus } from '../utils/mqtt';
|
import { getMqttStatus, getClientId } from '../utils/mqtt';
|
||||||
import { aiStatus } from '../utils/statusBus';
|
import { aiStatus } from '../utils/statusBus';
|
||||||
import { renderMarkdown } from '../voice/markdown.jsx';
|
import { renderMarkdown } from '../voice/markdown.jsx';
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ export default function AiChatPanel() {
|
|||||||
const res = await fetch(`${API_BASE()}/api/ai/chat`, {
|
const res = await fetch(`${API_BASE()}/api/ai/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ messages: history }),
|
body: JSON.stringify({ messages: history, screen_id: getClientId() }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
data = await res.json();
|
data = await res.json();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|||||||
import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js';
|
import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js';
|
||||||
import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js';
|
import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js';
|
||||||
import { getApiBase as API_BASE, getVoiceWsUrl } from '../config';
|
import { getApiBase as API_BASE, getVoiceWsUrl } from '../config';
|
||||||
|
import { getClientId } from '../utils/mqtt';
|
||||||
import { aiStatus } from '../utils/statusBus';
|
import { aiStatus } from '../utils/statusBus';
|
||||||
import '../styles/datascreen.css';
|
import '../styles/datascreen.css';
|
||||||
import './voice-original.css';
|
import './voice-original.css';
|
||||||
@@ -242,7 +243,7 @@ export default function VoiceAssistant() {
|
|||||||
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
|
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name, args }),
|
body: JSON.stringify({ name, args, screen_id: getClientId() }),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
output = data?.result ?? JSON.stringify(data);
|
output = data?.result ?? JSON.stringify(data);
|
||||||
|
|||||||
Reference in New Issue
Block a user