# Conflicts:
#	docs/视频快处智能信息采集机器人交互接口文档V1.0.10.docx
#	src/api/endpoints.py
#	src/core/fastgpt_client.py
This commit is contained in:
Eric Wang
2026-10-11 21:39:49 +08:00
31 changed files with 4005 additions and 40 deletions
+2 -5
View File
@@ -3,11 +3,8 @@ SECRET_KEY=your_secret_key
DEBUG=True
ANALYSIS_SERVICE_URL=http://127.0.0.1:3030
ANALYSIS_AUTH_TOKEN=fastgpt-hSPnXMoBNGVAEpTLkQT3YfAnN26gQSyvLd4ABL1MRDoh68nL4RDlopFHXqmH8
APP_ID=683ea1bc86197e19f71fc1ae
DELETE_SESSION_URL=http://127.0.0.1:3030/api/core/chat/delHistory?chatId={chatId}&appId={appId}
DELETE_CHAT_URL=http://127.0.0.1:3030/api/core/chat/item/delete?contentId={contentId}&chatId={chatId}&appId={appId}
GET_CHAT_RECORDS_URL=http://127.0.0.1:3030/api/core/chat/getPaginationRecords
ANALYSIS_AUTH_TOKEN=replace-with-fastgpt-api-key
APP_ID=replace-with-fastgpt-app-id
# Voice demo (Pipecat /ws-product). Relative to project root, or an absolute path.
VOICE_CONFIG=config/voice.json
+40 -14
View File
@@ -57,6 +57,13 @@ def normalize_stage_code(stage_code: str) -> str:
return stage_code
def external_stage_code(client_mode: str, stage_code: str) -> str:
"""Apply the external namespace required by the client mode."""
if client_mode == "browser_addon":
return f"browser_addon.{stage_code}"
return stage_code
def extract_first_state_and_clean_content(text: str) -> tuple[str | None, str]:
"""Return the first state code and content with all state tags removed."""
match = STATE_TAG_PATTERN.search(text)
@@ -73,7 +80,7 @@ def format_set_info_input(payload: dict, include_input_info: bool) -> str:
async def delete_last_two_chat_records(
client: AsyncChatClient,
session_id: str
session_id: str,
) -> None:
"""Delete the last two chat records."""
try:
@@ -126,17 +133,19 @@ async def chat(
):
"""Handle chat completion request."""
json_data = request.model_dump()
client_mode = json_data['clientMode']
need_form_update = json_data.get('needFormUpdate', False)
use_text_chunk = json_data.get('useTextChunk', False)
chat_input = ChatInput(
session_id=json_data['sessionId'],
text=json_data['text'],
need_form_update=need_form_update,
client_mode=client_mode,
)
request_started_at = time.perf_counter()
logger.info(
"Chat request received "
f"sessionId={json_data['sessionId']} stream={stream} "
f"sessionId={json_data['sessionId']} clientMode={client_mode} stream={stream} "
f"needFormUpdate={need_form_update} useTextChunk={use_text_chunk} "
f"text_len={len(json_data.get('text', ''))} "
f"input={json_data.get('text', '')!r}"
@@ -280,11 +289,17 @@ async def chat(
if state_code:
# Apply logic to map/adjust state code
nextStageCode = normalize_stage_code(state_code)
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
normalized_stage_code = normalize_stage_code(state_code)
nextStageCode = external_stage_code(
client_mode,
normalized_stage_code,
)
nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '')
logger.info(
"FastGPT stream stage_code parsed "
f"sessionId={json_data['sessionId']} "
f"clientMode={client_mode} "
f"rawStageCode={state_code} "
f"nextStageCode={nextStageCode} nextStage={nextStage}"
)
@@ -426,15 +441,17 @@ async def chat(
logger.error(f"content既不是list也不是str, type: {type(content)}")
raise ValueError("大模型回复不是list也不是str")
nextStageCode = content_stage_code or result.status_code
if not nextStageCode:
raw_stage_code = content_stage_code or result.status_code
if not raw_stage_code:
raise ValueError("大模型回复中缺少state")
nextStageCode = normalize_stage_code(nextStageCode)
nextStage = STATUS_CODE_MAP.get(nextStageCode, '')
normalized_stage_code = normalize_stage_code(raw_stage_code)
nextStageCode = external_stage_code(client_mode, normalized_stage_code)
nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '')
form_update = result.form_update
logger.info(
"Chat non-stream completed "
f"sessionId={json_data['sessionId']} "
f"clientMode={client_mode} rawStageCode={raw_stage_code} "
f"duration_ms={(time.perf_counter() - request_started_at) * 1000:.1f} "
f"nextStageCode={nextStageCode} nextStage={nextStage} "
f"output_len={len(content)} formUpdate_type={type(form_update).__name__} "
@@ -468,7 +485,7 @@ async def chat(
@router.post("/set_info", response_model=ProcessResponse_set)
async def set_info(
request: ProcessRequest_set,
client: AsyncChatClient = Depends(get_fastgpt_client)
client: AsyncChatClient = Depends(get_fastgpt_client),
):
"""Set information in chat state."""
json_data = request.model_dump()
@@ -504,7 +521,10 @@ async def set_info(
)
try:
await delete_last_two_chat_records(client, json_data['sessionId'])
await delete_last_two_chat_records(
client,
json_data['sessionId'],
)
except Exception as e:
logger.error(f"Error deleting chat records: {e}")
return ProcessResponse_set(
@@ -533,7 +553,10 @@ async def set_info(
response.raise_for_status()
# Delete records again after update
await delete_last_two_chat_records(client, json_data['sessionId'])
await delete_last_two_chat_records(
client,
json_data['sessionId'],
)
return ProcessResponse_set(
sessionId=json_data['sessionId'],
@@ -554,7 +577,7 @@ async def set_info(
@router.post("/get_info", response_model=ProcessResponse_get)
async def get_info(
request: ProcessRequest_get,
client: AsyncChatClient = Depends(get_fastgpt_client)
client: AsyncChatClient = Depends(get_fastgpt_client),
):
"""Get information from chat state."""
json_data = request.model_dump()
@@ -591,7 +614,10 @@ async def get_info(
)
try:
await delete_last_two_chat_records(client, json_data['sessionId'])
await delete_last_two_chat_records(
client,
json_data['sessionId'],
)
except Exception as e:
logger.error(f"Error deleting records: {e}")
return ProcessResponse_get(
@@ -656,7 +682,7 @@ async def get_info(
@router.delete("/delete_session", response_model=ProcessResponse_delete_session)
async def delete_session(
request: ProcessRequest_delete_session,
client: AsyncChatClient = Depends(get_fastgpt_client)
client: AsyncChatClient = Depends(get_fastgpt_client),
):
"""Delete a chat session."""
json_data = request.model_dump()
+1
View File
@@ -12,6 +12,7 @@ class ChatInput:
session_id: str
text: str
need_form_update: bool = False
client_mode: str = ""
@dataclass(frozen=True)
+24 -7
View File
@@ -18,7 +18,7 @@ from .chat import (
)
FORM_EXTRACT_MODULE_NAME = "文本内容提取事故信息"
FORM_EXTRACT_MODULE_NAME = "更新表单"
def _parse_json_value(value: Any) -> Any:
@@ -46,12 +46,23 @@ def _extract_form_update(nodes: Any) -> Any:
if node.get("moduleName") != FORM_EXTRACT_MODULE_NAME:
continue
extract_result = node.get("extractResult", {})
if not isinstance(extract_result, dict):
# 代码节点返回值在 customOutputs;兼容旧版/其他形态
outputs = node.get("customOutputs")
if not isinstance(outputs, dict):
outputs = node.get("nodeOutputs")
if not isinstance(outputs, dict):
outputs = node.get("extractResult")
if not isinstance(outputs, dict):
return {}
form_update = extract_result.get("formUpdate", "")
return _parse_json_value(form_update) if form_update else {}
form_update = outputs.get("formUpdate", "")
if form_update in ("", None):
return {}
parsed = _parse_json_value(form_update)
# 无变更时代码节点常返回 {},直接透传空对象
if not isinstance(parsed, dict):
return {}
return parsed
return {}
@@ -88,7 +99,10 @@ class FastGPTBackend:
chatId=chat_input.session_id,
stream=True,
detail=True,
variables={"needFormUpdate": chat_input.need_form_update},
variables={
"needFormUpdate": chat_input.need_form_update,
"clientMode": chat_input.client_mode,
},
)
async for event in aiter_stream_events(response):
@@ -123,7 +137,10 @@ class FastGPTBackend:
chatId=chat_input.session_id,
stream=False,
detail=True,
variables={"needFormUpdate": chat_input.need_form_update},
variables={
"needFormUpdate": chat_input.need_form_update,
"clientMode": chat_input.client_mode,
},
)
response.raise_for_status()
data = response.json()
+4 -6
View File
@@ -14,17 +14,15 @@ class Config:
FASTGPT_API_KEY = os.getenv("ANALYSIS_AUTH_TOKEN")
FASTGPT_BASE_URL = os.getenv("ANALYSIS_SERVICE_URL")
FASTGPT_APP_ID = os.getenv("APP_ID")
DELETE_SESSION_URL = os.getenv("DELETE_SESSION_URL")
DELETE_CHAT_URL = os.getenv("DELETE_CHAT_URL")
GET_CHAT_RECORDS_URL = os.getenv("GET_CHAT_RECORDS_URL")
@classmethod
def validate(cls):
"""Validate required configuration."""
required = [
("FASTGPT_API_KEY", cls.FASTGPT_API_KEY),
("FASTGPT_APP_ID", cls.FASTGPT_APP_ID),
("FASTGPT_BASE_URL (ANALYSIS_SERVICE_URL)", cls.FASTGPT_BASE_URL),
("FASTGPT_API_KEY (ANALYSIS_AUTH_TOKEN)", cls.FASTGPT_API_KEY),
("FASTGPT_APP_ID (APP_ID)", cls.FASTGPT_APP_ID),
]
missing = [name for name, value in required if not value]
if missing:
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
+3 -4
View File
@@ -6,7 +6,7 @@ from ..backends.chat import ChatBackend
from ..backends.fastgpt import FastGPTBackend
from .config import Config
# Global client instance
_fastgpt_client: AsyncChatClient | None = None
_chat_backend: ChatBackend | None = None
@@ -16,8 +16,7 @@ async def lifespan(app: FastAPI):
"""Manage FastGPT client lifecycle."""
global _chat_backend, _fastgpt_client
Config.validate()
# Initialize client
_fastgpt_client = AsyncChatClient(
api_key=Config.FASTGPT_API_KEY,
base_url=Config.FASTGPT_BASE_URL,
@@ -39,7 +38,7 @@ async def lifespan(app: FastAPI):
def get_fastgpt_client() -> AsyncChatClient:
"""Get the FastGPT client instance."""
"""Get the initialized FastGPT client."""
if _fastgpt_client is None:
raise RuntimeError("FastGPT client not initialized")
return _fastgpt_client
+15 -3
View File
@@ -1,10 +1,22 @@
from pydantic import BaseModel, Field
from typing import Any, Optional
from pydantic import BaseModel, BeforeValidator, Field
from typing import Annotated, Any, Literal, Optional
ClientMode = Literal["direct", "browser_addon"]
def normalize_client_mode(value: Any) -> Any:
if value is None or (isinstance(value, str) and value.strip() == ""):
return "direct"
return value
NormalizedClientMode = Annotated[ClientMode, BeforeValidator(normalize_client_mode)]
class ProcessRequest_chat(BaseModel):
sessionId: str = Field(..., max_length=64)
timeStamp: str = Field(..., max_length=32)
text: str = Field(...)
clientMode: NormalizedClientMode = "direct"
needFormUpdate: bool = False
useTextChunk: bool = False
@@ -14,7 +26,7 @@ class ProcessResponse_chat(BaseModel):
outputText: str = Field(...)
formUpdate: Any = Field(default_factory=dict)
nextStage: str = Field(..., max_length=32)
nextStageCode: str = Field(..., max_length=4)
nextStageCode: str = Field(..., max_length=32)
code: str = Field(..., max_length=4)
msg: Optional[str] = None