Skip to content

Commit 7ef39f8

Browse files
HanFengRuYueclaude
andcommitted
feat: v1.2 — 术语占位符替换、移除控制台切换、版本升级
- LlmTranslationService: 添加术语表占位符替换机制({{G_x}}),确保非正则术语翻译精确匹配 - LlmTranslationService: 系统提示词术语表部分改为中文 - SystemTrayService: 移除控制台显示/隐藏功能及相关 P/Invoke 声明 - build.ps1: 版本号从 1.1 升级至 1.2 - CLAUDE.md: 补充 TMP Font API 端点和同步点文档,更新端口配置说明 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 88fae57 commit 7ef39f8

4 files changed

Lines changed: 105 additions & 77 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ cd XUnityToolkit-Vue && npx vue-tsc --noEmit
7272
## API Endpoints
7373

7474
- **Game:** `GET/POST /api/games/`, `GET/DELETE /api/games/{id}`, `POST .../add-with-detection`, `PUT /api/games/{id}` (rename), `POST .../detect`, `POST .../open-folder`, `POST .../launch`
75+
- **TMP Font:** `GET/POST/DELETE /api/games/{id}/tmp-font` — check/install/uninstall bundled TMP font (version-matched to game's Unity version); used by `ConfigPanel.vue`
7576
- **Framework:** `DELETE /api/games/{id}/framework/{framework}`
7677
- **Install:** `POST /api/games/{id}/install`, `DELETE .../install` (uninstall), `GET .../status`, `POST .../cancel`
7778
- **Icon:** `GET /api/games/{id}/icon` (custom > exe icon), `POST .../icon/upload`, `DELETE .../icon/custom`, `POST .../icon/{search,grids,select}` (SteamGridDB), `POST .../icon/web-search`, `POST .../icon/web-select`
@@ -105,7 +106,7 @@ cd XUnityToolkit-Vue && npx vue-tsc --noEmit
105106
- **Data storage:** `{programDir}/data/` for all runtime data; `AppDataPaths` centralizes paths; shipped assets (`bundled/`, `wwwroot/`) stay at program root
106107
- **Sensitive data encryption:** `DpapiProtector` (DPAPI CurrentUser) encrypts `ApiEndpointConfig.ApiKey` and `SteamGridDbApiKey`; prefix `ENC:DPAPI:` + Base64; encrypt/decrypt in `AppSettingsService.ReadAsync`/`WriteAsync` boundary; decryption failure preserves ciphertext + creates `.bak` backup
107108
- **Pre-DI paths:** `Program.cs` reads `builder.Configuration["AppData:Root"]` fallback to `{baseDir}/data` before DI — must stay in sync with `AppDataPaths._root` formula
108-
- **App URL:** `http://127.0.0.1:51821`; **MUST use `127.0.0.1` not `localhost`** (Unity Mono resolves to IPv6 `::1`)
109+
- **App URL:** `http://127.0.0.1:{port}` default `51821`; **MUST use `127.0.0.1` not `localhost`** (Unity Mono resolves to IPv6 `::1`); port configurable via `settings.json``aiTranslation.port` (read pre-DI in `Program.cs`)
109110
- Named `HttpClient`: `"LLM"` (120s/200conn), `"SteamGridDB"` (30s), `"LocalLlmDownload"` (12h), `"WebImageSearch"` (15s, browser UA)
110111
- **Mirror:** `AppSettings.HfMirrorUrl`; HF host-replacement for model downloads; plugins/llama binaries are bundled (no runtime GitHub downloads)
111112
- **Fire-and-forget:** `CancellationToken.None` in `Task.Run`; `CancellationTokenSource` dicts for user cancellation
@@ -290,6 +291,7 @@ cd XUnityToolkit-Vue && npx vue-tsc --noEmit
290291
- **Adding AiTranslationSettings fields:** Sync 4 places: `Models/AiTranslationSettings.cs`, `src/api/types.ts`, `AiTranslationView.vue` (`DEFAULT_AI_TRANSLATION`), `SettingsView.vue`
291292
- **Adding DoNotTranslateEntry fields:** Sync 2 places: `Models/DoNotTranslateEntry.cs`, `src/api/types.ts`
292293
- **Font generation models:** Sync `CharacterSetConfig`/`FontGenerationReport`/`CharsetInfo` between `Models/FontGeneration.cs``src/api/types.ts`; phase values between `TmpFontGeneratorService``FontGeneratorView.vue` phaseLabels; charset IDs between `BuiltinCharsets` ↔ frontend checkbox values
294+
- **TMP font models:** Sync `TmpFontStatus` between `Models/``src/api/types.ts`; API methods in `src/api/games.ts`
293295
- Frontend state lifecycle: `GameDetailView.loadGame()` resets state when `isInstalled=false`
294296
- Install store `operationType` tracks install vs uninstall
295297
- **`[LLMTranslate]` INI config:** Written in 3 places — `POST /ai-endpoint`, `InstallOrchestrator`, DLL `Initialize`

XUnityToolkit-WebUI/Services/LlmTranslationService.cs

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,19 +225,49 @@ public async Task<IList<string>> TranslateAsync(
225225
}
226226
}
227227

228+
// Apply glossary placeholder substitution for non-regex entries
229+
// This guarantees glossary translations are used regardless of LLM compliance
230+
Dictionary<string, string>? glossaryMapping = null;
231+
List<GlossaryEntry>? promptGlossary = glossary;
232+
if (glossary is not null)
233+
{
234+
var nonRegexEntries = glossary.Where(e => !e.IsRegex && !string.IsNullOrEmpty(e.Original)).ToList();
235+
if (nonRegexEntries.Count > 0)
236+
{
237+
var (replaced, mapping) = ApplyGlossaryReplacements(textsToTranslate, nonRegexEntries);
238+
if (mapping.Count > 0)
239+
{
240+
textsToTranslate = replaced;
241+
glossaryMapping = mapping;
242+
243+
// Only show regex entries in prompt (non-regex handled by placeholders)
244+
var regexEntries = glossary.Where(e => e.IsRegex).ToList();
245+
promptGlossary = regexEntries.Count > 0 ? regexEntries : null;
246+
247+
// Tell LLM to preserve glossary placeholders
248+
dntHint = (dntHint ?? "") +
249+
"\n\n文本中的 {{G_x}} 是术语占位符,请在翻译结果中原样保留,不要修改、翻译或删除。";
250+
}
251+
}
252+
}
253+
228254
// Batch translation: send entire batch as one LLM call
229255
var (batchResult, tokens, ms, endpointName) = await TranslateBatchAsync(
230-
textsToTranslate, from, to, ai, enabledEndpoints, glossary,
256+
textsToTranslate, from, to, ai, enabledEndpoints, promptGlossary,
231257
gameDescription, memoryContext, dntHint, semaphore, ct);
232258

233259
// Copy to mutable list for post-processing
234260
var translations = new List<string>(batchResult);
235261

236-
// Restore do-not-translate placeholders (before glossary post-processing)
262+
// Restore glossary placeholders (replace {{G_x}} with glossary translations)
263+
if (glossaryMapping is not null)
264+
translations = RestoreGlossaryPlaceholders(translations, glossaryMapping);
265+
266+
// Restore do-not-translate placeholders
237267
if (dntMapping is not null)
238268
translations = RestoreDoNotTranslatePlaceholders(translations, dntMapping);
239269

240-
// Apply glossary post-processing
270+
// Apply glossary post-processing (safety net: catches regex entries + any remaining originals)
241271
for (int i = 0; i < translations.Count; i++)
242272
{
243273
if (glossary is not null)
@@ -645,6 +675,10 @@ or LlmProvider.GLM or LlmProvider.Kimi
645675
@"\{{1,2}\s*DNT_(\d+)\s*\}{1,2}",
646676
RegexOptions.Compiled);
647677

678+
private static readonly Regex GlossaryRestoreRegex = new(
679+
@"\{{1,2}\s*G_(\d+)\s*\}{1,2}",
680+
RegexOptions.Compiled);
681+
648682
private static (List<string> replacedTexts, Dictionary<string, string> mapping)
649683
ApplyDoNotTranslateReplacements(IList<string> texts, List<DoNotTranslateEntry> entries)
650684
{
@@ -724,6 +758,67 @@ private static List<string> RestoreDoNotTranslatePlaceholders(
724758
return result;
725759
}
726760

761+
// ── Glossary placeholder substitution ──
762+
763+
private static (List<string> replacedTexts, Dictionary<string, string> mapping)
764+
ApplyGlossaryReplacements(IList<string> texts, List<GlossaryEntry> nonRegexEntries)
765+
{
766+
var sorted = nonRegexEntries
767+
.OrderByDescending(e => e.Original.Length)
768+
.ToList();
769+
770+
// mapping: placeholder → translation (for post-restoration)
771+
var mapping = new Dictionary<string, string>();
772+
var originalToPlaceholder = new Dictionary<string, string>(StringComparer.Ordinal);
773+
int nextIndex = 0;
774+
775+
var result = new List<string>(texts.Count);
776+
foreach (var text in texts)
777+
{
778+
var current = text;
779+
foreach (var entry in sorted)
780+
{
781+
int searchStart = 0;
782+
while (true)
783+
{
784+
int idx = current.IndexOf(entry.Original, searchStart, StringComparison.Ordinal);
785+
if (idx < 0) break;
786+
787+
if (!originalToPlaceholder.TryGetValue(entry.Original, out var placeholder))
788+
{
789+
placeholder = $"{{{{G_{nextIndex}}}}}";
790+
originalToPlaceholder[entry.Original] = placeholder;
791+
mapping[placeholder] = entry.Translation;
792+
nextIndex++;
793+
}
794+
795+
current = current[..idx] + placeholder + current[(idx + entry.Original.Length)..];
796+
searchStart = idx + placeholder.Length;
797+
}
798+
}
799+
result.Add(current);
800+
}
801+
802+
return (result, mapping);
803+
}
804+
805+
private static List<string> RestoreGlossaryPlaceholders(
806+
IList<string> translations, Dictionary<string, string> mapping)
807+
{
808+
var result = new List<string>(translations.Count);
809+
foreach (var text in translations)
810+
{
811+
var restored = GlossaryRestoreRegex.Replace(text, m =>
812+
{
813+
var index = m.Groups[1].Value;
814+
var fullKey = $"{{{{G_{index}}}}}";
815+
return mapping.TryGetValue(fullKey, out var translation) ? translation : m.Value;
816+
});
817+
result.Add(restored);
818+
}
819+
return result;
820+
}
821+
727822
// ── System prompt + glossary injection ──
728823

729824
private static string BuildSystemPrompt(string template, string from, string to,
@@ -740,11 +835,11 @@ private static string BuildSystemPrompt(string template, string from, string to,
740835

741836
if (glossary is { Count: > 0 })
742837
{
743-
sb.Append("\n\nGlossary (use these exact translations):\n");
838+
sb.Append("\n\n术语表(翻译时必须严格使用以下译文,不得自行翻译):\n");
744839
foreach (var entry in glossary)
745840
{
746841
if (entry.IsRegex)
747-
sb.Append($" Pattern: /{entry.Original}/ → {entry.Translation}");
842+
sb.Append($" 正则匹配: /{entry.Original}/ → {entry.Translation}");
748843
else
749844
sb.Append($" {entry.Original}{entry.Translation}");
750845

XUnityToolkit-WebUI/Services/SystemTrayService.cs

Lines changed: 0 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System.Diagnostics;
2-
using System.Runtime.InteropServices;
3-
using System.Text;
42

53
namespace XUnityToolkit_WebUI.Services;
64

@@ -9,33 +7,6 @@ public sealed class SystemTrayService(
97
IHostApplicationLifetime lifetime,
108
IConfiguration configuration) : IHostedService, IDisposable
119
{
12-
[DllImport("kernel32.dll")]
13-
private static extern IntPtr GetConsoleWindow();
14-
15-
[DllImport("user32.dll")]
16-
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
17-
18-
[DllImport("kernel32.dll")]
19-
private static extern bool AllocConsole();
20-
21-
[DllImport("kernel32.dll")]
22-
private static extern bool FreeConsole();
23-
24-
[DllImport("user32.dll")]
25-
[return: MarshalAs(UnmanagedType.Bool)]
26-
private static extern bool IsWindowVisible(IntPtr hWnd);
27-
28-
[DllImport("user32.dll")]
29-
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
30-
31-
[DllImport("user32.dll")]
32-
private static extern bool DeleteMenu(IntPtr hMenu, uint uPosition, uint uFlags);
33-
34-
private const int SW_HIDE = 0;
35-
private const int SW_SHOW = 5;
36-
private const uint SC_CLOSE = 0xF060;
37-
private const uint MF_BYCOMMAND = 0x00000000;
38-
3910
private Thread? _staThread;
4011
private volatile NotifyIcon? _trayIcon;
4112
private volatile SynchronizationContext? _syncContext;
@@ -137,9 +108,6 @@ private ContextMenuStrip BuildContextMenu()
137108
var openItem = new ToolStripMenuItem("打开浏览器");
138109
openItem.Click += (_, _) => OpenBrowser();
139110

140-
var consoleItem = new ToolStripMenuItem("显示控制台");
141-
consoleItem.Click += (_, _) => ToggleConsole(consoleItem);
142-
143111
var exitItem = new ToolStripMenuItem("退出");
144112
exitItem.Click += (_, _) =>
145113
{
@@ -148,48 +116,11 @@ private ContextMenuStrip BuildContextMenu()
148116
};
149117

150118
menu.Items.Add(openItem);
151-
menu.Items.Add(consoleItem);
152119
menu.Items.Add(new ToolStripSeparator());
153120
menu.Items.Add(exitItem);
154121
return menu;
155122
}
156123

157-
private void ToggleConsole(ToolStripMenuItem menuItem)
158-
{
159-
var hwnd = GetConsoleWindow();
160-
161-
if (hwnd == IntPtr.Zero)
162-
{
163-
// WinExe: no console allocated — create one on demand
164-
AllocConsole();
165-
hwnd = GetConsoleWindow();
166-
167-
// Remove close button to prevent accidental app termination
168-
var sysMenu = GetSystemMenu(hwnd, false);
169-
if (sysMenu != IntPtr.Zero)
170-
DeleteMenu(sysMenu, SC_CLOSE, MF_BYCOMMAND);
171-
172-
// Redirect streams to new console
173-
var stdOut = new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
174-
var stdErr = new StreamWriter(Console.OpenStandardError()) { AutoFlush = true };
175-
Console.SetOut(stdOut);
176-
Console.SetError(stdErr);
177-
Console.OutputEncoding = Encoding.UTF8;
178-
179-
menuItem.Text = "隐藏控制台";
180-
}
181-
else if (IsWindowVisible(hwnd))
182-
{
183-
ShowWindow(hwnd, SW_HIDE);
184-
menuItem.Text = "显示控制台";
185-
}
186-
else
187-
{
188-
ShowWindow(hwnd, SW_SHOW);
189-
menuItem.Text = "隐藏控制台";
190-
}
191-
}
192-
193124
private void OpenBrowser()
194125
{
195126
try

build.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ $EndpointProject = Join-Path $ProjectRoot 'TranslatorEndpoint\TranslatorEndpoint
6565
$Runtimes = if ($Runtime -eq 'all') { @('win-x64', 'win-arm64') } else { @($Runtime) }
6666
$hasEndpoint = Test-Path $EndpointProject
6767

68-
# Generate version: 1.1.{YYYYMMDDHHmm}
69-
$BuildVersion = "1.1.$(Get-Date -Format 'yyyyMMddHHmm')"
68+
# Generate version: 1.2.{YYYYMMDDHHmm}
69+
$BuildVersion = "1.2.$(Get-Date -Format 'yyyyMMddHHmm')"
7070

7171
# ── GitHub repo owners ──
7272
$BepInEx5Owner = "BepInEx"

0 commit comments

Comments
 (0)