Spaces:
Sleeping
Sleeping
Trae Assistant commited on
Commit ·
4797b80
1
Parent(s): 9cdfe90
init
Browse files- .gitignore +5 -0
- Dockerfile +16 -0
- README.md +55 -5
- app.py +615 -0
- requirements.txt +3 -0
- templates/index.html +532 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*.db
|
| 4 |
+
instance/
|
| 5 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY requirements.txt /app/requirements.txt
|
| 9 |
+
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r /app/requirements.txt
|
| 10 |
+
|
| 11 |
+
COPY . /app
|
| 12 |
+
|
| 13 |
+
ENV PORT=7865
|
| 14 |
+
EXPOSE 7865
|
| 15 |
+
|
| 16 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,60 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: 冷链闭环智能体
|
| 3 |
+
emoji: 🧊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: teal
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7865
|
| 8 |
+
short_description: 冷链闭环智能体:医药与生鲜的智能合规运营平台
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# 冷链闭环智能体(ColdChain Optimizer Agent)
|
| 14 |
+
|
| 15 |
+
## 项目简介
|
| 16 |
+
面向医药、生鲜与高价值温控品类的冷链运营平台,提供完整的“推理/决策 → 工具行动 → 状态/记忆 → 校验/验收 → 迭代/回放”闭环体系。
|
| 17 |
+
支持多智能体协同、指标验收、资产沉淀与历史回放,适用于冷链合规、运力优化与成本控制等商业场景。
|
| 18 |
+
|
| 19 |
+
## 核心能力
|
| 20 |
+
- 冷链闭环决策:从策略推理到执行校验的一体化流程
|
| 21 |
+
- 工具行动层:温控风险、线路规模、准时率、成本节约的量化输出
|
| 22 |
+
- 资产沉淀:SOP、指标字典、异常追溯模板与协作清单存档
|
| 23 |
+
- 可回放:历史会话复盘,支持再次迭代优化
|
| 24 |
+
- Markdown 渲染:推理结果与报告直接展示为可读文档
|
| 25 |
+
- 移动端适配:响应式界面,适合现场调度使用
|
| 26 |
+
|
| 27 |
+
## 技术栈
|
| 28 |
+
- 后端:Python 3.11 + Flask + SQLite
|
| 29 |
+
- 前端:原生 HTML/CSS/JS + Marked.js
|
| 30 |
+
- AI:SiliconFlow API(支持本地 Mock 模式回退)
|
| 31 |
+
- 部署:Docker(Hugging Face Spaces 兼容)
|
| 32 |
+
|
| 33 |
+
## 快速开始
|
| 34 |
+
|
| 35 |
+
### 本地运行
|
| 36 |
+
```bash
|
| 37 |
+
pip install -r requirements.txt
|
| 38 |
+
python app.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
访问:http://localhost:7865
|
| 42 |
+
|
| 43 |
+
### Docker 运行
|
| 44 |
+
```bash
|
| 45 |
+
docker build -t coldchain-optimizer-agent .
|
| 46 |
+
docker run -p 7865:7865 --env-file .env coldchain-optimizer-agent
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## 环境变量
|
| 50 |
+
在项目根目录创建 `.env` 文件,可配置:
|
| 51 |
+
```env
|
| 52 |
+
SILICONFLOW_API_KEY=你的_siliconflow_key
|
| 53 |
+
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1
|
| 54 |
+
SILICONFLOW_MODEL=Qwen/Qwen2.5-7B-Instruct
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## 商业价值
|
| 58 |
+
- 为医药/生鲜冷链企业提供合规与成本优化的可执行方案
|
| 59 |
+
- 通过资产沉淀形成可复用的冷链运营知识库
|
| 60 |
+
- 支持作为 SaaS 产品或行业解决方案持续变现
|
app.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import sqlite3
|
| 4 |
+
import datetime
|
| 5 |
+
import random
|
| 6 |
+
import requests
|
| 7 |
+
from flask import Flask, render_template, request, jsonify, g
|
| 8 |
+
from werkzeug.utils import secure_filename
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
|
| 11 |
+
# 冷链闭环智能体:加载环境变量
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
| 15 |
+
INSTANCE_DIR = os.path.join(BASE_DIR, "instance")
|
| 16 |
+
os.makedirs(INSTANCE_DIR, exist_ok=True)
|
| 17 |
+
|
| 18 |
+
app = Flask(__name__)
|
| 19 |
+
app.config["DATABASE"] = os.path.join(INSTANCE_DIR, "coldchain.db")
|
| 20 |
+
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "coldchain-secret-key")
|
| 21 |
+
app.config["JSON_AS_ASCII"] = False
|
| 22 |
+
app.config["MAX_CONTENT_LENGTH"] = 32 * 1024 * 1024
|
| 23 |
+
|
| 24 |
+
SILICONFLOW_API_KEY = os.getenv("SILICONFLOW_API_KEY", "").strip()
|
| 25 |
+
SILICONFLOW_BASE_URL = os.getenv("SILICONFLOW_BASE_URL", "https://api.siliconflow.cn/v1").strip()
|
| 26 |
+
SILICONFLOW_MODEL = os.getenv("SILICONFLOW_MODEL", "Qwen/Qwen2.5-7B-Instruct").strip()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_db():
|
| 30 |
+
# 统一获取数据库连接
|
| 31 |
+
db = getattr(g, "_database", None)
|
| 32 |
+
if db is None:
|
| 33 |
+
db = g._database = sqlite3.connect(app.config["DATABASE"])
|
| 34 |
+
db.row_factory = sqlite3.Row
|
| 35 |
+
return db
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@app.teardown_appcontext
|
| 39 |
+
def close_connection(exception):
|
| 40 |
+
# 请求结束后关闭连接
|
| 41 |
+
db = getattr(g, "_database", None)
|
| 42 |
+
if db is not None:
|
| 43 |
+
db.close()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def simulate_coldchain_tools(scenario, target, constraints):
|
| 47 |
+
seed = len(scenario) + len(target or "") + len(constraints or "")
|
| 48 |
+
random.seed(seed)
|
| 49 |
+
route_count = max(5, min(18, seed // 40))
|
| 50 |
+
hubs = max(2, min(8, seed // 90))
|
| 51 |
+
temp_risk = round(random.uniform(0.8, 2.8), 2)
|
| 52 |
+
on_time = round(96 + random.uniform(1.2, 2.8), 2)
|
| 53 |
+
compliance = round(99.0 + random.uniform(0.2, 0.7), 2)
|
| 54 |
+
cost_saving = round(random.uniform(5.0, 12.0), 1)
|
| 55 |
+
|
| 56 |
+
risk_keywords = ["堵车", "夜间", "限行", "偏差", "高峰", "雨雪", "台风"]
|
| 57 |
+
risk_score = 0
|
| 58 |
+
for k in risk_keywords:
|
| 59 |
+
if k in scenario or k in (constraints or ""):
|
| 60 |
+
risk_score += 2
|
| 61 |
+
risk_score += min(6, seed // 160)
|
| 62 |
+
if risk_score <= 3:
|
| 63 |
+
risk_level = "低"
|
| 64 |
+
elif risk_score <= 6:
|
| 65 |
+
risk_level = "中"
|
| 66 |
+
else:
|
| 67 |
+
risk_level = "高"
|
| 68 |
+
|
| 69 |
+
return {
|
| 70 |
+
"route_count": route_count,
|
| 71 |
+
"hub_count": hubs,
|
| 72 |
+
"temp_risk": temp_risk,
|
| 73 |
+
"on_time": on_time,
|
| 74 |
+
"compliance": compliance,
|
| 75 |
+
"cost_saving": cost_saving,
|
| 76 |
+
"risk_level": risk_level,
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def seed_demo(db):
|
| 81 |
+
row = db.execute("SELECT COUNT(*) AS c FROM sessions").fetchone()
|
| 82 |
+
if row and row["c"]:
|
| 83 |
+
return
|
| 84 |
+
demo = {
|
| 85 |
+
"name": "示例会话 · 医药冷链干线 + 末端",
|
| 86 |
+
"scenario": (
|
| 87 |
+
"为华东地区 6 个仓与 28 家三甲医院建立 2-8℃医药冷链网络,"
|
| 88 |
+
"需要在 24 小时内完成跨城配送,温控偏差不能超过 30 分钟。"
|
| 89 |
+
"要求支持节假日高峰、夜间配送以及异常追溯。"
|
| 90 |
+
),
|
| 91 |
+
"target": "准时交付率 ≥ 98%,温控合规率 ≥ 99.5%,综合成本降低 8%",
|
| 92 |
+
"constraints": "夜间限行、部分城市冷链车辆通行证审批较慢,预算上限 450 万/年",
|
| 93 |
+
}
|
| 94 |
+
cursor = db.execute(
|
| 95 |
+
"INSERT INTO sessions (name, scenario, target, constraints) VALUES (?, ?, ?, ?)",
|
| 96 |
+
(demo["name"], demo["scenario"], demo["target"], demo["constraints"]),
|
| 97 |
+
)
|
| 98 |
+
session_id = cursor.lastrowid
|
| 99 |
+
tool_result = simulate_coldchain_tools(demo["scenario"], demo["target"], demo["constraints"])
|
| 100 |
+
db.execute(
|
| 101 |
+
"""
|
| 102 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 103 |
+
VALUES (?, 1, 'Planner', 'reasoning', ?)
|
| 104 |
+
""",
|
| 105 |
+
(
|
| 106 |
+
session_id,
|
| 107 |
+
"## 示例推理结果\n\n"
|
| 108 |
+
"- 将华东划分为干线 + 末端两级网络,优先保障医院到区域仓的时效\n"
|
| 109 |
+
"- 对夜间与节假日高峰设置冗余线路与备用车辆池\n"
|
| 110 |
+
"- 使用温度记录仪与异常告警系统保障 2-8℃ 全程可追溯\n",
|
| 111 |
+
),
|
| 112 |
+
)
|
| 113 |
+
db.execute(
|
| 114 |
+
"""
|
| 115 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 116 |
+
VALUES (?, 2, 'RouteOps', 'tool_action', ?)
|
| 117 |
+
""",
|
| 118 |
+
(
|
| 119 |
+
session_id,
|
| 120 |
+
"## 示例工具行动结果\n\n"
|
| 121 |
+
f"- 规划干线/支线数:{tool_result['route_count']} 条\n"
|
| 122 |
+
f"- 冷链枢纽节点:{tool_result['hub_count']} 个\n"
|
| 123 |
+
f"- 温控偏差风险指数:{tool_result['temp_risk']}\n"
|
| 124 |
+
f"- 预计准时交付率:{tool_result['on_time']}%\n"
|
| 125 |
+
f"- 预计温控合规率:{tool_result['compliance']}%\n"
|
| 126 |
+
f"- 预计综合成本节约:{tool_result['cost_saving']}%\n",
|
| 127 |
+
),
|
| 128 |
+
)
|
| 129 |
+
db.execute(
|
| 130 |
+
"""
|
| 131 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 132 |
+
VALUES (?, 3, 'Memory', 'memory', ?)
|
| 133 |
+
""",
|
| 134 |
+
(
|
| 135 |
+
session_id,
|
| 136 |
+
"## 示例状态记���\n\n"
|
| 137 |
+
"- 首轮试点重点关注医院急配线路与疫苗仓温控告警阈值\n"
|
| 138 |
+
"- 记录各城市夜间限行时间窗口与通行证办理周期\n"
|
| 139 |
+
"- 建立合作冷链车队与外包商的服务等级基线\n",
|
| 140 |
+
),
|
| 141 |
+
)
|
| 142 |
+
db.execute(
|
| 143 |
+
"""
|
| 144 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 145 |
+
VALUES (?, 4, 'QualityAuditor', 'validation', ?)
|
| 146 |
+
""",
|
| 147 |
+
(
|
| 148 |
+
session_id,
|
| 149 |
+
"## 示例校验与验收\n\n"
|
| 150 |
+
"- 首轮模拟结果满足准时率与温控合规目标\n"
|
| 151 |
+
"- 建议在真实运营前增加极端天气与高峰场景的压力测试\n",
|
| 152 |
+
),
|
| 153 |
+
)
|
| 154 |
+
db.execute(
|
| 155 |
+
"""
|
| 156 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 157 |
+
VALUES (?, 5, 'Optimizer', 'iteration', ?)
|
| 158 |
+
""",
|
| 159 |
+
(
|
| 160 |
+
session_id,
|
| 161 |
+
"## 示例迭代计划\n\n"
|
| 162 |
+
"- 第二阶段纳入更多区域仓与三方物流合作方\n"
|
| 163 |
+
"- 收集真实运营数据后,进一步优化线路与车辆编组\n",
|
| 164 |
+
),
|
| 165 |
+
)
|
| 166 |
+
db.execute(
|
| 167 |
+
"""
|
| 168 |
+
INSERT INTO assets (session_id, asset_type, title, content)
|
| 169 |
+
VALUES (?, 'report', '冷链闭环运营资产包(示例)', ?)
|
| 170 |
+
""",
|
| 171 |
+
(
|
| 172 |
+
session_id,
|
| 173 |
+
"## 资产包概要(示例)\n\n"
|
| 174 |
+
"- 冷链网络拓扑草案:干线 + 末端两级结构\n"
|
| 175 |
+
"- 温控合规模板:2-8℃ 温度区间与偏差告警策略\n"
|
| 176 |
+
"- KPI 指标字典:准时率、合规率、成本节约率\n"
|
| 177 |
+
"- 合作方清单:仓、车队与医院联络窗口\n",
|
| 178 |
+
),
|
| 179 |
+
)
|
| 180 |
+
db.execute(
|
| 181 |
+
"""
|
| 182 |
+
INSERT INTO assets (session_id, asset_type, title, content)
|
| 183 |
+
VALUES (?, 'metrics', '本轮关键指标快照(示例)', ?)
|
| 184 |
+
""",
|
| 185 |
+
(session_id, json.dumps(tool_result, ensure_ascii=False)),
|
| 186 |
+
)
|
| 187 |
+
db.commit()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def init_db():
|
| 191 |
+
db = get_db()
|
| 192 |
+
db.execute(
|
| 193 |
+
"""
|
| 194 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 195 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 196 |
+
name TEXT,
|
| 197 |
+
scenario TEXT NOT NULL,
|
| 198 |
+
target TEXT,
|
| 199 |
+
constraints TEXT,
|
| 200 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 201 |
+
)
|
| 202 |
+
"""
|
| 203 |
+
)
|
| 204 |
+
db.execute(
|
| 205 |
+
"""
|
| 206 |
+
CREATE TABLE IF NOT EXISTS steps (
|
| 207 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 208 |
+
session_id INTEGER NOT NULL,
|
| 209 |
+
step_order INTEGER NOT NULL,
|
| 210 |
+
role TEXT NOT NULL,
|
| 211 |
+
step_type TEXT NOT NULL,
|
| 212 |
+
content TEXT NOT NULL,
|
| 213 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 214 |
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
| 215 |
+
)
|
| 216 |
+
"""
|
| 217 |
+
)
|
| 218 |
+
db.execute(
|
| 219 |
+
"""
|
| 220 |
+
CREATE TABLE IF NOT EXISTS assets (
|
| 221 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 222 |
+
session_id INTEGER NOT NULL,
|
| 223 |
+
asset_type TEXT NOT NULL,
|
| 224 |
+
title TEXT NOT NULL,
|
| 225 |
+
content TEXT NOT NULL,
|
| 226 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 227 |
+
FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
| 228 |
+
)
|
| 229 |
+
"""
|
| 230 |
+
)
|
| 231 |
+
db.commit()
|
| 232 |
+
seed_demo(db)
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
with app.app_context():
|
| 236 |
+
init_db()
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def build_api_url(base_url):
|
| 240 |
+
# 统一拼接 SiliconFlow API 地址
|
| 241 |
+
if base_url.endswith("/chat/completions"):
|
| 242 |
+
return base_url
|
| 243 |
+
return f"{base_url.rstrip('/')}/chat/completions"
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def call_llm(messages, temperature=0.7, max_tokens=900):
|
| 247 |
+
# 调用硅基流大模型,失败时自动回落
|
| 248 |
+
if not SILICONFLOW_API_KEY or not SILICONFLOW_API_KEY.startswith("sk-"):
|
| 249 |
+
return mock_completion(messages)
|
| 250 |
+
|
| 251 |
+
payload = {
|
| 252 |
+
"model": SILICONFLOW_MODEL,
|
| 253 |
+
"messages": messages,
|
| 254 |
+
"stream": False,
|
| 255 |
+
"temperature": temperature,
|
| 256 |
+
"max_tokens": max_tokens,
|
| 257 |
+
}
|
| 258 |
+
headers = {
|
| 259 |
+
"Authorization": f"Bearer {SILICONFLOW_API_KEY}",
|
| 260 |
+
"Content-Type": "application/json",
|
| 261 |
+
}
|
| 262 |
+
try:
|
| 263 |
+
resp = requests.post(build_api_url(SILICONFLOW_BASE_URL), json=payload, headers=headers, timeout=20)
|
| 264 |
+
if resp.status_code == 200:
|
| 265 |
+
data = resp.json()
|
| 266 |
+
return data["choices"][0]["message"]["content"]
|
| 267 |
+
return mock_completion(messages)
|
| 268 |
+
except Exception:
|
| 269 |
+
return mock_completion(messages)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def mock_completion(messages):
|
| 273 |
+
# 本地模拟输出,保持闭环演示可运行
|
| 274 |
+
last_user = ""
|
| 275 |
+
for m in reversed(messages):
|
| 276 |
+
if m.get("role") == "user":
|
| 277 |
+
last_user = m.get("content", "")[:240]
|
| 278 |
+
break
|
| 279 |
+
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 280 |
+
return (
|
| 281 |
+
f"## 本地模拟输出\n\n"
|
| 282 |
+
f"- 时间:{now}\n"
|
| 283 |
+
f"- 说明:未配置硅基流 API Key,系统使用规则化策略生成示意结果。\n\n"
|
| 284 |
+
f"### 场景摘要\n\n{last_user}\n\n"
|
| 285 |
+
f"### 建���要点\n\n"
|
| 286 |
+
f"- 优先保证温控合规与准时交付\n"
|
| 287 |
+
f"- 对高风险线路设置双层冗余方案\n"
|
| 288 |
+
f"- 建议分阶段上线与持续迭代"
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def simulate_coldchain_tools(scenario, target, constraints):
|
| 293 |
+
# 工具层模拟:输出可量化指标
|
| 294 |
+
seed = len(scenario) + len(target or "") + len(constraints or "")
|
| 295 |
+
random.seed(seed)
|
| 296 |
+
route_count = max(5, min(18, seed // 40))
|
| 297 |
+
hubs = max(2, min(8, seed // 90))
|
| 298 |
+
temp_risk = round(random.uniform(0.8, 2.8), 2)
|
| 299 |
+
on_time = round(96 + random.uniform(1.2, 2.8), 2)
|
| 300 |
+
compliance = round(99.0 + random.uniform(0.2, 0.7), 2)
|
| 301 |
+
cost_saving = round(random.uniform(5.0, 12.0), 1)
|
| 302 |
+
|
| 303 |
+
risk_keywords = ["堵车", "夜间", "限行", "偏差", "高峰", "雨雪", "台风"]
|
| 304 |
+
risk_score = 0
|
| 305 |
+
for k in risk_keywords:
|
| 306 |
+
if k in scenario or k in (constraints or ""):
|
| 307 |
+
risk_score += 2
|
| 308 |
+
risk_score += min(6, seed // 160)
|
| 309 |
+
if risk_score <= 3:
|
| 310 |
+
risk_level = "低"
|
| 311 |
+
elif risk_score <= 6:
|
| 312 |
+
risk_level = "中"
|
| 313 |
+
else:
|
| 314 |
+
risk_level = "高"
|
| 315 |
+
|
| 316 |
+
return {
|
| 317 |
+
"route_count": route_count,
|
| 318 |
+
"hub_count": hubs,
|
| 319 |
+
"temp_risk": temp_risk,
|
| 320 |
+
"on_time": on_time,
|
| 321 |
+
"compliance": compliance,
|
| 322 |
+
"cost_saving": cost_saving,
|
| 323 |
+
"risk_level": risk_level,
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def run_coldchain_workflow(scenario, target, constraints):
|
| 328 |
+
# 冷链闭环主流程:推理 -> 工具行动 -> 状态记忆 -> 校验验收 -> 迭代复盘
|
| 329 |
+
tool_result = simulate_coldchain_tools(scenario, target, constraints)
|
| 330 |
+
|
| 331 |
+
system_role = (
|
| 332 |
+
"你是冷链物流与医药合规专家,负责构建可执行的闭环方案。"
|
| 333 |
+
"输出中文 Markdown,结构清晰,强调可落地与可复用资产。"
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
reasoning_prompt = (
|
| 337 |
+
f"冷链场景:{scenario}\n"
|
| 338 |
+
f"目标:{target or '未指定'}\n"
|
| 339 |
+
f"约束:{constraints or '未指定'}\n"
|
| 340 |
+
"请生成推理/决策阶段的方案,包含关键假设、优先级、核心路径。"
|
| 341 |
+
)
|
| 342 |
+
reasoning = call_llm(
|
| 343 |
+
[
|
| 344 |
+
{"role": "system", "content": system_role},
|
| 345 |
+
{"role": "user", "content": reasoning_prompt},
|
| 346 |
+
],
|
| 347 |
+
temperature=0.6,
|
| 348 |
+
)
|
| 349 |
+
|
| 350 |
+
tool_md = (
|
| 351 |
+
"## 工具行动结果(模拟)\n\n"
|
| 352 |
+
f"- 规划干线/支线数:{tool_result['route_count']} 条\n"
|
| 353 |
+
f"- 冷链枢纽节点:{tool_result['hub_count']} 个\n"
|
| 354 |
+
f"- 温控偏差风险指数:{tool_result['temp_risk']}\n"
|
| 355 |
+
f"- 预计准时交付率:{tool_result['on_time']}%\n"
|
| 356 |
+
f"- 预计温控合规率:{tool_result['compliance']}%\n"
|
| 357 |
+
f"- 预计综合成本节约:{tool_result['cost_saving']}%\n"
|
| 358 |
+
f"- 风险等级:{tool_result['risk_level']}\n"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
memory_prompt = (
|
| 362 |
+
"请输出可沉淀的状态/记忆资产清单,至少包含:"
|
| 363 |
+
"标准化 SOP、关键指标字典、异常追溯模板、合作方清单。"
|
| 364 |
+
)
|
| 365 |
+
memory_md = call_llm(
|
| 366 |
+
[
|
| 367 |
+
{"role": "system", "content": system_role},
|
| 368 |
+
{"role": "user", "content": memory_prompt},
|
| 369 |
+
],
|
| 370 |
+
temperature=0.5,
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
validation_prompt = (
|
| 374 |
+
f"当前工具指标:准时交付率 {tool_result['on_time']}%,"
|
| 375 |
+
f"温控合规率 {tool_result['compliance']}%,成本节约 {tool_result['cost_saving']}%。"
|
| 376 |
+
"请生成校验/验收结论,列出是否达标与需补强项。"
|
| 377 |
+
)
|
| 378 |
+
validation_md = call_llm(
|
| 379 |
+
[
|
| 380 |
+
{"role": "system", "content": system_role},
|
| 381 |
+
{"role": "user", "content": validation_prompt},
|
| 382 |
+
],
|
| 383 |
+
temperature=0.4,
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
iteration_prompt = (
|
| 387 |
+
"请生成迭代/复盘计划,包含下一轮优化假设、需要补采的数据、"
|
| 388 |
+
"以及可以立即执行的 3 条改进动作。"
|
| 389 |
+
)
|
| 390 |
+
iteration_md = call_llm(
|
| 391 |
+
[
|
| 392 |
+
{"role": "system", "content": system_role},
|
| 393 |
+
{"role": "user", "content": iteration_prompt},
|
| 394 |
+
],
|
| 395 |
+
temperature=0.6,
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
steps = [
|
| 399 |
+
{
|
| 400 |
+
"step_order": 1,
|
| 401 |
+
"role": "Planner",
|
| 402 |
+
"step_type": "reasoning",
|
| 403 |
+
"content": reasoning,
|
| 404 |
+
},
|
| 405 |
+
{
|
| 406 |
+
"step_order": 2,
|
| 407 |
+
"role": "RouteOps",
|
| 408 |
+
"step_type": "tool_action",
|
| 409 |
+
"content": tool_md,
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"step_order": 3,
|
| 413 |
+
"role": "Memory",
|
| 414 |
+
"step_type": "memory",
|
| 415 |
+
"content": memory_md,
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"step_order": 4,
|
| 419 |
+
"role": "QualityAuditor",
|
| 420 |
+
"step_type": "validation",
|
| 421 |
+
"content": validation_md,
|
| 422 |
+
},
|
| 423 |
+
{
|
| 424 |
+
"step_order": 5,
|
| 425 |
+
"role": "Optimizer",
|
| 426 |
+
"step_type": "iteration",
|
| 427 |
+
"content": iteration_md,
|
| 428 |
+
},
|
| 429 |
+
]
|
| 430 |
+
|
| 431 |
+
assets = [
|
| 432 |
+
{
|
| 433 |
+
"asset_type": "report",
|
| 434 |
+
"title": "冷链闭环运营资产包",
|
| 435 |
+
"content": (
|
| 436 |
+
"## 资产包概要\n\n"
|
| 437 |
+
"- 冷链网络拓扑草案\n"
|
| 438 |
+
"- 温控合规模板与异常追溯SOP\n"
|
| 439 |
+
"- KPI 指标字典与验收阈值\n"
|
| 440 |
+
"- 供应商与节点协作清单\n"
|
| 441 |
+
),
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"asset_type": "metrics",
|
| 445 |
+
"title": "本轮关键指标快照",
|
| 446 |
+
"content": json.dumps(tool_result, ensure_ascii=False),
|
| 447 |
+
},
|
| 448 |
+
]
|
| 449 |
+
|
| 450 |
+
return steps, assets
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
@app.route("/")
|
| 454 |
+
def index():
|
| 455 |
+
return render_template("index.html")
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
@app.route("/api/overview")
|
| 459 |
+
def overview():
|
| 460 |
+
db = get_db()
|
| 461 |
+
session_count = db.execute("SELECT COUNT(*) AS c FROM sessions").fetchone()["c"]
|
| 462 |
+
step_count = db.execute("SELECT COUNT(*) AS c FROM steps").fetchone()["c"]
|
| 463 |
+
asset_count = db.execute("SELECT COUNT(*) AS c FROM assets").fetchone()["c"]
|
| 464 |
+
return jsonify(
|
| 465 |
+
{
|
| 466 |
+
"session_count": session_count,
|
| 467 |
+
"step_count": step_count,
|
| 468 |
+
"asset_count": asset_count,
|
| 469 |
+
}
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
@app.route("/api/sessions")
|
| 474 |
+
def list_sessions():
|
| 475 |
+
db = get_db()
|
| 476 |
+
rows = db.execute(
|
| 477 |
+
"SELECT id, name, scenario, target, created_at FROM sessions ORDER BY id DESC LIMIT 50"
|
| 478 |
+
).fetchall()
|
| 479 |
+
sessions = [
|
| 480 |
+
{
|
| 481 |
+
"id": r["id"],
|
| 482 |
+
"name": r["name"],
|
| 483 |
+
"scenario": r["scenario"],
|
| 484 |
+
"target": r["target"],
|
| 485 |
+
"created_at": r["created_at"],
|
| 486 |
+
}
|
| 487 |
+
for r in rows
|
| 488 |
+
]
|
| 489 |
+
return jsonify({"sessions": sessions})
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
@app.route("/api/session/<int:session_id>")
|
| 493 |
+
def get_session(session_id):
|
| 494 |
+
db = get_db()
|
| 495 |
+
session = db.execute(
|
| 496 |
+
"SELECT * FROM sessions WHERE id = ?", (session_id,)
|
| 497 |
+
).fetchone()
|
| 498 |
+
if not session:
|
| 499 |
+
return jsonify({"error": "会话不存在"}), 404
|
| 500 |
+
steps = db.execute(
|
| 501 |
+
"SELECT step_order, role, step_type, content, created_at FROM steps WHERE session_id = ? ORDER BY step_order ASC",
|
| 502 |
+
(session_id,),
|
| 503 |
+
).fetchall()
|
| 504 |
+
assets = db.execute(
|
| 505 |
+
"SELECT asset_type, title, content, created_at FROM assets WHERE session_id = ? ORDER BY id DESC",
|
| 506 |
+
(session_id,),
|
| 507 |
+
).fetchall()
|
| 508 |
+
return jsonify(
|
| 509 |
+
{
|
| 510 |
+
"session": dict(session),
|
| 511 |
+
"steps": [dict(s) for s in steps],
|
| 512 |
+
"assets": [dict(a) for a in assets],
|
| 513 |
+
}
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
@app.route("/api/assets")
|
| 518 |
+
def list_assets():
|
| 519 |
+
db = get_db()
|
| 520 |
+
rows = db.execute(
|
| 521 |
+
"SELECT id, session_id, asset_type, title, content, created_at FROM assets ORDER BY id DESC LIMIT 50"
|
| 522 |
+
).fetchall()
|
| 523 |
+
assets = [dict(r) for r in rows]
|
| 524 |
+
return jsonify({"assets": assets})
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
@app.route("/api/upload", methods=["POST"])
|
| 528 |
+
def upload_file():
|
| 529 |
+
file = request.files.get("file")
|
| 530 |
+
if not file:
|
| 531 |
+
return jsonify({"error": "未收到上传文件"}), 400
|
| 532 |
+
|
| 533 |
+
filename = secure_filename(file.filename or "unnamed.bin")
|
| 534 |
+
if not filename:
|
| 535 |
+
filename = "unnamed.bin"
|
| 536 |
+
|
| 537 |
+
uploads_dir = os.path.join(INSTANCE_DIR, "uploads")
|
| 538 |
+
os.makedirs(uploads_dir, exist_ok=True)
|
| 539 |
+
|
| 540 |
+
path = os.path.join(uploads_dir, filename)
|
| 541 |
+
file.save(path)
|
| 542 |
+
size_bytes = os.path.getsize(path)
|
| 543 |
+
size_kb = round(size_bytes / 1024, 2)
|
| 544 |
+
|
| 545 |
+
db = get_db()
|
| 546 |
+
db.execute(
|
| 547 |
+
"""
|
| 548 |
+
INSERT INTO assets (session_id, asset_type, title, content)
|
| 549 |
+
VALUES (?, ?, ?, ?)
|
| 550 |
+
""",
|
| 551 |
+
(
|
| 552 |
+
1,
|
| 553 |
+
"upload",
|
| 554 |
+
f"上传文件:{filename}",
|
| 555 |
+
f"大小:{size_kb} KB;存储于内部 uploads 目录,用于演示 Hugging Face 大文件处理策略。",
|
| 556 |
+
),
|
| 557 |
+
)
|
| 558 |
+
db.commit()
|
| 559 |
+
|
| 560 |
+
return jsonify({"filename": filename, "size_kb": size_kb})
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
@app.route("/api/run", methods=["POST"])
|
| 564 |
+
def run_workflow():
|
| 565 |
+
try:
|
| 566 |
+
data = request.get_json(force=True)
|
| 567 |
+
except Exception:
|
| 568 |
+
return jsonify({"error": "请求体必须为 JSON"}), 400
|
| 569 |
+
|
| 570 |
+
scenario = (data or {}).get("scenario", "").strip()
|
| 571 |
+
target = (data or {}).get("target", "").strip()
|
| 572 |
+
constraints = (data or {}).get("constraints", "").strip()
|
| 573 |
+
name = (data or {}).get("name", "").strip() or "冷链运营会话"
|
| 574 |
+
|
| 575 |
+
if not scenario:
|
| 576 |
+
return jsonify({"error": "请提供冷链场景描述"}), 400
|
| 577 |
+
|
| 578 |
+
db = get_db()
|
| 579 |
+
cursor = db.execute(
|
| 580 |
+
"INSERT INTO sessions (name, scenario, target, constraints) VALUES (?, ?, ?, ?)",
|
| 581 |
+
(name, scenario, target, constraints),
|
| 582 |
+
)
|
| 583 |
+
session_id = cursor.lastrowid
|
| 584 |
+
|
| 585 |
+
steps, assets = run_coldchain_workflow(scenario, target, constraints)
|
| 586 |
+
for s in steps:
|
| 587 |
+
db.execute(
|
| 588 |
+
"""
|
| 589 |
+
INSERT INTO steps (session_id, step_order, role, step_type, content)
|
| 590 |
+
VALUES (?, ?, ?, ?, ?)
|
| 591 |
+
""",
|
| 592 |
+
(session_id, s["step_order"], s["role"], s["step_type"], s["content"]),
|
| 593 |
+
)
|
| 594 |
+
for a in assets:
|
| 595 |
+
db.execute(
|
| 596 |
+
"""
|
| 597 |
+
INSERT INTO assets (session_id, asset_type, title, content)
|
| 598 |
+
VALUES (?, ?, ?, ?)
|
| 599 |
+
""",
|
| 600 |
+
(session_id, a["asset_type"], a["title"], a["content"]),
|
| 601 |
+
)
|
| 602 |
+
db.commit()
|
| 603 |
+
|
| 604 |
+
return jsonify(
|
| 605 |
+
{
|
| 606 |
+
"session_id": session_id,
|
| 607 |
+
"steps": steps,
|
| 608 |
+
"assets": assets,
|
| 609 |
+
}
|
| 610 |
+
)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
if __name__ == "__main__":
|
| 614 |
+
port = int(os.getenv("PORT", "7865"))
|
| 615 |
+
app.run(host="0.0.0.0", port=port, debug=False)
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
requests==2.31.0
|
| 3 |
+
python-dotenv==1.0.0
|
templates/index.html
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>冷链闭环智能体 · ColdChain Optimizer</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg: #f4f7fb;
|
| 11 |
+
--card: #ffffff;
|
| 12 |
+
--primary: #0f3d5e;
|
| 13 |
+
--accent: #14b8a6;
|
| 14 |
+
--text: #1f2937;
|
| 15 |
+
--muted: #6b7280;
|
| 16 |
+
--shadow: 0 12px 30px rgba(15, 61, 94, 0.12);
|
| 17 |
+
}
|
| 18 |
+
* {
|
| 19 |
+
box-sizing: border-box;
|
| 20 |
+
}
|
| 21 |
+
body {
|
| 22 |
+
margin: 0;
|
| 23 |
+
font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
| 24 |
+
background: var(--bg);
|
| 25 |
+
color: var(--text);
|
| 26 |
+
}
|
| 27 |
+
.app-shell {
|
| 28 |
+
max-width: 1200px;
|
| 29 |
+
margin: 0 auto;
|
| 30 |
+
padding: 24px 20px 48px;
|
| 31 |
+
}
|
| 32 |
+
header {
|
| 33 |
+
display: flex;
|
| 34 |
+
flex-wrap: wrap;
|
| 35 |
+
align-items: center;
|
| 36 |
+
justify-content: space-between;
|
| 37 |
+
gap: 16px;
|
| 38 |
+
margin-bottom: 20px;
|
| 39 |
+
}
|
| 40 |
+
.brand {
|
| 41 |
+
display: flex;
|
| 42 |
+
align-items: center;
|
| 43 |
+
gap: 16px;
|
| 44 |
+
}
|
| 45 |
+
.brand-mark {
|
| 46 |
+
width: 52px;
|
| 47 |
+
height: 52px;
|
| 48 |
+
border-radius: 16px;
|
| 49 |
+
background: linear-gradient(135deg, #0f3d5e, #14b8a6);
|
| 50 |
+
color: #fff;
|
| 51 |
+
font-weight: 700;
|
| 52 |
+
display: flex;
|
| 53 |
+
align-items: center;
|
| 54 |
+
justify-content: center;
|
| 55 |
+
font-size: 20px;
|
| 56 |
+
letter-spacing: 2px;
|
| 57 |
+
}
|
| 58 |
+
.brand-title {
|
| 59 |
+
font-size: 22px;
|
| 60 |
+
font-weight: 700;
|
| 61 |
+
}
|
| 62 |
+
.brand-subtitle {
|
| 63 |
+
color: var(--muted);
|
| 64 |
+
margin-top: 4px;
|
| 65 |
+
}
|
| 66 |
+
.chips {
|
| 67 |
+
display: flex;
|
| 68 |
+
gap: 8px;
|
| 69 |
+
flex-wrap: wrap;
|
| 70 |
+
}
|
| 71 |
+
.chip {
|
| 72 |
+
background: rgba(20, 184, 166, 0.12);
|
| 73 |
+
color: #0f766e;
|
| 74 |
+
padding: 6px 12px;
|
| 75 |
+
border-radius: 999px;
|
| 76 |
+
font-size: 12px;
|
| 77 |
+
font-weight: 600;
|
| 78 |
+
}
|
| 79 |
+
.overview {
|
| 80 |
+
display: grid;
|
| 81 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 82 |
+
gap: 16px;
|
| 83 |
+
margin-bottom: 24px;
|
| 84 |
+
}
|
| 85 |
+
.overview-card {
|
| 86 |
+
background: var(--card);
|
| 87 |
+
border-radius: 16px;
|
| 88 |
+
padding: 18px;
|
| 89 |
+
box-shadow: var(--shadow);
|
| 90 |
+
}
|
| 91 |
+
.overview-title {
|
| 92 |
+
font-size: 13px;
|
| 93 |
+
color: var(--muted);
|
| 94 |
+
}
|
| 95 |
+
.overview-value {
|
| 96 |
+
font-size: 26px;
|
| 97 |
+
font-weight: 700;
|
| 98 |
+
margin-top: 8px;
|
| 99 |
+
}
|
| 100 |
+
.layout {
|
| 101 |
+
display: grid;
|
| 102 |
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
| 103 |
+
gap: 20px;
|
| 104 |
+
}
|
| 105 |
+
.panel {
|
| 106 |
+
background: var(--card);
|
| 107 |
+
border-radius: 18px;
|
| 108 |
+
padding: 20px;
|
| 109 |
+
box-shadow: var(--shadow);
|
| 110 |
+
}
|
| 111 |
+
.panel-title {
|
| 112 |
+
font-size: 16px;
|
| 113 |
+
font-weight: 700;
|
| 114 |
+
margin-bottom: 12px;
|
| 115 |
+
}
|
| 116 |
+
.panel-subtitle {
|
| 117 |
+
color: var(--muted);
|
| 118 |
+
margin-bottom: 16px;
|
| 119 |
+
font-size: 13px;
|
| 120 |
+
}
|
| 121 |
+
.form-grid {
|
| 122 |
+
display: grid;
|
| 123 |
+
gap: 12px;
|
| 124 |
+
}
|
| 125 |
+
label {
|
| 126 |
+
font-size: 13px;
|
| 127 |
+
color: var(--muted);
|
| 128 |
+
}
|
| 129 |
+
input,
|
| 130 |
+
textarea {
|
| 131 |
+
width: 100%;
|
| 132 |
+
border-radius: 12px;
|
| 133 |
+
border: 1px solid #e5e7eb;
|
| 134 |
+
padding: 10px 12px;
|
| 135 |
+
font-size: 14px;
|
| 136 |
+
background: #f9fafb;
|
| 137 |
+
}
|
| 138 |
+
textarea {
|
| 139 |
+
min-height: 110px;
|
| 140 |
+
resize: vertical;
|
| 141 |
+
}
|
| 142 |
+
.btn-row {
|
| 143 |
+
display: flex;
|
| 144 |
+
gap: 12px;
|
| 145 |
+
flex-wrap: wrap;
|
| 146 |
+
}
|
| 147 |
+
button {
|
| 148 |
+
border: none;
|
| 149 |
+
border-radius: 12px;
|
| 150 |
+
padding: 10px 16px;
|
| 151 |
+
font-weight: 600;
|
| 152 |
+
cursor: pointer;
|
| 153 |
+
}
|
| 154 |
+
.btn-primary {
|
| 155 |
+
background: var(--primary);
|
| 156 |
+
color: #fff;
|
| 157 |
+
}
|
| 158 |
+
.btn-ghost {
|
| 159 |
+
background: rgba(15, 61, 94, 0.08);
|
| 160 |
+
color: var(--primary);
|
| 161 |
+
}
|
| 162 |
+
.status-card {
|
| 163 |
+
background: #0f3d5e;
|
| 164 |
+
color: #fff;
|
| 165 |
+
border-radius: 16px;
|
| 166 |
+
padding: 16px;
|
| 167 |
+
margin-top: 16px;
|
| 168 |
+
}
|
| 169 |
+
.timeline {
|
| 170 |
+
display: grid;
|
| 171 |
+
gap: 16px;
|
| 172 |
+
}
|
| 173 |
+
.timeline-item {
|
| 174 |
+
border: 1px solid #e5e7eb;
|
| 175 |
+
border-radius: 14px;
|
| 176 |
+
padding: 16px;
|
| 177 |
+
background: #fbfdff;
|
| 178 |
+
}
|
| 179 |
+
.timeline-header {
|
| 180 |
+
display: flex;
|
| 181 |
+
justify-content: space-between;
|
| 182 |
+
align-items: center;
|
| 183 |
+
margin-bottom: 8px;
|
| 184 |
+
}
|
| 185 |
+
.badge {
|
| 186 |
+
background: rgba(15, 61, 94, 0.1);
|
| 187 |
+
color: var(--primary);
|
| 188 |
+
padding: 4px 10px;
|
| 189 |
+
border-radius: 999px;
|
| 190 |
+
font-size: 12px;
|
| 191 |
+
}
|
| 192 |
+
.badge-soft {
|
| 193 |
+
background: rgba(20, 184, 166, 0.1);
|
| 194 |
+
color: #0f766e;
|
| 195 |
+
}
|
| 196 |
+
.timeline-type {
|
| 197 |
+
font-size: 13px;
|
| 198 |
+
}
|
| 199 |
+
.markdown {
|
| 200 |
+
line-height: 1.7;
|
| 201 |
+
font-size: 14px;
|
| 202 |
+
}
|
| 203 |
+
.history-list,
|
| 204 |
+
.asset-list {
|
| 205 |
+
display: grid;
|
| 206 |
+
gap: 10px;
|
| 207 |
+
margin-top: 12px;
|
| 208 |
+
}
|
| 209 |
+
.history-item,
|
| 210 |
+
.asset-item {
|
| 211 |
+
border: 1px solid #e5e7eb;
|
| 212 |
+
border-radius: 12px;
|
| 213 |
+
padding: 12px;
|
| 214 |
+
background: #fff;
|
| 215 |
+
cursor: pointer;
|
| 216 |
+
}
|
| 217 |
+
.muted {
|
| 218 |
+
color: var(--muted);
|
| 219 |
+
font-size: 12px;
|
| 220 |
+
}
|
| 221 |
+
.footer-tip,
|
| 222 |
+
.upload-tip {
|
| 223 |
+
margin-top: 18px;
|
| 224 |
+
color: var(--muted);
|
| 225 |
+
font-size: 12px;
|
| 226 |
+
}
|
| 227 |
+
@media (max-width: 960px) {
|
| 228 |
+
.layout {
|
| 229 |
+
grid-template-columns: 1fr;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
</style>
|
| 233 |
+
</head>
|
| 234 |
+
<body>
|
| 235 |
+
<div class="app-shell">
|
| 236 |
+
<header>
|
| 237 |
+
<div class="brand">
|
| 238 |
+
<div class="brand-mark">CC</div>
|
| 239 |
+
<div>
|
| 240 |
+
<div class="brand-title">冷链闭环智能体 · ColdChain Optimizer</div>
|
| 241 |
+
<div class="brand-subtitle">
|
| 242 |
+
推理 / 工具行动 / 状态记忆 / 校验验收 / 迭代复盘
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="chips">
|
| 247 |
+
<div class="chip">医药冷链</div>
|
| 248 |
+
<div class="chip">多智能体协作</div>
|
| 249 |
+
<div class="chip">资产沉淀</div>
|
| 250 |
+
<div class="chip">移动端适配</div>
|
| 251 |
+
</div>
|
| 252 |
+
</header>
|
| 253 |
+
|
| 254 |
+
<section class="overview" id="overview">
|
| 255 |
+
<div class="overview-card">
|
| 256 |
+
<div class="overview-title">累计会话</div>
|
| 257 |
+
<div class="overview-value" id="session-count">0</div>
|
| 258 |
+
</div>
|
| 259 |
+
<div class="overview-card">
|
| 260 |
+
<div class="overview-title">闭环步骤</div>
|
| 261 |
+
<div class="overview-value" id="step-count">0</div>
|
| 262 |
+
</div>
|
| 263 |
+
<div class="overview-card">
|
| 264 |
+
<div class="overview-title">沉淀资产</div>
|
| 265 |
+
<div class="overview-value" id="asset-count">0</div>
|
| 266 |
+
</div>
|
| 267 |
+
</section>
|
| 268 |
+
|
| 269 |
+
<main class="layout">
|
| 270 |
+
<section class="panel">
|
| 271 |
+
<div class="panel-title">冷链场景输入</div>
|
| 272 |
+
<div class="panel-subtitle">
|
| 273 |
+
输入真实业务场景,系统将自动生成完整闭环,并支持历史回放
|
| 274 |
+
</div>
|
| 275 |
+
<form id="run-form" class="form-grid">
|
| 276 |
+
<div>
|
| 277 |
+
<label>会话名称</label>
|
| 278 |
+
<input id="name" placeholder="如:华东医药冷链升级计划" />
|
| 279 |
+
</div>
|
| 280 |
+
<div>
|
| 281 |
+
<label>场景描述</label>
|
| 282 |
+
<textarea id="scenario" placeholder="描述冷链网络、温控要求、运输路径、服务对象等"></textarea>
|
| 283 |
+
</div>
|
| 284 |
+
<div>
|
| 285 |
+
<label>目标指标</label>
|
| 286 |
+
<input id="target" placeholder="如:准时交付率≥98%,温控合规率≥99.5%" />
|
| 287 |
+
</div>
|
| 288 |
+
<div>
|
| 289 |
+
<label>约束条件</label>
|
| 290 |
+
<textarea id="constraints" placeholder="如:夜间限行、预算、车辆通行证等"></textarea>
|
| 291 |
+
</div>
|
| 292 |
+
<div class="btn-row">
|
| 293 |
+
<button class="btn-primary" type="submit" id="run-btn">生成闭环</button>
|
| 294 |
+
<button class="btn-ghost" type="button" id="demo-btn">填充示例</button>
|
| 295 |
+
</div>
|
| 296 |
+
</form>
|
| 297 |
+
<div class="status-card" id="status-card">等待生成冷链闭环方案</div>
|
| 298 |
+
|
| 299 |
+
<div class="panel-title" style="margin-top: 18px;">历史会话资产</div>
|
| 300 |
+
<div class="history-list" id="history-list"></div>
|
| 301 |
+
|
| 302 |
+
<div class="panel-title" style="margin-top: 18px;">数据上传(可选)</div>
|
| 303 |
+
<div class="history-list">
|
| 304 |
+
<div class="history-item">
|
| 305 |
+
<div class="muted upload-tip">
|
| 306 |
+
可上传冷链线路 CSV/JSON 或二进制温度记录文件,系统将只记录文件名与大小作为资产示例,避免大文件占用 Hugging Face 空间。
|
| 307 |
+
</div>
|
| 308 |
+
<input type="file" id="file-input" />
|
| 309 |
+
<div class="btn-row" style="margin-top: 8px;">
|
| 310 |
+
<button class="btn-ghost" type="button" id="upload-btn">上传文件并登记资产</button>
|
| 311 |
+
</div>
|
| 312 |
+
<div class="muted" id="upload-status">尚未上传任何文件</div>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</section>
|
| 316 |
+
|
| 317 |
+
<section class="panel">
|
| 318 |
+
<div class="panel-title">闭环轨迹与资产</div>
|
| 319 |
+
<div class="panel-subtitle">按时间顺序展示本轮推理与执行过程(已内置示例会话)</div>
|
| 320 |
+
<div class="timeline" id="timeline"></div>
|
| 321 |
+
|
| 322 |
+
<div class="panel-title" style="margin-top: 18px;">资产沉淀</div>
|
| 323 |
+
<div class="asset-list" id="asset-list"></div>
|
| 324 |
+
<div class="footer-tip">
|
| 325 |
+
提示:设置 .env 的 SILICONFLOW_API_KEY 可接入真实硅基流推理
|
| 326 |
+
</div>
|
| 327 |
+
</section>
|
| 328 |
+
</main>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 332 |
+
<script>
|
| 333 |
+
const sessionCountEl = document.getElementById("session-count");
|
| 334 |
+
const stepCountEl = document.getElementById("step-count");
|
| 335 |
+
const assetCountEl = document.getElementById("asset-count");
|
| 336 |
+
const historyListEl = document.getElementById("history-list");
|
| 337 |
+
const timelineEl = document.getElementById("timeline");
|
| 338 |
+
const assetListEl = document.getElementById("asset-list");
|
| 339 |
+
const statusCardEl = document.getElementById("status-card");
|
| 340 |
+
const runForm = document.getElementById("run-form");
|
| 341 |
+
const runBtn = document.getElementById("run-btn");
|
| 342 |
+
const demoBtn = document.getElementById("demo-btn");
|
| 343 |
+
|
| 344 |
+
const renderMarkdown = (content) => {
|
| 345 |
+
if (!content) return "";
|
| 346 |
+
return marked.parse(content);
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
const updateOverview = async () => {
|
| 350 |
+
const res = await fetch("/api/overview");
|
| 351 |
+
const data = await res.json();
|
| 352 |
+
sessionCountEl.textContent = data.session_count || 0;
|
| 353 |
+
stepCountEl.textContent = data.step_count || 0;
|
| 354 |
+
assetCountEl.textContent = data.asset_count || 0;
|
| 355 |
+
};
|
| 356 |
+
|
| 357 |
+
const loadHistory = async () => {
|
| 358 |
+
const res = await fetch("/api/sessions");
|
| 359 |
+
const data = await res.json();
|
| 360 |
+
historyListEl.innerHTML = "";
|
| 361 |
+
(data.sessions || []).forEach((item) => {
|
| 362 |
+
const div = document.createElement("div");
|
| 363 |
+
div.className = "history-item";
|
| 364 |
+
div.innerHTML = `
|
| 365 |
+
<div><strong>${item.name || "未命名会话"}</strong></div>
|
| 366 |
+
<div class="muted">${item.created_at || ""}</div>
|
| 367 |
+
<div class="muted">目标:${item.target || "未填写"}</div>
|
| 368 |
+
`;
|
| 369 |
+
div.addEventListener("click", () => loadSession(item.id));
|
| 370 |
+
historyListEl.appendChild(div);
|
| 371 |
+
});
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
const stepTypeLabel = (type) => {
|
| 375 |
+
const map = {
|
| 376 |
+
reasoning: "推理决策",
|
| 377 |
+
tool_action: "工具行动",
|
| 378 |
+
memory: "状态记忆",
|
| 379 |
+
validation: "校验验收",
|
| 380 |
+
iteration: "迭代复盘",
|
| 381 |
+
};
|
| 382 |
+
return map[type] || type || "步骤";
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
const stepRoleLabel = (role) => {
|
| 386 |
+
const map = {
|
| 387 |
+
Planner: "规划智能体",
|
| 388 |
+
RouteOps: "线路与运力智能体",
|
| 389 |
+
Memory: "资产沉淀智能体",
|
| 390 |
+
QualityAuditor: "质量合规智能体",
|
| 391 |
+
Optimizer: "迭代优化智能体",
|
| 392 |
+
};
|
| 393 |
+
return map[role] || role || "智能体";
|
| 394 |
+
};
|
| 395 |
+
|
| 396 |
+
const renderTimeline = (steps = []) => {
|
| 397 |
+
timelineEl.innerHTML = "";
|
| 398 |
+
if (!steps.length) {
|
| 399 |
+
timelineEl.innerHTML =
|
| 400 |
+
"<div class='muted'>暂无闭环步骤,可点击左侧示例按钮快速体验。</div>";
|
| 401 |
+
return;
|
| 402 |
+
}
|
| 403 |
+
steps.forEach((step) => {
|
| 404 |
+
const card = document.createElement("div");
|
| 405 |
+
card.className = "timeline-item";
|
| 406 |
+
card.innerHTML = `
|
| 407 |
+
<div class="timeline-header">
|
| 408 |
+
<div class="timeline-type">
|
| 409 |
+
<strong>${stepTypeLabel(step.step_type)}</strong>
|
| 410 |
+
· ${stepRoleLabel(step.role)}
|
| 411 |
+
</div>
|
| 412 |
+
<span class="badge badge-soft">步骤 ${step.step_order}</span>
|
| 413 |
+
</div>
|
| 414 |
+
<div class="markdown">${renderMarkdown(step.content)}</div>
|
| 415 |
+
`;
|
| 416 |
+
timelineEl.appendChild(card);
|
| 417 |
+
});
|
| 418 |
+
};
|
| 419 |
+
|
| 420 |
+
const renderAssets = (assets = []) => {
|
| 421 |
+
assetListEl.innerHTML = "";
|
| 422 |
+
if (!assets.length) {
|
| 423 |
+
assetListEl.innerHTML = "<div class='muted'>暂无资产沉淀</div>";
|
| 424 |
+
return;
|
| 425 |
+
}
|
| 426 |
+
assets.forEach((asset) => {
|
| 427 |
+
const card = document.createElement("div");
|
| 428 |
+
card.className = "asset-item";
|
| 429 |
+
const isMetrics = asset.asset_type === "metrics";
|
| 430 |
+
const content = isMetrics
|
| 431 |
+
? `<pre class="muted">${asset.content}</pre>`
|
| 432 |
+
: renderMarkdown(asset.content);
|
| 433 |
+
card.innerHTML = `
|
| 434 |
+
<div><strong>${asset.title}</strong></div>
|
| 435 |
+
<div class="muted">${asset.created_at || ""}</div>
|
| 436 |
+
<div class="markdown">${content}</div>
|
| 437 |
+
`;
|
| 438 |
+
assetListEl.appendChild(card);
|
| 439 |
+
});
|
| 440 |
+
};
|
| 441 |
+
|
| 442 |
+
const loadSession = async (sessionId) => {
|
| 443 |
+
const res = await fetch(`/api/session/${sessionId}`);
|
| 444 |
+
const data = await res.json();
|
| 445 |
+
if (data.error) {
|
| 446 |
+
statusCardEl.textContent = data.error;
|
| 447 |
+
return;
|
| 448 |
+
}
|
| 449 |
+
const title = data.session?.name || "冷链运营会话";
|
| 450 |
+
statusCardEl.textContent = `正在回放:${title}`;
|
| 451 |
+
renderTimeline(data.steps || []);
|
| 452 |
+
renderAssets(data.assets || []);
|
| 453 |
+
};
|
| 454 |
+
|
| 455 |
+
runForm.addEventListener("submit", async (event) => {
|
| 456 |
+
event.preventDefault();
|
| 457 |
+
runBtn.disabled = true;
|
| 458 |
+
statusCardEl.textContent = "正在生成闭环方案,请稍候...";
|
| 459 |
+
const payload = {
|
| 460 |
+
name: document.getElementById("name").value,
|
| 461 |
+
scenario: document.getElementById("scenario").value,
|
| 462 |
+
target: document.getElementById("target").value,
|
| 463 |
+
constraints: document.getElementById("constraints").value,
|
| 464 |
+
};
|
| 465 |
+
const res = await fetch("/api/run", {
|
| 466 |
+
method: "POST",
|
| 467 |
+
headers: { "Content-Type": "application/json" },
|
| 468 |
+
body: JSON.stringify(payload),
|
| 469 |
+
});
|
| 470 |
+
const data = await res.json();
|
| 471 |
+
if (data.error) {
|
| 472 |
+
statusCardEl.textContent = data.error;
|
| 473 |
+
runBtn.disabled = false;
|
| 474 |
+
return;
|
| 475 |
+
}
|
| 476 |
+
statusCardEl.textContent = "闭环生成完成,可继续迭代";
|
| 477 |
+
renderTimeline(data.steps || []);
|
| 478 |
+
renderAssets(data.assets || []);
|
| 479 |
+
await updateOverview();
|
| 480 |
+
await loadHistory();
|
| 481 |
+
runBtn.disabled = false;
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
const fileInput = document.getElementById("file-input");
|
| 485 |
+
const uploadBtn = document.getElementById("upload-btn");
|
| 486 |
+
const uploadStatus = document.getElementById("upload-status");
|
| 487 |
+
|
| 488 |
+
uploadBtn.addEventListener("click", async () => {
|
| 489 |
+
if (!fileInput.files || !fileInput.files[0]) {
|
| 490 |
+
uploadStatus.textContent = "请先选择要上传的文件";
|
| 491 |
+
return;
|
| 492 |
+
}
|
| 493 |
+
const file = fileInput.files[0];
|
| 494 |
+
uploadStatus.textContent = "正在上传与登记资产...";
|
| 495 |
+
const formData = new FormData();
|
| 496 |
+
formData.append("file", file);
|
| 497 |
+
try {
|
| 498 |
+
const res = await fetch("/api/upload", {
|
| 499 |
+
method: "POST",
|
| 500 |
+
body: formData,
|
| 501 |
+
});
|
| 502 |
+
const data = await res.json();
|
| 503 |
+
if (data.error) {
|
| 504 |
+
uploadStatus.textContent = data.error;
|
| 505 |
+
return;
|
| 506 |
+
}
|
| 507 |
+
uploadStatus.textContent = `已登记资产:${data.filename}(${data.size_kb} KB)`;
|
| 508 |
+
await updateOverview();
|
| 509 |
+
await loadHistory();
|
| 510 |
+
} catch (e) {
|
| 511 |
+
uploadStatus.textContent = "上传失败,请稍后重试";
|
| 512 |
+
}
|
| 513 |
+
});
|
| 514 |
+
|
| 515 |
+
demoBtn.addEventListener("click", () => {
|
| 516 |
+
document.getElementById("name").value = "华南疫苗冷链优化";
|
| 517 |
+
document.getElementById("scenario").value =
|
| 518 |
+
"覆盖 4 个省会城市与 16 家疾控中心,要求 2-8℃温控,疫苗需 12 小时内送达;" +
|
| 519 |
+
"高峰期需提升运力 30%,并确保可追溯与异常预警。";
|
| 520 |
+
document.getElementById("target").value =
|
| 521 |
+
"准时交付率≥98%,温控合规率≥99.7%,运输成本降低 10%";
|
| 522 |
+
document.getElementById("constraints").value =
|
| 523 |
+
"夜间限行、高速收费上涨、部分区域冷库容量不足";
|
| 524 |
+
});
|
| 525 |
+
|
| 526 |
+
updateOverview();
|
| 527 |
+
loadHistory();
|
| 528 |
+
renderTimeline([]);
|
| 529 |
+
renderAssets([]);
|
| 530 |
+
</script>
|
| 531 |
+
</body>
|
| 532 |
+
</html>
|