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:
WeiCHEN
2026-09-13 02:50:22 +00:00
committed by weichen
parent cc2aa33b10
commit eea28f0be4
6 changed files with 117 additions and 22 deletions
+25 -22
View File
@@ -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,
+67
View File
@@ -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)
}
}
+15
View File
@@ -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', () => {
+3
View File
@@ -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;
}
+6
View File
@@ -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;
}
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
include: ['src/components/chat/**/*.test.ts'],
},
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/api': {