diff --git a/cmd/cmd_gateway.go b/cmd/cmd_gateway.go index 65f80b8..fdbf2b1 100644 --- a/cmd/cmd_gateway.go +++ b/cmd/cmd_gateway.go @@ -1156,7 +1156,7 @@ func buildHeartbeatService(cfg *config.Config, msgBus *bus.MessageBus) *heartbea } func setupEmbeddedWhatsAppBridge(ctx context.Context, cfg *config.Config) (*channels.WhatsAppBridgeService, bool) { - if cfg == nil || !cfg.Channels.WhatsApp.Enabled || !shouldEmbedWhatsAppBridge(cfg) { + if !shouldStartEmbeddedWhatsAppBridge(cfg) { return nil, false } cfg.Channels.WhatsApp.BridgeURL = embeddedWhatsAppBridgeURL(cfg) @@ -1169,6 +1169,10 @@ func setupEmbeddedWhatsAppBridge(ctx context.Context, cfg *config.Config) (*chan return svc, true } +func shouldStartEmbeddedWhatsAppBridge(cfg *config.Config) bool { + return cfg != nil && shouldEmbedWhatsAppBridge(cfg) +} + func shouldEmbedWhatsAppBridge(cfg *config.Config) bool { raw := strings.TrimSpace(cfg.Channels.WhatsApp.BridgeURL) if raw == "" { diff --git a/cmd/cmd_gateway_test.go b/cmd/cmd_gateway_test.go new file mode 100644 index 0000000..b313c91 --- /dev/null +++ b/cmd/cmd_gateway_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + "github.com/YspCoder/clawgo/pkg/config" +) + +func TestShouldStartEmbeddedWhatsAppBridge(t *testing.T) { + t.Parallel() + + cfg := config.DefaultConfig() + cfg.Channels.WhatsApp.Enabled = false + cfg.Channels.WhatsApp.BridgeURL = "" + if !shouldStartEmbeddedWhatsAppBridge(cfg) { + t.Fatalf("expected embedded bridge to start when using default embedded url") + } + + cfg.Channels.WhatsApp.BridgeURL = "ws://127.0.0.1:3001" + if !shouldStartEmbeddedWhatsAppBridge(cfg) { + t.Fatalf("expected embedded bridge to start for legacy local bridge url") + } + + cfg.Channels.WhatsApp.BridgeURL = "ws://example.com:3001/ws" + if shouldStartEmbeddedWhatsAppBridge(cfg) { + t.Fatalf("expected external bridge url to disable embedded bridge") + } + + if shouldStartEmbeddedWhatsAppBridge(nil) { + t.Fatalf("expected nil config to disable embedded bridge") + } +} diff --git a/scripts/build-slim.ps1 b/scripts/build-slim.ps1 new file mode 100644 index 0000000..3bdd0e7 --- /dev/null +++ b/scripts/build-slim.ps1 @@ -0,0 +1,79 @@ +[CmdletBinding()] +param( + [string]$Output = "build/clawgo-windows-amd64-slim.exe", + [switch]$EmbedWebUI, + [switch]$Compress +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$embedDir = Join-Path $repoRoot "cmd/workspace" +$workspaceDir = Join-Path $repoRoot "workspace" +$webuiDistDir = Join-Path $repoRoot "webui/dist" +$outputPath = Join-Path $repoRoot $Output + +function Copy-DirectoryContents { + param( + [Parameter(Mandatory = $true)][string]$Source, + [Parameter(Mandatory = $true)][string]$Destination + ) + + if (Test-Path $Destination) { + Remove-Item -Recurse -Force $Destination + } + New-Item -ItemType Directory -Force -Path $Destination | Out-Null + Copy-Item -Path (Join-Path $Source "*") -Destination $Destination -Recurse -Force +} + +try { + if (-not (Test-Path $workspaceDir)) { + throw "Missing workspace source directory: $workspaceDir" + } + + Copy-DirectoryContents -Source $workspaceDir -Destination $embedDir + + if ($EmbedWebUI) { + if (-not (Test-Path $webuiDistDir)) { + throw "EmbedWebUI was requested, but WebUI dist is missing: $webuiDistDir" + } + $embedWebuiDir = Join-Path $embedDir "webui" + Copy-DirectoryContents -Source $webuiDistDir -Destination $embedWebuiDir + } + + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $outputPath) | Out-Null + + $env:CGO_ENABLED = "0" + $env:GOOS = "windows" + $env:GOARCH = "amd64" + + $version = "dev" + $buildTime = [DateTimeOffset]::Now.ToString("yyyy-MM-ddTHH:mm:sszzz") + $ldflags = "-X main.version=$version -X main.buildTime=$buildTime -s -w -buildid=" + $tags = "purego,netgo,osusergo" + + & go build -trimpath -buildvcs=false -tags $tags -ldflags $ldflags -o $outputPath ./cmd + if ($LASTEXITCODE -ne 0) { + throw "go build failed with exit code $LASTEXITCODE" + } + + if ($Compress) { + $upx = Get-Command upx -ErrorAction SilentlyContinue + if ($null -ne $upx) { + & $upx.Source --best --lzma $outputPath | Out-Null + if ($LASTEXITCODE -ne 0) { + throw "upx failed with exit code $LASTEXITCODE" + } + } else { + Write-Warning "Compress was requested, but upx was not found in PATH" + } + } + + $sizeMB = [Math]::Round((Get-Item $outputPath).Length / 1MB, 2) + Write-Host "Build complete: $outputPath ($sizeMB MB)" +} +finally { + if (Test-Path $embedDir) { + Remove-Item -Recurse -Force $embedDir + } +} diff --git a/webui/src/components/Checkbox.tsx b/webui/src/components/Checkbox.tsx new file mode 100644 index 0000000..0d2786b --- /dev/null +++ b/webui/src/components/Checkbox.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +type CheckboxProps = Omit, 'type'>; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(' '); +} + +const Checkbox = React.forwardRef(function Checkbox( + { className, ...props }, + ref, +) { + return ( + + ); +}); + +export default Checkbox; +export type { CheckboxProps }; diff --git a/webui/src/components/FieldCard.tsx b/webui/src/components/FieldCard.tsx new file mode 100644 index 0000000..db315c3 --- /dev/null +++ b/webui/src/components/FieldCard.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +type FieldCardProps = { + title?: React.ReactNode; + hint?: React.ReactNode; + className?: string; + titleClassName?: string; + hintClassName?: string; + children: React.ReactNode; +}; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(' '); +} + +const FieldCard: React.FC = ({ + title, + hint, + className, + titleClassName, + hintClassName, + children, +}) => { + return ( +
+ {title !== undefined && title !== null && ( +
{title}
+ )} + {hint !== undefined && hint !== null && ( +
{hint}
+ )} + {children} +
+ ); +}; + +export default FieldCard; +export type { FieldCardProps }; diff --git a/webui/src/components/FormField.tsx b/webui/src/components/FormField.tsx new file mode 100644 index 0000000..fdbd0d4 --- /dev/null +++ b/webui/src/components/FormField.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +type FormFieldProps = { + label?: React.ReactNode; + help?: React.ReactNode; + className?: string; + labelClassName?: string; + helpClassName?: string; + children: React.ReactNode; +}; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(' '); +} + +const FormField: React.FC = ({ + label, + help, + className, + labelClassName, + helpClassName, + children, +}) => { + return ( + + ); +}; + +export default FormField; +export type { FormFieldProps }; diff --git a/webui/src/components/GlobalDialog.tsx b/webui/src/components/GlobalDialog.tsx index e88a44d..0a5b724 100644 --- a/webui/src/components/GlobalDialog.tsx +++ b/webui/src/components/GlobalDialog.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AnimatePresence, motion } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { Button } from './Button'; +import Input from './Input'; type DialogOptions = { title?: string; @@ -45,7 +46,7 @@ export const GlobalDialog: React.FC<{ {kind === 'prompt' && (
{options.inputLabel && } - setValue(e.target.value)} diff --git a/webui/src/components/Input.tsx b/webui/src/components/Input.tsx new file mode 100644 index 0000000..8716fce --- /dev/null +++ b/webui/src/components/Input.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +type InputProps = React.InputHTMLAttributes; + +function joinClasses(...values: Array) { + return values.filter(Boolean).join(' '); +} + +const Input = React.forwardRef(function Input( + { className, ...props }, + ref, +) { + return ( + + ); +}); + +export default Input; +export type { InputProps }; diff --git a/webui/src/components/RecursiveConfig.tsx b/webui/src/components/RecursiveConfig.tsx index b2f8242..7e341a6 100644 --- a/webui/src/components/RecursiveConfig.tsx +++ b/webui/src/components/RecursiveConfig.tsx @@ -2,6 +2,10 @@ import React, { useMemo, useState } from 'react'; import { Plus } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { FixedButton } from './Button'; +import Checkbox from './Checkbox'; +import Input from './Input'; +import Select from './Select'; +import Textarea from './Textarea'; interface RecursiveConfigProps { data: any; @@ -67,12 +71,12 @@ const PrimitiveArrayEditor: React.FC<{
- setDraft(e.target.value)} placeholder={t('recursiveAddValuePlaceholder')} - className="ui-input rounded-xl px-3 py-2 text-sm" + className="rounded-xl px-3 py-2 text-sm" /> {suggestions.map((s) => ( @@ -91,7 +95,7 @@ const PrimitiveArrayEditor: React.FC<{ - +
); @@ -140,7 +144,7 @@ const RecursiveConfig: React.FC = ({ data, labels, path = onChange={(next) => onChange(currentPath, next)} /> ) : ( -