diff --git a/README.md b/README.md
index e51943e..ee52c48 100644
--- a/README.md
+++ b/README.md
@@ -66,10 +66,14 @@ Optional input audio filtering can be enabled through `audio_filter`. See
Product endpoint:
```text
-ws://localhost:8000/ws-product
+ws://localhost:8000/ws-product?chatId=customer-chat-001
```
This endpoint uses a stable JSON/base64 protocol named `va.ws.v1`. It is meant for browser, mobile, or other product applications that should not depend on Pipecat's internal protobuf frame schema.
+For FastGPT sessions, pass `chatId` on the websocket URL. The engine uses
+that id for FastGPT server-side memory; if the id has existing FastGPT
+records, the assistant greets with `欢迎回来继续对话`, otherwise it uses the
+FastGPT app opener.
Start a session:
@@ -77,6 +81,7 @@ Start a session:
{
"type": "session.start",
"protocol": "va.ws.v1",
+ "chatId": "customer-chat-001",
"audio": {
"encoding": "pcm_s16le",
"sample_rate": 16000,
diff --git a/config/fastgpt.example.json b/config/fastgpt.example.json
index ca88edb..8ba2153 100644
--- a/config/fastgpt.example.json
+++ b/config/fastgpt.example.json
@@ -45,7 +45,8 @@
"user_speech_timeout_sec": 0.2
},
"agent": {
- "greeting_mode": "fastgpt_opener"
+ "greeting_mode": "fastgpt_opener",
+ "fastgpt_reconnect_greeting": "欢迎回来继续对话"
},
"services": {
"stt": {
@@ -67,7 +68,6 @@
"base_url": "http://localhost:3030",
"model": "my-voice-app",
"app_id": "6a153aed53e3f8d9f2744905",
- "chat_id": null,
"variables": {},
"detail": false,
"timeout_sec": 60.0
diff --git a/config/fastgpt.state.xfyun.json b/config/fastgpt.state.xfyun.json
index c992636..f9da9a0 100644
--- a/config/fastgpt.state.xfyun.json
+++ b/config/fastgpt.state.xfyun.json
@@ -49,6 +49,7 @@
},
"agent": {
"greeting_mode": "fastgpt_opener",
+ "fastgpt_reconnect_greeting": "欢迎回来继续对话,请告诉我准备好了之后继续办理",
"response_state": {
"enabled": true,
"tag": "state",
@@ -76,7 +77,6 @@
"base_url": "http://localhost:3030",
"model": "my-voice-app",
"app_id": "691eddaa53e3f8d9f25f1370",
- "chat_id": null,
"variables": {},
"detail": false,
"timeout_sec": 60.0
diff --git a/config/fastgpt.xfyun.dfn.json b/config/fastgpt.xfyun.dfn.json
index a40a3b4..cce3d90 100644
--- a/config/fastgpt.xfyun.dfn.json
+++ b/config/fastgpt.xfyun.dfn.json
@@ -56,6 +56,7 @@
},
"agent": {
"greeting_mode": "fastgpt_opener",
+ "fastgpt_reconnect_greeting": "欢迎回来继续对话",
"response_state": {
"enabled": false,
"tag": "state",
@@ -83,7 +84,6 @@
"base_url": "http://localhost:3030",
"model": "my-voice-app",
"app_id": "6a153aed53e3f8d9f2744905",
- "chat_id": null,
"variables": {},
"detail": false,
"timeout_sec": 60.0
diff --git a/config/fastgpt.xfyun.json b/config/fastgpt.xfyun.json
index b91ee6d..4af5d15 100644
--- a/config/fastgpt.xfyun.json
+++ b/config/fastgpt.xfyun.json
@@ -46,6 +46,7 @@
},
"agent": {
"greeting_mode": "fastgpt_opener",
+ "fastgpt_reconnect_greeting": "欢迎回来继续对话",
"response_state": {
"enabled": false,
"tag": "state",
@@ -73,7 +74,6 @@
"base_url": "http://localhost:3030",
"model": "my-voice-app",
"app_id": "6a153aed53e3f8d9f2744905",
- "chat_id": null,
"variables": {},
"detail": false,
"timeout_sec": 60.0
diff --git a/config/fastgpt.xfyun.supertts.json b/config/fastgpt.xfyun.supertts.json
index b454295..f643f84 100644
--- a/config/fastgpt.xfyun.supertts.json
+++ b/config/fastgpt.xfyun.supertts.json
@@ -49,6 +49,7 @@
},
"agent": {
"greeting_mode": "fastgpt_opener",
+ "fastgpt_reconnect_greeting": "欢迎回来继续对话",
"response_state": {
"enabled": false,
"tag": "state",
@@ -76,7 +77,6 @@
"base_url": "http://localhost:3030",
"model": "my-voice-app",
"app_id": "6a153aed53e3f8d9f2744905",
- "chat_id": null,
"variables": {},
"detail": false,
"timeout_sec": 60.0
diff --git a/engine/config.py b/engine/config.py
index 60a31a7..bb060fe 100644
--- a/engine/config.py
+++ b/engine/config.py
@@ -126,6 +126,7 @@ class AgentConfig:
system_prompt: str = "You are a helpful, friendly voice assistant."
greeting: str | None = None
greeting_mode: str = "generated"
+ fastgpt_reconnect_greeting: str = "欢迎回来继续对话"
response_state: ResponseStateConfig = field(default_factory=ResponseStateConfig)
@@ -134,7 +135,7 @@ class LLMConfig:
"""LLM backend selection via ``provider``.
Set ``provider`` to ``"openai"`` (alias ``"llm"``) for OpenAI-compatible chat
- completions, or ``"fastgpt"`` for FastGPT server-side memory via ``chat_id``.
+ completions, or ``"fastgpt"`` for FastGPT server-side memory via runtime ``chatId``.
"""
provider: str = "openai"
diff --git a/engine/fastgpt_llm.py b/engine/fastgpt_llm.py
index d3fb01b..935d0a7 100644
--- a/engine/fastgpt_llm.py
+++ b/engine/fastgpt_llm.py
@@ -271,6 +271,39 @@ class FastGPTLLMService(LLMService):
logger.warning(f"FastGPT chat init error: {exc}")
return None
+ async def has_chat_history(self) -> bool:
+ """Return whether FastGPT has persisted records for this chatId."""
+ if not self._app_id:
+ return False
+
+ try:
+ response = await self._client.get_chat_records(
+ appId=self._app_id,
+ chatId=self._chat_id,
+ offset=0,
+ pageSize=1,
+ )
+ response.raise_for_status()
+ data = response.json()
+ records = data.get("data", {}).get("list", [])
+ return isinstance(records, list) and bool(records)
+ except FastGPTError as exc:
+ logger.warning(f"FastGPT chat records failed: {exc}")
+ except httpx.HTTPError as exc:
+ logger.warning(f"FastGPT chat records HTTP error: {exc}")
+ except Exception as exc:
+ logger.warning(f"FastGPT chat records error: {exc}")
+ return False
+
+ async def fetch_session_greeting_text(self, reconnect_greeting: str) -> str | None:
+ """Use opener for a new chatId and a fixed greeting for reconnects."""
+ if await self.has_chat_history():
+ logger.info(f"FastGPT chatId={self._chat_id} has history; using reconnect greeting")
+ return reconnect_greeting.strip() or None
+
+ logger.info(f"FastGPT chatId={self._chat_id} has no history; using app opener")
+ return await self.fetch_welcome_text()
+
async def _close_active_response(self) -> None:
response = self._active_response
self._active_response = None
diff --git a/engine/pipeline.py b/engine/pipeline.py
index f0af44c..252ee3e 100644
--- a/engine/pipeline.py
+++ b/engine/pipeline.py
@@ -47,6 +47,18 @@ from .transcript_stream import ProductTranscriptStreamProcessor
from .turn_start import InterruptionGateUserTurnStartStrategy
+def _chat_id_from_websocket(websocket) -> str | None:
+ query_params = getattr(websocket, "query_params", None)
+ if not query_params:
+ return None
+
+ for name in ("chatId", "chat_id"):
+ value = query_params.get(name)
+ if isinstance(value, str) and value.strip():
+ return value.strip()
+ return None
+
+
async def run_voice_pipeline(websocket, config: EngineConfig) -> None:
await run_pipeline_with_serializer(
websocket,
@@ -93,7 +105,7 @@ async def run_pipeline_with_serializer(
stt = create_stt_service(config.services.stt, config.audio)
llm_config = config.services.llm
- chat_id = llm_config.chat_id or f"voice_{uuid.uuid4().hex[:16]}"
+ chat_id = _chat_id_from_websocket(websocket) or f"voice_{uuid.uuid4().hex[:16]}"
llm = create_llm_service(
llm_config,
chat_id=chat_id,
@@ -200,7 +212,9 @@ async def run_pipeline_with_serializer(
await task.queue_frames([TTSSpeakFrame(config.agent.greeting)])
elif config.agent.greeting_mode == "fastgpt_opener":
if isinstance(llm, FastGPTLLMService):
- welcome = await llm.fetch_welcome_text()
+ welcome = await llm.fetch_session_greeting_text(
+ config.agent.fastgpt_reconnect_greeting
+ )
if welcome:
await task.queue_frames([TTSSpeakFrame(welcome)])
else:
diff --git a/engine/product_protocol.py b/engine/product_protocol.py
index 9de8af0..bce39dc 100644
--- a/engine/product_protocol.py
+++ b/engine/product_protocol.py
@@ -103,10 +103,12 @@ class ProductWebsocketSerializer(FrameSerializer):
message_type = message.get("type")
if message_type == "session.start":
+ chat_id = message.get("chatId") or message.get("chat_id")
return InputTransportMessageFrame(
message={
"type": "session.started",
"protocol": self.protocol,
+ "chatId": chat_id if isinstance(chat_id, str) else None,
"audio": {
"encoding": "pcm_s16le",
"sample_rate": self._sample_rate,
diff --git a/engine/services.py b/engine/services.py
index 20778bc..8fbd916 100644
--- a/engine/services.py
+++ b/engine/services.py
@@ -61,7 +61,7 @@ def create_llm_service(
return FastGPTLLMService(
api_key=config.api_key,
base_url=config.base_url or "http://localhost:3000",
- chat_id=chat_id or config.chat_id,
+ chat_id=chat_id,
app_id=config.app_id,
greeting_prompt=greeting_prompt,
timeout=config.timeout_sec,
diff --git a/examples/webpage/app.js b/examples/webpage/app.js
index ea6010a..2648e69 100644
--- a/examples/webpage/app.js
+++ b/examples/webpage/app.js
@@ -44,6 +44,7 @@ function defaultWsUrl() {
const els = {
url: document.getElementById("ws-url"),
+ chatId: document.getElementById("chat-id"),
connectBtn: document.getElementById("connect-btn"),
statusDot: document.getElementById("status-dot"),
statusText: document.getElementById("status-text"),
@@ -69,6 +70,21 @@ const els = {
sendBtn: document.getElementById("send-btn"),
};
+function wsUrlWithChatId() {
+ const rawUrl = (els.url.value || "").trim();
+ const chatId = (els.chatId.value || "").trim();
+ if (!rawUrl || !chatId) return rawUrl;
+
+ try {
+ const url = new URL(rawUrl, location.href);
+ url.searchParams.set("chatId", chatId);
+ return url.href;
+ } catch (_) {
+ const separator = rawUrl.includes("?") ? "&" : "?";
+ return `${rawUrl}${separator}chatId=${encodeURIComponent(chatId)}`;
+ }
+}
+
const state = {
ws: null,
connected: false,
@@ -110,6 +126,7 @@ function setStatus(kind, text) {
}
function setConnectButton() {
+ els.chatId.disabled = state.connected || state.connecting;
if (state.connecting) {
els.connectBtn.textContent = "Connecting…";
els.connectBtn.disabled = true;
@@ -874,7 +891,7 @@ function handleEvent(event) {
async function connect() {
if (state.connected || state.connecting) return;
- const url = (els.url.value || "").trim();
+ const url = wsUrlWithChatId();
if (!url) {
setStatus("error", "Missing URL");
return;
@@ -912,6 +929,7 @@ async function connect() {
state.ws = ws;
ws.addEventListener("open", () => {
+ const chatId = (els.chatId.value || "").trim();
const startMessage = {
type: "session.start",
protocol: PROTOCOL,
@@ -921,6 +939,9 @@ async function connect() {
channels: CHANNELS,
},
};
+ if (chatId) {
+ startMessage.chatId = chatId;
+ }
state.connecting = false;
state.connected = true;
diff --git a/examples/webpage/index.html b/examples/webpage/index.html
index 0d0f76a..deb87f9 100644
--- a/examples/webpage/index.html
+++ b/examples/webpage/index.html
@@ -25,6 +25,16 @@
autocomplete="off"
/>
+
diff --git a/examples/webpage/styles.css b/examples/webpage/styles.css
index 0854611..b50e872 100644
--- a/examples/webpage/styles.css
+++ b/examples/webpage/styles.css
@@ -304,6 +304,10 @@ body {
flex: 1;
}
+.connection__field--chat {
+ flex: 0 1 220px;
+}
+
.connection__field span {
font-size: 11px;
color: var(--text-dim);
@@ -1013,6 +1017,10 @@ body {
align-items: stretch;
}
+ .connection__field--chat {
+ flex-basis: auto;
+ }
+
.ws-log__entry,
.ws-log__group-header {
grid-template-columns: 54px 38px minmax(0, 1fr);