diff --git a/.gitignore b/.gitignore
index 8797862..f7f5548 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,8 @@
/*.test
/agentdock.killed*
-# Windows WPF 控制面板的本地构建中间文件。
+# Windows WPF 控制面板的本地构建产物。
+/desktop/windows/control-panel/bin/
/desktop/windows/control-panel/obj/
# Python 辅助脚本的本地缓存不得进入 Skill 源码和发布包。
diff --git a/README.md b/README.md
index 9c9524b..416f8aa 100644
--- a/README.md
+++ b/README.md
@@ -284,7 +284,7 @@ After you save the plugin, the browser opens the AgentDock authorization page. C
A public endpoint must use HTTPS. `AGENTDOCK_SERVER_URL` must contain only the origin, without `/mcp`. See [Connect ChatGPT to AgentDock](https://uvwt.github.io/agentdock-docs/docs/guides/chatgpt) for the complete procedure, endpoint checks, and troubleshooting.
-On Windows desktop installs, create `oauth-access-token-ttl.txt` in the runtime root to persist the access-token lifetime using the same syntax as `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL`. The file overrides an inherited environment variable; for example, `never` keeps standard startup and elevated scheduled-task startup consistent. The desktop runtime also normalizes an accidentally pasted `https://agentdock.example.com/mcp` URL back to its origin.
+On Windows desktop installs, configure the OAuth access-token TTL from Control Panel > Advanced Settings. It accepts the same syntax as `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL`, including `1h`, `30d`, and `never`. Once saved, standard startup and elevated scheduled-task startup read the same `control-panel-settings.json`; leaving the field blank falls back to an inherited `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL`, and if neither is set the core default is `1h`. The desktop runtime also normalizes an accidentally pasted `https://agentdock.example.com/mcp` URL back to its origin.
## Image variants
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 43c6f1f..1804cf9 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -286,7 +286,7 @@ https://agentdock.example.com/mcp
公网入口必须使用 HTTPS,`AGENTDOCK_SERVER_URL` 只填写 Origin,不附加 `/mcp`。完整步骤、端点验证和常见问题见 [ChatGPT 接入教程](https://uvwt.github.io/agentdock-docs/zh-CN/docs/guides/chatgpt)。
-Windows 桌面服务可在运行目录创建 `oauth-access-token-ttl.txt` 持久化 Access Token 有效期,内容语法与 `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL` 相同。文件值优先于继承的环境变量;例如写入 `never` 可确保普通启动和管理员计划任务使用一致的不失效策略。桌面端也会把误填的 `https://agentdock.example.com/mcp` 自动规范为 Origin。
+Windows 桌面端可在控制面板“高级设置”中配置 OAuth Access Token TTL,支持 `1h`、`30d`、`never` 等与 `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL` 相同的语法。保存后普通启动和管理员计划任务都会读取同一份 `control-panel-settings.json`;留空时才继承启动环境里的 `AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL`,两者都未设置则使用核心默认的 `1h`。桌面端也会把误填的 `https://agentdock.example.com/mcp` 自动规范为 Origin。
## 镜像版本
diff --git a/desktop/windows/control-panel/MainWindow.xaml b/desktop/windows/control-panel/MainWindow.xaml
index 4223414..622cd84 100644
--- a/desktop/windows/control-panel/MainWindow.xaml
+++ b/desktop/windows/control-panel/MainWindow.xaml
@@ -165,6 +165,7 @@
+
@@ -185,6 +186,11 @@
+
+
+
+
+
diff --git a/desktop/windows/control-panel/MainWindow.xaml.cs b/desktop/windows/control-panel/MainWindow.xaml.cs
index ae8d4a2..616b052 100644
--- a/desktop/windows/control-panel/MainWindow.xaml.cs
+++ b/desktop/windows/control-panel/MainWindow.xaml.cs
@@ -100,6 +100,7 @@ private void ApplySnapshot(RuntimeSnapshot snapshot)
PortTextBox.Text = snapshot.Settings.Port.ToString();
SelectLogLevel(snapshot.Settings.LogLevel);
NexusEndpointTextBox.Text = snapshot.Settings.NexusEndpoint;
+ OAuthAccessTokenTtlTextBox.Text = snapshot.Settings.OAuthAccessTokenTtl;
BrowserEnabledCheckBox.IsChecked = snapshot.Settings.BrowserEnabled;
BrowserCdpUrlTextBox.Text = snapshot.Settings.BrowserCdpUrl;
BrowserReuseExistingCdpCheckBox.IsChecked = snapshot.Settings.BrowserReuseExistingCdp;
@@ -321,6 +322,7 @@ private async void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
Port = port,
LogLevel = SelectedLogLevel(),
NexusEndpoint = NexusEndpointTextBox.Text.Trim(),
+ OAuthAccessTokenTtl = OAuthAccessTokenTtlTextBox.Text.Trim(),
BrowserEnabled = BrowserEnabledCheckBox.IsChecked == true,
BrowserCdpUrl = BrowserCdpUrlTextBox.Text.Trim(),
BrowserReuseExistingCdp = BrowserReuseExistingCdpCheckBox.IsChecked == true,
diff --git a/desktop/windows/control-panel/Models/RuntimeModels.cs b/desktop/windows/control-panel/Models/RuntimeModels.cs
index 528393d..130d9f6 100644
--- a/desktop/windows/control-panel/Models/RuntimeModels.cs
+++ b/desktop/windows/control-panel/Models/RuntimeModels.cs
@@ -64,6 +64,9 @@ public sealed class ControlPanelSettings
[JsonPropertyName("nexus_endpoint")]
public string NexusEndpoint { get; set; } = "";
+ [JsonPropertyName("oauth_access_token_ttl")]
+ public string OAuthAccessTokenTtl { get; set; } = "";
+
[JsonPropertyName("browser_enabled")]
public bool BrowserEnabled { get; set; }
diff --git a/desktop/windows/control-panel/Services/RuntimeService.cs b/desktop/windows/control-panel/Services/RuntimeService.cs
index b2246f9..e4cc38e 100644
--- a/desktop/windows/control-panel/Services/RuntimeService.cs
+++ b/desktop/windows/control-panel/Services/RuntimeService.cs
@@ -227,6 +227,7 @@ public async Task SaveSettingsAsync(
"--port", settings.Port.ToString(),
"--log-level", settings.LogLevel,
"--nexus-endpoint", settings.NexusEndpoint ?? "",
+ "--oauth-access-token-ttl", settings.OAuthAccessTokenTtl ?? "",
$"--browser-enabled={settings.BrowserEnabled.ToString().ToLowerInvariant()}",
"--browser-cdp-url", settings.BrowserCdpUrl ?? "",
$"--browser-reuse-existing-cdp={settings.BrowserReuseExistingCdp.ToString().ToLowerInvariant()}",
diff --git a/internal/config/config.go b/internal/config/config.go
index 18f892f..4a35958 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -471,22 +471,48 @@ func getenvOAuthAccessTokenTTL(key string, fallback int64) (seconds int64, never
if value == "" {
return fallback, false, nil
}
+ seconds, never, err = parseOAuthAccessTokenTTL(value)
+ if err != nil {
+ return 0, false, fmt.Errorf("parse %s as duration: %w", key, err)
+ }
+ return seconds, never, nil
+}
+
+// ValidateOAuthAccessTokenTTL validates an explicit access-token lifetime using
+// the same syntax and bounds as AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL.
+func ValidateOAuthAccessTokenTTL(value string) error {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ return errors.New("value is empty")
+ }
+ seconds, never, err := parseOAuthAccessTokenTTL(value)
+ if err != nil {
+ return err
+ }
+ if !never && (seconds < int64(time.Minute/time.Second) || seconds > maxOAuthAccessTokenTTLSeconds) {
+ return fmt.Errorf("value must be between 1m and 999999d: %ds", seconds)
+ }
+ return nil
+}
+
+func parseOAuthAccessTokenTTL(value string) (seconds int64, never bool, err error) {
+ value = strings.TrimSpace(value)
if strings.EqualFold(value, "never") {
return 0, true, nil
}
if strings.HasSuffix(strings.ToLower(value), "d") {
days, err := strconv.ParseInt(strings.TrimSpace(value[:len(value)-1]), 10, 64)
if err != nil || days <= 0 || days > maxOAuthAccessTokenTTLSeconds/(24*60*60) {
- return 0, false, fmt.Errorf("parse %s as duration: invalid day count %q", key, value)
+ return 0, false, fmt.Errorf("invalid day count %q", value)
}
return days * 24 * 60 * 60, false, nil
}
parsed, err := time.ParseDuration(value)
if err != nil {
- return 0, false, fmt.Errorf("parse %s as duration: %w", key, err)
+ return 0, false, err
}
if parsed%time.Second != 0 {
- return 0, false, fmt.Errorf("parse %s as duration: value must use whole seconds", key)
+ return 0, false, errors.New("value must use whole seconds")
}
return int64(parsed / time.Second), false, nil
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index da301ed..e914505 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -257,6 +257,19 @@ func TestFromEnvParsesNeverExpiringOAuthAccessToken(t *testing.T) {
}
}
+func TestValidateOAuthAccessTokenTTL(t *testing.T) {
+ for _, value := range []string{"1m", "24h", "30d", "999999d", "never"} {
+ if err := ValidateOAuthAccessTokenTTL(value); err != nil {
+ t.Fatalf("ValidateOAuthAccessTokenTTL(%q) error = %v", value, err)
+ }
+ }
+ for _, value := range []string{"", "59s", "1000000d", "1.5s", "invalid"} {
+ if err := ValidateOAuthAccessTokenTTL(value); err == nil {
+ t.Fatalf("ValidateOAuthAccessTokenTTL(%q) accepted invalid value", value)
+ }
+ }
+}
+
func TestNormalizeValidatesOAuthAccessTokenTTL(t *testing.T) {
home := t.TempDir()
for _, test := range []struct {
diff --git a/internal/desktopruntime/config_command.go b/internal/desktopruntime/config_command.go
index 8e2948d..ac29cb4 100644
--- a/internal/desktopruntime/config_command.go
+++ b/internal/desktopruntime/config_command.go
@@ -8,6 +8,8 @@ import (
"io"
"net/url"
"strings"
+
+ agentconfig "github.com/uvwt/agentdock/internal/config"
)
// ConfigUpdateRequest 是桌面端保存日常运行设置时使用的结构化请求。
@@ -17,6 +19,7 @@ type ConfigUpdateRequest struct {
LogLevel string
NexusEndpoint string
NexusTokenFile string
+ OAuthAccessTokenTTL string
BrowserEnabled bool
BrowserCDPURL string
BrowserReuseExistingCDP bool
@@ -37,6 +40,7 @@ func RunConfigCommand(ctx context.Context, args []string, stdout, stderr io.Writ
logLevel := flags.String("log-level", "info", "日志级别")
nexusEndpoint := flags.String("nexus-endpoint", "", "Nexus endpoint")
nexusTokenFile := flags.String("nexus-token-file", "", "Nexus Token 临时文件")
+ oauthAccessTokenTTL := flags.String("oauth-access-token-ttl", "", "OAuth Access Token 有效期;留空表示继承环境变量或使用默认值")
browserEnabled := flags.Bool("browser-enabled", false, "启用浏览器")
browserCDPURL := flags.String("browser-cdp-url", "", "已有 Chromium CDP 地址")
browserReuseExistingCDP := flags.Bool("browser-reuse-existing-cdp", false, "自动发现并复用唯一已有 CDP")
@@ -54,6 +58,7 @@ func RunConfigCommand(ctx context.Context, args []string, stdout, stderr io.Writ
LogLevel: strings.ToLower(strings.TrimSpace(*logLevel)),
NexusEndpoint: strings.TrimSpace(*nexusEndpoint),
NexusTokenFile: strings.TrimSpace(*nexusTokenFile),
+ OAuthAccessTokenTTL: strings.TrimSpace(*oauthAccessTokenTTL),
BrowserEnabled: *browserEnabled,
BrowserCDPURL: strings.TrimSpace(*browserCDPURL),
BrowserReuseExistingCDP: *browserReuseExistingCDP,
@@ -88,6 +93,11 @@ func validateConfigUpdate(request ConfigUpdateRequest) error {
if strings.ContainsAny(request.NexusEndpoint, "\r\n") {
return errors.New("配置值不能包含换行符")
}
+ if request.OAuthAccessTokenTTL != "" {
+ if err := agentconfig.ValidateOAuthAccessTokenTTL(request.OAuthAccessTokenTTL); err != nil {
+ return fmt.Errorf("OAuth Access Token 有效期无效: %w", err)
+ }
+ }
if request.BrowserCDPURL != "" {
parsed, err := url.Parse(request.BrowserCDPURL)
if err != nil || parsed.Host == "" {
diff --git a/internal/desktopruntime/config_command_test.go b/internal/desktopruntime/config_command_test.go
index 9bb7b9e..216c4b4 100644
--- a/internal/desktopruntime/config_command_test.go
+++ b/internal/desktopruntime/config_command_test.go
@@ -25,6 +25,17 @@ func TestValidateConfigUpdate(t *testing.T) {
t.Fatalf("valid ACP config rejected: %v", err)
}
+ validTTL := valid
+ validTTL.OAuthAccessTokenTTL = "30d"
+ if err := validateConfigUpdate(validTTL); err != nil {
+ t.Fatalf("valid OAuth access token TTL rejected: %v", err)
+ }
+ validNeverTTL := valid
+ validNeverTTL.OAuthAccessTokenTTL = "never"
+ if err := validateConfigUpdate(validNeverTTL); err != nil {
+ t.Fatalf("valid never-expiring OAuth access token TTL rejected: %v", err)
+ }
+
cases := []ConfigUpdateRequest{
{Port: 8765, LogLevel: "info"},
{RuntimeRoot: "runtime", Port: 0, LogLevel: "info"},
@@ -34,6 +45,8 @@ func TestValidateConfigUpdate(t *testing.T) {
{RuntimeRoot: "runtime", Port: 8765, LogLevel: "info", BrowserCDPURL: "http://user:pass@browser.internal:9222"},
{RuntimeRoot: "runtime", Port: 8765, LogLevel: "info", BrowserCDPURL: "http://browser.internal:9222/#fragment"},
{RuntimeRoot: "runtime", Port: 8765, LogLevel: "info", ACPEnabled: true, ACPAgent: "other"},
+ {RuntimeRoot: "runtime", Port: 8765, LogLevel: "info", OAuthAccessTokenTTL: "59s"},
+ {RuntimeRoot: "runtime", Port: 8765, LogLevel: "info", OAuthAccessTokenTTL: "1000000d"},
}
for _, request := range cases {
if err := validateConfigUpdate(request); err == nil {
diff --git a/internal/desktopruntime/config_windows.go b/internal/desktopruntime/config_windows.go
index 6be24fb..69bf91d 100644
--- a/internal/desktopruntime/config_windows.go
+++ b/internal/desktopruntime/config_windows.go
@@ -121,6 +121,7 @@ func platformUpdateConfig(ctx context.Context, request ConfigUpdateRequest) erro
Port: request.Port,
LogLevel: request.LogLevel,
NexusEndpoint: request.NexusEndpoint,
+ OAuthAccessTokenTTL: request.OAuthAccessTokenTTL,
BrowserEnabled: request.BrowserEnabled,
BrowserCDPURL: request.BrowserCDPURL,
BrowserReuseExistingCDP: request.BrowserReuseExistingCDP,
diff --git a/internal/desktopruntime/service_environment_windows.go b/internal/desktopruntime/service_environment_windows.go
index 9049c63..e6da8d9 100644
--- a/internal/desktopruntime/service_environment_windows.go
+++ b/internal/desktopruntime/service_environment_windows.go
@@ -13,6 +13,7 @@ import (
"strings"
"unsafe"
+ agentconfig "github.com/uvwt/agentdock/internal/config"
"golang.org/x/sys/windows"
)
@@ -45,6 +46,7 @@ type controlPanelSettings struct {
Port int `json:"port"`
LogLevel string `json:"log_level"`
NexusEndpoint string `json:"nexus_endpoint"`
+ OAuthAccessTokenTTL string `json:"oauth_access_token_ttl,omitempty"`
BrowserEnabled bool `json:"browser_enabled"`
BrowserCDPURL string `json:"browser_cdp_url"`
BrowserReuseExistingCDP bool `json:"browser_reuse_existing_cdp"`
@@ -63,7 +65,7 @@ func platformPrepareCoreEnvironment(runtimeRoot string) error {
if err != nil {
return err
}
- oauthAccessTokenTTL := strings.TrimSpace(os.Getenv("AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL"))
+ inheritedOAuthAccessTokenTTL := strings.TrimSpace(os.Getenv("AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL"))
for _, name := range managedCoreEnvironment {
if err := os.Unsetenv(name); err != nil {
@@ -141,13 +143,7 @@ func platformPrepareCoreEnvironment(runtimeRoot string) error {
managed["AGENTDOCK_OAUTH_PASSWORD"] = oauthPassword
managed["AGENTDOCK_OAUTH_TOKEN_SECRET"] = oauthSecret
}
- storedOAuthAccessTokenTTL, err := readTrimmedText(filepath.Join(root, "oauth-access-token-ttl.txt"))
- if err != nil {
- return err
- }
- if storedOAuthAccessTokenTTL != "" {
- oauthAccessTokenTTL = storedOAuthAccessTokenTTL
- }
+ oauthAccessTokenTTL := effectiveOAuthAccessTokenTTL(settings.OAuthAccessTokenTTL, inheritedOAuthAccessTokenTTL)
if oauthAccessTokenTTL != "" {
managed["AGENTDOCK_OAUTH_ACCESS_TOKEN_TTL"] = oauthAccessTokenTTL
}
@@ -183,6 +179,12 @@ func loadControlPanelSettings(runtimeRoot string, fallbackPort int) (controlPane
return controlPanelSettings{}, fmt.Errorf("不支持的日志级别: %s", settings.LogLevel)
}
settings.NexusEndpoint = strings.TrimSpace(settings.NexusEndpoint)
+ settings.OAuthAccessTokenTTL = strings.TrimSpace(settings.OAuthAccessTokenTTL)
+ if settings.OAuthAccessTokenTTL != "" {
+ if err := agentconfig.ValidateOAuthAccessTokenTTL(settings.OAuthAccessTokenTTL); err != nil {
+ return controlPanelSettings{}, fmt.Errorf("OAuth Access Token 有效期无效: %w", err)
+ }
+ }
settings.ACPAgent = strings.ToLower(strings.TrimSpace(settings.ACPAgent))
if settings.ACPAgent == "" {
settings.ACPAgent = "codex"
@@ -204,6 +206,13 @@ func loadControlPanelSettings(runtimeRoot string, fallbackPort int) (controlPane
return settings, nil
}
+func effectiveOAuthAccessTokenTTL(configured, inherited string) string {
+ if configured = strings.TrimSpace(configured); configured != "" {
+ return configured
+ }
+ return strings.TrimSpace(inherited)
+}
+
func readOptionalProtectedText(path, entropy string) (string, error) {
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return "", nil
diff --git a/internal/desktopruntime/service_environment_windows_test.go b/internal/desktopruntime/service_environment_windows_test.go
new file mode 100644
index 0000000..7508ebb
--- /dev/null
+++ b/internal/desktopruntime/service_environment_windows_test.go
@@ -0,0 +1,51 @@
+//go:build windows
+
+package desktopruntime
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestEffectiveOAuthAccessTokenTTL(t *testing.T) {
+ cases := []struct {
+ name string
+ configured string
+ inherited string
+ want string
+ }{
+ {name: "persisted wins", configured: "30d", inherited: "1h", want: "30d"},
+ {name: "env fallback", inherited: "24h", want: "24h"},
+ {name: "core default", want: ""},
+ }
+ for _, test := range cases {
+ t.Run(test.name, func(t *testing.T) {
+ if got := effectiveOAuthAccessTokenTTL(test.configured, test.inherited); got != test.want {
+ t.Fatalf("effectiveOAuthAccessTokenTTL(%q, %q) = %q, want %q", test.configured, test.inherited, got, test.want)
+ }
+ })
+ }
+}
+
+func TestLoadControlPanelSettingsValidatesOAuthAccessTokenTTL(t *testing.T) {
+ root := t.TempDir()
+ settingsPath := filepath.Join(root, "control-panel-settings.json")
+ if err := os.WriteFile(settingsPath, []byte(`{"port":8765,"log_level":"info","oauth_access_token_ttl":"never"}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ settings, err := loadControlPanelSettings(root, 8765)
+ if err != nil {
+ t.Fatalf("loadControlPanelSettings() error = %v", err)
+ }
+ if settings.OAuthAccessTokenTTL != "never" {
+ t.Fatalf("OAuthAccessTokenTTL = %q, want never", settings.OAuthAccessTokenTTL)
+ }
+
+ if err := os.WriteFile(settingsPath, []byte(`{"port":8765,"log_level":"info","oauth_access_token_ttl":"59s"}`), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := loadControlPanelSettings(root, 8765); err == nil {
+ t.Fatal("loadControlPanelSettings() accepted invalid OAuth access token TTL")
+ }
+}
diff --git a/internal/desktopruntime/service_startup_windows_test.go b/internal/desktopruntime/service_startup_windows_test.go
index 34963df..32e49cc 100644
--- a/internal/desktopruntime/service_startup_windows_test.go
+++ b/internal/desktopruntime/service_startup_windows_test.go
@@ -22,6 +22,21 @@ func TestParseScheduledTaskXMLAcceptsUTF16LE(t *testing.T) {
}
}
+func TestParseScheduledTaskXMLAcceptsUTF16BE(t *testing.T) {
+ runes := utf16.Encode([]rune(`true`))
+ data := []byte{0xfe, 0xff}
+ for _, value := range runes {
+ data = append(data, byte(value>>8), byte(value))
+ }
+ task, err := parseScheduledTaskXML(data)
+ if err != nil {
+ t.Fatalf("parseScheduledTaskXML() error = %v", err)
+ }
+ if task.Settings.Enabled == nil || !*task.Settings.Enabled {
+ t.Fatal("scheduled task should be enabled")
+ }
+}
+
func TestParseScheduledTaskXMLAcceptsUTF8(t *testing.T) {
task, err := parseScheduledTaskXML([]byte(`false`))
if err != nil {
diff --git a/scripts/install/manage-windows.ps1 b/scripts/install/manage-windows.ps1
index 6e63756..c2fad7f 100644
--- a/scripts/install/manage-windows.ps1
+++ b/scripts/install/manage-windows.ps1
@@ -536,58 +536,10 @@ function Invoke-LaunchCore {
if (-not (Test-Path -LiteralPath $AgentDockBinary -PathType Leaf)) {
throw "找不到 AgentDock 核心程序:$AgentDockBinary"
}
- Ensure-Credentials
- $settings = Get-ControlPanelSettings
-
- $env:AGENTDOCK_AUTH_TOKEN = Read-ProtectedText -Path $AuthTokenPath -Entropy 'agentdock.startup.v1'
- $env:AGENTDOCK_HOST = '127.0.0.1'
- $env:AGENTDOCK_PORT = [string] $settings.port
- $env:AGENTDOCK_LOG_LEVEL = [string] $settings.log_level
- $env:AGENTDOCK_BROWSER_ENABLED = ([bool] $settings.browser_enabled).ToString().ToLowerInvariant()
- $env:AGENTDOCK_ACP_ENABLED = ([bool] $settings.acp_enabled).ToString().ToLowerInvariant()
-
- foreach ($name in @(
- 'AGENTDOCK_NEXUS_ENDPOINT',
- 'AGENTDOCK_NEXUS_TOKEN',
- 'AGENTDOCK_ACP_AGENT',
- 'AGENTDOCK_ACP_COMMAND',
- 'AGENTDOCK_ACP_ARGS_JSON',
- 'AGENTDOCK_ACP_ENV_FROM_ENV_JSON',
- 'AGENTDOCK_ACP_ALLOWED_ROOTS',
- 'AGENTDOCK_ACP_MAX_CONCURRENT_PROMPTS',
- 'AGENTDOCK_ACP_INTERACTION_TIMEOUT_MS',
- 'AGENTDOCK_SERVER_URL',
- 'AGENTDOCK_OAUTH_ENABLED',
- 'AGENTDOCK_OAUTH_PASSWORD',
- 'AGENTDOCK_OAUTH_TOKEN_SECRET'
- )) {
- Remove-Item -LiteralPath "Env:$name" -ErrorAction SilentlyContinue
- }
-
- if (-not [string]::IsNullOrWhiteSpace([string] $settings.nexus_endpoint)) {
- $env:AGENTDOCK_NEXUS_ENDPOINT = [string] $settings.nexus_endpoint
- }
- if (Test-Path -LiteralPath $NexusTokenPath -PathType Leaf) {
- $env:AGENTDOCK_NEXUS_TOKEN = Read-ProtectedText -Path $NexusTokenPath -Entropy 'agentdock.nexus.token.v1'
- }
- if ([bool] $settings.acp_enabled) {
- if (-not (Test-Path -LiteralPath ([string] $settings.acp_command) -PathType Leaf)) {
- throw "找不到 Coding Agent 命令:$($settings.acp_command)"
- }
- $env:AGENTDOCK_ACP_AGENT = [string] $settings.acp_agent
- $env:AGENTDOCK_ACP_COMMAND = [string] $settings.acp_command
- $env:AGENTDOCK_ACP_ARGS_JSON = ConvertTo-Json -InputObject @($settings.acp_args) -Compress
- }
-
- $activeServerUrl = Read-TextFile -Path $ServerUrlPath
- if (-not [string]::IsNullOrWhiteSpace($activeServerUrl)) {
- $env:AGENTDOCK_SERVER_URL = $activeServerUrl
- $env:AGENTDOCK_OAUTH_ENABLED = 'true'
- $env:AGENTDOCK_OAUTH_PASSWORD = Read-ProtectedText -Path $OAuthPasswordPath -Entropy 'agentdock.oauth.password.v1'
- $env:AGENTDOCK_OAUTH_TOKEN_SECRET = Read-ProtectedText -Path $OAuthSecretPath -Entropy 'agentdock.oauth.secret.v1'
- }
- & $AgentDockBinary
+ Ensure-Credentials
+ # 兼容入口也统一交给原生 launch-core 恢复设置和 DPAPI 凭据,避免形成第二套启动配置逻辑。
+ & $AgentDockBinary service launch-core --runtime-root $RuntimeRoot
return $LASTEXITCODE
}
diff --git a/scripts/test/install_test.go b/scripts/test/install_test.go
index 565c9bc..df81964 100644
--- a/scripts/test/install_test.go
+++ b/scripts/test/install_test.go
@@ -394,6 +394,7 @@ func TestWindowsManagerKeepsTunnelTransitionsAndManualTaskStartValid(t *testing.
"-FilePath $TrayBinary",
"'start-tunnel'",
"'stop-tunnel'",
+ "& $AgentDockBinary service launch-core --runtime-root $RuntimeRoot",
} {
if !strings.Contains(script, want) {
t.Fatalf("manage-windows.ps1 missing %q", want)
@@ -451,6 +452,37 @@ func TestWindowsControlPanelUsesNativeTunnelCommands(t *testing.T) {
}
}
+func TestWindowsControlPanelPersistsOAuthAccessTokenTTLThroughNativeConfig(t *testing.T) {
+ files := []string{
+ filepath.Join("..", "..", "desktop", "windows", "control-panel", "Models", "RuntimeModels.cs"),
+ filepath.Join("..", "..", "desktop", "windows", "control-panel", "MainWindow.xaml"),
+ filepath.Join("..", "..", "desktop", "windows", "control-panel", "MainWindow.xaml.cs"),
+ filepath.Join("..", "..", "desktop", "windows", "control-panel", "Services", "RuntimeService.cs"),
+ filepath.Join("..", "..", "internal", "desktopruntime", "config_windows.go"),
+ filepath.Join("..", "..", "internal", "desktopruntime", "service_environment_windows.go"),
+ }
+ var source strings.Builder
+ for _, path := range files {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("read %s: %v", path, err)
+ }
+ source.Write(data)
+ }
+ for _, want := range []string{
+ `oauth_access_token_ttl`,
+ `OAuthAccessTokenTtlTextBox`,
+ `OAuthAccessTokenTtl = OAuthAccessTokenTtlTextBox.Text.Trim()`,
+ `"--oauth-access-token-ttl", settings.OAuthAccessTokenTtl`,
+ `OAuthAccessTokenTTL: request.OAuthAccessTokenTTL`,
+ `effectiveOAuthAccessTokenTTL(settings.OAuthAccessTokenTTL, inheritedOAuthAccessTokenTTL)`,
+ } {
+ if !strings.Contains(source.String(), want) {
+ t.Fatalf("Windows OAuth TTL persistence chain missing %q", want)
+ }
+ }
+}
+
func TestWindowsControlPanelReadsVersionFromCoreBuildInfo(t *testing.T) {
appData, err := os.ReadFile(filepath.Join("..", "..", "desktop", "windows", "control-panel", "App.xaml.cs"))
if err != nil {