This commit is contained in:
commit
5e4b42493d
|
|
@ -0,0 +1,2 @@
|
|||
/.idea
|
||||
/outputs
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# ============================================================
|
||||
# 阶段1: 构建
|
||||
# ============================================================
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git make
|
||||
|
||||
# 复制 go.mod 和 go.sum 并下载依赖(利用 Docker 缓存)
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download && go mod verify
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建二进制文件
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||
-ldflags="-s -w" \
|
||||
-o /app/sdk-generator \
|
||||
./cmd/server
|
||||
|
||||
# ============================================================
|
||||
# 阶段2: 运行
|
||||
# ============================================================
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN apk add --no-cache ca-certificates tzdata git
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN adduser -D -g '' appuser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/sdk-generator /app/sdk-generator
|
||||
|
||||
# 复制 Web 静态文件和模板
|
||||
COPY --chown=appuser:appuser ./web /app/web
|
||||
|
||||
# 创建数据目录并设置权限
|
||||
RUN mkdir -p /app/outputs /app/uploads && \
|
||||
chown -R appuser:appuser /app
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5002
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动
|
||||
ENTRYPOINT ["/app/sdk-generator"]
|
||||
|
||||
|
||||
#docker run -d \
|
||||
# -p 8080:8080 \
|
||||
# -v /var/www/sdk-generator/outputs:/app/outputs \
|
||||
# --name sdk-generator \
|
||||
# sdk-generator
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
module sdk-generator
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
code.gitea.io/sdk/gitea v0.25.1
|
||||
github.com/gofiber/fiber/v2 v2.52.13
|
||||
github.com/gofiber/template/html/v2 v2.1.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/sashabaranov/go-openai v1.28.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/42wim/httpsig v1.2.4 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/gofiber/template v1.8.3 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
code.gitea.io/sdk/gitea v0.25.1 h1:yywxWwoV+SdjHtbC6unBiXojWdZOtoHuGhEazEXeWuE=
|
||||
code.gitea.io/sdk/gitea v0.25.1/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg=
|
||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
|
||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk=
|
||||
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
|
||||
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
|
||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sashabaranov/go-openai v1.28.0 h1:WS9F9BriSvtHvknPQy2Oi3b+8zkmJdEXcycrWqrSicQ=
|
||||
github.com/sashabaranov/go-openai v1.28.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package call
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
type LlmCallSet struct {
|
||||
ApiKey string
|
||||
BaseUrl string
|
||||
ModelName string
|
||||
}
|
||||
|
||||
type CallLLM struct {
|
||||
ModelName string
|
||||
openaiClient *openai.Client
|
||||
}
|
||||
|
||||
func NewCallLLM(LlmCallSet *LlmCallSet) *CallLLM {
|
||||
clientConfig := openai.DefaultConfig(LlmCallSet.ApiKey)
|
||||
clientConfig.BaseURL = LlmCallSet.BaseUrl
|
||||
client := openai.NewClientWithConfig(clientConfig)
|
||||
return &CallLLM{
|
||||
openaiClient: client,
|
||||
ModelName: LlmCallSet.ModelName,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CallLLM) Do(ctx context.Context, RequestOption openai.ChatCompletionRequest, modelName ...string) (string, error) {
|
||||
if modelName == nil {
|
||||
modelName = append(modelName, s.ModelName)
|
||||
}
|
||||
if modelName[0] == "" {
|
||||
return "", fmt.Errorf("model name is nil")
|
||||
}
|
||||
RequestOption.Model = modelName[0]
|
||||
resp, err := s.openaiClient.CreateChatCompletion(ctx, RequestOption)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(resp.Choices) == 0 {
|
||||
return "", fmt.Errorf("模型未返回任何内容")
|
||||
}
|
||||
content := resp.Choices[0].Message.Content
|
||||
//content := test.Res
|
||||
return content, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// 服务配置
|
||||
Port string
|
||||
UploadDir string
|
||||
OutputDir string
|
||||
|
||||
// 大模型配置
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Model string
|
||||
MaxFixAttempts int
|
||||
Timeout time.Duration
|
||||
|
||||
// 限流配置
|
||||
MaxFileSize int64
|
||||
MaxTasks int
|
||||
|
||||
//gitea配置
|
||||
GiteaUrl string
|
||||
GiteaToken string
|
||||
OrgName string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Port: getEnv("PORT", "5002"),
|
||||
UploadDir: getEnv("UPLOAD_DIR", "./uploads"),
|
||||
OutputDir: getEnv("OUTPUT_DIR", "./outputs"),
|
||||
APIKey: getEnv("DOUBAO_API_KEY", "ark-3f942f9b-7428-4880-b939-026087b99957-062f1"),
|
||||
BaseURL: getEnv("DOUBAO_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"),
|
||||
Model: getEnv("DOUBAO_MODEL", "ep-20260717134040-w9qhc"),
|
||||
Timeout: 5 * time.Minute,
|
||||
MaxFileSize: 10 * 1024 * 1024, // 10MB
|
||||
MaxTasks: 100,
|
||||
MaxFixAttempts: 3,
|
||||
GiteaUrl: "https://gitea.cdlsxd.cn",
|
||||
GiteaToken: "406ebea224965f9be4c25e7ff2614588827fd31f",
|
||||
OrgName: "ai_sdk",
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultVal string) string {
|
||||
if val := os.Getenv(key); val != "" {
|
||||
return val
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package extractor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Path string
|
||||
Content string
|
||||
}
|
||||
|
||||
// Extract 从模型响应中提取代码文件
|
||||
// 使用纯字符串解析,不依赖 goldmark AST
|
||||
func Extract(response string) ([]File, error) {
|
||||
var files []File
|
||||
seen := make(map[string]bool)
|
||||
|
||||
// 按 // File: 分割
|
||||
parts := strings.Split(response, "// File:")
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 第一行是文件名
|
||||
lines := strings.SplitN(part, "\n", 2)
|
||||
if len(lines) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := strings.TrimSpace(lines[0])
|
||||
code := strings.TrimSpace(lines[1])
|
||||
|
||||
if fileName == "" || code == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 清理代码块标记(移除 ```go 或 ```)
|
||||
code = strings.TrimPrefix(code, "```go")
|
||||
code = strings.TrimPrefix(code, "```")
|
||||
code = strings.TrimSuffix(code, "```")
|
||||
code = strings.TrimSpace(code)
|
||||
|
||||
// 智能识别文件类型
|
||||
fileName = smartFileName(fileName, code)
|
||||
|
||||
// 规范化路径
|
||||
fullPath := normalizePath(fileName)
|
||||
|
||||
// 去重
|
||||
if seen[fullPath] {
|
||||
continue
|
||||
}
|
||||
seen[fullPath] = true
|
||||
|
||||
files = append(files, File{
|
||||
Path: fullPath,
|
||||
Content: code,
|
||||
})
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf("未找到任何代码块,请确保响应格式为:// File: 文件名\n```go\n代码\n```")
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// smartFileName 智能识别文件名
|
||||
func smartFileName(fileName, code string) string {
|
||||
fileName = strings.TrimSpace(fileName)
|
||||
fileName = strings.TrimLeft(fileName, "0123456789.- ")
|
||||
|
||||
// 如果文件名是 code.go 或类似通用名,根据内容推断
|
||||
base := filepath.Base(fileName)
|
||||
if base == "code.go" || base == "code" || base == "main.go" {
|
||||
// 根据内容推断
|
||||
if strings.Contains(code, "package sdk_test") {
|
||||
return "example_test.go"
|
||||
}
|
||||
if strings.Contains(code, "module ") {
|
||||
return "go.mod"
|
||||
}
|
||||
if strings.Contains(code, "package sdk") && strings.Contains(code, "func (c *Client)") {
|
||||
// 检查是哪个 API 文件
|
||||
if strings.Contains(code, "CreateKeyOrder") || strings.Contains(code, "OrderKey") {
|
||||
return "api_key.go"
|
||||
}
|
||||
if strings.Contains(code, "BatchOrder") || strings.Contains(code, "BatchQuery") {
|
||||
return "api_batch.go"
|
||||
}
|
||||
if strings.Contains(code, "QueryKey") || strings.Contains(code, "DiscardKey") {
|
||||
return "api_key.go"
|
||||
}
|
||||
}
|
||||
if strings.Contains(code, "package sdk") && strings.Contains(code, "type Client struct") {
|
||||
return "client.go"
|
||||
}
|
||||
if strings.Contains(code, "package sdk") && (strings.Contains(code, "type APIError") || strings.Contains(code, "CodeSuccess")) {
|
||||
return "errors.go"
|
||||
}
|
||||
if strings.Contains(code, "package sdk") && (strings.Contains(code, "ParseRSAPrivateKey") || strings.Contains(code, "AESEncryptECB")) {
|
||||
return "crypto.go"
|
||||
}
|
||||
if strings.Contains(code, "package sdk") && (strings.Contains(code, "type RequestEnvelope") || strings.Contains(code, "type KeyOrderInfo")) {
|
||||
return "types.go"
|
||||
}
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
path = strings.TrimSpace(path)
|
||||
path = strings.TrimLeft(path, "0123456789.- ")
|
||||
|
||||
// 确保文件扩展名正确
|
||||
if !strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, ".mod") {
|
||||
// 如果是 go.mod 或 go.work,保持原样
|
||||
if path != "go.mod" && path != "go.work" {
|
||||
path = path + ".go"
|
||||
}
|
||||
}
|
||||
|
||||
// 确保路径以 sdk/ 开头
|
||||
//if !strings.HasPrefix(path, "sdk/") && !strings.HasPrefix(path, "output/") {
|
||||
// path = filepath.Join("sdk", path)
|
||||
//}
|
||||
return path
|
||||
}
|
||||
|
||||
// WriteFiles 将文件写入本地
|
||||
func WriteFiles(files []File, outputDir string) error {
|
||||
for _, f := range files {
|
||||
fullPath := filepath.Join(outputDir, f.Path)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
|
||||
return fmt.Errorf("创建目录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(fullPath, []byte(f.Content), 0644); err != nil {
|
||||
return fmt.Errorf("写入文件失败: %w", fullPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConvertResponse 定义接口响应结构
|
||||
type ConvertResponse struct {
|
||||
Filename string `json:"filename"`
|
||||
LlmUsed bool `json:"llm_used"`
|
||||
Markdown string `json:"markdown"`
|
||||
Size int `json:"size"`
|
||||
Success bool `json:"success"`
|
||||
Title *string `json:"title"`
|
||||
}
|
||||
|
||||
// ConvertRequest 定义请求参数结构
|
||||
type ConvertRequest struct {
|
||||
File multipart.File
|
||||
FileName string
|
||||
LlmModel string
|
||||
LlmApiKey string
|
||||
LlmBaseUrl string
|
||||
LlmPrompt string
|
||||
}
|
||||
|
||||
const url = "http://121.199.38.107:5001/convert"
|
||||
|
||||
// ConvertFile 调用 /convert 接口识别本地文件内容
|
||||
func ConvertFile(req ConvertRequest) (*ConvertResponse, error) {
|
||||
defer req.File.Close()
|
||||
// 创建 multipart 表单
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
// 添加文件字段
|
||||
part, err := writer.CreateFormFile("file", req.FileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件表单字段失败: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(part, req.File); err != nil {
|
||||
return nil, fmt.Errorf("复制文件内容失败: %w", err)
|
||||
}
|
||||
// 添加可选参数(如果提供)
|
||||
if req.LlmModel != "" {
|
||||
_ = writer.WriteField("llm_model", req.LlmModel)
|
||||
}
|
||||
if req.LlmApiKey != "" {
|
||||
_ = writer.WriteField("llm_api_key", req.LlmApiKey)
|
||||
}
|
||||
if req.LlmBaseUrl != "" {
|
||||
_ = writer.WriteField("llm_base_url", req.LlmBaseUrl)
|
||||
}
|
||||
if req.LlmPrompt != "" {
|
||||
_ = writer.WriteField("llm_prompt", req.LlmPrompt)
|
||||
}
|
||||
|
||||
// 关闭 multipart writer
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("关闭表单写入器失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建 HTTP 请求
|
||||
|
||||
httpReq, err := http.NewRequest("POST", url, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建HTTP请求失败: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("请求返回非200状态码: %d, 响应: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// 解析响应 JSON
|
||||
var result ConvertResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应JSON失败: %w, 原始响应: %s", err, string(respBody))
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type PageHandler struct{}
|
||||
|
||||
func NewPageHandler() *PageHandler {
|
||||
return &PageHandler{}
|
||||
}
|
||||
|
||||
// Index 首页
|
||||
func (h *PageHandler) Index(c *fiber.Ctx) error {
|
||||
return c.SendFile("./web/templates/index.html")
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sdk-generator/internal/call"
|
||||
"sdk-generator/internal/models"
|
||||
"sdk-generator/internal/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type SDKHandler struct {
|
||||
service *service.SDKGeneratorService
|
||||
}
|
||||
|
||||
func NewSDKHandler(service *service.SDKGeneratorService) *SDKHandler {
|
||||
return &SDKHandler{service: service}
|
||||
}
|
||||
|
||||
// GenerateSDK 上传文档并生成 SDK
|
||||
// POST /api/v1/generate
|
||||
func (h *SDKHandler) GenerateSDK(c *fiber.Ctx) error {
|
||||
//获取上传文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "请上传文件 (field: file)",
|
||||
})
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if file.Size > 10*1024*1024 {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "文件大小超过 10MB 限制",
|
||||
})
|
||||
}
|
||||
var content []byte
|
||||
// 检查文件类型
|
||||
filename := file.Filename
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "打开文件失败",
|
||||
})
|
||||
}
|
||||
llmSet := call.LlmCallSet{
|
||||
ApiKey: c.FormValue("llm_api_key", ""),
|
||||
BaseUrl: c.FormValue("llm_base_url", ""),
|
||||
ModelName: c.FormValue("llm_model", ""),
|
||||
}
|
||||
if llmSet.ModelName == "" || llmSet.ApiKey == "" || llmSet.BaseUrl == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "缺少 llm_model 或 llm_api_key 或 llm_base_url",
|
||||
})
|
||||
}
|
||||
defer src.Close()
|
||||
switch ext {
|
||||
case ".md", ".txt", ".markdown":
|
||||
// 读取文件内容
|
||||
content, err = io.ReadAll(src)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "读取文件失败",
|
||||
})
|
||||
}
|
||||
break
|
||||
case ".docx", ".doc", ".pdf":
|
||||
res, err := ConvertFile(ConvertRequest{
|
||||
File: src,
|
||||
FileName: filename,
|
||||
LlmModel: llmSet.ModelName,
|
||||
LlmApiKey: llmSet.ApiKey,
|
||||
LlmBaseUrl: llmSet.BaseUrl,
|
||||
//LlmPrompt: c.FormValue("llm_prompt", ""),
|
||||
})
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "读取文件失败" + err.Error(),
|
||||
})
|
||||
}
|
||||
content = []byte(res.Markdown)
|
||||
break
|
||||
default:
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "不支持的文件类型"})
|
||||
}
|
||||
|
||||
sdkName := c.FormValue("sdk_name", time.Now().Format(time.RFC3339))
|
||||
// 调用服务生成
|
||||
taskID, err := h.service.Generate(c.Context(), content, sdkName, &llmSet)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusAccepted).JSON(models.GenerateResponse{
|
||||
TaskID: taskID,
|
||||
Status: string(models.StatusPending),
|
||||
Message: "SDK 生成任务已提交,请通过 /api/v1/tasks/:task_id 查询状态",
|
||||
})
|
||||
}
|
||||
|
||||
// GetTaskStatus 查询任务状态
|
||||
// GET /api/v1/tasks/:task_id
|
||||
func (h *SDKHandler) GetTaskStatus(c *fiber.Ctx) error {
|
||||
taskID := c.Params("task_id")
|
||||
if taskID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "缺少 task_id",
|
||||
})
|
||||
}
|
||||
|
||||
task, ok := h.service.GetTask(taskID)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"error": "任务不存在",
|
||||
})
|
||||
}
|
||||
task.StatusName = task.Status.Desc()
|
||||
task.Percent = task.Status.Percent()
|
||||
response := models.TaskResponse{
|
||||
Task: *task,
|
||||
}
|
||||
|
||||
// 如果任务完成,列出生成的文件
|
||||
if task.Status == models.StatusCompleted {
|
||||
files, _ := listFiles(task.OutputDir)
|
||||
response.Files = files
|
||||
}
|
||||
|
||||
return c.JSON(response)
|
||||
}
|
||||
|
||||
// DownloadSDK 下载生成的 SDK
|
||||
// GET /api/v1/tasks/:task_id/download
|
||||
func (h *SDKHandler) DownloadSDK(c *fiber.Ctx) error {
|
||||
taskID := c.Params("task_id")
|
||||
if taskID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "缺少 task_id",
|
||||
})
|
||||
}
|
||||
|
||||
task, ok := h.service.GetTask(taskID)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"error": "任务不存在",
|
||||
})
|
||||
}
|
||||
|
||||
if task.Status != models.StatusCompleted {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": fmt.Sprintf("任务未完成,当前状态: %s", task.Status),
|
||||
})
|
||||
}
|
||||
|
||||
// 检查输出目录是否存在
|
||||
if _, err := os.Stat(task.OutputDir); os.IsNotExist(err) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"error": "SDK 文件不存在",
|
||||
})
|
||||
}
|
||||
|
||||
// 创建 ZIP 文件并流式返回
|
||||
zipFilename := fmt.Sprintf("sdk_%s.zip", taskID)
|
||||
c.Set("Content-Type", "application/zip")
|
||||
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipFilename))
|
||||
|
||||
// 使用 Fiber 的 Response 直接写入
|
||||
zipWriter := zip.NewWriter(c.Response().BodyWriter())
|
||||
defer zipWriter.Close()
|
||||
|
||||
// 遍历目录添加文件到 ZIP
|
||||
err := filepath.Walk(task.OutputDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算相对路径
|
||||
relPath, err := filepath.Rel(task.OutputDir, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建 ZIP 条目
|
||||
zipFile, err := zipWriter.Create(relPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = zipFile.Write(content)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": fmt.Sprintf("打包 SDK 失败: %v", err),
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTasks 列出所有任务
|
||||
// GET /api/v1/tasks
|
||||
func (h *SDKHandler) ListTasks(c *fiber.Ctx) error {
|
||||
tasks := h.service.ListTasks()
|
||||
|
||||
// 分页
|
||||
page, _ := strconv.Atoi(c.Query("page", "1"))
|
||||
size, _ := strconv.Atoi(c.Query("size", "20"))
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 {
|
||||
size = 20
|
||||
}
|
||||
if size > 100 {
|
||||
size = 100
|
||||
}
|
||||
|
||||
total := len(tasks)
|
||||
start := (page - 1) * size
|
||||
end := start + size
|
||||
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
return c.JSON(models.ListTasksResponse{
|
||||
Data: tasks[start:end],
|
||||
Total: total,
|
||||
Page: page,
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务
|
||||
// DELETE /api/v1/tasks/:task_id
|
||||
func (h *SDKHandler) DeleteTask(c *fiber.Ctx) error {
|
||||
taskID := c.Params("task_id")
|
||||
if taskID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "缺少 task_id",
|
||||
})
|
||||
}
|
||||
|
||||
if err := h.service.DeleteTask(taskID); err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"message": "任务已删除",
|
||||
})
|
||||
}
|
||||
|
||||
// 辅助函数:列出目录下的文件
|
||||
func listFiles(dir string) ([]string, error) {
|
||||
var files []string
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
relPath, _ := filepath.Rel(dir, path)
|
||||
files = append(files, relPath)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files, err
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"sdk-generator/internal/call"
|
||||
"sdk-generator/internal/extractor"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TaskStatus string
|
||||
|
||||
const (
|
||||
StatusPending TaskStatus = "pending"
|
||||
StatusProcessing TaskStatus = "processing"
|
||||
StatusMkdir TaskStatus = "mkdir"
|
||||
StatusAnaMd TaskStatus = "anaMd"
|
||||
StatusGenerateCode TaskStatus = "generateCode"
|
||||
StatusCreatFile TaskStatus = "creatFile"
|
||||
StatusValid TaskStatus = "valid"
|
||||
StatusProcess TaskStatus = "process"
|
||||
StatusFix TaskStatus = "fix"
|
||||
StatusSendToGit TaskStatus = "sendToGit "
|
||||
StatusMsgSend TaskStatus = "msgSend"
|
||||
StatusCompleted TaskStatus = "completed"
|
||||
StatusFailed TaskStatus = "failed"
|
||||
)
|
||||
|
||||
func (s TaskStatus) Desc() string {
|
||||
return StatusName[s]
|
||||
}
|
||||
|
||||
func (s TaskStatus) Percent() int {
|
||||
return StatusPercent[s]
|
||||
}
|
||||
|
||||
var StatusPercent = map[TaskStatus]int{
|
||||
StatusPending: 0,
|
||||
StatusProcessing: 1,
|
||||
StatusMkdir: 10,
|
||||
StatusAnaMd: 20,
|
||||
StatusGenerateCode: 60,
|
||||
StatusCreatFile: 50,
|
||||
StatusValid: 70,
|
||||
StatusProcess: 62,
|
||||
StatusFix: 80,
|
||||
StatusSendToGit: 85,
|
||||
StatusMsgSend: 95,
|
||||
StatusCompleted: 100,
|
||||
StatusFailed: 100,
|
||||
}
|
||||
|
||||
var StatusName = map[TaskStatus]string{
|
||||
StatusPending: "准备中",
|
||||
StatusProcessing: "开始创建",
|
||||
StatusMkdir: "新建项目文件夹",
|
||||
StatusAnaMd: "分析文档",
|
||||
StatusGenerateCode: "生成代码",
|
||||
StatusCreatFile: "创建代码目录",
|
||||
StatusValid: "验证功能",
|
||||
StatusProcess: "编译检测",
|
||||
StatusFix: "问题修复",
|
||||
StatusSendToGit: "上传代码仓库",
|
||||
StatusMsgSend: "发送钉钉消息",
|
||||
StatusCompleted: "任务完成",
|
||||
StatusFailed: "任务失败",
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
Status TaskStatus `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
Percent int `json:"percent"`
|
||||
SdkName string `json:"sdk_name"`
|
||||
OutputDir string `json:"output_dir"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CallLLM *call.CallLLM
|
||||
SdkPackage string
|
||||
MarkdownContent string
|
||||
Refine string
|
||||
Resp string
|
||||
Valid string
|
||||
Files []extractor.File
|
||||
RepoURL string
|
||||
}
|
||||
|
||||
type GenerateResponse struct {
|
||||
TaskID string `json:"task_id"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type TaskResponse struct {
|
||||
Task
|
||||
Files []string `json:"files,omitempty"`
|
||||
}
|
||||
|
||||
type ListTasksResponse struct {
|
||||
Data []*Task `json:"data"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var MesSendUrl = "https://oapi.dingtalk.com/robot/send?access_token=d8367c13499dfb164b1e02a4c024024f6857a892e7406265be08d5b5e06fc590"
|
||||
|
||||
func SendDingTalkAlert(ctx context.Context, sdkName, url string) {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// ✅ 正确的 link 消息格式
|
||||
message := map[string]interface{}{
|
||||
"msgtype": "link",
|
||||
"link": map[string]interface{}{
|
||||
"title": "[ai_sdk]🎉 " + sdkName + " 代码生成完成,点此进入仓库",
|
||||
"text": url + ".git",
|
||||
"messageUrl": url,
|
||||
"picUrl": "", // 可选:图片 URL
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
log.Printf("JSON序列化失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Post(MesSendUrl, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Printf("发送钉钉告警失败: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("钉钉告警发送结果,状态码: %s,内容:%s", resp.Status, data)
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package msg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendDingTalkAlert(t *testing.T) {
|
||||
// 测试正常发送
|
||||
ctx := context.Background()
|
||||
sdkName := "ymt_v3_generate"
|
||||
url := "https://gitea.cdlsxd.cn/ai_sdk/ymt_v3_generate-20260721-095556"
|
||||
|
||||
SendDingTalkAlert(ctx, sdkName, url)
|
||||
|
||||
t.Log("所有测试完成")
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package postprocess
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Process(ctx context.Context, sdkPackage string) error {
|
||||
// 确保 outputDir 存在
|
||||
if err := os.MkdirAll(sdkPackage, 0755); err != nil {
|
||||
return fmt.Errorf("创建输出目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查是否有 go.mod,没有则初始化
|
||||
goModPath := filepath.Join(sdkPackage, "go.mod")
|
||||
if _, err := os.Stat(goModPath); os.IsNotExist(err) {
|
||||
cmd := exec.Command("go", "mod", "init", sdkPackage)
|
||||
cmd.Dir = sdkPackage
|
||||
if err := cmd.Run(); err != nil {
|
||||
// 忽略错误,可能已经有 go.mod 了
|
||||
}
|
||||
}
|
||||
|
||||
commands := []struct {
|
||||
name string
|
||||
args []string
|
||||
desc string
|
||||
}{
|
||||
{"go", []string{"fmt", "./..."}, "格式化代码"},
|
||||
{"go", []string{"mod", "tidy"}, "整理依赖"},
|
||||
{"go", []string{"build", "./..."}, "编译检查"},
|
||||
}
|
||||
|
||||
var errors []string
|
||||
for _, cmdInfo := range commands {
|
||||
c := exec.Command(cmdInfo.name, cmdInfo.args...)
|
||||
c.Dir = sdkPackage
|
||||
|
||||
var stderr bytes.Buffer
|
||||
c.Stderr = &stderr
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %v\n%s", cmdInfo.desc, err, stderr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("\n%s", strings.Join(errors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// internal/prompts/fix_prompt.go
|
||||
package prompts
|
||||
|
||||
import "github.com/sashabaranov/go-openai"
|
||||
|
||||
func FixPrompt(codeContent, errorMsg, sdkName, refineDoc string) openai.ChatCompletionRequest {
|
||||
return openai.ChatCompletionRequest{
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: "你是一个资深的 Go 语言工程师。生成的 SDK 代码在编译/格式化时出现错误,请修复。",
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: GetFixPrompt(codeContent, errorMsg, sdkName, refineDoc),
|
||||
},
|
||||
},
|
||||
Temperature: 0.1, // 更低,保证确定性输出
|
||||
TopP: 0.9, // 核采样,平衡多样性和质量
|
||||
MaxTokens: 65536, // 加大到 64k,确保足够
|
||||
FrequencyPenalty: 0.0, // 代码生成不需要
|
||||
PresencePenalty: 0.0, // 代码生成不需要
|
||||
Stop: nil, // 不设置,让模型完整输出
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// GetFixPrompt 构建修复代码的提示词
|
||||
func GetFixPrompt(codeContent, errorMsg, refineDoc, sdkName string) string {
|
||||
return `
|
||||
## 错误信息
|
||||
` + errorMsg + `
|
||||
|
||||
## 文档(供参考)
|
||||
` + refineDoc + `
|
||||
|
||||
## 当前代码
|
||||
` + codeContent + `
|
||||
|
||||
## ⚠️ 重要:输出格式要求(必须严格遵守)
|
||||
每个文件必须以以下格式输出:
|
||||
|
||||
// File: ` + sdkName + `/文件名.go
|
||||
` + "```go" + `
|
||||
package ` + sdkName + `
|
||||
// ... 代码内容 ...
|
||||
` + "```" + `
|
||||
|
||||
|
||||
## 修复要求
|
||||
1. 根据错误信息修复代码
|
||||
2. 确保代码符合 Go 语法规范
|
||||
3. 不要改变接口定义和业务逻辑
|
||||
4. 保持原有代码风格
|
||||
5. 请直接输出修复后的完整代码,不要有任何额外文字
|
||||
|
||||
请返回修复后的完整代码。`
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package prompts
|
||||
|
||||
import "github.com/sashabaranov/go-openai"
|
||||
|
||||
func SDKGeneratorPrompt(prompt string) openai.ChatCompletionRequest {
|
||||
return openai.ChatCompletionRequest{
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: "你是一个资深的 Go 语言工程师,擅长生成高质量、可直接编译的 SDK 代码。",
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: prompt,
|
||||
},
|
||||
},
|
||||
Temperature: 0.1, // 更低,保证确定性输出
|
||||
TopP: 0.9, // 核采样,平衡多样性和质量
|
||||
MaxTokens: 65536, // 加大到 64k,确保足够
|
||||
FrequencyPenalty: 0.0, // 代码生成不需要
|
||||
PresencePenalty: 0.0, // 代码生成不需要
|
||||
Stop: nil, // 不设置,让模型完整输出
|
||||
}
|
||||
}
|
||||
|
||||
const GenSDKPrompt = `你是一个资深的 Go 语言工程师,擅长生成高质量的 SDK 代码。
|
||||
|
||||
## 任务
|
||||
根据用户提供的 API 接口文档(Markdown 格式),生成一个完整的 Go SDK 代码工程。
|
||||
|
||||
## ⚠️ 重要:输出格式要求(必须严格遵守)
|
||||
每个文件必须以以下格式输出:
|
||||
|
||||
// File: {{sdk_name}}/文件名.go
|
||||
` + "```go" + `
|
||||
package {{sdk_name}}
|
||||
// ... 代码内容 ...
|
||||
` + "```" + `
|
||||
|
||||
## ⚠️ 重要:导入路径规范(必须遵守)
|
||||
生成的 SDK 代码中,**示例代码(example_test.go)必须使用本地导入路径**,而不是远程 GitHub 路径:
|
||||
|
||||
✅ 正确示例:
|
||||
` + "```go" + `
|
||||
package {{sdk_name}}
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
"net/http"
|
||||
)
|
||||
` + "```" + `
|
||||
|
||||
❌ 错误示例(禁止使用):
|
||||
` + "```go" + `
|
||||
import sdk "github.com/bluebrothers/marketing-sdk-go" // 禁止!这会拉取远程包
|
||||
` + "```" + `
|
||||
|
||||
## 文件结构
|
||||
{{sdk_name}}/
|
||||
├── go.mod # 模块名为 generated-sdk
|
||||
├── client.go
|
||||
├── types.go
|
||||
├── crypto.go
|
||||
├── errors.go
|
||||
├── api_key.go
|
||||
└── example_test.go # 使用本地导入路径
|
||||
|
||||
## 代码规范
|
||||
1. 遵循 Effective Go 规范
|
||||
2. 所有导出类型/方法必须有 godoc 注释
|
||||
3. context.Context 作为方法的第一个参数
|
||||
4. 错误处理统一返回 error
|
||||
|
||||
## 客户端设计
|
||||
1. 提供 NewClient 构造函数,支持 Option 模式
|
||||
2. 支持 WithBaseURL、WithTimeout、WithHTTPClient 等配置
|
||||
3. 根据文档中描述的认证方式实现认证逻辑
|
||||
|
||||
## ⚠️ 加密/签名实现规范(必须遵守)
|
||||
|
||||
**核心原则:使用成熟的第三方库,不要自己实现加解密算法。**
|
||||
|
||||
### 推荐使用的第三方库
|
||||
|
||||
**AES 加密:**
|
||||
` + "```go" + `
|
||||
import "crypto/aes" // Go 标准库
|
||||
import "crypto/cipher" // Go 标准库
|
||||
// 或使用更高级的封装
|
||||
import "github.com/tink-crypto/tink-go/v2/aead"
|
||||
` + "```" + `
|
||||
|
||||
**RSA 加密/签名:**
|
||||
` + "```go" + `
|
||||
import "crypto/rsa" // Go 标准库
|
||||
import "crypto/rand" // Go 标准库
|
||||
` + "```" + `
|
||||
|
||||
**HMAC 签名:**
|
||||
` + "```go" + `
|
||||
import "crypto/hmac"
|
||||
import "crypto/sha256"
|
||||
` + "```" + `
|
||||
|
||||
**SM2/SM3/SM4(国密):**
|
||||
` + "```go" + `
|
||||
import "github.com/tjfoc/gmsm/sm2"
|
||||
import "github.com/tjfoc/gmsm/sm3"
|
||||
import "github.com/tjfoc/gmsm/sm4"
|
||||
` + "```" + `
|
||||
|
||||
**JWT:**
|
||||
` + "```go" + `
|
||||
import "github.com/golang-jwt/jwt/v5"
|
||||
` + "```" + `
|
||||
|
||||
### 禁止的行为
|
||||
|
||||
❌ **不要自己实现**:
|
||||
- 不要自己写 AES 加密函数(除非调用标准库)
|
||||
- 不要自己写 RSA 签名函数(除非调用标准库)
|
||||
- 不要自己实现 PKCS7 填充(使用标准库或成熟库)
|
||||
- 不要自己实现任何加解密算法
|
||||
|
||||
✅ **正确做法**:
|
||||
- 调用 Go 标准库 "crypto/*" 包
|
||||
- 使用成熟的开源库(如 gmsm、jwt 等)
|
||||
- 如果文档要求特殊算法,先查找是否有现成的 Go 实现
|
||||
- 只在标准库和成熟库都不支持时才考虑自己实现
|
||||
|
||||
## ⚠️ 单元测试规范(必须严格遵守)
|
||||
1. 所有单元测试必须写在 example_test.go 文件中
|
||||
2. 必须使用标准库 testing 包
|
||||
3. **每个 API 接口必须对应一个独立的单元测试函数,格式必须为:**
|
||||
|
||||
## 关键要求
|
||||
1. 生成的代码必须可直接编译运行
|
||||
2. go的版本为1.21
|
||||
3. **go.mod 的 module 名使用 {{sdk_name}}**
|
||||
4. 不要引用任何不存在的远程 GitHub 仓库
|
||||
|
||||
## 接口文档
|
||||
%s`
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package prompts
|
||||
|
||||
import "github.com/sashabaranov/go-openai"
|
||||
|
||||
// BuildRefinePrompt 构建文档精炼的请求
|
||||
func BuildRefinePrompt(rawDoc string) openai.ChatCompletionRequest {
|
||||
return openai.ChatCompletionRequest{
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: `你是一个专业的 API 文档分析专家,擅长从各种格式的文档中提取技术信息。你的输出必须准确、完整,因为后续会基于你的输出生成可运行的代码。`,
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: RefinePrompt(rawDoc),
|
||||
},
|
||||
},
|
||||
Temperature: 0.1,
|
||||
TopP: 0.9,
|
||||
MaxTokens: 65536,
|
||||
FrequencyPenalty: 0.0,
|
||||
PresencePenalty: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// RefinePrompt 文档精炼提示词模板
|
||||
func RefinePrompt(rawDoc string) string {
|
||||
return `分析以下 API 文档,为后续代码生成做准备。
|
||||
|
||||
## 你的任务
|
||||
|
||||
1. 判断文档类型:是"调用文档"(生成客户端 SDK)还是"对接文档"(生成服务端骨架)
|
||||
2. 完整提取所有接口信息(路径、方法、参数、响应)
|
||||
3. 完整提取认证/加密/签名细节
|
||||
4. 去除营销文案、流程说明等噪音,但保留所有技术信息
|
||||
|
||||
## 必须保留的内容
|
||||
|
||||
- 所有接口(一个不漏)
|
||||
- 所有参数和字段(包括类型、必填、描述)
|
||||
- 所有错误码
|
||||
- 认证、加密、签名的完整流程
|
||||
- 所有示例
|
||||
|
||||
## 可以去掉的内容
|
||||
|
||||
- 公司介绍、平台介绍
|
||||
- 接入流程步骤
|
||||
- 联调建议
|
||||
- 重复内容
|
||||
|
||||
## 输出格式
|
||||
|
||||
` + "```" + `
|
||||
文档类型:[调用文档/对接文档/混合型]
|
||||
|
||||
[接口列表...保持完整]
|
||||
[认证加密签名...保持完整]
|
||||
[错误码...保持完整]
|
||||
` + "```" + `
|
||||
|
||||
## 关键原则
|
||||
|
||||
1. **完整性优先**:不确定是否重要的信息,保留
|
||||
2. **忠于原文**:类型、字段名、示例值保持原样,不要修改
|
||||
3. **不编造**:文档中没有的信息,不要补充
|
||||
|
||||
原始文档:
|
||||
` + rawDoc
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// internal/prompts/validate_prompt.go
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// GetValidatePrompt 验证代码完整性,直接返回修复后的代码
|
||||
func GetValidatePrompt(refineDoc, sdkName, codeContent string) openai.ChatCompletionRequest {
|
||||
return openai.ChatCompletionRequest{
|
||||
Messages: []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: "你是代码审查专家。检查生成的 SDK 代码是否完整实现了文档中的所有内容。",
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: ValidPrompt(refineDoc, sdkName, codeContent),
|
||||
},
|
||||
},
|
||||
Temperature: 0.1, // 更低,保证确定性输出
|
||||
TopP: 0.9, // 核采样,平衡多样性和质量
|
||||
MaxTokens: 65536, // 加大到 64k,确保足够
|
||||
FrequencyPenalty: 0.0, // 代码生成不需要
|
||||
PresencePenalty: 0.0, // 代码生成不需要
|
||||
Stop: nil, // 不设置,让模型完整输出
|
||||
}
|
||||
}
|
||||
|
||||
func ValidPrompt(codeContent, sdkName, refineDoc string) string {
|
||||
|
||||
return `你是代码审查专家。检查生成的 SDK 代码是否完整实现了文档中的所有内容。
|
||||
|
||||
## 精炼文档
|
||||
` + refineDoc + `
|
||||
|
||||
## 当前代码
|
||||
` + codeContent + `
|
||||
|
||||
## 检查清单
|
||||
|
||||
1. 文档中的所有接口是否都已实现?
|
||||
2. 每个接口的请求参数是否完整(字段名、类型、必填)?
|
||||
3. 每个接口的响应字段是否完整?
|
||||
4. 认证方式是否已实现?
|
||||
5. 加密方式是否已实现?
|
||||
6. 签名方式是否已实现?
|
||||
7. 错误码是否已定义?
|
||||
|
||||
## 输出规则(严格遵守)
|
||||
|
||||
**情况一:审查通过,没有任何问题**
|
||||
只输出两个字符:` + "`OK`" + `
|
||||
|
||||
**情况二:存在问题**
|
||||
输出修复后的**全量代码**,每个文件用以下格式:
|
||||
|
||||
// File: ` + sdkName + `/文件名.go
|
||||
` + "```go" + `
|
||||
package ` + sdkName + `
|
||||
// ... 代码内容 ...
|
||||
` + "```" + `
|
||||
|
||||
|
||||
|
||||
## 重要提醒
|
||||
|
||||
1. 如果存在问题,必须输出**所有文件**的完整代码,不能只输出修改的部分
|
||||
2. 保持原有代码风格不变
|
||||
3. 补全所有遗漏的接口、参数、加密/签名方式
|
||||
4. 不要加任何描述性注释
|
||||
`
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
package push
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
var (
|
||||
// 全局客户端实例(单例)
|
||||
defaultClient *GiteaClient
|
||||
once sync.Once
|
||||
initErr error
|
||||
)
|
||||
|
||||
// GiteaClient Gitea 客户端封装
|
||||
type GiteaClient struct {
|
||||
client *gitea.Client
|
||||
}
|
||||
|
||||
// RepoConfig 仓库配置
|
||||
type RepoConfig struct {
|
||||
Name string // 仓库名称
|
||||
Description string // 仓库描述
|
||||
Private bool // 是否私有,false 为公共仓库
|
||||
AutoInit bool // 是否自动初始化
|
||||
Gitignores string // .gitignore 模板,如 "Go", "Python"
|
||||
License string // 许可证,如 "MIT", "Apache-2.0"
|
||||
Readme string // README 模板,如 "Default"
|
||||
}
|
||||
|
||||
// FileConfig 文件配置
|
||||
type FileConfig struct {
|
||||
Path string // 文件路径,如 "main.go"
|
||||
Content string // 文件内容(原始字符串)
|
||||
Message string // 提交信息
|
||||
Branch string // 分支名,为空则使用默认分支
|
||||
}
|
||||
|
||||
// TagConfig Tag 配置
|
||||
type TagConfig struct {
|
||||
TagName string // tag 名称,如 "v1.0.0"
|
||||
Message string // tag 说明
|
||||
}
|
||||
|
||||
// GetClient 获取全局客户端实例(单例模式)
|
||||
func GetClient() (*GiteaClient, error) {
|
||||
//if defaultClient == nil {
|
||||
// once.Do(func() {
|
||||
// defaultClient, initErr = NewGiteaClient(
|
||||
// "https://gitea.cdlsxd.cn/api/v1",
|
||||
// "rzy_lsxd_2026",
|
||||
// )
|
||||
// })
|
||||
// if initErr != nil {
|
||||
// return nil, initErr
|
||||
// }
|
||||
//}
|
||||
return defaultClient, nil
|
||||
}
|
||||
|
||||
// NewGiteaClient 创建 Gitea 客户端实例
|
||||
func NewGiteaClient(url, token string) (*GiteaClient, error) {
|
||||
client, err := gitea.NewClient(url, gitea.SetToken(token))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 Gitea 客户端失败: %v", err)
|
||||
}
|
||||
defaultClient = &GiteaClient{
|
||||
client: client,
|
||||
}
|
||||
return defaultClient, nil
|
||||
}
|
||||
|
||||
// CreateRepo 创建仓库(支持个人和组织)
|
||||
func (g *GiteaClient) CreateRepo(cfg RepoConfig, orgName ...string) (*gitea.Repository, error) {
|
||||
if g.client == nil {
|
||||
return nil, fmt.Errorf("客户端未初始化")
|
||||
}
|
||||
|
||||
opts := gitea.CreateRepoOption{
|
||||
Name: cfg.Name,
|
||||
Description: cfg.Description,
|
||||
Private: cfg.Private,
|
||||
AutoInit: cfg.AutoInit,
|
||||
Gitignores: cfg.Gitignores,
|
||||
License: cfg.License,
|
||||
//Readme: cfg.Readme,
|
||||
}
|
||||
|
||||
var repo *gitea.Repository
|
||||
var err error
|
||||
|
||||
// 如果传入了组织名,则为组织创建仓库
|
||||
if len(orgName) > 0 && orgName[0] != "" {
|
||||
repo, _, err = g.client.CreateOrgRepo(orgName[0], opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("为组织 %s 创建仓库失败: %v", orgName[0], err)
|
||||
}
|
||||
} else {
|
||||
repo, _, err = g.client.CreateRepo(opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建仓库失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
// PushFile 推送单个文件
|
||||
func (g *GiteaClient) PushFile(owner, repoName string, fileCfg FileConfig) error {
|
||||
if g.client == nil {
|
||||
return fmt.Errorf("客户端未初始化")
|
||||
}
|
||||
// Base64 编码文件内容
|
||||
contentBase64 := base64.StdEncoding.EncodeToString([]byte(fileCfg.Content))
|
||||
|
||||
opts := gitea.CreateFileOptions{
|
||||
FileOptions: gitea.FileOptions{
|
||||
Message: fileCfg.Message,
|
||||
BranchName: fileCfg.Branch,
|
||||
},
|
||||
Content: contentBase64,
|
||||
}
|
||||
|
||||
_, _, err := g.client.CreateFile(owner, repoName, fileCfg.Path, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("推送文件 %s 失败: %v", fileCfg.Path, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushFiles 批量推送多个文件
|
||||
func (g *GiteaClient) PushFiles(owner, repoName string, files []FileConfig) error {
|
||||
if g.client == nil {
|
||||
return fmt.Errorf("客户端未初始化")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if err := g.PushFile(owner, repoName, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTag 创建标签
|
||||
func (g *GiteaClient) CreateTag(owner, repoName string, tagCfg TagConfig) (*gitea.Tag, error) {
|
||||
if g.client == nil {
|
||||
return nil, fmt.Errorf("客户端未初始化")
|
||||
}
|
||||
|
||||
opts := gitea.CreateTagOption{
|
||||
TagName: tagCfg.TagName,
|
||||
Message: tagCfg.Message,
|
||||
}
|
||||
|
||||
tag, _, err := g.client.CreateTag(owner, repoName, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建标签 %s 失败: %v", tagCfg.TagName, err)
|
||||
}
|
||||
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
// CreateRepoAndPush 创建仓库并推送代码(完整流程)
|
||||
func (g *GiteaClient) CreateRepoAndPush(cfg RepoConfig, files []FileConfig, tagCfg *TagConfig, orgName ...string) error {
|
||||
if g.client == nil {
|
||||
return fmt.Errorf("客户端未初始化")
|
||||
}
|
||||
|
||||
// 1. 创建仓库
|
||||
repo, err := g.CreateRepo(cfg, orgName...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 等待仓库初始化完成
|
||||
if cfg.AutoInit {
|
||||
fmt.Println("⏳ 等待仓库初始化...")
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// 2. 推送代码
|
||||
if len(files) > 0 {
|
||||
fmt.Println("📤 开始推送文件...")
|
||||
if err := g.PushFiles(repo.Owner.UserName, repo.Name, files); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建标签(如果需要)
|
||||
if tagCfg != nil && tagCfg.TagName != "" {
|
||||
fmt.Println("🏷️ 创建标签...")
|
||||
if _, err := g.CreateTag(repo.Owner.UserName, repo.Name, *tagCfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("🎉 全部操作完成!")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 便捷函数(使用全局单例) ====================
|
||||
|
||||
// CreateRepoQuick 使用全局客户端快速创建仓库
|
||||
func CreateRepoQuick(cfg RepoConfig, orgName ...string) (*gitea.Repository, error) {
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateRepo(cfg, orgName...)
|
||||
}
|
||||
|
||||
// PushFileQuick 使用全局客户端快速推送文件
|
||||
func PushFileQuick(owner, repoName string, fileCfg FileConfig) error {
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.PushFile(owner, repoName, fileCfg)
|
||||
}
|
||||
|
||||
// PushFilesQuick 使用全局客户端快速批量推送文件
|
||||
func PushFilesQuick(owner, repoName string, files []FileConfig) error {
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.PushFiles(owner, repoName, files)
|
||||
}
|
||||
|
||||
// CreateTagQuick 使用全局客户端快速创建标签
|
||||
func CreateTagQuick(owner, repoName string, tagCfg TagConfig) (*gitea.Tag, error) {
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.CreateTag(owner, repoName, tagCfg)
|
||||
}
|
||||
|
||||
// PushQuick 使用全局客户端快速完整推送
|
||||
func PushQuick(repoName, description string, files []FileConfig, tagCfg *TagConfig, orgName ...string) error {
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := RepoConfig{
|
||||
Name: repoName,
|
||||
Description: description,
|
||||
Private: false,
|
||||
AutoInit: true,
|
||||
Gitignores: "Go",
|
||||
License: "MIT",
|
||||
Readme: "Default",
|
||||
}
|
||||
|
||||
return client.CreateRepoAndPush(cfg, files, tagCfg, orgName...)
|
||||
}
|
||||
|
||||
// ==================== 示例 ====================
|
||||
|
||||
// ExampleUsage 使用示例
|
||||
func ExampleUsage() {
|
||||
// 准备文件
|
||||
files := []FileConfig{
|
||||
{
|
||||
Path: "main.go",
|
||||
Content: `package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello, Gitea!")\n}\n`,
|
||||
Message: "添加主程序",
|
||||
Branch: "main",
|
||||
},
|
||||
{
|
||||
Path: "README.md",
|
||||
Content: `# Auto Created Repo\n\nThis repo is created by Gitea API.`,
|
||||
Message: "添加 README",
|
||||
Branch: "main",
|
||||
},
|
||||
}
|
||||
|
||||
tagCfg := &TagConfig{
|
||||
TagName: "v1.0.0",
|
||||
Message: "首次发布",
|
||||
}
|
||||
|
||||
// 方式1:使用快捷函数(自动使用全局单例)
|
||||
err := PushQuick("my-auto-repo", "通过 API 自动创建的公共仓库", files, tagCfg)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 操作失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 方式2:手动获取客户端实例(更灵活)
|
||||
client, err := GetClient()
|
||||
if err != nil {
|
||||
fmt.Printf("获取客户端失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建私有仓库
|
||||
cfg := RepoConfig{
|
||||
Name: "private-repo",
|
||||
Description: "私有仓库",
|
||||
Private: true,
|
||||
AutoInit: true,
|
||||
}
|
||||
|
||||
// 为组织创建仓库
|
||||
err = client.CreateRepoAndPush(cfg, files, nil, "my-org")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 操作失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 方式3:单独使用各个功能
|
||||
// 只创建仓库
|
||||
repoCfg := RepoConfig{
|
||||
Name: "test-repo",
|
||||
Private: false,
|
||||
AutoInit: true,
|
||||
}
|
||||
repo, err := CreateRepoQuick(repoCfg)
|
||||
if err != nil {
|
||||
fmt.Printf("创建仓库失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 只推送文件
|
||||
err = PushFileQuick(repo.Owner.UserName, repo.Name, FileConfig{
|
||||
Path: "test.txt",
|
||||
Content: "Hello World",
|
||||
Message: "添加测试文件",
|
||||
Branch: "main",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("推送文件失败: %v\n", err)
|
||||
}
|
||||
|
||||
// 只创建标签
|
||||
_, err = CreateTagQuick(repo.Owner.UserName, repo.Name, TagConfig{
|
||||
TagName: "v0.1.0",
|
||||
Message: "测试版本",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建标签失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sdk-generator/internal/call"
|
||||
"sdk-generator/internal/config"
|
||||
"sdk-generator/internal/extractor"
|
||||
"sdk-generator/internal/models"
|
||||
"sdk-generator/internal/msg"
|
||||
"sdk-generator/internal/postprocess"
|
||||
"sdk-generator/internal/prompts"
|
||||
"sdk-generator/internal/push"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SDKGeneratorService struct {
|
||||
config *config.Config
|
||||
tasks map[string]*models.Task
|
||||
tasksMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewSDKGeneratorService(cfg *config.Config) *SDKGeneratorService {
|
||||
// 创建 OpenAI 客户端配置
|
||||
return &SDKGeneratorService{
|
||||
config: cfg,
|
||||
tasks: make(map[string]*models.Task),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate 异步生成 SDK
|
||||
func (s *SDKGeneratorService) Generate(ctx context.Context, content []byte, sdkName string, llmSet *call.LlmCallSet) (string, error) {
|
||||
// 限制任务数量
|
||||
s.tasksMu.RLock()
|
||||
if len(s.tasks) >= s.config.MaxTasks {
|
||||
s.tasksMu.RUnlock()
|
||||
return "", fmt.Errorf("任务队列已满,请稍后重试")
|
||||
}
|
||||
s.tasksMu.RUnlock()
|
||||
|
||||
taskID := uuid.New().String()
|
||||
task := &models.Task{
|
||||
ID: taskID,
|
||||
Status: models.StatusPending,
|
||||
SdkName: sdkName,
|
||||
OutputDir: filepath.Join(s.config.OutputDir, taskID),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
CallLLM: call.NewCallLLM(llmSet),
|
||||
MarkdownContent: string(content),
|
||||
}
|
||||
|
||||
s.tasksMu.Lock()
|
||||
s.tasks[taskID] = task
|
||||
s.tasksMu.Unlock()
|
||||
|
||||
// 异步处理
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.failTask(task, fmt.Sprintf("panic: %v", r))
|
||||
}
|
||||
}()
|
||||
s.processTask(context.Background(), task, string(content))
|
||||
}()
|
||||
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) processTask(ctx context.Context, task *models.Task, markdownContent string) {
|
||||
s.processTaskStatus(task, models.StatusProcessing)
|
||||
|
||||
//0.创建目录
|
||||
s.processTaskStatus(task, models.StatusMkdir)
|
||||
err := s.mkdir(ctx, task)
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("[mkdir]: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 分析文档
|
||||
s.processTaskStatus(task, models.StatusAnaMd)
|
||||
err = s.anaMd(ctx, task)
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("[anaMd]: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 生成代码
|
||||
s.processTaskStatus(task, models.StatusGenerateCode)
|
||||
err = s.generateCode(ctx, task)
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("[generateCode]: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 验证是否存在缺失
|
||||
s.processTaskStatus(task, models.StatusValid)
|
||||
err = s.valid(ctx, task)
|
||||
if err != nil {
|
||||
// 后处理失败不中断流程
|
||||
s.failTask(task, fmt.Sprintf("[valid]: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 处理文件
|
||||
s.processTaskStatus(task, models.StatusCreatFile)
|
||||
err = s.creatFile(ctx, task)
|
||||
if err != nil {
|
||||
// 后处理失败不中断流程
|
||||
s.failTask(task, fmt.Sprintf("[creatFile]: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 后处理
|
||||
s.processTaskStatus(task, models.StatusProcess)
|
||||
err = postprocess.Process(ctx, task.SdkPackage)
|
||||
if err != nil {
|
||||
|
||||
//如果运行报错则多轮重试
|
||||
s.processTaskStatus(task, models.StatusFix)
|
||||
err = s.fix(ctx, task, err.Error(), 1)
|
||||
}
|
||||
|
||||
// 6. 上传仓库
|
||||
s.processTaskStatus(task, models.StatusSendToGit)
|
||||
err = s.sendToGit(ctx, task)
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("[sendToGit]: %v", err))
|
||||
}
|
||||
|
||||
// 6. 发送钉钉消息
|
||||
s.processTaskStatus(task, models.StatusMsgSend)
|
||||
msg.SendDingTalkAlert(ctx, task.SdkName, task.RepoURL)
|
||||
|
||||
// 5. 完成
|
||||
now := time.Now()
|
||||
task.Status = models.StatusCompleted
|
||||
task.UpdatedAt = now
|
||||
task.CompletedAt = &now
|
||||
s.updateTask(task)
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) sendToGit(ctx context.Context, task *models.Task) error {
|
||||
// 1. 获取 Gitea 客户端
|
||||
giteaClient, err := push.GetClient()
|
||||
if err != nil || giteaClient == nil {
|
||||
return fmt.Errorf("[GetClient]: %v", err)
|
||||
}
|
||||
|
||||
// 2. 准备文件列表
|
||||
var files []push.FileConfig
|
||||
|
||||
// 如果有提取的文件列表,推送所有文件
|
||||
if len(task.Files) > 0 {
|
||||
for _, file := range task.Files {
|
||||
files = append(files, push.FileConfig{
|
||||
Path: file.Path,
|
||||
Content: file.Content,
|
||||
Message: fmt.Sprintf("添加文件: %s", file.Path),
|
||||
Branch: "main",
|
||||
})
|
||||
}
|
||||
} else if task.Resp != "" {
|
||||
// 如果只有 Resp 内容,作为 main.go 推送
|
||||
files = append(files, push.FileConfig{
|
||||
Path: "main.go",
|
||||
Content: task.Resp,
|
||||
Message: "Initial commit: SDK code",
|
||||
Branch: "main",
|
||||
})
|
||||
}
|
||||
|
||||
//3. 如果有 MarkdownContent,添加为 README.md
|
||||
if task.MarkdownContent != "" {
|
||||
files = append(files, push.FileConfig{
|
||||
Path: "README.md",
|
||||
Content: task.MarkdownContent,
|
||||
Message: "添加 README 文档",
|
||||
Branch: "main",
|
||||
})
|
||||
}
|
||||
|
||||
// 4. 如果没有文件,返回错误
|
||||
if len(files) == 0 {
|
||||
|
||||
return fmt.Errorf("没有可推送的文件内容")
|
||||
}
|
||||
|
||||
// 5. 生成仓库名称(使用时间戳确保唯一性)
|
||||
repoName := fmt.Sprintf("%s-%s", task.SdkName, task.CreatedAt.Format("20060102-150405"))
|
||||
|
||||
// 6. 配置标签(可选)
|
||||
var tagCfg *push.TagConfig
|
||||
if task.CompletedAt != nil {
|
||||
tagCfg = &push.TagConfig{
|
||||
TagName: fmt.Sprintf("v%s", task.CompletedAt.Format("20060102.150405")),
|
||||
Message: fmt.Sprintf("SDK 生成完成: %s", task.SdkName),
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 创建仓库并推送代码
|
||||
repoCfg := push.RepoConfig{
|
||||
Name: repoName,
|
||||
Description: task.SdkName,
|
||||
Private: false, // 公共仓库
|
||||
AutoInit: false,
|
||||
Gitignores: "",
|
||||
License: "",
|
||||
Readme: "",
|
||||
}
|
||||
|
||||
err = giteaClient.CreateRepoAndPush(repoCfg, files, tagCfg, s.config.OrgName)
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("推送代码到 Gitea 失败: %v", err))
|
||||
return fmt.Errorf("推送代码到 Gitea 失败: %v", err)
|
||||
}
|
||||
|
||||
task.RepoURL = fmt.Sprintf("%s/%s/%s", s.config.GiteaUrl, s.config.OrgName, repoName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) mkdir(ctx context.Context, task *models.Task) (err error) {
|
||||
if _, err = os.Stat(task.OutputDir); err == nil {
|
||||
if err = os.RemoveAll(task.OutputDir); err != nil {
|
||||
return fmt.Errorf("删除旧目录失败: %v", err)
|
||||
}
|
||||
}
|
||||
if err = os.MkdirAll(task.OutputDir, 0755); err != nil {
|
||||
|
||||
return fmt.Errorf("创建输出目录失败: %v", err)
|
||||
}
|
||||
task.SdkPackage = filepath.Join(task.OutputDir, task.SdkName)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) generateCode(ctx context.Context, task *models.Task) (err error) {
|
||||
promptSetSdtName := strings.Replace(prompts.GenSDKPrompt, "{{sdk_name}}", task.SdkName, -1)
|
||||
doc := task.Refine
|
||||
if len(doc) == 0 {
|
||||
doc = task.MarkdownContent
|
||||
}
|
||||
prompt := fmt.Sprintf(promptSetSdtName, doc)
|
||||
task.Resp, err = task.CallLLM.Do(ctx, prompts.SDKGeneratorPrompt(prompt))
|
||||
if err != nil {
|
||||
s.failTask(task, fmt.Sprintf("调用大模型失败: %v", err))
|
||||
return
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) anaMd(ctx context.Context, task *models.Task) (err error) {
|
||||
task.Refine, err = task.CallLLM.Do(ctx, prompts.BuildRefinePrompt(task.MarkdownContent))
|
||||
if err != nil {
|
||||
|
||||
return fmt.Errorf("调用大模型失败: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) valid(ctx context.Context, task *models.Task) (err error) {
|
||||
|
||||
validRes, err := task.CallLLM.Do(ctx, prompts.GetValidatePrompt(task.Refine, task.SdkName, task.Resp))
|
||||
if err != nil {
|
||||
return fmt.Errorf("调用大模型失败: %v", err)
|
||||
}
|
||||
if validRes != "OK" {
|
||||
task.Valid = validRes
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) fix(ctx context.Context, task *models.Task, errMsg string, fixCount int) (err error) {
|
||||
log.Printf("代码存在问题,需要修复: %s,当前修复次数:%d", errMsg, fixCount)
|
||||
if fixCount >= s.config.MaxFixAttempts {
|
||||
return fmt.Errorf("修复失败,已达到最大尝试次数: %v", fixCount)
|
||||
}
|
||||
fix, err := task.CallLLM.Do(ctx, prompts.FixPrompt(task.Resp, errMsg, task.SdkName, task.Refine))
|
||||
if err != nil {
|
||||
return fmt.Errorf("调用大模型失败: %v", err)
|
||||
}
|
||||
if len(task.Valid) > 0 && task.Valid != "OK" {
|
||||
task.Valid = fix
|
||||
} else {
|
||||
task.Resp = fix
|
||||
}
|
||||
if err = s.creatFile(ctx, task); err != nil {
|
||||
// 后处理失败不中断流程
|
||||
return fmt.Errorf("创建文件失败: %v", err)
|
||||
}
|
||||
|
||||
if err = postprocess.Process(ctx, task.SdkPackage); err != nil {
|
||||
err = s.fix(ctx, task, err.Error(), fixCount+1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) creatFile(ctx context.Context, task *models.Task) error {
|
||||
// 3. 提取代码
|
||||
extract := task.Resp
|
||||
if task.Valid != "" {
|
||||
extract = task.Valid
|
||||
}
|
||||
files, err := extractor.Extract(extract)
|
||||
if err != nil {
|
||||
|
||||
return fmt.Errorf("提取代码失败: %v", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未提取到任何代码文件")
|
||||
}
|
||||
files = append(files,
|
||||
extractor.File{
|
||||
Path: filepath.Join(task.SdkName, "doc.md"),
|
||||
Content: task.MarkdownContent,
|
||||
},
|
||||
extractor.File{
|
||||
Path: filepath.Join(task.SdkName, "refine.md"),
|
||||
Content: task.Refine,
|
||||
},
|
||||
extractor.File{
|
||||
Path: filepath.Join(task.SdkName, "generate.md"),
|
||||
Content: task.Resp,
|
||||
},
|
||||
extractor.File{
|
||||
Path: filepath.Join(task.SdkName, "valid.md"),
|
||||
Content: task.Valid,
|
||||
},
|
||||
)
|
||||
// 3. 写入文件
|
||||
if err = extractor.WriteFiles(files, task.OutputDir); err != nil {
|
||||
return fmt.Errorf("写入文件失败: %v", err)
|
||||
}
|
||||
task.Files = files
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) failTask(task *models.Task, errMsg string) {
|
||||
task.Status = models.StatusFailed
|
||||
task.Error = errMsg
|
||||
task.UpdatedAt = time.Now()
|
||||
s.updateTask(task)
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) processTaskStatus(task *models.Task, process models.TaskStatus) {
|
||||
task.Status = process
|
||||
task.UpdatedAt = time.Now()
|
||||
s.tasksMu.Lock()
|
||||
defer s.tasksMu.Unlock()
|
||||
s.tasks[task.ID] = task
|
||||
}
|
||||
|
||||
func (s *SDKGeneratorService) updateTask(task *models.Task) {
|
||||
s.tasksMu.Lock()
|
||||
defer s.tasksMu.Unlock()
|
||||
s.tasks[task.ID] = task
|
||||
}
|
||||
|
||||
// GetTask 获取任务状态
|
||||
func (s *SDKGeneratorService) GetTask(taskID string) (*models.Task, bool) {
|
||||
s.tasksMu.RLock()
|
||||
defer s.tasksMu.RUnlock()
|
||||
task, ok := s.tasks[taskID]
|
||||
return task, ok
|
||||
}
|
||||
|
||||
// ListTasks 列出所有任务
|
||||
func (s *SDKGeneratorService) ListTasks() []*models.Task {
|
||||
s.tasksMu.RLock()
|
||||
defer s.tasksMu.RUnlock()
|
||||
|
||||
tasks := make([]*models.Task, 0, len(s.tasks))
|
||||
for _, task := range s.tasks {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
// DeleteTask 删除任务
|
||||
func (s *SDKGeneratorService) DeleteTask(taskID string) error {
|
||||
s.tasksMu.Lock()
|
||||
defer s.tasksMu.Unlock()
|
||||
|
||||
task, ok := s.tasks[taskID]
|
||||
if !ok {
|
||||
return fmt.Errorf("任务不存在")
|
||||
}
|
||||
|
||||
// 删除输出目录
|
||||
if err := os.RemoveAll(task.OutputDir); err != nil {
|
||||
return fmt.Errorf("删除输出目录失败: %w", err)
|
||||
}
|
||||
|
||||
delete(s.tasks, taskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskOutputDir 获取任务输出目录
|
||||
func (s *SDKGeneratorService) GetTaskOutputDir(taskID string) string {
|
||||
return filepath.Join(s.config.OutputDir, taskID)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,92 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sdk-generator/internal/config"
|
||||
"sdk-generator/internal/handler"
|
||||
"sdk-generator/internal/push"
|
||||
"sdk-generator/internal/service"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg := config.Load()
|
||||
|
||||
// 创建服务
|
||||
sdkService := service.NewSDKGeneratorService(cfg)
|
||||
|
||||
// 创建 Handler
|
||||
sdkHandler := handler.NewSDKHandler(sdkService)
|
||||
pageHandler := handler.NewPageHandler()
|
||||
|
||||
// 配置模板引擎
|
||||
engine := html.New("./web/templates", ".html")
|
||||
engine.Reload(true) // 开发时热重载
|
||||
|
||||
// 创建 Fiber 应用
|
||||
app := fiber.New(fiber.Config{
|
||||
AppName: "SDK Generator API",
|
||||
Views: engine,
|
||||
Prefork: false,
|
||||
ServerHeader: "Fiber",
|
||||
BodyLimit: 10 * 1024 * 1024, // 10MB
|
||||
})
|
||||
|
||||
// 全局中间件
|
||||
app.Use(recover.New())
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "[${time}] ${status} - ${method} ${path} ${latency}\n",
|
||||
}))
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowOrigins: "*",
|
||||
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
|
||||
AllowHeaders: "Content-Type,Authorization",
|
||||
}))
|
||||
|
||||
// 健康检查
|
||||
app.Get("/health", func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"app": "SDK Generator API",
|
||||
})
|
||||
})
|
||||
|
||||
// 静态文件
|
||||
app.Static("/static", "./web/static")
|
||||
|
||||
// 页面路由
|
||||
app.Get("/", pageHandler.Index)
|
||||
|
||||
// API 路由
|
||||
api := app.Group("/api/v1")
|
||||
|
||||
// 上传文档生成 SDK
|
||||
api.Post("/generate", sdkHandler.GenerateSDK)
|
||||
|
||||
// 查询生成任务状态
|
||||
api.Get("/tasks/:task_id", sdkHandler.GetTaskStatus)
|
||||
|
||||
// 下载生成的 SDK
|
||||
api.Get("/tasks/:task_id/download", sdkHandler.DownloadSDK)
|
||||
|
||||
// 列出所有任务
|
||||
api.Get("/tasks", sdkHandler.ListTasks)
|
||||
|
||||
// 删除任务
|
||||
api.Delete("/tasks/:task_id", sdkHandler.DeleteTask)
|
||||
//初始化代码仓库客户端
|
||||
_, err := push.NewGiteaClient(cfg.GiteaUrl, cfg.GiteaToken)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize Gitea client: %v", err)
|
||||
}
|
||||
// 启动服务请访问以下链接获取接口“生成”的接口定义信息:https://api.apifox.cn/temp-links/api/488609283?t=cb680e83-e054-41e5-a167-715db384e1d1
|
||||
if err := app.Listen(":" + cfg.Port); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,747 @@
|
|||
# 识别内容(本地文件)
|
||||
|
||||
## OpenAPI Specification
|
||||
|
||||
```yaml
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: ''
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/convert:
|
||||
post:
|
||||
summary: 识别内容(本地文件)
|
||||
deprecated: false
|
||||
description: ''
|
||||
tags:
|
||||
- any2md
|
||||
parameters: []
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
example: file://C:\Users\Administrator\Desktop\sucai\bl.png
|
||||
type: string
|
||||
format: binary
|
||||
llm_model:
|
||||
example: doubao-seed-2-0-mini-260428
|
||||
type: string
|
||||
llm_api_key:
|
||||
example: '******'
|
||||
type: string
|
||||
llm_base_url:
|
||||
example: https://ark.cn-beijing.volces.com/api/v3
|
||||
type: string
|
||||
llm_prompt:
|
||||
example: 这个图片合法吗
|
||||
type: string
|
||||
required:
|
||||
- file
|
||||
example:
|
||||
file: file://C:\Users\Administrator\Desktop\sucai\bl.png
|
||||
llm_model: doubao-seed-2-0-mini-260428
|
||||
llm_api_key: '******'
|
||||
llm_base_url: https://ark.cn-beijing.volces.com/api/v3
|
||||
llm_prompt: 这个图片合法吗
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filename:
|
||||
type: string
|
||||
llm_used:
|
||||
type: boolean
|
||||
markdown:
|
||||
type: string
|
||||
size:
|
||||
type: integer
|
||||
success:
|
||||
type: boolean
|
||||
title:
|
||||
type: 'null'
|
||||
required:
|
||||
- filename
|
||||
- llm_used
|
||||
- markdown
|
||||
- size
|
||||
- success
|
||||
- title
|
||||
example:
|
||||
filename: 蓝色兄弟营销开放API V3.docx
|
||||
llm_used: false
|
||||
markdown: >-
|
||||
# **蓝色兄弟营销开放API V3**
|
||||
|
||||
|
||||
## 接入流程
|
||||
|
||||
|
||||
1. **注册开发应用账号**:联系平台商务或技术支持,提交接入申请。
|
||||
|
||||
2. **获取应用接入信息**:创建应用成功后,可获取应用 ID、应用私钥配置要求、平台公钥及业务加密 Key。
|
||||
|
||||
3. **阅读接口文档**:仔细阅读本文档中的接口说明,确认请求加密、签名、响应解密方式。
|
||||
|
||||
4. **联调测试**:在正式使用前,完成单发、查询、作废、批量、异常场景验证。
|
||||
|
||||
|
||||
### 环境配置
|
||||
|
||||
|
||||
测试环境地址:https://gateway.dev.cdlsxd.cn
|
||||
|
||||
|
||||
正式环境地址:https://market.api.86698.cn
|
||||
|
||||
|
||||
### 测试参数
|
||||
|
||||
|
||||
# 客户应用id
|
||||
|
||||
app\_id: "xxx"
|
||||
|
||||
# 应用客户私钥,用于请求签名
|
||||
|
||||
private\_key: "xxx"
|
||||
|
||||
# 应用平台公钥,用于平台响应或回调验签
|
||||
|
||||
public\_key: "xxx"
|
||||
|
||||
# 业务参数加密key
|
||||
|
||||
key: "xxxx"
|
||||
|
||||
# 活动编号
|
||||
|
||||
activity\_no: "xxxx"
|
||||
|
||||
# 签名类型
|
||||
|
||||
sign\_type: "RSA"
|
||||
|
||||
|
||||
## 概述
|
||||
|
||||
|
||||
### V3 接入说明
|
||||
|
||||
|
||||
V3 版本统一采用 ciphertext 传输业务报文,默认接入方式与现有开放 API 保持一致。
|
||||
|
||||
|
||||
### 业务参数
|
||||
|
||||
|
||||
将业务参数组装为 JSON 明文 plaintext
|
||||
|
||||
再使用应用 key 加密,得到 ciphertext
|
||||
|
||||
|
||||
### 签名规则
|
||||
|
||||
|
||||
拼接签名字符串:分配给开发者的应用ID + 发送请求的时间 + ciphertext
|
||||
|
||||
使用应用私钥生成签名字符串 sign
|
||||
|
||||
|
||||
### 请求模式
|
||||
|
||||
|
||||
V3 统一采用 Header 鉴权模式:
|
||||
|
||||
|
||||
* Header 传:Appid、Timestamp、Sign
|
||||
|
||||
* Body 传:ciphertext
|
||||
|
||||
|
||||
### 响应说明
|
||||
|
||||
|
||||
V3 统一采用严格密文响应模式,业务响应数据通过 ciphertext 返回。
|
||||
|
||||
|
||||
### 时间戳规则
|
||||
|
||||
|
||||
时间格式:yyyy-MM-dd HH:mm:ss
|
||||
|
||||
请求时间与服务端时间误差不能超过 3 分钟
|
||||
|
||||
|
||||
### SDK
|
||||
|
||||
|
||||
开发者可参考以下 SDK:
|
||||
|
||||
|
||||
// Go
|
||||
|
||||
https://gitee.com/chengdu\_blue\_brothers/ymt-openapi-go-sdk.git
|
||||
|
||||
// Java
|
||||
|
||||
https://codeup.aliyun.com/lsxd/marketing/ymt-openapi-java-sdk.git
|
||||
|
||||
|
||||
### 公共 Header 请求参数
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | **示例值** |
|
||||
|
||||
| Appid | string | 分配给开发者的应用 ID | 123456 |
|
||||
|
||||
| Timestamp | string | 发送请求的时间,格式 yyyy-MM-dd HH:mm:ss |
|
||||
2026-06-22 15:30:00 |
|
||||
|
||||
| Sign | string | 请求签名串 | 详见 SDK 示例 |
|
||||
|
||||
| Content-Type | string | 请求数据格式 | application/json |
|
||||
|
||||
|
||||
#### 公共 Header 请求参数示例
|
||||
|
||||
|
||||
{
|
||||
"Sign": ["签名字符串"],
|
||||
"Appid": ["应用ID"],
|
||||
"Timestamp": ["请求时间"],
|
||||
"Content-Type": ["application/json"]
|
||||
}
|
||||
|
||||
|
||||
### 公共请求参数
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | **示例值** |
|
||||
|
||||
| ciphertext | string | 请求业务参数加密串 | 详见 SDK 示例 |
|
||||
|
||||
|
||||
#### 公共请求参数示例
|
||||
|
||||
|
||||
{
|
||||
"ciphertext": "加密后的业务报文"
|
||||
}
|
||||
|
||||
|
||||
### 公共响应参数
|
||||
|
||||
|
||||
| | | |
|
||||
|
||||
| --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** |
|
||||
|
||||
| code | int32 | 200 成功 |
|
||||
|
||||
| message | string | 请求描述 |
|
||||
|
||||
| reason | string | 错误原因,错误时返回 |
|
||||
|
||||
| data.ciphertext | string | 业务响应加密串 |
|
||||
|
||||
|
||||
### 公共错误码
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **code状态码** | **reason** | **错误原因** | **解决** |
|
||||
|
||||
| 500 | PANIC/其它 | 系统错误 | 联系平台处理 |
|
||||
|
||||
| 400 | INVALID\_PAYLOAD | 请求外壳格式错误 | 请检查请求 JSON 结构 |
|
||||
|
||||
| 400 | MISSING\_PARAM | 缺少必要参数 | 请检查
|
||||
app\_id、timestamp、sign、ciphertext |
|
||||
|
||||
| 400 | INVALID\_TIMESTAMP | 时间格式错误 | 请检查时间格式 |
|
||||
|
||||
| 400 | DECRYPT\_FAILED | 业务参数解密失败 | 请检查加密方式与密钥 |
|
||||
|
||||
| 401 | APP\_NOT\_FOUND | 应用不存在 | 请检查应用 ID |
|
||||
|
||||
| 401 | INVALID\_SIGNATURE | 签名错误 | 请检查签名串与私钥 |
|
||||
|
||||
| 401 | EXPIRED\_TIMESTAMP | 请求已过期 | 请检查客户端时间 |
|
||||
|
||||
| 429 | DUPLICATE\_REQUEST | 重复请求,请稍后重试 | 请避免短时间内重复提交同一业务号 |
|
||||
|
||||
|
||||
### 业务错误码[汇总]
|
||||
|
||||
|
||||
备注:若出现其它未知状态码,请联系平台技术人员。
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **code状态码** | **reason** | **错误原因** | **解决** |
|
||||
|
||||
| 401 | ACTIVITY\_NOT\_AUTH | 活动未授权 | 请检查活动授权状态 |
|
||||
|
||||
| 401 | MERCHANT\_NOT\_EXIST | 客户不存在 | 请检查客户是否存在 |
|
||||
|
||||
| 401 | MERCHANT\_NOT\_AUTH | 客户冻结 | 请检查客户授权状态 |
|
||||
|
||||
| 401 | MERCHANT\_APP\_INCOMPLETE | 客户应用配置未完善 | 请检查客户应用配置 |
|
||||
|
||||
| 401 | MERCHANT\_APP\_NOT\_AUTH | 应用不存在或未授权 | 请检查客户应用授权状态 |
|
||||
|
||||
| 403 | ACTIVITY\_EXPIRE | 活动已结束 | 请检查活动是否有效 |
|
||||
|
||||
| 403 | ACTIVITY\_OUT\_OF\_STOCK | 活动剩余量不足 | 请检查活动库存 |
|
||||
|
||||
| 404 | ACTIVITY\_NOT\_EXIST | 活动不存在 | 请检查活动编号 |
|
||||
|
||||
| 404 | MERCHANT\_ORDER\_NOT\_EXIST | 订单不存在 | 请检查交易号或外部业务号 |
|
||||
|
||||
| 404 | KEY\_NOT\_EXIST | key码不存在 | 请检查订单信息 |
|
||||
|
||||
| 400 | PARAM\_FAIL | 参数错误 | 请检查业务参数 |
|
||||
|
||||
| 400 | PARAM\_DECRYPT\_FAIL | 明文参数格式错误 | 请检查密文解密后的业务报文 |
|
||||
|
||||
|
||||
### 获取券码
|
||||
|
||||
|
||||
* 请求方式:POST
|
||||
|
||||
* Content-Type:application/json
|
||||
|
||||
* 请求路由:/openapi/v3/key/order
|
||||
|
||||
|
||||
#### 业务请求参数
|
||||
|
||||
|
||||
| | | | | |
|
||||
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | 是否必填 | **示例值** |
|
||||
|
||||
| out\_biz\_no | string | 外部业务号,幂等 | M | 123456 |
|
||||
|
||||
| activity\_no | string | 活动编号 | M | ACT20260622001 |
|
||||
|
||||
| account | string | 账号,按活动类型透传 | N | 18666666666 |
|
||||
|
||||
| notify\_url | string | 回调通知地址 | N |
|
||||
https://notify.example.com/openapi |
|
||||
|
||||
|
||||
#### 业务响应参数(解密后)
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | 是否必填 | **描述** |
|
||||
|
||||
| out\_biz\_no | string | M | 外部业务号 |
|
||||
|
||||
| trade\_no | string | M | 交易号 |
|
||||
|
||||
| key | string | N | 卡密 |
|
||||
|
||||
| url | string | N | 链接型活动返回短链接,key/url 不会同时为空 |
|
||||
|
||||
| valid\_begin\_time | string | N | 生效时间 |
|
||||
|
||||
| valid\_end\_time | string | N | 失效时间 |
|
||||
|
||||
| usable\_num | uint32 | M | 可用次数 |
|
||||
|
||||
| usage\_num | uint32 | M | 已使用次数 |
|
||||
|
||||
| status | uint32 | M | 状态:1 正常,2 已核销,3 已作废 |
|
||||
|
||||
| settlement\_price | float | N | 结算价 |
|
||||
|
||||
| account | string | N | 上报账号 |
|
||||
|
||||
|
||||
#### 请求示例
|
||||
|
||||
|
||||
明文业务参数:
|
||||
|
||||
|
||||
{
|
||||
"out\_biz\_no": "order\_001",
|
||||
"activity\_no": "ACT20260622001",
|
||||
"account": "18666666666",
|
||||
"notify\_url": "https://notify.example.com/openapi"
|
||||
}
|
||||
|
||||
|
||||
请求体示例:
|
||||
|
||||
|
||||
{
|
||||
"ciphertext": "加密后的业务报文"
|
||||
}
|
||||
|
||||
|
||||
#### 响应示例
|
||||
|
||||
|
||||
异常
|
||||
|
||||
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Signature verification failed",
|
||||
"reason": "INVALID\_SIGNATURE"
|
||||
}
|
||||
|
||||
|
||||
成功
|
||||
|
||||
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
|
||||
|
||||
解密后示例
|
||||
|
||||
|
||||
{
|
||||
"out\_biz\_no": "order\_001",
|
||||
"trade\_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid\_begin\_time": "2026-06-22 15:30:00",
|
||||
"valid\_end\_time": "2026-12-31 23:59:59",
|
||||
"usable\_num": 1,
|
||||
"usage\_num": 0,
|
||||
"status": 1,
|
||||
"settlement\_price": 9.9,
|
||||
"account": "18666666666"
|
||||
}
|
||||
|
||||
|
||||
### 券码查询
|
||||
|
||||
|
||||
* 请求方式:POST
|
||||
|
||||
* Content-Type:application/json
|
||||
|
||||
* 请求路由:/openapi/v3/key/query
|
||||
|
||||
|
||||
#### 业务请求参数
|
||||
|
||||
|
||||
| | | | | |
|
||||
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | 是否必填 | **示例值** |
|
||||
|
||||
| out\_biz\_no | string | 外部业务号,与 trade\_no 二选一 | N |
|
||||
order\_001 |
|
||||
|
||||
| trade\_no | string | 交易号,与 out\_biz\_no 二选一 | N |
|
||||
7251449503000383488 |
|
||||
|
||||
|
||||
#### 业务响应参数
|
||||
|
||||
|
||||
与“获取券码”响应参数一致。
|
||||
|
||||
|
||||
#### 响应示例
|
||||
|
||||
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out\_biz\_no": "order\_001",
|
||||
"trade\_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid\_begin\_time": "2026-06-22 15:30:00",
|
||||
"valid\_end\_time": "2026-12-31 23:59:59",
|
||||
"usable\_num": 1,
|
||||
"usage\_num": 0,
|
||||
"status": 1,
|
||||
"settlement\_price": 9.9,
|
||||
"account": "18666666666",
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
|
||||
|
||||
### 券码作废
|
||||
|
||||
|
||||
* 请求方式:POST
|
||||
|
||||
* Content-Type:application/json
|
||||
|
||||
* 请求路由:/openapi/v3/key/discard
|
||||
|
||||
|
||||
#### 业务请求参数
|
||||
|
||||
|
||||
| | | | | |
|
||||
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | 是否必填 | **示例值** |
|
||||
|
||||
| out\_biz\_no | string | 外部业务号,与 trade\_no 二选一 | N |
|
||||
order\_001 |
|
||||
|
||||
| trade\_no | string | 交易号,与 out\_biz\_no 二选一 | N |
|
||||
7251449503000383488 |
|
||||
|
||||
|
||||
#### 业务响应参数
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | 是否必填 | **描述** |
|
||||
|
||||
| out\_biz\_no | string | M | 外部业务号 |
|
||||
|
||||
| trade\_no | string | M | 交易号 |
|
||||
|
||||
| status | uint32 | M | 3 表示已作废 |
|
||||
|
||||
|
||||
#### 响应示例
|
||||
|
||||
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out\_biz\_no": "order\_001",
|
||||
"trade\_no": "7251449503000383488",
|
||||
"status": 3,
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
|
||||
|
||||
### 批量发卡
|
||||
|
||||
|
||||
* 请求方式:POST
|
||||
|
||||
* Content-Type:application/json
|
||||
|
||||
* 请求路由:/openapi/v3/key/batch\_order
|
||||
|
||||
|
||||
#### 业务请求参数
|
||||
|
||||
|
||||
| | | | | |
|
||||
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | 是否必填 | **示例值** |
|
||||
|
||||
| out\_biz\_no | string | 外部业务号,幂等 | M | batch\_001 |
|
||||
|
||||
| activity\_no | string | 活动编号 | M | ACT20260622001 |
|
||||
|
||||
| number | int32 | 发卡数量 | M | 100 |
|
||||
|
||||
| notify\_url | string | 回调通知地址 | N |
|
||||
https://notify.example.com/openapi |
|
||||
|
||||
|
||||
#### 业务响应参数
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | 是否必填 | **描述** |
|
||||
|
||||
| out\_biz\_no | string | M | 外部业务号 |
|
||||
|
||||
| trade\_no | string | M | 交易号 |
|
||||
|
||||
| status | string | M | 任务状态,初始返回 processing |
|
||||
|
||||
|
||||
#### 响应示例
|
||||
|
||||
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
|
||||
|
||||
解密后示例:
|
||||
|
||||
|
||||
{
|
||||
"out\_biz\_no": "batch\_001",
|
||||
"trade\_no": "7251449503000383499",
|
||||
"status": "processing"
|
||||
}
|
||||
|
||||
|
||||
### 批量查询
|
||||
|
||||
|
||||
* 请求方式:POST
|
||||
|
||||
* Content-Type:application/json
|
||||
|
||||
* 请求路由:/openapi/v3/key/batch\_query
|
||||
|
||||
|
||||
#### 业务请求参数
|
||||
|
||||
|
||||
| | | | | |
|
||||
|
||||
| --- | --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | **描述** | 是否必填 | **示例值** |
|
||||
|
||||
| out\_biz\_no | string | 外部业务号,与 trade\_no 二选一 | N |
|
||||
batch\_001 |
|
||||
|
||||
| trade\_no | string | 交易号,与 out\_biz\_no 二选一 | N |
|
||||
7251449503000383499 |
|
||||
|
||||
|
||||
#### 业务响应参数
|
||||
|
||||
|
||||
| | | | |
|
||||
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
| **字段名称** | **类型** | 是否必填 | **描述** |
|
||||
|
||||
| out\_biz\_no | string | M | 外部业务号 |
|
||||
|
||||
| trade\_no | string | M | 交易号 |
|
||||
|
||||
| status | string | M | processing / success / failed |
|
||||
|
||||
| download\_url | string | N | 批量任务成功后返回下载地址 |
|
||||
|
||||
| zip\_password | string | N | 批量任务成功后返回压缩包密码 |
|
||||
|
||||
|
||||
#### 响应示例
|
||||
|
||||
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
|
||||
|
||||
解密后示例:
|
||||
|
||||
|
||||
{
|
||||
"out\_biz\_no": "batch\_001",
|
||||
"trade\_no": "7251449503000383499",
|
||||
"status": "success",
|
||||
"download\_url": "https://oss.example.com/openapi\\_7251449503000383499.zip",
|
||||
"zip\_password": "123456"
|
||||
}
|
||||
|
||||
|
||||
### 高级能力说明
|
||||
|
||||
|
||||
如客户有特殊签名套件、加密套件适配需求,可通过商户应用 GatewayConfig 做高级配置。
|
||||
|
||||
|
||||
默认接入无需关注该项;如需启用,请联系平台技术人员对接。
|
||||
|
||||
|
||||
### 联调建议
|
||||
|
||||
|
||||
1. 先验证单发、查询、作废三个基础接口。
|
||||
|
||||
2. 再验证批量发卡与批量查询。
|
||||
|
||||
3. 重点验证响应密文解密结果是否正确。
|
||||
|
||||
4. 批量发卡需验证 processing -> success/failed 状态流转。
|
||||
|
||||
5. 批量成功后需验证 download\_url、zip\_password 是否可用。
|
||||
size: 12582
|
||||
success: true
|
||||
title: null
|
||||
headers: {}
|
||||
x-apifox-name: 成功
|
||||
x-apifox-ordering: 0
|
||||
security: []
|
||||
x-apifox-folder: any2md
|
||||
x-apifox-status: developing
|
||||
x-run-in-apifox: https://app.apifox.com/web/project/8591432/apis/api-488221832-run
|
||||
components:
|
||||
schemas: {}
|
||||
responses: {}
|
||||
securitySchemes: {}
|
||||
servers:
|
||||
- url: 127.0.0.1:5002
|
||||
description: generator_api
|
||||
security: []
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
/* ===== 全局重置 ===== */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f7fa;
|
||||
color: #1a1a2e;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 820px;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 24px;
|
||||
padding: 40px 48px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
/* ===== 头部 ===== */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 15px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== 上传区域 ===== */
|
||||
.upload-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 16px;
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: #8b5cf6;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.upload-zone.dragover {
|
||||
border-color: #8b5cf6;
|
||||
background: #ede9fe;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
#fileInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #f0fdf4;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #86efac;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
color: #166534;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ef4444;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* ===== 配置区域 ===== */
|
||||
.config-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.form-group .required {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
transition: border 0.2s;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #8b5cf6;
|
||||
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
|
||||
}
|
||||
|
||||
.form-group input:invalid {
|
||||
border-color: #fca5a5;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* ===== 操作按钮 ===== */
|
||||
.action-section {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn-generate {
|
||||
padding: 14px 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-generate:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.35);
|
||||
}
|
||||
|
||||
.btn-generate:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-spinner {
|
||||
display: inline-block;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ===== 进度区域 ===== */
|
||||
.progress-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.progress-status {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.progress-logs {
|
||||
margin-top: 12px;
|
||||
background: #1f2937;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.progress-logs .log-line {
|
||||
padding: 2px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.progress-logs .log-line.info { color: #60a5fa; }
|
||||
.progress-logs .log-line.success { color: #34d399; }
|
||||
.progress-logs .log-line.warning { color: #fbbf24; }
|
||||
.progress-logs .log-line.error { color: #f87171; }
|
||||
|
||||
/* ===== 结果区域 ===== */
|
||||
.result-section {
|
||||
padding: 20px 0;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.result-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result-files {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.result-files .file-tag {
|
||||
background: #f3f4f6;
|
||||
padding: 4px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.result-actions button {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: #4f46e5;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.btn-view:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.result-error {
|
||||
margin-top: 12px;
|
||||
padding: 12px 16px;
|
||||
background: #fef2f2;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #fca5a5;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.btn-generate {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* 查看代码仓库按钮 */
|
||||
.btn-view-repo {
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-view-repo:hover {
|
||||
background: #059669;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
|
@ -0,0 +1,496 @@
|
|||
// ============================================================
|
||||
// 状态管理
|
||||
// ============================================================
|
||||
const state = {
|
||||
file: null,
|
||||
taskId: null,
|
||||
status: 'idle',
|
||||
pollInterval: null,
|
||||
currentStatusName: '',
|
||||
repoURL: '',
|
||||
dotCount: 0,
|
||||
dotInterval: null,
|
||||
progressSimInterval: null,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// DOM 引用
|
||||
// ============================================================
|
||||
const uploadZone = document.getElementById('uploadZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const fileSize = document.getElementById('fileSize');
|
||||
const removeFile = document.getElementById('removeFile');
|
||||
|
||||
const sdkName = document.getElementById('sdkName');
|
||||
const llmModel = document.getElementById('llmModel');
|
||||
const apiKey = document.getElementById('apiKey');
|
||||
const baseUrl = document.getElementById('baseUrl');
|
||||
|
||||
const generateBtn = document.getElementById('generateBtn');
|
||||
const btnText = document.querySelector('.btn-text');
|
||||
const btnSpinner = document.querySelector('.btn-spinner');
|
||||
|
||||
const progressSection = document.getElementById('progressSection');
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const progressStatus = document.getElementById('progressStatus');
|
||||
const progressLogs = document.getElementById('progressLogs');
|
||||
|
||||
const resultSection = document.getElementById('resultSection');
|
||||
const resultFiles = document.getElementById('resultFiles');
|
||||
const resultError = document.getElementById('resultError');
|
||||
const downloadBtn = document.getElementById('downloadBtn');
|
||||
const viewRepoBtn = document.getElementById('viewRepoBtn');
|
||||
|
||||
// ============================================================
|
||||
// 动态效果函数
|
||||
// ============================================================
|
||||
|
||||
// 1. 进度条闪烁动画
|
||||
function addProgressGlow() {
|
||||
const existingStyle = document.getElementById('progressGlowStyle');
|
||||
if (existingStyle) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'progressGlowStyle';
|
||||
style.textContent = `
|
||||
.progress-fill.glow {
|
||||
background: linear-gradient(90deg, #6366f1, #8b5cf6, #6366f1);
|
||||
background-size: 200% 100%;
|
||||
animation: glowMove 1.5s ease-in-out infinite;
|
||||
position: relative;
|
||||
}
|
||||
.progress-fill.glow::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #8b5cf6;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 20px 8px rgba(139, 92, 246, 0.6);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes glowMove {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.3); opacity: 0.6; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 2. 状态文字动态点点点
|
||||
function startDotAnimation() {
|
||||
if (state.dotInterval) {
|
||||
clearInterval(state.dotInterval);
|
||||
}
|
||||
|
||||
let dotCount = 0;
|
||||
|
||||
state.dotInterval = setInterval(() => {
|
||||
dotCount = (dotCount + 1) % 4;
|
||||
const dots = '.'.repeat(dotCount);
|
||||
if (state.status === 'generating' || state.status === 'pending') {
|
||||
const currentText = progressStatus.textContent.replace(/\.\.\.$/, '').trim();
|
||||
if (!currentText.includes('✅') && !currentText.includes('❌')) {
|
||||
progressStatus.textContent = currentText + dots;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function stopDotAnimation() {
|
||||
if (state.dotInterval) {
|
||||
clearInterval(state.dotInterval);
|
||||
state.dotInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 进度条模拟前进
|
||||
function startProgressSimulation() {
|
||||
if (state.progressSimInterval) {
|
||||
clearInterval(state.progressSimInterval);
|
||||
}
|
||||
|
||||
let lastPercent = parseInt(progressFill.style.width) || 0;
|
||||
state.progressSimInterval = setInterval(() => {
|
||||
if (state.status === 'generating' && lastPercent < 95) {
|
||||
const increment = 0.5 + Math.random() * 1.5;
|
||||
lastPercent = Math.min(lastPercent + increment, 95);
|
||||
progressFill.style.width = lastPercent + '%';
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopProgressSimulation() {
|
||||
if (state.progressSimInterval) {
|
||||
clearInterval(state.progressSimInterval);
|
||||
state.progressSimInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 日志管理(只让最新的一条闪烁)
|
||||
// ============================================================
|
||||
let logCounter = 0;
|
||||
|
||||
function addLog(type, message) {
|
||||
const line = document.createElement('div');
|
||||
line.className = `log-line ${type}`;
|
||||
line.textContent = message;
|
||||
progressLogs.appendChild(line);
|
||||
progressLogs.scrollTop = progressLogs.scrollHeight;
|
||||
|
||||
// 移除之前所有日志的闪烁效果
|
||||
document.querySelectorAll('.log-line .log-dot-blink').forEach(el => {
|
||||
el.classList.remove('log-dot-blink');
|
||||
});
|
||||
|
||||
// 为最新日志添加闪烁点(在行首添加)
|
||||
const dotSpan = document.createElement('span');
|
||||
dotSpan.className = 'log-dot-blink';
|
||||
dotSpan.textContent = '● ';
|
||||
dotSpan.style.cssText = `
|
||||
color: #60a5fa;
|
||||
animation: logBlink 1s ease-in-out infinite;
|
||||
`;
|
||||
line.prepend(dotSpan);
|
||||
|
||||
// 确保动画样式存在
|
||||
const styleId = 'logBlinkStyle';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
@keyframes logBlink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.2; transform: scale(0.8); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
progressLogs.innerHTML = '';
|
||||
logCounter = 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 上传区域事件
|
||||
// ============================================================
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
|
||||
uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', () => {
|
||||
uploadZone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
handleFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFile(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
removeFile.addEventListener('click', () => {
|
||||
state.file = null;
|
||||
fileInfo.style.display = 'none';
|
||||
uploadZone.style.display = 'block';
|
||||
generateBtn.disabled = true;
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 文件处理
|
||||
// ============================================================
|
||||
function handleFile(file) {
|
||||
const validTypes = ['.md', '.txt', '.doc', '.docx', '.pdf'];
|
||||
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (!validTypes.includes(ext)) {
|
||||
alert('不支持的文件类型,请上传 .md .txt .doc .docx .pdf');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
alert('文件大小超过 50MB 限制');
|
||||
return;
|
||||
}
|
||||
|
||||
state.file = file;
|
||||
fileName.textContent = file.name;
|
||||
fileSize.textContent = formatFileSize(file.size);
|
||||
fileInfo.style.display = 'flex';
|
||||
uploadZone.style.display = 'none';
|
||||
generateBtn.disabled = false;
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 生成按钮
|
||||
// ============================================================
|
||||
generateBtn.addEventListener('click', generateSDK);
|
||||
|
||||
async function generateSDK() {
|
||||
if (!state.file) return;
|
||||
|
||||
const sdkNameVal = sdkName.value.trim();
|
||||
const modelVal = llmModel.value.trim();
|
||||
const apiKeyVal = apiKey.value.trim();
|
||||
const baseUrlVal = baseUrl.value.trim();
|
||||
|
||||
if (!sdkNameVal) {
|
||||
alert('请输入 SDK 名称');
|
||||
sdkName.focus();
|
||||
return;
|
||||
}
|
||||
if (!modelVal) {
|
||||
alert('请输入大模型名称');
|
||||
llmModel.focus();
|
||||
return;
|
||||
}
|
||||
if (!apiKeyVal) {
|
||||
alert('请输入 API Key');
|
||||
apiKey.focus();
|
||||
return;
|
||||
}
|
||||
if (!baseUrlVal) {
|
||||
alert('请输入 Base URL');
|
||||
baseUrl.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
state.status = 'generating';
|
||||
state.taskId = null;
|
||||
state.currentStatusName = '';
|
||||
state.repoURL = '';
|
||||
resultSection.style.display = 'none';
|
||||
resultError.style.display = 'none';
|
||||
resultFiles.innerHTML = '';
|
||||
progressSection.style.display = 'block';
|
||||
progressFill.style.width = '0%';
|
||||
progressStatus.textContent = '准备中...';
|
||||
clearLogs();
|
||||
generateBtn.disabled = true;
|
||||
btnText.textContent = '生成中';
|
||||
btnSpinner.style.display = 'inline-block';
|
||||
|
||||
viewRepoBtn.style.display = 'none';
|
||||
|
||||
// 启动动态效果
|
||||
addProgressGlow();
|
||||
progressFill.classList.add('glow');
|
||||
startDotAnimation();
|
||||
startProgressSimulation();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', state.file);
|
||||
formData.append('sdk_name', sdkNameVal);
|
||||
formData.append('llm_model', modelVal);
|
||||
formData.append('llm_api_key', apiKeyVal);
|
||||
formData.append('llm_base_url', baseUrlVal);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/generate', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '生成失败');
|
||||
}
|
||||
|
||||
state.taskId = data.task_id;
|
||||
addLog('info', '📤 任务已提交: ' + data.task_id);
|
||||
|
||||
pollTaskStatus();
|
||||
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 轮询任务状态
|
||||
// ============================================================
|
||||
function pollTaskStatus() {
|
||||
if (state.pollInterval) {
|
||||
clearInterval(state.pollInterval);
|
||||
}
|
||||
|
||||
state.pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/tasks/${state.taskId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || '查询状态失败');
|
||||
}
|
||||
|
||||
updateProgress(data);
|
||||
|
||||
if (data.status === 'generateComplete' || data.status === 'completed') {
|
||||
clearInterval(state.pollInterval);
|
||||
state.pollInterval = null;
|
||||
onComplete(data);
|
||||
} else if (data.status === 'failed') {
|
||||
clearInterval(state.pollInterval);
|
||||
state.pollInterval = null;
|
||||
showError(data.error || '生成失败');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
clearInterval(state.pollInterval);
|
||||
state.pollInterval = null;
|
||||
showError(err.message);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function updateProgress(data) {
|
||||
const percent = data.percent || 0;
|
||||
progressFill.style.width = percent + '%';
|
||||
|
||||
const statusName = data.status_name || '';
|
||||
if (statusName && statusName !== state.currentStatusName) {
|
||||
state.currentStatusName = statusName;
|
||||
addLog('info', '🔄 ' + statusName);
|
||||
}
|
||||
|
||||
// ✅ 更新状态文字(带闪烁前缀)
|
||||
const statusTextMap = {
|
||||
'pending': '⏳ 等待中',
|
||||
'generateCode': '🔄 生成代码',
|
||||
'validateCode': '🔍 验证代码',
|
||||
'generateComplete': '✅ 完成!',
|
||||
'completed': '✅ 完成!',
|
||||
'failed': '❌ 失败',
|
||||
};
|
||||
const baseText = statusTextMap[data.status] || (statusName || data.status);
|
||||
|
||||
// ✅ 只有进行中的状态才让文字闪烁
|
||||
const isProcessing = data.status !== 'generateComplete' && data.status !== 'completed' && data.status !== 'failed';
|
||||
if (isProcessing) {
|
||||
progressStatus.innerHTML = `<span class="status-blink">●</span> ${baseText}`;
|
||||
} else {
|
||||
progressStatus.textContent = baseText;
|
||||
stopDotAnimation();
|
||||
}
|
||||
|
||||
// ✅ 确保闪烁样式存在
|
||||
const styleId = 'statusBlinkStyle';
|
||||
if (!document.getElementById(styleId) && isProcessing) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
.status-blink {
|
||||
color: #60a5fa;
|
||||
display: inline-block;
|
||||
animation: statusBlink 1s ease-in-out infinite;
|
||||
margin-right: 6px;
|
||||
}
|
||||
@keyframes statusBlink {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.2; transform: scale(0.8); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 完成处理
|
||||
// ============================================================
|
||||
function onComplete(data) {
|
||||
stopDotAnimation();
|
||||
stopProgressSimulation();
|
||||
progressFill.classList.remove('glow');
|
||||
|
||||
generateBtn.disabled = false;
|
||||
btnText.textContent = '生成 SDK';
|
||||
btnSpinner.style.display = 'none';
|
||||
|
||||
resultSection.style.display = 'block';
|
||||
resultFiles.innerHTML = '';
|
||||
|
||||
const viewBtn = document.getElementById('viewBtn');
|
||||
if (viewBtn) {
|
||||
viewBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
downloadBtn.onclick = () => {
|
||||
window.location.href = `/api/v1/tasks/${state.taskId}/download`;
|
||||
};
|
||||
|
||||
const repoURL = data.RepoURL || data.repo_url || '';
|
||||
if (repoURL) {
|
||||
viewRepoBtn.style.display = 'inline-block';
|
||||
viewRepoBtn.onclick = () => {
|
||||
window.open(repoURL, '_blank');
|
||||
};
|
||||
} else {
|
||||
viewRepoBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
// ✅ 完成状态不带闪烁
|
||||
progressStatus.textContent = '✅ 生成完成!';
|
||||
progressFill.style.width = '100%';
|
||||
addLog('success', '🎉 SDK 生成完成!');
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 错误处理
|
||||
// ============================================================
|
||||
function showError(message) {
|
||||
stopDotAnimation();
|
||||
stopProgressSimulation();
|
||||
progressFill.classList.remove('glow');
|
||||
|
||||
state.status = 'error';
|
||||
generateBtn.disabled = false;
|
||||
btnText.textContent = '生成 SDK';
|
||||
btnSpinner.style.display = 'none';
|
||||
|
||||
resultSection.style.display = 'block';
|
||||
resultError.style.display = 'block';
|
||||
resultError.textContent = '❌ ' + message;
|
||||
|
||||
viewRepoBtn.style.display = 'none';
|
||||
|
||||
// ✅ 错误状态不带闪烁
|
||||
progressStatus.textContent = '❌ 失败';
|
||||
progressFill.style.width = '100%';
|
||||
progressFill.style.background = '#f87171';
|
||||
|
||||
if (state.pollInterval) {
|
||||
clearInterval(state.pollInterval);
|
||||
state.pollInterval = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SDK 生成器</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<!-- 头部 -->
|
||||
<header class="header">
|
||||
<h1>🚀 SDK 生成器</h1>
|
||||
<p class="subtitle">上传 API 文档,自动生成 Go SDK</p>
|
||||
</header>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<section class="upload-section">
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<div class="upload-icon">📄</div>
|
||||
<p class="upload-text">拖拽文档到此处,或点击上传</p>
|
||||
<p class="upload-hint">支持 .md .txt .doc .docx .pdf</p>
|
||||
<input type="file" id="fileInput" accept=".md,.txt,.doc,.docx,.pdf" />
|
||||
</div>
|
||||
|
||||
<div class="file-info" id="fileInfo" style="display:none;">
|
||||
<span class="file-name" id="fileName"></span>
|
||||
<span class="file-size" id="fileSize"></span>
|
||||
<button class="btn-remove" id="removeFile">✕</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 配置区域 -->
|
||||
<section class="config-section">
|
||||
<div class="form-group">
|
||||
<label for="sdkName">SDK 名称 <span class="required">*</span></label>
|
||||
<input type="text" id="sdkName" placeholder="例如:marketing-sdk" value="my-sdk" required />
|
||||
<small>将作为 Go 模块名和包名</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="llmModel">大模型名称 <span class="required">*</span></label>
|
||||
<input type="text" id="llmModel" placeholder="例如:doubao-seed-evolving" value="doubao-seed-evolving" required />
|
||||
<small>请输入完整的大模型标识符</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiKey">API Key <span class="required">*</span></label>
|
||||
<input type="password" id="apiKey" placeholder="请输入 API Key" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="baseUrl">Base URL <span class="required">*</span></label>
|
||||
<input type="text" id="baseUrl" placeholder="https://ark.cn-beijing.volces.com/api/v3" value="https://ark.cn-beijing.volces.com/api/v3" required />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<section class="action-section">
|
||||
<button class="btn-generate" id="generateBtn" disabled>
|
||||
<span class="btn-text">生成 SDK</span>
|
||||
<span class="btn-spinner" style="display:none;">⟳</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- 进度区域 -->
|
||||
<section class="progress-section" id="progressSection" style="display:none;">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="progress-status" id="progressStatus">准备中...</div>
|
||||
<div class="progress-logs" id="progressLogs"></div>
|
||||
</section>
|
||||
|
||||
<!-- 结果区域 -->
|
||||
<section class="result-section" id="resultSection" style="display:none;">
|
||||
<div class="result-header">
|
||||
<span class="result-icon">✅</span>
|
||||
<span class="result-title">生成完成!</span>
|
||||
</div>
|
||||
<!-- 文件列表隐藏 -->
|
||||
<div class="result-files" id="resultFiles" style="display:none;"></div>
|
||||
<div class="result-actions">
|
||||
<button class="btn-download" id="downloadBtn">📥 下载 SDK</button>
|
||||
<button class="btn-view-repo" id="viewRepoBtn" style="display:none;">📂 查看代码仓库</button>
|
||||
</div>
|
||||
<div class="result-error" id="resultError" style="display:none;"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue