Compare commits

...

1 Commits

Author SHA1 Message Date
4858414c6b !87 Devcontaienr页面中改为终端样式
All checks were successful
DevStar Studio CI Pipeline - dev branch / build-and-push-x86-64-docker-image (push) Successful in 28m13s
* 恢复合并时误删的/logo router
* change terminal
* change terminal
* Merge   main
* fix api bug
* feature-permission
2025-07-27 04:40:19 +00:00
12 changed files with 203 additions and 11 deletions

View File

@@ -0,0 +1 @@
ls

View File

@@ -0,0 +1,2 @@
.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{border:0;height:0;left:-9999em;margin:0;opacity:0;overflow:hidden;padding:0;position:absolute;resize:none;top:0;white-space:nowrap;width:0;z-index:-5}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;bottom:0;cursor:default;left:0;overflow-y:scroll;position:absolute;right:0;top:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{left:0;position:absolute;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;left:-9999em;line-height:normal;position:absolute;top:0;visibility:hidden}.xterm.enable-mouse-events{cursor:default}.xterm .xterm-cursor-pointer,.xterm.xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{bottom:0;color:transparent;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:10}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:transparent}.xterm .xterm-accessibility-tree{user-select:text;white-space:pre}.xterm .live-region{height:1px;left:-9999px;overflow:hidden;position:absolute;width:1px}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{position:absolute;z-index:6}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{pointer-events:none;position:absolute;right:0;top:0;z-index:8}.xterm-decoration-top{position:relative;z-index:2}.modal{align-items:center;display:flex;overflow:hidden;position:fixed;z-index:40}.modal,.modal-background{bottom:0;left:0;right:0;top:0}.modal-background{background-color:rgba(74,74,74,.8);position:absolute}.modal-content{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}.modal-content .box{background-color:#fff;color:#4a4a4a;display:block;padding:1.25rem}.modal-content header{border-bottom:1px solid #ddd;font-weight:700;margin-bottom:10px;padding-bottom:10px;text-align:center}.modal-content .file-input{height:.01em;left:0;outline:none;position:absolute;top:0;width:.01em}.modal-content .file-cta{align-items:center;background-color:#f5f5f5;border-color:#dbdbdb;border-radius:3px;box-shadow:none;color:#6200ee;cursor:pointer;display:inline-flex;font-size:1em;font-weight:500;height:2.25em;justify-content:flex-start;line-height:1.5;outline:none;padding:calc(.375em - 1px) 1em;position:relative;vertical-align:top;white-space:nowrap}@media print,screen and (min-width:769px){.modal-content{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}body,html{height:100%;margin:0;min-height:100%;overflow:hidden}#terminal-container{height:100%;margin:0 auto;padding:0;width:auto}#terminal-container .terminal{height:calc(100% - 10px);padding:5px}
/*# sourceMappingURL=app.180babeee880c26cc052.css.map*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
/*! crc32.js (C) 2014-present SheetJS -- http://sheetjs.com */

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="mobile-web-app-capable" content="yes"><title>ttyd - Terminal</title><link inline rel="icon" type="image/png" href="favicon.png"><link inline rel="stylesheet" type="text/css" href="app.180babeee880c26cc052.css"></head><body><script inline type="text/javascript" src="app.8249fcf680b639f40957.js"></script></body></html>

File diff suppressed because one or more lines are too long

View File

@@ -3,9 +3,15 @@ package devcontainer
import (
"encoding/json"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models/db"
devcontainer_models "code.gitea.io/gitea/models/devcontainer"
@@ -119,6 +125,20 @@ func GetRepoDevContainerDetails(ctx *context.Context) {
if err == nil {
ctx.Data["WebSSHUrl"] = webTerminalURL
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
log.Info("URL解析失败: %v", err)
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
if err != nil {
ctx.Data["TerminalParams"] = "user=" + ctx.Doer.Name + "&repo=" + ctx.Repo.Repository.Name + "&repoid=" + strconv.FormatInt(ctx.Repo.Repository.ID, 10) + "&userid=" + strconv.FormatInt(ctx.Doer.ID, 10)
log.Info("URL解析失败: %v", err)
} else {
ctx.Data["TerminalParams"] = "ip=" + terminalHost + "&port=" + terminalPort + "&user=" + ctx.Doer.Name + "&repo=" + ctx.Repo.Repository.Name + "&repoid=" + strconv.FormatInt(ctx.Repo.Repository.ID, 10) + "&userid=" + strconv.FormatInt(ctx.Doer.ID, 10)
log.Info("TerminalParams: %v", ctx.Data["TerminalParams"])
}
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, &devContainerMetadata)
if err == nil {
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
@@ -310,6 +330,8 @@ func DeleteRepoDevContainerForCurrentActor(ctx *context.Context) {
type OutputResponse struct {
CurrentJob struct {
IP string `json:"ip"`
Port string `json:"port"`
Title string `json:"title"`
Detail string `json:"detail"`
Steps []*ViewJobStep `json:"steps"`
@@ -329,28 +351,73 @@ type ViewStepLogLine struct {
}
func GetContainerOutput(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", "*")
query := ctx.Req.URL.Query()
var paramInfo = ParamInfo{
RepoID: query.Get("repo"),
UserID: query.Get("user"),
}
var devContainerOutput []devcontainer_models.DevcontainerOutput
dbEngine := db.GetEngine(*ctx)
err := dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", ctx.Doer.ID, ctx.Repo.Repository.ID).
var status string
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select("devcontainer_status").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Get(&status)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
var containerName string
_, err = db.GetEngine(ctx).
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Get(&containerName)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
err = dbEngine.Table("devcontainer_output").
Where("user_id = ? AND repo_id = ?", paramInfo.UserID, paramInfo.RepoID).
Find(&devContainerOutput)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
if len(devContainerOutput) > 0 {
resp := &OutputResponse{}
resp.CurrentJob.Title = ctx.Repo.Repository.Name + " Devcontainer Info"
resp.CurrentJob.Detail = "success"
for _, item := range devContainerOutput {
if item.Status != "success" {
resp.CurrentJob.Detail = "running"
if devContainerOutput[0].Status == "success" && devContainerOutput[1].Status == "success" {
resp.CurrentJob.Detail = "created"
}
resp.CurrentJob.Detail = status
if status == "4" {
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, containerName)
if err == nil {
log.Info("URL解析失败: %v", err)
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
log.Info("URL解析失败: %v", err)
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
resp.CurrentJob.IP = terminalHost
resp.CurrentJob.Port = terminalPort
if err != nil {
log.Info("URL解析失败: %v", err)
}
}
for _, item := range devContainerOutput {
logLines := []ViewStepLogLine{}
logLines = append(logLines, ViewStepLogLine{
Index: 1,
@@ -399,3 +466,105 @@ func StopContainer(ctx *context.Context) {
ctx.JSON(http.StatusOK, map[string]string{})
}
type ParamInfo struct {
RepoID string
UserID string
}
func GetCommand(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", "*")
query := ctx.Req.URL.Query()
var commandInfo = ParamInfo{
RepoID: query.Get("repo"),
UserID: query.Get("user"),
}
var status string
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select("devcontainer_status").
Where("user_id = ? AND repo_id = ?", commandInfo.UserID, commandInfo.RepoID).
Get(&status)
if status == "4" {
var scriptContent []byte
_, err = os.Stat("devcontainer_after_init.sh")
if os.IsNotExist(err) {
_, err = os.Stat("/app/gitea/devcontainer_after_init.sh")
if os.IsNotExist(err) {
return
} else {
scriptContent, err = os.ReadFile("/app/gitea/devcontainer_after_init.sh")
if err != nil {
return
}
}
} else {
scriptContent, err = os.ReadFile("devcontainer_after_init.sh")
if err != nil {
return
}
}
ctx.JSON(http.StatusOK, map[string]string{"command": string(scriptContent)})
return
}
ctx.JSON(http.StatusOK, map[string]string{"command": "ls"})
}
func GetTerminalToken(ctx *context.Context) {
query := ctx.Req.URL.Query()
var containerName string
_, err := db.GetEngine(ctx).
Table("devcontainer").
Select("name").
Where("user_id = ? AND repo_id = ?", query.Get("user"), query.Get("repo")).
Get(&containerName)
if err != nil {
ctx.Error(http.StatusInternalServerError, err.Error())
return
}
if containerName == "" {
ctx.Error(404, "Service not found")
}
// 获取WebSSH服务端口
webTerminalURL, err := devcontainer_service.GetWebTerminalURL(ctx, containerName)
if err == nil {
log.Info("URL解析失败: %v", err)
}
// 解析URL
u, err := url.Parse(webTerminalURL)
if err != nil {
log.Info("URL解析失败: %v", err)
return
}
// 分离主机和端口
terminalHost, terminalPort, err := net.SplitHostPort(u.Host)
if err != nil {
log.Info("URL解析失败: %v", err)
return
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("http://" + terminalHost + ":" + terminalPort + "/token")
if err != nil {
log.Error("Failed to connect terminal: %v", err)
ctx.Error(http.StatusInternalServerError, "Failed to connect terminal")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("Failed to read response body: %v", err)
ctx.Error(http.StatusInternalServerError, "Failed to read response body")
return
}
ctx.Resp.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
ctx.Status(resp.StatusCode)
_, _ = ctx.Write(body)
}

View File

@@ -474,6 +474,13 @@ func registerRoutes(m *web.Router) {
// Especially some AJAX requests, we can reduce middleware number to improve performance.
m.Get("/", Home)
m.Get("/command", devcontainer_web.GetCommand)
m.Methods("OPTIONS", "/command", func(ctx *context.Context) {
ctx.Resp.Header().Set("Access-Control-Allow-Origin", "*")
ctx.Resp.Header().Set("Access-Control-Allow-Methods", "GET, POST")
ctx.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
})
m.Get("/devstar-home", DevstarHome)
m.Get("/sitemap.xml", sitemapEnabled, ignExploreSignIn, HomeSitemap)
m.Group("/.well-known", func() {
@@ -1361,8 +1368,10 @@ func registerRoutes(m *web.Router) {
m.Get("/output", devcontainer_web.GetContainerOutput)
m.Get("/restart", devcontainer_web.RestartContainer)
m.Get("/stop", devcontainer_web.StopContainer)
m.Get("/terminalToken", devcontainer_web.GetTerminalToken)
m.Post("/delete", devcontainer_web.DeleteRepoDevContainerForCurrentActor)
m.Post("/update", devcontainer_web.UpdateRepoDevContainerForCurrentActor)
},
// 进入 Dev Container 编辑页面需要符合条件:
// 1. 已登录

View File

@@ -37,10 +37,10 @@
</div>
</form>
<iframe src="http://localhost:3000/assets/terminal/index.html?{{.TerminalParams}}" width="100%" height="400" frameborder="0">您的浏览器不支持iframe</iframe>
</div>
<div id="repo-devcontainer-view" data-outputLink="{{.Repository.Link}}/dev-container/output"></div>
<!--<div id="repo-devcontainer-view" data-outputLink="{{.Repository.Link}}/dev-container/output"></div>-->
{{end}}
</div>