feat: 品牌升级(知冶→战知) + 应用工具广场重构 + 新增工具集成
品牌升级: - 全站品牌从"知冶"更名为"战知" - 更换 favicon、侧边栏 logo、登录页 logo - 更新登录页标语和首页欢迎语 应用广场重构: - 从后端数据库驱动改为前端静态配置,按分类 tab 展示 - 新增工具卡片 UI,支持 logo 图片和 emoji 图标 新增工具部署: - Stirling PDF (端口18080) - PDF 处理工具箱 - Excalidraw (端口18081) - 手绘风格白板,集成 AI 绘图 - TrWebOCR (端口18083) - 中文离线 OCR - LibreTranslate (端口18084) - 中英翻译引擎 - PPTist (端口18085) - 在线 PPT 编辑器 - PPTist AI 后端 (端口18086) - 对接 deepseek-v3 生成大纲/PPT/写作 - Excalidraw AI 代理 (端口18082) - 对接 deepseek-v3 生成 Mermaid 图 其他: - 智能场景仅保留"选题推荐" - vite 代理配置增加 /pdf/ 和 /draw/ 路由 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
scripts/excalidraw-ai-proxy.py
Normal file
98
scripts/excalidraw-ai-proxy.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Excalidraw AI 代理服务
|
||||
接收 Excalidraw 的 text-to-diagram 请求,转发给 deepseek-v3 生成 Mermaid 语法
|
||||
"""
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import requests
|
||||
import json
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
LLM_API = "http://10.102.24.75:3000/v1/chat/completions"
|
||||
LLM_KEY = "sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb"
|
||||
|
||||
SYSTEM_PROMPT = """You are an expert at creating Mermaid diagrams.
|
||||
When the user describes a diagram, workflow, flowchart, or similar,
|
||||
generate ONLY valid Mermaid syntax. Do not include any explanation,
|
||||
just the Mermaid code. Do not wrap it in markdown code blocks.
|
||||
Always start with a diagram type declaration like: graph TD, flowchart LR, sequenceDiagram, classDiagram, etc."""
|
||||
|
||||
|
||||
@app.post("/v1/ai/text-to-diagram/generate")
|
||||
async def text_to_diagram(request: Request):
|
||||
body = await request.json()
|
||||
print(f"[AI] Received request: {json.dumps(body, ensure_ascii=False)[:500]}")
|
||||
prompt = body.get("prompt", body.get("text", body.get("message", "")))
|
||||
|
||||
try:
|
||||
resp = requests.post(LLM_API, json={
|
||||
"model": "deepseek-v3",
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 2048
|
||||
}, headers={"Authorization": f"Bearer {LLM_KEY}"}, timeout=60)
|
||||
|
||||
result = resp.json()
|
||||
print(f"[AI] LLM response status: {resp.status_code}")
|
||||
mermaid_code = result["choices"][0]["message"]["content"].strip()
|
||||
|
||||
# 清理可能的 markdown 包裹
|
||||
if mermaid_code.startswith("```"):
|
||||
mermaid_code = mermaid_code.split("\n", 1)[1] if "\n" in mermaid_code else mermaid_code[3:]
|
||||
if mermaid_code.endswith("```"):
|
||||
mermaid_code = mermaid_code[:-3].strip()
|
||||
|
||||
print(f"[AI] Mermaid output: {mermaid_code[:200]}")
|
||||
return {"generatedResponse": mermaid_code}
|
||||
except Exception as e:
|
||||
print(f"[AI] Error: {e}")
|
||||
return {"generatedResponse": f"graph TD\n A[Error: {str(e)[:50]}]"}
|
||||
|
||||
|
||||
@app.post("/v1/ai/diagram-to-code/generate")
|
||||
async def diagram_to_code(request: Request):
|
||||
body = await request.json()
|
||||
texts = body.get("texts", [])
|
||||
theme = body.get("theme", "light")
|
||||
print(f"[AI] diagram-to-code request, texts: {texts[:5]}, theme: {theme}")
|
||||
|
||||
prompt = f"Based on these UI element labels: {', '.join(texts) if texts else 'empty wireframe'}. Theme: {theme}. Generate a complete, beautiful HTML page with inline CSS that represents this wireframe layout. Only output the HTML code, nothing else."
|
||||
|
||||
try:
|
||||
resp = requests.post(LLM_API, json={
|
||||
"model": "deepseek-v3",
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are an expert web developer. Generate complete HTML with inline CSS based on wireframe descriptions. Output ONLY the HTML code, no explanation, no markdown code blocks."},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 4096
|
||||
}, headers={"Authorization": f"Bearer {LLM_KEY}"}, timeout=60)
|
||||
|
||||
result = resp.json()
|
||||
html_code = result["choices"][0]["message"]["content"].strip()
|
||||
if html_code.startswith("```"):
|
||||
html_code = html_code.split("\n", 1)[1] if "\n" in html_code else html_code[3:]
|
||||
if html_code.endswith("```"):
|
||||
html_code = html_code[:-3].strip()
|
||||
print(f"[AI] HTML output length: {len(html_code)}")
|
||||
return html_code
|
||||
except Exception as e:
|
||||
print(f"[AI] diagram-to-code error: {e}")
|
||||
return f"<html><body><h1>Generation Error</h1><p>{str(e)[:100]}</p></body></html>"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=18082)
|
||||
239
scripts/pptist-ai-backend.py
Normal file
239
scripts/pptist-ai-backend.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
PPTist AI 后端服务
|
||||
接收 PPTist 的 AIPPT 请求,调用 deepseek-v3 生成大纲和PPT内容
|
||||
端口: 18086
|
||||
"""
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
import httpx
|
||||
import json
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
LLM_API = "http://10.102.24.75:3000/v1/chat/completions"
|
||||
LLM_KEY = "sk-BlQIGRrotbVDWE5mXCPBFjVWIvJ83hldzz67xInNwzVo7pPb"
|
||||
LLM_MODEL = "deepseek-v3"
|
||||
|
||||
OUTLINE_SYSTEM_PROMPT = """你是一个专业的PPT大纲生成助手。用户会给你一个主题,你需要生成一份内容丰富、结构清晰的PPT大纲。
|
||||
|
||||
要求:
|
||||
1. 使用Markdown格式,用 # 表示演示文稿标题,## 表示章节标题,### 表示该章节下的内容页标题
|
||||
2. 必须包含6个主要章节(##)
|
||||
3. 每个章节下必须有2-3个内容页(###)
|
||||
4. 每个内容页标题要具体、有信息量,不要太笼统
|
||||
5. 内容要专业、逻辑清晰、层层递进
|
||||
6. 直接输出Markdown内容,不要有多余的解释
|
||||
|
||||
示例格式:
|
||||
# 人工智能技术发展
|
||||
## 人工智能概述
|
||||
### 定义与发展历史
|
||||
### 核心技术与分支
|
||||
## 机器学习基础
|
||||
### 监督学习与无监督学习
|
||||
### 深度学习与神经网络
|
||||
### 常用算法对比
|
||||
..."""
|
||||
|
||||
AIPPT_SYSTEM_PROMPT = """你是一个专业的PPT内容生成助手。用户会给你一份PPT大纲,你需要根据大纲生成完整、丰富、详实的PPT内容。
|
||||
|
||||
你必须严格按照以下JSON数组格式输出,不要输出任何其他内容。
|
||||
|
||||
结构规则:
|
||||
1. 第一页:cover类型,包含演示标题和副标题描述
|
||||
2. 第二页:contents类型,列出所有章节名称
|
||||
3. 每个章节:先一个transition过渡页,然后每个###子内容对应一个content页
|
||||
4. 每个content页必须包含4个items,每个item有title和text
|
||||
5. text内容必须详实具体,每条20-40字,包含实质性的数据、观点或分析,不要空泛
|
||||
6. 最后一页:end类型
|
||||
|
||||
输出格式(JSON数组,每个元素是一页slide):
|
||||
[
|
||||
{"type": "cover", "data": {"title": "演示标题", "text": "一句话描述演示主题和价值"}},
|
||||
{"type": "contents", "data": {"items": ["章节1名称", "章节2名称", "章节3名称", "章节4名称", "章节5名称", "章节6名称"]}},
|
||||
{"type": "transition", "data": {"title": "章节标题", "text": "本章将介绍什么内容,一句话概述"}},
|
||||
{"type": "content", "data": {"title": "内容页标题", "items": [
|
||||
{"title": "要点1标题", "text": "要点1的详细说明,包含具体信息、数据或分析"},
|
||||
{"title": "要点2标题", "text": "要点2的详细说明"},
|
||||
{"title": "要点3标题", "text": "要点3的详细说明"},
|
||||
{"title": "要点4标题", "text": "要点4的详细说明"}
|
||||
]}},
|
||||
{"type": "end"}
|
||||
]
|
||||
|
||||
重要:
|
||||
- 生成的总页数应在20-25页之间
|
||||
- 只输出JSON数组,不要markdown代码块,不要任何解释
|
||||
- 确保JSON格式完全正确,可被直接解析"""
|
||||
|
||||
|
||||
async def stream_llm(messages, temperature=0.7, max_tokens=4096):
|
||||
"""流式调用LLM并逐块返回文本"""
|
||||
async with httpx.AsyncClient(timeout=120) as client:
|
||||
async with client.stream(
|
||||
"POST",
|
||||
LLM_API,
|
||||
json={
|
||||
"model": LLM_MODEL,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": True,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {LLM_KEY}"},
|
||||
) as resp:
|
||||
async for line in resp.aiter_lines():
|
||||
if line.startswith("data: "):
|
||||
data = line[6:]
|
||||
if data.strip() == "[DONE]":
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk["choices"][0].get("delta", {})
|
||||
content = delta.get("content", "")
|
||||
if content:
|
||||
yield content
|
||||
except (json.JSONDecodeError, KeyError, IndexError):
|
||||
continue
|
||||
|
||||
|
||||
@app.post("/tools/aippt_outline")
|
||||
async def aippt_outline(request: Request):
|
||||
data = await request.json()
|
||||
content = data.get("content", "")
|
||||
language = data.get("language", "中文")
|
||||
print(f"[AIPPT] Outline request: {content}, language: {language}")
|
||||
|
||||
user_prompt = f"请用{language}为以下主题生成PPT大纲:\n\n{content}"
|
||||
|
||||
async def generate():
|
||||
async for text in stream_llm(
|
||||
[
|
||||
{"role": "system", "content": OUTLINE_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=2048,
|
||||
):
|
||||
yield text
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
|
||||
|
||||
@app.post("/tools/aippt")
|
||||
async def aippt(request: Request):
|
||||
data = await request.json()
|
||||
content = data.get("content", "")
|
||||
language = data.get("language", "中文")
|
||||
style = data.get("style", "通用")
|
||||
print(f"[AIPPT] Generate request, style: {style}, language: {language}")
|
||||
|
||||
user_prompt = f"请根据以下大纲生成PPT内容,风格为「{style}」,语言为{language}:\n\n{content}"
|
||||
|
||||
async def generate():
|
||||
"""流式收集LLM输出,按JSON对象级别缓冲,每检测到一个完整对象就立即发送"""
|
||||
buf = ""
|
||||
brace_depth = 0
|
||||
in_string = False
|
||||
escape_next = False
|
||||
found_array = False # 是否已经跳过了数组开头的 [
|
||||
|
||||
try:
|
||||
async for chunk in stream_llm(
|
||||
[
|
||||
{"role": "system", "content": AIPPT_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=8192,
|
||||
):
|
||||
for ch in chunk:
|
||||
# 跳过数组开头的 [ 和结尾的 ] 以及逗号分隔符
|
||||
if not found_array:
|
||||
if ch == '[':
|
||||
found_array = True
|
||||
continue
|
||||
|
||||
if brace_depth == 0 and ch in (' ', '\n', '\r', '\t', ',', ']'):
|
||||
continue
|
||||
|
||||
buf += ch
|
||||
|
||||
if escape_next:
|
||||
escape_next = False
|
||||
continue
|
||||
if ch == '\\' and in_string:
|
||||
escape_next = True
|
||||
continue
|
||||
if ch == '"' and not escape_next:
|
||||
in_string = not in_string
|
||||
continue
|
||||
if in_string:
|
||||
continue
|
||||
|
||||
if ch == '{':
|
||||
brace_depth += 1
|
||||
elif ch == '}':
|
||||
brace_depth -= 1
|
||||
if brace_depth == 0:
|
||||
# 得到一个完整的JSON对象
|
||||
try:
|
||||
obj = json.loads(buf)
|
||||
yield json.dumps(obj, ensure_ascii=False) + "\n"
|
||||
print(f"[AIPPT] Sent slide: {obj.get('type', '?')}")
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[AIPPT] JSON parse error: {e}, buf: {buf[:100]}")
|
||||
buf = ""
|
||||
|
||||
except Exception as e:
|
||||
print(f"[AIPPT] Error: {e}")
|
||||
yield json.dumps({"type": "cover", "data": {"title": "生成失败", "text": str(e)[:100]}}) + "\n"
|
||||
yield json.dumps({"type": "end"}) + "\n"
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
|
||||
|
||||
WRITING_COMMANDS = {
|
||||
"rewrite": "请改写以下文本,保持原意但使用不同的表达方式:",
|
||||
"expand": "请扩写以下文本,补充更多细节和内容:",
|
||||
"abbreviate": "请缩写以下文本,保留核心要点,使其更简洁:",
|
||||
"polish": "请润色以下文本,使其更流畅、专业:",
|
||||
"translate": "请将以下文本翻译为英文:",
|
||||
}
|
||||
|
||||
|
||||
@app.post("/tools/ai_writing")
|
||||
async def ai_writing(request: Request):
|
||||
data = await request.json()
|
||||
content = data.get("content", "")
|
||||
command = data.get("command", "rewrite")
|
||||
print(f"[AIPPT] Writing request, command: {command}")
|
||||
|
||||
instruction = WRITING_COMMANDS.get(command, WRITING_COMMANDS["rewrite"])
|
||||
user_prompt = f"{instruction}\n\n{content}"
|
||||
|
||||
async def generate():
|
||||
async for text in stream_llm(
|
||||
[
|
||||
{"role": "system", "content": "你是一个专业的文字助手,帮助用户改写、扩写、缩写、润色或翻译文本。直接输出结果,不要有额外解释。"},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=2048,
|
||||
):
|
||||
yield text
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=18086)
|
||||
41
scripts/pptist-deploy/Dockerfile
Normal file
41
scripts/pptist-deploy/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
# PPTist 构建 & 部署
|
||||
FROM docker.1ms.run/node:20 AS builder
|
||||
ENV http_proxy=http://219.234.197.247:53128
|
||||
ENV https_proxy=http://219.234.197.247:53128
|
||||
ENV no_proxy=localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,172.17.0.0/16
|
||||
WORKDIR /app
|
||||
COPY PPTist/package*.json ./
|
||||
RUN npm config set registry https://registry.npmmirror.com && npm install
|
||||
COPY PPTist/ .
|
||||
|
||||
# 修改 SERVER_URL: 生产环境使用相对路径 /pptapi,由 nginx 反代到 AI 后端
|
||||
RUN sed -i "s|'https://server.pptist.cn'|'/pptapi'|g" src/services/index.ts
|
||||
|
||||
# 修改模型选项为 deepseek-v3
|
||||
RUN sed -i "s/glm-4\.7-flash/deepseek-v3/g" src/views/Editor/AIPPTDialog.vue && \
|
||||
sed -i "s/GLM-4\.7-Flash/DeepSeek-V3/g" src/views/Editor/AIPPTDialog.vue && \
|
||||
sed -i "s/doubao-seed-1\.6-flash/deepseek-v3/g" src/views/Editor/AIPPTDialog.vue && \
|
||||
sed -i "s/Doubao-Seed-1\.6-Flash/DeepSeek-V3/g" src/views/Editor/AIPPTDialog.vue && \
|
||||
sed -i "s/GLM-4\.5-Flash/DeepSeek-V3/g" src/services/index.ts
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM docker.1ms.run/nginx:alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
RUN echo 'server { \
|
||||
listen 80; \
|
||||
root /usr/share/nginx/html; \
|
||||
index index.html; \
|
||||
location / { \
|
||||
try_files $uri $uri/ /index.html; \
|
||||
} \
|
||||
location /pptapi/ { \
|
||||
proxy_pass http://172.17.0.1:18086/; \
|
||||
proxy_http_version 1.1; \
|
||||
proxy_set_header Connection ""; \
|
||||
proxy_buffering off; \
|
||||
proxy_cache off; \
|
||||
} \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
41
scripts/pptist-deploy/deploy-all-tools.sh
Normal file
41
scripts/pptist-deploy/deploy-all-tools.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# 一键部署三个工具: TrWebOCR, LibreTranslate, PPTist + AI后端
|
||||
set -e
|
||||
|
||||
echo "========== 1. 部署 TrWebOCR (端口 18083) =========="
|
||||
docker rm -f trwebocr 2>/dev/null || true
|
||||
docker run -d --name trwebocr --restart always -p 18083:8089 mmmz/trwebocr:latest
|
||||
echo "TrWebOCR 启动完成"
|
||||
|
||||
echo "========== 2. 部署 LibreTranslate (端口 18084) =========="
|
||||
docker rm -f libretranslate 2>/dev/null || true
|
||||
docker run -d --name libretranslate --restart always \
|
||||
-p 18084:5000 \
|
||||
-e LT_LOAD_ONLY=en,zh \
|
||||
-v lt-data:/home/libretranslate/.local \
|
||||
libretranslate/libretranslate
|
||||
echo "LibreTranslate 启动完成(语言模型下载中,约5分钟后可用)"
|
||||
|
||||
echo "========== 3. 启动 PPTist AI 后端 (端口 18086) =========="
|
||||
# 先安装依赖
|
||||
pip install fastapi uvicorn httpx 2>/dev/null || pip3 install fastapi uvicorn httpx
|
||||
# 杀掉旧进程
|
||||
pkill -f "pptist-ai-backend" 2>/dev/null || true
|
||||
sleep 1
|
||||
# 用 screen 启动后台运行
|
||||
screen -dmS pptist-ai python3 /root/gangyan/scripts/pptist-ai-backend.py
|
||||
echo "PPTist AI 后端启动完成"
|
||||
|
||||
echo "========== 4. 构建并部署 PPTist (端口 18085) =========="
|
||||
cd /root/gangyan/scripts/pptist-deploy
|
||||
docker rm -f pptist 2>/dev/null || true
|
||||
docker build -t pptist:latest .
|
||||
docker run -d --name pptist --restart always -p 18085:80 pptist:latest
|
||||
echo "PPTist 启动完成"
|
||||
|
||||
echo ""
|
||||
echo "========== 部署完成 =========="
|
||||
echo "TrWebOCR: http://localhost:18083"
|
||||
echo "LibreTranslate: http://localhost:18084"
|
||||
echo "PPTist: http://localhost:18085"
|
||||
echo "PPTist AI后端: http://localhost:18086"
|
||||
Reference in New Issue
Block a user