AI 生成: 一个基于 Flask 的 Web 应用,用户输入主题后由 AI 生成结构化的登记手册,包含封面、目录、正文与附录,并支持导出为 Markdown 文件。

This commit is contained in:
admin
2026-09-30 20:41:38 +08:00
commit 1750ff3a83
9 changed files with 498 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# AI 生成登记手册
一个基于 Flask 的 Web 应用,输入主题即可由 AI 生成结构化的登记手册。
## 功能
- 输入主题,生成完整登记手册(封面、目录、正文、附录)
- 支持在线预览与一键导出 Markdown
- 内置离线模板引擎(未配置 API Key 时使用)
- 可选接入 OpenAI 兼容接口
## 目录结构
```
ai-registration-handbook/
├── app.py
├── requirements.txt
├── README.md
├── templates/
│ └── index.html
├── static/
│ ├── style.css
│ └── app.js
└── handbook/
├── __init__.py
├── generator.py
└── templates.py
```
## 安装
```bash
pip install -r requirements.txt
```
## 运行
```bash
python app.py
```
浏览器访问 http://127.0.0.1:5000
## 配置 AI(可选)
设置环境变量后启用真实 AI 生成:
```bash
export OPENAI_API_KEY=your-key
export OPENAI_BASE_URL=https://api.openai.com/v1
export OPENAI_MODEL=gpt-4o-mini
```
未配置时使用内置模板引擎生成示例手册。
## 接口
- `POST /api/generate` 请求体 `{"topic": "..."}` 返回 `{"markdown": "..."}`
- `POST /api/export` 请求体 `{"topic": "...", "markdown": "..."}` 下载 `.md` 文件
+53
View File
@@ -0,0 +1,53 @@
import io
import os
from flask import Flask, jsonify, render_template, request, send_file
from handbook.generator import generate_handbook
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/generate", methods=["POST"])
def api_generate():
data = request.get_json(silent=True) or {}
topic = (data.get("topic") or "").strip()
if not topic:
return jsonify({"error": "主题不能为空"}), 400
try:
markdown = generate_handbook(topic)
except Exception as exc: # noqa: BLE001
return jsonify({"error": f"生成失败: {exc}"}), 500
return jsonify({"topic": topic, "markdown": markdown})
@app.route("/api/export", methods=["POST"])
def api_export():
data = request.get_json(silent=True) or {}
topic = (data.get("topic") or "handbook").strip() or "handbook"
markdown = data.get("markdown") or ""
if not markdown:
return jsonify({"error": "内容不能为空"}), 400
buffer = io.BytesIO(markdown.encode("utf-8"))
buffer.seek(0)
safe_name = "".join(c for c in topic if c.isalnum() or c in "-_ ").strip() or "handbook"
return send_file(
buffer,
mimetype="text/markdown",
as_attachment=True,
download_name=f"{safe_name}-登记手册.md",
)
if __name__ == "__main__":
port = int(os.environ.get("PORT", "5000"))
app.run(host="0.0.0.0", port=port, debug=True)
+1
View File
@@ -0,0 +1 @@
"""AI 登记手册生成器包。"""
+53
View File
@@ -0,0 +1,53 @@
"""手册生成入口:优先调用 AI,失败则回退到离线模板。"""
import os
import requests
from handbook.templates import render_offline_handbook
SYSTEM_PROMPT = (
"你是一名专业的文档撰写助手,擅长编写结构清晰、条理分明的登记手册。"
"请使用 Markdown 输出,包含封面信息、目录、正文(总则、登记范围、登记流程、"
"登记信息字段、附则)以及附录。语言简洁、专业、可执行。"
)
def _call_openai(topic: str) -> str:
api_key = os.environ.get("OPENAI_API_KEY")
base_url = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1").rstrip("/")
model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
if not api_key:
raise RuntimeError("未配置 OPENAI_API_KEY")
resp = requests.post(
f"{base_url}/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": f"请为主题「{topic}」生成一份完整的登记手册。",
},
],
"temperature": 0.7,
},
timeout=60,
)
resp.raise_for_status()
payload = resp.json()
return payload["choices"][0]["message"]["content"].strip()
def generate_handbook(topic: str) -> str:
"""生成登记手册 Markdown 文本。"""
try:
return _call_openai(topic)
except Exception: # noqa: BLE001
return render_offline_handbook(topic)
+82
View File
@@ -0,0 +1,82 @@
"""内置离线模板,用于未配置 AI 时的示例生成。"""
from datetime import date
def render_offline_handbook(topic: str) -> str:
"""根据主题生成结构化的登记手册 Markdown 文本。"""
today = date.today().isoformat()
sections = [
(
"一、总则",
[
f"本手册用于规范「{topic}」相关事项的登记流程,确保信息完整、准确、可追溯。",
"所有登记人员应按照本手册要求填写相关字段,不得遗漏或伪造信息。",
"本手册的解释权归登记管理部门所有。",
],
),
(
"二、登记范围",
[
f"凡涉及「{topic}」的新增、变更、注销等事项,均应进行登记。",
"登记对象包括但不限于:基本信息、责任主体、时间节点、附件材料。",
],
),
(
"三、登记流程",
[
"1. 申请人填写登记表并提交相关证明材料。",
"2. 登记人员核对材料完整性,必要时要求补充。",
"3. 审核通过后录入系统,生成唯一登记编号。",
"4. 登记结果通知申请人,并归档留存。",
],
),
(
"四、登记信息字段",
[
"| 字段 | 说明 | 是否必填 |",
"| --- | --- | --- |",
"| 登记编号 | 系统自动生成 | 是 |",
f"| 主题 | 与「{topic}」相关的事项名称 | 是 |",
"| 申请人 | 提交登记的主体 | 是 |",
"| 登记日期 | 登记完成日期 | 是 |",
"| 备注 | 补充说明 | 否 |",
],
),
(
"五、附则",
[
"本手册自发布之日起施行。",
"如遇政策调整,以最新规定为准。",
],
),
]
lines = [
f"# {topic} 登记手册",
"",
f"**生成日期:** {today}",
"",
"## 目录",
"",
]
for title, _ in sections:
lines.append(f"- {title}")
lines.append("")
for title, paragraphs in sections:
lines.append(f"## {title}")
lines.append("")
for p in paragraphs:
lines.append(p)
lines.append("")
lines.append("## 附录")
lines.append("")
lines.append("- 登记表模板")
lines.append("- 常见问题解答")
lines.append("- 联系方式")
lines.append("")
return "\n".join(lines)
+2
View File
@@ -0,0 +1,2 @@
Flask==3.0.3
requests==2.32.3
+84
View File
@@ -0,0 +1,84 @@
const topicInput = document.getElementById("topic");
const generateBtn = document.getElementById("generate-btn");
const exportBtn = document.getElementById("export-btn");
const statusEl = document.getElementById("status");
const outputEl = document.getElementById("output");
let current = { topic: "", markdown: "" };
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.classList.toggle("error", isError);
}
async function generate() {
const topic = topicInput.value.trim();
if (!topic) {
setStatus("请先输入主题", true);
return;
}
generateBtn.disabled = true;
exportBtn.disabled = true;
setStatus("生成中,请稍候…");
outputEl.textContent = "正在生成…";
try {
const resp = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ topic }),
});
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.error || "生成失败");
}
current = { topic: data.topic, markdown: data.markdown };
outputEl.textContent = data.markdown;
exportBtn.disabled = false;
setStatus("生成完成");
} catch (err) {
outputEl.textContent = "生成失败";
setStatus(err.message, true);
} finally {
generateBtn.disabled = false;
}
}
async function exportMarkdown() {
if (!current.markdown) return;
try {
const resp = await fetch("/api/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(current),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data.error || "导出失败");
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${current.topic}-登记手册.md`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setStatus("已开始下载");
} catch (err) {
setStatus(err.message, true);
}
}
generateBtn.addEventListener("click", generate);
exportBtn.addEventListener("click", exportMarkdown);
topicInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") generate();
});
+128
View File
@@ -0,0 +1,128 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 32px 20px 64px;
}
.header h1 {
margin: 0 0 8px;
font-size: 28px;
}
.subtitle {
margin: 0 0 24px;
color: #6b7280;
}
.panel {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.panel label {
display: block;
font-weight: 600;
margin-bottom: 8px;
}
.row {
display: flex;
gap: 12px;
}
input[type="text"] {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 15px;
outline: none;
}
input[type="text"]:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
background: #2563eb;
color: #ffffff;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
button:hover:not(:disabled) {
background: #1d4ed8;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
button.secondary {
background: #ffffff;
color: #2563eb;
border: 1px solid #2563eb;
}
button.secondary:hover:not(:disabled) {
background: #eff6ff;
}
.status {
margin: 12px 0 0;
min-height: 20px;
color: #6b7280;
font-size: 14px;
}
.status.error {
color: #dc2626;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.panel-head h2 {
margin: 0;
font-size: 18px;
}
.output {
margin: 0;
padding: 16px;
background: #0f172a;
color: #e2e8f0;
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-height: 520px;
overflow: auto;
}
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI 生成登记手册</title>
<link rel="stylesheet" href="static/style.css" />
</head>
<body>
<main class="container">
<header class="header">
<h1>AI 生成登记手册</h1>
<p class="subtitle">输入主题,一键生成结构化的登记手册</p>
</header>
<section class="panel">
<label for="topic">手册主题</label>
<div class="row">
<input id="topic" type="text" placeholder="例如:设备借用登记" />
<button id="generate-btn">生成手册</button>
</div>
<p id="status" class="status"></p>
</section>
<section class="panel">
<div class="panel-head">
<h2>预览</h2>
<button id="export-btn" class="secondary" disabled>导出 Markdown</button>
</div>
<pre id="output" class="output">生成的手册将显示在这里…</pre>
</section>
</main>
<script src="static/app.js"></script>
</body>
</html>