您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

399 行
13KB

  1. package handlers
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "strconv"
  6. "strings"
  7. "github.com/go-chi/chi/v5"
  8. "qctextbuilder/internal/buildsvc"
  9. "qctextbuilder/internal/domain"
  10. "qctextbuilder/internal/draftsvc"
  11. "qctextbuilder/internal/logging"
  12. "qctextbuilder/internal/onboarding"
  13. "qctextbuilder/internal/templatesvc"
  14. )
  15. type API struct {
  16. templateSvc *templatesvc.Service
  17. onboardSvc *onboarding.Service
  18. draftSvc *draftsvc.Service
  19. buildSvc buildsvc.Service
  20. recentLogs *logging.RecentStore
  21. }
  22. func NewAPI(templateSvc *templatesvc.Service, onboardSvc *onboarding.Service, draftSvc *draftsvc.Service, buildSvc buildsvc.Service, recentLogs *logging.RecentStore) *API {
  23. return &API{
  24. templateSvc: templateSvc,
  25. onboardSvc: onboardSvc,
  26. draftSvc: draftSvc,
  27. buildSvc: buildSvc,
  28. recentLogs: recentLogs,
  29. }
  30. }
  31. func (a *API) Health(w http.ResponseWriter, _ *http.Request) {
  32. writeJSON(w, http.StatusOK, map[string]any{"status": "ok"})
  33. }
  34. func (a *API) SyncTemplates(w http.ResponseWriter, r *http.Request) {
  35. templates, err := a.templateSvc.SyncAITemplates(r.Context())
  36. if err != nil {
  37. writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
  38. return
  39. }
  40. writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
  41. }
  42. func (a *API) ListTemplates(w http.ResponseWriter, r *http.Request) {
  43. templates, err := a.templateSvc.ListTemplates(r.Context())
  44. if err != nil {
  45. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  46. return
  47. }
  48. writeJSON(w, http.StatusOK, map[string]any{"count": len(templates), "templates": templates})
  49. }
  50. func (a *API) GetTemplateDetail(w http.ResponseWriter, r *http.Request) {
  51. rawID := chi.URLParam(r, "id")
  52. templateID, err := strconv.ParseInt(rawID, 10, 64)
  53. if err != nil {
  54. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  55. return
  56. }
  57. detail, err := a.templateSvc.GetTemplateDetail(r.Context(), templateID)
  58. if err != nil {
  59. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  60. return
  61. }
  62. writeJSON(w, http.StatusOK, detail)
  63. }
  64. func (a *API) OnboardTemplate(w http.ResponseWriter, r *http.Request) {
  65. rawID := chi.URLParam(r, "id")
  66. templateID, err := strconv.ParseInt(rawID, 10, 64)
  67. if err != nil {
  68. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  69. return
  70. }
  71. manifest, fields, err := a.onboardSvc.OnboardTemplate(r.Context(), templateID)
  72. if err != nil {
  73. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  74. return
  75. }
  76. writeJSON(w, http.StatusOK, map[string]any{
  77. "manifestId": manifest.ID,
  78. "fieldCount": len(fields),
  79. "status": "reviewed",
  80. })
  81. }
  82. type updateTemplateFieldsRequest struct {
  83. ManifestID string `json:"manifestId"`
  84. Fields []updateTemplateFieldItem `json:"fields"`
  85. }
  86. type updateTemplateFieldItem struct {
  87. Path string `json:"path"`
  88. IsEnabled *bool `json:"isEnabled,omitempty"`
  89. IsRequiredByUs *bool `json:"isRequiredByUs,omitempty"`
  90. DisplayLabel *string `json:"displayLabel,omitempty"`
  91. DisplayOrder *int `json:"displayOrder,omitempty"`
  92. WebsiteSection *string `json:"websiteSection,omitempty"`
  93. Notes *string `json:"notes,omitempty"`
  94. }
  95. func (a *API) UpdateTemplateFields(w http.ResponseWriter, r *http.Request) {
  96. rawID := chi.URLParam(r, "id")
  97. templateID, err := strconv.ParseInt(rawID, 10, 64)
  98. if err != nil {
  99. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid template id"})
  100. return
  101. }
  102. var req updateTemplateFieldsRequest
  103. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  104. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  105. return
  106. }
  107. if len(req.Fields) == 0 {
  108. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "fields is required"})
  109. return
  110. }
  111. patches := make([]onboarding.FieldPatch, 0, len(req.Fields))
  112. for _, f := range req.Fields {
  113. patches = append(patches, onboarding.FieldPatch{
  114. Path: f.Path,
  115. IsEnabled: f.IsEnabled,
  116. IsRequiredByUs: f.IsRequiredByUs,
  117. DisplayLabel: f.DisplayLabel,
  118. DisplayOrder: f.DisplayOrder,
  119. WebsiteSection: f.WebsiteSection,
  120. Notes: f.Notes,
  121. })
  122. }
  123. manifest, fields, err := a.onboardSvc.UpdateTemplateFields(r.Context(), templateID, req.ManifestID, patches)
  124. if err != nil {
  125. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  126. return
  127. }
  128. writeJSON(w, http.StatusOK, map[string]any{
  129. "templateId": templateID,
  130. "manifestId": manifest.ID,
  131. "fieldCount": len(fields),
  132. "fields": fields,
  133. })
  134. }
  135. func (a *API) StartBuild(w http.ResponseWriter, r *http.Request) {
  136. var req buildsvc.StartBuildRequest
  137. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  138. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  139. return
  140. }
  141. result, err := a.buildSvc.StartBuild(r.Context(), req)
  142. if err != nil {
  143. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  144. return
  145. }
  146. writeJSON(w, http.StatusAccepted, result)
  147. }
  148. type upsertDraftRequest struct {
  149. TemplateID *int64 `json:"templateId,omitempty"`
  150. ManifestID string `json:"manifestId"`
  151. Source string `json:"source"`
  152. RequestName string `json:"requestName"`
  153. GlobalData map[string]any `json:"globalData"`
  154. FieldValues map[string]string `json:"fieldValues"`
  155. DraftContext *domain.DraftContext `json:"draftContext,omitempty"`
  156. SuggestionState *domain.DraftSuggestionState `json:"suggestionState,omitempty"`
  157. Status string `json:"status"`
  158. Notes string `json:"notes"`
  159. }
  160. type intakeDraftRequest struct {
  161. DraftID string `json:"draftId,omitempty"`
  162. Source string `json:"source"`
  163. RequestName string `json:"requestName"`
  164. TemplateID *int64 `json:"templateId,omitempty"`
  165. GlobalData map[string]any `json:"globalData"`
  166. Notes string `json:"notes"`
  167. WebsiteURL string `json:"websiteUrl,omitempty"`
  168. WebsiteSummary string `json:"websiteSummary,omitempty"`
  169. BusinessType string `json:"businessType,omitempty"`
  170. LocaleStyle string `json:"localeStyle,omitempty"`
  171. MarketStyle string `json:"marketStyle,omitempty"`
  172. AddressMode string `json:"addressMode,omitempty"`
  173. ContentTone string `json:"contentTone,omitempty"`
  174. PromptInstructions string `json:"promptInstructions,omitempty"`
  175. StyleProfile *domain.DraftStyleProfile `json:"styleProfile,omitempty"`
  176. }
  177. func (a *API) IntakeDraft(w http.ResponseWriter, r *http.Request) {
  178. var req intakeDraftRequest
  179. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  180. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  181. return
  182. }
  183. globalData := req.GlobalData
  184. if globalData == nil {
  185. globalData = map[string]any{}
  186. }
  187. if strings.TrimSpace(req.BusinessType) != "" && strings.TrimSpace(getMapString(globalData, "businessType")) == "" {
  188. globalData["businessType"] = strings.TrimSpace(req.BusinessType)
  189. }
  190. styleProfile := domain.DraftStyleProfile{
  191. LocaleStyle: strings.TrimSpace(req.LocaleStyle),
  192. MarketStyle: strings.TrimSpace(req.MarketStyle),
  193. AddressMode: strings.TrimSpace(req.AddressMode),
  194. ContentTone: strings.TrimSpace(req.ContentTone),
  195. PromptInstructions: strings.TrimSpace(req.PromptInstructions),
  196. }
  197. if req.StyleProfile != nil {
  198. styleProfile = *req.StyleProfile
  199. if styleProfile.LocaleStyle == "" {
  200. styleProfile.LocaleStyle = strings.TrimSpace(req.LocaleStyle)
  201. }
  202. if styleProfile.MarketStyle == "" {
  203. styleProfile.MarketStyle = strings.TrimSpace(req.MarketStyle)
  204. }
  205. if styleProfile.AddressMode == "" {
  206. styleProfile.AddressMode = strings.TrimSpace(req.AddressMode)
  207. }
  208. if styleProfile.ContentTone == "" {
  209. styleProfile.ContentTone = strings.TrimSpace(req.ContentTone)
  210. }
  211. if styleProfile.PromptInstructions == "" {
  212. styleProfile.PromptInstructions = strings.TrimSpace(req.PromptInstructions)
  213. }
  214. }
  215. businessType := strings.TrimSpace(req.BusinessType)
  216. if businessType == "" {
  217. businessType = strings.TrimSpace(getMapString(globalData, "businessType"))
  218. }
  219. draftContext := &domain.DraftContext{
  220. IntakeSource: strings.TrimSpace(req.Source),
  221. LLM: domain.DraftLLMContext{
  222. BusinessType: businessType,
  223. WebsiteURL: strings.TrimSpace(req.WebsiteURL),
  224. WebsiteSummary: strings.TrimSpace(req.WebsiteSummary),
  225. StyleProfile: styleProfile,
  226. },
  227. }
  228. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  229. DraftID: strings.TrimSpace(req.DraftID),
  230. TemplateID: req.TemplateID,
  231. Source: defaultStr(req.Source, "intake-api"),
  232. RequestName: req.RequestName,
  233. GlobalData: globalData,
  234. FieldValues: map[string]string{},
  235. DraftContext: draftContext,
  236. Status: "draft",
  237. Notes: req.Notes,
  238. })
  239. if err != nil {
  240. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  241. return
  242. }
  243. if strings.TrimSpace(req.DraftID) == "" {
  244. writeJSON(w, http.StatusCreated, draft)
  245. return
  246. }
  247. writeJSON(w, http.StatusOK, draft)
  248. }
  249. func (a *API) ListDrafts(w http.ResponseWriter, r *http.Request) {
  250. limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
  251. drafts, err := a.draftSvc.ListDrafts(r.Context(), limit)
  252. if err != nil {
  253. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  254. return
  255. }
  256. writeJSON(w, http.StatusOK, map[string]any{"count": len(drafts), "drafts": drafts})
  257. }
  258. func (a *API) GetDraft(w http.ResponseWriter, r *http.Request) {
  259. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  260. draft, err := a.draftSvc.GetDraft(r.Context(), draftID)
  261. if err != nil {
  262. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  263. return
  264. }
  265. writeJSON(w, http.StatusOK, draft)
  266. }
  267. func (a *API) UpdateDraft(w http.ResponseWriter, r *http.Request) {
  268. draftID := strings.TrimSpace(chi.URLParam(r, "id"))
  269. var req upsertDraftRequest
  270. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  271. writeJSON(w, http.StatusBadRequest, map[string]any{"error": "invalid JSON body"})
  272. return
  273. }
  274. draft, err := a.draftSvc.SaveDraft(r.Context(), draftsvc.UpsertDraftRequest{
  275. DraftID: draftID,
  276. TemplateID: req.TemplateID,
  277. ManifestID: req.ManifestID,
  278. Source: req.Source,
  279. RequestName: req.RequestName,
  280. GlobalData: req.GlobalData,
  281. FieldValues: req.FieldValues,
  282. DraftContext: req.DraftContext,
  283. SuggestionState: req.SuggestionState,
  284. Status: req.Status,
  285. Notes: req.Notes,
  286. })
  287. if err != nil {
  288. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  289. return
  290. }
  291. writeJSON(w, http.StatusOK, draft)
  292. }
  293. func (a *API) GetBuild(w http.ResponseWriter, r *http.Request) {
  294. buildID := chi.URLParam(r, "id")
  295. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  296. if err != nil {
  297. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  298. return
  299. }
  300. writeJSON(w, http.StatusOK, build)
  301. }
  302. func (a *API) PollBuildOnce(w http.ResponseWriter, r *http.Request) {
  303. buildID := chi.URLParam(r, "id")
  304. if err := a.buildSvc.PollOnce(r.Context(), buildID); err != nil {
  305. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  306. return
  307. }
  308. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  309. if err != nil {
  310. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  311. return
  312. }
  313. writeJSON(w, http.StatusOK, build)
  314. }
  315. func (a *API) FetchBuildEditorURL(w http.ResponseWriter, r *http.Request) {
  316. buildID := chi.URLParam(r, "id")
  317. if err := a.buildSvc.FetchEditorURL(r.Context(), buildID); err != nil {
  318. writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
  319. return
  320. }
  321. build, err := a.buildSvc.GetBuild(r.Context(), buildID)
  322. if err != nil {
  323. writeJSON(w, http.StatusNotFound, map[string]any{"error": err.Error()})
  324. return
  325. }
  326. writeJSON(w, http.StatusOK, build)
  327. }
  328. func (a *API) ListLogs(w http.ResponseWriter, r *http.Request) {
  329. limit, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("limit")))
  330. if limit <= 0 {
  331. limit = 100
  332. }
  333. if limit > 500 {
  334. limit = 500
  335. }
  336. logs := a.recentLogs.List(limit)
  337. writeJSON(w, http.StatusOK, map[string]any{
  338. "count": len(logs),
  339. "logs": logs,
  340. })
  341. }
  342. func writeJSON(w http.ResponseWriter, status int, v any) {
  343. w.Header().Set("Content-Type", "application/json")
  344. w.WriteHeader(status)
  345. _ = json.NewEncoder(w).Encode(v)
  346. }
  347. func defaultStr(v, fallback string) string {
  348. if strings.TrimSpace(v) == "" {
  349. return fallback
  350. }
  351. return strings.TrimSpace(v)
  352. }
  353. func getMapString(values map[string]any, key string) string {
  354. if values == nil {
  355. return ""
  356. }
  357. raw, _ := values[key].(string)
  358. return raw
  359. }