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