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. 构造轻微异常(路径不规范、文件已存在)确认其会主动补救而非直接停住。 +