refactor(mention): 优化 Agent 输入框文件 mention,支持未启动对话的时候 mention 工作区的文件
This commit is contained in:
parent
a9d05480f0
commit
1437a6be93
@ -16,7 +16,7 @@ from yuxi.services.upload_utils import write_upload_to_buffer
|
||||
from yuxi.services.viewer_filesystem_service import _detect_preview_type
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp
|
||||
from yuxi.utils.paths import WORKSPACE_DIR_NAME
|
||||
from yuxi.utils.paths import VIRTUAL_PATH_WORKSPACE, WORKSPACE_DIR_NAME
|
||||
|
||||
EDITABLE_WORKSPACE_SUFFIXES = {".md", ".markdown", ".mdx", ".txt"}
|
||||
MAX_WORKSPACE_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
|
||||
@ -69,8 +69,10 @@ def _entry_for_path(root: Path, path: Path) -> dict:
|
||||
display_path = f"/{relative}" if relative else "/"
|
||||
if is_dir and display_path != "/" and not display_path.endswith("/"):
|
||||
display_path = f"{display_path}/"
|
||||
virtual_path = VIRTUAL_PATH_WORKSPACE if display_path == "/" else f"{VIRTUAL_PATH_WORKSPACE}{display_path}"
|
||||
return {
|
||||
"path": display_path,
|
||||
"virtual_path": virtual_path,
|
||||
"name": path.name or "工作区",
|
||||
"is_dir": is_dir,
|
||||
"size": 0 if is_dir else stat.st_size,
|
||||
@ -113,19 +115,26 @@ def _resolve_new_child(root: Path, parent: Path, name: str) -> Path:
|
||||
return target
|
||||
|
||||
|
||||
def _list_directory(root: Path, target: Path) -> list[dict]:
|
||||
entries = [_entry_for_path(root, child) for child in target.iterdir()]
|
||||
def _list_directory(root: Path, target: Path, *, recursive: bool = False, files_only: bool = False) -> list[dict]:
|
||||
children = list(target.iterdir())
|
||||
entries = [_entry_for_path(root, child) for child in children if not files_only or child.is_file()]
|
||||
if recursive:
|
||||
for child in children:
|
||||
if child.is_dir() and not child.is_symlink():
|
||||
entries.extend(_list_directory(root, child, recursive=True, files_only=files_only))
|
||||
return _sort_entries(entries)
|
||||
|
||||
|
||||
async def list_workspace_tree(*, path: str, current_user: User) -> dict:
|
||||
async def list_workspace_tree(
|
||||
*, path: str, recursive: bool = False, files_only: bool = False, current_user: User
|
||||
) -> dict:
|
||||
root = _workspace_root(current_user)
|
||||
target = _resolve_workspace_path(current_user, path)
|
||||
if not target.exists():
|
||||
return {"entries": []}
|
||||
if not target.is_dir():
|
||||
raise HTTPException(status_code=400, detail="当前路径不是目录")
|
||||
entries = await asyncio.to_thread(_list_directory, root, target)
|
||||
entries = await asyncio.to_thread(_list_directory, root, target, recursive=recursive, files_only=files_only)
|
||||
return {"entries": entries}
|
||||
|
||||
|
||||
|
||||
@ -31,9 +31,16 @@ class UpdateWorkspaceFileContentRequest(BaseModel):
|
||||
@workspace.get("/tree", response_model=dict)
|
||||
async def get_workspace_tree(
|
||||
path: str = Query("/", description="工作区目录路径"),
|
||||
recursive: bool = Query(False, description="是否递归返回子目录文件"),
|
||||
files_only: bool = Query(False, description="是否仅返回文件"),
|
||||
current_user: User = Depends(get_required_user),
|
||||
):
|
||||
return await list_workspace_tree(path=path, current_user=current_user)
|
||||
return await list_workspace_tree(
|
||||
path=path,
|
||||
recursive=recursive,
|
||||
files_only=files_only,
|
||||
current_user=current_user,
|
||||
)
|
||||
|
||||
|
||||
@workspace.get("/file", response_model=dict)
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
### 0.6.2 开发记录
|
||||
|
||||
<!-- 0.6.2 的内容请放在这里 -->
|
||||
- 优化 Agent 输入框文件 mention:用户级 workspace 文件候选改为从独立 workspace API 递归加载,不再依赖 active thread;插入时仍转换为 `/home/gem/user-data/workspace/` 沙盒虚拟路径,并修复附件上传后未立即刷新 mention 候选的问题。
|
||||
- 调整知识库思维导图后端结构:将思维导图路由文件重命名为知识库语义更明确的 router,并把文件列表整理、提示词构建、AI JSON 解析等纯逻辑下沉到知识库 utils。
|
||||
- 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API,并增加“工作区”页面,用于浏览个人 workspace 文件、预览 Markdown/文本/代码/图片/PDF;支持新建文件夹、上传文件、下载文件、删除文件/文件夹和多选删除;工作区预览支持 Markdown/TXT 在右侧预览框内切换编辑并保存,其他格式和非工作区预览默认只读;知识库与团队空间入口先展示到占位层级;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。
|
||||
- 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY` 与 `YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并在鉴权阶段拒绝已删除或登录锁定用户继续使用旧令牌访问系统。
|
||||
|
||||
@ -10,8 +10,8 @@ const buildQuery = (params) => {
|
||||
return query.toString()
|
||||
}
|
||||
|
||||
export const getWorkspaceTree = (path = '/') => {
|
||||
const query = buildQuery({ path })
|
||||
export const getWorkspaceTree = (path = '/', recursive = false, filesOnly = false) => {
|
||||
const query = buildQuery({ path, recursive, files_only: filesOnly })
|
||||
return apiGet(`/api/workspace/tree?${query}`)
|
||||
}
|
||||
|
||||
|
||||
@ -252,6 +252,7 @@ import { useConfigStore } from '@/stores/config'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { MessageProcessor } from '@/utils/messageProcessor'
|
||||
import { agentApi, threadApi } from '@/apis'
|
||||
import { getWorkspaceTree } from '@/apis/workspace_api'
|
||||
import HumanApprovalModal from '@/components/HumanApprovalModal.vue'
|
||||
import { useApproval } from '@/composables/useApproval'
|
||||
import { useAgentThreadState } from '@/composables/useAgentThreadState'
|
||||
@ -349,6 +350,7 @@ const { getThreadState, resetOnGoingConv, stopThreadStream } = useAgentThreadSta
|
||||
const threadMessages = ref({})
|
||||
const threadFilesMap = ref({})
|
||||
const threadAttachmentsMap = ref({})
|
||||
const workspaceMentionFiles = ref([])
|
||||
const threadConfigNoticeMap = ref({})
|
||||
const threadPendingConfigNoticeMap = ref({})
|
||||
const threadConfigSnapshotMap = ref({})
|
||||
@ -454,6 +456,7 @@ const { mentionConfig } = useAgentMentionConfig({
|
||||
currentAgentState,
|
||||
currentThreadFiles,
|
||||
currentThreadAttachments,
|
||||
workspaceMentionFiles,
|
||||
configurableItems,
|
||||
agentConfig,
|
||||
availableKnowledgeBases,
|
||||
@ -894,7 +897,13 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
let skipNextWorkspaceMentionActivation = true
|
||||
onActivated(() => {
|
||||
if (skipNextWorkspaceMentionActivation) {
|
||||
skipNextWorkspaceMentionActivation = false
|
||||
} else {
|
||||
void fetchWorkspaceMentionFiles()
|
||||
}
|
||||
nextTick(() => {
|
||||
startChatMainResizeObserver()
|
||||
})
|
||||
@ -1019,7 +1028,25 @@ const refreshThreadFilesAndAttachments = async (threadId) => {
|
||||
await Promise.all([fetchThreadFiles(threadId), fetchThreadAttachments(threadId)])
|
||||
}
|
||||
|
||||
let workspaceMentionFilesRequest = null
|
||||
const fetchWorkspaceMentionFiles = async () => {
|
||||
if (workspaceMentionFilesRequest) return workspaceMentionFilesRequest
|
||||
workspaceMentionFilesRequest = (async () => {
|
||||
try {
|
||||
const response = await getWorkspaceTree('/', true, true)
|
||||
workspaceMentionFiles.value = Array.isArray(response?.entries) ? response.entries : []
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch workspace mention files:', error)
|
||||
workspaceMentionFiles.value = []
|
||||
} finally {
|
||||
workspaceMentionFilesRequest = null
|
||||
}
|
||||
})()
|
||||
return workspaceMentionFilesRequest
|
||||
}
|
||||
|
||||
const handleArtifactSaved = async () => {
|
||||
await fetchWorkspaceMentionFiles()
|
||||
if (!currentChatId.value) return
|
||||
await refreshThreadFilesAndAttachments(currentChatId.value)
|
||||
}
|
||||
@ -1088,7 +1115,10 @@ const handleAttachmentUpload = async (files) => {
|
||||
await threadApi.uploadThreadAttachment(threadId, file)
|
||||
}
|
||||
message.success({ content: '附件上传成功', key: 'upload-attachment', duration: 2 })
|
||||
await fetchAgentState(currentAgentId.value, threadId)
|
||||
await Promise.all([
|
||||
fetchAgentState(currentAgentId.value, threadId),
|
||||
refreshThreadFilesAndAttachments(threadId)
|
||||
])
|
||||
} catch (error) {
|
||||
message.destroy('upload-attachment')
|
||||
handleChatError(error, 'upload')
|
||||
@ -1801,7 +1831,7 @@ const initAll = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initAll()
|
||||
await Promise.all([initAll(), fetchWorkspaceMentionFiles()])
|
||||
scrollController.enableAutoScroll()
|
||||
})
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ export function useAgentMentionConfig({
|
||||
currentAgentState,
|
||||
currentThreadFiles,
|
||||
currentThreadAttachments,
|
||||
workspaceMentionFiles,
|
||||
configurableItems,
|
||||
agentConfig,
|
||||
availableKnowledgeBases,
|
||||
@ -15,6 +16,9 @@ export function useAgentMentionConfig({
|
||||
const files = []
|
||||
const seenPaths = new Set()
|
||||
const workspaceFiles = Array.isArray(currentThreadFiles?.value) ? currentThreadFiles.value : []
|
||||
const userWorkspaceFiles = Array.isArray(workspaceMentionFiles?.value)
|
||||
? workspaceMentionFiles.value
|
||||
: []
|
||||
|
||||
const pushFile = (entry) => {
|
||||
const path = entry?.path || ''
|
||||
@ -62,6 +66,17 @@ export function useAgentMentionConfig({
|
||||
})
|
||||
})
|
||||
|
||||
userWorkspaceFiles.forEach((entry) => {
|
||||
const path = entry?.virtual_path || ''
|
||||
if (!path || entry?.is_dir) return
|
||||
pushFile({
|
||||
path,
|
||||
size: entry.size,
|
||||
modified_at: entry.modified_at,
|
||||
file_name: entry.name
|
||||
})
|
||||
})
|
||||
|
||||
workspaceFiles.forEach((entry) => {
|
||||
const path = entry?.path || ''
|
||||
if (!path.startsWith('/home/gem/user-data/workspace/') || entry?.is_dir) return
|
||||
|
||||
Loading…
Reference in New Issue
Block a user