From c8a75ef2d535e6a285fe5c805e6360d43fad144c Mon Sep 17 00:00:00 2001
From: wdh-home <243823965@qq.com>
Date: Wed, 13 May 2026 20:40:14 +0800
Subject: [PATCH] init
---
app.go | 34 ++-
app_options.go | 31 ++
app_options_test.go | 23 ++
frontend/package.json | 1 +
frontend/scripts/check-chinese-ui.mjs | 94 ++++++
frontend/src/App.css | 7 +
frontend/src/App.tsx | 177 ++++++-----
frontend_config_test.go | 25 ++
frontend_github_link_test.go | 27 ++
go.mod | 2 +-
internal/gitea/client.go | 38 ++-
internal/gitea/client_test.go | 25 ++
internal/tray/controller.go | 42 +++
internal/tray/controller_test.go | 57 ++++
internal/tray/tray_other.go | 9 +
internal/tray/tray_windows.go | 405 ++++++++++++++++++++++++++
main.go | 16 +-
17 files changed, 920 insertions(+), 93 deletions(-)
create mode 100644 app_options.go
create mode 100644 app_options_test.go
create mode 100644 frontend/scripts/check-chinese-ui.mjs
create mode 100644 frontend_config_test.go
create mode 100644 frontend_github_link_test.go
create mode 100644 internal/tray/controller.go
create mode 100644 internal/tray/controller_test.go
create mode 100644 internal/tray/tray_other.go
create mode 100644 internal/tray/tray_windows.go
diff --git a/app.go b/app.go
index c266398..5483c38 100644
--- a/app.go
+++ b/app.go
@@ -5,12 +5,16 @@ import (
"sgg-ai-skill-manager/internal/domain"
"sgg-ai-skill-manager/internal/service"
+ "sgg-ai-skill-manager/internal/tray"
+
+ wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
type App struct {
- ctx context.Context
- service *service.Service
- initErr error
+ ctx context.Context
+ service *service.Service
+ initErr error
+ trayStop func()
}
func NewApp() *App {
@@ -23,6 +27,30 @@ func (a *App) startup(ctx context.Context) {
if a.service != nil {
a.service.StartAutoUpdate(ctx)
}
+ a.startTray(ctx)
+}
+
+func (a *App) shutdown(context.Context) {
+ if a.trayStop != nil {
+ a.trayStop()
+ a.trayStop = nil
+ }
+}
+
+func (a *App) startTray(ctx context.Context) {
+ stop, err := tray.Start(ctx, tray.Config{
+ Tooltip: appTitle,
+ IconPath: defaultTrayIconPath,
+ OnOpen: func() {
+ wailsruntime.WindowShow(ctx)
+ wailsruntime.WindowUnminimise(ctx)
+ },
+ })
+ if err != nil {
+ wailsruntime.LogErrorf(ctx, "failed to start tray icon: %v", err)
+ return
+ }
+ a.trayStop = stop
}
func (a *App) LoadConfig() (domain.Config, error) {
diff --git a/app_options.go b/app_options.go
new file mode 100644
index 0000000..c0f4c4d
--- /dev/null
+++ b/app_options.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "io/fs"
+
+ "github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/assetserver"
+)
+
+const (
+ appTitle = "SGG AI 技能管理器"
+ defaultTrayIconPath = "build/windows/icon.ico"
+)
+
+func newAppOptions(app *App, assets fs.FS) *options.App {
+ return &options.App{
+ Title: appTitle,
+ Width: 1024,
+ Height: 768,
+ HideWindowOnClose: true,
+ AssetServer: &assetserver.Options{
+ Assets: assets,
+ },
+ BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ OnStartup: app.startup,
+ OnShutdown: app.shutdown,
+ Bind: []interface{}{
+ app,
+ },
+ }
+}
diff --git a/app_options_test.go b/app_options_test.go
new file mode 100644
index 0000000..88037c8
--- /dev/null
+++ b/app_options_test.go
@@ -0,0 +1,23 @@
+package main
+
+import "testing"
+
+func TestNewAppOptionsHidesWindowOnClose(t *testing.T) {
+ app := &App{}
+
+ options := newAppOptions(app, assets)
+
+ if !options.HideWindowOnClose {
+ t.Fatal("expected closing the window to hide it instead of quitting")
+ }
+}
+
+func TestNewAppOptionsStopsTrayOnShutdown(t *testing.T) {
+ app := &App{}
+
+ options := newAppOptions(app, assets)
+
+ if options.OnShutdown == nil {
+ t.Fatal("expected shutdown hook to clean up the tray icon")
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
index 46da039..014f58f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
+ "test:ui-language": "node scripts/check-chinese-ui.mjs",
"preview": "vite preview"
},
"dependencies": {
diff --git a/frontend/scripts/check-chinese-ui.mjs b/frontend/scripts/check-chinese-ui.mjs
new file mode 100644
index 0000000..8acc235
--- /dev/null
+++ b/frontend/scripts/check-chinese-ui.mjs
@@ -0,0 +1,94 @@
+import {readFileSync} from 'node:fs';
+import {fileURLToPath} from 'node:url';
+import {resolve} from 'node:path';
+
+const root = resolve(fileURLToPath(new URL('../..', import.meta.url)));
+const appSource = readFileSync(resolve(root, 'frontend/src/App.tsx'), 'utf8');
+const mainSource = readFileSync(resolve(root, 'main.go'), 'utf8');
+const appOptionsSource = readFileSync(resolve(root, 'app_options.go'), 'utf8');
+const backendEntrySource = `${mainSource}\n${appOptionsSource}`;
+
+const forbiddenUiText = [
+ /
Skill Manager<\/h1>/,
+ />\s*Config\s*,
+ />\s*Remote\s*,
+ />\s*Local\s*,
+ />\s*Check Updates\s*,
+ /Gitea Base URL<\/span>/,
+ /Organization<\/span>/,
+ /Auth Type<\/span>/,
+ />\s*Password\s*,
+ /Username<\/span>/,
+ /Credential Key<\/span>/,
+ /Auto update<\/span>/,
+ /Check on startup<\/span>/,
+ /Interval Minutes<\/span>/,
+ />\s*Save\s*,
+ />\s*Test\s*,
+ /placeholder="Search repositories"/,
+ />\s*Refresh\s*,
+ /| Description<\/th>/,
+ / | Branch<\/th>/,
+ / | Status<\/th>/,
+ / | Action<\/th>/,
+ / | Actions<\/th>/,
+ />\s*Update\s*,
+ />\s*Download\s*,
+ /title="Open folder"/,
+ />No remote skills,
+ />No local skills,
+ /return 'Remote Market';/,
+ /return 'Local Skills';/,
+ /return 'Configuration';/,
+ /Delete local skill/,
+ /title="Delete local"/,
+ /Remote list refreshed/,
+ /Config saved/,
+ /Connected as/,
+ /installed to/,
+ /uninstalled from/,
+ />not installed,
+];
+
+const requiredChineseText = [
+ '技能管理器',
+ '配置',
+ '远程市场',
+ '本地技能',
+ '检查更新',
+ '保存',
+ '测试连接',
+ '搜索仓库',
+ '刷新',
+ '下载',
+ '更新',
+ '操作',
+ '暂无远程技能',
+ '暂无本地技能',
+ '已下载',
+ '已安装',
+ '未安装',
+];
+
+const failures = [];
+
+for (const pattern of forbiddenUiText) {
+ if (pattern.test(appSource)) {
+ failures.push(`App.tsx still matches English UI pattern: ${pattern}`);
+ }
+}
+
+for (const text of requiredChineseText) {
+ if (!appSource.includes(text)) {
+ failures.push(`App.tsx is missing Chinese UI text: ${text}`);
+ }
+}
+
+if (!backendEntrySource.includes('SGG AI 技能管理器')) {
+ failures.push('window title is not Chinese');
+}
+
+if (failures.length > 0) {
+ console.error(failures.join('\n'));
+ process.exit(1);
+}
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 9ba5835..8d59755 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -131,6 +131,13 @@ button.danger:hover:not(:disabled) {
line-height: 28px;
}
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
.notice {
margin-bottom: 12px;
padding: 10px 12px;
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 222a7fe..eef5ded 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -11,6 +11,7 @@ import {
Trash2,
Unlink,
} from 'lucide-react';
+import {BrowserOpenURL} from '../wailsjs/runtime/runtime';
import './App.css';
import {api} from './api';
import {Config, RemoteSkill, SkillState, defaultConfig, saveConfigRequest} from './types';
@@ -18,6 +19,29 @@ import {domain} from '../wailsjs/go/models';
type Tab = 'config' | 'remote' | 'local';
+const statusLabels: Record = {
+ remote: '远程',
+ downloaded: '已下载',
+ check_failed: '检查失败',
+ installed: '已安装',
+ not_installed: '未安装',
+ conflict: '冲突',
+ unknown: '未知',
+ local: '就绪',
+};
+
+const localizedMessages: Record = {
+ connected: '已连接',
+ 'gitea baseURL is required': '请填写 Gitea 服务地址',
+ 'gitea org is required': '请填写组织',
+ 'gitea username is required': '请填写用户名',
+ 'authType must be password or token': '认证方式必须是密码或 Token',
+ 'update interval must be positive': '检查间隔必须大于 0',
+ 'local changes present': '存在本地改动',
+};
+
+const projectRepositoryURL = 'http://10.1.0.1:3000/sgg-sgg-tools/sgg-sgg-ai-skill-manage-windows';
+
function App() {
const [activeTab, setActiveTab] = useState('config');
const [config, setConfig] = useState(defaultConfig());
@@ -57,7 +81,7 @@ function App() {
async function refreshRemote() {
await run('remote:refresh', async () => {
setRemoteSkills(await api.listRemoteSkills());
- setMessage('Remote list refreshed');
+ setMessage('远程列表已刷新');
});
}
@@ -70,14 +94,14 @@ function App() {
await api.saveConfig(saveConfigRequest(config, password, token));
setPassword('');
setToken('');
- setMessage('Config saved');
+ setMessage('配置已保存');
});
}
async function testConnection() {
await run('config:test', async () => {
const result = await api.testConnection(saveConfigRequest(config, password, token));
- setMessage(result.ok ? `Connected as ${result.username}` : result.message);
+ setMessage(result.ok ? (result.username ? `已连接:${result.username}` : '已连接') : localizeMessage(result.message));
});
}
@@ -85,10 +109,10 @@ function App() {
await run(`remote:${skill.name}`, async () => {
if (skill.isDownloaded) {
await api.updateSkill(config.gitea.org, skill.name);
- setMessage(`${skill.name} updated`);
+ setMessage(`${skill.name} 已更新`);
} else {
await api.downloadSkill(skill);
- setMessage(`${skill.name} downloaded`);
+ setMessage(`${skill.name} 已下载`);
}
await refreshLocal();
await refreshRemote();
@@ -99,7 +123,7 @@ function App() {
await run(`local:update:${skill.repo}`, async () => {
await api.updateSkill(skill.org, skill.repo);
await refreshLocal();
- setMessage(`${skill.repo} updated`);
+ setMessage(`${skill.repo} 已更新`);
});
}
@@ -107,7 +131,7 @@ function App() {
await run(`install:${targetID}:${skill.repo}`, async () => {
await api.installSkill(skill.org, skill.repo, targetID);
await refreshLocal();
- setMessage(`${skill.repo} installed to ${targetLabel(targetID)}`);
+ setMessage(`${skill.repo} 已安装到 ${targetLabel(targetID)}`);
});
}
@@ -115,18 +139,18 @@ function App() {
await run(`uninstall:${targetID}:${skill.repo}`, async () => {
await api.uninstallSkill(skill.org, skill.repo, targetID);
await refreshLocal();
- setMessage(`${skill.repo} uninstalled from ${targetLabel(targetID)}`);
+ setMessage(`${skill.repo} 已从 ${targetLabel(targetID)} 卸载`);
});
}
async function deleteSkill(skill: SkillState) {
- if (!window.confirm(`Delete local skill ${skill.repo}?`)) {
+ if (!window.confirm(`确认删除本地技能 ${skill.repo}?`)) {
return;
}
await run(`delete:${skill.repo}`, async () => {
await api.deleteSkill(skill.org, skill.repo);
await refreshLocal();
- setMessage(`${skill.repo} deleted`);
+ setMessage(`${skill.repo} 已删除`);
});
}
@@ -155,7 +179,7 @@ function App() {
try {
await action();
} catch (err) {
- setError(err instanceof Error ? err.message : String(err));
+ setError(localizeMessage(err instanceof Error ? err.message : String(err)));
} finally {
setBusy('');
}
@@ -173,19 +197,19 @@ function App() {
S
- Skill Manager
+ 技能管理器
Gitea / Codex / Claude
- |