Compare commits
4 Commits
0ee006f71f
...
v0.7.3-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24b09f5700 | ||
|
|
1442b4fd8a | ||
|
|
a1da9da3db | ||
|
|
a7d4b31a0d |
@@ -138,11 +138,12 @@ jobs:
|
||||
echo "sudo mv muyue-darwin-arm64 /usr/local/bin/muyue"
|
||||
echo "\`\`\`"
|
||||
echo ""
|
||||
echo "**Windows (x86_64)**"
|
||||
echo "**Windows (x86_64)** — sans privilèges admin, crée les raccourcis Bureau + Menu Démarrer :"
|
||||
echo "\`\`\`powershell"
|
||||
echo "Invoke-WebRequest -Uri \"${DL_URL}/muyue-windows-amd64.zip\" -OutFile \"muyue.zip\""
|
||||
echo "Expand-Archive -Path \"muyue.zip\" -DestinationPath \".\""
|
||||
echo "Move-Item muyue-windows-amd64.exe C:\\Windows\\muyue.exe"
|
||||
echo "\$dest = \"\$env:LOCALAPPDATA\\Muyue\""
|
||||
echo "Invoke-WebRequest -Uri \"${DL_URL}/muyue-windows-amd64.zip\" -OutFile \"\$env:TEMP\\muyue.zip\""
|
||||
echo "Expand-Archive -Path \"\$env:TEMP\\muyue.zip\" -DestinationPath \$dest -Force"
|
||||
echo "& \"\$dest\\muyue-windows-amd64.exe\" install-shortcuts"
|
||||
echo "\`\`\`"
|
||||
} > /tmp/stable_changelog.md
|
||||
echo "path=/tmp/stable_changelog.md" >> $GITHUB_OUTPUT
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## v0.7.3
|
||||
|
||||
### Onboarding — focus MiniMax + MiMo
|
||||
|
||||
- L'étape `apikey` du wizard de premier lancement propose désormais **les deux clés** (MiniMax + MiMo) côte à côte ; au moins une doit être validée pour continuer.
|
||||
- Les autres fournisseurs (OpenAI, Anthropic, Z.AI, Ollama) ne sont plus proposés dans le wizard — l'utilisateur les configure ensuite via l'onglet **Configuration** s'il le souhaite. Justification : pour les nouveaux utilisateurs, deux choix simples > six choix qui ralentissent le démarrage.
|
||||
- Si MiniMax est validé, il devient le provider actif. Sinon, c'est MiMo. Si les deux sont validés, MiniMax reste actif (peut être basculé via `/model change` plus tard).
|
||||
|
||||
### Install Windows — pas d'admin + raccourcis automatiques
|
||||
|
||||
- **Avant** : la 3ᵉ ligne du snippet d'install (`Move-Item ... C:\Windows\muyue.exe`) échouait avec `UnauthorizedAccessException` sur PowerShell sans élévation.
|
||||
- **Maintenant** : 4 lignes, toutes exécutables sans admin :
|
||||
```powershell
|
||||
$dest = "$env:LOCALAPPDATA\Muyue"
|
||||
Invoke-WebRequest -Uri ".../muyue-windows-amd64.zip" -OutFile "$env:TEMP\muyue.zip"
|
||||
Expand-Archive -Path "$env:TEMP\muyue.zip" -DestinationPath $dest -Force
|
||||
& "$dest\muyue-windows-amd64.exe" install-shortcuts
|
||||
```
|
||||
- Nouvelle commande `muyue install-shortcuts` (Windows uniquement) :
|
||||
- crée `Muyue.lnk` sur le Bureau et dans le Menu Démarrer (résolus via `[Environment]::GetFolderPath`, robuste OneDrive / profils non-standards) ;
|
||||
- utilise WScript.Shell COM via PowerShell pour générer les `.lnk` (pas de dépendance Go ajoutée) ;
|
||||
- ajoute le dossier d'install au `PATH` utilisateur (scope User, pas de modif système).
|
||||
- Une icône custom pourra être branchée plus tard en remplaçant la ressource embed du `.exe` ; pour l'instant, l'icône Windows par défaut du binaire est utilisée.
|
||||
|
||||
## v0.7.2
|
||||
|
||||
### Amélioration
|
||||
|
||||
- **feat(studio): réflexion avancée forcée automatiquement pendant les tests** — quand au moins une session `browser_test` est connectée, chaque message à Studio active automatiquement la réflexion avancée (un second modèle, si configuré, produit un rapport préalable injecté dans le prompt actif). Le toggle UI est ignoré tant qu'une session de test est active. Justification : pendant un test piloté par l'IA, avoir une analyse complémentaire d'un autre modèle améliore matériellement la qualité des décisions de clic et la couverture du rapport final.
|
||||
- Si aucun second provider n'est configuré, le comportement reste silencieux (fallback chat normal — pas d'erreur visible côté utilisateur).
|
||||
- Hint UI ajouté dans l'onglet Tests pour expliquer le comportement.
|
||||
|
||||
## v0.7.1
|
||||
|
||||
### Fix
|
||||
|
||||
151
cmd/muyue/commands/install_shortcuts.go
Normal file
151
cmd/muyue/commands/install_shortcuts.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// installShortcutsCmd creates desktop + Start Menu shortcuts on Windows so
|
||||
// non-technical users can launch Muyue without opening a terminal. It also
|
||||
// adds the install directory to the user's PATH (per-user, no admin).
|
||||
//
|
||||
// Implementation note: shortcut (.lnk) creation on Windows is most reliable
|
||||
// via WScript.Shell COM. We invoke it via PowerShell — keeps the Go binary
|
||||
// dependency-free and works on any Windows 10+ host.
|
||||
var installShortcutsCmd = &cobra.Command{
|
||||
Use: "install-shortcuts",
|
||||
Short: "Create Desktop + Start Menu shortcuts (Windows only) and add Muyue to PATH",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runtime.GOOS != "windows" {
|
||||
fmt.Println("install-shortcuts is a Windows-only command (no-op on this platform)")
|
||||
return nil
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate executable: %w", err)
|
||||
}
|
||||
exe, _ = filepath.Abs(exe)
|
||||
installDir := filepath.Dir(exe)
|
||||
|
||||
fmt.Println("Installing Muyue shortcuts...")
|
||||
fmt.Printf(" Executable : %s\n", exe)
|
||||
|
||||
desktop, err := userShellFolder("Desktop")
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate Desktop folder: %w", err)
|
||||
}
|
||||
startMenu, err := userShellFolder("Programs")
|
||||
if err != nil {
|
||||
return fmt.Errorf("locate Start Menu Programs folder: %w", err)
|
||||
}
|
||||
|
||||
desktopLnk := filepath.Join(desktop, "Muyue.lnk")
|
||||
startLnk := filepath.Join(startMenu, "Muyue.lnk")
|
||||
|
||||
if err := createWindowsShortcut(desktopLnk, exe, installDir, "Muyue — AI-powered dev environment"); err != nil {
|
||||
return fmt.Errorf("create desktop shortcut: %w", err)
|
||||
}
|
||||
fmt.Printf(" Desktop : %s\n", desktopLnk)
|
||||
|
||||
if err := createWindowsShortcut(startLnk, exe, installDir, "Muyue — AI-powered dev environment"); err != nil {
|
||||
return fmt.Errorf("create Start Menu shortcut: %w", err)
|
||||
}
|
||||
fmt.Printf(" Start Menu : %s\n", startLnk)
|
||||
|
||||
if err := addUserPATH(installDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, " PATH : warning — could not add %s to user PATH: %v\n", installDir, err)
|
||||
} else {
|
||||
fmt.Printf(" PATH : added %s (open a new terminal to pick it up)\n", installDir)
|
||||
}
|
||||
|
||||
fmt.Println("\nDone — double-click the Muyue icon on your Desktop to launch.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(installShortcutsCmd)
|
||||
}
|
||||
|
||||
// userShellFolder asks Windows for a user shell folder via PowerShell —
|
||||
// resilient to OneDrive redirection and non-default profile locations.
|
||||
// `which` is one of: Desktop, Programs (Start Menu Programs), StartMenu.
|
||||
func userShellFolder(which string) (string, error) {
|
||||
ps := fmt.Sprintf(`[Environment]::GetFolderPath('%s')`, which)
|
||||
out, err := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", ps).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
path := stripTrailingWhitespace(string(out))
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("empty path for %s", which)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func stripTrailingWhitespace(s string) string {
|
||||
for len(s) > 0 && (s[len(s)-1] == '\n' || s[len(s)-1] == '\r' || s[len(s)-1] == ' ' || s[len(s)-1] == '\t') {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// createWindowsShortcut generates a .lnk via WScript.Shell COM. The arguments
|
||||
// are passed through PowerShell variables (not interpolated into the script
|
||||
// body) to avoid quoting issues with paths containing spaces or special chars.
|
||||
func createWindowsShortcut(lnkPath, target, workingDir, description string) error {
|
||||
script := `
|
||||
$lnk = $env:MUYUE_LNK
|
||||
$target = $env:MUYUE_TARGET
|
||||
$workdir = $env:MUYUE_WORKDIR
|
||||
$desc = $env:MUYUE_DESC
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$sc = $wsh.CreateShortcut($lnk)
|
||||
$sc.TargetPath = $target
|
||||
$sc.WorkingDirectory = $workdir
|
||||
$sc.Description = $desc
|
||||
$sc.IconLocation = "$target,0"
|
||||
$sc.Save()
|
||||
`
|
||||
cmd := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", script)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"MUYUE_LNK="+lnkPath,
|
||||
"MUYUE_TARGET="+target,
|
||||
"MUYUE_WORKDIR="+workingDir,
|
||||
"MUYUE_DESC="+description,
|
||||
)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("powershell: %v: %s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addUserPATH appends installDir to the user's PATH if not already present.
|
||||
// Uses PowerShell to read/write the User-scope environment via .NET API,
|
||||
// which broadcasts WM_SETTINGCHANGE so new processes pick it up.
|
||||
func addUserPATH(installDir string) error {
|
||||
script := `
|
||||
$dir = $env:MUYUE_INSTALL_DIR
|
||||
$current = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ($current -eq $null) { $current = '' }
|
||||
$parts = $current -split ';' | Where-Object { $_ -ne '' }
|
||||
if ($parts -notcontains $dir) {
|
||||
$new = if ($current -eq '') { $dir } else { "$current;$dir" }
|
||||
[Environment]::SetEnvironmentVariable('Path', $new, 'User')
|
||||
}
|
||||
`
|
||||
cmd := exec.Command("powershell", "-NoLogo", "-NoProfile", "-Command", script)
|
||||
cmd.Env = append(os.Environ(), "MUYUE_INSTALL_DIR="+installDir)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("powershell: %v: %s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -213,7 +213,17 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
orb.SetSystemPrompt(studioPrompt.String())
|
||||
orb.SetTools(s.agentToolsJSON)
|
||||
|
||||
if body.AdvancedReflection {
|
||||
// Auto-force advanced reflection while a browser-test session is active:
|
||||
// the user is doing AI-driven UI testing, where having a second model
|
||||
// produce a preliminary report (when one is configured) materially
|
||||
// improves which clicks the active model decides to perform. The toggle
|
||||
// remains user-controllable for non-test conversations.
|
||||
wantReflection := body.AdvancedReflection
|
||||
if !wantReflection && s.browserTestStore != nil && len(s.browserTestStore.List()) > 0 {
|
||||
wantReflection = true
|
||||
}
|
||||
|
||||
if wantReflection {
|
||||
if report, ok := s.runReflectionReport(enrichedMessage); ok {
|
||||
enrichedMessage = enrichedMessage + "\n\n[RAPPORT PRÉALABLE — produit par un autre modèle, à valider]\n" + report + "\n[/RAPPORT PRÉALABLE]"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
const (
|
||||
Name = "muyue"
|
||||
Version = "0.7.1"
|
||||
Version = "0.7.3"
|
||||
Author = "La Légion de Muyue"
|
||||
)
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ export default function OnboardingWizard({ api, onComplete }) {
|
||||
language: 'fr',
|
||||
keyboard: 'azerty',
|
||||
apikey: '',
|
||||
apikey_mimo: '',
|
||||
editor: '',
|
||||
})
|
||||
const [keyValidMimo, setKeyValidMimo] = useState(false)
|
||||
const [errorMimo, setErrorMimo] = useState(null)
|
||||
const [validatingMimo, setValidatingMimo] = useState(false)
|
||||
const [editorList, setEditorList] = useState(BASE_EDITORS)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
@@ -52,7 +56,7 @@ export default function OnboardingWizard({ api, onComplete }) {
|
||||
case 'name': return answers.name.trim().length > 0
|
||||
case 'language': return !!answers.language
|
||||
case 'keyboard': return !!answers.keyboard
|
||||
case 'apikey': return keyValid && !scanning
|
||||
case 'apikey': return (keyValid || keyValidMimo) && !scanning
|
||||
case 'editor': return true
|
||||
case 'done': return true
|
||||
default: return true
|
||||
@@ -173,6 +177,33 @@ export default function OnboardingWizard({ api, onComplete }) {
|
||||
setValidating(false)
|
||||
}
|
||||
|
||||
const handleValidateKeyMimo = async () => {
|
||||
if (!answers.apikey_mimo.trim()) return
|
||||
setValidatingMimo(true)
|
||||
setErrorMimo(null)
|
||||
try {
|
||||
await api.validateProvider({
|
||||
name: 'mimo',
|
||||
api_key: answers.apikey_mimo,
|
||||
model: 'mimo-v2.5-pro',
|
||||
base_url: 'https://token-plan-ams.xiaomimimo.com/v1',
|
||||
})
|
||||
setKeyValidMimo(true)
|
||||
// Save MiMo. If MiniMax wasn't validated yet, MiMo becomes the active provider.
|
||||
await api.saveProvider({
|
||||
name: 'mimo',
|
||||
api_key: answers.apikey_mimo,
|
||||
model: 'mimo-v2.5-pro',
|
||||
base_url: 'https://token-plan-ams.xiaomimimo.com/v1',
|
||||
active: !keyValid,
|
||||
})
|
||||
} catch (err) {
|
||||
setErrorMimo(err.message || 'Clé invalide')
|
||||
setKeyValidMimo(false)
|
||||
}
|
||||
setValidatingMimo(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -201,6 +232,15 @@ export default function OnboardingWizard({ api, onComplete }) {
|
||||
active: true,
|
||||
})
|
||||
}
|
||||
if (answers.apikey_mimo.trim()) {
|
||||
await api.saveProvider({
|
||||
name: 'mimo',
|
||||
api_key: answers.apikey_mimo,
|
||||
model: 'mimo-v2.5-pro',
|
||||
base_url: 'https://token-plan-ams.xiaomimimo.com/v1',
|
||||
active: !answers.apikey.trim(),
|
||||
})
|
||||
}
|
||||
onComplete()
|
||||
} catch (err) {
|
||||
setError(err.message || 'Erreur lors de la sauvegarde')
|
||||
@@ -283,38 +323,71 @@ export default function OnboardingWizard({ api, onComplete }) {
|
||||
|
||||
{current.key === 'apikey' && (
|
||||
<div className="onboarding-step">
|
||||
<div className="onboarding-title">Clé API MiniMax</div>
|
||||
<div className="onboarding-title">Clés API</div>
|
||||
<div className="onboarding-desc">
|
||||
Entrez votre clé API MiniMax pour activer l'assistant IA. La clé est obligatoire pour continuer.
|
||||
Renseignez au moins l'une des deux clés pour activer l'assistant. Les autres fournisseurs (OpenAI, Anthropic, Ollama, Z.AI) se configurent plus tard depuis l'onglet Configuration.
|
||||
</div>
|
||||
<input
|
||||
className="onboarding-input"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx"
|
||||
type="password"
|
||||
value={answers.apikey}
|
||||
onChange={e => { setAnswers(a => ({ ...a, apikey: e.target.value })); setKeyValid(false); setError(null) }}
|
||||
autoFocus
|
||||
/>
|
||||
{error && !keyValid && <div className="onboarding-required">{error}</div>}
|
||||
{keyValid && !scanning && <div className="onboarding-valid">Clé valide ✓ — Appuyez sur Entrée pour continuer</div>}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-tertiary)', fontWeight: 600 }}>MiniMax</label>
|
||||
<input
|
||||
className="onboarding-input"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx (MiniMax)"
|
||||
type="password"
|
||||
value={answers.apikey}
|
||||
onChange={e => { setAnswers(a => ({ ...a, apikey: e.target.value })); setKeyValid(false); setError(null) }}
|
||||
autoFocus
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className="sm primary"
|
||||
onClick={handleValidateKey}
|
||||
disabled={validating || !answers.apikey.trim()}
|
||||
>
|
||||
{validating ? 'Validation...' : 'Valider MiniMax'}
|
||||
</button>
|
||||
{keyValid && <span className="onboarding-valid">✓ MiniMax OK</span>}
|
||||
{error && !keyValid && <span className="onboarding-required">{error}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 12 }}>
|
||||
<label style={{ fontSize: 12, color: 'var(--text-tertiary)', fontWeight: 600 }}>MiMo (Xiaomi)</label>
|
||||
<input
|
||||
className="onboarding-input"
|
||||
placeholder="sk-xxxxxxxxxxxxxxxx (MiMo)"
|
||||
type="password"
|
||||
value={answers.apikey_mimo}
|
||||
onChange={e => { setAnswers(a => ({ ...a, apikey_mimo: e.target.value })); setKeyValidMimo(false); setErrorMimo(null) }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className="sm primary"
|
||||
onClick={handleValidateKeyMimo}
|
||||
disabled={validatingMimo || !answers.apikey_mimo.trim()}
|
||||
>
|
||||
{validatingMimo ? 'Validation...' : 'Valider MiMo'}
|
||||
</button>
|
||||
{keyValidMimo && <span className="onboarding-valid">✓ MiMo OK</span>}
|
||||
{errorMimo && !keyValidMimo && <span className="onboarding-required">{errorMimo}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{scanning && (
|
||||
<div className="onboarding-scanning">
|
||||
<div className="onboarding-scanning" style={{ marginTop: 8 }}>
|
||||
<Loader size={14} className="spin-icon" />
|
||||
<span>{scanMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
{requiredError && <div className="onboarding-required">Veuillez valider votre clé API pour continuer</div>}
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 4 }}>
|
||||
<button
|
||||
className="sm primary"
|
||||
onClick={handleValidateKey}
|
||||
disabled={validating || !answers.apikey.trim()}
|
||||
>
|
||||
{validating ? 'Validation...' : 'Valider la clé'}
|
||||
</button>
|
||||
</div>
|
||||
{!keyValid && !error && answers.apikey.trim() && (
|
||||
<div className="onboarding-hint">Entrez votre clé puis cliquez "Valider la clé"</div>
|
||||
{requiredError && (
|
||||
<div className="onboarding-required" style={{ marginTop: 8 }}>
|
||||
Veuillez valider au moins une clé (MiniMax ou MiMo) pour continuer.
|
||||
</div>
|
||||
)}
|
||||
{(keyValid || keyValidMimo) && !scanning && (
|
||||
<div className="onboarding-valid" style={{ marginTop: 8 }}>
|
||||
Au moins une clé est valide — appuyez sur Suivant pour continuer.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,6 +148,9 @@ lesquels déclenchent une erreur console.`}
|
||||
<p style={{ margin: '8px 0 0', opacity: 0.75, fontSize: '0.85em' }}>
|
||||
L'IA dispose de l'outil <code>browser_test</code> avec les actions <code>list_clickables</code>, <code>click</code>, <code>console</code>, <code>eval</code>, <code>type</code>, <code>current_url</code>, <code>wait</code>, <code>summary</code>.
|
||||
</p>
|
||||
<p style={{ margin: '8px 0 0', padding: 8, fontSize: '0.85em', background: 'var(--accent-bg, rgba(108,92,231,0.1))', border: '1px solid var(--accent, #6c5ce7)', borderRadius: 4 }}>
|
||||
<strong>Réflexion avancée auto :</strong> tant qu'au moins une session de test est connectée, chaque message dans Studio utilise automatiquement la réflexion avancée — un second modèle (s'il est configuré) produit un rapport d'analyse préalable injecté dans le prompt actif. Le toggle Studio est ignoré pendant la session.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user