Merge branch 'songquanpeng:main' into main
This commit is contained in:
commit
fa5774c7cb
2
.github/workflows/linux-release.yml
vendored
2
.github/workflows/linux-release.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Build Frontend (theme default)
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/macos-release.yml
vendored
2
.github/workflows/macos-release.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Build Frontend (theme default)
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
run: |
|
run: |
|
||||||
|
2
.github/workflows/windows-release.yml
vendored
2
.github/workflows/windows-release.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
- name: Build Frontend (theme default)
|
- name: Build Frontend
|
||||||
env:
|
env:
|
||||||
CI: ""
|
CI: ""
|
||||||
run: |
|
run: |
|
||||||
|
@ -23,7 +23,7 @@ ADD go.mod go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=builder /web/build ./web/build
|
COPY --from=builder /web/build ./web/build
|
||||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
RUN go build -ldflags "-s -w -X 'github.com/songquanpeng/one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine
|
||||||
|
|
||||||
|
@ -137,6 +137,7 @@ func GetUUID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
const keyNumbers = "0123456789"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
@ -168,6 +169,15 @@ func GetRandomString(length int) string {
|
|||||||
return string(key)
|
return string(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRandomNumberString(length int) string {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
key := make([]byte, length)
|
||||||
|
for i := 0; i < length; i++ {
|
||||||
|
key[i] = keyNumbers[rand.Intn(len(keyNumbers))]
|
||||||
|
}
|
||||||
|
return string(key)
|
||||||
|
}
|
||||||
|
|
||||||
func GetTimestamp() int64 {
|
func GetTimestamp() int64 {
|
||||||
return time.Now().Unix()
|
return time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,24 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
"github.com/songquanpeng/one-api/common/helper"
|
"github.com/songquanpeng/one-api/common/helper"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/middleware"
|
||||||
|
dbmodel "github.com/songquanpeng/one-api/model"
|
||||||
"github.com/songquanpeng/one-api/relay/constant"
|
"github.com/songquanpeng/one-api/relay/constant"
|
||||||
"github.com/songquanpeng/one-api/relay/controller"
|
"github.com/songquanpeng/one-api/relay/controller"
|
||||||
"github.com/songquanpeng/one-api/relay/model"
|
"github.com/songquanpeng/one-api/relay/model"
|
||||||
"github.com/songquanpeng/one-api/relay/util"
|
"github.com/songquanpeng/one-api/relay/util"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://platform.openai.com/docs/api-reference/chat
|
// https://platform.openai.com/docs/api-reference/chat
|
||||||
|
|
||||||
func Relay(c *gin.Context) {
|
func relay(c *gin.Context, relayMode int) *model.ErrorWithStatusCode {
|
||||||
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
|
||||||
var err *model.ErrorWithStatusCode
|
var err *model.ErrorWithStatusCode
|
||||||
switch relayMode {
|
switch relayMode {
|
||||||
case constant.RelayModeImagesGenerations:
|
case constant.RelayModeImagesGenerations:
|
||||||
@ -31,33 +32,81 @@ func Relay(c *gin.Context) {
|
|||||||
default:
|
default:
|
||||||
err = controller.RelayTextHelper(c)
|
err = controller.RelayTextHelper(c)
|
||||||
}
|
}
|
||||||
if err != nil {
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Relay(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
relayMode := constant.Path2RelayMode(c.Request.URL.Path)
|
||||||
|
bizErr := relay(c, relayMode)
|
||||||
|
if bizErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
lastFailedChannelId := channelId
|
||||||
|
channelName := c.GetString("channel_name")
|
||||||
|
group := c.GetString("group")
|
||||||
|
originalModel := c.GetString("original_model")
|
||||||
|
go processChannelRelayError(ctx, channelId, channelName, bizErr)
|
||||||
requestId := c.GetString(logger.RequestIdKey)
|
requestId := c.GetString(logger.RequestIdKey)
|
||||||
retryTimesStr := c.Query("retry")
|
retryTimes := config.RetryTimes
|
||||||
retryTimes, _ := strconv.Atoi(retryTimesStr)
|
if !shouldRetry(bizErr.StatusCode) {
|
||||||
if retryTimesStr == "" {
|
logger.Errorf(ctx, "relay error happen, but status code is %d, won't retry in this case", bizErr.StatusCode)
|
||||||
retryTimes = config.RetryTimes
|
retryTimes = 0
|
||||||
}
|
}
|
||||||
if retryTimes > 0 {
|
for i := retryTimes; i > 0; i-- {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1))
|
channel, err := dbmodel.CacheGetRandomSatisfiedChannel(group, originalModel)
|
||||||
} else {
|
if err != nil {
|
||||||
if err.StatusCode == http.StatusTooManyRequests {
|
logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %w", err)
|
||||||
err.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
break
|
||||||
}
|
}
|
||||||
err.Error.Message = helper.MessageWithRequestId(err.Error.Message, requestId)
|
logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.Id, i)
|
||||||
c.JSON(err.StatusCode, gin.H{
|
if channel.Id == lastFailedChannelId {
|
||||||
"error": err.Error,
|
continue
|
||||||
|
}
|
||||||
|
middleware.SetupContextForSelectedChannel(c, channel, originalModel)
|
||||||
|
bizErr = relay(c, relayMode)
|
||||||
|
if bizErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelId := c.GetInt("channel_id")
|
||||||
|
lastFailedChannelId = channelId
|
||||||
|
channelName := c.GetString("channel_name")
|
||||||
|
go processChannelRelayError(ctx, channelId, channelName, bizErr)
|
||||||
|
}
|
||||||
|
if bizErr != nil {
|
||||||
|
if bizErr.StatusCode == http.StatusTooManyRequests {
|
||||||
|
bizErr.Error.Message = "当前分组上游负载已饱和,请稍后再试"
|
||||||
|
}
|
||||||
|
bizErr.Error.Message = helper.MessageWithRequestId(bizErr.Error.Message, requestId)
|
||||||
|
c.JSON(bizErr.StatusCode, gin.H{
|
||||||
|
"error": bizErr.Error,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
channelId := c.GetInt("channel_id")
|
}
|
||||||
logger.Error(c.Request.Context(), fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message))
|
|
||||||
|
func shouldRetry(statusCode int) bool {
|
||||||
|
if statusCode == http.StatusTooManyRequests {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if statusCode/100 == 5 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if statusCode == http.StatusBadRequest {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if statusCode/100 == 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func processChannelRelayError(ctx context.Context, channelId int, channelName string, err *model.ErrorWithStatusCode) {
|
||||||
|
logger.Errorf(ctx, "relay error (channel #%d): %s", channelId, err.Message)
|
||||||
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
// https://platform.openai.com/docs/guides/error-codes/api-errors
|
||||||
if util.ShouldDisableChannel(&err.Error, err.StatusCode) {
|
if util.ShouldDisableChannel(&err.Error, err.StatusCode) {
|
||||||
channelId := c.GetInt("channel_id")
|
|
||||||
channelName := c.GetString("channel_name")
|
|
||||||
disableChannel(channelId, channelName, err.Message)
|
disableChannel(channelId, channelName, err.Message)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RelayNotImplemented(c *gin.Context) {
|
func RelayNotImplemented(c *gin.Context) {
|
||||||
|
@ -456,6 +456,7 @@
|
|||||||
"已绑定的邮箱账户": "Email Account Bound",
|
"已绑定的邮箱账户": "Email Account Bound",
|
||||||
"用户信息更新成功!": "User information updated successfully!",
|
"用户信息更新成功!": "User information updated successfully!",
|
||||||
"模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
|
"模型倍率 %.2f,分组倍率 %.2f": "model rate %.2f, group rate %.2f",
|
||||||
|
"模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f": "model rate %.2f, group rate %.2f, completion rate %.2f",
|
||||||
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
|
"使用明细(总消耗额度:{renderQuota(stat.quota)})": "Usage Details (Total Consumption Quota: {renderQuota(stat.quota)})",
|
||||||
"用户名称": "User Name",
|
"用户名称": "User Name",
|
||||||
"令牌名称": "Token Name",
|
"令牌名称": "Token Name",
|
||||||
|
@ -108,7 +108,7 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
c.Set("token_name", token.Name)
|
c.Set("token_name", token.Name)
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
if model.IsAdmin(token.UserId) {
|
if model.IsAdmin(token.UserId) {
|
||||||
c.Set("channelId", parts[1])
|
c.Set("specific_channel_id", parts[1])
|
||||||
} else {
|
} else {
|
||||||
abortWithMessage(c, http.StatusForbidden, "普通用户不支持指定渠道")
|
abortWithMessage(c, http.StatusForbidden, "普通用户不支持指定渠道")
|
||||||
return
|
return
|
||||||
|
@ -21,8 +21,9 @@ func Distribute() func(c *gin.Context) {
|
|||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
userGroup, _ := model.CacheGetUserGroup(userId)
|
||||||
c.Set("group", userGroup)
|
c.Set("group", userGroup)
|
||||||
|
var requestModel string
|
||||||
var channel *model.Channel
|
var channel *model.Channel
|
||||||
channelId, ok := c.Get("channelId")
|
channelId, ok := c.Get("specific_channel_id")
|
||||||
if ok {
|
if ok {
|
||||||
id, err := strconv.Atoi(channelId.(string))
|
id, err := strconv.Atoi(channelId.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -66,6 +67,7 @@ func Distribute() func(c *gin.Context) {
|
|||||||
modelRequest.Model = "whisper-1"
|
modelRequest.Model = "whisper-1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
requestModel = modelRequest.Model
|
||||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
||||||
@ -77,10 +79,17 @@ func Distribute() func(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SetupContextForSelectedChannel(c, channel, requestModel)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) {
|
||||||
c.Set("channel", channel.Type)
|
c.Set("channel", channel.Type)
|
||||||
c.Set("channel_id", channel.Id)
|
c.Set("channel_id", channel.Id)
|
||||||
c.Set("channel_name", channel.Name)
|
c.Set("channel_name", channel.Name)
|
||||||
c.Set("model_mapping", channel.GetModelMapping())
|
c.Set("model_mapping", channel.GetModelMapping())
|
||||||
|
c.Set("original_model", modelName) // for retry
|
||||||
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key))
|
||||||
c.Set("base_url", channel.GetBaseURL())
|
c.Set("base_url", channel.GetBaseURL())
|
||||||
// this is for backward compatibility
|
// this is for backward compatibility
|
||||||
@ -100,6 +109,4 @@ func Distribute() func(c *gin.Context) {
|
|||||||
for k, v := range cfg {
|
for k, v := range cfg {
|
||||||
c.Set(common.ConfigKeyPrefix+k, v)
|
c.Set(common.ConfigKeyPrefix+k, v)
|
||||||
}
|
}
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
func RequestId() func(c *gin.Context) {
|
func RequestId() func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
id := helper.GetTimeString() + helper.GetRandomString(8)
|
id := helper.GetTimeString() + helper.GetRandomNumberString(8)
|
||||||
c.Set(logger.RequestIdKey, id)
|
c.Set(logger.RequestIdKey, id)
|
||||||
ctx := context.WithValue(c.Request.Context(), logger.RequestIdKey, id)
|
ctx := context.WithValue(c.Request.Context(), logger.RequestIdKey, id)
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
|
@ -94,7 +94,7 @@ func CacheUpdateUserQuota(id int) error {
|
|||||||
if !common.RedisEnabled {
|
if !common.RedisEnabled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
quota, err := GetUserQuota(id)
|
quota, err := CacheGetUserQuota(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ func responseAIProxyLibrary2OpenAI(response *LibraryResponse) *openai.TextRespon
|
|||||||
FinishReason: "stop",
|
FinishReason: "stop",
|
||||||
}
|
}
|
||||||
fullTextResponse := openai.TextResponse{
|
fullTextResponse := openai.TextResponse{
|
||||||
Id: helper.GetUUID(),
|
Id: fmt.Sprintf("chatcmpl-%s", helper.GetUUID()),
|
||||||
Object: "chat.completion",
|
Object: "chat.completion",
|
||||||
Created: helper.GetTimestamp(),
|
Created: helper.GetTimestamp(),
|
||||||
Choices: []openai.TextResponseChoice{choice},
|
Choices: []openai.TextResponseChoice{choice},
|
||||||
@ -66,7 +66,7 @@ func documentsAIProxyLibrary(documents []LibraryDocument) *openai.ChatCompletion
|
|||||||
choice.Delta.Content = aiProxyDocuments2Markdown(documents)
|
choice.Delta.Content = aiProxyDocuments2Markdown(documents)
|
||||||
choice.FinishReason = &constant.StopFinishReason
|
choice.FinishReason = &constant.StopFinishReason
|
||||||
return &openai.ChatCompletionsStreamResponse{
|
return &openai.ChatCompletionsStreamResponse{
|
||||||
Id: helper.GetUUID(),
|
Id: fmt.Sprintf("chatcmpl-%s", helper.GetUUID()),
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: helper.GetTimestamp(),
|
Created: helper.GetTimestamp(),
|
||||||
Model: "",
|
Model: "",
|
||||||
@ -78,7 +78,7 @@ func streamResponseAIProxyLibrary2OpenAI(response *LibraryStreamResponse) *opena
|
|||||||
var choice openai.ChatCompletionsStreamResponseChoice
|
var choice openai.ChatCompletionsStreamResponseChoice
|
||||||
choice.Delta.Content = response.Content
|
choice.Delta.Content = response.Content
|
||||||
return &openai.ChatCompletionsStreamResponse{
|
return &openai.ChatCompletionsStreamResponse{
|
||||||
Id: helper.GetUUID(),
|
Id: fmt.Sprintf("chatcmpl-%s", helper.GetUUID()),
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: helper.GetTimestamp(),
|
Created: helper.GetTimestamp(),
|
||||||
Model: response.Model,
|
Model: response.Model,
|
||||||
|
@ -118,8 +118,10 @@ type ImageResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ChatCompletionsStreamResponseChoice struct {
|
type ChatCompletionsStreamResponseChoice struct {
|
||||||
|
Index int `json:"index"`
|
||||||
Delta struct {
|
Delta struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Role string `json:"role,omitempty"`
|
||||||
} `json:"delta"`
|
} `json:"delta"`
|
||||||
FinishReason *string `json:"finish_reason,omitempty"`
|
FinishReason *string `json:"finish_reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ func responseXunfei2OpenAI(response *ChatResponse) *openai.TextResponse {
|
|||||||
FinishReason: constant.StopFinishReason,
|
FinishReason: constant.StopFinishReason,
|
||||||
}
|
}
|
||||||
fullTextResponse := openai.TextResponse{
|
fullTextResponse := openai.TextResponse{
|
||||||
|
Id: fmt.Sprintf("chatcmpl-%s", helper.GetUUID()),
|
||||||
Object: "chat.completion",
|
Object: "chat.completion",
|
||||||
Created: helper.GetTimestamp(),
|
Created: helper.GetTimestamp(),
|
||||||
Choices: []openai.TextResponseChoice{choice},
|
Choices: []openai.TextResponseChoice{choice},
|
||||||
@ -92,6 +93,7 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *ChatResponse) *openai.ChatCompl
|
|||||||
choice.FinishReason = &constant.StopFinishReason
|
choice.FinishReason = &constant.StopFinishReason
|
||||||
}
|
}
|
||||||
response := openai.ChatCompletionsStreamResponse{
|
response := openai.ChatCompletionsStreamResponse{
|
||||||
|
Id: fmt.Sprintf("chatcmpl-%s", helper.GetUUID()),
|
||||||
Object: "chat.completion.chunk",
|
Object: "chat.completion.chunk",
|
||||||
Created: helper.GetTimestamp(),
|
Created: helper.GetTimestamp(),
|
||||||
Model: "SparkDesk",
|
Model: "SparkDesk",
|
||||||
|
@ -39,6 +39,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode {
|
|||||||
ratio := modelRatio * groupRatio
|
ratio := modelRatio * groupRatio
|
||||||
// pre-consume quota
|
// pre-consume quota
|
||||||
promptTokens := getPromptTokens(textRequest, meta.Mode)
|
promptTokens := getPromptTokens(textRequest, meta.Mode)
|
||||||
|
meta.PromptTokens = promptTokens
|
||||||
preConsumedQuota, bizErr := preConsumeQuota(ctx, textRequest, promptTokens, ratio, meta)
|
preConsumedQuota, bizErr := preConsumeQuota(ctx, textRequest, promptTokens, ratio, meta)
|
||||||
if bizErr != nil {
|
if bizErr != nil {
|
||||||
logger.Warnf(ctx, "preConsumeQuota failed: %+v", *bizErr)
|
logger.Warnf(ctx, "preConsumeQuota failed: %+v", *bizErr)
|
||||||
|
10
web/build.sh
10
web/build.sh
@ -1,13 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
version=$(cat VERSION)
|
version=$(cat VERSION)
|
||||||
themes=$(cat THEMES)
|
pwd
|
||||||
IFS=$'\n'
|
|
||||||
|
|
||||||
for theme in $themes; do
|
while IFS= read -r theme; do
|
||||||
echo "Building theme: $theme"
|
echo "Building theme: $theme"
|
||||||
cd $theme
|
rm -r build/$theme
|
||||||
|
cd "$theme"
|
||||||
npm install
|
npm install
|
||||||
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$version npm run build
|
DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$version npm run build
|
||||||
cd ..
|
cd ..
|
||||||
done
|
done < THEMES
|
||||||
|
Loading…
Reference in New Issue
Block a user