Trae Assistant commited on
Commit
4797b80
·
1 Parent(s): 9cdfe90
Files changed (6) hide show
  1. .gitignore +5 -0
  2. Dockerfile +16 -0
  3. README.md +55 -5
  4. app.py +615 -0
  5. requirements.txt +3 -0
  6. 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: Coldchain Optimizer Agent
3
- emoji: 🏃
4
- colorFrom: red
5
- colorTo: green
6
  sdk: docker
 
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>