图片上传识别功能

This commit is contained in:
renjianbo
2026-04-13 22:52:36 +08:00
parent df4fab1e6e
commit 63b54116a5
13 changed files with 708 additions and 17 deletions

View File

@@ -0,0 +1,80 @@
# 启动注意事项
本文面向在 **Windows** 上本地运行本仓库(后端 API、Celery、前端、Redis时的常见配置与排错要点。
---
## 1. Redis 端口与 `.env` 必须一致
- `backend/.env` 中的 **`REDIS_URL`**(例如 `redis://localhost:6380/0`)必须与**实际监听的 Redis 端口**一致。
- 仓库内 `start_aiagent.ps1` 默认启动的是 **`6379`**。若 `.env` 写的是 **6380**,请要么:
- 在本机启动监听 **6380** 的 Redis要么
-`.env` 改为 **6379** 并使用脚本启动的 Redis。
- **症状**:创建执行返回 **503**、日志中出现无法连接 Redis / Celery 入队失败。
---
## 2. API 与 Celery 必须使用同一虚拟环境
- 工作流/Agent 执行由 **Celery Worker** 跑,与 **uvicorn API** 应共用 **`backend\venv`**。
- 更新依赖后请在 **`backend` 目录**执行:
```powershell
.\venv\Scripts\pip install -r requirements.txt
```
- **改完 `.env` 或 Python 依赖后**,需要**重启 API 和 Celery**,否则仍加载旧环境。
- 仓库提供**仅重启后端 + Celery**(不停止前端/本机 Redis的脚本
```powershell
powershell -ExecutionPolicy Bypass -File D:\aaa\aiagent\restart_backend_celery.ps1
```
---
## 3. 图片 OCR作业/聊天里识别图中文字)
- `file_read` 读图片依赖:**Pillow**、**pytesseract**,以及本机安装 **Tesseract 可执行文件**。
- 在 `backend/.env` 中建议配置(路径按本机修改):
- **`TESSERACT_CMD`**:指向 `tesseract.exe`(例如 `C:/Program Files/Tesseract-OCR/tesseract.exe`)。
- **`TESSERACT_TESSDATA_DIR`**(可选):指向含 **`chi_sim.traineddata`** 的目录,中文识别更稳定。
- 自检:
```powershell
cd D:\aaa\aiagent\backend
.\venv\Scripts\python scripts\check_ocr_env.py
```
- **症状**:助手回复里出现「请安装 Pillow / pytesseract」或无法识别图中文字 → 先检查 venv 是否已 `pip install`,再检查 Tesseract 与 `.env`,最后**重启 Celery**。
---
## 4. 前端与后端地址
- 前端开发服通过代理访问 API一键脚本里会通过环境变量指向当前 API 地址。
- 若 OpenAPI 里**没有**新加的路由(例如上传),多半是 **API 进程仍是旧代码/旧进程**,需要重启后端。
---
## 5. 鉴权与安全
- 上传、执行等接口需要 **JWT**;预览对话若出现 **401**,请重新登录后再试。
- **勿**在文档或 Git 中提交 **明文密码、密钥、完整 `.env`**`.env` 应留在本机并加入版本忽略。
---
## 6. 一键启停脚本(参考)
| 脚本 | 作用 |
|------|------|
| `start_aiagent.ps1` | 启动 Redis(6379)、API、Celery、前端按脚本内端口逻辑 |
| `stop_aiagent.ps1` | 停止 API、Celery、前端、以及匹配的 redis-server 进程 |
| `restart_backend_celery.ps1` | 仅重启 API(8037) + Celery适合改依赖或 `.env` 后快速生效 |
实际端口以你本机 **`start_aiagent.ps1` / `restart_backend_celery.ps1`** 及 `.env` 为准。
---
## 7. 修改记录建议
- 变更依赖或环境变量后:在本机记一句「已重启 API + Celery」便于以后排查。

View File

@@ -76,6 +76,7 @@ class PreviewChatTurnResponse(BaseModel):
created_at: datetime
user_text: str
agent_text: str
attachments: List[Dict[str, Any]] = Field(default_factory=list)
class Config:
from_attributes = True

View File

@@ -6,15 +6,17 @@ from __future__ import annotations
import re
import uuid
import logging
import mimetypes
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Query
from fastapi.responses import FileResponse
from pydantic import BaseModel
from app.api.auth import get_current_user
from app.core.config import settings
from app.models.user import User
from app.services.builtin_tools import _local_file_workspace_root
from app.services.builtin_tools import _local_file_workspace_root, _resolve_path_under_workspace
logger = logging.getLogger(__name__)
@@ -110,3 +112,32 @@ async def upload_preview_file(
size=total,
content_type=file.content_type,
)
@router.get("/preview/file")
async def get_preview_file(
file_path: str = Query(..., description="上传后返回的 relative_path"),
current_user: User = Depends(get_current_user),
):
"""
读取当前用户预览附件(用于前端历史回显图片缩略图)。
仅允许访问 uploads/preview/<current_user.id>/ 下文件。
"""
path, err = _resolve_path_under_workspace(file_path)
if err or path is None:
raise HTTPException(status_code=400, detail=f"无效文件路径: {err or file_path}")
if not path.is_file():
raise HTTPException(status_code=404, detail="文件不存在")
root = _local_file_workspace_root()
try:
rel = path.relative_to(root).as_posix()
except ValueError:
raise HTTPException(status_code=403, detail="不允许访问该文件")
prefix = f"uploads/preview/{current_user.id}/"
if not rel.startswith(prefix):
raise HTTPException(status_code=403, detail="无权访问该文件")
media_type, _ = mimetypes.guess_type(str(path))
return FileResponse(path=str(path), media_type=media_type or "application/octet-stream")

View File

@@ -97,6 +97,31 @@ def _user_id_from_input(input_data: Optional[Dict[str, Any]]) -> Optional[str]:
return None
def _extract_attachments(input_data: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
if not input_data:
return []
raw = input_data.get("attachments")
if not isinstance(raw, list):
return []
out: List[Dict[str, Any]] = []
for item in raw:
if not isinstance(item, dict):
continue
rel = str(item.get("relative_path") or "").strip()
name = str(item.get("filename") or "").strip()
if not rel:
continue
content_type = str(item.get("content_type") or "").strip() or None
out.append(
{
"relative_path": rel,
"filename": name or rel.rsplit("/", 1)[-1],
"content_type": content_type,
}
)
return out
def fetch_agent_preview_chat_turns(
db: Session,
agent_id: str,
@@ -149,6 +174,7 @@ def fetch_agent_preview_chat_turns(
"created_at": ex.created_at,
"user_text": user_text or "(无文本)",
"agent_text": agent_text or "(无输出)",
"attachments": _extract_attachments(inp),
}
)
return out

View File

@@ -0,0 +1,58 @@
"""检查图片 OCR 环境Pillow、pytesseract、Tesseract 可执行文件、chi_sim 语言包。
在 backend 目录执行:
.\\venv\\Scripts\\python scripts\\check_ocr_env.py
"""
from __future__ import annotations
import sys
from pathlib import Path
# 保证能加载 app 配置
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from app.core.config import settings # noqa: E402
from app.services.builtin_tools import _local_file_workspace_root, _tessdata_dir_for_ocr # noqa: E402
def main() -> int:
print("TESSERACT_CMD (settings):", settings.TESSERACT_CMD or "(空,将尝试 PATH)")
print("TESSERACT_TESSDATA_DIR (settings):", settings.TESSERACT_TESSDATA_DIR or "(空,将尝试仓库 tessdata/)")
try:
import PIL # noqa: F401
print("Pillow: OK")
except ImportError as e:
print("Pillow: 缺失 —", e)
print(" 请执行: pip install Pillow")
return 2
try:
import pytesseract as pt
print("pytesseract: OK")
except ImportError as e:
print("pytesseract: 缺失 —", e)
print(" 请执行: pip install pytesseract")
return 3
cmd = (settings.TESSERACT_CMD or "").strip()
if cmd:
pt.pytesseract.tesseract_cmd = cmd
try:
ver = pt.get_tesseract_version()
print("Tesseract 版本:", ver)
except Exception as e:
print("Tesseract 可执行文件: 不可用 —", e)
print(" Windows 请安装 Tesseract并在 .env 设置 TESSERACT_CMD=.../tesseract.exe")
return 4
td = _tessdata_dir_for_ocr()
print("解析到的 tessdata 目录:", td or "(未找到)")
root = _local_file_workspace_root()
loc = root / "tessdata"
if loc.is_dir():
has_chi = any(loc.glob("chi_sim.traineddata"))
print("仓库 tessdata/chi_sim.traineddata:", "存在" if has_chi else "缺失(中文识别差)")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -48,6 +48,28 @@
/>
<div class="message-content">
<div class="message-bubble" v-html="formatMessage(message.content)"></div>
<div
v-if="message.attachments?.length"
:class="['message-attachments', message.role === 'user' ? 'is-user' : 'is-agent']"
>
<div
v-for="(a, ai) in message.attachments"
:key="`msg-att-${index}-${ai}`"
class="message-attachment-item"
>
<img
v-if="a.isImage && a.thumbUrl"
:src="a.thumbUrl"
:alt="a.filename"
class="message-attachment-image"
@click="openImagePreview(a)"
/>
<div v-else class="message-attachment-file">
<el-icon :size="14"><Document /></el-icon>
<span class="message-attachment-name">{{ a.filename }}</span>
</div>
</div>
</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<el-avatar
@@ -104,8 +126,31 @@
</el-alert>
</div>
</div>
<div class="chat-input-area">
<el-dialog
v-model="imagePreviewVisible"
title="图片预览"
width="min(86vw, 980px)"
append-to-body
destroy-on-close
>
<div class="image-preview-dialog-body">
<img
v-if="imagePreviewSrc"
:src="imagePreviewSrc"
:alt="imagePreviewTitle || '图片预览'"
class="image-preview-dialog-img"
/>
</div>
</el-dialog>
<div
class="chat-input-area"
:class="{ 'is-drag-over': inputDragOver }"
@dragenter.prevent="onInputDragEnter"
@dragleave.prevent="onInputDragLeave"
@dragover.prevent="onInputDragOver"
@drop.prevent="onInputDrop"
>
<input
ref="fileInputRef"
type="file"
@@ -160,7 +205,7 @@
v-model="inputMessage"
type="textarea"
:rows="2"
placeholder="发送消息..."
placeholder="发送消息…(可将图片拖入此区域)"
@keydown.enter.exact.prevent="handleSendMessage"
@keydown.enter.shift.exact="handleNewLine"
:disabled="loading || !agentId"
@@ -206,12 +251,14 @@ interface Message {
role: 'user' | 'agent'
content: string
timestamp: number
attachments?: MessageAttachment[]
}
interface PendingAttachment {
relative_path: string
filename: string
size?: number
content_type?: string
/** 图片:本地 blob URL用于缩略图移除或发送后 revoke */
thumbUrl?: string
/** 浏览器本地读取的纯文本预览(发送后也会写入气泡) */
@@ -220,6 +267,14 @@ interface PendingAttachment {
previewNote?: string
}
interface MessageAttachment {
relative_path: string
filename: string
thumbUrl?: string
isImage?: boolean
content_type?: string
}
const props = defineProps<{
agentId?: string
agentName?: string
@@ -244,6 +299,11 @@ const fileInputRef = ref<HTMLInputElement | null>(null)
const pendingAttachments = ref<PendingAttachment[]>([])
/** 已发送、待轮询结束后再 revoke 的附件(含 thumbUrl */
const blobsPendingRevokeAfterRun = ref<PendingAttachment[] | null>(null)
/** 拖放图片到输入区时高亮 */
const inputDragOver = ref(false)
const imagePreviewVisible = ref(false)
const imagePreviewSrc = ref('')
const imagePreviewTitle = ref('')
let pollingInterval: any = null
let replyAdded = false // 标志位:防止重复添加回复
@@ -275,7 +335,17 @@ function readDesignChatFromStorage(agentId: string): Message[] {
.map((m: any) => ({
role: m.role,
content: m.content,
timestamp: m.timestamp
timestamp: m.timestamp,
attachments: Array.isArray(m.attachments)
? m.attachments
.filter((a: any) => a && typeof a.relative_path === 'string' && typeof a.filename === 'string')
.map((a: any) => ({
relative_path: String(a.relative_path),
filename: String(a.filename),
isImage: !!a.isImage,
content_type: a.content_type ? String(a.content_type) : undefined
}))
: undefined
}))
} catch {
return []
@@ -285,7 +355,18 @@ function readDesignChatFromStorage(agentId: string): Message[] {
function writeDesignChatToStorage(agentId: string, list: Message[]) {
try {
const slice = list.slice(-MAX_PERSIST_MESSAGES)
let raw = JSON.stringify(slice)
const compact = slice.map((m) => ({
role: m.role,
content: m.content,
timestamp: m.timestamp,
attachments: m.attachments?.map((a) => ({
relative_path: a.relative_path,
filename: a.filename,
isImage: !!a.isImage,
content_type: a.content_type
}))
}))
let raw = JSON.stringify(compact)
if (raw.length > 450_000) {
raw = JSON.stringify(slice.slice(-80))
}
@@ -363,6 +444,7 @@ async function loadChatHistory() {
const cached = readDesignChatFromStorage(aid)
if (cached.length > 0) {
messages.value = cached
await hydrateMessageAttachmentThumbs(messages.value)
nextTick(() => scrollToBottom())
}
@@ -372,15 +454,29 @@ async function loadChatHistory() {
agent_text: string
created_at: string
execution_id: string
attachments?: Array<{
relative_path: string
filename: string
content_type?: string
}>
}>
) => {
const next: Message[] = []
for (const t of turns) {
const base = new Date(t.created_at).getTime()
const msgAttachments: MessageAttachment[] = (t.attachments || []).map((a) => ({
relative_path: a.relative_path,
filename: a.filename,
content_type: a.content_type,
isImage:
!!a.content_type?.startsWith('image/') ||
/\.(png|jpe?g|gif|webp|bmp|tiff?)$/i.test(a.filename || a.relative_path)
}))
next.push({
role: 'user',
content: t.user_text || ' ',
timestamp: Number.isFinite(base) ? base - 400 : Date.now() - 400
timestamp: Number.isFinite(base) ? base - 400 : Date.now() - 400,
attachments: msgAttachments.length ? msgAttachments : undefined
})
next.push({
role: 'agent',
@@ -396,6 +492,11 @@ async function loadChatHistory() {
agent_text: string
created_at: string
execution_id: string
attachments?: Array<{
relative_path: string
filename: string
content_type?: string
}>
}
const fetchHist = async (previewUserId: string | undefined): Promise<HistTurn[]> => {
@@ -439,7 +540,9 @@ async function loadChatHistory() {
if (Array.isArray(turns) && turns.length > 0) {
const next = applyServerTurns(turns)
revokeMessageAttachmentBlobs(messages.value)
messages.value = next
await hydrateMessageAttachmentThumbs(messages.value)
writeDesignChatToStorage(aid, next)
}
// 服务端无记录时保留上面已展示的本地镜像(若有)
@@ -476,6 +579,55 @@ function revokeAttachmentBlobs(items: PendingAttachment[]) {
}
}
function revokeMessageAttachmentBlobs(items: Message[]) {
for (const m of items) {
for (const a of m.attachments || []) {
if (a.thumbUrl) {
try {
URL.revokeObjectURL(a.thumbUrl)
} catch {
/* ignore */
}
}
}
}
}
function isThumbInMessages(url: string): boolean {
if (!url) return false
return messages.value.some((m) => (m.attachments || []).some((a) => a.thumbUrl === url))
}
async function fetchAttachmentThumb(relativePath: string): Promise<string | undefined> {
try {
const res = await api.get('/api/v1/uploads/preview/file', {
params: { file_path: relativePath },
responseType: 'blob',
skipErrorHandler: true
})
return URL.createObjectURL(res.data as Blob)
} catch {
return undefined
}
}
async function hydrateMessageAttachmentThumbs(target: Message[]) {
for (const m of target) {
for (const a of m.attachments || []) {
if (!a.isImage || a.thumbUrl || !a.relative_path) continue
const url = await fetchAttachmentThumb(a.relative_path)
if (url) a.thumbUrl = url
}
}
}
function openImagePreview(a: MessageAttachment) {
if (!a.thumbUrl) return
imagePreviewSrc.value = a.thumbUrl
imagePreviewTitle.value = a.filename
imagePreviewVisible.value = true
}
function removeAttachment(index: number) {
const cur = pendingAttachments.value[index]
if (cur?.thumbUrl) {
@@ -552,18 +704,15 @@ function readLocalTextPreview(file: File): Promise<string | null> {
})
}
async function onFileInputChange(ev: Event) {
const input = ev.target as HTMLInputElement
const files = input.files
input.value = ''
if (!files?.length) return
/** 与回形针选择、拖放共用:上传到预览区并加入待发送列表 */
async function uploadPreviewFiles(fileList: File[]) {
if (!fileList.length) return
if (!props.agentId) {
ElMessage.warning('请先选中要预览的智能体后再上传附件')
return
}
const uploadedNames: string[] = []
for (let i = 0; i < files.length; i++) {
const file = files[i]
for (const file of fileList) {
const fd = new FormData()
fd.append('file', file)
try {
@@ -582,6 +731,7 @@ async function onFileInputChange(ev: Event) {
relative_path: d.relative_path,
filename: d.filename || file.name,
size: d.size,
content_type: (res.data as any)?.content_type,
thumbUrl,
previewText,
previewNote: previewText
@@ -614,6 +764,56 @@ async function onFileInputChange(ev: Event) {
}
}
async function onFileInputChange(ev: Event) {
const input = ev.target as HTMLInputElement
const files = input.files
input.value = ''
if (!files?.length) return
await uploadPreviewFiles(Array.from(files))
}
function onInputDragEnter(e: DragEvent) {
if (!props.agentId || loading.value) return
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
inputDragOver.value = true
}
function onInputDragLeave(e: DragEvent) {
e.preventDefault()
const el = e.currentTarget as HTMLElement
const rel = e.relatedTarget as Node | null
if (rel && el.contains(rel)) return
inputDragOver.value = false
}
function onInputDragOver(e: DragEvent) {
if (!props.agentId || loading.value) return
e.preventDefault()
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'
}
async function onInputDrop(e: DragEvent) {
e.preventDefault()
inputDragOver.value = false
if (!props.agentId || loading.value) {
if (!props.agentId) ElMessage.warning('请先选中要预览的智能体后再拖入图片')
return
}
const dt = e.dataTransfer
if (!dt?.files?.length) return
const list = Array.from(dt.files)
const images = list.filter((f) => isImageFile(f))
if (images.length === 0) {
ElMessage.warning('请拖入图片文件(如 png、jpg、jpeg、webp、gif 等)')
return
}
if (images.length < list.length) {
ElMessage.info('已忽略非图片文件,仅添加图片附件')
}
await uploadPreviewFiles(images)
}
// 发送消息
const handleSendMessage = async () => {
if ((!inputMessage.value.trim() && pendingAttachments.value.length === 0) || loading.value || !props.agentId)
@@ -647,10 +847,21 @@ const handleSendMessage = async () => {
}
// 添加用户消息
const userAttachments: MessageAttachment[] = attachSnap.map((a) => ({
relative_path: a.relative_path,
filename: a.filename,
thumbUrl: a.thumbUrl,
content_type: a.content_type,
isImage:
!!a.thumbUrl ||
Boolean(a.content_type?.startsWith('image/')) ||
/\.(png|jpe?g|gif|webp|bmp|tiff?)$/i.test(a.filename)
}))
messages.value.push({
role: 'user',
content: userBubble || '(附件)',
timestamp: Date.now()
timestamp: Date.now(),
attachments: userAttachments.length ? userAttachments : undefined
})
// 滚动到底部
@@ -679,7 +890,10 @@ const handleSendMessage = async () => {
const checkStatus = async () => {
const finishBlobs = () => {
if (blobsPendingRevokeAfterRun.value) {
revokeAttachmentBlobs(blobsPendingRevokeAfterRun.value)
const revokable = blobsPendingRevokeAfterRun.value.filter(
(x) => !x.thumbUrl || !isThumbInMessages(x.thumbUrl)
)
revokeAttachmentBlobs(revokable)
blobsPendingRevokeAfterRun.value = null
}
}
@@ -849,6 +1063,7 @@ const handlePresetQuestion = (question: string) => {
// 清空对话
const handleClearChat = () => {
revokeAttachmentBlobs([...pendingAttachments.value])
revokeMessageAttachmentBlobs(messages.value)
pendingAttachments.value = []
if (blobsPendingRevokeAfterRun.value) {
revokeAttachmentBlobs(blobsPendingRevokeAfterRun.value)
@@ -996,6 +1211,7 @@ onUnmounted(() => {
pollingInterval = null
}
revokeAttachmentBlobs([...pendingAttachments.value])
revokeMessageAttachmentBlobs(messages.value)
if (blobsPendingRevokeAfterRun.value) {
revokeAttachmentBlobs(blobsPendingRevokeAfterRun.value)
blobsPendingRevokeAfterRun.value = null
@@ -1096,6 +1312,76 @@ onUnmounted(() => {
padding: 0 4px;
}
.message-attachments {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
max-width: min(70%, 420px);
gap: 8px;
margin-top: 6px;
padding-bottom: 2px;
}
.message-attachments.is-user {
justify-content: flex-end;
}
.message-attachments.is-agent {
justify-content: flex-start;
}
.message-attachment-item {
max-width: 220px;
flex: 0 0 auto;
}
.message-attachment-image {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #e4e7ed;
background: #fff;
display: block;
cursor: zoom-in;
}
.message-attachment-file {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border: 1px solid #e4e7ed;
border-radius: 8px;
font-size: 12px;
background: #fff;
color: #606266;
max-width: 220px;
}
.message-attachment-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-preview-dialog-body {
display: flex;
justify-content: center;
align-items: center;
min-height: 240px;
}
.image-preview-dialog-img {
max-width: 100%;
max-height: 72vh;
object-fit: contain;
border-radius: 10px;
border: 1px solid #ebeef5;
background: #fff;
}
.preset-questions {
display: flex;
flex-direction: column;
@@ -1167,6 +1453,13 @@ onUnmounted(() => {
background: white;
border-top: 1px solid #e4e7ed;
padding: 12px;
border-radius: 0;
transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
.chat-input-area.is-drag-over {
background: #ecf5ff;
box-shadow: inset 0 0 0 2px #409eff;
}
.input-toolbar {

View File

@@ -0,0 +1,56 @@
# Restart backend API (uvicorn) and Celery worker only; does not stop frontend/Redis.$ErrorActionPreference = "Continue"
$RepoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$Backend = Join-Path $RepoRoot "backend"
$ApiPort = 8037
Write-Host "== Stop API + Celery only ==" -ForegroundColor Cyan
function Stop-ByCommandLine([string]$pattern, [string]$name) {
$targets = Get-CimInstance Win32_Process | Where-Object {
$_.CommandLine -and $_.CommandLine -match $pattern
}
if (-not $targets) {
Write-Host "[SKIP] ${name}: no matching process" -ForegroundColor DarkGray
return
}
foreach ($p in $targets) {
try {
Stop-Process -Id $p.ProcessId -Force
Write-Host "[OK] stopped ${name} PID=$($p.ProcessId)" -ForegroundColor Green
} catch {
Write-Host "[WARN] failed to stop ${name} PID=$($p.ProcessId)" -ForegroundColor Yellow
}
}
}
Stop-ByCommandLine "uvicorn\s+app\.main:app" "backend-api"
Stop-ByCommandLine "celery\s+-A\s+app\.core\.celery_app\s+worker" "celery-worker"
Start-Sleep -Seconds 2
Write-Host "== Start API on $ApiPort + Celery ==" -ForegroundColor Cyan
Start-Process powershell -ArgumentList @(
"-NoExit",
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command",
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m uvicorn app.main:app --host 0.0.0.0 --port $ApiPort"
)
Start-Process powershell -ArgumentList @(
"-NoExit",
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command",
"Set-Location '$Backend'; .\venv\Scripts\Activate.ps1; python -m celery -A app.core.celery_app worker --loglevel=info --pool=threads --concurrency=8"
)
Write-Host ""
Write-Host "[DONE] 已在新窗口启动 API 与 Celery (请查看弹出的 PowerShell 窗口日志)" -ForegroundColor Green
Write-Host "API: http://127.0.0.1:$ApiPort/docs" -ForegroundColor Cyan
Start-Sleep -Seconds 2
Write-Host ""
Write-Host "Port $ApiPort :" -ForegroundColor Cyan
netstat -ano | findstr ":$ApiPort"

1
三字经.md Normal file
View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,38 @@
# 《三字经》选段:从"人之初"到"不知义"
## 原文内容
人之初,性本善。
性相近,习相远。
苟不教,性乃迁。
教之道,贵以专。
昔孟母,择邻处。
子不学,断机杼。
窦燕山,有义方。
教五子,名俱扬。
养不教,父之过。
教不严,师之惰。
子不学,非所宜。
幼不学,老何为。
玉不琢,不成器。
人不学,不知义。
## 白话译文
人刚出生的时候,本性都是善良的。
天性虽然相近,但后天的习惯却相差很远。
如果不加以教育,善良的本性就会改变。
教育的方法,贵在专心致志。
从前孟子的母亲,为了选择好的邻居而三次搬家。
孟子不学习,她就割断织布机上的布来教育他。
窦燕山有好的教育方法。
他教育的五个儿子,都很有成就,名声远扬。
生养孩子却不教育,是父亲的过错。
教育学生却不严格,是老师的懒惰。
孩子不学习,是不应该的。
小时候不学习,老了能做什么呢?
玉石不经过雕琢,就不能成为精美的器物。
人不学习,就不懂得道义。
## 注释
1. **人之初,性本善**:儒家思想认为人性本善,这是孟子"性善论"的观点。
2. **性相近,习相远**:出自《论语·阳货》,孔子说:"性相近也,习相远也。"
3. **苟不教,性乃迁**:如果不教育,善良的本性就会改变。
4. **教之道,贵以专**:教育的方法,最重要的是专心致志。
5. **昔孟母,择邻处**:孟母三迁的故事,强调环境对教育的重要性。
6. **子不学,断机杼**:孟母断织

3
作业.txt Normal file
View File

@@ -0,0 +1,3 @@
语文作业1背诵第八课人之初
2写生字
3帮妈妈扫地

46
满江红.md Normal file
View File

@@ -0,0 +1,46 @@
---
title: 满江红·写怀
author: 岳飞
date: 2024-12-01
---
# 满江红·写怀
**怒发冲冠,凭栏处、潇潇雨歇。**
**抬望眼,仰天长啸,壮怀激烈。**
**三十功名尘与土,八千里路云和月。**
**莫等闲、白了少年头,空悲切!**
**靖康耻,犹未雪。**
**臣子恨,何时灭!**
**驾长车,踏破贺兰山缺。**
**壮志饥餐胡虏肉,笑谈渴饮匈奴血。**
**待从头、收拾旧山河,朝天阙。**
---
## 创作背景
《满江红·写怀》是南宋抗金名将岳飞创作的一首词。此词上片抒写作者对中原重陷敌手的悲愤,对局势前功尽弃的痛惜,表达了自己继续努力争取壮年立功的心愿;下片抒写作者对民族敌人的深仇大恨,对祖国统一的殷切愿望,对国家朝廷的赤胆忠诚。
## 词牌解析
- **词牌名**:满江红
- **字数**93字
- **韵脚**:仄韵格,气势磅礴
- **创作时间**约公元1136年绍兴六年
## 艺术特色
1. **情感激昂**:全词情感激越,气势磅礴
2. **对仗工整**"三十功名尘与土,八千里路云和月"等句对仗精妙
3. **意象鲜明**:运用"怒发冲冠"、"仰天长啸"等生动意象
4. **爱国情怀**:表达了强烈的爱国主义精神和民族气节
## 历史影响
这首词成为中华民族爱国主义精神的象征之一,激励了无数仁人志士为国家和民族奋斗。
---
*注:此版本为经典传世版本,收录于《全宋词》。*

View File

@@ -0,0 +1,58 @@
# 知你客服 17 号 · 能力说明C主动排障闭环执行
## 定位
**知你客服15号**(可持续多步工具执行)基础上,增加 **C 能力**
遇到问题时优先主动完成「**自检 → 执行 → 验证 → 补救**」闭环,而不是停留在“我去检查一下”。
## 核心增强
1. **主动自检**
- 任务开始先确认必要前提(工作区、目标路径、文件是否存在、输入是否完整)。
2. **主动执行**
- 明确调用工具推进,不只给计划语句。
3. **结果验证**
- `file_write` 后强制回读或关键字段校验,避免“说已写入但未落盘”。
4. **失败补救**
- 单步失败时至少尝试 1-2 个替代方案(路径、文件名、编码、模式)。
5. **最小化追问**
- 仅在确实缺少关键输入时才向用户追问;否则优先自助完成。
## 工具策略
- 默认本地闭环:`system_info``file_read``file_write``text_analyze``json_process`
- `http_request` 仅在用户明确要求联网或本地信息不足时使用。
- `database_query` 仅允许 `SELECT`
- 古文/常识续写(如《三字经》补全)优先直接生成并落盘,不依赖联网。
## 运行参数
- `llm-unified.max_tool_iterations = 22`(默认)。
- 工具列表与 15 号一致(全量内置工具)。
## 创建/更新脚本
```text
backend/scripts/create_zhini_kefu_17.py
```
用法:
```powershell
cd backend
.\venv\Scripts\python.exe scripts/create_zhini_kefu_17.py
```
环境变量:
- `PLATFORM_BASE_URL`(默认 `http://127.0.0.1:8037`
- `PLATFORM_USERNAME`(默认 `admin`
- `PLATFORM_PASSWORD`(默认 `123456`
- `SOURCE_AGENT_NAME`(默认 `知你客服15号`
- `TARGET_NAME`(默认 `知你客服17号`
## 验收建议
1. 让 17 号执行“创建并写入文件,再读回验证”的任务。
2. 让 17 号执行“已有文件续写并覆盖指定段落”的任务。
3. 构造轻微异常(路径不规范、文件已存在)确认其会主动补救而非直接停住。