feat: 添加后端部署和API基本检索的Docker Compose配置

- 引入新的`docker-compose.yml`文件,便于后端和MQTT代理(EMQX)的部署。
- 更新多个组件中的API基本检索,使用函数进行动态解析。
- 加强了多个组件中API调用的错误处理和日志记录。
- 优化了AI聊天面板离线场景的回退响应。
- 更新DataScreen和MediaScreen组件中的数据显示和统计,以反映准确的指标。
- 重构MQTT连接逻辑,以支持动态凭证和客户端ID。
This commit is contained in:
Pine
2026-08-18 06:50:50 +08:00
parent fbf2eccad2
commit 18d28be943
23 changed files with 621 additions and 142 deletions
+47
View File
@@ -0,0 +1,47 @@
# ============================================================
# Docker 构建上下文忽略(构建根目录 = 项目根目录)
# 说明:仅 backend/ 会 COPY 进镜像;media 内的媒体文件必须保留,
# 因此图片/视频后缀不做全局排除,只锚定根目录的开发素材。
# ============================================================
# ---- 版本控制 / 元数据 ----
.git
.gitignore
.DS_Store
**/.DS_Store
# ---- 前端 / 桌面端(不属于后端镜像) ----
src
src-tauri
dist
public
node_modules
package.json
yarn.lock
package-lock.json
vite.config.js
# ---- 根目录开发素材 / 标准资料(不进入镜像) ----
/IMG_7801.JPG
/空间布局.png
/详细的布局.png
/数据资料
*.zip
*.wps
*.xlsx
*.xls
*.pdf
*.ai
/昆明市创业园宣传册.pdf
# ---- 后端内部:排除开发产物与密钥 ----
backend/.venv
backend/.env
backend/**/__pycache__
backend/**/*.pyc
backend/.pytest_cache
backend/.mypy_cache
backend/.ruff_cache
backend/.ultralytics
backend/kb_index.json
backend/README.md
+60
View File
@@ -0,0 +1,60 @@
# syntax=docker/dockerfile:1
# ============================================================
# OPC 智能园区后端(FastAPI + s2s 语音栈)Docker 镜像
# 构建: docker build -f backend/Dockerfile -t dpm-backend .
# 运行: docker compose up -d --build (推荐,见根目录 docker-compose.yml
# 说明:
# · 依赖使用 uv 按 uv.lock 锁定安装(Python 3.12
# · vendor/s2s-cloud(补丁版语音栈)以 --no-deps 方式安装,避免拖入多余依赖
# · silero VAD / nltk 数据 / YOLO 模型随镜像打包,运行期不联网下载
# · media / knowledge / data.json 挂载为持久卷(数据与知识库可热更新)
# ============================================================
FROM python:3.12-slim-bookworm
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/opt/venv \
PATH="/opt/venv/bin:$PATH"
# 系统依赖:libsndfile1soundfile 音频解码)、libgomp1torch)、
# libgl1/libglib2.0-0OpenCV/ultralytics 图像处理)
RUN apt-get update && apt-get install -y --no-install-recommends \
libsndfile1 \
libgomp1 \
libgl1 \
libglib2.0-0 \
curl \
&& rm -rf /var/lib/apt/lists/*
# uv 包管理器(按 lock 锁定版本安装)
COPY --from=ghcr.io/astral-sh/uv:0.7.4 /uv /uvx /usr/local/bin/
WORKDIR /app
COPY backend /app/backend
WORKDIR /app/backend
# 1) 按 uv.lock 安装全部依赖(不含 dev;不安装项目本身,保持 /app/backend 为源码目录)
RUN uv sync --locked --no-dev --no-install-project
# 2) 安装补丁后的 s2s 语音栈(--no-deps:仅装包体,依赖已由 uv sync 覆盖)
RUN uv pip install --no-deps --no-build-isolation -e vendor/s2s-cloud
# 3) 运行期资源目录(本地缓存随镜像打包,避免运行期联网下载)
ENV TORCH_HOME=/app/backend/.torch-cache \
NLTK_DATA=/app/backend/nltk_data \
YOLO_CONFIG_DIR=/app/backend/.ultralytics \
NO_PROXY=* \
no_proxy=*
# 持久卷:媒体资源 / 知识库 / 运行数据
VOLUME ["/app/backend/media", "/app/backend/knowledge", "/app/backend/data.json"]
EXPOSE 10085 8765
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:10085/api/health', timeout=4).status==200 else 1)" || exit 1
CMD ["python", "docker_entry.py"]
+9 -9
View File
@@ -52,37 +52,37 @@ PAGES = {
RULES = [
{
"match": ["入驻", "申请", "入孵", "入园", "如何加入", "怎样加入"],
"reply": "入驻流程如下:\n\n1. 提交入驻申请(线上表单或到园区服务中心现场办理)\n2. 项目评审(园区组织专家对项目可行性、团队背景进行评估)\n3. 签订入驻协议,办理入驻手续\n4. 安排办公场地,对接创业导师与孵化服务\n\n目前园区在园项目 158 家,累计孵化企业 208 家。如需协助,可直接联系园区服务中心",
"reply": "入驻流程(按《入驻管理规程》,每年原则上两次):\n\n1. 提出申请:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照签证)、学历证明、营业执照、资质专利证书等材料\n2. 项目初审:服务机构初审,材料不齐一次性告知补全\n3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,路演答辩,量化打分 100 分制,80 分及以上通过\n4. 评审公示:报主管部门备案后在昆明市人社局官网公示\n5. 签署协议:签订《入驻孵化协议》,分配场地/工位,正式入驻\n\n按官方统计(2026年7月)园区实际入驻企业 38 家。咨询电话:18087174891",
"tools": [{"type": "show_card", "params": {"card": "overview"}}],
},
{
"match": ["政策", "补贴", "优惠", "扶持", "减免", "奖励", "资助"],
"reply": "园区政策服务覆盖「七位一体」中的政策维度:\n\n· 场地租金减免:符合条件的大学生创业项目可享受场地优惠\n· 创业补贴:协助对接省、市两级创业补贴与一次性创业资助\n· 税费优惠:对接小微企业所得税减免、增值税优惠等政策\n· 项目申报:指导高企认定(目前在园高新技术企业 32 家)、科技型中小企业备案\n\n更多政策细节可咨询园区政策服务窗口。",
"reply": "园区政策服务覆盖「七位一体」中的政策维度:\n\n· 场地租金减免:符合条件的大学生创业项目可享受场地优惠\n· 创业补贴:协助对接省、市两级创业补贴与一次性创业资助\n· 税费优惠:对接小微企业所得税减免、增值税优惠等政策\n· 项目申报:协助高新技术企业认定、科技型中小企业备案申报辅导\n\n更多政策细节可咨询园区政策服务窗口(电话:18087174891",
"tools": [],
},
{
"match": ["场地", "办公室", "工位", "租金", "收费", "面积", "多少钱", "价格"],
"reply": "园区建筑面积 1.2 万㎡,按功能划分为三个区域:\n\n· 加速区(A1–A12):成长型项目办公室,共 11 间独立办公室 + 1 间园区管理办公室\n· 国际区(I1–I7):面向国际合作的办公空间(7 间)\n· 成长区(G1–G8):初创团队成长空间(8 间)\n\n租金按创业项目阶段与政策给予差异化优惠,具体可到园区服务中心咨询",
"reply": "园区建筑面积约 3000 ㎡(官方统计口径),可容纳约 50 个创业团队,按发展阶段设置四大孵化区域(《宣传册》):\n\n· OPC创业空间:AI技术支撑、数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域,拎包入驻,最长 2 年\n· 创业苗圃区:创意阶段未注册团队,开放式共享工位,最长 2 年\n· 孵化加速区:已注册初创企业,独立办公空间,基础 2 年可延 1 年(累计≤3年)\n· 国际创客区:归国/外籍留学生,个性化办公空间,参照加速区\n\n办公场地免费提供(含桌椅、文件柜、高速网络等基础配套),水电网络全配套、拎包入驻",
"tools": [{"type": "show_card", "params": {"card": "zones"}}],
},
{
"match": ["企业", "公司", "名录", "名单", "入驻企业", "有哪些企业", "查企业"],
"reply": "园区目前有 39 家入驻企业,分布在三个区域:\n\n· 加速区:云南派音人工智能科技、米勒克尔蓝宝石珠宝、云南宸中低空经济等 9 家\n· 国际区:仰光客厅、中越生物医疗、酷享野农AI农业等 7 家\n· 成长区:Facebook越南跨境电商、昆明云韵体育、云南星瑞航空等 8 家\n\n我已在屏幕上为您展示企业分布卡片。",
"reply": "按官方统计(2026年7月),园区实际入驻企业 38;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,包括云南派音人工智能科技(PineSound)、米勒克尔蓝宝石珠宝、云南宸中低空经济仰光客厅、中越生物医疗、酷享野农AI农业、研X同行者网络等。\n\n行业分布:人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家。\n\n我已在屏幕上为您展示企业分布卡片。",
"tools": [{"type": "show_card", "params": {"card": "companies"}}],
},
{
"match": ["融资", "投资", "资金", "贷款", "估值", "资本"],
"reply": "园区融资服务包含:\n\n· 融资对接:定期组织路演与投资机构对接活动\n· 孵化估值:累计孵化企业估值约 5.2 亿元\n· 金融支持:协助对接银行创业贷款、政府创业担保贷款\n· 财务辅导:提供融资方案设计与商业计划书辅导",
"reply": "园区融资服务包含:\n\n· 融资对接:定期组织路演与投资机构对接活动\n· 经营成效:入驻团队累计经营收入约 2.19 亿元(省级绩效累计口径,截至2026年5月)\n· 金融支持:协助对接银行创业贷款、政府创业担保贷款\n· 财务辅导:提供融资方案设计与商业计划书辅导",
"tools": [],
},
{
"match": ["就业", "招聘", "岗位", "人才", "用工"],
"reply": "园区累计带动就业 2,186 人(其中高校毕业生 1,680 人,实习岗位 506 个)。\n\n· 企业招聘:在驻企业持续发布岗位\n· 人才对接:与在昆高校共建实习实训基地\n· 创业带动:园区鼓励以创业带动就业",
"reply": "园区就业数据(口径说明):\n\n· 官方统计(2026年7月):当年累计带动就业 213 人\n· 省级绩效累计口径(截至2026年5月):累计带动就业 1,529 人(本年度 106 人)\n\n· 企业招聘:在驻企业持续发布岗位\n· 人才对接:与在昆高校共建实习实训基地\n· 创业带动:园区鼓励以创业带动就业",
"tools": [],
},
{
"match": ["导师", "培训", "课程", "辅导", "孵化服务", "孵化器"],
"reply": "园区配备创业导师 58 位,提供全周期孵化服务:\n\n· 一对一辅导:创业导师定期坐诊,覆盖商业模式、法律财税、市场营销\n· 培训课程:创业训练营、行业专题沙龙、政策解读会\n· 资源对接:技术、供应链、渠道等产业资源链接\n· 公共服务:工商注册、知识产权、法律咨询一站式代办",
"reply": "园区组建创业指导专家团队 50 人(省级绩效材料口径),提供全周期孵化服务:\n\n· 一对一辅导:创业导师定期坐诊,覆盖商业模式、法律财税、市场营销\n· 培训课程:创业训练营、行业专题沙龙、政策解读会\n· 资源对接:技术、供应链、渠道等产业资源链接\n· 公共服务:工商注册、知识产权、法律咨询一站式代办",
"tools": [],
},
{
@@ -92,7 +92,7 @@ RULES = [
},
{
"match": ["加速区", "国际区", "成长区", "分区", "区域", "区别", "布局"],
"reply": "园区按企业成长阶段划分三个区域:\n\n· 加速区(蓝色):成长型项目独立办公室(A1–A12),在驻 9 家\n· 国际区(青色):国际合作与跨境业务(I1–I7),在驻 7 家\n· 成长区(绿色):初创团队共享空间(G1–G8),在驻 8 家\n\n园区管理办公室位于加速区 M 房间",
"reply": "园区按创业企业(项目)发展阶段设置四大孵化区域(《宣传册》《入驻管理规程》):\n\n· OPC创业空间:AI技术支撑、轻资产领域,拎包入驻,孵化周期最长 2 年\n· 创业苗圃区:创意阶段未注册团队,共享工位,最长 2 年\n· 孵化加速区:已注册初创企业,独立办公,基础 2 年可延 1 年(累计≤3年)\n· 国际创客区:归国/外籍留学生,个性化空间,参照加速区\n\n我已在屏幕上为您展示分区介绍卡片",
"tools": [{"type": "show_card", "params": {"card": "zones"}}],
},
{
@@ -111,7 +111,7 @@ RULES = [
},
]
DEFAULT_REPLY = "园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务,在园项目 158 家,累计带动就业 2,186 人,累计营收 2.08 亿元。\n\n您可以这样问我:\n· 如何申请入驻园区?\n· 展示入驻企业卡片\n· 切换到数字孪生页面"
DEFAULT_REPLY = "园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务\n\n按官方统计(2026年7月):实际入驻企业 38 家,当年累计带动就业 213 人,当年生产经营总额 252.15 万元;省级绩效累计口径(截至2026年5月):累计孵化企业 181 家、累计带动就业 1,529 人、累计经营收入约 2.19 亿元。\n\n您可以这样问我:\n· 如何申请入驻园区?\n· 展示入驻企业卡片\n· 切换到数字孪生页面"
def _detect_page(text):
+15 -3
View File
@@ -21,8 +21,10 @@ _SSL_CTX = ssl.create_default_context(cafile=certifi.where())
SYSTEM_PROMPT = (
"你是「昆明市大学生创业园 · OPC 智能园区」的 AI 智能助手,运行在大屏展播系统上。"
"园区提供空间、孵化、融资、政策、资源、AI 赋能、综合服务七位一体服务"
"在园项目 158 家,累计带动就业 2,186 人,累计营收 2.08 亿元。"
"园区提供空间、孵化、融资、政策、资源、AI 赋能、综合服务七位一体服务"
"回答园区相关问题时,必须严格依据【园区知识库】中的标准资料口径回答,"
"不得使用资料外的数字;涉及运营数据时请区分口径(官方统计/省级绩效累计/AI平台指标),"
"并在需要时调用 get_park_overview 工具获取实时准确数据。"
"当用户要求「切换页面 / 控制播放 / 展示卡片 / 弹出通知」时,必须调用对应工具;"
"其余园区相关问题用简洁、专业的中文回答,可适当使用列表。"
)
@@ -146,7 +148,17 @@ def run_chat(messages):
log.warning("未配置 DASHSCOPE_API_KEY,使用本地规则引擎")
return _fallback(messages)
msgs = [{"role": "system", "content": SYSTEM_PROMPT}] + [
# 注入园区知识库摘要(标准资料口径),确保回答准确
system = SYSTEM_PROMPT
try:
from .rag import brief
kb = brief()
if kb:
system += f"\n\n【园区知识库】\n{kb}"
except Exception as e: # noqa: BLE001
log.warning("llm: 知识库注入失败(仅用基础提示词): %s", e)
msgs = [{"role": "system", "content": system}] + [
{"role": m.get("role") == "me" and "user" or m.get("role", "user"), "content": m.get("content", "")}
for m in (messages or [])
]
+33
View File
@@ -85,6 +85,17 @@ class ChatBody(BaseModel):
# ==================== 认证 / 设置 ====================
@router.get("/api/health")
async def health():
"""健康检查(Docker healthcheck / 运维探活)"""
return {
"ok": True,
"service": "dpm-backend",
"mqtt": hub.connected if getattr(hub, "connected", None) is not None else False,
"ts": __import__("time").time(),
}
@router.post("/api/login")
async def login(request: Request):
form = await request.form()
@@ -98,6 +109,28 @@ async def get_settings():
return {k: s[k] for k in ("volume", "play_mode", "image_duration", "fullscreen", "autostart")}
@router.get("/api/config")
async def runtime_config():
"""运行配置(供前端启动引导覆盖):MQTT 地址/账号、API 基址
—— 打包部署时展播端从后端拉取,避免构建期写死的局域网 IP 失效
"""
ws = settings.MQTT_WS_URL
# 从 ws://host:port/mqtt 中提取 broker 主机
host = "127.0.0.1"
try:
host = ws.split("://", 1)[1].split(":", 1)[0]
except Exception: # noqa: BLE001
pass
return {
"ok": True,
"mqtt_url": ws,
"mqtt_username": settings.MQTT_USERNAME or "",
"mqtt_password": settings.MQTT_PASSWORD or "",
"api_base": f"http://{host}:{settings.PORT}",
"broker_host": host,
}
@router.post("/api/settings")
async def update_settings(body: SettingsBody):
storage.update_settings(**body.model_dump(exclude_none=True))
+29 -9
View File
@@ -14,19 +14,28 @@ import random
import threading
import time
# ---------- 真实锚点(仅来自 xls 统计表 + 宣传册) ----------
# ---------- 真实锚点(仅来自 xls 统计表 + 附件1 省级材料 + 宣传册) ----------
REAL = {
"as_of": "2026-07",
"token_monthly_yi": 200, # 月均消耗 200 亿 tokens真实业务锚点
"token_monthly_yi": 200, # 月均消耗 200 亿 tokensAI 平台业务锚点,非政府统计口径
"token_daily_wan": 66666.7, # 日均 6.67 亿 tokens200亿/30 ≈ 66666.7 万)
"token_rate": 7700, # 实时 t/s6.67e8 / 86400 ≈ 7,716
"tools_per_call_tokens": 150000, # 平均每次工具调用消耗 tokens(真实测算:12.5M输入 ÷ 81步 ≈ 15.4万)
"occupied": 39, # 园区实际入驻企业数(信息表名单 39 家,统一口径
"occupied": 38, # 实际入驻企业数(官方口径:《2026年7月运行情况统计表》38 家
"capacity": 49, # 园区可容纳企业个数(统计表)
"invested": 560, # 开园以来累计投入运营资金(万元,统计表)
"jobs": 213, # 入驻企业累计带动就业人数(统计表)
"revenue_total": 252.15, # 入驻企业累计生产经营总额(万元,统计表)
"revenue_tax": 1.03, # 入驻企业累计上缴税利总额(万元,统计表)
"jobs": 213, # 带动就业人数(统计表,当年累计口径
"revenue_total": 252.15, # 生产经营总额(万元,统计表,当年累计口径
"revenue_tax": 1.03, # 上缴税利总额(万元,统计表,当年累计口径
"incubated_total": 181, # 累计孵化企业(附件1 省级绩效累计口径,截至2026年5月)
"in_incubation": 31, # 当前实有在孵创业实体(附件1
"graduated": 149, # 累计成功孵化出园企业(统计表,开园以来累计)
"patents": 171, # 入驻团队发明专利(附件1 累计口径)
"jobs_total": 1529, # 累计带动就业(附件1 省级绩效累计口径)
"revenue_total_cum": 21911.14, # 入驻团队累计经营收入(万元,附件1 累计口径 ≈ 2.19 亿元)
"revenue_tax_cum": 426.2, # 入驻团队累计税收(万元,附件1 累计口径)
"mentors": 50, # 创业指导专家团队(附件1
"success_rate": 95.91, # 近3年入孵实体孵化成功率(%)(附件1)
"area": 3000, # 园区建筑面积(㎡,统计表+宣传册)
"founded": 2009, # 成立年份(宣传册)
"province_level": 2012, # 获评省级创业园年份(宣传册)
@@ -177,12 +186,23 @@ def init_snapshot():
"nodes": 12, # AI 服务节点(演示)
},
"projects": {
"inPark": REAL["occupied"], # 实际入驻 39
"inPark": REAL["occupied"], # 实际入驻 382026年7月统计表官方口径)
"capacity": REAL["capacity"], # 可容纳 49 个
"invested": REAL["invested"], # 累计投入 560 万元
},
"jobs": {"total": REAL["jobs"]}, # 带动就业 213 人
"revenue": {"total": REAL["revenue_total"], "tax": REAL["revenue_tax"]}, # 万元
"jobs": {"total": REAL["jobs"]}, # 带动就业 213 人(当年累计口径)
"revenue": {"total": REAL["revenue_total"], "tax": REAL["revenue_tax"]}, # 万元(当年累计口径)
"cumulative": { # 省级绩效累计口径(附件1,截至2026年5月)
"incubated": REAL["incubated_total"], # 累计孵化企业 181 家
"inIncubation": REAL["in_incubation"], # 当前实有在孵 31 家
"graduated": REAL["graduated"], # 累计成功出园 149 家
"patents": REAL["patents"], # 发明专利 171 项
"jobs": REAL["jobs_total"], # 累计带动就业 1,529 人
"revenue": REAL["revenue_total_cum"], # 累计经营收入 21,911.14 万元
"tax": REAL["revenue_tax_cum"], # 累计税收 426.2 万元
"mentors": REAL["mentors"], # 创业指导专家团队 50 人
"successRate": REAL["success_rate"], # 近3年孵化成功率 95.91%
},
"park": {
"area": REAL["area"],
"founded": REAL["founded"],
+21 -14
View File
@@ -15,23 +15,30 @@ log = logging.getLogger("dpm.tools")
# ── 工具实现 ────────────────────────────────────────────────────────────
def _get_park_overview(args):
"""园区实时概览(在园项目/就业/营收/在园人数等"""
"""园区实时概览(官方统计口径 + 省级绩效累计口径 + AI 平台运行指标"""
from .sim_engine import sim_engine
d = sim_engine.snapshot()
return json.dumps({
"在园项目": d["projects"]["inPark"],
"累计孵化企业": d["projects"]["cum"],
"今日新增项目": d["projects"]["todayNew"],
"带动就业(人)": d["jobs"]["total"],
"今日新增就业": d["jobs"]["todayNew"],
"累计营收(万元)": d["revenue"]["total"],
"今日营收(万元)": d["revenue"]["today"],
"营收同比增速(%)": d["revenue"]["growth"],
"设备在线率(%)": d["park"]["devices"],
"今日能耗(kWh)": d["park"]["energy"],
"当前在园人数": d["park"]["people"],
"会议预约": d["park"]["meeting"],
"工位使用": d["park"]["desk"],
"数据口径": "官方统计(《2026年7月运行情况统计表》);累计口径(附件1省级材料,截至2026年5月);AI平台指标:TOKEN/工具调用",
"实际入驻企业数(家)": d["projects"]["inPark"],
"园区可容纳企业数(个)": d["projects"]["capacity"],
"开园以来累计投入运营资金(万元)": d["projects"]["invested"],
"带动就业人数(人,当年累计)": d["jobs"]["total"],
"生产经营总额(万元,当年累计)": d["revenue"]["total"],
"上缴税利总额(万元,当年累计)": d["revenue"]["tax"],
"累计孵化企业(家,省级绩效累计)": d["cumulative"]["incubated"],
"当前实有在孵实体(家,省级绩效累计)": d["cumulative"]["inIncubation"],
"累计成功孵化出园企业(家,开园以来累计)": d["cumulative"]["graduated"],
"入驻团队发明专利(项,累计)": d["cumulative"]["patents"],
"累计带动就业(人,省级绩效累计)": d["cumulative"]["jobs"],
"入驻团队累计经营收入(万元)": d["cumulative"]["revenue"],
"入驻团队累计税收(万元)": d["cumulative"]["tax"],
"创业指导专家团队(人)": d["cumulative"]["mentors"],
"近3年入孵实体孵化成功率(%)": d["cumulative"]["successRate"],
"园区建筑面积(㎡)": d["park"]["area"],
"TOKEN月均消耗(亿)": 200,
"TOKEN实时速率(t/s)": d["token"]["rate"],
"今日TOKEN消耗(万)": d["token"]["today"],
}, ensure_ascii=False)
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""Docker 生产入口 —— 与 main.py 环境初始化完全一致,但关闭 reload。
用法(镜像内):python docker_entry.py
- import main 触发 prepare_env():读取 backend/.env、设置 TORCH_HOME/NLTK_DATA/
NO_PROXY/SSL_CERT_FILE、默认开启 s2s(可用环境变量 DPM_S2S_ENABLED=0 关闭)
- 再以 uvicorn 非 reload 模式启动 app.main:app
"""
import main # noqa: F401 (执行 prepare_env 与环境初始化)
import uvicorn
from app.config import settings
if __name__ == "__main__":
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=int(settings.PORT),
reload=False,
workers=1,
access_log=True,
)
File diff suppressed because one or more lines are too long
+80 -21
View File
@@ -1,31 +1,90 @@
# 昆明大学生创业园(OPC 园区)
# 昆明大学生创业园(OPC 智能园区)知识库
## 园区简介
昆明大学生创业园是云南省首家政府主办的大学生创业孵化园区,为大学生创业者提供"空间+孵化+融资+政策+资源+AI赋能+综合服务"七位一体的一站式创业服务。
> 数据口径声明:本知识库内容全部依据《昆明市创业园宣传册》《昆明市大学生创业园入驻企业信息表》《昆明市大学生创业园运行情况统计表(2026年7月)》《附件1.省级创业孵化载体相关情况简介材料》《昆明市大学生创业园创业企业(项目)入驻管理规程》等标准资料整理;涉及运营数据时严格区分「官方统计口径」「省级绩效累计口径」「AI 平台运行指标」三类口径,不得混用。
## 空间服务
园区提供办公工位、独立办公室、共享会议室、路演大厅、直播基地等创业空间,水电网络全配套,拎包入驻。
## 一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)
- 全称:昆明市大学生创业园(KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK
- 性质:云南省首家由政府主办、免费为创业青年大学生提供创业孵化服务的开放式园区
- 成立时间:2009 年(2009 年 9 月成立)
- 主管部门:昆明市人力资源和社会保障局
- 园区地址:昆明市民航路 229 号(昆明市人力资源中心 1—2 楼)
- 建筑面积:约 3000 平方米,可容纳约 50 个大学生创业团队入驻
- 主导产业:数字经济与电子信息产业
- 认定省级创业园时间:2012 年
- 咨询电话:18087174891 / 17787129131 / 18214573650;接收邮箱:714066050@qq.com
- 服务模式:以「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体全链条创业扶持体系,打造「一站式」创业孵化服务平台
## 孵化服务
园区为入驻企业提供创业辅导、工商财税、知识产权、法律咨询等孵化服务;配备创业导师团队,提供一对一辅导;定期举办创业大赛、项目路演、融资对接会。
## 二、园区官方运营数据(口径:《2026年7月运行情况统计表》)
- 实际入驻企业数:38 家(2026 年 7 月报送当月数)
- 园区可容纳企业(实体)个数:49 个
- 开园以来累计投入园区运营资金:560 万元
- 入驻企业累计带动就业人数(当年累计):213 人
- 入驻企业累计生产经营总额(当年累计):252.15 万元
- 入驻企业累计上缴税利总额(当年累计):1.03 万元
- 开园以来累计成功孵化出园企业数:149 家
- 当年开展活动场次:11 场;当年培训人数:362 人
- 备注:统计表 3、9 栏为开园以来累计数,5 栏为报送当月实际入驻数,其余为当年累计数
## 融资服务
园区对接银行、投资机构、政府引导基金,为入驻企业提供贷款贴息、天使投资、股权融资对接等金融服务。
## 三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)
- 认定为省级创业孵化载体以来累计孵化企业:181 家;当前实有在孵创业实体:31 家
- 团队(实体)出园数:累计 150 家(本年度 11 家)
- 入驻团队发明专利数:累计 171 项(本年度 13 项)
- 入驻团队累计经营收入:21,911.14 万元(约 2.19 亿元)
- 入驻团队累计税收缴纳:426.2 万元
- 累计带动就业:1,529 人(本年度 106 人)
- 创业指导专家团队:50 人;运营团队:3 人
- 开展创业指导服务活动:累计 83 场(本年度 18 场)
- 近 3 年入孵实体孵化成功率:95.91%
## 政策支持
入驻企业可享受大学生创业补贴、场地租金减免、税收优惠、一次性创业补贴、社保补贴等政府政策;园区协助申报各类科技、人才项目。
## 四、入驻对象与条件(依据《入驻管理规程》)
- 入驻对象范围:在校大学生及毕业 8 年内的高校毕业生、归国留学生、外籍留学生等青年创业企业(项目);申请人须担任企业法定代表人或项目负责人;对符合昆明市产业发展的优质创业企业(项目)或 OPC 企业可适当放宽入驻条件
- 产业方向:符合国家产业政策,聚焦昆明市「10+N」产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等)
- 其他要求:有明确可行的经营计划、启动资金和风险承担能力,具备规范的企业管理制度;无知识产权纠纷、无违法犯罪记录、无经营异常记录
- 优先入驻情形:获得市级及以上创业大赛奖项、拥有突出学术成就、重大发明创造及高新专利技术的项目
## AI 赋能
园区建设 OPC 智能运营中心(数字运营平台),通过 PineSound 智能语音助手、数据大屏、YOLO 视觉识别、手势控制等 AI 能力,为企业提供智能咨询、数据洞察与展厅互动服务。
## 五、入驻流程(依据《入驻管理规程》)
园区原则上每年组织两次入驻工作,可适时增减调整。流程如下:
1. 提出申请:提交《入驻申请表》《创业计划书》、创业者身份证(外籍提供护照、签证)、学历证明、企业营业执照(已注册企业)、相关资质专利证书等材料
2. 项目初审:服务机构对申请材料初审,材料不齐的一次性告知补全
3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,采取路演答辩方式评审,实行量化打分制(总分 100 分,80 分及以上准予通过)。评分维度:团队资质与能力(20 分)、项目创新性与技术壁垒(25 分)、产业适配与市场前景(25 分)、运营规划与落地能力(15 分)、社会价值与发展潜力(15 分)
4. 评审公示:报主管部门备案后,在昆明市人力资源和社会保障局官方网站公示,公示无异议的准予入驻
5. 签署协议:签订《入驻孵化协议》,分配办公场地/工位,正式入驻
## 入驻流程
1. 提交入驻申请(线上/线下);2. 项目评审;3. 签订入驻协议;4. 办理工商注册;5. 入驻孵化。
## 六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)
- OPC 创业空间:面向以人工智能技术为支撑、聚焦数字经济/AI 应用/软件开发/文创设计/电商直播等轻资产领域的项目,提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年
- 创业苗圃区:面向创意阶段、未注册企业的大学生创业团队,提供开放式共享工位,孵化周期最长 2 年;成长好的项目经评审可推荐至孵化加速区孵化 1 年
- 孵化加速区:面向已完成工商注册、处于初创期的企业,提供独立办公空间,基础孵化周期 2 年,期满经评估合格可申请延期 1 年,累计不超过 3 年
- 国际创客区:面向归国留学生、外籍留学生创业项目,提供个性化办公空间及国际化配套服务,孵化周期参照孵化加速区执行
## 园区数据口径
园区累计孵化企业 200 余家,目前在园企业 150 余家,带动就业 2000 余人,累计营收超 2 亿元,高新技术企业 30 余家,省级以上重点研发项目 18 项,创业大赛奖项 47 项。
(实时运营数据请调用 get_park_overview 工具获取。)
## 七、政策与孵化服务(依据《宣传册》)
- 空间保障:免费提供办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等
- 孵化服务:创业指导专家一对一辅导,定期举办创业沙龙、创业讲堂、项目路演等
- 融资支持:联动省内外创业投资机构,提供政策性贷款、商业银行贷款、股权融资等推荐指导
- 政策对接:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策
- 资源整合:对接驻昆高校、科研院所、龙头企业,纳入昆明创业创新服务体系,优先参与「春城创业荟」等品牌活动
- 专项赋能培训:定期开展 AI 公益性培训(AI 技术应用、数字内容创作、一人公司创业落地、企业 AI 降本增效)
- 其他服务:工商注册、税务代办、知识产权咨询、法律援助等
## 八、年度考核与退出管理(依据《入驻管理规程》)
- 年度考核:量化打分制(总分 100 分),维度为经营发展能力(20)、技术创新能力(25)、合规与履约情况(20)、创业带动成效(20)、发展潜力与孵化成效(15);等级:优秀(80 分及以上)、合格(60—79 分)、不合格(60 分以下)
- 退出类型:成功孵化出园、孵化期满退出、主动申请退出、强制勒令退出
- 强制退出情形:累计 30 个工作日无人到现场办公、连续 7 个工作日场地空置且未报备累计超 3 次、给园区造成重大经济损失或名誉损失、不服从管理情节严重、被吊销执照或破产清算、违法违规经营、私自转租或虚报数据等
## 九、OPC 概念(一人公司创业模式)
- OPC 即 One Person Company「一人公司」创业模式:以人工智能技术为支撑,聚焦数字经济、AI 应用、软件开发、文创设计、电商直播等轻资产领域,一人即可启动运营的创业形态
- 园区 OPC 创业空间为 OPC 创业者提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年;园区定期开展「AI 一人公司实战课」等专项培训
- 云南首个人工智能 OPC 创新人才基地落地昆明(2026 年 3 月公开报道),填补省内个体 AI 创业培育空白
## 十、入驻企业名录(依据《入驻企业信息表》39 家名单)
在园企业以《2026年7月运行情况统计表》官方口径 38 家为准;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,含云南派音人工智能科技、米勒克尔蓝宝石珠宝产业化、中泰研学合作、云南宸中低空经济、昆明智海银高文化科技、人工智能机器人大模型训练、云南廷秀文旅康养、瀚颖 AI+ 教育咨询、云南大学 AI+ 创业平台、仰光客厅、云南上古绝学文化、千楣之路、中越生物医疗、中越文旅交流、绿态环保科技、联翼航空低空经济、酷享野农 AI 农业、滇缅国际设计、昆明舒诺生物科技、达岸教育管理、昆明滇油石化、花仙子园艺肥料、Facebook 越南茶器跨境、研X 同行者网络、鬼才明 AI 创意工作室、朵哈·玫瑰特色产业链、昆明云韵体育、昆明舒护安养老服务、南菌优培食用菌、五华区丽裳文化、云南星瑞航空、云南直播私域平台、昆明屿澈电商、中外青少年研学、星杭商务信息咨询、蓝智科技、云品出滇·纸享万家、云迹轻旅、启元人工智能科技。行业分布(按简介分类):人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家
## 十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)
- TOKEN 消耗:月均约 200 亿 tokens(折合日均约 6.67 亿 tokens、实时速率约 7,700 t/s)——AI 平台运行锚点测算
- 工具调用:按平均每次工具调用约 15 万 tokens 折算,与 TOKEN 消耗实时联动
- 实时运营数据请调用 get_park_overview 工具获取,以工具返回为准
## 常见问题
- 问:如何入驻园区?答:见"入驻流程",可直接联系园区服务台或通过 PineSound 语音助手咨询
- 问:园区提供哪些服务?答:见"孵化服务"与"七位一体",涵盖空间、孵化、融资、政策、资源、AI 赋能、综合服务
- 问:有什么政策支持?答:见"政策支持",包括补贴、租金减免、税收优惠等,具体以最新政策为准
- 问:如何入驻园区?答:提交申请(入驻申请表、创业计划书、身份证、学历证明等)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位;每年原则上两次
- 问:园区有哪些政策扶持?答:空间免费、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商税务法律一站式服务(详见「政策与孵化服务」)
- 问:园区有多少家企业、带动多少就业?答:按官方统计(2026 年 7 月)实际入驻 38 家、当年累计带动就业 213 人;省级绩效累计口径(截至 2026 年 5 月)累计带动就业 1,529 人、累计经营收入约 2.19 亿元。请说明所需口径
- 问:什么是 OPC?答:一人公司(One Person Company)创业模式,详见「OPC 概念」。
+100
View File
@@ -0,0 +1,100 @@
# ============================================================
# OPC 智能园区后端 · Docker Compose 一键部署
#
# 启动: docker compose up -d --build
# 停止: docker compose down
# 查看: docker compose logs -f backend
# 重建: docker compose up -d --build backend
#
# 部署前配置(可选,均可用环境变量覆盖):
# export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端能访问到的 Broker 地址(服务器局域网 IP)
# export EMQX_USER=dpmserver EMQX_PASS=xxxxx
# export DASHSCOPE_API_KEY=sk-xxxx # AI/语音(未配置时 AI 走本地规则引擎)
# export DPM_S2S_ENABLED=0 # 不需要实时语音对话可关闭
#
# 端口:
# · 10085 HTTPAPI + 管理后台 + 媒体)
# · 8765 s2s 实时语音 WebSocketDPM_S2S_ENABLED=1 时)
# · 1883/8083/18083 EMQXMQTT / MQTT-WS / 控制台)
# ============================================================
services:
# ---------- MQTT BrokerEMQX 5.x ----------
emqx:
image: emqx/emqx:5.8
container_name: dpm-emqx
restart: unless-stopped
ports:
- "1883:1883" # MQTT
- "8083:8083" # MQTT over WebSocket(展播端 /api/config 下发)
- "18083:18083" # EMQX Dashboard(可选,账号 admin / public
environment:
EMQX_NAME: dpm-emqx
EMQX_HOST: node.emqx.dpm.local
EMQX_NODE__COOKIE: dpm-emqx-cookie
EMQX_DASHBOARD__DEFAULT_PASSWORD: "${EMQX_DASHBOARD_PASS:-public}"
# 鉴权:禁止匿名,单账号 dpmserver(与 backend/.env 一致)
EMQX_ALLOW_ANONYMOUS: "false"
EMQX_AUTH__USER__1__LOGIN: "${EMQX_USER:-dpmserver}"
EMQX_AUTH__USER__1__PASSWORD: "${EMQX_PASS:-123456}"
volumes:
- emqx_data:/opt/emqx/data
- emqx_log:/opt/emqx/log
healthcheck:
test: ["CMD", "emqx", "ctl", "status"]
interval: 10s
timeout: 5s
retries: 12
start_period: 15s
networks:
- dpm_net
# ---------- 园区后端(FastAPI :10085 + s2s :8765 ----------
backend:
build:
context: .
dockerfile: backend/Dockerfile
image: dpm-backend:latest
container_name: dpm-backend
restart: unless-stopped
depends_on:
emqx:
condition: service_healthy
ports:
- "10085:10085" # API / 管理后台 / 媒体
- "8765:8765" # s2s 实时语音(DPM_S2S_ENABLED=1 时)
environment:
DPM_PORT: "10085"
DPM_MQTT_ENABLED: "1"
MQTT_BROKER_HOST: emqx
MQTT_BROKER_PORT: "1883"
MQTT_USERNAME: "${EMQX_USER:-dpmserver}"
MQTT_PASSWORD: "${EMQX_PASS:-123456}"
# 展播端(Tauri 大屏)经后端 /api/config 获取的 WebSocket 地址:
# 必须填"展播机能访问到的服务器地址",默认 127.0.0.1(同机部署)
DPM_MQTT_WS: "ws://${MQTT_PUBLIC_HOST:-127.0.0.1}:8083/mqtt"
DPM_S2S_ENABLED: "${DPM_S2S_ENABLED:-1}"
DASHSCOPE_API_KEY: "${DASHSCOPE_API_KEY:-}"
volumes:
- dpm_media:/app/backend/media # 媒体资源(上传持久化)
- dpm_knowledge:/app/backend/knowledge # 知识库 park.md(可热更新,删除 kb_index.json 触发重建)
- dpm_data:/app/backend/data.json # 播放列表 / 设置
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:10085/api/health', timeout=4).status==200 else 1)"]
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
networks:
- dpm_net
networks:
dpm_net:
driver: bridge
volumes:
emqx_data:
emqx_log:
dpm_media:
dpm_knowledge:
dpm_data:
+8 -8
View File
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
import { getMqttStatus } from '../utils/mqtt';
import { renderMarkdown } from '../voice/markdown.jsx';
@@ -19,12 +19,12 @@ const QUICK_QUESTIONS = [
'切换到数字孪生页面',
];
/* 离线兜底回复(后端不可用时) */
/* 离线兜底回复(后端不可用时 · 口径按《2026年7月统计表》/《附件1省级材料》/《宣传册》 */
const FALLBACK_REPLIES = [
'园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务,在园项目 158 家,累计带动就业 2,186 人,累计营收 2.08 亿元。',
'入驻流程:提交申请 → 项目评审 → 签订协议 → 安排场地并对接导师服务。在园项目 158 家,累计孵化 208 余家。',
'园区政策覆盖场地租金减免、创业补贴、税费优惠与高企认定申报辅导(在园高企 32 家)。',
'园区按成长阶段分三个区域:加速区(A1–A12)、国际区(I1–I7)、成长区(G1–G8),共 39 家入驻企业。',
'园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务。按官方统计(2026年7月):实际入驻企业 38 家,当年累计带动就业 213 人,当年生产经营总额 252.15 万元。',
'入驻流程(按《入驻管理规程》):提出申请(入驻申请表、创业计划书、身份证、学历证明等材料)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、100 分制 80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位。',
'园区政策服务覆盖:免费办公空间、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商注册与法律咨询等一站式服务;每年原则上组织两次入驻。',
'园区按发展阶段设四大孵化区域(《宣传册》):OPC创业空间(AI轻资产、最长2年)、创业苗圃区(未注册团队、最长2年)、孵化加速区(已注册企业、基础2年可延1年)、国际创客区(留学生、参照加速区)。',
];
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
@@ -118,7 +118,7 @@ export default function AiChatPanel() {
.filter((m) => m.role === 'me' || (m.role === 'ai' && !/我是「问问小园」/.test(m.text)))
.map((m) => ({ role: m.role === 'me' ? 'user' : 'assistant', content: m.text }));
history.push({ role: 'user', content: text });
const res = await fetch(`${API_BASE}/api/ai/chat`, {
const res = await fetch(`${API_BASE()}/api/ai/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: history }),
@@ -183,7 +183,7 @@ export default function AiChatPanel() {
const form = new FormData();
form.append('file', blob, `recording.${fmt === 'mp4' ? 'm4a' : fmt}`);
form.append('format', fmt === 'mp4' ? 'm4a' : fmt);
const res = await fetch(`${API_BASE}/api/ai/asr`, { method: 'POST', body: form });
const res = await fetch(`${API_BASE()}/api/ai/asr`, { method: 'POST', body: form });
const data = await res.json();
const text = (data && data.text || '').trim();
if (text) {
+4 -4
View File
@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
DpmOverlays —— 后端/AI 指令驱动的全局覆盖层
@@ -16,17 +16,17 @@ function CardContent({ card }) {
useEffect(() => {
let alive = true;
if (type === 'companies') {
fetch(`${API_BASE}/api/park/companies`)
fetch(`${API_BASE()}/api/park/companies`)
.then((r) => r.json())
.then((d) => alive && setData(d.companies || []))
.catch(() => alive && setData([]));
} else if (type === 'zones') {
fetch(`${API_BASE}/api/park/zones`)
fetch(`${API_BASE()}/api/park/zones`)
.then((r) => r.json())
.then((d) => alive && setData(d.zones || []))
.catch(() => alive && setData([]));
} else if (type === 'overview') {
fetch(`${API_BASE}/api/dashboard/snapshot`)
fetch(`${API_BASE()}/api/dashboard/snapshot`)
.then((r) => r.json())
.then((d) => alive && setData(d))
.catch(() => alive && setData(null));
+2 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import Icon from './Icons';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
AI 助手 · 预设问题面板(右侧)
@@ -17,7 +17,7 @@ export default function PromptPanel({ onSend }) {
let cancelled = false;
(async () => {
try {
const res = await fetch(`${API_BASE}/api/ai/questions`, { cache: 'no-store' });
const res = await fetch(`${API_BASE()}/api/ai/questions`, { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) {
+34 -8
View File
@@ -1,20 +1,46 @@
/* =========================================================
运行配置 —— 后端地址 / MQTT Broker
优先级:window.__DPM_*__ 全局变量 > Vite 环境变量(.env.local> 内置默认
部署时在项目根目录 .env.local 配置(已 gitignore):
VITE_API_BASE=http://192.168.x.x:18003
VITE_MQTT_URL=ws://192.168.x.x:8083/mqtt
VITE_MQTT_USERNAME=...
VITE_MQTT_PASSWORD=...
优先级:window.__DPM_*__ 全局变量(运行时可被 /api/config 引导覆盖)
> Vite 环境变量(.env.local,构建时)> 内置默认
内置默认(打包部署):Tauri 内 WebView 的 hostname 为 tauri.localhost
后端与 MQTT Broker 若与展播程序同机部署,回退到 127.0.0.1;
浏览器开发/局域网访问时回退到当前主机 hostname。
========================================================= */
const env = import.meta.env || {};
export const API_BASE = window.__DPM_API__ || env.VITE_API_BASE || 'http://192.168.1.9:10085';
export const MQTT_URL = window.__DPM_MQTT__ || env.VITE_MQTT_URL || 'ws://192.168.1.3:8083/mqtt';
/* 运行期解析(惰性读取 window.__DPM_*__,供 main.jsx bootstrap 覆盖后再生效) */
function resolveHost() {
const h = window.location.hostname || '127.0.0.1';
// Tauri 打包 WebViewtauri.localhost / localhost)→ 服务同机部署用 127.0.0.1
if (h === 'tauri.localhost' || h === 'localhost' || h === '127.0.0.1' || h.includes('tauri')) {
return '127.0.0.1';
}
return h; // 浏览器开发/局域网:当前主机 IP
}
export const API_BASE = window.__DPM_API__ || env.VITE_API_BASE || `http://${resolveHost()}:10085`;
export const MQTT_URL = window.__DPM_MQTT__ || env.VITE_MQTT_URL || `ws://${resolveHost()}:8083/mqtt`;
export const MQTT_USERNAME = window.__DPM_MQTT_USER__ || env.VITE_MQTT_USERNAME || '';
export const MQTT_PASSWORD = window.__DPM_MQTT_PASS__ || env.VITE_MQTT_PASSWORD || '';
/** 惰性读取 API 基址(允许 bootstrap 从后端 /api/config 覆盖后再生效) */
export function getApiBase() {
return window.__DPM_API__ || API_BASE;
}
/** 惰性读取 MQTT 地址(允许 bootstrap 覆盖后再生效) */
export function getMqttUrl() {
return window.__DPM_MQTT__ || MQTT_URL;
}
export function getMqttCredentials() {
return {
username: window.__DPM_MQTT_USER__ || MQTT_USERNAME,
password: window.__DPM_MQTT_PASS__ || MQTT_PASSWORD,
};
}
export const MQTT_TOPIC_COMMAND = 'opc/display/command';
export const MQTT_TOPIC_TICK = 'opc/dashboard/tick';
export const MQTT_TOPIC_HEARTBEAT = 'opc/display/heartbeat';
+33 -7
View File
@@ -2,12 +2,38 @@ import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "@appica/ui-react/providers/theme-provider";
import { API_BASE } from "./config";
import "./styles/global.css";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
);
/* =========================================================
启动引导(打包部署关键):
渲染前先从后端 GET /api/config 拉取运行时配置
(MQTT 地址/账号、API 基址),写入 window.__DPM_*__
之后 api.js / mqtt.js 均以惰性读取方式生效。
后端不可达时静默使用内置默认(Tauri 打包回退 127.0.0.1)。
========================================================= */
async function bootstrapRuntimeConfig() {
try {
const res = await fetch(`${API_BASE()}/api/config`, { cache: 'no-store' });
if (!res.ok) return;
const cfg = await res.json();
// 仅覆盖 MQTT 连接信息(后端 .env 为准);API 基址以前端实际可达地址为准
if (cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url;
if (cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username;
if (cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password;
// eslint-disable-next-line no-console
if (cfg.mqtt_url) console.info(`[bootstrap] MQTT -> ${cfg.mqtt_url}`);
} catch {
/* 后端不可达:使用内置默认地址 */
}
}
bootstrapRuntimeConfig().then(() => {
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</React.StrictMode>,
);
});
+10 -7
View File
@@ -8,15 +8,18 @@ import { useParkSim } from '../utils/parkData';
昆明市大学生创业园 · OPC 智能园区数字运营中心
—— 数据大屏(真实数据展示)
---------------------------------------------------------
数据口径(用户指定,仅两个权威来源):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》
实际入驻 39 家 / 可容纳 49 个 / 累计投入 560 万 /
带动就业 213 人 / 累计生产经营总额 252.15 万 / 累计税利 1.03 万 / 面积 3000㎡
数据口径(标准资料,多个权威来源):
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方统计)
实际入驻 38 家 / 可容纳 49 个 / 累计投入 560 万 /
当年累计带动就业 213 人 / 当年生产经营总额 252.15 万 / 当年税利 1.03 万 / 面积 3000㎡
· 《附件1.省级创业孵化载体相关情况简介材料》(省级绩效累计口径,截至2026年5月)
累计孵化 181 家 / 在孵 31 家 / 累计就业 1,529 人 / 累计经营收入 21,911.14 万
· 《昆明市创业园宣传册.pdf》
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域 / 28 家企业名录
测算口径(用户确认):
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域
· 《入驻企业信息表》39 家名录(含创始人/简介/场地安排)
测算口径(AI 平台业务锚点,非政府统计口径):
· TOKEN:月均 200 亿 tokens → 日均 ≈ 6.67 亿,实时 ≈ 7,700 t/s,近30日累计 200 亿
· 工具调用:按每次调用约 2 万 tokens 折算(今日 ≈ 3.3 万次)
· 工具调用:按每次调用约 15 万 tokens 折算
TOKEN/工具为基于真实锚点的测算演示,其余为真实统计值
========================================================= */
+62 -13
View File
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import * as api from '../utils/api';
import { getApiBase } from '../config';
import { API_BASE } from '../utils/api';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { enable, disable } from '@tauri-apps/plugin-autostart';
@@ -58,7 +59,8 @@ export default function MediaScreen() {
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
const getUrl = useCallback((item) => {
return item.source === 'url' ? item.relative_path : `${API_BASE}/file/${item.relative_path}`;
// 惰性读取 API 基址:打包部署引导覆盖后媒体文件地址同步生效
return item.source === 'url' ? item.relative_path : `${getApiBase()}/file/${item.relative_path}`;
}, []);
// ============ 核心播放控制 ============
@@ -106,14 +108,19 @@ export default function MediaScreen() {
const url = getUrl(item);
video.src = url;
video.load();
// 打包部署:WebView2 已加 --autoplay-policy=no-user-gesture-required
// 这里仍先尝试带声音播放;仅真实自动播放策略拦截(NotAllowedError)才进入静音待解锁
video.muted = soundBlocked;
video.volume = volume;
const p = video.play();
if (p !== undefined) {
p.catch(() => {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
p.catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
}
// 其他错误(网络/解码等)不强制静音,等重试
});
}
} else {
@@ -132,15 +139,54 @@ export default function MediaScreen() {
if (!item || item.type !== 'video') return;
const video = videoRef.current;
if (video && video.paused && video.src) {
video.play().catch(() => {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
video.play().catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
video.play().catch(() => {});
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paused]);
// ============ 声音自动恢复(打包部署无需人工点击) ============
// 1) 视频真正开始播放(playing)→ 解除静音标记
useEffect(() => {
const video = videoRef.current;
if (!video) return;
const onPlaying = () => {
setSoundBlocked(false);
if (video.muted && !soundBlockedRef.current) {
video.muted = false;
}
};
video.addEventListener('playing', onPlaying);
return () => video.removeEventListener('playing', onPlaying);
}, [mediaKey, loaded]);
// 2) 若因自动播放策略被静音,每 3 秒自动尝试解静音播放(无需用户点击)
useEffect(() => {
if (!loaded) return undefined;
const iv = setInterval(() => {
if (!soundBlockedRef.current) return;
const video = videoRef.current;
if (!video || !video.src) return;
video.muted = false;
const p = video.play();
if (p !== undefined) {
p.then(() => {
setSoundBlocked(false);
const hint = document.getElementById('soundHint');
if (hint) hint.style.display = 'none';
}).catch(() => {
video.muted = true;
});
}
}, 3000);
return () => clearInterval(iv);
}, [loaded]);
// ============ 事件监听 ============
useEffect(() => {
const video = videoRef.current;
@@ -176,9 +222,12 @@ export default function MediaScreen() {
testPlay.then(() => {
setSoundBlocked(false);
video.pause();
}).catch(() => {
setSoundBlocked(true);
video.muted = true;
}).catch((err) => {
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
setSoundBlocked(true);
video.muted = true;
}
// 其他错误不强制静音(自动重试由 3s 定时器接管)
});
}
}
@@ -188,7 +237,7 @@ export default function MediaScreen() {
}, []);
useEffect(() => {
const es = new EventSource(`${API_BASE}/api/events`);
const es = new EventSource(`${getApiBase()}/api/events`);
es.onmessage = (e) => {
try {
const msg = JSON.parse(e.data);
+5 -5
View File
@@ -8,7 +8,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js';
import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
import '../styles/datascreen.css';
import './voice-original.css';
import './voice-overrides.css';
@@ -222,7 +222,7 @@ export default function VoiceAssistant() {
pushMessage('assistant', `🔧 调用工具:${name}`, false);
let output;
try {
const res = await fetch(`${API_BASE}/api/tools/exec`, {
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, args }),
@@ -303,7 +303,7 @@ export default function VoiceAssistant() {
// 基础提示词:从后端拉取(配置 + 园区知识库已由服务端组装);失败用极简兜底
let instructions = FALLBACK_INSTRUCTIONS;
try {
const insRes = await fetch(`${API_BASE}/api/s2s/instructions`, { cache: 'no-store' });
const insRes = await fetch(`${API_BASE()}/api/s2s/instructions`, { cache: 'no-store' });
const insData = await insRes.json();
if (insData?.ok && insData.instructions) instructions = insData.instructions;
} catch { /* 后端不可用不影响对话 */ }
@@ -452,7 +452,7 @@ export default function VoiceAssistant() {
} else {
void start(); // start 连接成功后检查 pendingGreetingRef 再发问候
}
fetch(`${API_BASE}/api/vision/event`, {
fetch(`${API_BASE()}/api/vision/event`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ event: 'triggered', faces: vision.faces, dwell_ms: VISION_DWELL_MS }),
@@ -466,7 +466,7 @@ export default function VoiceAssistant() {
// 摄像头识别状态变化 → 上报后端(详细日志定位)
useEffect(() => {
if (!vision.status || vision.status === 'off' || vision.status === 'loading') return;
fetch(`${API_BASE}/api/vision/event`, {
fetch(`${API_BASE()}/api/vision/event`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+12 -10
View File
@@ -1,10 +1,12 @@
// API 基地址 —— 独立 FastAPI 后端(配置见 src/config.js,可用 window.__DPM_API__ 覆盖)
import { API_BASE as BASE } from '../config';
import { getApiBase } from '../config';
export const API_BASE = BASE;
/* 惰性读取 API 基址:允许 main.jsx 引导阶段从后端 /api/config 覆盖(打包部署时生效) */
export const API_BASE = getApiBase();
const base = () => getApiBase();
async function request(url, options = {}) {
const res = await fetch(`${BASE}${url}`, {
const res = await fetch(`${base()}${url}`, {
headers: { 'Content-Type': 'application/json' },
...options,
});
@@ -19,7 +21,7 @@ function formEncode(data) {
// ============ Auth ============
export async function login(username, password) {
const res = await fetch(`${BASE}/api/login`, {
const res = await fetch(`${base()}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formEncode({ username, password }),
@@ -29,7 +31,7 @@ export async function login(username, password) {
// ============ Settings ============
export async function getSettings() {
const res = await fetch(`${BASE}/api/settings`);
const res = await fetch(`${base()}/api/settings`);
return res.json();
}
@@ -42,7 +44,7 @@ export async function updateSettings(data) {
// ============ Media Files ============
export async function getMediaFiles() {
const res = await fetch(`${BASE}/media`);
const res = await fetch(`${base()}/media`);
return res.json();
}
@@ -50,7 +52,7 @@ export async function uploadFiles(files) {
for (const file of files) {
const fd = new FormData();
fd.append('file', file);
const res = await fetch(`${BASE}/upload`, { method: 'POST', body: fd });
const res = await fetch(`${base()}/upload`, { method: 'POST', body: fd });
if (!res.ok) throw new Error(`上传失败 (${res.status})`);
}
}
@@ -60,7 +62,7 @@ export async function uploadFilesWithProgress(files, onProgress) {
const file = files[i];
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${BASE}/upload`);
xhr.open('POST', `${base()}/upload`);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
@@ -109,7 +111,7 @@ export async function deleteMedia(path) {
// ============ Playlist ============
export async function getPlaylist() {
const res = await fetch(`${BASE}/api/playlist`);
const res = await fetch(`${base()}/api/playlist`);
return res.json();
}
@@ -136,7 +138,7 @@ export async function sendControl(action) {
}
export async function getState() {
const res = await fetch(`${BASE}/api/state`);
const res = await fetch(`${base()}/api/state`);
return res.json();
}
+19 -6
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import mqtt from 'mqtt';
import { MQTT_URL, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK, MQTT_USERNAME, MQTT_PASSWORD } from '../config';
import { getMqttUrl, getMqttCredentials, MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK } from '../config';
/* =========================================================
MQTT 客户端(mqtt.js)—— 后端控制通道
@@ -17,18 +17,31 @@ function notifyStatus() {
window.dispatchEvent(new CustomEvent('dpm:mqtt-status', { detail: { status } }));
}
/* 每个连接生成全局唯一 client_id(独立客户端 ID 铁律):
优先 crypto.randomUUIDWebView2/现代浏览器),回退时间戳+双段高熵随机 */
function makeClientId() {
try {
if (window.crypto && typeof window.crypto.randomUUID === 'function') {
return `dpm-screen-${crypto.randomUUID().replace(/-/g, '')}`;
}
} catch { /* 继续回退 */ }
return `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}-${Math.random().toString(16).slice(2, 10)}`;
}
export function connectMqtt() {
if (client) return;
try {
// 每个页面会话生成唯一 client_id(时间戳 + 高熵随机),避免多屏/多实例竞争
clientId = `dpm-screen-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 12)}`;
client = mqtt.connect(MQTT_URL, {
// 独立客户端 ID:避免多屏/多实例/多标签页同 ID 被 broker 互相踢下线
clientId = makeClientId();
// 连接时惰性读取 MQTT 地址/账号:打包部署时 main.jsx 引导已从后端 /api/config 覆盖
const { username, password } = getMqttCredentials();
client = mqtt.connect(getMqttUrl(), {
reconnectPeriod: 3000,
connectTimeout: 8000,
clientId,
clean: true,
username: MQTT_USERNAME || undefined,
password: MQTT_PASSWORD || undefined,
username: username || undefined,
password: password || undefined,
});
client.on('connect', () => {
status = 'connected';
+13 -13
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
/* =========================================================
园区数据引擎(共享)—— 供数据大屏 / 媒体播放页复用
@@ -15,19 +15,19 @@ import { API_BASE } from '../config';
export const fmtInt = (n) => Math.round(n).toLocaleString('zh-CN');
export const fmtWan = (n) => (n >= 10000 ? (n / 10000).toFixed(2) + '亿' : n >= 100 ? n.toFixed(1) + '万' : n.toFixed(1));
/* ---------- 真实锚点(仅来自 xls 统计表 + 宣传册) ---------- */
/* ---------- 真实锚点(官方统计《2026年7月运行情况统计表》+ 附件1省级材料 + 宣传册) ---------- */
const REAL = {
tokenMonthlyYi: 200, // 月均 200 亿 tokens真实业务锚点
tokenDailyWan: 66666.7, // 日均 6.67 亿 tokens
tokenRate: 7700, // 实时 t/s
toolsPerCallTokens: 150000, // 平均每次工具调用消耗 tokens(真实测算:12.5M输入 ÷ 81步 ≈ 15.4万
occupied: 39, // 实际入驻企业数(信息表名单 39 家,统一口径
tokenMonthlyYi: 200, // 月均 200 亿 tokensAI 平台业务锚点,非政府统计口径
tokenDailyWan: 66666.7, // 日均 6.67 亿 tokensAI 平台锚点)
tokenRate: 7700, // 实时 t/sAI 平台锚点)
toolsPerCallTokens: 150000, // 平均每次工具调用消耗 tokens(AI 平台测算
occupied: 38, // 实际入驻企业数(官方口径:《2026年7月运行情况统计表》38 家
capacity: 49, // 可容纳企业个数(统计表)
invested: 560, // 累计投入运营资金(万元)
jobs: 213, // 累计带动就业(统计表)
revenueTotal: 252.15, // 累计生产经营总额(万元)
revenueTax: 1.03, // 累计上缴税利(万元)
area: 3000, // 建筑面积(㎡)
invested: 560, // 开园以来累计投入运营资金(万元,统计表
jobs: 213, // 带动就业人数(统计表,当年累计口径
revenueTotal: 252.15, // 生产经营总额(万元,统计表,当年累计口径
revenueTax: 1.03, // 上缴税利总额(万元,统计表,当年累计口径
area: 3000, // 建筑面积(㎡,统计表+宣传册
founded: 2009, // 成立年份(宣传册)
provinceLevel: 2012, // 省级创业园认定年份(宣传册)
address: '昆明市民航路229号(人力资源中心1-2楼)',
@@ -212,7 +212,7 @@ export function useParkSim() {
let cancelled = false;
const load = async () => {
try {
const res = await fetch(`${API_BASE}/api/dashboard/snapshot`, { cache: 'no-store' });
const res = await fetch(`${API_BASE()}/api/dashboard/snapshot`, { cache: 'no-store' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled && data && data.token) setSnap(data);
+2 -2
View File
@@ -7,7 +7,7 @@
- 帧经局域网传到后端(大屏机同机部署则不出设备);画面预览仍本地
========================================================= */
import { useCallback, useEffect, useRef, useState } from 'react';
import { API_BASE } from '../config';
import { getApiBase as API_BASE } from '../config';
export const VISION_DWELL_MS = 10000; // 面向停留阈值
export const VISION_SILENT_MS = 180000; // 触发(自动问候)后静默 3 分钟,期间不重复触发
@@ -107,7 +107,7 @@ export default function useVisionDetection({ onTrigger, onGesture, enabled = tru
// JPEG base64 → 后端 YOLO
const b64 = canvasRef.current.toDataURL('image/jpeg', 0.6).split(',')[1];
setModelPhase('ok');
const res = await fetch(`${API_BASE}/api/vision/frame`, {
const res = await fetch(`${API_BASE()}/api/vision/frame`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: b64 }),