From 1ae6930db14bad5655d068a946744bd6100aa8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Wed, 24 Jun 2026 23:26:01 +0800 Subject: [PATCH] Add files via upload --- internal/project/workspace.go | 68 ++++++++++++++++++++++++++++++ internal/project/workspace_test.go | 52 +++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 internal/project/workspace.go create mode 100644 internal/project/workspace_test.go diff --git a/internal/project/workspace.go b/internal/project/workspace.go new file mode 100644 index 00000000..8090a7d2 --- /dev/null +++ b/internal/project/workspace.go @@ -0,0 +1,68 @@ +package project + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func sanitizeWorkspacePathSegment(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "default" + } + s = strings.ReplaceAll(s, string(filepath.Separator), "-") + s = strings.ReplaceAll(s, "/", "-") + s = strings.ReplaceAll(s, "\\", "-") + s = strings.ReplaceAll(s, "..", "__") + if len(s) > 180 { + s = s[:180] + } + return s +} + +// WorkspaceRootDir returns the relative workspace root for downloads and local analysis. +// Project-bound sessions share projects//; otherwise conversations//. +func WorkspaceRootDir(configuredBase, projectID, conversationID string) string { + base := strings.TrimSpace(configuredBase) + if base == "" { + base = filepath.Join("tmp", "workspace") + } + if pid := strings.TrimSpace(projectID); pid != "" { + return filepath.Join(base, "projects", sanitizeWorkspacePathSegment(pid)) + } + conv := strings.TrimSpace(conversationID) + if conv == "" { + conv = "default" + } + return filepath.Join(base, "conversations", sanitizeWorkspacePathSegment(conv)) +} + +// EnsureWorkspace creates the workspace directory and returns its absolute path. +func EnsureWorkspace(root string) (string, error) { + abs, err := filepath.Abs(strings.TrimSpace(root)) + if err != nil { + return "", fmt.Errorf("workspace abs: %w", err) + } + if err := os.MkdirAll(abs, 0o755); err != nil { + return "", fmt.Errorf("workspace mkdir: %w", err) + } + return abs, nil +} + +// BuildWorkspaceBlock instructs the agent to use the session workspace instead of /tmp. +func BuildWorkspaceBlock(absPath string) string { + absPath = strings.TrimSpace(absPath) + if absPath == "" { + return "" + } + return fmt.Sprintf(`## 会话工作目录(下载与本地分析) + +**必须使用以下目录**保存 curl/wget 下载的文件、临时 HTML/JS,以及 read_file/glob/grep 的检索范围: +`+"`%s`"+` + +- **禁止**使用系统 `+"`/tmp`"+` 或其它全局临时目录(多项目/多会话会互窜遗留文件)。 +- 下载示例:`+"`curl -o '%s/page.html' 'https://target/'`"+`;exec 时可将 `+"`workdir`"+` 设为该目录。 +- 读取前用 glob/grep/read_file **限定在该目录**下搜索,勿在 `+"`/tmp`"+` 盲目检索。`, absPath, absPath) +} diff --git a/internal/project/workspace_test.go b/internal/project/workspace_test.go new file mode 100644 index 00000000..2f12fb97 --- /dev/null +++ b/internal/project/workspace_test.go @@ -0,0 +1,52 @@ +package project + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestWorkspaceRootDirProjectScoped(t *testing.T) { + got := WorkspaceRootDir("", "proj-1", "conv-1") + want := filepath.Join("tmp", "workspace", "projects", "proj-1") + if got != want { + t.Fatalf("got %q want %q", got, want) + } +} + +func TestWorkspaceRootDirConversationScoped(t *testing.T) { + got := WorkspaceRootDir("/data/ws", "", "conv-abc") + want := filepath.Join("/data/ws", "conversations", "conv-abc") + if got != want { + t.Fatalf("got %q want %q", got, want) + } +} + +func TestEnsureWorkspaceCreatesDir(t *testing.T) { + root := filepath.Join(t.TempDir(), "nested", "workspace") + abs, err := EnsureWorkspace(root) + if err != nil { + t.Fatalf("EnsureWorkspace: %v", err) + } + st, err := os.Stat(abs) + if err != nil { + t.Fatalf("Stat: %v", err) + } + if !st.IsDir() { + t.Fatal("expected directory") + } +} + +func TestBuildWorkspaceBlockMentionsPath(t *testing.T) { + block := BuildWorkspaceBlock("/opt/csai/tmp/workspace/projects/p1") + if block == "" { + t.Fatal("expected non-empty block") + } + if !strings.Contains(block, "/opt/csai/tmp/workspace/projects/p1") { + t.Fatalf("block missing path: %s", block) + } + if !strings.Contains(block, "/tmp") { + t.Fatalf("block should warn about /tmp: %s", block) + } +}