Skip to content

Commit cf21aa0

Browse files
author
Sisyphus Agent
committed
feat: 添加 session submit 命令支持一键提交任务
- 新增 oho/cmd/session/submit.go 实现 submit 命令 - 支持创建会话、可选初始化、发送消息一站式完成 - 添加 --init-project 标志支持项目初始化 - 添加 --provider 和 --model 标志用于 AGENTS.md 初始化 - 支持 --title, --directory, --agent, --no-reply 等标志 - 支持 --file 标志添加附件文件 - 在 session.go 中注册 submit 命令
1 parent 4753ee5 commit cf21aa0

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

oho/cmd/session/session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func init() {
6565
Cmd.AddCommand(revertCmd)
6666
Cmd.AddCommand(unrevertCmd)
6767
Cmd.AddCommand(permissionsCmd)
68+
Cmd.AddCommand(submitCmd)
6869

6970
// 全局会话标志
7071
Cmd.PersistentFlags().StringVarP(&sessionID, "session", "s", "", "会话 ID")

oho/cmd/session/submit.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package session
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
13+
"github.com/anomalyco/oho/internal/client"
14+
"github.com/anomalyco/oho/internal/types"
15+
)
16+
17+
// submitCmd 提交任务命令
18+
var submitCmd = &cobra.Command{
19+
Use: "submit [message]",
20+
Short: "Submit a task by creating a session and sending a message in one step",
21+
Long: "Create a new session, optionally initialize it with AGENTS.md, and send a message in one command",
22+
Args: cobra.ExactArgs(1),
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
// Step 1: Validate flags
25+
if initProject {
26+
if providerID == "" || modelID == "" {
27+
return fmt.Errorf("when using --init-project, --provider and --model are required")
28+
}
29+
}
30+
31+
c := client.NewClient()
32+
ctx := context.Background()
33+
34+
// Step 2: Create session
35+
req := map[string]interface{}{}
36+
if title != "" {
37+
req["title"] = title
38+
}
39+
if directory != "" {
40+
req["directory"] = directory
41+
}
42+
43+
resp, err := c.Post(ctx, "/session", req)
44+
if err != nil {
45+
return fmt.Errorf("failed to create session: %w", err)
46+
}
47+
48+
var session types.Session
49+
if err := json.Unmarshal(resp, &session); err != nil {
50+
return fmt.Errorf("failed to create session: %w", err)
51+
}
52+
53+
fmt.Printf("Session created: %s\n", session.ID)
54+
55+
// Step 3: Initialize session (if requested)
56+
if initProject {
57+
initReq := map[string]interface{}{
58+
"providerID": providerID,
59+
"modelID": modelID,
60+
}
61+
62+
_, err := c.Post(ctx, fmt.Sprintf("/session/%s/init", session.ID), initReq)
63+
if err != nil {
64+
return fmt.Errorf("failed to initialize session: %w", err)
65+
}
66+
67+
fmt.Println("Session initialized successfully")
68+
}
69+
70+
// Step 4: Prepare message parts
71+
var parts []types.Part
72+
73+
// Add text part from args[0]
74+
text := args[0]
75+
parts = append(parts, types.Part{
76+
Type: "text",
77+
Text: &text,
78+
})
79+
80+
// Add file parts for each file
81+
for _, filePath := range files {
82+
// Check if file exists
83+
if _, err := os.Stat(filePath); os.IsNotExist(err) {
84+
return fmt.Errorf("file not found: %s", filePath)
85+
}
86+
87+
// Read file content
88+
fileData, err := os.ReadFile(filePath)
89+
if err != nil {
90+
return fmt.Errorf("failed to read file: %s: %w", filePath, err)
91+
}
92+
93+
// Detect MIME type
94+
mimeType := detectMimeType(filePath)
95+
96+
// Encode to base64 data URL
97+
base64Data := base64.StdEncoding.EncodeToString(fileData)
98+
dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
99+
100+
parts = append(parts, types.Part{
101+
Type: "file",
102+
URL: dataURL,
103+
Mime: mimeType,
104+
})
105+
}
106+
107+
// Step 5: Send message
108+
msgReq := types.MessageRequest{
109+
MessageID: messageID,
110+
Model: messageModel,
111+
Agent: messageAgent,
112+
NoReply: noReply,
113+
System: systemPrompt,
114+
Tools: tools,
115+
Parts: parts,
116+
}
117+
118+
msgResp, err := c.Post(ctx, fmt.Sprintf("/session/%s/message", session.ID), msgReq)
119+
if err != nil {
120+
return fmt.Errorf("failed to send message: %w", err)
121+
}
122+
123+
// Handle empty response
124+
if len(msgResp) == 0 {
125+
fmt.Println("Message sent successfully")
126+
return nil
127+
}
128+
129+
var result types.MessageWithParts
130+
if err := json.Unmarshal(msgResp, &result); err != nil {
131+
return fmt.Errorf("failed to send message: %w", err)
132+
}
133+
134+
fmt.Printf("Message sent successfully: %s\n", result.Info.ID)
135+
136+
// Step 6: Return nil on success
137+
return nil
138+
},
139+
}
140+
141+
// Flag variables for submit command (additional ones not in session.go)
142+
var (
143+
initProject bool
144+
directory string
145+
messageAgent string
146+
messageModel string
147+
noReply bool
148+
systemPrompt string
149+
tools []string
150+
files []string
151+
)
152+
153+
func init() {
154+
// Session creation flags
155+
submitCmd.Flags().BoolVar(&initProject, "init-project", false, "Initialize project with AGENTS.md")
156+
submitCmd.Flags().StringVar(&providerID, "provider", "", "Provider ID for initialization")
157+
submitCmd.Flags().StringVar(&modelID, "model", "", "Model ID for initialization")
158+
submitCmd.Flags().StringVar(&title, "title", "", "Session title")
159+
submitCmd.Flags().StringVar(&directory, "directory", "", "Working directory for the session")
160+
161+
// Message flags
162+
submitCmd.Flags().StringVar(&messageAgent, "agent", "", "Agent ID for message")
163+
submitCmd.Flags().StringVar(&messageModel, "message-model", "", "Model ID for message")
164+
submitCmd.Flags().BoolVar(&noReply, "no-reply", false, "Don't wait for response")
165+
submitCmd.Flags().StringVar(&systemPrompt, "system", "", "System prompt")
166+
submitCmd.Flags().StringSliceVar(&tools, "tools", nil, "Tools list (can be specified multiple times)")
167+
submitCmd.Flags().StringSliceVar(&files, "file", nil, "File attachments (can be specified multiple times)")
168+
}
169+
170+
// detectMimeType 根据文件扩展名检测 MIME 类型
171+
func detectMimeType(filePath string) string {
172+
ext := strings.ToLower(filePath[strings.LastIndex(filePath, "."):])
173+
174+
mimeTypes := map[string]string{
175+
// 图片
176+
".jpg": "image/jpeg",
177+
".jpeg": "image/jpeg",
178+
".png": "image/png",
179+
".gif": "image/gif",
180+
".webp": "image/webp",
181+
".bmp": "image/bmp",
182+
".svg": "image/svg+xml",
183+
184+
// 文档
185+
".pdf": "application/pdf",
186+
".doc": "application/msword",
187+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
188+
".xls": "application/vnd.ms-excel",
189+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
190+
".ppt": "application/vnd.ms-powerpoint",
191+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
192+
193+
// 文本
194+
".txt": "text/plain",
195+
".md": "text/markdown",
196+
".html": "text/html",
197+
".css": "text/css",
198+
".js": "application/javascript",
199+
".json": "application/json",
200+
".xml": "application/xml",
201+
".yaml": "application/x-yaml",
202+
".yml": "application/x-yaml",
203+
204+
// 代码
205+
".py": "text/x-python",
206+
".go": "text/x-go",
207+
".java": "text/x-java",
208+
".c": "text/x-c",
209+
".cpp": "text/x-c++",
210+
".h": "text/x-c",
211+
".rs": "text/x-rust",
212+
".ts": "text/x-typescript",
213+
".tsx": "text/x-typescript",
214+
215+
// 其他
216+
".zip": "application/zip",
217+
".tar": "application/x-tar",
218+
".gz": "application/gzip",
219+
".mp3": "audio/mpeg",
220+
".mp4": "video/mp4",
221+
".wav": "audio/wav",
222+
}
223+
224+
if mimeType, ok := mimeTypes[ext]; ok {
225+
return mimeType
226+
}
227+
228+
// 默认返回 octet-stream
229+
return "application/octet-stream"
230+
}

0 commit comments

Comments
 (0)