Browse Source

Merge branch 'V20220328' into fix-1591

tags/v1.22.3.2^2
zhoupzh 3 years ago
parent
commit
ab02144fad
15 changed files with 268 additions and 64 deletions
  1. +6
    -0
      README.md
  2. +13
    -1
      models/cloudbrain.go
  3. +6
    -2
      models/repo.go
  4. +2
    -0
      modules/cloudbrain/cloudbrain.go
  5. +2
    -0
      modules/modelarts/modelarts.go
  6. +1
    -1
      options/locale/locale_en-US.ini
  7. +2
    -0
      routers/private/internal.go
  8. +168
    -12
      routers/repo/cloudbrain.go
  9. +48
    -28
      routers/repo/modelarts.go
  10. +3
    -3
      templates/base/head.tmpl
  11. +3
    -3
      templates/base/head_fluid.tmpl
  12. +3
    -3
      templates/base/head_home.tmpl
  13. +3
    -3
      templates/base/head_pro.tmpl
  14. +1
    -1
      templates/repo/cloudbrain/benchmark/index.tmpl
  15. +7
    -7
      web_src/js/components/ProAnalysis.vue

+ 6
- 0
README.md View File

@@ -41,6 +41,7 @@
## 授权许可
本项目采用 MIT 开源授权许可证,完整的授权说明已放置在 [LICENSE](https://git.openi.org.cn/OpenI/aiforge/src/branch/develop/LICENSE) 文件中。


## 需要帮助?
如果您在使用或者开发过程中遇到问题,可以在以下渠道咨询:
- 点击[这里](https://git.openi.org.cn/OpenI/aiforge/issues)在线提交问题(点击页面右上角绿色按钮**创建任务**)
@@ -49,3 +50,8 @@

## 启智社区小白训练营:
- 结合案例给大家详细讲解如何使用社区平台,帮助无技术背景的小白成长为启智社区达人 (https://git.openi.org.cn/zeizei/OpenI_Learning)

## 平台引用
如果本平台对您的科研工作提供了帮助,可在论文致谢中加入:
英文版:```Thanks for the support provided by OpenI Community (https://git.openi.org.cn).```
中文版:```感谢启智社区提供的技术支持(https://git.openi.org.cn)。```

+ 13
- 1
models/cloudbrain.go View File

@@ -87,6 +87,8 @@ const (
ModelArtsTrainJobCheckRunning ModelArtsJobStatus = "CHECK_RUNNING" //审核作业正在运行中
ModelArtsTrainJobCheckRunningCompleted ModelArtsJobStatus = "CHECK_RUNNING_COMPLETED" //审核作业已经完成
ModelArtsTrainJobCheckFailed ModelArtsJobStatus = "CHECK_FAILED" //审核作业失败

DURATION_STR_ZERO = "00:00:00"
)

type Cloudbrain struct {
@@ -174,7 +176,7 @@ func (task *Cloudbrain) ComputeAndSetDuration() {

func ConvertDurationToStr(duration int64) string {
if duration == 0 {
return "00:00:00"
return DURATION_STR_ZERO
}
return util.AddZero(duration/3600) + ":" + util.AddZero(duration%3600/60) + ":" + util.AddZero(duration%60)
}
@@ -1323,6 +1325,7 @@ func CloudbrainsVersionList(opts *CloudbrainsOptions) ([]*CloudbrainInfo, int, e
}

func CreateCloudbrain(cloudbrain *Cloudbrain) (err error) {
cloudbrain.TrainJobDuration = DURATION_STR_ZERO
if _, err = x.Insert(cloudbrain); err != nil {
return err
}
@@ -1467,6 +1470,15 @@ func GetCloudBrainUnStoppedJob() ([]*Cloudbrain, error) {
Find(&cloudbrains)
}

func GetStoppedJobWithNoDurationJob() ([]*Cloudbrain, error) {
cloudbrains := make([]*Cloudbrain, 0)
return cloudbrains, x.
In("status", ModelArtsTrainJobCompleted, ModelArtsTrainJobFailed, ModelArtsTrainJobKilled, ModelArtsStopped, JobStopped, JobFailed, JobSucceeded).
Where("train_job_duration is null or train_job_duration = '' ").
Limit(100).
Find(&cloudbrains)
}

func GetCloudbrainCountByUserID(userID int64, jobType string) (int, error) {
count, err := x.In("status", JobWaiting, JobRunning).And("job_type = ? and user_id = ? and type = ?", jobType, userID, TypeCloudBrainOne).Count(new(Cloudbrain))
return int(count), err


+ 6
- 2
models/repo.go View File

@@ -2715,7 +2715,7 @@ func ReadLatestFileInRepo(userName, repoName, refName, treePath string) (*RepoFi
log.Error("ReadLatestFileInRepo error when OpenRepository,error=%v", err)
return nil, err
}
commitID, err := gitRepo.GetBranchCommitID(refName)
_, err = gitRepo.GetBranchCommitID(refName)
if err != nil {
log.Error("ReadLatestFileInRepo error when GetBranchCommitID,error=%v", err)
return nil, err
@@ -2747,5 +2747,9 @@ func ReadLatestFileInRepo(userName, repoName, refName, treePath string) (*RepoFi
if n >= 0 {
buf = buf[:n]
}
return &RepoFile{CommitId: commitID, Content: buf}, nil
commitId := ""
if blob != nil {
commitId = fmt.Sprint(blob.ID)
}
return &RepoFile{CommitId: commitId, Content: buf}, nil
}

+ 2
- 0
modules/cloudbrain/cloudbrain.go View File

@@ -158,10 +158,12 @@ func GenerateTask(ctx *context.Context, displayJobName, jobName, image, command,
if ResourceSpecs == nil {
json.Unmarshal([]byte(setting.ResourceSpecs), &ResourceSpecs)
}

for _, spec := range ResourceSpecs.ResourceSpec {
if resourceSpecId == spec.Id {
resourceSpec = spec
}

}

if resourceSpec == nil {


+ 2
- 0
modules/modelarts/modelarts.go View File

@@ -51,6 +51,8 @@ const (
DataUrl = "data_url"
ResultUrl = "result_url"
CkptUrl = "ckpt_url"
DeviceTarget = "device_target"
Ascend = "Ascend"
PerPage = 10
IsLatestVersion = "1"
NotLatestVersion = "0"


+ 1
- 1
options/locale/locale_en-US.ini View File

@@ -913,7 +913,7 @@ gpu_type_all=All
model_download=Model Download
submit_image=Submit Image
download=Download
score=score
score=Score

cloudbrain=Cloudbrain
cloudbrain.new=New cloudbrain


+ 2
- 0
routers/private/internal.go View File

@@ -6,6 +6,7 @@
package private

import (
"code.gitea.io/gitea/routers/repo"
"strings"

"code.gitea.io/gitea/modules/log"
@@ -45,6 +46,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/tool/update_all_repo_commit_cnt", UpdateAllRepoCommitCnt)
m.Post("/tool/repo_stat/:date", RepoStatisticManually)
m.Post("/tool/update_repo_visit/:date", UpdateRepoVisit)
m.Post("/task/history_handle/duration", repo.HandleTaskWithNoDuration)

}, CheckInternalToken)
}

+ 168
- 12
routers/repo/cloudbrain.go View File

@@ -419,13 +419,16 @@ func cloudBrainShow(ctx *context.Context, tpName base.TplName) {
}
}
if task.TrainJobDuration == "" {
var duration int64
if task.Status == string(models.JobRunning) {
duration = time.Now().Unix() - int64(task.CreatedUnix)
} else {
duration = int64(task.UpdatedUnix) - int64(task.CreatedUnix)
if task.Duration == 0 {
var duration int64
if task.Status == string(models.JobRunning) {
duration = time.Now().Unix() - int64(task.CreatedUnix)
} else {
duration = int64(task.UpdatedUnix) - int64(task.CreatedUnix)
}
task.Duration = duration
}
task.TrainJobDuration = models.ConvertDurationToStr(duration)
task.TrainJobDuration = models.ConvertDurationToStr(task.Duration)
}
ctx.Data["duration"] = task.TrainJobDuration
ctx.Data["task"] = task
@@ -1062,6 +1065,156 @@ func SyncCloudbrainStatus() {
return
}

func HandleTaskWithNoDuration(ctx *context.Context) {
log.Info("HandleTaskWithNoDuration start")
count := 0
for {
cloudBrains, err := models.GetStoppedJobWithNoDurationJob()
if err != nil {
log.Error("HandleTaskWithNoTrainJobDuration failed:", err.Error())
break
}
if len(cloudBrains) == 0 {
log.Info("HandleTaskWithNoTrainJobDuration:no task need handle")
break
}
handleNoDurationTask(cloudBrains)
count += len(cloudBrains)
if len(cloudBrains) < 100 {
log.Info("HandleTaskWithNoTrainJobDuration:task less than 100")
break
}
}
log.Info("HandleTaskWithNoTrainJobDuration:count=%d", count)
ctx.JSON(200, "success")
}

func handleNoDurationTask(cloudBrains []*models.Cloudbrain) {
for _, task := range cloudBrains {
log.Info("Handle job ,%+v", task)
if task.Type == models.TypeCloudBrainOne {
result, err := cloudbrain.GetJob(task.JobID)
if err != nil {
log.Error("GetJob(%s) failed:%v", task.JobName, err)
updateDefaultDuration(task)
continue
}

if result != nil {
if result.Msg != "success" {
updateDefaultDuration(task)
continue
}
jobRes, err := models.ConvertToJobResultPayload(result.Payload)
if err != nil || len(jobRes.TaskRoles) == 0 {
updateDefaultDuration(task)
continue
}
taskRoles := jobRes.TaskRoles
taskRes, err := models.ConvertToTaskPod(taskRoles[cloudbrain.SubTaskName].(map[string]interface{}))
if err != nil || len(taskRes.TaskStatuses) == 0 {
updateDefaultDuration(task)
continue
}
task.Status = taskRes.TaskStatuses[0].State
startTime := taskRes.TaskStatuses[0].StartAt.Unix()
endTime := taskRes.TaskStatuses[0].FinishedAt.Unix()
log.Info("task startTime = %v endTime= %v ,jobId=%d", startTime, endTime, task.ID)
if startTime > 0 {
task.StartTime = timeutil.TimeStamp(startTime)
} else {
task.StartTime = task.CreatedUnix
}
if endTime > 0 {
task.EndTime = timeutil.TimeStamp(endTime)
} else {
task.EndTime = task.UpdatedUnix
}

if task.EndTime < task.StartTime {
log.Info("endTime[%v] is less than starTime[%v],jobId=%d", task.EndTime, task.StartTime, task.ID)
st := task.StartTime
task.StartTime = task.EndTime
task.EndTime = st
}
task.ComputeAndSetDuration()
err = models.UpdateJob(task)
if err != nil {
log.Error("UpdateJob(%s) failed:%v", task.JobName, err)
}
}
} else if task.Type == models.TypeCloudBrainTwo {
if task.JobType == string(models.JobTypeDebug) {
//result, err := modelarts.GetJob(task.JobID)
result, err := modelarts.GetNotebook2(task.JobID)
if err != nil {
log.Error("GetJob(%s) failed:%v", task.JobName, err)
task.StartTime = task.CreatedUnix
task.EndTime = task.UpdatedUnix
task.ComputeAndSetDuration()
err = models.UpdateJob(task)
if err != nil {
log.Error("UpdateJob(%s) failed:%v", task.JobName, err)
}
continue
}

if result != nil {
task.Status = result.Status
startTime := result.Lease.CreateTime
duration := result.Lease.Duration / 1000
if startTime > 0 {
task.StartTime = timeutil.TimeStamp(startTime)
task.EndTime = task.StartTime.Add(duration)
}
task.ComputeAndSetDuration()
err = models.UpdateJob(task)
if err != nil {
log.Error("UpdateJob(%s) failed:%v", task.JobName, err)
continue
}
}
} else if task.JobType == string(models.JobTypeTrain) {
result, err := modelarts.GetTrainJob(task.JobID, strconv.FormatInt(task.VersionID, 10))
if err != nil {
log.Error("GetTrainJob(%s) failed:%v", task.JobName, err)
continue
}

if result != nil {
startTime := result.StartTime / 1000
if startTime > 0 {
task.StartTime = timeutil.TimeStamp(startTime)
task.EndTime = task.StartTime.Add(result.Duration / 1000)
}
task.ComputeAndSetDuration()
err = models.UpdateJob(task)
if err != nil {
log.Error("UpdateJob(%s) failed:%v", task.JobName, err)
continue
}
}
} else {
log.Error("task.JobType(%s) is error:%s", task.JobName, task.JobType)
}

} else {
log.Error("task.Type(%s) is error:%d", task.JobName, task.Type)
}
}
}

func updateDefaultDuration(task *models.Cloudbrain) {
log.Info("updateDefaultDuration: taskId=%d", task.ID)
task.StartTime = task.CreatedUnix
task.EndTime = task.UpdatedUnix
task.ComputeAndSetDuration()
err := models.UpdateJob(task)
if err != nil {
log.Error("UpdateJob(%s) failed:%v", task.JobName, err)
}
}

func CloudBrainBenchmarkIndex(ctx *context.Context) {
MustEnableCloudbrain(ctx)
repo := ctx.Repo.Repository
@@ -1090,13 +1243,16 @@ func CloudBrainBenchmarkIndex(ctx *context.Context) {
ciTasks[i].CanDel = cloudbrain.CanDeleteJob(ctx, &task.Cloudbrain)
ciTasks[i].Cloudbrain.ComputeResource = task.ComputeResource
if ciTasks[i].TrainJobDuration == "" {
var duration int64
if task.Status == string(models.JobRunning) {
duration = time.Now().Unix() - int64(task.Cloudbrain.CreatedUnix)
} else {
duration = int64(task.Cloudbrain.UpdatedUnix) - int64(task.Cloudbrain.CreatedUnix)
if ciTasks[i].Duration == 0 {
var duration int64
if task.Status == string(models.JobRunning) {
duration = time.Now().Unix() - int64(task.Cloudbrain.CreatedUnix)
} else {
duration = int64(task.Cloudbrain.UpdatedUnix) - int64(task.Cloudbrain.CreatedUnix)
}
ciTasks[i].Duration = duration
}
ciTasks[i].TrainJobDuration = models.ConvertDurationToStr(duration)
ciTasks[i].TrainJobDuration = models.ConvertDurationToStr(ciTasks[i].Duration)
}

ciTasks[i].BenchmarkTypeName = ""


+ 48
- 28
routers/repo/modelarts.go View File

@@ -962,17 +962,9 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm)
return
}

//todo: del local code?

var parameters models.Parameters
param := make([]models.Parameter, 0)
param = append(param, models.Parameter{
Label: modelarts.TrainUrl,
Value: outputObsPath,
}, models.Parameter{
Label: modelarts.DataUrl,
Value: dataPath,
})
existDeviceTarget := false
if len(params) != 0 {
err := json.Unmarshal([]byte(params), &parameters)
if err != nil {
@@ -983,6 +975,9 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm)
}

for _, parameter := range parameters.Parameter {
if parameter.Label == modelarts.DeviceTarget {
existDeviceTarget = true
}
if parameter.Label != modelarts.TrainUrl && parameter.Label != modelarts.DataUrl {
param = append(param, models.Parameter{
Label: parameter.Label,
@@ -991,9 +986,22 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm)
}
}
}
if !existDeviceTarget {
param = append(param, models.Parameter{
Label: modelarts.DeviceTarget,
Value: modelarts.Ascend,
})
}

//save param config
if isSaveParam == "on" {
saveparams := append(param, models.Parameter{
Label: modelarts.TrainUrl,
Value: outputObsPath,
}, models.Parameter{
Label: modelarts.DataUrl,
Value: dataPath,
})
if form.ParameterTemplateName == "" {
log.Error("ParameterTemplateName is empty")
trainJobNewDataPrepare(ctx)
@@ -1015,7 +1023,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm)
EngineID: int64(engineID),
LogUrl: logObsPath,
PoolID: poolID,
Parameter: param,
Parameter: saveparams,
})

if err != nil {
@@ -1041,7 +1049,7 @@ func TrainJobCreate(ctx *context.Context, form auth.CreateModelArtsTrainJobForm)
LogUrl: logObsPath,
PoolID: poolID,
Uuid: uuid,
Parameters: parameters.Parameter,
Parameters: param,
CommitID: commitID,
IsLatestVersion: isLatestVersion,
BranchName: branch_name,
@@ -1177,13 +1185,7 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ

var parameters models.Parameters
param := make([]models.Parameter, 0)
param = append(param, models.Parameter{
Label: modelarts.TrainUrl,
Value: outputObsPath,
}, models.Parameter{
Label: modelarts.DataUrl,
Value: dataPath,
})
existDeviceTarget := true
if len(params) != 0 {
err := json.Unmarshal([]byte(params), &parameters)
if err != nil {
@@ -1192,8 +1194,10 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ
ctx.RenderWithErr("运行参数错误", tplModelArtsTrainJobVersionNew, &form)
return
}

for _, parameter := range parameters.Parameter {
if parameter.Label == modelarts.DeviceTarget {
existDeviceTarget = true
}
if parameter.Label != modelarts.TrainUrl && parameter.Label != modelarts.DataUrl {
param = append(param, models.Parameter{
Label: parameter.Label,
@@ -1202,9 +1206,22 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ
}
}
}
if !existDeviceTarget {
param = append(param, models.Parameter{
Label: modelarts.DeviceTarget,
Value: modelarts.Ascend,
})
}

//save param config
if isSaveParam == "on" {
saveparams := append(param, models.Parameter{
Label: modelarts.TrainUrl,
Value: outputObsPath,
}, models.Parameter{
Label: modelarts.DataUrl,
Value: dataPath,
})
if form.ParameterTemplateName == "" {
log.Error("ParameterTemplateName is empty")
versionErrorDataPrepare(ctx, form)
@@ -1226,7 +1243,7 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ
EngineID: int64(engineID),
LogUrl: logObsPath,
PoolID: poolID,
Parameter: parameters.Parameter,
Parameter: saveparams,
})

if err != nil {
@@ -1237,12 +1254,6 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ
}
}

if err != nil {
log.Error("getFlavorNameByEngineID(%s) failed:%v", engineID, err.Error())
ctx.RenderWithErr(err.Error(), tplModelArtsTrainJobVersionNew, &form)
return
}

task, err := models.GetCloudbrainByJobIDAndVersionName(jobID, PreVersionName)
if err != nil {
log.Error("GetCloudbrainByJobIDAndVersionName(%s) failed:%v", jobID, err.Error())
@@ -1266,7 +1277,7 @@ func TrainJobCreateVersion(ctx *context.Context, form auth.CreateModelArtsTrainJ
PoolID: poolID,
Uuid: uuid,
Params: form.Params,
Parameters: parameters.Parameter,
Parameters: param,
PreVersionId: task.VersionID,
CommitID: commitID,
BranchName: branch_name,
@@ -1791,7 +1802,6 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference
return
}

//todo: del local code?
var parameters models.Parameters
param := make([]models.Parameter, 0)
param = append(param, models.Parameter{
@@ -1801,6 +1811,7 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference
Label: modelarts.CkptUrl,
Value: "s3:/" + ckptUrl,
})
existDeviceTarget := false
if len(params) != 0 {
err := json.Unmarshal([]byte(params), &parameters)
if err != nil {
@@ -1811,6 +1822,9 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference
}

for _, parameter := range parameters.Parameter {
if parameter.Label == modelarts.DeviceTarget {
existDeviceTarget = true
}
if parameter.Label != modelarts.TrainUrl && parameter.Label != modelarts.DataUrl {
param = append(param, models.Parameter{
Label: parameter.Label,
@@ -1819,6 +1833,12 @@ func InferenceJobCreate(ctx *context.Context, form auth.CreateModelArtsInference
}
}
}
if !existDeviceTarget {
param = append(param, models.Parameter{
Label: modelarts.DeviceTarget,
Value: modelarts.Ascend,
})
}

req := &modelarts.GenerateInferenceJobReq{
JobName: jobName,


+ 3
- 3
templates/base/head.tmpl View File

@@ -215,10 +215,10 @@ var _hmt = _hmt || [];
localStorage.setItem("isCloseNotice",true)
}
function isShowNotice(){
var current_notice = localStorage.getItem("notice")
var current_notice = localStorage.getItem("notices")

if (current_notice != "{{.notice.CommitId}}"){
localStorage.setItem('notice',"{{.notice.CommitId}}");
if (current_notice != "{{.notices.CommitId}}"){
localStorage.setItem('notices',"{{.notices.CommitId}}");
isNewNotice=true;
localStorage.setItem("isCloseNotice",false)
}else{


+ 3
- 3
templates/base/head_fluid.tmpl View File

@@ -216,10 +216,10 @@ var _hmt = _hmt || [];
localStorage.setItem("isCloseNotice",true)
}
function isShowNotice(){
var current_notice = localStorage.getItem("notice")
var current_notice = localStorage.getItem("notices")

if (current_notice != "{{.notice.CommitId}}"){
localStorage.setItem('notice',"{{.notice.CommitId}}");
if (current_notice != "{{.notices.CommitId}}"){
localStorage.setItem('notices',"{{.notices.CommitId}}");
isNewNotice=true;
localStorage.setItem("isCloseNotice",false)
}else{


+ 3
- 3
templates/base/head_home.tmpl View File

@@ -220,10 +220,10 @@ var _hmt = _hmt || [];
localStorage.setItem("isCloseNotice",true)
}
function isShowNotice(){
var current_notice = localStorage.getItem("notice")
var current_notice = localStorage.getItem("notices")

if (current_notice != "{{.notice.CommitId}}"){
localStorage.setItem('notice',"{{.notice.CommitId}}");
if (current_notice != "{{.notices.CommitId}}"){
localStorage.setItem('notices',"{{.notices.CommitId}}");
isNewNotice=true;
localStorage.setItem("isCloseNotice",false)
}else{


+ 3
- 3
templates/base/head_pro.tmpl View File

@@ -217,10 +217,10 @@ var _hmt = _hmt || [];
localStorage.setItem("isCloseNotice",true)
}
function isShowNotice(){
var current_notice = localStorage.getItem("notice")
var current_notice = localStorage.getItem("notices")

if (current_notice != "{{.notice.CommitId}}"){
localStorage.setItem('notice',"{{.notice.CommitId}}");
if (current_notice != "{{.notices.CommitId}}"){
localStorage.setItem('notices',"{{.notices.CommitId}}");
isNewNotice=true;
localStorage.setItem("isCloseNotice",false)
}else{


+ 1
- 1
templates/repo/cloudbrain/benchmark/index.tmpl View File

@@ -155,7 +155,7 @@
{{end}}
</form>
<a class="ui basic button {{if $.IsSigned}} blue{{else}} disabled{{end}}" href="{{$.RepoLink}}/cloudbrain/{{.Cloudbrain.ID}}/rate" target="_blank">
{{$.i18n.Tr "repo.stop"}}
{{$.i18n.Tr "repo.score"}}
</a>

<!-- 删除任务 -->


+ 7
- 7
web_src/js/components/ProAnalysis.vue View File

@@ -150,21 +150,21 @@
align="center">
</el-table-column>
<el-table-column
prop="isMirror"
prop="isFork"
label="派生"
align="center">
<template slot-scope="scope">
{{scope.row.isMirror|changeType}}
</template>
</el-table-column>
{{scope.row.isFork|changeType}}
</template>
</el-table-column>
<el-table-column
prop="isFork"
prop="isMirror"
label="镜像"
align="center">
<template slot-scope="scope">
{{scope.row.isFork|changeType}}
{{scope.row.isMirror|changeType}}
</template>
</el-table-column>
</el-table-column>
<el-table-column
prop="createUnix"
label="项目创建时间"


Loading…
Cancel
Save