AI 生成: 一个基于 Flask 的 Web 应用,用户输入主题后由 AI 生成结构化的登记手册,包含封面、目录、正文与附录,并支持导出为 Markdown 文件。
This commit is contained in:
@@ -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` 文件
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
"""AI 登记手册生成器包。"""
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.3
|
||||
requests==2.32.3
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user