feat: initialize project skeleton with Go backend, database, and config

- Go module with standard library + gorilla/sessions + mysql driver
- Config: env-based configuration with .env file support
- Database: MySQL connection with auto-migration for 6 tables
- Seed data: 12 system tags with options, 8 system snippets
- Auth: session cookie middleware
- Handlers: auth, tags, snippets, builder, claude, dashboard, suggestions, settings
- LLM: OpenAI-compatible client with 30s timeout
- Docker: Dockerfile + docker-compose.yml
- .env.example with all configuration options

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:58:15 +08:00
parent 83ae0bfe74
commit 01ecdc8c23
19 changed files with 1820 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# 服务配置
SERVER_PORT=8080
SESSION_SECRET=your-random-secret-key
# 数据库(连接已有的远程 MySQL 实例)
DB_HOST=your-mysql-public-ip
DB_PORT=3306
DB_USER=your-db-user
DB_PASSWORD=your-db-password
DB_NAME=prompt_generator
# 认证
AUTH_PASSWORD=your-access-password
# LLM
LLM_API_BASE_URL=https://api.deepseek.com/v1
LLM_API_KEY=sk-xxx
LLM_MODEL_NAME=deepseek-chat
+18
View File
@@ -0,0 +1,18 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /app/frontend ./frontend
EXPOSE 8080
CMD ["./server"]
+7
View File
@@ -0,0 +1,7 @@
services:
app:
build: .
ports:
- "8080:8080"
env_file: .env
restart: unless-stopped
+10
View File
@@ -0,0 +1,10 @@
module prompt-generator
go 1.26.3
require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
)
+8
View File
@@ -0,0 +1,8 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
+46
View File
@@ -0,0 +1,46 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"github.com/gorilla/sessions"
)
var (
Store *sessions.CookieStore
mu sync.Mutex
)
func Init(secret string) {
Store = sessions.NewCookieStore([]byte(secret))
Store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7, // 7 days
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
}
func GenerateToken() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
// AuthMiddleware checks if the user is authenticated
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := Store.Get(r, "session")
auth, ok := session.Values["authenticated"].(bool)
if !ok || !auth {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"code":401,"message":"未登录"}`))
return
}
next.ServeHTTP(w, r)
})
}
+42
View File
@@ -0,0 +1,42 @@
package config
import (
"os"
)
type Config struct {
ServerPort string
SessionSecret string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
AuthPassword string
LLMAPIBaseURL string
LLMAPIKey string
LLMModelName string
}
func Load() *Config {
return &Config{
ServerPort: getEnv("SERVER_PORT", "8080"),
SessionSecret: getEnv("SESSION_SECRET", "default-secret-change-me"),
DBHost: getEnv("DB_HOST", "127.0.0.1"),
DBPort: getEnv("DB_PORT", "3306"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "prompt_generator"),
AuthPassword: getEnv("AUTH_PASSWORD", "admin"),
LLMAPIBaseURL: getEnv("LLM_API_BASE_URL", "https://api.deepseek.com/v1"),
LLMAPIKey: getEnv("LLM_API_KEY", ""),
LLMModelName: getEnv("LLM_MODEL_NAME", "deepseek-chat"),
}
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
+331
View File
@@ -0,0 +1,331 @@
package db
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
"prompt-generator/internal/config"
)
var DB *sql.DB
func Init(cfg *config.Config) error {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=Local",
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName)
var err error
DB, err = sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
if err = DB.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
DB.SetMaxOpenConns(25)
DB.SetMaxIdleConns(5)
log.Println("Database connected successfully")
return nil
}
func AutoMigrate() error {
tables := []string{
`CREATE TABLE IF NOT EXISTS tags (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(64) NOT NULL,
description varchar(255) NOT NULL DEFAULT '',
scope enum('system','personal') NOT NULL DEFAULT 'system',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_scope (scope)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS tag_options (
id bigint NOT NULL AUTO_INCREMENT,
tag_id bigint NOT NULL,
label varchar(128) NOT NULL,
constraint_text text NOT NULL,
sort_order int NOT NULL DEFAULT 0,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tag_id (tag_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS snippets (
id bigint NOT NULL AUTO_INCREMENT,
name varchar(128) NOT NULL,
content text NOT NULL,
category varchar(64) NOT NULL DEFAULT '',
scope enum('system','personal') NOT NULL DEFAULT 'system',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_scope (scope)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS builder_sessions (
id bigint NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL DEFAULT '',
project_name varchar(255) NOT NULL DEFAULT '',
final_prompt text,
claude_session_id varchar(128) DEFAULT NULL,
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_project_name (project_name),
KEY idx_claude_session_id (claude_session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS builder_session_tags (
id bigint NOT NULL AUTO_INCREMENT,
builder_session_id bigint NOT NULL,
tag_id bigint NOT NULL,
tag_option_id bigint NOT NULL,
PRIMARY KEY (id),
KEY idx_session_id (builder_session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
`CREATE TABLE IF NOT EXISTS builder_session_snippets (
id bigint NOT NULL AUTO_INCREMENT,
builder_session_id bigint NOT NULL,
snippet_id bigint NOT NULL,
sort_order int NOT NULL DEFAULT 0,
PRIMARY KEY (id),
KEY idx_session_id (builder_session_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
}
for _, ddl := range tables {
if _, err := DB.Exec(ddl); err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
}
log.Println("Database migration completed")
return nil
}
func SeedData() error {
// Check if system tags already exist
var count int
err := DB.QueryRow("SELECT COUNT(*) FROM tags WHERE scope='system'").Scan(&count)
if err != nil {
return err
}
if count > 0 {
log.Println("Seed data already exists, skipping")
return nil
}
tx, err := DB.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// Insert system tags
type tagSeed struct {
name string
desc string
options []struct {
label string
constraint string
}
}
seeds := []tagSeed{
{
name: "Git 规范",
desc: "控制 Git 操作范围和提交规范",
options: []struct{ label, constraint string }{
{"标准 commit,禁止 push", "所有变更必须先 commit,禁止直接 push 到远程仓库。每次 commit 需要清晰的提交信息。"},
{"标准 commit,允许 push", "变更完成后可 commit 并 push 到远程仓库。"},
{"仅允许 commit,禁止 merge/rebase", "只允许 commit 操作,禁止 merge、rebase、force push 等危险操作。"},
},
},
{
name: "环境背景",
desc: "提供运行环境上下文信息",
options: []struct{ label, constraint string }{
{"服务端环境(Linux 生产)", "当前工作环境为 Linux 生产服务器,注意路径分隔符和命令兼容性。"},
{"本地开发环境", "当前为本地开发环境,可自由使用开发工具和调试手段。"},
{"CI/CD 环境", "当前在 CI/CD 流水线中运行,注意环境变量和权限限制。"},
{"测试环境", "当前为测试环境,可以进行破坏性测试。"},
},
},
{
name: "工具约束",
desc: "限制可用的工具和操作方式",
options: []struct{ label, constraint string }{
{"必须使用 AskUserQuestion 提问", "遇到需要用户确认的问题时,必须使用 AskUserQuestion 工具,不要自行判断。"},
{"优先使用专用工具而非 shell", "优先使用 Read/Write/Edit 等专用工具,避免直接使用 shell 命令处理文件。"},
{"禁止使用 Agent 工具", "不要使用 Agent 工具派发子任务,在当前会话中完成所有工作。"},
},
},
{
name: "提交策略",
desc: "控制 Git 提交的粒度和时机",
options: []struct{ label, constraint string }{
{"每完成一个小任务 commit 一次", "每完成一个独立的子任务就立即 commit,保持提交粒度细小。"},
{"全部完成后一次性 commit", "所有任务完成后统一 commit 一个大的变更。"},
{"按功能模块分别 commit", "按功能模块划分,每个模块完成后分别 commit。"},
},
},
{
name: "阅读策略",
desc: "信息获取的顺序和方式",
options: []struct{ label, constraint string }{
{"先读 docs/ 了解架构再读代码", "先阅读项目文档和架构说明,理解整体设计后再阅读具体代码实现。"},
{"直接读代码", "直接阅读源代码,通过代码理解项目结构。"},
{"先读 README 和 CLAUDE.md", "先阅读 README.md 和 CLAUDE.md 了解项目规范和约束。"},
},
},
{
name: "输出格式",
desc: "约束输出内容的格式",
options: []struct{ label, constraint string }{
{"Markdown", "输出内容使用 Markdown 格式,包含适当的标题、列表和代码块。"},
{"JSON", "输出内容使用 JSON 格式,确保数据结构清晰。"},
{"纯文本", "输出纯文本内容,不使用任何格式标记。"},
{"代码块优先", "优先使用代码块展示内容,减少文字说明。"},
},
},
{
name: "安全约束",
desc: "操作安全边界",
options: []struct{ label, constraint string }{
{"禁止访问外部网络", "不要发起任何网络请求,不访问外部 API 或下载资源。"},
{"禁止修改系统文件", "不要修改 /etc、/usr 等系统目录下的文件。"},
{"禁止执行危险命令", "不要执行 rm -rf、chmod 777、dd 等危险命令。"},
},
},
{
name: "代码风格",
desc: "代码编写规范",
options: []struct{ label, constraint string }{
{"遵循项目现有风格", "严格遵循项目中已有的代码风格和命名约定,保持一致性。"},
{"严格 ESLint/Prettier", "代码必须符合 ESLint 和 Prettier 规则,不得有 lint 错误。"},
{"自由风格", "不限制代码风格,以可读性为优先。"},
},
},
{
name: "测试要求",
desc: "测试策略和要求",
options: []struct{ label, constraint string }{
{"必须编写单元测试", "所有新增功能必须配套单元测试,确保测试覆盖关键逻辑。"},
{"仅手动验证", "通过手动测试验证功能,不强制要求自动化测试。"},
{"TDD 方式", "采用测试驱动开发,先写测试再写实现。"},
},
},
{
name: "错误处理",
desc: "遇到异常情况的行为",
options: []struct{ label, constraint string }{
{"遇到不确定先问用户", "遇到不确定的情况时,使用 AskUserQuestion 向用户确认后再继续。"},
{"尽可能自行判断", "尽量自行分析和判断,减少对用户的打扰。"},
{"遇错停止等待指示", "遇到任何错误立即停止,等待用户进一步指示。"},
},
},
{
name: "上下文策略",
desc: "Token 使用和输出策略",
options: []struct{ label, constraint string }{
{"节约上下文,精简输出", "输出尽量精简,只包含必要信息,减少 token 消耗。"},
{"详细输出,不省略", "输出完整详细的信息,不省略任何内容。"},
{"平衡模式", "在信息完整性和 token 效率之间取得平衡。"},
},
},
{
name: "工作模式",
desc: "工作执行方式",
options: []struct{ label, constraint string }{
{"规划优先,先出方案再执行", "先制定详细方案,获得用户确认后再动手执行。"},
{"直接执行,边做边调", "直接开始执行,根据反馈及时调整方向。"},
{"探索模式,先研究再动手", "先深入研究和探索,充分理解后再开始执行。"},
},
},
}
for _, s := range seeds {
res, err := tx.Exec("INSERT INTO tags (name, description, scope) VALUES (?, ?, 'system')", s.name, s.desc)
if err != nil {
return err
}
tagID, _ := res.LastInsertId()
for i, opt := range s.options {
_, err := tx.Exec("INSERT INTO tag_options (tag_id, label, constraint_text, sort_order) VALUES (?, ?, ?, ?)",
tagID, opt.label, opt.constraint, i)
if err != nil {
return err
}
}
}
// Insert system snippets
snippets := []struct {
name string
content string
category string
}{
{
name: "项目架构概述",
content: "## 项目架构\n\n请在开始工作前了解项目整体架构:\n- 后端框架和技术栈\n- 前端框架和技术栈\n- 数据库和缓存方案\n- 部署和运维方式\n\n确保你的修改符合项目整体架构设计。",
category: "架构",
},
{
name: "技术栈说明",
content: "## 技术栈\n\n请在此处填写项目使用的主要技术栈:\n- 语言:\n- 框架:\n- 数据库:\n- 缓存:\n- 消息队列:\n- 部署:",
category: "架构",
},
{
name: "目录结构描述",
content: "## 目录结构\n\n项目主要目录结构:\n```\n/\n├── src/ # 源代码\n├── tests/ # 测试文件\n├── docs/ # 文档\n├── config/ # 配置文件\n└── scripts/ # 脚本工具\n```\n\n请确保新增文件放在正确的目录下。",
category: "架构",
},
{
name: "编码规范要求",
content: "## 编码规范\n\n1. 变量和函数命名使用 camelCase\n2. 常量使用 UPPER_SNAKE_CASE\n3. 类名使用 PascalCase\n4. 每个函数不超过 50 行\n5. 复杂逻辑必须添加注释\n6. 错误处理不能忽略",
category: "规范",
},
{
name: "测试框架配置",
content: "## 测试要求\n\n- 使用项目现有的测试框架\n- 测试文件命名:xxx_test.go / xxx.test.ts\n- 测试覆盖率要求:核心逻辑 > 80%\n- 每个测试用例独立,不依赖执行顺序\n- Mock 外部依赖,不依赖真实服务",
category: "测试",
},
{
name: "部署流程说明",
content: "## 部署流程\n\n1. 代码合并到 main 分支\n2. CI 自动运行测试\n3. 构建 Docker 镜像\n4. 部署到 staging 环境验证\n5. 手动确认后部署到生产环境\n\n注意:不要修改 CI/CD 配置文件,除非明确要求。",
category: "运维",
},
{
name: "PR 规范",
content: "## PR 规范\n\n- PR 标题简洁明了,说明变更内容\n- 描述中包含:变更原因、主要改动、测试方式\n- 单个 PR 不超过 500 行变更\n- 必须通过 CI 检查\n- 至少一人 Code Review 后合并",
category: "规范",
},
{
name: "Code Review 要点",
content: "## Code Review 要点\n\n- 代码逻辑是否正确\n- 是否有边界条件未处理\n- 是否有安全隐患\n- 命名是否清晰\n- 是否有冗余代码\n- 测试是否充分\n- 性能是否有问题",
category: "规范",
},
}
for _, s := range snippets {
_, err := tx.Exec("INSERT INTO snippets (name, content, category, scope) VALUES (?, ?, ?, 'system')",
s.name, s.content, s.category)
if err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
log.Println("Seed data inserted successfully")
return nil
}
+69
View File
@@ -0,0 +1,69 @@
package handlers
import (
"encoding/json"
"net/http"
"prompt-generator/internal/auth"
"prompt-generator/internal/config"
"prompt-generator/internal/models"
)
var cfg *config.Config
func Init(c *config.Config) {
cfg = c
}
func writeJSON(w http.ResponseWriter, code int, resp models.APIResponse) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(resp)
}
func success(w http.ResponseWriter, data interface{}) {
writeJSON(w, http.StatusOK, models.APIResponse{Code: 0, Message: "success", Data: data})
}
func fail(w http.ResponseWriter, httpCode int, msg string) {
writeJSON(w, httpCode, models.APIResponse{Code: httpCode, Message: msg})
}
func Login(w http.ResponseWriter, r *http.Request) {
var req struct {
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
if req.Password != cfg.AuthPassword {
fail(w, 401, "密码错误")
return
}
session, _ := auth.Store.Get(r, "session")
session.Values["authenticated"] = true
session.Save(r, w)
success(w, nil)
}
func Logout(w http.ResponseWriter, r *http.Request) {
session, _ := auth.Store.Get(r, "session")
session.Values["authenticated"] = false
session.Options.MaxAge = -1
session.Save(r, w)
success(w, nil)
}
func AuthCheck(w http.ResponseWriter, r *http.Request) {
session, _ := auth.Store.Get(r, "session")
authed, ok := session.Values["authenticated"].(bool)
if !ok || !authed {
fail(w, 401, "未登录")
return
}
success(w, map[string]bool{"authenticated": true})
}
+240
View File
@@ -0,0 +1,240 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"prompt-generator/internal/db"
"prompt-generator/internal/models"
)
func GetBuilderSessions(w http.ResponseWriter, r *http.Request) {
rows, err := db.DB.Query("SELECT id, title, project_name, final_prompt, claude_session_id, created_at, updated_at FROM builder_sessions ORDER BY updated_at DESC")
if err != nil {
fail(w, 500, "查询会话列表失败")
return
}
defer rows.Close()
var sessions []models.BuilderSession
for rows.Next() {
var s models.BuilderSession
if err := rows.Scan(&s.ID, &s.Title, &s.ProjectName, &s.FinalPrompt, &s.ClaudeSessionID, &s.CreatedAt, &s.UpdatedAt); err != nil {
continue
}
sessions = append(sessions, s)
}
if sessions == nil {
sessions = []models.BuilderSession{}
}
success(w, sessions)
}
func CreateBuilderSession(w http.ResponseWriter, r *http.Request) {
var req struct {
Title string `json:"title"`
ProjectName string `json:"project_name"`
FinalPrompt string `json:"final_prompt"`
ClaudeSessionID string `json:"claude_session_id"`
TagOptionIDs []struct {
TagID int64 `json:"tag_id"`
TagOptionID int64 `json:"tag_option_id"`
} `json:"tag_option_ids"`
SnippetIDs []struct {
SnippetID int64 `json:"snippet_id"`
SortOrder int `json:"sort_order"`
} `json:"snippet_ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
var claudeID *string
if req.ClaudeSessionID != "" {
claudeID = &req.ClaudeSessionID
}
res, err := db.DB.Exec("INSERT INTO builder_sessions (title, project_name, final_prompt, claude_session_id) VALUES (?, ?, ?, ?)",
req.Title, req.ProjectName, req.FinalPrompt, claudeID)
if err != nil {
fail(w, 500, "创建会话失败")
return
}
sessionID, _ := res.LastInsertId()
// Insert tag associations
for _, t := range req.TagOptionIDs {
db.DB.Exec("INSERT INTO builder_session_tags (builder_session_id, tag_id, tag_option_id) VALUES (?, ?, ?)",
sessionID, t.TagID, t.TagOptionID)
}
// Insert snippet associations
for _, s := range req.SnippetIDs {
db.DB.Exec("INSERT INTO builder_session_snippets (builder_session_id, snippet_id, sort_order) VALUES (?, ?, ?)",
sessionID, s.SnippetID, s.SortOrder)
}
success(w, map[string]int64{"id": sessionID})
}
func GetBuilderSession(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var s models.BuilderSession
err = db.DB.QueryRow("SELECT id, title, project_name, final_prompt, claude_session_id, created_at, updated_at FROM builder_sessions WHERE id=?", id).
Scan(&s.ID, &s.Title, &s.ProjectName, &s.FinalPrompt, &s.ClaudeSessionID, &s.CreatedAt, &s.UpdatedAt)
if err != nil {
fail(w, 404, "会话不存在")
return
}
// Load tags
tagRows, err := db.DB.Query("SELECT id, builder_session_id, tag_id, tag_option_id FROM builder_session_tags WHERE builder_session_id=?", id)
if err == nil {
for tagRows.Next() {
var t models.SessionTag
if err := tagRows.Scan(&t.ID, &t.BuilderSessionID, &t.TagID, &t.TagOptionID); err == nil {
s.Tags = append(s.Tags, t)
}
}
tagRows.Close()
}
// Load snippets
snippetRows, err := db.DB.Query("SELECT id, builder_session_id, snippet_id, sort_order FROM builder_session_snippets WHERE builder_session_id=? ORDER BY sort_order", id)
if err == nil {
for snippetRows.Next() {
var sn models.SessionSnippet
if err := snippetRows.Scan(&sn.ID, &sn.BuilderSessionID, &sn.SnippetID, &sn.SortOrder); err == nil {
s.Snippets = append(s.Snippets, sn)
}
}
snippetRows.Close()
}
if s.Tags == nil {
s.Tags = []models.SessionTag{}
}
if s.Snippets == nil {
s.Snippets = []models.SessionSnippet{}
}
success(w, s)
}
func UpdateBuilderSession(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var req struct {
Title string `json:"title"`
ProjectName string `json:"project_name"`
FinalPrompt string `json:"final_prompt"`
ClaudeSessionID string `json:"claude_session_id"`
TagOptionIDs []struct {
TagID int64 `json:"tag_id"`
TagOptionID int64 `json:"tag_option_id"`
} `json:"tag_option_ids"`
SnippetIDs []struct {
SnippetID int64 `json:"snippet_id"`
SortOrder int `json:"sort_order"`
} `json:"snippet_ids"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
var claudeID *string
if req.ClaudeSessionID != "" {
claudeID = &req.ClaudeSessionID
}
_, err = db.DB.Exec("UPDATE builder_sessions SET title=?, project_name=?, final_prompt=?, claude_session_id=? WHERE id=?",
req.Title, req.ProjectName, req.FinalPrompt, claudeID, id)
if err != nil {
fail(w, 500, "更新会话失败")
return
}
// Replace tags
db.DB.Exec("DELETE FROM builder_session_tags WHERE builder_session_id=?", id)
for _, t := range req.TagOptionIDs {
db.DB.Exec("INSERT INTO builder_session_tags (builder_session_id, tag_id, tag_option_id) VALUES (?, ?, ?)",
id, t.TagID, t.TagOptionID)
}
// Replace snippets
db.DB.Exec("DELETE FROM builder_session_snippets WHERE builder_session_id=?", id)
for _, s := range req.SnippetIDs {
db.DB.Exec("INSERT INTO builder_session_snippets (builder_session_id, snippet_id, sort_order) VALUES (?, ?, ?)",
id, s.SnippetID, s.SortOrder)
}
success(w, nil)
}
func DeleteBuilderSession(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
db.DB.Exec("DELETE FROM builder_session_tags WHERE builder_session_id=?", id)
db.DB.Exec("DELETE FROM builder_session_snippets WHERE builder_session_id=?", id)
db.DB.Exec("DELETE FROM builder_sessions WHERE id=?", id)
success(w, nil)
}
// AssemblePrompt 组装最终 Prompt
func AssemblePrompt(projectName string, customContent string, tagOptions []models.TagOption, snippets []models.Snippet, suggestions []string) string {
var parts []string
// Project context
if projectName != "" {
parts = append(parts, fmt.Sprintf("## 项目上下文\n\n项目名称: %s", projectName))
}
// Custom content
if customContent != "" {
parts = append(parts, fmt.Sprintf("## 用户自定义内容\n\n%s", customContent))
}
// Tag constraints
if len(tagOptions) > 0 {
var constraints []string
for _, opt := range tagOptions {
constraints = append(constraints, fmt.Sprintf("- %s", opt.ConstraintText))
}
parts = append(parts, fmt.Sprintf("## 标签约束\n\n%s", strings.Join(constraints, "\n")))
}
// Snippets
if len(snippets) > 0 {
var snippetParts []string
for _, s := range snippets {
snippetParts = append(snippetParts, fmt.Sprintf("--- 片段: %s ---\n%s", s.Name, s.Content))
}
parts = append(parts, fmt.Sprintf("## 片段内容\n\n%s", strings.Join(snippetParts, "\n\n")))
}
// Suggestions
if len(suggestions) > 0 {
parts = append(parts, fmt.Sprintf("## 智能建议补充\n\n%s", strings.Join(suggestions, "\n")))
}
return strings.Join(parts, "\n\n")
}
+89
View File
@@ -0,0 +1,89 @@
package handlers
import (
"net/http"
"prompt-generator/internal/db"
)
func GetClaudeSessions(w http.ResponseWriter, r *http.Request) {
projectName := r.URL.Query().Get("project_name")
if projectName == "" {
fail(w, 400, "project_name 参数必填")
return
}
rows, err := db.DB.Query(`
SELECT session_id, COUNT(*) as prompt_count, MAX(created_at) as last_time
FROM prompts
WHERE project_name=?
GROUP BY session_id
ORDER BY last_time DESC
LIMIT 50
`, projectName)
if err != nil {
fail(w, 500, "查询失败")
return
}
defer rows.Close()
var sessions []map[string]interface{}
for rows.Next() {
var sessionID string
var count int
var lastTime string
if err := rows.Scan(&sessionID, &count, &lastTime); err != nil {
continue
}
sessions = append(sessions, map[string]interface{}{
"session_id": sessionID,
"prompt_count": count,
"last_time": lastTime,
})
}
if sessions == nil {
sessions = []map[string]interface{}{}
}
success(w, sessions)
}
func GetClaudePrompts(w http.ResponseWriter, r *http.Request) {
sessionID := r.PathValue("session_id")
if sessionID == "" {
fail(w, 400, "session_id 必填")
return
}
rows, err := db.DB.Query(`
SELECT id, session_id, project_name, prompt, created_at
FROM prompts
WHERE session_id=?
ORDER BY created_at DESC
LIMIT 10
`, sessionID)
if err != nil {
fail(w, 500, "查询失败")
return
}
defer rows.Close()
var prompts []map[string]interface{}
for rows.Next() {
var id int64
var sid, pname, prompt, createdAt string
if err := rows.Scan(&id, &sid, &pname, &prompt, &createdAt); err != nil {
continue
}
prompts = append(prompts, map[string]interface{}{
"id": id,
"session_id": sid,
"project": pname,
"prompt": prompt,
"created_at": createdAt,
})
}
if prompts == nil {
prompts = []map[string]interface{}{}
}
success(w, prompts)
}
+161
View File
@@ -0,0 +1,161 @@
package handlers
import (
"net/http"
"strconv"
"prompt-generator/internal/db"
)
func GetDashboardProjects(w http.ResponseWriter, r *http.Request) {
rows, err := db.DB.Query(`
SELECT project_name, COUNT(*) as session_count, MIN(created_at) as first_time, MAX(created_at) as last_time
FROM prompts
GROUP BY project_name
ORDER BY last_time DESC
`)
if err != nil {
fail(w, 500, "查询项目列表失败")
return
}
defer rows.Close()
var projects []map[string]interface{}
for rows.Next() {
var name string
var count int
var firstTime, lastTime string
if err := rows.Scan(&name, &count, &firstTime, &lastTime); err != nil {
continue
}
if name == "" {
name = "(未命名项目)"
}
projects = append(projects, map[string]interface{}{
"project_name": name,
"session_count": count,
"first_time": firstTime,
"last_time": lastTime,
})
}
if projects == nil {
projects = []map[string]interface{}{}
}
success(w, projects)
}
func GetDashboardSessions(w http.ResponseWriter, r *http.Request) {
projectName := r.PathValue("name")
if projectName == "" {
fail(w, 400, "项目名称必填")
return
}
rows, err := db.DB.Query(`
SELECT session_id, COUNT(*) as prompt_count, MIN(created_at) as first_time, MAX(created_at) as last_time
FROM prompts
WHERE project_name=?
GROUP BY session_id
ORDER BY last_time DESC
LIMIT 100
`, projectName)
if err != nil {
fail(w, 500, "查询会话列表失败")
return
}
defer rows.Close()
var sessions []map[string]interface{}
for rows.Next() {
var sessionID string
var count int
var firstTime, lastTime string
if err := rows.Scan(&sessionID, &count, &firstTime, &lastTime); err != nil {
continue
}
sessions = append(sessions, map[string]interface{}{
"session_id": sessionID,
"prompt_count": count,
"first_time": firstTime,
"last_time": lastTime,
})
}
if sessions == nil {
sessions = []map[string]interface{}{}
}
success(w, sessions)
}
func GetDashboardPrompts(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
keyword := r.URL.Query().Get("keyword")
pageStr := r.URL.Query().Get("page")
pageSizeStr := r.URL.Query().Get("page_size")
if sessionID == "" {
fail(w, 400, "session_id 参数必填")
return
}
page := 1
pageSize := 20
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 && ps <= 100 {
pageSize = ps
}
offset := (page - 1) * pageSize
query := "SELECT id, session_id, project_name, prompt, created_at FROM prompts WHERE session_id=?"
countQuery := "SELECT COUNT(*) FROM prompts WHERE session_id=?"
args := []interface{}{sessionID}
countArgs := []interface{}{sessionID}
if keyword != "" {
query += " AND prompt LIKE ?"
countQuery += " AND prompt LIKE ?"
kw := "%" + keyword + "%"
args = append(args, kw)
countArgs = append(countArgs, kw)
}
var total int
db.DB.QueryRow(countQuery, countArgs...).Scan(&total)
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, pageSize, offset)
rows, err := db.DB.Query(query, args...)
if err != nil {
fail(w, 500, "查询 Prompt 列表失败")
return
}
defer rows.Close()
var prompts []map[string]interface{}
for rows.Next() {
var id int64
var sid, pname, prompt, createdAt string
if err := rows.Scan(&id, &sid, &pname, &prompt, &createdAt); err != nil {
continue
}
prompts = append(prompts, map[string]interface{}{
"id": id,
"session_id": sid,
"project": pname,
"prompt": prompt,
"created_at": createdAt,
})
}
if prompts == nil {
prompts = []map[string]interface{}{}
}
success(w, map[string]interface{}{
"prompts": prompts,
"total": total,
"page": page,
"page_size": pageSize,
})
}
+54
View File
@@ -0,0 +1,54 @@
package handlers
import (
"encoding/json"
"net/http"
"prompt-generator/internal/db"
"prompt-generator/internal/models"
)
func GetSettings(w http.ResponseWriter, r *http.Request) {
settings := models.Settings{
LLMAPIBaseURL: cfg.LLMAPIBaseURL,
LLMModelName: cfg.LLMModelName,
}
success(w, settings)
}
func UpdateSettings(w http.ResponseWriter, r *http.Request) {
var req struct {
LLMAPIBaseURL string `json:"llm_api_base_url"`
LLMAPIKey string `json:"llm_api_key"`
LLMModelName string `json:"llm_model_name"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
// Update in-memory config
if req.LLMAPIBaseURL != "" {
cfg.LLMAPIBaseURL = req.LLMAPIBaseURL
}
if req.LLMModelName != "" {
cfg.LLMModelName = req.LLMModelName
}
if req.LLMAPIKey != "" {
cfg.LLMAPIKey = req.LLMAPIKey
}
// Persist to database (use a simple key-value table)
db.DB.Exec(`CREATE TABLE IF NOT EXISTS settings (
key_name varchar(64) NOT NULL PRIMARY KEY,
value text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`)
db.DB.Exec("REPLACE INTO settings (key_name, value) VALUES (?, ?)", "llm_api_base_url", cfg.LLMAPIBaseURL)
db.DB.Exec("REPLACE INTO settings (key_name, value) VALUES (?, ?)", "llm_model_name", cfg.LLMModelName)
if req.LLMAPIKey != "" {
db.DB.Exec("REPLACE INTO settings (key_name, value) VALUES (?, ?)", "llm_api_key", cfg.LLMAPIKey)
}
success(w, nil)
}
+115
View File
@@ -0,0 +1,115 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"prompt-generator/internal/db"
"prompt-generator/internal/models"
)
func GetSnippets(w http.ResponseWriter, r *http.Request) {
category := r.URL.Query().Get("category")
query := "SELECT id, name, content, category, scope, created_at FROM snippets"
var args []interface{}
if category != "" {
query += " WHERE category=?"
args = append(args, category)
}
query += " ORDER BY scope, category, id"
rows, err := db.DB.Query(query, args...)
if err != nil {
fail(w, 500, "查询片段失败")
return
}
defer rows.Close()
var snippets []models.Snippet
for rows.Next() {
var s models.Snippet
if err := rows.Scan(&s.ID, &s.Name, &s.Content, &s.Category, &s.Scope, &s.CreatedAt); err != nil {
continue
}
snippets = append(snippets, s)
}
if snippets == nil {
snippets = []models.Snippet{}
}
success(w, snippets)
}
func CreateSnippet(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Content string `json:"content"`
Category string `json:"category"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
fail(w, 400, "片段名称不能为空")
return
}
res, err := db.DB.Exec("INSERT INTO snippets (name, content, category, scope) VALUES (?, ?, ?, 'personal')",
req.Name, req.Content, req.Category)
if err != nil {
fail(w, 500, "创建片段失败")
return
}
id, _ := res.LastInsertId()
success(w, map[string]int64{"id": id})
}
func UpdateSnippet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var scope string
db.DB.QueryRow("SELECT scope FROM snippets WHERE id=?", id).Scan(&scope)
if scope != "personal" {
fail(w, 403, "不能修改系统片段")
return
}
var req struct {
Name string `json:"name"`
Content string `json:"content"`
Category string `json:"category"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
_, err = db.DB.Exec("UPDATE snippets SET name=?, content=?, category=? WHERE id=?",
req.Name, req.Content, req.Category, id)
if err != nil {
fail(w, 500, "更新片段失败")
return
}
success(w, nil)
}
func DeleteSnippet(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var scope string
db.DB.QueryRow("SELECT scope FROM snippets WHERE id=?", id).Scan(&scope)
if scope != "personal" {
fail(w, 403, "不能删除系统片段")
return
}
db.DB.Exec("DELETE FROM snippets WHERE id=?", id)
success(w, nil)
}
+59
View File
@@ -0,0 +1,59 @@
package handlers
import (
"encoding/json"
"net/http"
"prompt-generator/internal/llm"
"prompt-generator/internal/models"
)
func GetSuggestions(w http.ResponseWriter, r *http.Request) {
var req struct {
CurrentPrompt string `json:"current_prompt"`
ClaudeMD string `json:"claude_md"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
if req.CurrentPrompt == "" {
fail(w, 400, "当前 Prompt 不能为空")
return
}
client := llm.NewClient(cfg.LLMAPIBaseURL, cfg.LLMAPIKey, cfg.LLMModelName)
suggestions, err := client.GetSuggestions(req.CurrentPrompt, req.ClaudeMD)
if err != nil {
fail(w, 500, "获取建议失败: "+err.Error())
return
}
success(w, suggestions)
}
// parseSuggestions 尝试从 LLM 响应中解析建议列表
func parseSuggestions(data string) []models.Suggestion {
// Try to find JSON array in the response
start := -1
end := -1
for i := 0; i < len(data); i++ {
if data[i] == '[' && start == -1 {
start = i
}
if data[i] == ']' {
end = i + 1
}
}
if start == -1 || end == -1 {
return nil
}
var suggestions []models.Suggestion
if err := json.Unmarshal([]byte(data[start:end]), &suggestions); err != nil {
return nil
}
return suggestions
}
+182
View File
@@ -0,0 +1,182 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"prompt-generator/internal/db"
"prompt-generator/internal/models"
)
func GetTags(w http.ResponseWriter, r *http.Request) {
rows, err := db.DB.Query("SELECT id, name, description, scope, created_at FROM tags ORDER BY scope, id")
if err != nil {
fail(w, 500, "查询标签失败")
return
}
defer rows.Close()
var tags []models.Tag
for rows.Next() {
var t models.Tag
if err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.Scope, &t.CreatedAt); err != nil {
continue
}
// Load options
optRows, err := db.DB.Query("SELECT id, tag_id, label, constraint_text, sort_order, created_at FROM tag_options WHERE tag_id=? ORDER BY sort_order", t.ID)
if err == nil {
for optRows.Next() {
var o models.TagOption
if err := optRows.Scan(&o.ID, &o.TagID, &o.Label, &o.ConstraintText, &o.SortOrder, &o.CreatedAt); err == nil {
t.Options = append(t.Options, o)
}
}
optRows.Close()
}
tags = append(tags, t)
}
if tags == nil {
tags = []models.Tag{}
}
success(w, tags)
}
func CreateTag(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
fail(w, 400, "标签名称不能为空")
return
}
res, err := db.DB.Exec("INSERT INTO tags (name, description, scope) VALUES (?, ?, 'personal')", req.Name, req.Description)
if err != nil {
fail(w, 500, "创建标签失败")
return
}
id, _ := res.LastInsertId()
success(w, map[string]int64{"id": id})
}
func UpdateTag(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
// Check ownership
var scope string
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
if scope != "personal" {
fail(w, 403, "不能修改系统标签")
return
}
var req struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
_, err = db.DB.Exec("UPDATE tags SET name=?, description=? WHERE id=?", req.Name, req.Description, id)
if err != nil {
fail(w, 500, "更新标签失败")
return
}
success(w, nil)
}
func DeleteTag(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var scope string
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
if scope != "personal" {
fail(w, 403, "不能删除系统标签")
return
}
// Delete options first
db.DB.Exec("DELETE FROM tag_options WHERE tag_id=?", id)
db.DB.Exec("DELETE FROM tags WHERE id=?", id)
success(w, nil)
}
func CreateTagOption(w http.ResponseWriter, r *http.Request) {
tagID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的标签 ID")
return
}
var req struct {
Label string `json:"label"`
ConstraintText string `json:"constraint_text"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Label == "" {
fail(w, 400, "选项名称不能为空")
return
}
// Get max sort order
var maxOrder int
db.DB.QueryRow("SELECT COALESCE(MAX(sort_order),0) FROM tag_options WHERE tag_id=?", tagID).Scan(&maxOrder)
res, err := db.DB.Exec("INSERT INTO tag_options (tag_id, label, constraint_text, sort_order) VALUES (?, ?, ?, ?)",
tagID, req.Label, req.ConstraintText, maxOrder+1)
if err != nil {
fail(w, 500, "创建选项失败")
return
}
id, _ := res.LastInsertId()
success(w, map[string]int64{"id": id})
}
func UpdateTagOption(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
var req struct {
Label string `json:"label"`
ConstraintText string `json:"constraint_text"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
fail(w, 400, "请求格式错误")
return
}
_, err = db.DB.Exec("UPDATE tag_options SET label=?, constraint_text=? WHERE id=?", req.Label, req.ConstraintText, id)
if err != nil {
fail(w, 500, "更新选项失败")
return
}
success(w, nil)
}
func DeleteTagOption(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, 400, "无效的 ID")
return
}
db.DB.Exec("DELETE FROM tag_options WHERE id=?", id)
success(w, nil)
}
+147
View File
@@ -0,0 +1,147 @@
package llm
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"prompt-generator/internal/models"
)
type Client struct {
BaseURL string
APIKey string
Model string
}
func NewClient(baseURL, apiKey, model string) *Client {
return &Client{
BaseURL: baseURL,
APIKey: apiKey,
Model: model,
}
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
}
type chatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
func (c *Client) GetSuggestions(currentPrompt string, claudeMD string) ([]models.Suggestion, error) {
systemPrompt := `你是一个 Prompt 工程专家。用户正在构建一个用于 Coding Agent 的 Prompt。
请分析当前 Prompt,找出缺失的关键约束或可以改进的地方。
以 JSON 数组返回建议,每条建议包含:
- title: 建议标题
- description: 详细说明
- constraint_text: 建议注入的约束文本
只返回 JSON 数组,不要有其他内容。`
userContent := fmt.Sprintf("当前 Prompt 内容如下:\n---\n%s\n---", currentPrompt)
if claudeMD != "" {
userContent += fmt.Sprintf("\n\n用户的 CLAUDE.md 内容:\n---\n%s\n---", claudeMD)
}
reqBody := chatRequest{
Model: c.Model,
Messages: []chatMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userContent},
},
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %w", err)
}
url := strings.TrimRight(c.BaseURL, "/") + "/chat/completions"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.APIKey)
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("调用 LLM API 失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("LLM API 返回错误 (%d): %s", resp.StatusCode, string(body))
}
var chatResp chatResponse
if err := json.Unmarshal(body, &chatResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
if chatResp.Error != nil {
return nil, fmt.Errorf("LLM 错误: %s", chatResp.Error.Message)
}
if len(chatResp.Choices) == 0 {
return nil, fmt.Errorf("LLM 返回空结果")
}
content := chatResp.Choices[0].Message.Content
suggestions := parseSuggestions(content)
if suggestions == nil {
return nil, fmt.Errorf("AI 返回格式异常,请重试")
}
return suggestions, nil
}
func parseSuggestions(data string) []models.Suggestion {
// Try to find JSON array in the response
start := -1
end := -1
for i := 0; i < len(data); i++ {
if data[i] == '[' && start == -1 {
start = i
}
if data[i] == ']' {
end = i + 1
}
}
if start == -1 || end == -1 {
return nil
}
var suggestions []models.Suggestion
if err := json.Unmarshal([]byte(data[start:end]), &suggestions); err != nil {
return nil
}
return suggestions
}
+91
View File
@@ -0,0 +1,91 @@
package models
import "time"
// Tag 标签定义
type Tag struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Scope string `json:"scope"` // system or personal
CreatedAt time.Time `json:"created_at"`
Options []TagOption `json:"options,omitempty"`
}
// TagOption 标签选项
type TagOption struct {
ID int64 `json:"id"`
TagID int64 `json:"tag_id"`
Label string `json:"label"`
ConstraintText string `json:"constraint_text"`
SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
}
// Snippet 片段模板
type Snippet struct {
ID int64 `json:"id"`
Name string `json:"name"`
Content string `json:"content"`
Category string `json:"category"`
Scope string `json:"scope"`
CreatedAt time.Time `json:"created_at"`
}
// BuilderSession 构建会话
type BuilderSession struct {
ID int64 `json:"id"`
Title string `json:"title"`
ProjectName string `json:"project_name"`
FinalPrompt string `json:"final_prompt"`
ClaudeSessionID *string `json:"claude_session_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Tags []SessionTag `json:"tags,omitempty"`
Snippets []SessionSnippet `json:"snippets,omitempty"`
}
// SessionTag 构建会话标签关联
type SessionTag struct {
ID int64 `json:"id"`
BuilderSessionID int64 `json:"builder_session_id"`
TagID int64 `json:"tag_id"`
TagOptionID int64 `json:"tag_option_id"`
}
// SessionSnippet 构建会话片段关联
type SessionSnippet struct {
ID int64 `json:"id"`
BuilderSessionID int64 `json:"builder_session_id"`
SnippetID int64 `json:"snippet_id"`
SortOrder int `json:"sort_order"`
}
// Prompt prompts 表记录(只读)
type Prompt struct {
ID int64 `json:"id"`
SessionID string `json:"session_id"`
ProjectName string `json:"project_name"`
Prompt string `json:"prompt"`
CreatedAt time.Time `json:"created_at"`
}
// Suggestion 智能建议
type Suggestion struct {
Title string `json:"title"`
Description string `json:"description"`
ConstraintText string `json:"constraint_text"`
}
// Settings 系统设置
type Settings struct {
LLMAPIBaseURL string `json:"llm_api_base_url"`
LLMModelName string `json:"llm_model_name"`
}
// APIResponse 统一响应
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
+133
View File
@@ -0,0 +1,133 @@
package main
import (
"log"
"net/http"
"os"
"prompt-generator/internal/auth"
"prompt-generator/internal/config"
"prompt-generator/internal/db"
"prompt-generator/internal/handlers"
)
func main() {
// Load .env file if exists
loadEnvFile(".env")
cfg := config.Load()
// Initialize database
if err := db.Init(cfg); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
// Auto migrate
if err := db.AutoMigrate(); err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// Seed data
if err := db.SeedData(); err != nil {
log.Fatalf("Failed to seed data: %v", err)
}
// Initialize auth
auth.Init(cfg.SessionSecret)
// Initialize handlers
handlers.Init(cfg)
// Setup routes
mux := http.NewServeMux()
// Static files (frontend)
fs := http.FileServer(http.Dir("frontend"))
mux.Handle("/", fs)
// Auth routes (no auth required)
mux.HandleFunc("POST /api/auth/login", handlers.Login)
mux.HandleFunc("POST /api/auth/logout", handlers.Logout)
mux.HandleFunc("GET /api/auth/check", handlers.AuthCheck)
// Protected API routes
apiMux := http.NewServeMux()
apiMux.HandleFunc("GET /api/tags", handlers.GetTags)
apiMux.HandleFunc("POST /api/tags", handlers.CreateTag)
apiMux.HandleFunc("PUT /api/tags/{id}", handlers.UpdateTag)
apiMux.HandleFunc("DELETE /api/tags/{id}", handlers.DeleteTag)
apiMux.HandleFunc("POST /api/tags/{id}/options", handlers.CreateTagOption)
apiMux.HandleFunc("PUT /api/tag-options/{id}", handlers.UpdateTagOption)
apiMux.HandleFunc("DELETE /api/tag-options/{id}", handlers.DeleteTagOption)
apiMux.HandleFunc("GET /api/snippets", handlers.GetSnippets)
apiMux.HandleFunc("POST /api/snippets", handlers.CreateSnippet)
apiMux.HandleFunc("PUT /api/snippets/{id}", handlers.UpdateSnippet)
apiMux.HandleFunc("DELETE /api/snippets/{id}", handlers.DeleteSnippet)
apiMux.HandleFunc("GET /api/builder/sessions", handlers.GetBuilderSessions)
apiMux.HandleFunc("POST /api/builder/sessions", handlers.CreateBuilderSession)
apiMux.HandleFunc("GET /api/builder/sessions/{id}", handlers.GetBuilderSession)
apiMux.HandleFunc("PUT /api/builder/sessions/{id}", handlers.UpdateBuilderSession)
apiMux.HandleFunc("DELETE /api/builder/sessions/{id}", handlers.DeleteBuilderSession)
apiMux.HandleFunc("GET /api/claude/sessions", handlers.GetClaudeSessions)
apiMux.HandleFunc("GET /api/claude/sessions/{session_id}/prompts", handlers.GetClaudePrompts)
apiMux.HandleFunc("GET /api/dashboard/projects", handlers.GetDashboardProjects)
apiMux.HandleFunc("GET /api/dashboard/projects/{name}/sessions", handlers.GetDashboardSessions)
apiMux.HandleFunc("GET /api/dashboard/prompts", handlers.GetDashboardPrompts)
apiMux.HandleFunc("POST /api/suggestions", handlers.GetSuggestions)
apiMux.HandleFunc("GET /api/settings", handlers.GetSettings)
apiMux.HandleFunc("PUT /api/settings", handlers.UpdateSettings)
// Wrap API routes with auth middleware
mux.Handle("/api/", auth.AuthMiddleware(apiMux))
log.Printf("Server starting on :%s", cfg.ServerPort)
if err := http.ListenAndServe(":"+cfg.ServerPort, mux); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
func loadEnvFile(path string) {
data, err := os.ReadFile(path)
if err != nil {
return // .env file is optional
}
lines := splitLines(string(data))
for _, line := range lines {
if len(line) == 0 || line[0] == '#' {
continue
}
for i := 0; i < len(line); i++ {
if line[i] == '=' {
key := line[:i]
value := line[i+1:]
// Remove quotes
if len(value) >= 2 && (value[0] == '"' && value[len(value)-1] == '"') {
value = value[1 : len(value)-1]
}
os.Setenv(key, value)
break
}
}
}
}
func splitLines(s string) []string {
var lines []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '\n' {
lines = append(lines, s[start:i])
start = i + 1
}
}
if start < len(s) {
lines = append(lines, s[start:])
}
return lines
}