add output feature

This commit is contained in:
2025-10-22 11:59:11 +08:00
parent 08004b10db
commit faae3f309a
5 changed files with 167 additions and 69 deletions

View File

@@ -89,24 +89,22 @@ func GetContainerStatus(cli *client.Client, containerID string) (string, error)
if err != nil { if err != nil {
return "", err return "", err
} }
state := containerInfo.State state := containerInfo.State
return state.Status, nil return state.Status, nil
} }
func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error { func PushImage(dockerHost string, username string, password string, registryUrl string, imageRef string) error {
script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " " script := "docker " + "-H " + dockerHost + " login -u " + username + " -p " + password + " " + registryUrl + " "
cmd := exec.Command("sh", "-c", script) cmd := exec.Command("sh", "-c", script)
_, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return err return fmt.Errorf("%s \n 镜像登录失败: %s", string(output), err.Error())
} }
// 推送到仓库 // 推送到仓库
script = "docker " + "-H " + dockerHost + " push " + imageRef script = "docker " + "-H " + dockerHost + " push " + imageRef
cmd = exec.Command("sh", "-c", script) cmd = exec.Command("sh", "-c", script)
_, err = cmd.CombinedOutput() output, err = cmd.CombinedOutput()
if err != nil { if err != nil {
return err return fmt.Errorf("%s \n 镜像推送失败: %s", string(output), err.Error())
} }
return nil return nil
} }

View File

@@ -324,9 +324,36 @@ func GetDevContainerOutput(ctx *context.Context) {
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*") ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*") ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*") ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
output, err := devcontainer_service.GetDevContainerOutput(ctx, ctx.Doer, ctx.Repo.Repository) query := ctx.Req.URL.Query()
output, err := devcontainer_service.GetDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository)
if err != nil { if err != nil {
log.Info(err.Error()) log.Info(err.Error())
} }
ctx.JSON(http.StatusOK, output) ctx.JSON(http.StatusOK, map[string]string{"output": output})
}
func SaveDevContainerOutput(ctx *context.Context) {
// 设置 CORS 响应头
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "*")
// 处理 OPTIONS 预检请求
if ctx.Req.Method == "OPTIONS" {
ctx.JSON(http.StatusOK, "")
return
}
query := ctx.Req.URL.Query()
// 从请求体中读取输出内容
body, err := io.ReadAll(ctx.Req.Body)
if err != nil {
log.Error("Failed to read request body: %v", err)
ctx.JSON(http.StatusBadRequest, map[string]string{"error": "Failed to read request body"})
return
}
err = devcontainer_service.SaveDevContainerOutput(ctx, query.Get("user"), ctx.Repo.Repository, string(body))
if err != nil {
log.Info(err.Error())
}
ctx.JSON(http.StatusOK, "")
} }

View File

@@ -1434,13 +1434,14 @@ func registerWebRoutes(m *web.Router) {
m.Get("/status", devcontainer_web.GetDevContainerStatus) m.Get("/status", devcontainer_web.GetDevContainerStatus)
m.Get("/command", devcontainer_web.GetTerminalCommand) m.Get("/command", devcontainer_web.GetTerminalCommand)
m.Get("/output", devcontainer_web.GetDevContainerOutput) m.Get("/output", devcontainer_web.GetDevContainerOutput)
m.Methods("POST, OPTIONS", "/output", devcontainer_web.SaveDevContainerOutput)
}, },
// 解析仓库信息 // 解析仓库信息
// 具有code读取权限 // 具有code读取权限
context.RepoAssignment, reqUnitCodeReader, context.RepoAssignment, reqUnitCodeReader,
) )
m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性 m.Get("/devstar-home", devcontainer_web.VscodeHome) // 旧地址,保留兼容性
m.Get("/vscode-home", devcontainer_web.VscodeHome) m.Get("/vscode-home", devcontainer_web.VscodeHome)
m.Group("/api/devcontainer", func() { m.Group("/api/devcontainer", func() {
// 获取 某用户在某仓库中的 DevContainer 细节包括SSH连接信息默认不会等待 (wait = false) // 获取 某用户在某仓库中的 DevContainer 细节包括SSH连接信息默认不会等待 (wait = false)
// 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId直接从 token 中提取 // 请求方式: GET /api/devcontainer?repoId=${repoId}&wait=true // 无需传入 userId直接从 token 中提取

View File

@@ -6,8 +6,6 @@ import (
"context" "context"
"fmt" "fmt"
"math" "math"
"net"
"net/url"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
@@ -456,7 +454,6 @@ func UpdateDevContainer(ctx context.Context, doer *user.User, repo *repo.Reposit
if err != nil { if err != nil {
return err return err
} }
otherCtx := context.Background() otherCtx := context.Background()
if cfg.Section("k8s").Key("ENABLE").Value() == "true" { if cfg.Section("k8s").Key("ENABLE").Value() == "true" {
//k8s的逻辑 //k8s的逻辑
@@ -655,67 +652,49 @@ func GetTerminalCommand(ctx context.Context, userID string, repo *repo.Repositor
} }
return cmd, fmt.Sprintf("%d", realTimeStatus), nil return cmd, fmt.Sprintf("%d", realTimeStatus), nil
} }
func GetDevContainerOutput(ctx context.Context, doer *user.User, repo *repo.Repository) (OutputResponse, error) { func GetDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository) (string, error) {
var devContainerOutput []devcontainer_models.DevcontainerOutput var devContainerOutput string
dbEngine := db.GetEngine(ctx) dbEngine := db.GetEngine(ctx)
resp := OutputResponse{}
var status string
var containerName string
_, err := dbEngine.
Table("devcontainer").
Select("devcontainer_status, name").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID).
Get(&status, &containerName)
if err != nil {
return resp, err
}
err = dbEngine.Table("devcontainer_output"). _, err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", doer.ID, repo.ID). Select("output").
Find(&devContainerOutput) Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
Get(&devContainerOutput)
if err != nil { if err != nil {
return resp, err return "", err
} }
if len(devContainerOutput) > 0 { return devContainerOutput, nil
}
func SaveDevContainerOutput(ctx context.Context, user_id string, repo *repo.Repository, newoutput string) error {
var devContainerOutput string
var finalOutput string
dbEngine := db.GetEngine(ctx)
resp.CurrentJob.Title = repo.Name + " Devcontainer Info" // 从数据库中获取现有的输出内容
resp.CurrentJob.Detail = status _, err := dbEngine.Table("devcontainer_output").
if status == "4" { Select("output").
// 获取WebSSH服务端口 Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
webTerminalURL, err := GetWebTerminalURL(ctx, doer.ID, repo.ID) Get(&devContainerOutput)
if err == nil { if err != nil {
return resp, err return err
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
return resp, err
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
return resp, err
}
}
for _, item := range devContainerOutput {
logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{
Index: 1,
Message: item.Output,
})
resp.CurrentJob.Steps = append(resp.CurrentJob.Steps, &ViewJobStep{
Summary: item.Command,
Status: item.Status,
Logs: logLines,
})
}
} }
return resp, nil devContainerOutput = strings.TrimSuffix(devContainerOutput, "\r\n")
if newoutput == "\b \b" {
finalOutput = devContainerOutput[:len(devContainerOutput)-1]
} else {
finalOutput = devContainerOutput + newoutput
}
_, err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ? AND list_id = ?", user_id, repo.ID, 4).
Update(map[string]interface{}{
"output": finalOutput + "\r\n",
})
if err != nil {
return err
}
return nil
} }
func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) { func GetMappedPort(ctx context.Context, containerName string, port string) (uint16, error) {
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)

View File

@@ -84,6 +84,16 @@
<!-- 结束Dev Container 正文内容 --> <!-- 结束Dev Container 正文内容 -->
</div> </div>
</div> </div>
<!-- 自定义警告框 -->
<div id="customAlert" class="custom-alert">
<div class="alert-content">
<div class="alert-header">
<strong>提示信息</strong>
<button class="alert-close" onclick="closeCustomAlert()">&times;</button>
</div>
<div id="alertText" class="alert-body"></div>
</div>
</div>
<!-- 确认删除 Dev Container 模态对话框 --> <!-- 确认删除 Dev Container 模态对话框 -->
<div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal"> <div class="ui g-modal-confirm delete modal" id="delete-repo-devcontainer-of-user-modal">
@@ -406,11 +416,31 @@ function togglePasswordVisibility(passwordFieldId, button) {
button.style.color = '#666'; // 恢复默认颜色 button.style.color = '#666'; // 恢复默认颜色
} }
} }
function showCustomAlert(message, title = "提示信息") {
const alertBox = document.getElementById('customAlert');
const alertText = document.getElementById('alertText');
const alertHeader = alertBox.querySelector('.alert-header strong');
alertHeader.textContent = title;
alertText.textContent = message;
alertBox.style.display = 'block';
}
function closeCustomAlert() {
document.getElementById('customAlert').style.display = 'none';
}
// 点击背景关闭
document.getElementById('customAlert').addEventListener('click', function(e) {
if (e.target === this) {
closeCustomAlert();
}
});
function submitForm(event) { function submitForm(event) {
event.preventDefault(); // 阻止默认的表单提交行为 event.preventDefault(); // 阻止默认的表单提交行为
const {csrfToken} = window.config; const {csrfToken} = window.config;
const {appSubUrl} = window.config; const {appSubUrl} = window.config;
console.log("????")
const formModal = document.getElementById('updatemodal'); const formModal = document.getElementById('updatemodal');
const form = document.getElementById('updateForm'); const form = document.getElementById('updateForm');
const submitButton = document.getElementById('updateSubmitButton'); const submitButton = document.getElementById('updateSubmitButton');
@@ -436,9 +466,9 @@ function submitForm(event) {
.then(data => { .then(data => {
submitButton.disabled = false; submitButton.disabled = false;
formModal.classList.remove('is-loading') formModal.classList.remove('is-loading')
alert(data.message); showCustomAlert(data.message);
if(data.redirect){ if(data.redirect){
closeButton.click() closeCustomAlert()
} }
intervalID = setInterval(getStatus, 3000); intervalID = setInterval(getStatus, 3000);
}) })
@@ -468,6 +498,69 @@ function submitForm(event) {
0%{-webkit-transform:rotate(0deg)} 0%{-webkit-transform:rotate(0deg)}
100%{-webkit-transform:rotate(360deg)} 100%{-webkit-transform:rotate(360deg)}
} }
.custom-alert {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 10000;
}
.alert-content {
color: black;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 0; /* 移除内边距,在内部元素中设置 */
border-radius: 8px;
width: 80%;
max-width: 600px;
max-height: 80%;
display: flex;
flex-direction: column;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.alert-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
border-radius: 8px 8px 0 0;
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert-close {
cursor: pointer;
font-size: 24px;
font-weight: bold;
color: #666;
background: none;
border: none;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.alert-close:hover {
background: #e9ecef;
color: #000;
}
.alert-body {
padding: 20px;
overflow-y: auto;
max-height: calc(80vh - 100px); /* 减去头部高度 */
white-space: pre-wrap;
word-wrap: break-word;
}
</style> </style>
{{template "base/footer" .}} {{template "base/footer" .}}