@wingsbutterfly/pi-zh 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/dict.ts ADDED
@@ -0,0 +1,303 @@
1
+ /**
2
+ * pi-zh 词典总表 — 唯一数据源(v0.3)
3
+ *
4
+ * 所有汉化条目集中在此,新增/修改汉化只需编辑本文件。
5
+ * 每条目是 [英文原文, 中文译文],补丁引擎按精确全串匹配替换。
6
+ * 条目按 target 分组:TARGET_DICTS[targetName] = DictEntry[]。
7
+ *
8
+ * 注意事项:
9
+ * 1. 英文原文必须与目标文件内容逐字一致(含空格/标点/转义)
10
+ * 2. 原文应当足够长/独特,避免误伤代码标识符
11
+ * 3. 工具 schema(给 AI 的 description)不在此列——汉化会影响模型理解
12
+ */
13
+
14
+ export interface DictEntry {
15
+ /** 英文原文(target 文件中逐字出现) */
16
+ en: string;
17
+ /** 中文译文 */
18
+ zh: string;
19
+ /** 限定目标文件(相对包根;省略则应用于该 target 的全部文件) */
20
+ targetFiles?: string[];
21
+ /** 备注 */
22
+ note?: string;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // target: pi — Pi 本体(命令补全 + TUI 文案)
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * pi 内置 slash 命令描述(输入框 / 自动补全面板)
31
+ * 目标:dist/core/slash-commands.js(发行独立文件)+ bundle chunk(内嵌副本)
32
+ */
33
+ const PI_COMMAND_DESCRIPTIONS: DictEntry[] = [
34
+ { en: '"Open settings menu"', zh: '"打开设置菜单"', note: "/settings" },
35
+ { en: '"Select model (opens selector UI)"', zh: '"选择模型(打开选择器)"', note: "/model" },
36
+ { en: '"Navigate session tree (switch branches)"', zh: '"导航会话树(切换分支)"', note: "/tree" },
37
+ { en: '"Set thinking level"', zh: '"设置思考级别"', note: "/thinking" },
38
+ { en: '"Enable/disable models for Ctrl+P cycling"', zh: '"启用/禁用 Ctrl+P 循环模型"', note: "/scoped-models" },
39
+ { en: '"Export session (HTML default, or specify path: .html/.jsonl)"', zh: '"导出会话(默认 HTML,可指定 .html/.jsonl 路径)"', note: "/export" },
40
+ { en: '"Import and resume a session from a JSONL file"', zh: '"从 JSONL 文件导入并恢复会话"', note: "/import" },
41
+ { en: '"Share session as a secret GitHub gist"', zh: '"将会话分享为私有 GitHub Gist"', note: "/share" },
42
+ { en: '"Copy last agent message to clipboard"', zh: '"复制最后一条助手消息到剪贴板"', note: "/copy" },
43
+ { en: '"Set session display name"', zh: '"设置会话显示名称"', note: "/name" },
44
+ { en: '"Show session info and stats"', zh: '"显示会话信息与统计"', note: "/session" },
45
+ { en: '"Show changelog entries"', zh: '"显示更新日志记录"', note: "/changelog" },
46
+ { en: '"Show all keyboard shortcuts"', zh: '"显示全部键盘快捷键"', note: "/hotkeys" },
47
+ { en: '"Create a new fork from a previous user message"', zh: '"从之前的用户消息创建新分支"', note: "/fork" },
48
+ { en: '"Duplicate the current session at the current position"', zh: '"在当前位置复制当前会话"', note: "/clone" },
49
+ { en: '"Save project trust decision for future sessions"', zh: '"为后续会话保存项目信任决策"', note: "/trust" },
50
+ { en: '"Configure provider authentication"', zh: '"配置服务商认证"', note: "/login" },
51
+ { en: '"Remove provider authentication"', zh: '"移除服务商认证"', note: "/logout" },
52
+ { en: '"Start a new session"', zh: '"开始新会话"', note: "/new" },
53
+ { en: '"Manually compact the session context"', zh: '"手动压缩会话上下文"', note: "/compact" },
54
+ { en: '"Resume a different session"', zh: '"恢复其他会话"', note: "/resume" },
55
+ { en: '"Reload keybindings, extensions, skills, prompts, themes, and context files"', zh: '"重载快捷键、扩展、技能、提示词、主题与上下文文件"', note: "/reload" },
56
+ { en: '`Quit ${APP_NAME}`', zh: '`退出 ${APP_NAME}`', note: "/quit(模板字符串)" },
57
+ ];
58
+
59
+ /** pi TUI 界面文案(bundle chunk 内唯一短语) */
60
+ const PI_TUI_DESCRIPTIONS: DictEntry[] = [
61
+ { en: '"Keyboard Shortcuts"', zh: '"键盘快捷键"', note: "/hotkeys 面板标题" },
62
+ { en: '"\\u2713 New session started"', zh: '"\\u2713 新会话已开始"', note: "/new 完成提示" },
63
+ ];
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // target: pi — /hotkeys 快捷键面板(bundle chunk 内模板字符串)
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const PI_HOTKEYS_PANEL: DictEntry[] = [
70
+ // 面板标题
71
+ { en: 'hotkeys=`\n**Navigation**', zh: 'hotkeys=`\n**导航**', note: "/hotkeys 节标题" },
72
+ { en: '\n\n**Editing**\n', zh: '\n\n**编辑**\n', note: "/hotkeys 节标题" },
73
+ { en: '\n\n**Other**\n', zh: '\n\n**其他**\n', note: "/hotkeys 节标题" },
74
+ // 表头(仅存在于 hotkeys 模板中,3 处)
75
+ { en: '| Key | Action |\n|-----|--------|', zh: '| 按键 | 操作 |\n|-----|--------|', note: "hotkeys 表头" },
76
+ // Navigation 节动作说明
77
+ { en: 'Move cursor / browse history', zh: '移动光标 / 浏览历史', note: "navigation" },
78
+ { en: 'Move by word', zh: '按词移动', note: "navigation" },
79
+ { en: 'Start of line', zh: '行首', note: "navigation" },
80
+ { en: 'End of line', zh: '行尾', note: "navigation" },
81
+ { en: 'Jump forward to character', zh: '向前跳到字符', note: "navigation" },
82
+ { en: 'Jump backward to character', zh: '向后跳到字符', note: "navigation" },
83
+ { en: 'Scroll by page', zh: '按页滚动', note: "navigation" },
84
+ // Editing 节动作说明
85
+ { en: 'Send message', zh: '发送消息', note: "editing" },
86
+ { en: 'New line', zh: '换行', note: "editing" },
87
+ { en: "(Ctrl+Enter on Windows Terminal)", zh: "(Windows 终端为 Ctrl+Enter)", note: "editing" },
88
+ { en: 'Delete word backwards', zh: '向后删除单词', note: "editing" },
89
+ { en: 'Delete word forwards', zh: '向前删除单词', note: "editing" },
90
+ { en: 'Delete to start of line', zh: '删除到行首', note: "editing" },
91
+ { en: 'Delete to end of line', zh: '删除到行尾', note: "editing" },
92
+ { en: 'Paste the most-recently-deleted text', zh: '粘贴最近删除的文本', note: "editing" },
93
+ { en: 'Cycle through the deleted text after pasting', zh: '粘贴后循环切换已删除文本', note: "editing" },
94
+ { en: 'Undo', zh: '撤销', note: "editing" },
95
+ // Other 节动作说明
96
+ { en: 'Path completion / accept autocomplete', zh: '路径补全 / 接受自动补全', note: "other" },
97
+ { en: 'Cancel autocomplete / abort streaming', zh: '取消自动补全 / 中止流式输出', note: "other" },
98
+ { en: 'Clear editor (first) / exit (second)', zh: '清空编辑器(第一次)/ 退出(第二次)', note: "other" },
99
+ { en: 'Exit (when editor is empty)', zh: '退出(编辑器为空时)', note: "other" },
100
+ { en: 'Suspend to background', zh: '挂起到后台', note: "other" },
101
+ { en: 'Cycle thinking level', zh: '循环切换思考级别', note: "other" },
102
+ { en: 'Cycle models', zh: '循环切换模型', note: "other" },
103
+ { en: 'Open model selector', zh: '打开模型选择器', note: "other" },
104
+ { en: 'Toggle tool output expansion', zh: '切换工具输出展开', note: "other" },
105
+ { en: 'Toggle thinking block visibility', zh: '切换思考块可见性', note: "other" },
106
+ { en: 'Edit message in external editor', zh: '在外部编辑器中编辑消息', note: "other" },
107
+ { en: 'Copy last assistant message', zh: '复制最后一条助手消息', note: "other" },
108
+ { en: 'Queue follow-up message', zh: '排队后续消息', note: "other" },
109
+ { en: 'Restore queued messages', zh: '恢复排队消息', note: "other" },
110
+ { en: 'Paste image or text from clipboard', zh: '从剪贴板粘贴图片或文本', note: "other" },
111
+ { en: 'Slash commands', zh: '斜杠命令', note: "other" },
112
+ { en: 'Run bash command', zh: '运行 bash 命令', note: "other" },
113
+ { en: 'Run bash command (excluded from context)', zh: '运行 bash 命令(不进入上下文)', note: "other" },
114
+ // Extensions 节
115
+ { en: '**Extensions**\n| Key | Action |\n|-----|--------|', zh: '**扩展**\n| 按键 | 操作 |\n|-----|--------|', note: "extensions 节标题" },
116
+ ];
117
+ // ---------------------------------------------------------------------------
118
+ // target: pi — /session 会话信息面板
119
+ // ---------------------------------------------------------------------------
120
+
121
+ const PI_SESSION_PANEL: DictEntry[] = [
122
+ { en: 'theme.bold("Session Info")', zh: 'theme.bold("会话信息")', note: "标题" },
123
+ { en: 'theme.fg("dim","Name:")', zh: 'theme.fg("dim","名称:")', note: "字段" },
124
+ { en: 'theme.fg("dim","File:")', zh: 'theme.fg("dim","文件:")', note: "字段" },
125
+ { en: '"In-memory"', zh: '"内存会话"', note: "文件列占位" },
126
+ { en: 'theme.bold("Messages")', zh: 'theme.bold("消息")', note: "节标题" },
127
+ { en: 'theme.fg("dim","Total:")', zh: 'theme.fg("dim","总计:")', note: "字段" },
128
+ { en: 'theme.fg("dim","User:")', zh: 'theme.fg("dim","用户:")', note: "字段" },
129
+ { en: 'theme.fg("dim","Assistant:")', zh: 'theme.fg("dim","助手:")', note: "字段" },
130
+ { en: 'theme.fg("dim","Tools:")', zh: 'theme.fg("dim","工具:")', note: "字段" },
131
+ { en: 'theme.bold("Tokens")', zh: 'theme.bold("令牌")', note: "节标题" },
132
+ { en: 'theme.fg("dim","Input:")', zh: 'theme.fg("dim","输入:")', note: "字段" },
133
+ { en: 'theme.fg("dim","Cached:")', zh: 'theme.fg("dim","缓存:")', note: "字段" },
134
+ { en: 'theme.fg("dim","Uncached:")', zh: 'theme.fg("dim","未缓存:")', note: "字段" },
135
+ { en: '`(${cacheWrite.toLocaleString()} written to cache)`', zh: '`(${cacheWrite.toLocaleString()} 已写入缓存)`', note: "写入提示" },
136
+ { en: 'theme.fg("dim","Output:")', zh: 'theme.fg("dim","输出:")', note: "字段" },
137
+ { en: 'theme.bold("Cost")', zh: 'theme.bold("费用")', note: "节标题" },
138
+ { en: 'theme.fg("dim","Cache Re-billed:")', zh: 'theme.fg("dim","缓存重新计费:")', note: "字段" },
139
+ { en: '"1 miss"', zh: '"1 次未命中"', note: "缓存未命中" },
140
+ { en: '`${cacheWaste.missCount} misses`', zh: '`${cacheWaste.missCount} 次未命中`', note: "缓存未命中" },
141
+ ];
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // target: pi — /settings 设置面板
145
+ // ---------------------------------------------------------------------------
146
+
147
+ const PI_SETTINGS_PANEL: DictEntry[] = [
148
+ // 顶级子菜单标题
149
+ { en: 'new SelectSubmenu("Theme"', zh: 'new SelectSubmenu("主题"', note: "Theme 子菜单" },
150
+ // settings 项(label + description 均替换;label 完整串)
151
+ { en: 'label:"Anthropic extra usage"', zh: 'label:"Anthropic 额外用量"', note: "设置项" },
152
+ { en: 'description:"Warn when Anthropic subscription auth may use paid extra usage"', zh: 'description:"当 Anthropic 订阅认证可能产生付费额外用量时警告"', note: "设置项说明" },
153
+ { en: 'label:"Light theme"', zh: 'label:"浅色主题"', note: "设置项" },
154
+ { en: 'description:"Theme to use in automatic mode when the terminal is light"', zh: 'description:"终端为浅色时自动模式使用的主题"', note: "设置项说明" },
155
+ { en: 'label:"Dark theme"', zh: 'label:"深色主题"', note: "设置项" },
156
+ { en: 'description:"Theme to use in automatic mode when the terminal is dark"', zh: 'description:"终端为深色时自动模式使用的主题"', note: "设置项说明" },
157
+ { en: 'label:"Apply"', zh: 'label:"应用"', note: "设置项" },
158
+ { en: 'description:"Save and go back"', zh: 'description:"保存并返回"', note: "设置项说明" },
159
+ { en: 'label:"Change mode"', zh: 'label:"切换模式"', note: "设置项" },
160
+ { en: 'description:"Switch to one theme for light and dark"', zh: 'description:"浅色和深色共用一套主题"', note: "设置项说明" },
161
+ { en: 'label:"Auto-compact"', zh: 'label:"自动压缩"', note: "设置项" },
162
+ { en: 'description:"Automatically compact context when it gets too large"', zh: 'description:"上下文过大时自动压缩"', note: "设置项说明" },
163
+ { en: 'label:"Steering mode"', zh: 'label:"引导模式"', note: "设置项" },
164
+ { en: 'label:"Transport"', zh: 'label:"传输方式"', note: "设置项" },
165
+ { en: 'description:"Preferred transport for providers that support multiple transports"', zh: 'description:"支持多种传输的服务商首选传输方式"', note: "设置项说明" },
166
+ { en: 'label:"HTTP idle timeout"', zh: 'label:"HTTP 空闲超时"', note: "设置项" },
167
+ { en: 'label:"Hide thinking"', zh: 'label:"隐藏思考"', note: "设置项" },
168
+ { en: 'description:"Hide thinking blocks in assistant responses"', zh: 'description:"在助手回复中隐藏思考块"', note: "设置项说明" },
169
+ { en: 'label:"Mermaid diagrams"', zh: 'label:"Mermaid 图表"', note: "设置项" },
170
+ { en: 'label:"Cache miss notices"', zh: 'label:"缓存未命中提示"', note: "设置项" },
171
+ { en: 'label:"Collapse changelog"', zh: 'label:"折叠更新日志"', note: "设置项" },
172
+ { en: 'label:"Quiet startup"', zh: 'label:"静默启动"', note: "设置项" },
173
+ { en: 'description:"Disable verbose printing at startup"', zh: 'description:"启动时不打印详情信息"', note: "设置项说明" },
174
+ { en: 'label:"Install telemetry"', zh: 'label:"安装遥测"', note: "设置项" },
175
+ { en: 'label:"Default project trust"', zh: 'label:"默认项目信任"', note: "设置项" },
176
+ { en: 'label:"Double-escape action"', zh: 'label:"双击 Escape 行为"', note: "设置项" },
177
+ { en: 'label:"Tree filter mode"', zh: 'label:"会话树过滤模式"', note: "设置项" },
178
+ { en: 'description:"Default filter when opening /tree"', zh: 'description:"打开 /tree 时的默认过滤"', note: "设置项说明" },
179
+ { en: 'label:"Warnings"', zh: 'label:"警告"', note: "设置项" },
180
+ { en: 'description:"Enable or disable individual warnings"', zh: 'description:"启用或禁用单项警告"', note: "设置项说明" },
181
+ { en: 'label:"TUI mode"', zh: 'label:"TUI 模式"', note: "设置项" },
182
+ { en: 'description:"Interface layout; fullscreen mode is experimental"', zh: 'description:"界面布局;全屏模式为实验性"', note: "设置项说明" },
183
+ { en: 'label:"Fullscreen exit output"', zh: 'label:"全屏退出输出"', note: "设置项" },
184
+ { en: 'label:"Fullscreen scrollbar"', zh: 'label:"全屏滚动条"', note: "设置项" },
185
+ { en: 'label:"Fullscreen copy on select"', zh: 'label:"全屏选中复制"', note: "设置项" },
186
+ { en: 'label:"Theme"', zh: 'label:"主题"', note: "设置项(两处)" },
187
+ { en: 'description:"Color theme for the interface"', zh: 'description:"界面配色主题"', note: "设置项说明" },
188
+ { en: 'label:"Show images"', zh: 'label:"显示图片"', note: "设置项" },
189
+ { en: 'label:"Image width"', zh: 'label:"图片宽度"', note: "设置项" },
190
+ { en: 'label:"Auto-resize images"', zh: 'label:"自动调整图片大小"', note: "设置项" },
191
+ { en: 'label:"Block images"', zh: 'label:"屏蔽图片"', note: "设置项" },
192
+ { en: 'label:"Skill commands"', zh: 'label:"技能命令"', note: "设置项" },
193
+ { en: 'description:"Register skills as /skill:name commands"', zh: 'description:"将技能注册为 /skill:name 命令"', note: "设置项说明" },
194
+ { en: 'label:"Show hardware cursor"', zh: 'label:"显示硬件光标"', note: "设置项" },
195
+ { en: 'label:"Editor padding"', zh: 'label:"编辑器内边距"', note: "设置项" },
196
+ { en: 'label:"Output padding"', zh: 'label:"输出内边距"', note: "设置项" },
197
+ { en: 'label:"Autocomplete max items"', zh: 'label:"自动补全最大项数"', note: "设置项" },
198
+ { en: 'label:"Clear on shrink"', zh: 'label:"缩小时清空"', note: "设置项" },
199
+ { en: 'label:"Terminal progress"', zh: 'label:"终端进度"', note: "设置项" },
200
+ // 子菜单/值选项
201
+ { en: '"Deep reasoning (~16k tokens)"', zh: '"深度思考(~16k 令牌)"', note: "thinking 级别" },
202
+ { en: '"Extra-high reasoning (~32k tokens)"', zh: '"超高思考(~32k 令牌)"', note: "thinking 级别" },
203
+ { en: '"Light reasoning (~2k tokens)"', zh: '"轻量思考(~2k 令牌)"', note: "thinking 级别" },
204
+ { en: '"Moderate reasoning (~8k tokens)"', zh: '"中等思考(~8k 令牌)"', note: "thinking 级别" },
205
+ { en: '"Very brief reasoning (~1k tokens)"', zh: '"极简思考(~1k 令牌)"', note: "thinking 级别" },
206
+ { en: '"No reasoning"', zh: '"无思考"', note: "thinking 级别" },
207
+ { en: '"Maximum reasoning"', zh: '"最大思考"', note: "thinking 级别" },
208
+ { en: '"Per-Model Thinking Level"', zh: '"按模型思考级别"', note: "子菜单标题" },
209
+ { en: '"Always trust"', zh: '"总是信任"', note: "信任选项" },
210
+ { en: '"Never trust"', zh: '"从不信任"', note: "信任选项" },
211
+ { en: '"Log in to a provider or configure an API key first"', zh: '"请先登录服务商或配置 API 密钥"', note: "提示" },
212
+ { en: '"No models available"', zh: '"没有可用模型"', note: "提示" },
213
+ { en: '"Select a model to configure"', zh: '"选择要配置的模型"', note: "提示" },
214
+ ];
215
+
216
+ // 占位条目会被过滤(zh 为 null 的无意义条目在此剔除)
217
+ const cleanHotkeys = PI_HOTKEYS_PANEL.filter((e) => e.zh !== null);
218
+ const cleanSession = PI_SESSION_PANEL.filter((e) => e.zh !== null);
219
+
220
+ // ---------------------------------------------------------------------------
221
+ // target: telegram — pi-telegram(菜单卡片/状态卡/子菜单)
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const TELEGRAM_PATCHES: DictEntry[] = [
225
+ // 菜单卡片命令说明
226
+ { en: '/start — Open menu / Pair bridge', zh: '/start — 打开菜单 / 配对桥接', targetFiles: ["lib/commands.ts"] },
227
+ { en: '/compact — Compact current session', zh: '/compact — 压缩当前会话', targetFiles: ["lib/commands.ts"] },
228
+ { en: '/next — Force next turn', zh: '/next — 强制下一轮', targetFiles: ["lib/commands.ts"] },
229
+ { en: '/continue — Queue continue prompt', zh: '/continue — 排入继续提示', targetFiles: ["lib/commands.ts"] },
230
+ { en: '/abort — Abort Pi', zh: '/abort — 中止 Pi', targetFiles: ["lib/commands.ts"] },
231
+ { en: '/stop — Abort Pi & Clear queue', zh: '/stop — 中止 Pi 并清空队列', targetFiles: ["lib/commands.ts"] },
232
+ // 状态卡标签
233
+ { en: 'buildStatusRow(\n "Status",', zh: 'buildStatusRow(\n "状态",', targetFiles: ["lib/status.ts"] },
234
+ { en: 'buildStatusRow("Status",', zh: 'buildStatusRow("状态",', targetFiles: ["lib/status.ts"] },
235
+ { en: 'buildStatusRow("Tokens", usageSummary)', zh: 'buildStatusRow("令牌", usageSummary)', targetFiles: ["lib/status.ts"] },
236
+ { en: 'buildStatusRow("Cache", cacheSummary)', zh: 'buildStatusRow("缓存", cacheSummary)', targetFiles: ["lib/status.ts"] },
237
+ { en: 'buildStatusRow("Cost", costSummary)', zh: 'buildStatusRow("费用", costSummary)', targetFiles: ["lib/status.ts"] },
238
+ { en: 'buildStatusRow("Context", buildContextSummary(ctx, activeModel))', zh: 'buildStatusRow("上下文", buildContextSummary(ctx, activeModel))', targetFiles: ["lib/status.ts"] },
239
+ // 状态摘要值
240
+ { en: 'if (state.hasActiveTurn || state.activeToolExecutions > 0) return "active";', zh: 'if (state.hasActiveTurn || state.activeToolExecutions > 0) return "忙碌";', targetFiles: ["lib/status.ts"] },
241
+ { en: 'if (ctx.isCompactionInProgress?.()) return "compacting";', zh: 'if (ctx.isCompactionInProgress?.()) return "压缩中";', targetFiles: ["lib/status.ts"] },
242
+ { en: 'if (ctx.hasPendingMessages?.()) return "pending";', zh: 'if (ctx.hasPendingMessages?.()) return "待处理";', targetFiles: ["lib/status.ts"] },
243
+ { en: 'if (ctx.isIdle?.() === false) return "active";', zh: 'if (ctx.isIdle?.() === false) return "忙碌";', targetFiles: ["lib/status.ts"] },
244
+ { en: 'if (ctx.isIdle?.() === true) return "idle";', zh: 'if (ctx.isIdle?.() === true) return "空闲";', targetFiles: ["lib/status.ts"] },
245
+ // 菜单按钮
246
+ { en: '`${formatTelegramCommandEmojiPrefix("model")}Model`', zh: '`${formatTelegramCommandEmojiPrefix("model")}模型`', targetFiles: ["lib/menu-status.ts"] },
247
+ { en: '`${formatTelegramCommandEmojiPrefix("thinking")}Thinking`', zh: '`${formatTelegramCommandEmojiPrefix("thinking")}思考`', targetFiles: ["lib/menu-status.ts"] },
248
+ { en: '`${queueItemCount === 0 ? "⌛" : "⏳"} Queue: ${queueItemCount}`', zh: '`${queueItemCount === 0 ? "⌛" : "⏳"} 队列: ${queueItemCount}`', targetFiles: ["lib/menu-status.ts"] },
249
+ { en: '"⚙️ Settings",\n callback_data: "menu:settings",', zh: '"⚙️ 设置",\n callback_data: "menu:settings",', targetFiles: ["lib/menu-status.ts"] },
250
+ // 子菜单标题
251
+ { en: 'MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>"', zh: 'MODEL_MENU_TITLE = "<b>🤖 选择模型:</b>"', targetFiles: ["lib/menu-model.ts"] },
252
+ { en: 'MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>"', zh: 'MODEL_DETAIL_MENU_TITLE = "<b>🤖 模型:</b>"', targetFiles: ["lib/menu-model.ts"] },
253
+ { en: 'return "<b>🧠 Choose a thinking level:</b>";', zh: 'return "<b>🧠 选择思考级别:</b>";', targetFiles: ["lib/menu-thinking.ts"] },
254
+ { en: 'SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>"', zh: 'SETTINGS_MENU_TITLE = "<b>⚙️ 设置:</b>"', targetFiles: ["lib/menu-settings.ts"] },
255
+ { en: '"<b>🧹 Thread cleanup:</b>";', zh: '"<b>🧹 会话清理:</b>";', targetFiles: ["lib/menu-settings.ts"] },
256
+ { en: 'DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>"', zh: 'DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 草稿预览:</b>"', targetFiles: ["lib/menu-settings.ts"] },
257
+ { en: '"<b>🧾 Assistant rendering:</b>";', zh: '"<b>🧾 助手渲染:</b>";', targetFiles: ["lib/menu-settings.ts"] },
258
+ { en: '"<b>🔬 Activity:</b>";', zh: '"<b>🔬 活动:</b>";', targetFiles: ["lib/menu-settings.ts"] },
259
+ { en: '"<b>🕒 Time injection mode:</b>";', zh: '"<b>🕒 时间注入模式:</b>";', targetFiles: ["lib/menu-settings.ts"] },
260
+ { en: 'VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>"', zh: 'VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 语音回复模式:</b>"', targetFiles: ["lib/menu-settings.ts"] },
261
+ // 导航
262
+ { en: '{ text: "⬆️ Main menu", callback_data: "menu:back" }', zh: '{ text: "⬆️ 主菜单", callback_data: "menu:back" }', targetFiles: ["lib/menu-model.ts", "lib/menu-thinking.ts", "lib/menu-queue.ts", "lib/menu-settings.ts"] },
263
+ { en: '[{ text: "⬆️ Back", callback_data: "queue:list" }]', zh: '[{ text: "⬆️ 返回", callback_data: "queue:list" }]', targetFiles: ["lib/menu-queue.ts"] },
264
+ // 队列空状态
265
+ { en: '"<b>🛸 No queued signals detected.</b>",', zh: '"<b>🛸 未检测到排队信号。</b>",', targetFiles: ["lib/menu-queue.ts"] },
266
+ { en: 'if (items.length > 0) return "<b>⏳ Queue:</b>";', zh: 'if (items.length > 0) return "<b>⏳ 队列:</b>";', targetFiles: ["lib/menu-queue.ts"] },
267
+ { en: '"<b>🕳 Nothing queued yet.</b>",', zh: '"<b>🕳 队列中还没有任务。</b>",', targetFiles: ["lib/menu-queue.ts"] },
268
+ ];
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // target: cache-optimizer — pi-cache-optimizer(/cache-optimizer 命令与提示)
272
+ // ---------------------------------------------------------------------------
273
+
274
+ const CACHE_OPTIMIZER_PATCHES: DictEntry[] = [
275
+ // 命令描述
276
+ { en: 'description: "Configure and diagnose Pi cache behavior"', zh: 'description: "配置和诊断 Pi 缓存行为"', targetFiles: ["index.ts"] },
277
+ // 用户可见提示
278
+ { en: '"No active model selected. Select a model first with /model or pi --model."', zh: '"未选择活动模型。请先用 /model 或 pi --model 选择模型。"', targetFiles: ["index.ts"] },
279
+ { en: '"ℹ️ Active model does not match a cache adapter. No stats to reset."', zh: '"ℹ️ 活动模型不匹配缓存适配器。没有可重置的统计。"', targetFiles: ["index.ts"] },
280
+ { en: '"No changes were made. Canceled by user."', zh: '"未做任何修改。已被用户取消。"', targetFiles: ["index.ts"] },
281
+ { en: '"✅ Compat fully configured."', zh: '"✅ 兼容配置完成。"', targetFiles: ["index.ts"] },
282
+ { en: '"Upstream provider prompt cache was not modified. "', zh: '"上游服务商提示缓存未被修改。"', targetFiles: ["index.ts"] },
283
+ { en: '"Usage: /cache-optimizer stats [all|contributors]"', zh: '"用法:/cache-optimizer stats [all|contributors]"', targetFiles: ["index.ts"] },
284
+ ];
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // 导出:按 target 分组
288
+ // ---------------------------------------------------------------------------
289
+
290
+ export const TARGET_DICTS: Record<string, DictEntry[]> = {
291
+ pi: [
292
+ ...PI_COMMAND_DESCRIPTIONS,
293
+ ...PI_TUI_DESCRIPTIONS,
294
+ ...cleanHotkeys,
295
+ ...cleanSession,
296
+ ...PI_SETTINGS_PANEL,
297
+ ],
298
+ telegram: TELEGRAM_PATCHES,
299
+ "cache-optimizer": CACHE_OPTIMIZER_PATCHES,
300
+ };
301
+
302
+ /** 全部条目(兼容旧接口的汇总) */
303
+ export const ALL_ENTRIES: DictEntry[] = Object.values(TARGET_DICTS).flat();
package/lib/patch.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * pi-zh 补丁引擎 — 通用、幂等、双向可逆、安全降级
3
+ *
4
+ * 对目标文件做精确字符串替换:
5
+ * - patch: en → zh(存在原文且译文不存在时才替换)
6
+ * - restore:zh → en(存在译文且原文不存在时才还原)
7
+ * - 幂等:重复执行无副作用
8
+ * - 安全:匹配不到时静默跳过,绝不破坏源码
9
+ * - 多目标:一条词典可作用于多个文件(targetFiles 限定)
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import type { DictEntry } from "./dict.ts";
15
+
16
+ export interface PatchTarget {
17
+ /** 绝对路径(可不存在,跳过) */
18
+ filePath: string;
19
+ /** 相对标识(仅日志用) */
20
+ label: string;
21
+ }
22
+
23
+ export interface PatchReport {
24
+ label: string;
25
+ /** 本次修改条数 */
26
+ applied: number;
27
+ }
28
+
29
+ export interface PatchOutcome {
30
+ changed: boolean;
31
+ reports: PatchReport[];
32
+ }
33
+
34
+ /** 条目是否适用于某文件别名(targetFiles 限定) */
35
+ function entryAppliesTo(entry: DictEntry, alias: string): boolean {
36
+ if (!entry.targetFiles || entry.targetFiles.length === 0) return true;
37
+ return entry.targetFiles.some((t) => alias.endsWith(t));
38
+ }
39
+
40
+ /** 对单个文件执行 patch(en→zh)或 restore(zh→en),返回命中条数 */
41
+ export function applyFile(
42
+ filePath: string,
43
+ alias: string,
44
+ entries: readonly DictEntry[],
45
+ mode: "patch" | "restore",
46
+ ): number {
47
+ if (!existsSync(filePath)) return 0;
48
+ let content = readFileSync(filePath, "utf8");
49
+ let applied = 0;
50
+
51
+ for (const entry of entries) {
52
+ if (!entryAppliesTo(entry, alias)) continue;
53
+ const from = mode === "patch" ? entry.en : entry.zh;
54
+ const to = mode === "patch" ? entry.zh : entry.en;
55
+ // 原文/译文不存在 → 自然幂等(该条目已完成);存在 → 全量替换
56
+ if (!content.includes(from)) continue;
57
+ content = content.split(from).join(to);
58
+ applied++;
59
+ }
60
+
61
+ if (applied > 0) {
62
+ writeFileSync(filePath, content, "utf8");
63
+ }
64
+ return applied;
65
+ }
66
+
67
+ /** 对多个目标执行 patch/restore */
68
+ export function applyTargets(
69
+ targets: readonly PatchTarget[],
70
+ entries: readonly DictEntry[],
71
+ mode: "patch" | "restore",
72
+ ): PatchOutcome {
73
+ const reports: PatchReport[] = [];
74
+ let changed = false;
75
+ for (const target of targets) {
76
+ const applied = applyFile(target.filePath, target.label, entries, mode);
77
+ if (applied > 0) {
78
+ changed = true;
79
+ reports.push({ label: target.label, applied });
80
+ }
81
+ }
82
+ return { changed, reports };
83
+ }
84
+
85
+ /** 便捷:判定文件当前态是否「需要 patch」(存在英文且无中文) */
86
+ export function needsPatch(
87
+ targets: readonly PatchTarget[],
88
+ entries: readonly DictEntry[],
89
+ ): boolean {
90
+ for (const target of targets) {
91
+ if (!existsSync(target.filePath)) continue;
92
+ const content = readFileSync(target.filePath, "utf8");
93
+ for (const entry of entries) {
94
+ if (!entryAppliesTo(entry, target.label)) continue;
95
+ if (content.includes(entry.en) && !content.includes(entry.zh)) return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+
101
+ /** 便捷:判定文件当前态是否「需要还原」(存在中文) */
102
+ export function needsRestore(
103
+ targets: readonly PatchTarget[],
104
+ entries: readonly DictEntry[],
105
+ ): boolean {
106
+ for (const target of targets) {
107
+ if (!existsSync(target.filePath)) continue;
108
+ const content = readFileSync(target.filePath, "utf8");
109
+ for (const entry of entries) {
110
+ if (!entryAppliesTo(entry, target.label)) continue;
111
+ if (content.includes(entry.zh) && !content.includes(entry.en)) return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+
117
+ /** 兼容:定位包根(存在标志文件即返回) */
118
+ export function findPackageRoot(
119
+ candidates: string[],
120
+ markerFile: string,
121
+ ): string | null {
122
+ for (const dir of candidates) {
123
+ if (existsSync(join(dir, markerFile))) return dir;
124
+ }
125
+ return null;
126
+ }
package/lib/sdk.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * pi-zh rpiv-i18n SDK 管理
3
+ *
4
+ * rpiv-todo / rpiv-ask-user-question 等插件自带 zh.json 中文翻译,
5
+ * 但需要 @juicesharp/rpiv-i18n SDK 激活(i18n bridge 动态 import,
6
+ * SDK 缺失时静默回退英文)。安装到 .pi/agent/npm 即全局生效,
7
+ * 用户可用插件提供的 /languages 命令实时切换。
8
+ *
9
+ * 本模块只在「启用时」确保 SDK 存在:存在则跳过,缺失则 npm install
10
+ * 到 pi 的扩展包目录。失败不影响其他汉化功能。
11
+ */
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import { existsSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ const SDK_NAME = "@juicesharp/rpiv-i18n";
19
+
20
+ /** SDK 是否已安装(在 pi 扩展包目录) */
21
+ export function isI18nSdkInstalled(): boolean {
22
+ const sdkPath = join(homedir(), ".pi/agent/npm/node_modules", SDK_NAME, "package.json");
23
+ return existsSync(sdkPath);
24
+ }
25
+
26
+ /**
27
+ * 确保 SDK 安装。返回 true 表示安装成功或已存在。
28
+ * 安装到 ~/.pi/agent/npm(pi 统一扩展包目录,模块解析自动覆盖)。
29
+ */
30
+ export function ensureI18nSdk(): boolean {
31
+ if (isI18nSdkInstalled()) return true;
32
+ try {
33
+ const npmDir = join(homedir(), ".pi/agent/npm");
34
+ if (!existsSync(npmDir)) return false;
35
+ execFileSync("npm", ["install", SDK_NAME, "--no-save", "--omit=dev"], {
36
+ cwd: npmDir,
37
+ stdio: "pipe",
38
+ timeout: 120_000,
39
+ });
40
+ return isI18nSdkInstalled();
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
package/lib/targets.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * pi-zh 目标探测模块
3
+ *
4
+ * 每个 target 声明:
5
+ * - name 标识符(config.targets 用)
6
+ * - label 显示名
7
+ * - probeFiles 探测文件(相对包根),存在任一个即认为插件已安装
8
+ * - packageRoots 可能安装位置(按顺序探测)
9
+ * - marker 包根标志文件(确认根目录)
10
+ *
11
+ * 核心原则:存在则汉化,不存在则不管。pi 更新/插件更新后仅需重启
12
+ * pi-zh 重新探测(模块顶层每次启动执行)。任一探测失败不影响其他 target。
13
+ */
14
+
15
+ import { existsSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { join } from "node:path";
18
+
19
+ export interface TargetDefinition {
20
+ name: string;
21
+ label: string;
22
+ /** 候选包根(按序探测,取第一个含 marker 的) */
23
+ packageRoots: string[];
24
+ /** 包根标志文件(确认根目录用) */
25
+ marker: string;
26
+ /** 探针文件(相对包根),存在即视为「插件存在」 */
27
+ probeFiles: string[];
28
+ }
29
+
30
+ /** pi 本体(bun 全局安装 / npm 全局 / .pi 包目录) */
31
+ const PI_ROOTS = [
32
+ join(homedir(), ".bun/install/global/node_modules/@earendil-works/pi-coding-agent"),
33
+ "/usr/local/lib/node_modules/@earendil-works/pi-coding-agent",
34
+ "/usr/lib/node_modules/@earendil-works/pi-coding-agent",
35
+ join(homedir(), ".pi/agent/npm/node_modules/@earendil-works/pi-coding-agent"),
36
+ ];
37
+
38
+ /** pi 扩展包统一目录 */
39
+ const EXT_DIR = join(homedir(), ".pi/agent/npm/node_modules");
40
+
41
+ export const TARGETS: TargetDefinition[] = [
42
+ {
43
+ name: "pi",
44
+ label: "Pi 本体(命令补全 / TUI 文案)",
45
+ packageRoots: PI_ROOTS,
46
+ marker: "package.json",
47
+ probeFiles: ["dist/core/slash-commands.js"],
48
+ },
49
+ {
50
+ name: "telegram",
51
+ label: "pi-telegram(菜单卡片 / 状态卡)",
52
+ packageRoots: [join(EXT_DIR, "@llblab/pi-telegram")],
53
+ marker: "package.json",
54
+ probeFiles: ["lib/commands.ts"],
55
+ },
56
+ {
57
+ name: "cache-optimizer",
58
+ label: "pi-cache-optimizer(/cache-optimizer 命令)",
59
+ packageRoots: [join(EXT_DIR, "pi-cache-optimizer")],
60
+ marker: "package.json",
61
+ probeFiles: ["index.ts"],
62
+ },
63
+ ];
64
+
65
+ /** 从候选根列表中取第一个含 marker 的目录(纯函数,可测) */
66
+ export function resolveFirstRoot(
67
+ candidates: string[],
68
+ marker: string,
69
+ ): string | null {
70
+ for (const dir of candidates) {
71
+ if (existsSync(join(dir, marker))) return dir;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /** 解析 target 的安装根目录;未安装返回 null */
77
+ export function resolveTargetRoot(target: TargetDefinition): string | null {
78
+ return resolveFirstRoot(target.packageRoots, target.marker);
79
+ }
80
+
81
+ /** target 是否已安装(探测文件存在) */
82
+ export function isTargetInstalled(target: TargetDefinition): boolean {
83
+ const root = resolveTargetRoot(target);
84
+ if (!root) return false;
85
+ return target.probeFiles.some((f) => existsSync(join(root, f)));
86
+ }
87
+
88
+ /** 全部已安装的 target */
89
+ export function getInstalledTargets(): TargetDefinition[] {
90
+ return TARGETS.filter(isTargetInstalled);
91
+ }
92
+
93
+ /** 按名称取 target;未知名返回 null */
94
+ export function getTargetByName(name: string): TargetDefinition | null {
95
+ return TARGETS.find((t) => t.name === name) ?? null;
96
+ }