feat(task): inject uploaded documents into task materials and enable LAN host access
- backend: add Materials field to TaskRunRequestDTO and forward to ExecuteTask - backend: add TestTaskStream_MaterialsInjection test case in taskstream_test.go - web: serialize materials in buildTaskRequest and add unit test - web: improve isSameMessage identity comparison in messageIdentity.ts - web: configure host: '0.0.0.0' in vite.config.ts for LAN collaboration access
This commit is contained in:
+25
-22
@@ -43,28 +43,30 @@ type ChatRequest struct {
|
||||
|
||||
// TaskRunRequestDTO 独立任务运行请求
|
||||
type TaskRunRequestDTO struct {
|
||||
TaskKey string `json:"task_key"`
|
||||
Stage string `json:"stage,omitempty"` // 若指定 stage,则执行成熟度阶段跃迁
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
Fields map[string]string `json:"fields,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
AutoRepair bool `json:"auto_repair"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
ProjectID int64 `json:"project_id,omitempty"`
|
||||
DocID string `json:"doc_id,omitempty"`
|
||||
AuthorID int64 `json:"author_id,omitempty"`
|
||||
AuthorName string `json:"author_name,omitempty"`
|
||||
ReviewerID int64 `json:"reviewer_id,omitempty"`
|
||||
ReviewerName string `json:"reviewer_name,omitempty"`
|
||||
ApproverID int64 `json:"approver_id,omitempty"`
|
||||
ApproverName string `json:"approver_name,omitempty"`
|
||||
ForceDirectEdit bool `json:"force_direct_edit,omitempty"`
|
||||
CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"`
|
||||
WorkflowKey string `json:"workflow_key,omitempty"`
|
||||
TaskKey string `json:"task_key"`
|
||||
Stage string `json:"stage,omitempty"` // 若指定 stage,则执行成熟度阶段跃迁
|
||||
ProjectName string `json:"project_name"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
DeviceType string `json:"device_type"`
|
||||
IntendedUse string `json:"intended_use"`
|
||||
TargetMarket string `json:"target_market"`
|
||||
Fields map[string]string `json:"fields,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
AutoRepair bool `json:"auto_repair"`
|
||||
UserID int64 `json:"user_id,omitempty"`
|
||||
ProjectID int64 `json:"project_id,omitempty"`
|
||||
DocID string `json:"doc_id,omitempty"`
|
||||
AuthorID int64 `json:"author_id,omitempty"`
|
||||
AuthorName string `json:"author_name,omitempty"`
|
||||
ReviewerID int64 `json:"reviewer_id,omitempty"`
|
||||
ReviewerName string `json:"reviewer_name,omitempty"`
|
||||
ApproverID int64 `json:"approver_id,omitempty"`
|
||||
ApproverName string `json:"approver_name,omitempty"`
|
||||
ForceDirectEdit bool `json:"force_direct_edit,omitempty"`
|
||||
CustomSteps []squad.StepSpec `json:"custom_steps,omitempty"`
|
||||
WorkflowKey string `json:"workflow_key,omitempty"`
|
||||
// Materials 用户上传的参考文档材料,由前端 Base64 编码后传入;后端 json 反序列化自动解码 []byte
|
||||
Materials []bizdev.MaterialInput `json:"materials,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -223,6 +225,7 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
|
||||
IntendedUse: req.IntendedUse,
|
||||
TargetMarket: req.TargetMarket,
|
||||
Fields: req.Fields,
|
||||
Materials: req.Materials,
|
||||
Backend: req.Backend,
|
||||
AutoRepair: req.AutoRepair,
|
||||
UserID: req.UserID,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -217,3 +218,69 @@ func TestTaskStream_ResumeInvalidWorkspace_ClassifiedResumable(t *testing.T) {
|
||||
t.Fatalf("actions=%v want [continue restart]", errEv.ErrorInfo.Actions)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskStream_MaterialsInjection 验证前端传入的 materials(Base64 编码的附件内容)
|
||||
// 能被后端 HTTP handler 正确反序列化,透传给 ExecuteTask,并写入工作区的 materials_text/ 目录
|
||||
func TestTaskStream_MaterialsInjection(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Env: "test",
|
||||
AgentBackend: "stub",
|
||||
DataDir: t.TempDir(),
|
||||
KbDbPort: 5432,
|
||||
}
|
||||
svc := bizdev.NewService(cfg)
|
||||
srv := NewServer(cfg, svc)
|
||||
|
||||
ts := httptest.NewServer(srv.Routes())
|
||||
defer ts.Close()
|
||||
|
||||
// 模拟前端发来的包含 materials 字段的请求
|
||||
// Content 经过 Base64 编码:"用户上传文档核心需求:需要支持 1080P 超声实时渲染。"
|
||||
const rawText = "用户上传文档核心需求:需要支持 1080P 超声实时渲染。"
|
||||
b64Content := "55So5oi35LiK5Lyg5paH5qGj5qC45b+D6ZyA5rGC77ya6ZyA6KaB5pSv5oyBIDEwODBQIOi2heWjsOWunuaXtua4suafk+OAgg=="
|
||||
|
||||
body, _ := json.Marshal(map[string]any{
|
||||
"task_key": "intake",
|
||||
"project_name": "超声材料注入测试",
|
||||
"customer_name": "北京水木东方",
|
||||
"device_type": "超声诊断设备",
|
||||
"backend": "stub",
|
||||
"materials": []map[string]string{
|
||||
{
|
||||
"filename": "initial_requirements.txt",
|
||||
"content": b64Content,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
resp, err := http.Post(ts.URL+"/api/v1/tasks/run", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("post tasks run failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
WorkDir string `json:"work_dir"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
if result.WorkDir == "" {
|
||||
t.Fatal("expected non-empty work_dir in response")
|
||||
}
|
||||
|
||||
// 验证 materials_text 目录下是否存在 initial_requirements.txt 且内容还原正确
|
||||
savedFile := result.WorkDir + "/materials_text/initial_requirements.txt"
|
||||
content, err := os.ReadFile(savedFile)
|
||||
if err != nil {
|
||||
t.Fatalf("expected material file at %s: %v", savedFile, err)
|
||||
}
|
||||
if string(content) != rawText {
|
||||
t.Fatalf("material content mismatch: got %q, want %q", string(content), rawText)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,21 @@ test('buildTaskRequest: omits optional fields when absent', () => {
|
||||
assert.equal('target_market' in req, false);
|
||||
assert.equal('user_id' in req, false);
|
||||
assert.equal('project_id' in req, false);
|
||||
assert.equal('materials' in req, false);
|
||||
});
|
||||
|
||||
test('buildTaskRequest: includes materials when provided and non-empty', () => {
|
||||
const req = buildTaskRequest({
|
||||
taskKey: 'intake',
|
||||
projectName: 'P',
|
||||
customerName: 'C',
|
||||
deviceType: 'D',
|
||||
materials: [
|
||||
{ filename: 'doc1.txt', content: 'b64content==' },
|
||||
],
|
||||
});
|
||||
assert.equal('materials' in req, true);
|
||||
assert.deepEqual(req.materials, [{ filename: 'doc1.txt', content: 'b64content==' }]);
|
||||
});
|
||||
|
||||
test('buildResumeRequest: adds work_dir on top of task request', () => {
|
||||
|
||||
@@ -58,6 +58,9 @@ export function buildTaskRequest(task: TaskRunContext): Record<string, unknown>
|
||||
if (task.targetMarket) body.target_market = task.targetMarket;
|
||||
if (task.userId) body.user_id = task.userId;
|
||||
if (task.projectId) body.project_id = task.projectId;
|
||||
if (task.materials && task.materials.length > 0) {
|
||||
body.materials = task.materials;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ export function isSameMessage(a: ChatMessage, b: ChatMessage): boolean {
|
||||
if (a.sessionId !== b.sessionId || a.role !== b.role) {
|
||||
return false;
|
||||
}
|
||||
if (a.id && b.id && a.id === b.id) {
|
||||
return true;
|
||||
}
|
||||
if (a.clientTurnId && b.clientTurnId && a.clientTurnId === b.clientTurnId) {
|
||||
return true;
|
||||
}
|
||||
@@ -30,6 +33,9 @@ export function isSameMessage(a: ChatMessage, b: ChatMessage): boolean {
|
||||
if (a.clientMessageId && b.clientMessageId && a.clientMessageId === b.clientMessageId) {
|
||||
return true;
|
||||
}
|
||||
if (a.content === b.content && a.timestamp === b.timestamp) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
include: ['src/components/chat/**/*.test.ts'],
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
Reference in New Issue
Block a user