From 63b54116a516bf53cc31874489a0c3fc2d0f2d5e Mon Sep 17 00:00:00 2001 From: renjianbo <18691577328@163.com> Date: Mon, 13 Apr 2026 22:52:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=89=87=E4=B8=8A=E4=BC=A0=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...¡å™¨å¯åŠ¨å’Œåœæ­¢.md => (红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md | 0 (红头)æœåС噍å¯åŠ¨çš„æ³¨æ„事项.md | 80 +++++ backend/app/api/agents.py | 1 + backend/app/api/uploads.py | 35 +- .../app/services/agent_workspace_chat_log.py | 26 ++ backend/scripts/check_ocr_env.py | 58 ++++ frontend/src/components/AgentChatPreview.vue | 323 +++++++++++++++++- restart_backend_celery.ps1 | 56 +++ 三字ç».md | 1 + 三字ç»é‡Œçš„人之åˆ.md | 38 +++ 作业.txt | 3 + 满江红.md | 46 +++ 知你客æœ17å·èƒ½åŠ›æ–‡æ¡£.md | 58 ++++ 13 files changed, 708 insertions(+), 17 deletions(-) rename (红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md => (红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md (100%) create mode 100644 (红头)æœåС噍å¯åŠ¨çš„æ³¨æ„事项.md create mode 100644 backend/scripts/check_ocr_env.py create mode 100644 restart_backend_celery.ps1 create mode 100644 三字ç».md create mode 100644 三字ç»é‡Œçš„人之åˆ.md create mode 100644 作业.txt create mode 100644 满江红.md create mode 100644 知你客æœ17å·èƒ½åŠ›æ–‡æ¡£.md diff --git a/(红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md b/(红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md similarity index 100% rename from (红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md rename to (红头)å‰åŽç«¯æœåС噍å¯åŠ¨å’Œåœæ­¢.md diff --git a/(红头)æœåС噍å¯åŠ¨çš„æ³¨æ„事项.md b/(红头)æœåС噍å¯åŠ¨çš„æ³¨æ„事项.md new file mode 100644 index 0000000..8786596 --- /dev/null +++ b/(红头)æœåС噍å¯åŠ¨çš„æ³¨æ„事项.md @@ -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ã€ä¾¿äºŽä»¥åŽæŽ’æŸ¥ã€‚ diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 689e434..d6c9d99 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -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 diff --git a/backend/app/api/uploads.py b/backend/app/api/uploads.py index 2636849..bf8f0e4 100644 --- a/backend/app/api/uploads.py +++ b/backend/app/api/uploads.py @@ -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// 下文件。 + """ + 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") diff --git a/backend/app/services/agent_workspace_chat_log.py b/backend/app/services/agent_workspace_chat_log.py index faab861..0c352a7 100644 --- a/backend/app/services/agent_workspace_chat_log.py +++ b/backend/app/services/agent_workspace_chat_log.py @@ -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 diff --git a/backend/scripts/check_ocr_env.py b/backend/scripts/check_ocr_env.py new file mode 100644 index 0000000..bf44fab --- /dev/null +++ b/backend/scripts/check_ocr_env.py @@ -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()) diff --git a/frontend/src/components/AgentChatPreview.vue b/frontend/src/components/AgentChatPreview.vue index 3efbe67..59ca115 100644 --- a/frontend/src/components/AgentChatPreview.vue +++ b/frontend/src/components/AgentChatPreview.vue @@ -48,6 +48,28 @@ />
+
+
+ +
+ + {{ a.filename }} +
+
+
{{ formatTime(message.timestamp) }}
- -
+ +
+ +
+
+ +
(null) const pendingAttachments = ref([]) /** å·²å‘é€ã€å¾…轮询结æŸåŽå† revoke çš„é™„ä»¶ï¼ˆå« thumbUrl) */ const blobsPendingRevokeAfterRun = ref(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 => { @@ -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 { + 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 { }) } -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 { diff --git a/restart_backend_celery.ps1 b/restart_backend_celery.ps1 new file mode 100644 index 0000000..02eeb60 --- /dev/null +++ b/restart_backend_celery.ps1 @@ -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" diff --git a/三字ç».md b/三字ç».md new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/三字ç».md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/三字ç»é‡Œçš„人之åˆ.md b/三字ç»é‡Œçš„人之åˆ.md new file mode 100644 index 0000000..fd1bc38 --- /dev/null +++ b/三字ç»é‡Œçš„人之åˆ.md @@ -0,0 +1,38 @@ +# 《三字ç»ã€‹é€‰æ®µï¼šä»Ž"人之åˆ"到"ä¸çŸ¥ä¹‰" +## 原文内容 +人之åˆï¼Œæ€§æœ¬å–„。 +性相近,习相远。 +è‹Ÿä¸æ•™ï¼Œæ€§ä¹ƒè¿ã€‚ +教之é“,贵以专。 +昔孟æ¯ï¼Œæ‹©é‚»å¤„。 +å­ä¸å­¦ï¼Œæ–­æœºæ¼ã€‚ +窦燕山,有义方。 +教五å­ï¼Œå俱扬。 +养䏿•™ï¼Œçˆ¶ä¹‹è¿‡ã€‚ +æ•™ä¸ä¸¥ï¼Œå¸ˆä¹‹æƒ°ã€‚ +å­ä¸å­¦ï¼Œéžæ‰€å®œã€‚ +å¹¼ä¸å­¦ï¼Œè€ä½•为。 +玉ä¸ç¢ï¼Œä¸æˆå™¨ã€‚ +人ä¸å­¦ï¼Œä¸çŸ¥ä¹‰ã€‚ +## 白è¯è¯‘æ–‡ +人刚出生的时候,本性都是善良的。 +天性虽然相近,但åŽå¤©çš„习惯å´ç›¸å·®å¾ˆè¿œã€‚ +如果ä¸åŠ ä»¥æ•™è‚²ï¼Œå–„è‰¯çš„æœ¬æ€§å°±ä¼šæ”¹å˜ã€‚ +教育的方法,贵在专心致志。 +从å‰å­Ÿå­çš„æ¯äº²ï¼Œä¸ºäº†é€‰æ‹©å¥½çš„é‚»å±…è€Œä¸‰æ¬¡æ¬å®¶ã€‚ +å­Ÿå­ä¸å­¦ä¹ ï¼Œå¥¹å°±å‰²æ–­ç»‡å¸ƒæœºä¸Šçš„å¸ƒæ¥æ•™è‚²ä»–。 +窦燕山有好的教育方法。 +他教育的五个儿å­ï¼Œéƒ½å¾ˆæœ‰æˆå°±ï¼Œå声远扬。 +生养孩å­å´ä¸æ•™è‚²ï¼Œæ˜¯çˆ¶äº²çš„过错。 +教育学生å´ä¸ä¸¥æ ¼ï¼Œæ˜¯è€å¸ˆçš„æ‡’惰。 +å­©å­ä¸å­¦ä¹ ï¼Œæ˜¯ä¸åº”该的。 +å°æ—¶å€™ä¸å­¦ä¹ ï¼Œè€äº†èƒ½åšä»€ä¹ˆå‘¢ï¼Ÿ +玉石ä¸ç»è¿‡é›•ç¢ï¼Œå°±ä¸èƒ½æˆä¸ºç²¾ç¾Žçš„器物。 +人ä¸å­¦ä¹ ï¼Œå°±ä¸æ‡‚å¾—é“义。 +## 注释 +1. **人之åˆï¼Œæ€§æœ¬å–„**ï¼šå„’å®¶æ€æƒ³è®¤ä¸ºäººæ€§æœ¬å–„,这是孟å­"性善论"的观点。 +2. **性相近,习相远**:出自《论语·阳货》,孔å­è¯´ï¼š"性相近也,习相远也。" +3. **è‹Ÿä¸æ•™ï¼Œæ€§ä¹ƒè¿**ï¼šå¦‚æžœä¸æ•™è‚²ï¼Œå–„良的本性就会改å˜ã€‚ +4. **教之é“,贵以专**:教育的方法,最é‡è¦çš„æ˜¯ä¸“心致志。 +5. **昔孟æ¯ï¼Œæ‹©é‚»å¤„**:孟æ¯ä¸‰è¿çš„æ•…事,强调环境对教育的é‡è¦æ€§ã€‚ +6. **å­ä¸å­¦ï¼Œæ–­æœºæ¼**ï¼šå­Ÿæ¯æ–­ç»‡ \ No newline at end of file diff --git a/作业.txt b/作业.txt new file mode 100644 index 0000000..8a5deaf --- /dev/null +++ b/作业.txt @@ -0,0 +1,3 @@ +语文作业:1èƒŒè¯µç¬¬å…«è¯¾äººä¹‹åˆ + 2写生字 + 3帮妈妈扫地 \ No newline at end of file diff --git a/满江红.md b/满江红.md new file mode 100644 index 0000000..1a2092f --- /dev/null +++ b/满江红.md @@ -0,0 +1,46 @@ +--- +title: 满江红·写怀 +author: 岳飞 +date: 2024-12-01 +--- + +# 满江红·写怀 + +**怒å‘冲冠,凭æ å¤„ã€æ½‡æ½‡é›¨æ­‡ã€‚** +**抬望眼,仰天长啸,壮怀激烈。** +**三å功å尘与土,八åƒé‡Œè·¯äº‘和月。** +**莫等闲ã€ç™½äº†å°‘年头,空悲切ï¼** + +**é–康耻,犹未雪。** +**è‡£å­æ¨ï¼Œä½•æ—¶ç­ï¼** +**驾长车,è¸ç ´è´ºå…°å±±ç¼ºã€‚** +**壮志饥é¤èƒ¡è™è‚‰ï¼Œç¬‘谈渴饮匈奴血。** +**å¾…ä»Žå¤´ã€æ”¶æ‹¾æ—§å±±æ²³ï¼Œæœå¤©é˜™ã€‚** + +--- + +## 创作背景 + +《满江红·写怀》是å—宋抗金å将岳飞创作的一首è¯ã€‚æ­¤è¯ä¸Šç‰‡æŠ’写作者对中原é‡é™·æ•Œæ‰‹çš„æ‚²æ„¤ï¼Œå¯¹å±€åŠ¿å‰åŠŸå°½å¼ƒçš„ç—›æƒœï¼Œè¡¨è¾¾äº†è‡ªå·±ç»§ç»­åŠªåŠ›äº‰å–å£®å¹´ç«‹åŠŸçš„å¿ƒæ„¿ï¼›ä¸‹ç‰‡æŠ’å†™ä½œè€…å¯¹æ°‘æ—æ•Œäººçš„æ·±ä»‡å¤§æ¨ï¼Œå¯¹ç¥–国统一的殷切愿望,对国家æœå»·çš„赤胆忠诚。 + +## è¯ç‰Œè§£æž + +- **è¯ç‰Œå**:满江红 +- **å­—æ•°**:93å­— +- **韵脚**:仄韵格,气势磅礴 +- **创作时间**:约公元1136年(ç»å…´å…­å¹´ï¼‰ + +## 艺术特色 + +1. **情感激昂**ï¼šå…¨è¯æƒ…感激越,气势磅礴 +2. **对仗工整**:"三å功å尘与土,八åƒé‡Œè·¯äº‘和月"ç­‰å¥å¯¹ä»—精妙 +3. **æ„象鲜明**:è¿ç”¨"怒å‘冲冠"ã€"仰天长啸"等生动æ„象 +4. **爱国情怀**ï¼šè¡¨è¾¾äº†å¼ºçƒˆçš„çˆ±å›½ä¸»ä¹‰ç²¾ç¥žå’Œæ°‘æ—æ°”节 + +## 历å²å½±å“ + +è¿™é¦–è¯æˆä¸ºä¸­åŽæ°‘æ—爱国主义精神的象å¾ä¹‹ä¸€ï¼Œæ¿€åŠ±äº†æ— æ•°ä»äººå¿—士为国家和民æ—奋斗。 + +--- + +*注:此版本为ç»å…¸ä¼ ä¸–版本,收录于《全宋è¯ã€‹ã€‚* \ No newline at end of file diff --git a/知你客æœ17å·èƒ½åŠ›æ–‡æ¡£.md b/知你客æœ17å·èƒ½åŠ›æ–‡æ¡£.md new file mode 100644 index 0000000..2710e93 --- /dev/null +++ b/知你客æœ17å·èƒ½åŠ›æ–‡æ¡£.md @@ -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. 构造轻微异常(路径ä¸è§„èŒƒã€æ–‡ä»¶å·²å­˜åœ¨ï¼‰ç¡®è®¤å…¶ä¼šä¸»åŠ¨è¡¥æ•‘è€Œéžç›´æŽ¥åœä½ã€‚ +