Trae Assistant commited on
Commit
be7a7ac
·
0 Parent(s):

init factory flow agent

Browse files
Files changed (7) hide show
  1. .gitattributes +35 -0
  2. .gitignore +3 -0
  3. Dockerfile +14 -0
  4. README.md +57 -0
  5. app.py +666 -0
  6. requirements.txt +2 -0
  7. templates/index.html +740 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__/
2
+ instance/*.db
3
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ RUN mkdir -p instance && chmod 777 instance
11
+
12
+ EXPOSE 7868
13
+
14
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: 产线闭环运营智能体
3
+ emoji: 🏭
4
+ colorFrom: orange
5
+ colorTo: slate
6
+ sdk: docker
7
+ app_port: 7868
8
+ short_description: 产线闭环运营智能体
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Factory Flow Agent(产线闭环运营智能体)
14
+
15
+ ## 项目简介
16
+ Factory Flow Agent 面向制造业产线与仓配协同场景,提供“推理决策 → 工具行动 → 状态记忆 → 校验验收 → 迭代复盘”的闭环智能体流程。
17
+ 系统支持方案回放、资产沉淀、对话分析与数据持久化,适用于生产优化、质量控制、能耗治理与交付保障等商业化场景。
18
+
19
+ ## 核心能力
20
+ - 六阶段闭环:推理决策、执行路线图、工具行动、状态记忆、校验验收、迭代复盘
21
+ - 指标看板:OEE、能耗、良率、交付与成本改善预估
22
+ - 快速模板:内置场景模板一键生成闭环
23
+ - 资产沉淀:可复用策略、模板与验收清单存储
24
+ - 对话分析:产线问题咨询与策略建议
25
+ - 数据持久化:SQLite 本地存储,支持历史查询
26
+
27
+ ## 技术栈
28
+ - 后端:Python + Flask + SQLite
29
+ - 前端:Vue 3 + Marked.js(Markdown 渲染)
30
+ - 模型:SiliconFlow API(未配置时自动使用本地模拟)
31
+ - 部署:Docker
32
+
33
+ ## 快速开始
34
+
35
+ ### 本地运行
36
+ ```bash
37
+ pip install -r requirements.txt
38
+ python app.py
39
+ ```
40
+ 访问:http://localhost:7868
41
+
42
+ ### Docker 运行
43
+ ```bash
44
+ docker build -t factory-flow-agent .
45
+ docker run -p 7868:7868 factory-flow-agent
46
+ ```
47
+
48
+ ### 模型配置
49
+ 如需连接硅基流,设置环境变量:
50
+ ```bash
51
+ export SILICONFLOW_API_KEY="你的_API_KEY"
52
+ ```
53
+
54
+ ## 商业价值
55
+ - 制造企业降本增效、缩短交付周期与提升良率
56
+ - 支持构建可复用的产线知识资产与运营标准
57
+ - 可扩展为 SaaS 或行业解决方案
app.py ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ import json
4
+ import datetime
5
+ import random
6
+ import requests
7
+ from flask import Flask, render_template, request, jsonify, g
8
+
9
+ app = Flask(__name__, instance_relative_config=True)
10
+ app.config["SECRET_KEY"] = "factory-flow-secret-key"
11
+ app.config["JSON_AS_ASCII"] = False
12
+
13
+ try:
14
+ os.makedirs(app.instance_path, exist_ok=True)
15
+ except OSError:
16
+ pass
17
+
18
+ DB_PATH = os.path.join(app.instance_path, "factory_flow.db")
19
+
20
+ SILICON_FLOW_API_URL = "https://api.siliconflow.cn/v1/chat/completions"
21
+ DEFAULT_MODEL = "Qwen/Qwen2.5-7B-Instruct"
22
+ DEFAULT_TEMPLATES = [
23
+ {
24
+ "title": "电池产线良率提升闭环",
25
+ "scenario": "动力电池产线希望在 2 个月内提升良品率 1.5%,同时降低涂布段停机率。请给出闭环方案与验收指标。",
26
+ "hint": "优先解决涂布段设备老化与质量波动问题。",
27
+ },
28
+ {
29
+ "title": "仓配协同排产闭环",
30
+ "scenario": "电子制造工厂需要将排产与仓配补料联动,降低缺料率并提高交付准时率。",
31
+ "hint": "关注安全库存阈值与跨班次协同。",
32
+ },
33
+ {
34
+ "title": "冷链质量追溯闭环",
35
+ "scenario": "冷链食品工厂的温控波动导致返工率上升,需要建立温控预警与批次追溯体系。",
36
+ "hint": "明确温控指标和追溯链路。",
37
+ },
38
+ {
39
+ "title": "设备预防性维护闭环",
40
+ "scenario": "设备故障频发导致产能波动,要求建立预防性维护节奏与停机风险预测。",
41
+ "hint": "补齐设备状态监测与维护日志闭环。",
42
+ },
43
+ ]
44
+
45
+
46
+ def get_db():
47
+ db = getattr(g, "_database", None)
48
+ if db is None:
49
+ db = g._database = sqlite3.connect(DB_PATH)
50
+ db.row_factory = sqlite3.Row
51
+ return db
52
+
53
+
54
+ @app.teardown_appcontext
55
+ def close_connection(exception):
56
+ db = getattr(g, "_database", None)
57
+ if db is not None:
58
+ db.close()
59
+
60
+
61
+ def init_db():
62
+ db = get_db()
63
+ db.execute(
64
+ """
65
+ CREATE TABLE IF NOT EXISTS sessions (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ name TEXT,
68
+ scenario TEXT NOT NULL,
69
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
70
+ )
71
+ """
72
+ )
73
+ db.execute(
74
+ """
75
+ CREATE TABLE IF NOT EXISTS steps (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ session_id INTEGER NOT NULL,
78
+ step_order INTEGER NOT NULL,
79
+ role TEXT NOT NULL,
80
+ step_type TEXT NOT NULL,
81
+ content TEXT NOT NULL,
82
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
83
+ FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
84
+ )
85
+ """
86
+ )
87
+ db.execute(
88
+ """
89
+ CREATE TABLE IF NOT EXISTS assets (
90
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
91
+ title TEXT NOT NULL,
92
+ category TEXT NOT NULL,
93
+ content TEXT NOT NULL,
94
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
95
+ )
96
+ """
97
+ )
98
+ db.execute(
99
+ """
100
+ CREATE TABLE IF NOT EXISTS chats (
101
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
102
+ role TEXT NOT NULL,
103
+ content TEXT NOT NULL,
104
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
105
+ )
106
+ """
107
+ )
108
+ db.commit()
109
+ seed_default_data(db)
110
+
111
+
112
+ def seed_default_data(db):
113
+ row = db.execute("SELECT COUNT(*) AS c FROM sessions").fetchone()
114
+ if row and row["c"]:
115
+ return
116
+
117
+ demos = [
118
+ {
119
+ "name": "示例会话 · 新能源电池产线降本增效",
120
+ "scenario": (
121
+ "一家动力电池工厂计划在 3 个月内将良品率提升 2%,并降低单位能耗 8%。"
122
+ "当前产线瓶颈集中在涂布与化成段,设备老化导致停机频繁。"
123
+ "要求输出:可执行的改造路径、数据采集方案、质量验证与验收指标。"
124
+ ),
125
+ "hint": "优先解决涂布段产能与能耗问题,同时建立快速验收闭环。",
126
+ },
127
+ {
128
+ "name": "示例会话 · 智能仓配协同工厂排产",
129
+ "scenario": (
130
+ "一家电子制造企业需要将产线排产与仓配补料联动。"
131
+ "目标是让关键物料缺料率下降 50%,并保证交付准时率 > 95%。"
132
+ "请给出排产策略、补料触发规则、异常处理与复盘机制。"
133
+ ),
134
+ "hint": "关注关键物料的安全库存阈值与跨班次协同。",
135
+ },
136
+ {
137
+ "name": "示例会话 · 食品工厂冷链质量闭环",
138
+ "scenario": (
139
+ "一家冷链食品工厂需要降低返工率并提升出货稳定性。"
140
+ "当前问题集中在冷链温控波动、包装漏检与批次追溯不完整。"
141
+ "请给出数据采集与批次追溯闭环方案,并输出验收指标。"
142
+ ),
143
+ "hint": "优先建立温控波动预警与批次追溯链路。",
144
+ },
145
+ ]
146
+
147
+ for item in demos:
148
+ cursor = db.execute(
149
+ "INSERT INTO sessions (name, scenario) VALUES (?, ?)",
150
+ (item["name"], item["scenario"]),
151
+ )
152
+ session_id = cursor.lastrowid
153
+ result = run_factory_workflow(item["scenario"], extra_hint=item["hint"])
154
+ for s in result["steps"]:
155
+ db.execute(
156
+ """
157
+ INSERT INTO steps (session_id, step_order, role, step_type, content)
158
+ VALUES (?, ?, ?, ?, ?)
159
+ """,
160
+ (session_id, s["step_order"], s["role"], s["step_type"], s["content"]),
161
+ )
162
+
163
+ db.execute(
164
+ """
165
+ INSERT INTO assets (title, category, content)
166
+ VALUES (?, ?, ?)
167
+ """,
168
+ (
169
+ "基准产线画像模板",
170
+ "流程资产",
171
+ "用于沉淀关键工序、设备台账与质量指标的基础模板,可直接复用为后续方案的输入。",
172
+ ),
173
+ )
174
+ db.execute(
175
+ """
176
+ INSERT INTO assets (title, category, content)
177
+ VALUES (?, ?, ?)
178
+ """,
179
+ (
180
+ "关键工序异常响应卡",
181
+ "运营资产",
182
+ "当检测到异常停机、良率波动或温控偏移时,按照分级响应流程快速锁定问题并记录复盘。",
183
+ ),
184
+ )
185
+ db.execute(
186
+ "INSERT INTO chats (role, content) VALUES (?, ?)",
187
+ ("assistant", "你好,我可以帮你评估产线瓶颈、成本与交付风险。"),
188
+ )
189
+ db.commit()
190
+
191
+
192
+ def call_llm(messages, model=DEFAULT_MODEL, max_tokens=1200):
193
+ api_key = os.getenv("SILICONFLOW_API_KEY", "").strip()
194
+
195
+ def mock_completion():
196
+ now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
197
+ last_user = ""
198
+ for m in reversed(messages):
199
+ if m.get("role") == "user":
200
+ last_user = m.get("content", "")[:300]
201
+ break
202
+ return (
203
+ f"## 本地模拟智能体回复\n\n"
204
+ f"- 时间:{now}\n"
205
+ f"- 说明:当前未连接硅基流 API,系统使用本地规则进行推演。\n\n"
206
+ f"### 输入摘要\n\n"
207
+ f"{last_user}\n\n"
208
+ f"### 建议\n\n"
209
+ f"- 先建立数据采集与异常预警,再扩展到全链路优化。"
210
+ )
211
+
212
+ if not api_key or not api_key.startswith("sk-"):
213
+ return mock_completion()
214
+
215
+ headers = {
216
+ "Authorization": f"Bearer {api_key}",
217
+ "Content-Type": "application/json",
218
+ }
219
+ payload = {
220
+ "model": model,
221
+ "messages": messages,
222
+ "stream": False,
223
+ "max_tokens": max_tokens,
224
+ }
225
+
226
+ try:
227
+ resp = requests.post(SILICON_FLOW_API_URL, headers=headers, json=payload, timeout=20)
228
+ if resp.status_code == 200:
229
+ data = resp.json()
230
+ return data["choices"][0]["message"]["content"]
231
+ return mock_completion()
232
+ except Exception:
233
+ return mock_completion()
234
+
235
+
236
+ def simulate_factory_tools(scenario_text):
237
+ length = len(scenario_text)
238
+ demand_index = min(96, max(55, 55 + length % 42))
239
+ energy_cost_index = round(0.62 + (length % 36) * 0.01, 2)
240
+ utilization = min(93, 66 + (length % 24))
241
+ defect_risk = min(88, 35 + (length % 52))
242
+
243
+ bottlenecks = ["涂布段", "化成段", "贴片段", "包装段", "检测段", "仓配补料"]
244
+ weighted = []
245
+ for item in bottlenecks:
246
+ weight = 1
247
+ if item in scenario_text:
248
+ weight += 2
249
+ weighted.extend([item] * weight)
250
+ bottleneck = random.choice(weighted)
251
+
252
+ keywords = ["冷链", "良品率", "缺料", "停机", "能耗", "交付", "追溯", "排产"]
253
+ risk_score = 0
254
+ for k in keywords:
255
+ if k in scenario_text:
256
+ risk_score += 7
257
+ risk_score = min(92, risk_score + length % 12)
258
+
259
+ sla_score = min(98, 72 + (100 - risk_score) // 4)
260
+ oee = min(90, max(58, 60 + (length % 28)))
261
+ throughput_gain = min(22, 6 + (length % 16))
262
+ cost_reduction = min(20, 5 + (length % 14))
263
+ energy_saving = min(18, 4 + (length % 12))
264
+ inventory_turnover = min(14, 6 + (length % 9))
265
+
266
+ return {
267
+ "demand_index": demand_index,
268
+ "energy_cost_index": energy_cost_index,
269
+ "capacity_utilization": utilization,
270
+ "defect_risk": defect_risk,
271
+ "bottleneck": bottleneck,
272
+ "risk_score": risk_score,
273
+ "sla_score": sla_score,
274
+ "oee": oee,
275
+ "throughput_gain": throughput_gain,
276
+ "cost_reduction": cost_reduction,
277
+ "energy_saving": energy_saving,
278
+ "inventory_turnover": inventory_turnover,
279
+ }
280
+
281
+
282
+ def build_state_memory(scenario_text, tool_data):
283
+ return (
284
+ "### 产线状态记忆快照\n\n"
285
+ f"- 当前瓶颈:{tool_data['bottleneck']}\n"
286
+ f"- 产能利用率:{tool_data['capacity_utilization']}%\n"
287
+ f"- 缺陷风险指数:{tool_data['defect_risk']}\n"
288
+ f"- OEE 综合效率:{tool_data['oee']}%\n"
289
+ f"- 能耗成本指数:{tool_data['energy_cost_index']}\n\n"
290
+ "### 可复用资产\n\n"
291
+ "- 产线瓶颈诊断模板\n"
292
+ "- 工序 KPI 看板结构\n"
293
+ "- 质量验收与复盘清单"
294
+ )
295
+
296
+
297
+ def build_acceptance(tool_data):
298
+ score = round(
299
+ (tool_data["sla_score"] * 0.5)
300
+ + (100 - tool_data["defect_risk"]) * 0.3
301
+ + (100 - tool_data["risk_score"]) * 0.2,
302
+ 1,
303
+ )
304
+ verdict = "通过" if score >= 75 else "需整改"
305
+ return {
306
+ "acceptance_score": score,
307
+ "verdict": verdict,
308
+ "key_checks": [
309
+ "瓶颈段产能提升是否达到目标",
310
+ "能耗与成本下降是否有数据闭环",
311
+ "质量缺陷率是否稳定下降",
312
+ "异常处理与复盘是否形成闭环机制",
313
+ ],
314
+ }
315
+
316
+ def derive_risk_level(score):
317
+ if score <= 35:
318
+ return "低"
319
+ if score <= 70:
320
+ return "中"
321
+ return "高"
322
+
323
+
324
+ def build_insight(tool_data):
325
+ risk_level = derive_risk_level(tool_data["risk_score"])
326
+ gaps = []
327
+ if tool_data["defect_risk"] >= 60:
328
+ gaps.append("质量波动偏高")
329
+ if tool_data["capacity_utilization"] < 75:
330
+ gaps.append("产能利用率偏低")
331
+ if tool_data["energy_cost_index"] > 0.75:
332
+ gaps.append("能耗成本偏高")
333
+ if not gaps:
334
+ gaps.append("暂无明显短板")
335
+ return {
336
+ "risk_level": risk_level,
337
+ "gaps": gaps,
338
+ "roi_hint": f"预计成本下降 {tool_data['cost_reduction']}%,产能提升 {tool_data['throughput_gain']}%。",
339
+ }
340
+
341
+
342
+ def build_action_plan(tool_data):
343
+ return (
344
+ "### 执行路线图\n\n"
345
+ "#### 第 1 周:数据基线建立\n"
346
+ "- 梳理瓶颈工序与关键设备状态\n"
347
+ "- 建立良率、能耗、停机与交付基线\n\n"
348
+ "#### 第 2-4 周:瓶颈突破与协同排产\n"
349
+ f"- 重点优化工序:{tool_data['bottleneck']}\n"
350
+ "- 建立补料触发规则与异常响应流程\n"
351
+ "- 引入批次追溯与质量预警\n\n"
352
+ "#### 第 5-8 周:质量闭环与节能优化\n"
353
+ "- 固化标准作业与质量验收清单\n"
354
+ "- 建立节能计划与设备维护节奏\n\n"
355
+ "#### 第 9-12 周:规模化复制与复盘\n"
356
+ "- 复制闭环方案到相似产线\n"
357
+ "- 形成复盘报告与资产沉淀"
358
+ )
359
+
360
+
361
+ def run_factory_workflow(scenario_text, extra_hint=None):
362
+ system_prompt = (
363
+ "你是产线闭环运营智能体,负责从推理决策到工具行动、验收与复盘。"
364
+ "输出内容必须是可执行的中文 Markdown。"
365
+ )
366
+ hint = extra_hint or "请给出可落地的改造步骤与量化指标。"
367
+
368
+ reasoning = call_llm(
369
+ [
370
+ {"role": "system", "content": system_prompt},
371
+ {
372
+ "role": "user",
373
+ "content": f"场景:{scenario_text}\n要求:{hint}\n请输出决策推理与行动路径。",
374
+ },
375
+ ]
376
+ )
377
+
378
+ tool_data = simulate_factory_tools(scenario_text)
379
+ tool_payload = json.dumps(tool_data, ensure_ascii=False, indent=2)
380
+
381
+ state_memory = build_state_memory(scenario_text, tool_data)
382
+ acceptance = build_acceptance(tool_data)
383
+ action_plan = build_action_plan(tool_data)
384
+ acceptance_md = (
385
+ "### 验收与校验\n\n"
386
+ f"- 综合得分:{acceptance['acceptance_score']}\n"
387
+ f"- 验收结论:{acceptance['verdict']}\n\n"
388
+ "#### 关键校验点\n\n"
389
+ + "\n".join([f"- {c}" for c in acceptance["key_checks"]])
390
+ )
391
+
392
+ iteration = call_llm(
393
+ [
394
+ {"role": "system", "content": system_prompt},
395
+ {
396
+ "role": "user",
397
+ "content": (
398
+ f"基于以下产线工具结果与验收结论,输出迭代与复盘计划:\n\n"
399
+ f"工具结果:{tool_payload}\n\n"
400
+ f"验收结论:{acceptance['verdict']}\n"
401
+ ),
402
+ },
403
+ ]
404
+ )
405
+
406
+ insight = build_insight(tool_data)
407
+ return {
408
+ "steps": [
409
+ {
410
+ "step_order": 1,
411
+ "role": "决策官",
412
+ "step_type": "reasoning",
413
+ "content": reasoning,
414
+ },
415
+ {
416
+ "step_order": 2,
417
+ "role": "计划官",
418
+ "step_type": "plan",
419
+ "content": action_plan,
420
+ },
421
+ {
422
+ "step_order": 3,
423
+ "role": "工具引擎",
424
+ "step_type": "tool_action",
425
+ "content": tool_payload,
426
+ },
427
+ {
428
+ "step_order": 4,
429
+ "role": "状态记忆",
430
+ "step_type": "state_memory",
431
+ "content": state_memory,
432
+ },
433
+ {
434
+ "step_order": 5,
435
+ "role": "验收官",
436
+ "step_type": "verification",
437
+ "content": acceptance_md,
438
+ },
439
+ {
440
+ "step_order": 6,
441
+ "role": "复盘官",
442
+ "step_type": "iteration",
443
+ "content": iteration,
444
+ },
445
+ ],
446
+ "tool_data": tool_data,
447
+ "acceptance": acceptance,
448
+ "insight": insight,
449
+ "state_memory": state_memory,
450
+ }
451
+
452
+
453
+ @app.route("/")
454
+ def index():
455
+ return render_template("index.html")
456
+
457
+
458
+ @app.route("/api/sessions", methods=["GET"])
459
+ def list_sessions():
460
+ db = get_db()
461
+ rows = db.execute(
462
+ "SELECT id, name, scenario, created_at FROM sessions ORDER BY id DESC LIMIT 50"
463
+ ).fetchall()
464
+ sessions = [
465
+ {
466
+ "id": r["id"],
467
+ "name": r["name"],
468
+ "scenario": r["scenario"],
469
+ "created_at": r["created_at"],
470
+ }
471
+ for r in rows
472
+ ]
473
+ return jsonify({"sessions": sessions})
474
+
475
+
476
+ @app.route("/api/templates", methods=["GET"])
477
+ def list_templates():
478
+ return jsonify({"templates": DEFAULT_TEMPLATES})
479
+
480
+
481
+ @app.route("/api/overview", methods=["GET"])
482
+ def overview():
483
+ db = get_db()
484
+ session_count = db.execute("SELECT COUNT(*) AS c FROM sessions").fetchone()["c"]
485
+ asset_count = db.execute("SELECT COUNT(*) AS c FROM assets").fetchone()["c"]
486
+ chat_count = db.execute("SELECT COUNT(*) AS c FROM chats").fetchone()["c"]
487
+ latest_tool = db.execute(
488
+ "SELECT content FROM steps WHERE step_type = 'tool_action' ORDER BY id DESC LIMIT 1"
489
+ ).fetchone()
490
+ tool_data = {}
491
+ if latest_tool:
492
+ try:
493
+ tool_data = json.loads(latest_tool["content"])
494
+ except Exception:
495
+ tool_data = {}
496
+ return jsonify(
497
+ {
498
+ "session_count": session_count,
499
+ "asset_count": asset_count,
500
+ "chat_count": chat_count,
501
+ "latest_tool": tool_data,
502
+ }
503
+ )
504
+
505
+
506
+ @app.route("/api/sessions/<int:session_id>", methods=["GET"])
507
+ def get_session_detail(session_id):
508
+ db = get_db()
509
+ steps = db.execute(
510
+ """
511
+ SELECT step_order, role, step_type, content, created_at
512
+ FROM steps WHERE session_id = ?
513
+ ORDER BY step_order ASC
514
+ """,
515
+ (session_id,),
516
+ ).fetchall()
517
+ step_list = [
518
+ {
519
+ "step_order": s["step_order"],
520
+ "role": s["role"],
521
+ "step_type": s["step_type"],
522
+ "content": s["content"],
523
+ "created_at": s["created_at"],
524
+ }
525
+ for s in steps
526
+ ]
527
+ return jsonify({"steps": step_list})
528
+
529
+
530
+ @app.route("/api/run", methods=["POST"])
531
+ def run_workflow():
532
+ try:
533
+ data = request.get_json(force=True)
534
+ except Exception:
535
+ return jsonify({"error": "请求体必须为 JSON"}), 400
536
+
537
+ scenario = (data or {}).get("scenario", "").strip()
538
+ name = (data or {}).get("name", "").strip() or "产线闭环会话"
539
+ hint = (data or {}).get("hint", "").strip()
540
+
541
+ if not scenario:
542
+ return jsonify({"error": "请提供场景描述"}), 400
543
+
544
+ db = get_db()
545
+ cursor = db.execute(
546
+ "INSERT INTO sessions (name, scenario) VALUES (?, ?)", (name, scenario)
547
+ )
548
+ session_id = cursor.lastrowid
549
+
550
+ result = run_factory_workflow(scenario, extra_hint=hint)
551
+ for s in result["steps"]:
552
+ db.execute(
553
+ """
554
+ INSERT INTO steps (session_id, step_order, role, step_type, content)
555
+ VALUES (?, ?, ?, ?, ?)
556
+ """,
557
+ (session_id, s["step_order"], s["role"], s["step_type"], s["content"]),
558
+ )
559
+ if result["acceptance"]["verdict"] == "通过":
560
+ db.execute(
561
+ "INSERT INTO assets (title, category, content) VALUES (?, ?, ?)",
562
+ (
563
+ f"{name} · 闭环方案摘要",
564
+ "闭环资产",
565
+ result["state_memory"],
566
+ ),
567
+ )
568
+ db.commit()
569
+ return jsonify(
570
+ {
571
+ "session_id": session_id,
572
+ "tool_data": result["tool_data"],
573
+ "acceptance": result["acceptance"],
574
+ "insight": result["insight"],
575
+ }
576
+ ), 201
577
+
578
+
579
+ @app.route("/api/assets", methods=["GET"])
580
+ def list_assets():
581
+ db = get_db()
582
+ rows = db.execute(
583
+ "SELECT id, title, category, content, created_at FROM assets ORDER BY id DESC"
584
+ ).fetchall()
585
+ assets = [
586
+ {
587
+ "id": r["id"],
588
+ "title": r["title"],
589
+ "category": r["category"],
590
+ "content": r["content"],
591
+ "created_at": r["created_at"],
592
+ }
593
+ for r in rows
594
+ ]
595
+ return jsonify({"assets": assets})
596
+
597
+
598
+ @app.route("/api/assets", methods=["POST"])
599
+ def create_asset():
600
+ data = request.get_json(force=True)
601
+ title = (data.get("title") or "").strip()
602
+ category = (data.get("category") or "").strip()
603
+ content = (data.get("content") or "").strip()
604
+
605
+ if not title or not category or not content:
606
+ return jsonify({"error": "标题、分类与内容不能为��"}), 400
607
+
608
+ db = get_db()
609
+ db.execute(
610
+ "INSERT INTO assets (title, category, content) VALUES (?, ?, ?)",
611
+ (title, category, content),
612
+ )
613
+ db.commit()
614
+ return jsonify({"status": "ok"})
615
+
616
+
617
+ @app.route("/api/chats", methods=["GET"])
618
+ def list_chats():
619
+ db = get_db()
620
+ rows = db.execute(
621
+ "SELECT role, content, created_at FROM chats ORDER BY id DESC LIMIT 50"
622
+ ).fetchall()
623
+ chats = [
624
+ {"role": r["role"], "content": r["content"], "created_at": r["created_at"]}
625
+ for r in rows[::-1]
626
+ ]
627
+ return jsonify({"messages": chats})
628
+
629
+
630
+ @app.route("/api/chat", methods=["POST"])
631
+ def chat():
632
+ data = request.get_json(force=True)
633
+ message = (data.get("message") or "").strip()
634
+ if not message:
635
+ return jsonify({"error": "消息不能为空"}), 400
636
+
637
+ system_prompt = (
638
+ "你是制造业产线运营顾问,擅长在成本、质量、交付与风险之间做平衡决策。"
639
+ "你的回复必须是中文 Markdown。"
640
+ )
641
+
642
+ reply = call_llm(
643
+ [
644
+ {"role": "system", "content": system_prompt},
645
+ {"role": "user", "content": message},
646
+ ]
647
+ )
648
+
649
+ db = get_db()
650
+ db.execute(
651
+ "INSERT INTO chats (role, content) VALUES (?, ?)", ("user", message)
652
+ )
653
+ db.execute(
654
+ "INSERT INTO chats (role, content) VALUES (?, ?)", ("assistant", reply)
655
+ )
656
+ db.commit()
657
+ return jsonify({"reply": reply})
658
+
659
+
660
+ with app.app_context():
661
+ init_db()
662
+
663
+
664
+ if __name__ == "__main__":
665
+ port = int(os.getenv("PORT", "7868"))
666
+ app.run(host="0.0.0.0", port=port, debug=False)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask
2
+ requests
templates/index.html ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Factory Flow Agent | 产线闭环运营智能体</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
+ <style>
10
+ :root {
11
+ color-scheme: light;
12
+ font-family: "Inter", "Noto Sans SC", system-ui, -apple-system, sans-serif;
13
+ background: #f4f6f8;
14
+ color: #1f2937;
15
+ }
16
+ body {
17
+ margin: 0;
18
+ min-height: 100vh;
19
+ background: #f4f6f8;
20
+ }
21
+ .app-shell {
22
+ display: flex;
23
+ flex-direction: column;
24
+ min-height: 100vh;
25
+ }
26
+ header {
27
+ background: linear-gradient(120deg, #111827, #1f2937);
28
+ color: #fff;
29
+ padding: 24px 28px;
30
+ }
31
+ .brand {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 12px;
35
+ }
36
+ .brand-mark {
37
+ width: 46px;
38
+ height: 46px;
39
+ border-radius: 12px;
40
+ background: linear-gradient(135deg, #f97316, #f59e0b);
41
+ display: grid;
42
+ place-items: center;
43
+ font-weight: 700;
44
+ font-size: 18px;
45
+ }
46
+ .brand-title {
47
+ font-size: 20px;
48
+ font-weight: 700;
49
+ }
50
+ .brand-subtitle {
51
+ font-size: 13px;
52
+ opacity: 0.75;
53
+ }
54
+ .tagline {
55
+ margin-top: 14px;
56
+ display: flex;
57
+ gap: 12px;
58
+ flex-wrap: wrap;
59
+ }
60
+ .chip {
61
+ background: rgba(255, 255, 255, 0.1);
62
+ padding: 6px 12px;
63
+ border-radius: 999px;
64
+ font-size: 12px;
65
+ }
66
+ main {
67
+ flex: 1;
68
+ display: grid;
69
+ grid-template-columns: 320px 1fr;
70
+ gap: 20px;
71
+ padding: 20px 24px 32px;
72
+ }
73
+ .panel {
74
+ background: #fff;
75
+ border-radius: 16px;
76
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
77
+ padding: 18px;
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 14px;
81
+ }
82
+ .panel-title {
83
+ font-weight: 700;
84
+ font-size: 15px;
85
+ display: flex;
86
+ justify-content: space-between;
87
+ align-items: center;
88
+ }
89
+ .panel-subtitle {
90
+ font-size: 12px;
91
+ color: #6b7280;
92
+ }
93
+ .input-group label {
94
+ font-size: 12px;
95
+ color: #374151;
96
+ margin-bottom: 6px;
97
+ display: block;
98
+ }
99
+ .input-group input,
100
+ .input-group textarea,
101
+ .input-group select {
102
+ width: 100%;
103
+ padding: 10px 12px;
104
+ border-radius: 10px;
105
+ border: 1px solid #e5e7eb;
106
+ font-size: 13px;
107
+ background: #f9fafb;
108
+ outline: none;
109
+ }
110
+ .input-group textarea {
111
+ min-height: 120px;
112
+ resize: vertical;
113
+ }
114
+ .primary-btn {
115
+ background: #f97316;
116
+ color: #fff;
117
+ border: none;
118
+ border-radius: 10px;
119
+ padding: 10px 14px;
120
+ font-weight: 600;
121
+ cursor: pointer;
122
+ }
123
+ .primary-btn:disabled {
124
+ opacity: 0.6;
125
+ cursor: not-allowed;
126
+ }
127
+ .secondary-btn {
128
+ background: #111827;
129
+ color: #fff;
130
+ border: none;
131
+ border-radius: 10px;
132
+ padding: 9px 12px;
133
+ font-weight: 600;
134
+ cursor: pointer;
135
+ }
136
+ .ghost-btn {
137
+ background: #fff;
138
+ color: #111827;
139
+ border: 1px solid #e5e7eb;
140
+ border-radius: 10px;
141
+ padding: 9px 12px;
142
+ font-weight: 600;
143
+ cursor: pointer;
144
+ }
145
+ .overview-grid {
146
+ display: grid;
147
+ grid-template-columns: repeat(2, 1fr);
148
+ gap: 10px;
149
+ }
150
+ .overview-card {
151
+ background: #f8fafc;
152
+ border-radius: 12px;
153
+ padding: 10px 12px;
154
+ border: 1px solid #e5e7eb;
155
+ }
156
+ .overview-title {
157
+ font-size: 11px;
158
+ color: #6b7280;
159
+ }
160
+ .overview-value {
161
+ font-size: 16px;
162
+ font-weight: 700;
163
+ margin-top: 4px;
164
+ }
165
+ .template-list {
166
+ display: grid;
167
+ gap: 8px;
168
+ }
169
+ .template-item {
170
+ border: 1px solid #e5e7eb;
171
+ border-radius: 12px;
172
+ padding: 10px 12px;
173
+ background: #fff;
174
+ }
175
+ .template-item h5 {
176
+ margin: 0 0 4px;
177
+ font-size: 13px;
178
+ }
179
+ .template-item p {
180
+ margin: 0;
181
+ font-size: 12px;
182
+ color: #6b7280;
183
+ }
184
+ .badge {
185
+ display: inline-flex;
186
+ align-items: center;
187
+ gap: 4px;
188
+ padding: 4px 10px;
189
+ border-radius: 999px;
190
+ font-size: 11px;
191
+ background: #fef3c7;
192
+ color: #92400e;
193
+ }
194
+ .metrics {
195
+ display: grid;
196
+ gap: 10px;
197
+ }
198
+ .metric-card {
199
+ background: #f8fafc;
200
+ border-radius: 12px;
201
+ padding: 12px;
202
+ border: 1px solid #e5e7eb;
203
+ }
204
+ .metric-label {
205
+ font-size: 12px;
206
+ color: #6b7280;
207
+ }
208
+ .metric-value {
209
+ font-size: 18px;
210
+ font-weight: 700;
211
+ margin-top: 4px;
212
+ }
213
+ .tabs {
214
+ display: flex;
215
+ gap: 10px;
216
+ }
217
+ .tab-btn {
218
+ padding: 8px 14px;
219
+ border-radius: 999px;
220
+ border: 1px solid #e5e7eb;
221
+ background: #fff;
222
+ font-size: 13px;
223
+ cursor: pointer;
224
+ }
225
+ .tab-btn.active {
226
+ background: #111827;
227
+ color: #fff;
228
+ border-color: #111827;
229
+ }
230
+ .list {
231
+ display: grid;
232
+ gap: 10px;
233
+ }
234
+ .card {
235
+ border-radius: 14px;
236
+ border: 1px solid #e5e7eb;
237
+ padding: 12px 14px;
238
+ background: #fff;
239
+ }
240
+ .card h4 {
241
+ margin: 0 0 6px;
242
+ font-size: 14px;
243
+ }
244
+ .card p {
245
+ margin: 0;
246
+ font-size: 12px;
247
+ color: #6b7280;
248
+ }
249
+ .step-tag {
250
+ font-size: 11px;
251
+ padding: 2px 8px;
252
+ border-radius: 999px;
253
+ background: #fff7ed;
254
+ color: #c2410c;
255
+ border: 1px solid #fed7aa;
256
+ }
257
+ .step-head {
258
+ display: flex;
259
+ justify-content: space-between;
260
+ align-items: center;
261
+ margin-bottom: 8px;
262
+ }
263
+ .markdown-body h1 { font-size: 1.3em; }
264
+ .markdown-body h2 { font-size: 1.15em; margin-top: 1em; }
265
+ .markdown-body h3 { font-size: 1.05em; margin-top: 0.8em; }
266
+ .markdown-body ul { padding-left: 1.2em; }
267
+ .markdown-body code { background: #f3f4f6; padding: 2px 4px; border-radius: 4px; }
268
+ .chat-box {
269
+ height: 360px;
270
+ overflow-y: auto;
271
+ border: 1px solid #e5e7eb;
272
+ border-radius: 14px;
273
+ padding: 12px;
274
+ background: #f9fafb;
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 10px;
278
+ }
279
+ .chat-item {
280
+ max-width: 80%;
281
+ padding: 10px 12px;
282
+ border-radius: 12px;
283
+ font-size: 13px;
284
+ }
285
+ .chat-user {
286
+ align-self: flex-end;
287
+ background: #111827;
288
+ color: #fff;
289
+ }
290
+ .chat-assistant {
291
+ align-self: flex-start;
292
+ background: #fff;
293
+ border: 1px solid #e5e7eb;
294
+ }
295
+ @media (max-width: 960px) {
296
+ main {
297
+ grid-template-columns: 1fr;
298
+ }
299
+ }
300
+ </style>
301
+ </head>
302
+ <body>
303
+ {% raw %}
304
+ <div id="app" class="app-shell" v-cloak>
305
+ <header>
306
+ <div class="brand">
307
+ <div class="brand-mark">FF</div>
308
+ <div>
309
+ <div class="brand-title">Factory Flow Agent</div>
310
+ <div class="brand-subtitle">产线闭环运营智能体 · 决策 / 工具 / 记忆 / 验收 / 复盘</div>
311
+ </div>
312
+ </div>
313
+ <div class="tagline">
314
+ <div class="chip">制造业生产力闭环</div>
315
+ <div class="chip">资产沉淀 + 可回放</div>
316
+ <div class="chip">移动端友好</div>
317
+ </div>
318
+ </header>
319
+
320
+ <main>
321
+ <section class="panel">
322
+ <div>
323
+ <div class="panel-title">闭环任务输入</div>
324
+ <div class="panel-subtitle">描述真实产线问题,自动生成推理到验收的完整闭环</div>
325
+ </div>
326
+ <div class="overview-grid">
327
+ <div class="overview-card">
328
+ <div class="overview-title">历史会话</div>
329
+ <div class="overview-value">{{ overview.session_count || 0 }}</div>
330
+ </div>
331
+ <div class="overview-card">
332
+ <div class="overview-title">沉淀资产</div>
333
+ <div class="overview-value">{{ overview.asset_count || 0 }}</div>
334
+ </div>
335
+ <div class="overview-card">
336
+ <div class="overview-title">对话条数</div>
337
+ <div class="overview-value">{{ overview.chat_count || 0 }}</div>
338
+ </div>
339
+ <div class="overview-card">
340
+ <div class="overview-title">最新风险级别</div>
341
+ <div class="overview-value">{{ insight.risk_level || "待分析" }}</div>
342
+ </div>
343
+ </div>
344
+ <div class="template-list">
345
+ <div class="template-item" v-for="tpl in templates" :key="tpl.title">
346
+ <h5>{{ tpl.title }}</h5>
347
+ <p>{{ tpl.scenario.slice(0, 36) }}...</p>
348
+ <div style="margin-top:8px; display:flex; gap:8px;">
349
+ <button class="ghost-btn" @click="applyTemplate(tpl)">一键填充</button>
350
+ <button class="secondary-btn" @click="applyTemplate(tpl); runWorkflow()">直接运行</button>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ <div class="input-group">
355
+ <label>会话名称</label>
356
+ <input v-model="form.name" placeholder="例如:涂布段产能优化闭环" />
357
+ </div>
358
+ <div class="input-group">
359
+ <label>场景描述</label>
360
+ <textarea v-model="form.scenario" placeholder="请输入产线现状、目标、约束与时间要求"></textarea>
361
+ </div>
362
+ <div class="input-group">
363
+ <label>补充提示</label>
364
+ <input v-model="form.hint" placeholder="例如:优先降低能耗、保证交付节奏" />
365
+ </div>
366
+ <div style="display:flex; gap:10px; flex-wrap:wrap;">
367
+ <button class="primary-btn" @click="runWorkflow" :disabled="loading">生成闭环方案</button>
368
+ <button class="ghost-btn" @click="refreshOverview">刷新看板</button>
369
+ </div>
370
+
371
+ <div class="panel-title">关键指标仪表</div>
372
+ <div class="metrics">
373
+ <div class="metric-card" v-for="(value, key) in metricCards" :key="key">
374
+ <div class="metric-label">{{ value.label }}</div>
375
+ <div class="metric-value">{{ value.value }}</div>
376
+ </div>
377
+ </div>
378
+ </section>
379
+
380
+ <section class="panel">
381
+ <div class="panel-title">
382
+ <span>产线闭环工作台</span>
383
+ <div class="tabs">
384
+ <button class="tab-btn" :class="{active: currentTab==='loop'}" @click="currentTab='loop'">决策闭环</button>
385
+ <button class="tab-btn" :class="{active: currentTab==='assets'}" @click="currentTab='assets'">资产库</button>
386
+ <button class="tab-btn" :class="{active: currentTab==='chat'}" @click="currentTab='chat'">对话分析</button>
387
+ </div>
388
+ </div>
389
+
390
+ <div v-if="currentTab === 'loop'" class="list">
391
+ <div class="card">
392
+ <h4>闭环洞察与验收</h4>
393
+ <div class="badge">风险级别:{{ insight.risk_level || "待分析" }}</div>
394
+ <div style="margin-top:10px;" class="panel-subtitle">ROI 预测</div>
395
+ <div class="markdown-body" v-html="renderMarkdown(insight.roi_hint || '等待生成闭环方案后更新')"></div>
396
+ <div class="panel-subtitle">关键短板</div>
397
+ <ul v-if="insight.gaps && insight.gaps.length" class="markdown-body">
398
+ <li v-for="gap in insight.gaps" :key="gap">{{ gap }}</li>
399
+ </ul>
400
+ <div v-else class="panel-subtitle">暂无风险洞察</div>
401
+ <div class="panel-subtitle" style="margin-top:8px;">验收结果:{{ acceptance.verdict || "待验收" }} · 得分 {{ acceptance.acceptance_score || "-" }}</div>
402
+ <div style="margin-top:10px; display:flex; gap:8px; flex-wrap:wrap;">
403
+ <button class="ghost-btn" @click="snapshotAsset">一键沉淀为资产</button>
404
+ <button class="secondary-btn" @click="currentTab='chat'">进入对话分析</button>
405
+ </div>
406
+ </div>
407
+ <div class="card">
408
+ <h4>会话记录</h4>
409
+ <div class="list">
410
+ <div class="card" v-for="session in sessions" :key="session.id" @click="loadSession(session.id)" style="cursor:pointer;">
411
+ <h4>{{ session.name }}</h4>
412
+ <p>{{ session.created_at }}</p>
413
+ <p>{{ session.scenario.slice(0, 60) }}...</p>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <div class="card">
418
+ <h4>闭环步骤回放</h4>
419
+ <div class="list">
420
+ <div class="card" v-for="step in steps" :key="step.step_order">
421
+ <div class="step-head">
422
+ <div>{{ step.role }}</div>
423
+ <span class="step-tag">{{ stepLabels[step.step_type] || step.step_type }}</span>
424
+ </div>
425
+ <div v-if="step.step_type === 'tool_action'">
426
+ <div class="markdown-body" v-html="renderTool(step.content)"></div>
427
+ </div>
428
+ <div v-else class="markdown-body" v-html="renderMarkdown(step.content)"></div>
429
+ </div>
430
+ <div v-if="steps.length === 0" class="panel-subtitle">选择左侧会话查看闭环步骤</div>
431
+ </div>
432
+ </div>
433
+ </div>
434
+
435
+ <div v-if="currentTab === 'assets'" class="list">
436
+ <div class="card">
437
+ <h4>沉淀资产</h4>
438
+ <div class="input-group">
439
+ <label>资产标题</label>
440
+ <input v-model="assetForm.title" placeholder="例如:瓶颈诊断清单" />
441
+ </div>
442
+ <div class="input-group">
443
+ <label>资产分类</label>
444
+ <input v-model="assetForm.category" placeholder="例如:质量验收" />
445
+ </div>
446
+ <div class="input-group">
447
+ <label>资产内容</label>
448
+ <textarea v-model="assetForm.content" placeholder="请填写可复用的策略、模板或指标"></textarea>
449
+ </div>
450
+ <button class="primary-btn" @click="createAsset">保存资产</button>
451
+ </div>
452
+ <div class="card">
453
+ <h4>资产列表</h4>
454
+ <div class="list">
455
+ <div class="card" v-for="asset in assets" :key="asset.id">
456
+ <h4>{{ asset.title }}</h4>
457
+ <p>{{ asset.category }} · {{ asset.created_at }}</p>
458
+ <div class="markdown-body" v-html="renderMarkdown(asset.content)"></div>
459
+ </div>
460
+ </div>
461
+ </div>
462
+ </div>
463
+
464
+ <div v-if="currentTab === 'chat'" class="list">
465
+ <div class="card">
466
+ <h4>产线分析对话</h4>
467
+ <div class="chat-box">
468
+ <div v-for="msg in chatMessages" :key="msg.created_at + msg.role" class="chat-item" :class="msg.role === 'user' ? 'chat-user' : 'chat-assistant'">
469
+ <div v-if="msg.role === 'assistant'" v-html="renderMarkdown(msg.content)"></div>
470
+ <div v-else>{{ msg.content }}</div>
471
+ </div>
472
+ </div>
473
+ <div class="input-group" style="margin-top:10px;">
474
+ <label>输入问题</label>
475
+ <input v-model="chatInput" @keyup.enter="sendChat" placeholder="例如:如何降低涂布段停机频率?" />
476
+ </div>
477
+ <button class="primary-btn" @click="sendChat" :disabled="chatLoading">发送</button>
478
+ </div>
479
+ </div>
480
+ </section>
481
+ </main>
482
+ </div>
483
+ {% endraw %}
484
+
485
+ <script>
486
+ const { createApp, ref, reactive, onMounted } = Vue;
487
+
488
+ createApp({
489
+ setup() {
490
+ const form = reactive({ name: "", scenario: "", hint: "" });
491
+ const sessions = ref([]);
492
+ const steps = ref([]);
493
+ const assets = ref([]);
494
+ const metricCards = ref({});
495
+ const loading = ref(false);
496
+ const currentTab = ref("loop");
497
+ const assetForm = reactive({ title: "", category: "", content: "" });
498
+ const chatMessages = ref([]);
499
+ const chatInput = ref("");
500
+ const chatLoading = ref(false);
501
+ const templates = ref([]);
502
+ const overview = ref({});
503
+ const insight = ref({});
504
+ const acceptance = ref({});
505
+
506
+ const stepLabels = {
507
+ reasoning: "推理决策",
508
+ plan: "执行路线图",
509
+ tool_action: "工具行动",
510
+ state_memory: "状态记忆",
511
+ verification: "校验验收",
512
+ iteration: "迭代复盘"
513
+ };
514
+
515
+ const renderMarkdown = (text) => marked.parse(text || "");
516
+
517
+ const renderTool = (text) => {
518
+ try {
519
+ const data = JSON.parse(text || "{}");
520
+ return marked.parse(
521
+ `**瓶颈工序**:${data.bottleneck}\n\n` +
522
+ `- 需求指数:${data.demand_index}\n` +
523
+ `- 产能利用率:${data.capacity_utilization}%\n` +
524
+ `- OEE:${data.oee}%\n` +
525
+ `- 缺陷风险:${data.defect_risk}\n` +
526
+ `- 能耗成本指数:${data.energy_cost_index}\n` +
527
+ `- 成本下降预估:${data.cost_reduction}%\n` +
528
+ `- 产能提升预估:${data.throughput_gain}%\n` +
529
+ `- 能耗节省预估:${data.energy_saving}%\n` +
530
+ `- 库存周转提升:${data.inventory_turnover}%\n` +
531
+ `- 交付评分:${data.sla_score}\n` +
532
+ `- 综合风险:${data.risk_score}`
533
+ );
534
+ } catch (e) {
535
+ return renderMarkdown(text);
536
+ }
537
+ };
538
+
539
+ const buildMetricCards = (parsed) => ({
540
+ demand: { label: "需求指数", value: parsed.demand_index ?? "-" },
541
+ bottleneck: { label: "瓶颈工序", value: parsed.bottleneck ?? "-" },
542
+ utilization: { label: "产能利用率", value: parsed.capacity_utilization ? `${parsed.capacity_utilization}%` : "-" },
543
+ oee: { label: "OEE", value: parsed.oee ? `${parsed.oee}%` : "-" },
544
+ defect: { label: "缺陷风险", value: parsed.defect_risk ?? "-" },
545
+ energy: { label: "能耗成本", value: parsed.energy_cost_index ?? "-" },
546
+ cost: { label: "成本下降", value: parsed.cost_reduction ? `${parsed.cost_reduction}%` : "-" },
547
+ gain: { label: "产能提升", value: parsed.throughput_gain ? `${parsed.throughput_gain}%` : "-" }
548
+ });
549
+
550
+ const buildInsightFromTool = (parsed) => {
551
+ const score = parsed.risk_score ?? 0;
552
+ let level = "低";
553
+ if (score > 70) level = "高";
554
+ else if (score > 35) level = "中";
555
+ const gaps = [];
556
+ if ((parsed.defect_risk ?? 0) >= 60) gaps.push("质量波动偏高");
557
+ if ((parsed.capacity_utilization ?? 100) < 75) gaps.push("产能利用率偏低");
558
+ if ((parsed.energy_cost_index ?? 0) > 0.75) gaps.push("能耗成本偏高");
559
+ if (!gaps.length) gaps.push("暂无明显短板");
560
+ return {
561
+ risk_level: level,
562
+ gaps,
563
+ roi_hint: `预计成本下降 ${parsed.cost_reduction ?? "-"}%,产能提升 ${parsed.throughput_gain ?? "-"}%。`
564
+ };
565
+ };
566
+
567
+ const applyTemplate = (tpl) => {
568
+ form.name = tpl.title;
569
+ form.scenario = tpl.scenario;
570
+ form.hint = tpl.hint;
571
+ };
572
+
573
+ const snapshotAsset = () => {
574
+ const memory = steps.value.find((s) => s.step_type === "state_memory");
575
+ if (memory) {
576
+ assetForm.title = "闭环状态快照资产";
577
+ assetForm.category = "闭环资产";
578
+ assetForm.content = memory.content;
579
+ currentTab.value = "assets";
580
+ }
581
+ };
582
+
583
+ const refreshOverview = async () => {
584
+ const res = await fetch("/api/overview");
585
+ const data = await res.json();
586
+ overview.value = data || {};
587
+ if (data && data.latest_tool) {
588
+ metricCards.value = buildMetricCards(data.latest_tool);
589
+ insight.value = buildInsightFromTool(data.latest_tool);
590
+ }
591
+ };
592
+
593
+ const fetchTemplates = async () => {
594
+ const res = await fetch("/api/templates");
595
+ const data = await res.json();
596
+ templates.value = data.templates || [];
597
+ };
598
+
599
+ const fetchSessions = async () => {
600
+ const res = await fetch("/api/sessions");
601
+ const data = await res.json();
602
+ sessions.value = data.sessions || [];
603
+ };
604
+
605
+ const loadSession = async (id) => {
606
+ const res = await fetch(`/api/sessions/${id}`);
607
+ const data = await res.json();
608
+ steps.value = data.steps || [];
609
+ const toolStep = steps.value.find((s) => s.step_type === "tool_action");
610
+ if (toolStep) {
611
+ try {
612
+ const parsed = JSON.parse(toolStep.content);
613
+ metricCards.value = buildMetricCards(parsed);
614
+ insight.value = buildInsightFromTool(parsed);
615
+ } catch (e) {
616
+ metricCards.value = {};
617
+ }
618
+ }
619
+ const verifyStep = steps.value.find((s) => s.step_type === "verification");
620
+ if (verifyStep) {
621
+ const scoreMatch = verifyStep.content.match(/综合得分:([0-9.]+)/);
622
+ const verdictMatch = verifyStep.content.match(/验收结论:([^\n]+)/);
623
+ acceptance.value = {
624
+ acceptance_score: scoreMatch ? scoreMatch[1] : "-",
625
+ verdict: verdictMatch ? verdictMatch[1] : "待验收"
626
+ };
627
+ }
628
+ };
629
+
630
+ const runWorkflow = async () => {
631
+ if (!form.scenario.trim()) return;
632
+ loading.value = true;
633
+ const res = await fetch("/api/run", {
634
+ method: "POST",
635
+ headers: { "Content-Type": "application/json" },
636
+ body: JSON.stringify(form)
637
+ });
638
+ loading.value = false;
639
+ if (res.ok) {
640
+ const data = await res.json();
641
+ await fetchSessions();
642
+ await loadSession(data.session_id);
643
+ insight.value = data.insight || insight.value;
644
+ acceptance.value = data.acceptance || acceptance.value;
645
+ if (data.tool_data) {
646
+ metricCards.value = buildMetricCards(data.tool_data);
647
+ }
648
+ form.name = "";
649
+ form.scenario = "";
650
+ form.hint = "";
651
+ }
652
+ };
653
+
654
+ const fetchAssets = async () => {
655
+ const res = await fetch("/api/assets");
656
+ const data = await res.json();
657
+ assets.value = data.assets || [];
658
+ };
659
+
660
+ const createAsset = async () => {
661
+ const payload = { ...assetForm };
662
+ if (!payload.title || !payload.category || !payload.content) return;
663
+ const res = await fetch("/api/assets", {
664
+ method: "POST",
665
+ headers: { "Content-Type": "application/json" },
666
+ body: JSON.stringify(payload)
667
+ });
668
+ if (res.ok) {
669
+ assetForm.title = "";
670
+ assetForm.category = "";
671
+ assetForm.content = "";
672
+ await fetchAssets();
673
+ }
674
+ };
675
+
676
+ const fetchChats = async () => {
677
+ const res = await fetch("/api/chats");
678
+ const data = await res.json();
679
+ chatMessages.value = data.messages || [];
680
+ };
681
+
682
+ const sendChat = async () => {
683
+ if (!chatInput.value.trim()) return;
684
+ chatLoading.value = true;
685
+ const message = chatInput.value;
686
+ chatInput.value = "";
687
+ const res = await fetch("/api/chat", {
688
+ method: "POST",
689
+ headers: { "Content-Type": "application/json" },
690
+ body: JSON.stringify({ message })
691
+ });
692
+ chatLoading.value = false;
693
+ if (res.ok) {
694
+ await fetchChats();
695
+ }
696
+ };
697
+
698
+ onMounted(async () => {
699
+ await fetchSessions();
700
+ await fetchAssets();
701
+ await fetchChats();
702
+ await fetchTemplates();
703
+ await refreshOverview();
704
+ if (sessions.value.length > 0) {
705
+ await loadSession(sessions.value[0].id);
706
+ }
707
+ });
708
+
709
+ return {
710
+ form,
711
+ sessions,
712
+ steps,
713
+ assets,
714
+ metricCards,
715
+ loading,
716
+ currentTab,
717
+ assetForm,
718
+ chatMessages,
719
+ chatInput,
720
+ chatLoading,
721
+ templates,
722
+ overview,
723
+ insight,
724
+ acceptance,
725
+ stepLabels,
726
+ renderMarkdown,
727
+ renderTool,
728
+ runWorkflow,
729
+ loadSession,
730
+ createAsset,
731
+ sendChat,
732
+ applyTemplate,
733
+ refreshOverview,
734
+ snapshotAsset
735
+ };
736
+ }
737
+ }).mount("#app");
738
+ </script>
739
+ </body>
740
+ </html>