Compare commits
8 Commits
variable_m
...
feat/devco
| Author | SHA1 | Date | |
|---|---|---|---|
| 92a8e2a6d0 | |||
| 3e94b55e8b | |||
| 416c954119 | |||
| e4baca8811 | |||
| a1ea929a8b | |||
| 85d3022496 | |||
| 98e8fec2d6 | |||
| 0333d323e0 |
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -47,9 +48,107 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
|
||||
}
|
||||
}
|
||||
|
||||
// DevContainerEditorApp represents a configured IDE for DevContainer
|
||||
type DevContainerEditorApp struct {
|
||||
DisplayName string // Display name (from name= parameter or DisplayName)
|
||||
Logo string // Logo URL (from logo= parameter)
|
||||
OpenURL string // Complete URL template with placeholders, e.g. "vscode://mengning.devstar/openProject?host={host}&..."
|
||||
}
|
||||
|
||||
type DevContainerEditorAppsType []DevContainerEditorApp
|
||||
|
||||
// ToTextareaString converts the configuration to textarea format
|
||||
func (t DevContainerEditorAppsType) ToTextareaString() string {
|
||||
ret := ""
|
||||
for _, app := range t {
|
||||
// Store the URL template with name and logo parameters included
|
||||
urlTemplate := app.OpenURL
|
||||
if app.DisplayName != "" && !strings.Contains(urlTemplate, "name=") {
|
||||
separator := "&"
|
||||
if !strings.Contains(urlTemplate, "?") {
|
||||
separator = "?"
|
||||
}
|
||||
urlTemplate += separator + "name=" + app.DisplayName
|
||||
}
|
||||
if app.Logo != "" && !strings.Contains(urlTemplate, "logo=") {
|
||||
urlTemplate += "&logo=" + app.Logo
|
||||
}
|
||||
ret += app.DisplayName + " = " + urlTemplate + "\n"
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// DefaultDevContainerEditorApps returns the default DevContainer IDE configuration
|
||||
// Based on current terminalURL format: ://mengning.devstar/openProject?host=xxx&hostname=xxx&port=xxx&username=xxx&path=xxx&access_token=xxx&devstar_username=xxx&devstar_domain=xxx
|
||||
func DefaultDevContainerEditorApps() DevContainerEditorAppsType {
|
||||
return DevContainerEditorAppsType{
|
||||
{
|
||||
DisplayName: "VSCode",
|
||||
Logo: "https://code.visualstudio.com/favicon.ico",
|
||||
OpenURL: "vscode://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
|
||||
},
|
||||
{
|
||||
DisplayName: "Cursor",
|
||||
Logo: "https://cursor.sh/favicon.ico",
|
||||
OpenURL: "cursor://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
|
||||
},
|
||||
{
|
||||
DisplayName: "Windsurf",
|
||||
Logo: "https://windsurf.ai/favicon.ico",
|
||||
OpenURL: "windsurf://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
|
||||
},
|
||||
{
|
||||
DisplayName: "Trae",
|
||||
Logo: "https://lf-static.traecdn.us/obj/trae-ai-tx/trae_website/favicon.png",
|
||||
OpenURL: "trae://mengning.devstar/openProject?host={host}&hostname={hostname}&port={port}&username={username}&path={path}&access_token={token}&devstar_username={devstar_username}&devstar_domain={domain}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ParseIDETemplate parses an IDE URL template and extracts name/logo parameters
|
||||
// Returns: DisplayName, Logo, CleanURL (without name/logo parameters)
|
||||
func ParseIDETemplate(urlTemplate string) (string, string, string) {
|
||||
displayName := ""
|
||||
logo := ""
|
||||
cleanURL := urlTemplate
|
||||
|
||||
// Check if URL has query parameters
|
||||
if strings.Contains(urlTemplate, "?") {
|
||||
parts := strings.SplitN(urlTemplate, "?", 2)
|
||||
if len(parts) == 2 {
|
||||
baseURL := parts[0]
|
||||
queryString := parts[1]
|
||||
|
||||
// Parse query parameters
|
||||
queryParams := strings.Split(queryString, "&")
|
||||
var cleanQueryParams []string
|
||||
|
||||
for _, param := range queryParams {
|
||||
if strings.HasPrefix(param, "name=") {
|
||||
displayName = strings.TrimPrefix(param, "name=")
|
||||
} else if strings.HasPrefix(param, "logo=") {
|
||||
logo = strings.TrimPrefix(param, "logo=")
|
||||
} else {
|
||||
cleanQueryParams = append(cleanQueryParams, param)
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild URL without name/logo parameters
|
||||
if len(cleanQueryParams) > 0 {
|
||||
cleanURL = baseURL + "?" + strings.Join(cleanQueryParams, "&")
|
||||
} else {
|
||||
cleanURL = baseURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return displayName, logo, cleanURL
|
||||
}
|
||||
|
||||
type RepositoryStruct struct {
|
||||
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
|
||||
GitGuideRemoteName *config.Value[string]
|
||||
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
|
||||
DevContainerEditorApps *config.Value[DevContainerEditorAppsType]
|
||||
GitGuideRemoteName *config.Value[string]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
@@ -70,8 +169,9 @@ func initDefaultConfig() {
|
||||
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
|
||||
},
|
||||
Repository: &RepositoryStruct{
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
DevContainerEditorApps: config.ValueJSON[DevContainerEditorAppsType]("repository.devcontainer.editor-apps").WithDefault(DefaultDevContainerEditorApps()),
|
||||
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,8 @@ var (
|
||||
ManifestData string
|
||||
|
||||
BeianNumber string // 网站备案号, e.g. 苏ICP备88888888888号-1
|
||||
ParentDomain string // 父域名,用于获取上级DevStar的相关服务 PARENT_DOMAIN
|
||||
ParentAccessToken string // 父域名访问令牌,用于获取上级DevStar的相关服务 PARENT_ACCESS_TOKEN
|
||||
|
||||
)
|
||||
|
||||
@@ -192,6 +194,8 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
||||
BeianNumber = sec.Key("BEIAN_NUMBER").MustString("")
|
||||
ParentDomain = sec.Key("PARENT_DOMAIN").MustString("https://devstar.cn")
|
||||
ParentAccessToken = sec.Key("PARENT_ACCESS_TOKEN").MustString("439ffb6e2cce9ecb4568a5c54750ef290831d2ef")
|
||||
|
||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||
// if these are removed, the warning will not be shown
|
||||
|
||||
@@ -161,6 +161,12 @@ func NewFuncMap() template.FuncMap {
|
||||
"BeianNumber": func() string {
|
||||
return setting.BeianNumber
|
||||
},
|
||||
"ParentDomain": func() string {
|
||||
return setting.ParentDomain
|
||||
},
|
||||
"ParentAccessToken": func() string {
|
||||
return setting.ParentAccessToken
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3596,6 +3596,8 @@ config.disable_gravatar = Disable Gravatar
|
||||
config.enable_federated_avatar = Enable Federated Avatars
|
||||
config.open_with_editor_app_help = The "Open with" editors for the clone menu. If left empty, the default will be used. Expand to see the default.
|
||||
config.git_guide_remote_name = Repository remote name for git commands in the guide
|
||||
config.devcontainer_editor_apps = DevContainer Editor Apps
|
||||
config.devcontainer_editor_app_help = Configure which AI IDEs appear in the devcontainer "Open with" menu. If left empty, the default will be used. Expand to see the default.
|
||||
|
||||
config.git_config = Git Configuration
|
||||
config.git_disable_diff_highlight = Disable Diff Syntax Highlight
|
||||
|
||||
@@ -3589,6 +3589,8 @@ config.disable_gravatar=禁用 Gravatar 头像
|
||||
config.enable_federated_avatar=启用 Federated 头像
|
||||
config.open_with_editor_app_help=用于克隆菜单的编辑器。如果为空将使用默认值。展开可以查看默认值。
|
||||
config.git_guide_remote_name=指南中 git 命令使用的仓库远程名称
|
||||
config.devcontainer_editor_apps=DevContainer 编辑器应用
|
||||
config.devcontainer_editor_app_help=配置 DevContainer "打开方式" 菜单中显示的 AI IDE。如果为空将使用默认值。展开可以查看默认值。
|
||||
|
||||
config.git_config=Git 配置
|
||||
config.git_disable_diff_highlight=禁用差异对比语法高亮
|
||||
|
||||
@@ -197,6 +197,7 @@ func ConfigSettings(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminConfig"] = true
|
||||
ctx.Data["PageIsAdminConfigSettings"] = true
|
||||
ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
|
||||
ctx.Data["DefaultDevContainerEditorAppsString"] = setting.DefaultDevContainerEditorApps().ToTextareaString()
|
||||
ctx.HTML(http.StatusOK, tplConfigSettings)
|
||||
}
|
||||
|
||||
@@ -235,11 +236,44 @@ func ChangeConfig(ctx *context.Context) {
|
||||
}
|
||||
return json.Marshal(openWithEditorApps)
|
||||
}
|
||||
|
||||
marshalDevContainerEditorApps := func(value string) ([]byte, error) {
|
||||
lines := strings.Split(value, "\n")
|
||||
var devContainerEditorApps setting.DevContainerEditorAppsType
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
configDisplayName, openURL, ok := strings.Cut(line, "=")
|
||||
configDisplayName, openURL = strings.TrimSpace(configDisplayName), strings.TrimSpace(openURL)
|
||||
if !ok || configDisplayName == "" || openURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse name and logo from URL template
|
||||
urlDisplayName, logo, cleanURL := setting.ParseIDETemplate(openURL)
|
||||
|
||||
// Use parsed name if available, otherwise use config display name
|
||||
finalDisplayName := configDisplayName
|
||||
if urlDisplayName != "" {
|
||||
finalDisplayName = urlDisplayName
|
||||
}
|
||||
|
||||
devContainerEditorApps = append(devContainerEditorApps, setting.DevContainerEditorApp{
|
||||
DisplayName: finalDisplayName,
|
||||
Logo: logo,
|
||||
OpenURL: cleanURL,
|
||||
})
|
||||
}
|
||||
return json.Marshal(devContainerEditorApps)
|
||||
}
|
||||
marshallers := map[string]func(string) ([]byte, error){
|
||||
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.DevContainerEditorApps.DynKey(): marshalDevContainerEditorApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -20,6 +22,39 @@ const (
|
||||
tplGetDevContainerDetails templates.TplName = "repo/devcontainer/details"
|
||||
)
|
||||
|
||||
// IDEInfo represents IDE information for frontend
|
||||
type IDEInfo struct {
|
||||
Name string // Display name
|
||||
Logo string // Logo URL
|
||||
URL string // Complete IDE URL
|
||||
}
|
||||
|
||||
// IDETemplateParams holds the parameters for IDE URL template replacement
|
||||
type IDETemplateParams struct {
|
||||
Host string
|
||||
Hostname string
|
||||
Port string
|
||||
Username string
|
||||
Path string
|
||||
Token string
|
||||
DevstarUsername string
|
||||
Domain string
|
||||
}
|
||||
|
||||
// replaceIDETemplate replaces placeholders in the IDE URL template with actual values
|
||||
func replaceIDETemplate(template string, params IDETemplateParams) string {
|
||||
result := template
|
||||
result = strings.ReplaceAll(result, "{host}", params.Host)
|
||||
result = strings.ReplaceAll(result, "{hostname}", params.Hostname)
|
||||
result = strings.ReplaceAll(result, "{port}", params.Port)
|
||||
result = strings.ReplaceAll(result, "{username}", params.Username)
|
||||
result = strings.ReplaceAll(result, "{path}", params.Path)
|
||||
result = strings.ReplaceAll(result, "{token}", params.Token)
|
||||
result = strings.ReplaceAll(result, "{devstar_username}", params.DevstarUsername)
|
||||
result = strings.ReplaceAll(result, "{domain}", params.Domain)
|
||||
return result
|
||||
}
|
||||
|
||||
// 获取仓库 Dev Container 详细信息
|
||||
// GET /{username}/{reponame}/devcontainer
|
||||
func GetDevContainerDetails(ctx *context.Context) {
|
||||
@@ -159,10 +194,46 @@ func GetDevContainerDetails(ctx *context.Context) {
|
||||
}
|
||||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||||
if err == nil {
|
||||
ctx.Data["VSCodeUrl"] = "vscode" + terminalURL
|
||||
ctx.Data["CursorUrl"] = "cursor" + terminalURL
|
||||
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL
|
||||
ctx.Data["TraeUrl"] = "trae" + terminalURL
|
||||
// Parse terminalURL to extract parameter values
|
||||
// terminalURL format: ://mengning.devstar/openProject?host=xxx&hostname=xxx&port=xxx&username=xxx&path=xxx&access_token=xxx&devstar_username=xxx&devstar_domain=xxx
|
||||
// Add a fake scheme to make it parseable
|
||||
parsedURL, err := url.Parse("http://dummy" + terminalURL)
|
||||
if err == nil {
|
||||
params := IDETemplateParams{
|
||||
Host: parsedURL.Query().Get("host"),
|
||||
Hostname: parsedURL.Query().Get("hostname"),
|
||||
Port: parsedURL.Query().Get("port"),
|
||||
Username: parsedURL.Query().Get("username"),
|
||||
Path: parsedURL.Query().Get("path"),
|
||||
Token: parsedURL.Query().Get("access_token"),
|
||||
DevstarUsername: parsedURL.Query().Get("devstar_username"),
|
||||
Domain: parsedURL.Query().Get("devstar_domain"),
|
||||
}
|
||||
|
||||
// Get IDE apps from admin config or use local defaults
|
||||
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
|
||||
|
||||
// If admin config is empty, use local defaults
|
||||
if len(devContainerApps) == 0 {
|
||||
devContainerApps = setting.DefaultDevContainerEditorApps()
|
||||
log.Info("Using local default IDE configs: %d IDEs", len(devContainerApps))
|
||||
} else {
|
||||
log.Info("Using admin configured IDEs: %d IDEs", len(devContainerApps))
|
||||
}
|
||||
|
||||
var ideInfos []IDEInfo
|
||||
for _, app := range devContainerApps {
|
||||
finalURL := replaceIDETemplate(app.OpenURL, params)
|
||||
ideInfos = append(ideInfos, IDEInfo{
|
||||
Name: app.DisplayName,
|
||||
Logo: app.Logo,
|
||||
URL: finalURL,
|
||||
})
|
||||
}
|
||||
ctx.Data["DevContainerIDEs"] = ideInfos
|
||||
} else {
|
||||
log.Error("Failed to parse terminalURL: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. 携带数据渲染页面,返回
|
||||
@@ -214,48 +285,70 @@ func GetDevContainerStatus(ctx *context.Context) {
|
||||
log.Info("%v\n", err)
|
||||
}
|
||||
|
||||
var vscodeUrl, cursorUrl, windsurfUrl, traeUrl string
|
||||
var ideInfos []IDEInfo
|
||||
// 增加对 ctx.Doer、ctx.Repo、ctx.Repo.Repository 的检查,避免出现空指针异常
|
||||
// 只在第一次状态变为 "5" 时生成 URL,避免频繁调用导致 token 被删除,随 Session 过期(默认 24 小时)
|
||||
// 通过检查 session 中是否已有 terminal_url 来判断是否是第一次
|
||||
// 通过检查 session 中是否已有 terminal_ides 来判断是否是第一次
|
||||
if realTimeStatus == "5" && ctx.Doer != nil && ctx.Repo != nil && ctx.Repo.Repository != nil {
|
||||
// 检查 session 中是否已有缓存的 URL
|
||||
cachedUrls := ctx.Session.Get("terminal_urls")
|
||||
// 检查 session 中是否已有缓存的 IDE 信息
|
||||
cachedUrls := ctx.Session.Get("terminal_ides")
|
||||
if cachedUrls == nil {
|
||||
// 第一次状态为 "4",生成 URL 并缓存
|
||||
// 第一次状态为 "5",生成 URL 并缓存
|
||||
terminalURL, err := devcontainer_service.Get_IDE_TerminalURL(ctx, ctx.Doer, ctx.Repo)
|
||||
if err == nil {
|
||||
vscodeUrl = "vscode" + terminalURL
|
||||
cursorUrl = "cursor" + terminalURL
|
||||
windsurfUrl = "windsurf" + terminalURL
|
||||
traeUrl = "trae" + terminalURL
|
||||
// 缓存 URL 到 session
|
||||
ctx.Session.Set("terminal_urls", map[string]string{
|
||||
"vscodeUrl": vscodeUrl,
|
||||
"cursorUrl": cursorUrl,
|
||||
"windsurfUrl": windsurfUrl,
|
||||
"traeUrl": traeUrl,
|
||||
})
|
||||
// Parse terminalURL to extract parameter values
|
||||
parsedURL, err := url.Parse("http://dummy" + terminalURL)
|
||||
if err == nil {
|
||||
params := IDETemplateParams{
|
||||
Host: parsedURL.Query().Get("host"),
|
||||
Hostname: parsedURL.Query().Get("hostname"),
|
||||
Port: parsedURL.Query().Get("port"),
|
||||
Username: parsedURL.Query().Get("username"),
|
||||
Path: parsedURL.Query().Get("path"),
|
||||
Token: parsedURL.Query().Get("access_token"),
|
||||
DevstarUsername: parsedURL.Query().Get("devstar_username"),
|
||||
Domain: parsedURL.Query().Get("devstar_domain"),
|
||||
}
|
||||
|
||||
// Get IDE apps from admin config or use local defaults
|
||||
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
|
||||
|
||||
// If admin config is empty, use local defaults
|
||||
if len(devContainerApps) == 0 {
|
||||
devContainerApps = setting.DefaultDevContainerEditorApps()
|
||||
log.Info("Using local default IDE configs: %d IDEs", len(devContainerApps))
|
||||
} else {
|
||||
log.Info("Using admin configured IDEs: %d IDEs", len(devContainerApps))
|
||||
}
|
||||
|
||||
var ideInfos []IDEInfo
|
||||
for _, app := range devContainerApps {
|
||||
finalURL := replaceIDETemplate(app.OpenURL, params)
|
||||
ideInfos = append(ideInfos, IDEInfo{
|
||||
Name: app.DisplayName,
|
||||
Logo: app.Logo,
|
||||
URL: finalURL,
|
||||
})
|
||||
}
|
||||
// 缓存 IDE 信息到 session
|
||||
ctx.Session.Set("terminal_ides", ideInfos)
|
||||
} else {
|
||||
log.Error("Failed to parse terminalURL: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用缓存的 URL
|
||||
urls := cachedUrls.(map[string]string)
|
||||
vscodeUrl = urls["vscodeUrl"]
|
||||
cursorUrl = urls["cursorUrl"]
|
||||
windsurfUrl = urls["windsurfUrl"]
|
||||
traeUrl = urls["traeUrl"]
|
||||
// 使用缓存的 IDE 信息
|
||||
ides := cachedUrls.([]IDEInfo)
|
||||
ideInfos = ides
|
||||
}
|
||||
} else {
|
||||
// 状态不是 "4" 或 ctx.Doer 为 nil,清除缓存的 URL
|
||||
ctx.Session.Delete("terminal_urls")
|
||||
// 状态不是 "5" 或 ctx.Doer 为 nil,清除缓存的 IDE 信息
|
||||
ctx.Session.Delete("terminal_ides")
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{
|
||||
"status": realTimeStatus,
|
||||
"vscodeUrl": vscodeUrl,
|
||||
"cursorUrl": cursorUrl,
|
||||
"windsurfUrl": windsurfUrl,
|
||||
"traeUrl": traeUrl,
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"status": realTimeStatus,
|
||||
"ideURLs": ideInfos,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ func Variables(ctx *context.Context) {
|
||||
tagsJSONStr = "[]"
|
||||
}
|
||||
// 创建一个新的请求
|
||||
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
|
||||
req, err := http.NewRequest("GET", setting.ParentDomain + "/variables/export", nil)
|
||||
if err != nil {
|
||||
ctx.Data["DevstarVariables"] = []*devcontainer_model.DevcontainerVariable{}
|
||||
} else {
|
||||
@@ -237,7 +237,7 @@ func ScriptCreate(ctx *context.Context) {
|
||||
}
|
||||
if !exists {
|
||||
// 创建一个新的请求来获取devstar变量
|
||||
req, err := http.NewRequest("GET", "http://devstar.cn/variables/export", nil)
|
||||
req, err := http.NewRequest("GET", setting.ParentDomain + "/variables/export", nil)
|
||||
if err != nil {
|
||||
log.Error("Failed to create request for devstar variables: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
||||
|
||||
@@ -1134,7 +1134,7 @@ func Get_IDE_TerminalURL(ctx *gitea_context.Context, doer *user.User, repo *gite
|
||||
"&username=" + doer.Name +
|
||||
"&path=" + fullWorkPath +
|
||||
"&access_token=" + access_token +
|
||||
"&devstar_username=" + repo.Repository.OwnerName +
|
||||
"&devstar_username=" + doer.Name +
|
||||
"&devstar_domain=" + setting.AppURL
|
||||
|
||||
// 添加 forwardPorts 参数(如果存在)
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
<input type="hidden" name="key" value="{{$cfg.DynKey}}">
|
||||
<input name="value" value="{{$cfg.Value ctx}}" placeholder="{{$cfg.DefaultValue}}" maxlength="100" dir="auto" required pattern="^[A-Za-z0-9][\-_A-Za-z0-9]*$">
|
||||
</div>
|
||||
<div class="field tw-mt-4">
|
||||
<label>{{ctx.Locale.Tr "admin.config.devcontainer_editor_apps"}}</label>
|
||||
<details>
|
||||
<summary>{{ctx.Locale.Tr "admin.config.devcontainer_editor_app_help"}}</summary>
|
||||
<pre class="tw-px-4">{{.DefaultDevContainerEditorAppsString}}</pre>
|
||||
</details>
|
||||
</div>
|
||||
<div class="field">
|
||||
{{$cfg = .SystemConfig.Repository.DevContainerEditorApps}}
|
||||
<input type="hidden" name="key" value="{{$cfg.DynKey}}">
|
||||
<textarea name="value" rows="6" placeholder="VSCode = vscode Cursor = cursor">{{($cfg.Value ctx).ToTextareaString}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</div>
|
||||
|
||||
@@ -57,14 +57,12 @@
|
||||
{{if .isAdmin}}
|
||||
<div style=" display: none;" id="updateContainer" class="item"><a class="flex-text-inline" style="color:black; cursor:pointer; " href="#" onclick="if(typeof openSaveModal === 'function') { openSaveModal('{{.Repository.Link}}', '{{.Repository.Name}}'); return false; }">{{svg "octicon-database"}}{{ctx.Locale.Tr "repo.dev_container_control.update"}}</a></div>
|
||||
{{end}}
|
||||
|
||||
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-code" 14}}open with WebTerminal</a></div>
|
||||
<div style=" display: none;" id="vsTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.VSCodeUrl}}'">{{svg "octicon-code" 14}}open with VSCode</a ></div>
|
||||
<div style=" display: none;" id="cursorTerminal" class="item"><a class="flex-text-inline" style="color:black; " onclick="window.location.href = '{{.CursorUrl}}'">{{svg "octicon-code" 14}}open with Cursor</a ></div>
|
||||
<div style=" display: none;" id="windsurfTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.WindsurfUrl}}'">{{svg "octicon-code" 14}}open with Windsurf</a ></div>
|
||||
<div style=" display: none;" id="traeTerminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.TraeUrl}}'">{{svg "octicon-code" 14}}open with Trae</a ></div>
|
||||
|
||||
|
||||
<div style=" display: none;" id="webTerminal" class="item"><a class="flex-text-inline" style="color:black; " href="{{.WebSSHUrl}}" target="_blank">{{svg "octicon-terminal" 14}}open with WebTerminal</a></div>
|
||||
{{range .DevContainerIDEs}}
|
||||
<div style=" display: none;" id="{{.Name}}Terminal" class="item"><a class="flex-text-inline" style="color:black;" onclick="window.location.href = '{{.URL}}'">{{svg "octicon-terminal" 14}}open with {{.Name}}</a></div>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
{{if .ValidateDevContainerConfiguration}}
|
||||
<div style=" display: none;" id="createContainer" class="item">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,13 +15,12 @@ function initDevContainerDetails() {
|
||||
const restartContainer = document.getElementById('restartContainer');
|
||||
const stopContainer = document.getElementById('stopContainer');
|
||||
const webTerminal = document.getElementById('webTerminal');
|
||||
const vsTerminal = document.getElementById('vsTerminal');
|
||||
const cursorTerminal = document.getElementById('cursorTerminal');
|
||||
const windsurfTerminal = document.getElementById('windsurfTerminal');
|
||||
const traeTerminal = document.getElementById('traeTerminal');
|
||||
const webTerminalContainer = document.getElementById('webTerminalContainer');
|
||||
const loadingElement = document.getElementById('loading');
|
||||
|
||||
// Dynamically get all IDE terminal buttons
|
||||
const ideTerminals = document.querySelectorAll('[id$="Terminal"]:not(#webTerminal)');
|
||||
|
||||
function concealElement() {
|
||||
if (createContainer) createContainer.style.display = 'none';
|
||||
if (deleteContainer) deleteContainer.style.display = 'none';
|
||||
@@ -29,10 +28,10 @@ function initDevContainerDetails() {
|
||||
if (restartContainer) restartContainer.style.display = 'none';
|
||||
if (stopContainer) stopContainer.style.display = 'none';
|
||||
if (webTerminal) webTerminal.style.display = 'none';
|
||||
if (vsTerminal) vsTerminal.style.display = 'none';
|
||||
if (cursorTerminal) cursorTerminal.style.display = 'none';
|
||||
if (windsurfTerminal) windsurfTerminal.style.display = 'none';
|
||||
if (traeTerminal) traeTerminal.style.display = 'none';
|
||||
// Hide all IDE terminals
|
||||
ideTerminals.forEach((terminal) => {
|
||||
(terminal as HTMLElement).style.display = 'none';
|
||||
});
|
||||
if (webTerminalContainer) webTerminalContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -42,10 +41,10 @@ function initDevContainerDetails() {
|
||||
if (restartContainer) restartContainer.style.display = 'block';
|
||||
if (stopContainer) stopContainer.style.display = 'block';
|
||||
if (webTerminal) webTerminal.style.display = 'block';
|
||||
if (vsTerminal) vsTerminal.style.display = 'block';
|
||||
if (cursorTerminal) cursorTerminal.style.display = 'block';
|
||||
if (windsurfTerminal) windsurfTerminal.style.display = 'block';
|
||||
if (traeTerminal) traeTerminal.style.display = 'block';
|
||||
// Show all IDE terminals
|
||||
ideTerminals.forEach((terminal) => {
|
||||
(terminal as HTMLElement).style.display = 'block';
|
||||
});
|
||||
if (webTerminalContainer) webTerminalContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -81,21 +80,37 @@ function initDevContainerDetails() {
|
||||
if (loadingElement) loadingElement.style.display = 'none';
|
||||
if (restartContainer) restartContainer.style.display = 'none';
|
||||
}
|
||||
if (data.vscodeUrl) {
|
||||
const vsBtn = document.querySelector('#vsTerminal a');
|
||||
if (vsBtn) vsBtn.setAttribute('onclick', `window.location.href = '${data.vscodeUrl}'`);
|
||||
}
|
||||
if (data.cursorUrl) {
|
||||
const cursorBtn = document.querySelector('#cursorTerminal a');
|
||||
if (cursorBtn) cursorBtn.setAttribute('onclick', `window.location.href = '${data.cursorUrl}'`);
|
||||
}
|
||||
if (data.windsurfUrl) {
|
||||
const windsurfBtn = document.querySelector('#windsurfTerminal a');
|
||||
if (windsurfBtn) windsurfBtn.setAttribute('onclick', `window.location.href = '${data.windsurfUrl}'`);
|
||||
}
|
||||
if (data.traeUrl) {
|
||||
const traeBtn = document.querySelector('#traeTerminal a');
|
||||
if (traeBtn) traeBtn.setAttribute('onclick', `window.location.href = '${data.traeUrl}'`);
|
||||
// Update IDE URLs dynamically
|
||||
if (data.ideURLs && Array.isArray(data.ideURLs)) {
|
||||
for (const ideInfo of data.ideURLs) {
|
||||
const terminal = document.getElementById(`${ideInfo.Name}Terminal`);
|
||||
if (terminal) {
|
||||
const link = terminal.querySelector('a');
|
||||
if (link) {
|
||||
link.setAttribute('onclick', `window.location.href = '${ideInfo.URL}'`);
|
||||
}
|
||||
// Update logo if available
|
||||
if (ideInfo.Logo) {
|
||||
const icon = terminal.querySelector('svg');
|
||||
if (icon) {
|
||||
// Clone the SVG to preserve it for fallback
|
||||
const svgClone = icon.cloneNode(true) as SVGElement;
|
||||
// Replace SVG with img if logo is available
|
||||
const imgElement = document.createElement('img');
|
||||
imgElement.src = ideInfo.Logo;
|
||||
imgElement.alt = ideInfo.Name;
|
||||
imgElement.style.width = '14px';
|
||||
imgElement.style.height = '14px';
|
||||
// Add error handler to restore default SVG if logo fails to load
|
||||
imgElement.onerror = function() {
|
||||
console.warn(`Failed to load logo for ${ideInfo.Name}: ${ideInfo.Logo}`);
|
||||
this.replaceWith(svgClone);
|
||||
};
|
||||
icon.replaceWith(imgElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
displayElement();
|
||||
if (loadingElement) loadingElement.style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user