Compare commits

...

9 Commits

Author SHA1 Message Date
416c954119 t添加setting.ParentDomain 和 setting.ParentAccessToken
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 30m22s
2026-01-13 14:25:22 +08:00
e4baca8811 Merge remote-tracking branch 'origin/main' into feat/devcontainer-ide-config
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 29m15s
Resolved conflicts by keeping dynamic IDE configuration from HEAD,
as it enhances the hardcoded version from origin/main.
2026-01-12 18:58:16 +08:00
a1ea929a8b fix: add marshaller for DevContainerEditorApps config
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 27m54s
2026-01-12 17:09:39 +08:00
85d3022496 fix: use current user for devstar_username parameter 2026-01-12 17:01:21 +08:00
ebd7d0753c refactor: remove redundant cmd parameter from IDE URLs (#69)
Some checks are pending
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (push) Waiting to run
## Summary

  The `cmd` parameter in devcontainer IDE terminal URLs is redundant since the IDE type is already specified by the URL protocol prefix (`vscode://`, `cursor://`, `windsurf://`, `trae://`). This change simplifies the URL generation and removes unnecessary parameters that duplicate information already present in the protocol scheme.

  ## Changes

  - Removed `&cmd=code`, `&cmd=cursor`, `&cmd=windsurf`, `&cmd=trae` from IDE terminal URLs in `routers/web/devcontainer/devcontainer.go`

  ## Before

  vscode://mengning.devstar/openProject?host=...&cmd=code
  cursor://mengning.devstar/openProject?host=...&cmd=cursor
  windsurf://mengning.devstar/openProject?host=...&cmd=windsurf
  trae://mengning.devstar/openProject?host=...&cmd=trae

  ## After

  vscode://mengning.devstar/openProject?host=...
  cursor://mengning.devstar/openProject?host=...
  windsurf://mengning.devstar/openProject?host=...
  trae://mengning.devstar/openProject?host=...

Reviewed-on: #69
Co-authored-by: Br1an67 <932039080@qq.com>
Co-committed-by: Br1an67 <932039080@qq.com>
2026-01-12 08:35:34 +00:00
98e8fec2d6 feat: add configurable DevContainer IDE support via admin settings 2026-01-12 16:18:47 +08:00
0333d323e0 refactor: remove redundant cmd parameter from IDE URLs
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 29m12s
2026-01-12 15:55:38 +08:00
51e4189dc9 Merge pull request 'Fix: Multiple JavaScript errors on Settings page' (#68) from fix/setting-page-errors into main
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (push) Failing after 35m59s
Reviewed-on: #68
2026-01-12 07:25:15 +00:00
8115eb4b6f Fix: Multiple JavaScript errors on Settings page
Some checks failed
DevStar Studio CI/CD Pipeline / DevStarStudio-CICD-Pipeline (pull_request) Failing after 39m44s
2026-01-12 12:43:00 +08:00
15 changed files with 125 additions and 1170 deletions

View File

@@ -47,9 +47,37 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
}
}
// DevContainerEditorApp represents a configured IDE for DevContainer
type DevContainerEditorApp struct {
DisplayName string // Display name, e.g. "VSCode"
Protocol string // Protocol prefix, e.g. "vscode"
}
type DevContainerEditorAppsType []DevContainerEditorApp
// ToTextareaString converts the configuration to textarea format
func (t DevContainerEditorAppsType) ToTextareaString() string {
ret := ""
for _, app := range t {
ret += app.DisplayName + " = " + app.Protocol + "\n"
}
return ret
}
// DefaultDevContainerEditorApps returns the default DevContainer IDE configuration
func DefaultDevContainerEditorApps() DevContainerEditorAppsType {
return DevContainerEditorAppsType{
{DisplayName: "VSCode", Protocol: "vscode"},
{DisplayName: "Cursor", Protocol: "cursor"},
{DisplayName: "Windsurf", Protocol: "windsurf"},
{DisplayName: "Trae", Protocol: "trae"},
}
}
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 +98,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"),
},
}
}

View File

@@ -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

View File

@@ -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
},
}
}

View File

@@ -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

View File

@@ -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=禁用差异对比语法高亮

View File

@@ -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)
}
@@ -236,10 +237,11 @@ func ChangeConfig(ctx *context.Context) {
return json.Marshal(openWithEditorApps)
}
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(): marshalOpenWithApps,
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
}
_ = ctx.Req.ParseForm()

View File

@@ -159,10 +159,14 @@ 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 + "&cmd=code"
ctx.Data["CursorUrl"] = "cursor" + terminalURL + "&cmd=cursor"
ctx.Data["WindsurfUrl"] = "windsurf" + terminalURL + "&cmd=windsurf"
ctx.Data["TraeUrl"] = "trae" + terminalURL + "&cmd=trae"
// Get configured IDE apps dynamically
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
ideURLs := make(map[string]string)
for _, app := range devContainerApps {
url := app.Protocol + terminalURL
ideURLs[app.DisplayName] = url
}
ctx.Data["DevContainerIDEs"] = ideURLs
}
}
// 3. 携带数据渲染页面,返回
@@ -214,7 +218,7 @@ func GetDevContainerStatus(ctx *context.Context) {
log.Info("%v\n", err)
}
var vscodeUrl, cursorUrl, windsurfUrl, traeUrl string
ideURLs := make(map[string]string)
// 增加对 ctx.Doer、ctx.Repo、ctx.Repo.Repository 的检查,避免出现空指针异常
// 只在第一次状态变为 "5" 时生成 URL避免频繁调用导致 token 被删除,随 Session 过期(默认 24 小时)
// 通过检查 session 中是否已有 terminal_url 来判断是否是第一次
@@ -222,40 +226,31 @@ func GetDevContainerStatus(ctx *context.Context) {
// 检查 session 中是否已有缓存的 URL
cachedUrls := ctx.Session.Get("terminal_urls")
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 + "&cmd=code"
cursorUrl = "cursor" + terminalURL + "&cmd=cursor"
windsurfUrl = "windsurf" + terminalURL + "&cmd=windsurf"
traeUrl = "trae" + terminalURL + "&cmd=trae"
// Get configured IDE apps dynamically
devContainerApps := setting.Config().Repository.DevContainerEditorApps.Value(ctx)
for _, app := range devContainerApps {
url := app.Protocol + terminalURL
ideURLs[app.DisplayName] = url
}
// 缓存 URL 到 session
ctx.Session.Set("terminal_urls", map[string]string{
"vscodeUrl": vscodeUrl,
"cursorUrl": cursorUrl,
"windsurfUrl": windsurfUrl,
"traeUrl": traeUrl,
})
ctx.Session.Set("terminal_urls", ideURLs)
}
} else {
// 使用缓存的 URL
urls := cachedUrls.(map[string]string)
vscodeUrl = urls["vscodeUrl"]
cursorUrl = urls["cursorUrl"]
windsurfUrl = urls["windsurfUrl"]
traeUrl = urls["traeUrl"]
ideURLs = urls
}
} else {
// 状态不是 "4" 或 ctx.Doer 为 nil清除缓存的 URL
// 状态不是 "5" 或 ctx.Doer 为 nil清除缓存的 URL
ctx.Session.Delete("terminal_urls")
}
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": ideURLs,
})
}

View File

@@ -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"))

View File

@@ -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 参数(如果存在)

View File

@@ -1,4 +1,3 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
<!-- 自定义logo upload -->
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.config.app_logo_config"}}

View File

@@ -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&#10;Cursor = cursor">{{($cfg.Value ctx).ToTextareaString}}</textarea>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>

View File

@@ -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-code" 14}}open with WebTerminal</a></div>
{{range $name, $url := .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-code" 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

View File

@@ -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,17 @@ 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) {
for (const [ideName, url] of Object.entries(data.ideURLs)) {
const terminal = document.getElementById(`${ideName}Terminal`);
if (terminal) {
const link = terminal.querySelector('a');
if (link) {
link.setAttribute('onclick', `window.location.href = '${url}'`);
}
}
}
}
displayElement();
if (loadingElement) loadingElement.style.display = 'none';

View File

@@ -11,6 +11,14 @@ import {initFomanticTab} from './fomantic/tab.ts';
export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
export function initGiteaFomantic() {
// Guard against race conditions where fomantic.js hasn't been fully initialized yet.
// This can happen on pages like Settings when document.readyState is already 'interactive'
// or 'complete' when index.js executes, causing onDomReady to fire immediately.
if (!$.fn.dropdown || !$.fn.dropdown.settings) {
console.warn('Fomantic UI dropdown not initialized yet, skipping initGiteaFomantic');
return;
}
// our extensions
$.fn.fomanticExt = {};
// By default, use "exact match" for full text search