Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c585261
feat: 新增个人工作区预览
xerrors Apr 30, 2026
35f09f0
feat: 完善工作区文件管理能力
xerrors Apr 30, 2026
2c4c2e0
docs: 同步修改 CLAUDE.md
xerrors Apr 30, 2026
874767d
feat: 替换文件图标并优化文件面板布局
xerrors Apr 30, 2026
9ff925e
feat(workspace): 增强工作区侧边栏,新增快速访问功能并优化路径选择逻辑
xerrors Apr 30, 2026
6fcfb09
feat(workspace): 重构工作区组件,优化路径选择和面包屑导航功能
xerrors Apr 30, 2026
981fd31
feat(workspace): 增强工作区功能,新增默认 agents 文件创建及路径管理
xerrors May 1, 2026
596eeea
feat(workspace): 新增工作区文件内容编辑功能,支持 Markdown 和 TXT 文件的保存与预览
xerrors May 1, 2026
5fb6af8
feat(workspace): 增强预览面板功能,支持关闭和折叠操作,优化样式
xerrors May 1, 2026
cbd265f
feat(workspace): 增强工作区功能,处理符号链接和非 UTF-8 文本文件的预览支持
xerrors May 1, 2026
02b3718
fix: preview
xerrors May 3, 2026
a450be5
fix(workspace): 修改文件上传逻辑,新增上传工具函数,支持文件大小限制和错误处理
xerrors May 4, 2026
016dfb4
fix(workspace): 修复面包屑路径逻辑,确保当前路径正常化;优化预览文件下载和调整功能
xerrors May 4, 2026
9e63482
style: 格式化导入语句,提升代码可读性
xerrors May 4, 2026
8b03c50
docs(workspace): 添加用户工作区的 AGENTS.md 文件支持,增强 Agent 的长期指令管理
xerrors May 4, 2026
7fbefc0
Merge branch 'main' into feat/workspace
xerrors May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ make format # 格式化代码
- 尽量使用较新的语法,避免使用旧版本的语法(版本兼容到 3.12+)
- 更新 [roadmap.md](docs/develop-guides/roadmap.md) 文档记录本次修改,多个类似的功能更新已经补充在一起
- 开发完成后务必在 docker 中进行测试,可以读取 .env 获取管理员账户和密码
- 不允许把代码写得稀碎:不要为简单线性逻辑拆出一堆细碎 helper;优先写成职责清晰、结构完整、可一眼读懂的实现。
- 拆函数必须服务于明确的复用、隔离副作用或降低认知负担;如果拆分后调用链更绕、上下文更分散,就应合并回更直接的实现。

**其他**:

Expand Down
64 changes: 59 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,69 @@ Yuxi 是一个基于大模型的智能知识库与知识图谱智能体开发平

## 开发准则

Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.

Don't add features, refactor code, or make "improvements" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.

Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use backwards-compatibility shims when you can just change the code.
## 1. Think Before Coding

Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is the minimum needed for the current task. Reuse existing abstractions where possible and follow the DRY principle.
**Don't assume. Don't hide confusion. Surface tradeoffs.**

To ensure readability, it is necessary to add essential comments at key points, particularly to explain the functionality of a function and the design intent.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.

## 2. Simplicity First

**Minimum code that solves the problem. Nothing speculative.**

- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.

Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

## 3. Surgical Changes

**Touch only what you must. Clean up only your own mess.**

When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.

When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.

The test: Every changed line should trace directly to the user's request.

## 4. Goal-Driven Execution

**Define success criteria. Loop until verified.**

Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"

For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```

Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

---

**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

## 开发与调试工作流 (Development & Debugging Workflow)

Expand Down
4 changes: 4 additions & 0 deletions backend/package/yuxi/agents/backends/sandbox/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from .paths import (
VIRTUAL_PATH_PREFIX,
ensure_thread_dirs,
ensure_workspace_default_files,
resolve_virtual_path,
sandbox_outputs_dir,
sandbox_uploads_dir,
sandbox_user_data_dir,
sandbox_workspace_agents_prompt_file,
sandbox_workspace_dir,
virtual_path_for_thread_file,
)
Expand Down Expand Up @@ -51,13 +53,15 @@
"ProvisionerSandboxProvider",
"VIRTUAL_PATH_PREFIX",
"ensure_thread_dirs",
"ensure_workspace_default_files",
"get_sandbox_provider",
"init_sandbox_provider",
"resolve_virtual_path",
"sandbox_id_for_thread",
"sandbox_outputs_dir",
"sandbox_uploads_dir",
"sandbox_user_data_dir",
"sandbox_workspace_agents_prompt_file",
"sandbox_workspace_dir",
"shutdown_sandbox_provider",
"virtual_path_for_thread_file",
Expand Down
41 changes: 39 additions & 2 deletions backend/package/yuxi/agents/backends/sandbox/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
from pathlib import Path

from yuxi import config as conf
from yuxi.utils.paths import OUTPUTS_DIR_NAME, UPLOADS_DIR_NAME, VIRTUAL_PATH_PREFIX, WORKSPACE_DIR_NAME
from yuxi.utils.logging_config import logger
from yuxi.utils.paths import (
OUTPUTS_DIR_NAME,
UPLOADS_DIR_NAME,
VIRTUAL_PATH_PREFIX,
WORKSPACE_AGENTS_DIR_NAME,
WORKSPACE_AGENTS_PROMPT_FILE_NAME,
WORKSPACE_DIR_NAME,
)

_SAFE_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")

Expand Down Expand Up @@ -51,6 +59,33 @@ def sandbox_workspace_dir(thread_id: str, user_id: str) -> Path:
return _global_user_data_dir(user_id) / WORKSPACE_DIR_NAME


def sandbox_workspace_agents_prompt_file(thread_id: str, user_id: str) -> Path:
return sandbox_workspace_dir(thread_id, user_id) / WORKSPACE_AGENTS_DIR_NAME / WORKSPACE_AGENTS_PROMPT_FILE_NAME


def ensure_workspace_default_files(workspace_dir: Path) -> None:
agents_dir = workspace_dir / WORKSPACE_AGENTS_DIR_NAME
agents_file = agents_dir / WORKSPACE_AGENTS_PROMPT_FILE_NAME

try:
agents_dir.mkdir(parents=True, exist_ok=True)
except FileExistsError:
logger.warning("工作区默认 Agents 目录创建失败:路径已被文件占用")
return
except OSError as exc:
logger.warning(f"工作区默认 Agents 目录初始化失败: {exc}")
return

try:
with agents_file.open("xb"):
pass
except FileExistsError:
if agents_file.is_dir():
logger.warning("工作区默认 AGENTS.md 创建失败:路径已被目录占用")
except OSError as exc:
logger.warning(f"工作区默认 Agents 文件初始化失败: {exc}")


def sandbox_uploads_dir(thread_id: str) -> Path:
return _thread_root_dir(thread_id) / UPLOADS_DIR_NAME

Expand All @@ -61,7 +96,9 @@ def sandbox_outputs_dir(thread_id: str) -> Path:

def ensure_thread_dirs(thread_id: str, user_id: str) -> None:
_global_user_data_dir(user_id).mkdir(parents=True, exist_ok=True)
sandbox_workspace_dir(thread_id, user_id).mkdir(parents=True, exist_ok=True)
workspace_dir = sandbox_workspace_dir(thread_id, user_id)
workspace_dir.mkdir(parents=True, exist_ok=True)
ensure_workspace_default_files(workspace_dir)
sandbox_uploads_dir(thread_id).mkdir(parents=True, exist_ok=True)
sandbox_outputs_dir(thread_id).mkdir(parents=True, exist_ok=True)

Expand Down
45 changes: 41 additions & 4 deletions backend/package/yuxi/services/chat_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from langchain.messages import AIMessage, AIMessageChunk, HumanMessage
from langgraph.types import Command
from yuxi import config as conf
from yuxi.agents.backends.sandbox.paths import sandbox_workspace_agents_prompt_file
from yuxi.agents.buildin import agent_manager
from yuxi.agents.state import AgentStatePayload
from yuxi.plugins.guard import content_guard
Expand All @@ -30,6 +31,43 @@
normalize_questions as _normalize_interrupt_questions,
)

WORKSPACE_AGENTS_PROMPT_MAX_BYTES = 64 * 1024


def _load_workspace_agents_prompt(thread_id: str, user_id: str) -> str:
prompt_file = sandbox_workspace_agents_prompt_file(thread_id, user_id)
try:
with prompt_file.open("rb") as buffer:
content = buffer.read(WORKSPACE_AGENTS_PROMPT_MAX_BYTES + 1)
except FileNotFoundError:
return ""
except IsADirectoryError:
logger.warning("读取工作区 AGENTS.md 失败: 路径是目录")
return ""
except OSError as exc:
logger.warning(f"读取工作区 AGENTS.md 失败: {exc}")
return ""

prompt = content[:WORKSPACE_AGENTS_PROMPT_MAX_BYTES].decode("utf-8", errors="replace").strip()
if not prompt:
return ""
if len(content) > WORKSPACE_AGENTS_PROMPT_MAX_BYTES:
return f"{prompt}\n\n[AGENTS.md 内容已截断]"
return prompt


async def _build_agent_input_context(agent_config: dict, *, thread_id: str, user_id: str) -> dict:
input_context = dict(agent_config or {})
agents_prompt = await asyncio.to_thread(_load_workspace_agents_prompt, thread_id, user_id)

if agents_prompt:
agents_section = f"用户工作区 agents/AGENTS.md 内容:\n{agents_prompt}"
base_prompt = str(input_context.get("system_prompt") or "").rstrip()
input_context["system_prompt"] = f"{base_prompt}\n\n{agents_section}" if base_prompt else agents_section

input_context.update({"user_id": user_id, "thread_id": thread_id})
return input_context


def _build_state_files(attachments: list[dict]) -> dict:
"""将附件列表转换为 StateBackend 格式的 files 字典
Expand Down Expand Up @@ -560,7 +598,7 @@ async def agent_chat(
thread_id = str(uuid.uuid4())
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")

input_context = agent_config | {"user_id": user_id, "thread_id": thread_id}
input_context = await _build_agent_input_context(agent_config, thread_id=thread_id, user_id=user_id)
langfuse_run = _build_langfuse_run_context(
current_user=current_user,
thread_id=thread_id,
Expand Down Expand Up @@ -776,7 +814,7 @@ def make_chunk(content=None, **kwargs):
thread_id = str(uuid.uuid4())
logger.warning(f"No thread_id provided, generated new thread_id: {thread_id}")

input_context = agent_config | {"user_id": user_id, "thread_id": thread_id}
input_context = await _build_agent_input_context(agent_config, thread_id=thread_id, user_id=user_id)
langfuse_run = _build_langfuse_run_context(
current_user=current_user,
thread_id=thread_id,
Expand Down Expand Up @@ -1011,8 +1049,7 @@ def make_resume_chunk(content=None, **kwargs):
return

context = agent.context_schema()
context.update(agent_config or {})
context.update({"user_id": user_id, "thread_id": thread_id})
context.update(await _build_agent_input_context(agent_config or {}, thread_id=thread_id, user_id=user_id))
graph = await agent.get_graph(context=context)
langfuse_run = _build_langfuse_run_context(
current_user=current_user,
Expand Down
23 changes: 7 additions & 16 deletions backend/package/yuxi/services/conversation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from dataclasses import dataclass
from pathlib import Path

import aiofiles
from fastapi import HTTPException, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.backends.sandbox import (
Expand All @@ -13,6 +12,7 @@
from yuxi.config import config as app_config
from yuxi.plugins.parser import Parser
from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.upload_utils import write_upload_to_path
from yuxi.utils.datetime_utils import utc_isoformat
from yuxi.utils.logging_config import logger
from yuxi.utils.paths import VIRTUAL_PATH_UPLOADS
Expand Down Expand Up @@ -41,21 +41,12 @@ def _ensure_workdir() -> Path:


async def _write_upload_to_disk(upload: UploadFile, dest: Path) -> int:
await upload.seek(0)
written = 0
chunk_size = 1024 * 1024

async with aiofiles.open(dest, "wb") as buffer:
while True:
chunk = await upload.read(chunk_size)
if not chunk:
break
written += len(chunk)
if written > MAX_ATTACHMENT_SIZE_BYTES:
raise ValueError("附件过大,当前仅支持 5 MB 以内的文件")
await buffer.write(chunk)

return written
return await write_upload_to_path(
upload,
dest,
max_size_bytes=MAX_ATTACHMENT_SIZE_BYTES,
too_large_message="附件过大,当前仅支持 5 MB 以内的文件",
)


def _truncate_markdown(markdown: str) -> tuple[str, bool]:
Expand Down
43 changes: 43 additions & 0 deletions backend/package/yuxi/services/upload_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from pathlib import Path

import aiofiles
from fastapi import UploadFile


async def write_upload_to_buffer(
upload: UploadFile,
buffer,
*,
max_size_bytes: int,
too_large_message: str,
chunk_size: int = 1024 * 1024,
) -> int:
await upload.seek(0)
written = 0

while chunk := await upload.read(chunk_size):
written += len(chunk)
if written > max_size_bytes:
raise ValueError(too_large_message)
await buffer.write(chunk)

return written


async def write_upload_to_path(
upload: UploadFile,
dest: Path,
*,
max_size_bytes: int,
too_large_message: str,
mode: str = "wb",
chunk_size: int = 1024 * 1024,
) -> int:
async with aiofiles.open(dest, mode) as buffer:
return await write_upload_to_buffer(
upload,
buffer,
max_size_bytes=max_size_bytes,
too_large_message=too_large_message,
chunk_size=chunk_size,
)
Loading
Loading