zcode-pro 1.0.0 → 1.2.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/README.en.md +5 -4
- package/README.md +5 -4
- package/dist/inject.js +1775 -1206
- package/package.json +1 -1
- package/src/host/helper.mjs +10 -1
- package/src/inject/core.js +88 -10
- package/src/inject/features/newtab-panels.js +224 -0
- package/src/inject/features/sandbox-reclaim.js +121 -0
- package/src/inject/features/session-switch.js +159 -24
- package/src/inject/features/settings-dialog.js +143 -3
- package/src/inject/features/ws-running.js +7 -0
- package/src/inject/index.js +2 -0
- package/src/inject/ui.js +14 -0
package/package.json
CHANGED
package/src/host/helper.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import { reorderWorkspaceTasks, reorderGroupMembers } from './taskOrder.mjs';
|
|
|
17
17
|
import { webdavRequest, webdavPutWithMkcol, webdavHttpText, parseWebdavListing, findCurl } from './webdav.mjs';
|
|
18
18
|
import { resolveZcodeExecutable } from './paths.mjs';
|
|
19
19
|
|
|
20
|
-
const VERSION = '1.
|
|
20
|
+
const VERSION = '1.2.0';
|
|
21
21
|
|
|
22
22
|
// 全局提示词固定在用户主目录:官方加载器按 HOME/USERPROFILE 拼 .zcode/AGENTS.md,
|
|
23
23
|
// 不读 ZCODE_DATA_BASE_DIR(数据根迁走时全局指令仍在原位)。
|
|
@@ -339,6 +339,15 @@ export function startHelper({ port, token, dataRoot, state, agentsFile = default
|
|
|
339
339
|
if (body && typeof body === 'object' && body.styles && typeof body.styles === 'object') {
|
|
340
340
|
current.styles = applyStylesPartial(current.styles, body.styles);
|
|
341
341
|
}
|
|
342
|
+
// 会话快捷切换的键位(设置 → 快捷键里修改):键名固定,值限单个字母或数字
|
|
343
|
+
if (body && typeof body === 'object' && body.shortcuts && typeof body.shortcuts === 'object') {
|
|
344
|
+
const patch = {};
|
|
345
|
+
for (const k of ['switchLast', 'switchPrev', 'switchNext', 'switchRecent', 'switchAttention']) {
|
|
346
|
+
const v = body.shortcuts[k];
|
|
347
|
+
if (typeof v === 'string' && /^[a-z0-9]$/.test(v)) patch[k] = v;
|
|
348
|
+
}
|
|
349
|
+
current.shortcuts = { ...(current.shortcuts || {}), ...patch };
|
|
350
|
+
}
|
|
342
351
|
if (body && typeof body === 'object' && body.titleGen && typeof body.titleGen === 'object') {
|
|
343
352
|
current.titleGen = mergeTitleGenConfig({ ...current.titleGen, ...body.titleGen });
|
|
344
353
|
}
|
package/src/inject/core.js
CHANGED
|
@@ -38,6 +38,16 @@ export async function getConfig(force = false) {
|
|
|
38
38
|
}
|
|
39
39
|
export function clearConfigCache() { configCache = { value: null, at: 0 }; }
|
|
40
40
|
|
|
41
|
+
// 会话快捷切换的默认键位(按住 alt 再按);settings 的“快捷键”标签页可改,
|
|
42
|
+
// 配置存在 config.shortcuts,未写的项回退到这里
|
|
43
|
+
export const DEFAULT_SHORTCUTS = {
|
|
44
|
+
switchLast: 'z', // 立即切到上次会话
|
|
45
|
+
switchPrev: 'x', // 弹窗中上移
|
|
46
|
+
switchNext: 'c', // 弹窗中下移
|
|
47
|
+
switchRecent: 'e', // 打开最近 10 条
|
|
48
|
+
switchAttention: 'r', // 打开运行中 / 未查看
|
|
49
|
+
};
|
|
50
|
+
|
|
41
51
|
// —— ZCode 版本判断(按官方版本启用/停用功能)——
|
|
42
52
|
// 版本来源:启动器从 app.asar 读出后写进 bootstrap(window.__ZCODEPRO__.zcodeVersion),
|
|
43
53
|
// helper 的 /config 响应也附加同名字段。读不到时为空串,各判断保持旧行为。
|
|
@@ -190,7 +200,7 @@ const zh = {
|
|
|
190
200
|
featureTaskOrder: '侧边栏会话拖动排序',
|
|
191
201
|
featureTaskOrderDesc: '让置顶、项目与分组中的会话拖动后记住顺序,刷新后保持。',
|
|
192
202
|
featureSessionSwitch: '会话快捷切换',
|
|
193
|
-
featureSessionSwitchDesc: '
|
|
203
|
+
featureSessionSwitchDesc: '{last} 在当前与上次会话间来回切换;按住 alt 再按 {prev}/{next} 弹出最近会话列表前后选择(初始 10 条,{next} 到底再加载 5 条),{recent} 直接打开最近 10 条,{attention} 打开运行中与运行完未查看的任务;方向键同样可选,Enter 或松开 alt 切换(类 alt+tab)',
|
|
194
204
|
featureTitleLock: '重命名会话名锁定',
|
|
195
205
|
featureTitleLockDesc: '手动重命名过的会话不再被自动标题覆盖:会话有新回答后界面可能改显自动标题,重启后又变回,本功能让侧栏始终显示重命名后的名字。',
|
|
196
206
|
tabTitleGen: '任务名提示词',
|
|
@@ -218,7 +228,7 @@ const zh = {
|
|
|
218
228
|
titleGenTestOk: '生成结果:',
|
|
219
229
|
titleGenTestFailed: '生成失败',
|
|
220
230
|
featureWsRunning: '折叠时任务运行中提示',
|
|
221
|
-
featureWsRunningDesc: '
|
|
231
|
+
featureWsRunningDesc: '有会话在运行时,折叠的项目与“任务”“已置顶”等分区头显示与任务运行中同款的加载图标,全部结束后消失。',
|
|
222
232
|
tabPerformance: '性能',
|
|
223
233
|
perfDesc: 'ZCode 为每个打开过的项目常驻一个会话进程,应用自身不会回收,项目多了内存占用就高。开启后,长时间未使用且没有任务在跑的项目会被自动释放:会话记录不受影响,再次打开时自动恢复,仅首次回复有几秒等待。正在使用的项目、运行中的会话与后台命令、定时任务都不会被回收。后台命令检测依赖系统进程信息(Linux / macOS / Windows 各走系统自带工具),检测不可用时功能自动停用。',
|
|
224
234
|
featureIdleReclaim: '空闲项目内存回收',
|
|
@@ -246,10 +256,44 @@ const zh = {
|
|
|
246
256
|
userFontDesc: '会话中提问内容的字体;选“默认”时跟随界面字体。',
|
|
247
257
|
assistantFontName: '回答字体',
|
|
248
258
|
assistantFontDesc: '会话中回答正文的字体(代码块仍用等宽字体);选“默认”时跟随界面字体。',
|
|
249
|
-
switcherHint: '
|
|
259
|
+
switcherHint: '{prev} / {next} 或 ↑ / ↓ 选择,Enter 或松开 alt 切换,Esc 取消',
|
|
250
260
|
switcherCurrent: '当前',
|
|
251
261
|
switcherEmpty: '暂无上次会话(切换过会话后可用)',
|
|
252
262
|
switcherFailed: '未能切换到该会话(可能已删除)',
|
|
263
|
+
switcherMore: '还有 {n} 条 · 按 c 加载',
|
|
264
|
+
switcherRunning: '运行中',
|
|
265
|
+
switcherUnseen: '未查看',
|
|
266
|
+
switcherAttentionHint: '运行中 / 运行完未查看的任务 · {prev} / {next} 或 ↑ / ↓ 选择,Enter 或松开 alt 切换',
|
|
267
|
+
switcherAttentionEmpty: '暂无运行中或未查看的任务',
|
|
268
|
+
featureNewtabPanels: '新标签页插件面板入口',
|
|
269
|
+
featureNewtabPanelsDesc: '官方“新标签页”只列官方面板与推荐的官方插件,此功能把第三方市场插件(headroom、rtk 等)的面板卡片加进去,点击在侧边面板打开。',
|
|
270
|
+
ntpDescSuffix: '插件面板',
|
|
271
|
+
ntpNeedSession: '先打开一个任务会话,再从新标签页打开插件面板',
|
|
272
|
+
tabShortcuts: '快捷键',
|
|
273
|
+
hkTitle: '会话快捷切换',
|
|
274
|
+
hkAltZ: '在当前与上次使用的会话间来回切换',
|
|
275
|
+
hkAltX: '弹窗中上移高亮(更早的会话)',
|
|
276
|
+
hkAltC: '弹窗中下移高亮(更新的会话);到底且还有更多时加载 5 条并选中新加载的第一条',
|
|
277
|
+
hkAltE: '打开最近 10 条任务列表',
|
|
278
|
+
hkAltR: '打开正在运行与运行完未查看的任务列表',
|
|
279
|
+
hkArrow: '弹窗打开时移动高亮(与 alt + {prev} / alt + {next} 同规则)',
|
|
280
|
+
hkEnter: '确认切换到高亮的会话',
|
|
281
|
+
hkEsc: '取消并关闭弹窗',
|
|
282
|
+
hkRelease: '移动过高亮时松开即切换;从未移动则弹窗保持打开,继续选择',
|
|
283
|
+
hkReleaseKey: '松开 alt',
|
|
284
|
+
hkModify: '修改',
|
|
285
|
+
hkRecording: '按下新键…',
|
|
286
|
+
hkInvalid: '快捷键只支持单个字母或数字',
|
|
287
|
+
hkConflict: 'alt+{k} 已用于“{name}”',
|
|
288
|
+
hkSaved: '快捷键已更新',
|
|
289
|
+
hkNameLast: '切换上次会话',
|
|
290
|
+
hkNamePrev: '上移高亮',
|
|
291
|
+
hkNameNext: '下移高亮',
|
|
292
|
+
hkNameRecent: '最近 10 条',
|
|
293
|
+
hkNameAttention: '运行中 / 未查看',
|
|
294
|
+
hkEditHint: '点击“修改”后按下新按键(字母或数字),Esc 取消',
|
|
295
|
+
hkReset: '重置为默认',
|
|
296
|
+
hkResetDone: '已恢复默认键位',
|
|
253
297
|
featureFileActions: '文件菜单增强',
|
|
254
298
|
featureFileActionsDesc: '在会话中文件链接的右键菜单里新增“默认应用打开”与“打开所在目录”。',
|
|
255
299
|
featureImageCopy: '图片右键复制',
|
|
@@ -374,7 +418,7 @@ const zh = {
|
|
|
374
418
|
headroomStoppedWarn: '代理已停止;指向 127.0.0.1 的供应商将无法连接',
|
|
375
419
|
headroomBackend: '压缩设备',
|
|
376
420
|
headroomBackendAuto: '自动 · 显卡优先',
|
|
377
|
-
headroomBackendCpu: '
|
|
421
|
+
headroomBackendCpu: '省电 · CPU 优先',
|
|
378
422
|
headroomBackendNative: '(原生值)',
|
|
379
423
|
headroomBackendDesc: '自动:headroom 选最快设备(CUDA/MPS 显卡优先);CPU:强制 CPU 压缩,省电且不占显存。也支持 headroom 原生值(onnx、pytorch_mps 等)。切换会重启代理立即生效。',
|
|
380
424
|
headroomPower: '省电自动切换',
|
|
@@ -412,7 +456,7 @@ const zh = {
|
|
|
412
456
|
aliasSaved: '别名已更新',
|
|
413
457
|
aliasCleared: '已恢复真实名称',
|
|
414
458
|
aliasHint: '磁盘目录名不变:终端、文件管理器与其他引用真实路径的界面仍显示原名。',
|
|
415
|
-
featureProjectMenuDescNew: '
|
|
459
|
+
featureProjectMenuDescNew: '项目“更多”菜单中的打开文件夹与复制路径。官方已支持修改项目名称(编辑项目),自定义别名已停用。',
|
|
416
460
|
aliasMigrateTitle: '迁移项目别名',
|
|
417
461
|
aliasMigrateDesc: '当前 ZCode 已支持修改项目名称(项目“更多”菜单 → 编辑项目),自定义别名功能已停用。可把已保存的别名写入官方项目名称,勾选要迁移的项目;迁移只改项目名称,磁盘目录不受影响。',
|
|
418
462
|
aliasMigrateItemHint: '迁移后将作为项目名称保存(可随时在“编辑项目”中再改)。仅无标记的条目可勾选;已迁移、已并入、项目已删除、本机不存在(旧机器路径)以及项目已重命名的都不可勾选——要改这些项目的名字,直接用项目“更多”菜单里的“编辑项目”。',
|
|
@@ -581,7 +625,7 @@ const en = {
|
|
|
581
625
|
featureTaskOrder: 'Sidebar session drag ordering',
|
|
582
626
|
featureTaskOrderDesc: 'Makes session drags in Pinned, Projects and Groups persist across refreshes.',
|
|
583
627
|
featureSessionSwitch: 'Session quick switch',
|
|
584
|
-
featureSessionSwitchDesc: '
|
|
628
|
+
featureSessionSwitchDesc: '{last} toggles between the current and the last session; hold alt and press {prev}/{next} to move the highlight across recently used sessions (10 shown initially, pressing {next} at the bottom loads 5 more), {recent} opens the last 10 directly, {attention} opens running / finished-but-unseen tasks; arrow keys also move the highlight, Enter or release alt to switch (like alt+tab)',
|
|
585
629
|
featureTitleLock: 'Keep renamed session titles',
|
|
586
630
|
featureTitleLockDesc: 'Sessions you renamed keep their custom title: after new replies the app may show an auto-generated title instead, which reverts after a restart; this keeps the renamed title in the sidebar.',
|
|
587
631
|
tabTitleGen: 'Task name prompt',
|
|
@@ -609,7 +653,7 @@ const en = {
|
|
|
609
653
|
titleGenTestOk: 'Result: ',
|
|
610
654
|
titleGenTestFailed: 'Generation failed',
|
|
611
655
|
featureWsRunning: 'Running indicator when collapsed',
|
|
612
|
-
featureWsRunningDesc: 'Shows the same spinning loader as running tasks on collapsed projects and on section headers (“Tasks”, “Pinned”) while any session is running; clears when they all finish.
|
|
656
|
+
featureWsRunningDesc: 'Shows the same spinning loader as running tasks on collapsed projects and on section headers (“Tasks”, “Pinned”) while any session is running; clears when they all finish.',
|
|
613
657
|
tabPerformance: 'Performance',
|
|
614
658
|
perfDesc: 'ZCode keeps a session process resident for every project you have opened and never reclaims it, so memory grows with projects. When enabled, projects unused for a while with nothing running release their memory automatically: sessions are untouched and reload when reopened, with a few seconds wait on the first reply. The project in use, running sessions, background commands and scheduled tasks are never reclaimed. Background-command detection reads system process info via built-in tools (Linux / macOS / Windows); when unavailable, the feature disables itself.',
|
|
615
659
|
featureIdleReclaim: 'Idle project memory reclaim',
|
|
@@ -637,10 +681,44 @@ const en = {
|
|
|
637
681
|
userFontDesc: 'Font of question text in conversations; "default" follows the UI font.',
|
|
638
682
|
assistantFontName: 'Answer font',
|
|
639
683
|
assistantFontDesc: 'Font of answer text in conversations (code blocks stay monospaced); "default" follows the UI font.',
|
|
640
|
-
switcherHint: '
|
|
684
|
+
switcherHint: '{prev} / {next} or ↑ / ↓ to choose, Enter or release alt to switch, Esc to cancel',
|
|
641
685
|
switcherCurrent: 'current',
|
|
642
686
|
switcherEmpty: 'No previous session yet (available after you switch sessions)',
|
|
643
687
|
switcherFailed: 'Failed to switch to that session (it may have been deleted)',
|
|
688
|
+
switcherMore: '{n} more · press c to load',
|
|
689
|
+
switcherRunning: 'running',
|
|
690
|
+
switcherUnseen: 'unseen',
|
|
691
|
+
switcherAttentionHint: 'Running / finished but unseen tasks · {prev} / {next} or ↑ / ↓ to choose, Enter or release alt to switch',
|
|
692
|
+
switcherAttentionEmpty: 'No running or unseen tasks',
|
|
693
|
+
featureNewtabPanels: 'Plugin panels on the new tab page',
|
|
694
|
+
featureNewtabPanelsDesc: "The built-in \"new tab page\" only lists built-in panels and recommended official plugins; this adds cards for third-party marketplace plugin panels (headroom, rtk, …) that open in the side pane.",
|
|
695
|
+
ntpDescSuffix: 'plugin panel',
|
|
696
|
+
ntpNeedSession: 'Open a task session first, then open plugin panels from the new tab page',
|
|
697
|
+
tabShortcuts: 'Shortcuts',
|
|
698
|
+
hkTitle: 'Session quick switch',
|
|
699
|
+
hkAltZ: 'Toggle between the current and the last-used session',
|
|
700
|
+
hkAltX: 'Move the highlight up (earlier sessions) in the popup',
|
|
701
|
+
hkAltC: 'Move the highlight down (recent sessions); at the bottom it loads 5 more and highlights the first newly loaded entry',
|
|
702
|
+
hkAltE: 'Open the last 10 tasks',
|
|
703
|
+
hkAltR: 'Open running / finished-but-unseen tasks',
|
|
704
|
+
hkArrow: 'Move the highlight while the popup is open (same rules as alt + {prev} / alt + {next})',
|
|
705
|
+
hkEnter: 'Switch to the highlighted session',
|
|
706
|
+
hkEsc: 'Cancel and close the popup',
|
|
707
|
+
hkRelease: 'Releasing alt switches once the highlight has moved; otherwise the popup stays open for further selection',
|
|
708
|
+
hkReleaseKey: 'release alt',
|
|
709
|
+
hkModify: 'Change',
|
|
710
|
+
hkRecording: 'press a key…',
|
|
711
|
+
hkInvalid: 'Shortcuts accept a single letter or digit only',
|
|
712
|
+
hkConflict: 'alt+{k} is already used by "{name}"',
|
|
713
|
+
hkSaved: 'Shortcut updated',
|
|
714
|
+
hkNameLast: 'last session',
|
|
715
|
+
hkNamePrev: 'move up',
|
|
716
|
+
hkNameNext: 'move down',
|
|
717
|
+
hkNameRecent: 'last 10 tasks',
|
|
718
|
+
hkNameAttention: 'running / unseen',
|
|
719
|
+
hkEditHint: 'Click "Change", press a new key (letter or digit); Esc cancels',
|
|
720
|
+
hkReset: 'Reset to defaults',
|
|
721
|
+
hkResetDone: 'Shortcuts reset to defaults',
|
|
644
722
|
featureFileActions: 'File menu actions',
|
|
645
723
|
featureFileActionsDesc: 'Adds "Open with default app" and "Reveal in file manager" to the right-click menu of file links in chat.',
|
|
646
724
|
featureImageCopy: 'Image right-click copy',
|
|
@@ -765,7 +843,7 @@ const en = {
|
|
|
765
843
|
headroomStoppedWarn: 'Proxy stopped; providers pointing at 127.0.0.1 cannot connect.',
|
|
766
844
|
headroomBackend: 'Compression device',
|
|
767
845
|
headroomBackendAuto: 'Auto · GPU first',
|
|
768
|
-
headroomBackendCpu: '
|
|
846
|
+
headroomBackendCpu: 'Power saving · CPU first',
|
|
769
847
|
headroomBackendNative: ' (native)',
|
|
770
848
|
headroomBackendDesc: 'Auto lets headroom pick the fastest device (CUDA/MPS); CPU forces CPU compression (power saving, no VRAM). Native headroom values (onnx, pytorch_mps, …) also work. Switching restarts the proxy to apply.',
|
|
771
849
|
headroomPower: 'Power-save auto switch',
|
|
@@ -803,7 +881,7 @@ const en = {
|
|
|
803
881
|
aliasSaved: 'Alias updated.',
|
|
804
882
|
aliasCleared: 'Real name restored.',
|
|
805
883
|
aliasHint: 'The directory name on disk is unchanged: terminals, file managers and other path-based UI still show the real name.',
|
|
806
|
-
featureProjectMenuDescNew: '
|
|
884
|
+
featureProjectMenuDescNew: 'Open folder and copy path in the project "More" menu. Renaming is built in since ZCode 3.16.1 ("Edit project"), so the custom alias is retired.',
|
|
807
885
|
aliasMigrateTitle: 'Migrate project aliases',
|
|
808
886
|
aliasMigrateDesc: 'This ZCode supports renaming projects (project "More" menu → Edit project), so the custom alias feature is retired. Write your saved aliases into the official project names by picking the projects below; only the project name changes, folders on disk are untouched.',
|
|
809
887
|
aliasMigrateItemHint: 'The alias becomes the project name (re-editable anytime via "Edit project"). Only unmarked entries can be checked; migrated, merged, deleted, not-on-this-machine and manually-renamed ones cannot — to rename those projects, use "Edit project" in the project "More" menu directly.',
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// 新标签页插件面板入口:ZCode 3.16.1 的“新标签页”只列官方面板(文件/终端/
|
|
2
|
+
// 辅助对话/浏览器/审查)与云端推荐的官方插件,第三方市场(duanluan/zcode-plugins
|
|
3
|
+
// 等)的面板(headroom、rtk……)注册正常(listSurfaces 有返回)却没有任何入口。
|
|
4
|
+
// 本功能检测新标签页的模块网格,把非官方市场的面板注入成同款卡片;点击走官方
|
|
5
|
+
// openSidePane(App 层注入 Context Provider 的 handleOpenPluginUi,从 React
|
|
6
|
+
// fiber 里按函数源码特征串挖出)在侧边面板打开 tab——与插件市场页“打开”同一条
|
|
7
|
+
// 调用路径,不绕开官方逻辑。
|
|
8
|
+
import { t, getConfig, isZhLocale } from '../core.js';
|
|
9
|
+
import { ensureStyle, showToast } from '../ui.js';
|
|
10
|
+
|
|
11
|
+
// 官方“新标签页”终端模块的描述片段(界面中英文),作为网格识别特征
|
|
12
|
+
const GRID_HINTS = ['运行命令和查看输出', 'Run commands and view output'];
|
|
13
|
+
// 官方市场的 pluginId 后缀:这些面板已有云端推荐入口,不注入
|
|
14
|
+
const OFFICIAL_SUFFIX = '@zcode-plugins-official';
|
|
15
|
+
// handleOpenPluginUi(useCallback 产物)源码里的 logger 字符串,压缩不改动
|
|
16
|
+
// 字面量,以此从多个 Context Provider 中精确挑出打开面板的函数
|
|
17
|
+
const OPEN_FN_MARK = '打开插件 UI';
|
|
18
|
+
|
|
19
|
+
let enabled = true;
|
|
20
|
+
let injectTimer = 0;
|
|
21
|
+
|
|
22
|
+
// —— 从 React fiber 取应用内部对象(ws-running.js findServices 同款遍历) ——
|
|
23
|
+
|
|
24
|
+
function eachFiber(visit) {
|
|
25
|
+
const rootEl = document.getElementById('root');
|
|
26
|
+
const key = rootEl && Object.keys(rootEl).find((k) => k.startsWith('__reactContainer$'));
|
|
27
|
+
if (!key) return;
|
|
28
|
+
let visited = 0;
|
|
29
|
+
const walk = (f) => {
|
|
30
|
+
if (!f || visited > 200000) return;
|
|
31
|
+
visited++;
|
|
32
|
+
if (visit(f)) return true;
|
|
33
|
+
return walk(f.child) || walk(f.sibling);
|
|
34
|
+
};
|
|
35
|
+
walk(rootEl[key]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Context Provider 的 value 里找打开插件面板的函数(特征:源码含 logger 字符串)
|
|
39
|
+
function findOpenSidePane() {
|
|
40
|
+
let found = null;
|
|
41
|
+
eachFiber((f) => {
|
|
42
|
+
const et = f.elementType;
|
|
43
|
+
if (et && typeof et === 'object' && typeof et.$$typeof === 'symbol'
|
|
44
|
+
&& String(et.$$typeof).includes('react.context')) {
|
|
45
|
+
const val = f.memoizedProps && f.memoizedProps.value;
|
|
46
|
+
if (typeof val === 'function' && val.toString().includes(OPEN_FN_MARK)) {
|
|
47
|
+
found = val;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// pluginUiBridgeService(listSurfaces 数据源)
|
|
57
|
+
function findBridge() {
|
|
58
|
+
let holder = null;
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const scan = (o) => {
|
|
61
|
+
if (!o || typeof o !== 'object' || seen.has(o) || holder) return;
|
|
62
|
+
seen.add(o);
|
|
63
|
+
if (typeof o.pluginUiBridgeService === 'object' && o.pluginUiBridgeService) {
|
|
64
|
+
holder = o.pluginUiBridgeService;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const v of Object.values(o)) if (v && typeof v === 'object') scan(v);
|
|
68
|
+
};
|
|
69
|
+
eachFiber((f) => {
|
|
70
|
+
for (const h of [f.memoizedProps, f.memoizedState]) {
|
|
71
|
+
if (!h || typeof h !== 'object') continue;
|
|
72
|
+
scan(h);
|
|
73
|
+
if (holder) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
});
|
|
77
|
+
return holder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// —— 会话与面板清单 ——
|
|
81
|
+
|
|
82
|
+
// 打开面板必须挂在某个会话上(availability: session):优先当前选中会话,
|
|
83
|
+
// 其次侧栏最新一行(新会话更可能已随启动拉起插件的 UI server,老会话的
|
|
84
|
+
// server 目录里没有该插件时会卡在“正在加载插件界面”);一个都没有返回 null
|
|
85
|
+
function sessionScope() {
|
|
86
|
+
const rows = [...document.querySelectorAll('[data-task-item-key]')];
|
|
87
|
+
const cur = rows.find((e) => (e.className + '').includes('bg-selected')) || rows[rows.length - 1];
|
|
88
|
+
const key = cur ? cur.getAttribute('data-task-item-key') : '';
|
|
89
|
+
if (!key) return null;
|
|
90
|
+
const i = key.indexOf(':sess_');
|
|
91
|
+
return i > 0 ? { workspacePath: key.slice(0, i), sessionId: key.slice(i + 1) } : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function thirdPartySurfaces(bridge, workspacePath) {
|
|
95
|
+
try {
|
|
96
|
+
const list = await bridge.listSurfaces({ workspacePath });
|
|
97
|
+
return (list || []).filter((s) => !String(s.pluginId || '').endsWith(OFFICIAL_SUFFIX));
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function surfaceTitle(s, locale) {
|
|
104
|
+
const title = s.title;
|
|
105
|
+
const pick = (obj) => {
|
|
106
|
+
const lang = (locale || '').split('-')[0];
|
|
107
|
+
return obj[locale] || (lang && obj[lang])
|
|
108
|
+
|| Object.entries(obj).find(([k]) => k.toLowerCase().startsWith(lang || '\0'))?.[1]
|
|
109
|
+
|| obj.en || Object.values(obj)[0] || '';
|
|
110
|
+
};
|
|
111
|
+
if (typeof title === 'string') return title;
|
|
112
|
+
if (title && typeof title === 'object') {
|
|
113
|
+
return pick(title) || s.pluginName || s.id;
|
|
114
|
+
}
|
|
115
|
+
return s.pluginName || s.id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// —— 卡片注入 ——
|
|
119
|
+
|
|
120
|
+
// 通用面板图标(lucide layout-dashboard 同款路径),替换模板卡片原图标
|
|
121
|
+
const PANEL_ICON_PATHS = ['<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>'];
|
|
122
|
+
|
|
123
|
+
// 找“新标签页”的模块网格:官方卡片按钮带 data-side-pane-open-tab-item
|
|
124
|
+
// (3.16.1 实测标识),其父容器即网格;文本特征兜底校验(终端模块描述)
|
|
125
|
+
function findNewTabGrid() {
|
|
126
|
+
const item = document.querySelector('[data-side-pane-open-tab-item]');
|
|
127
|
+
if (!item || item.offsetParent === null) return null;
|
|
128
|
+
const grid = item.parentElement;
|
|
129
|
+
if (!grid || grid === document.body) return null;
|
|
130
|
+
const text = grid.textContent || '';
|
|
131
|
+
return GRID_HINTS.some((h) => text.includes(h)) ? grid : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function injectPanels(grid) {
|
|
135
|
+
if (grid.dataset.zcodeproPanels) return;
|
|
136
|
+
const template = grid.querySelector('[data-side-pane-open-tab-item]');
|
|
137
|
+
if (!template) return;
|
|
138
|
+
const scope = sessionScope();
|
|
139
|
+
if (!scope) return; // 没有任何会话时暂不注入,下次 DOM 变动再试
|
|
140
|
+
const bridge = findBridge();
|
|
141
|
+
if (!bridge) return;
|
|
142
|
+
const surfaces = await thirdPartySurfaces(bridge, scope.workspacePath);
|
|
143
|
+
if (!surfaces.length) { grid.dataset.zcodeproPanels = '1'; return; }
|
|
144
|
+
grid.dataset.zcodeproPanels = '1';
|
|
145
|
+
const L = t();
|
|
146
|
+
const locale = isZhLocale() ? 'zh-CN' : 'en-US';
|
|
147
|
+
for (const s of surfaces) {
|
|
148
|
+
// 官方卡片整卡即 button:克隆同款,去掉官方标识,挂我们的标识与点击
|
|
149
|
+
const card = template.cloneNode(true);
|
|
150
|
+
card.removeAttribute('data-side-pane-open-tab-item');
|
|
151
|
+
card.setAttribute('data-zcodepro-panel', s.id);
|
|
152
|
+
card.setAttribute('data-zcodepro-panel-name', s.pluginName || '');
|
|
153
|
+
const title = surfaceTitle(s, locale);
|
|
154
|
+
const desc = (s.pluginName || '') + ' · ' + L.ntpDescSuffix;
|
|
155
|
+
const titleEl = card.querySelector('span.font-semibold');
|
|
156
|
+
if (titleEl) titleEl.textContent = title;
|
|
157
|
+
const descEl = card.querySelector('span[title]');
|
|
158
|
+
if (descEl) { descEl.textContent = desc; descEl.setAttribute('title', desc); }
|
|
159
|
+
// 图标换通用面板图形(模板 svg 的描边样式保留)
|
|
160
|
+
const svg = card.querySelector('svg');
|
|
161
|
+
if (svg) {
|
|
162
|
+
svg.innerHTML = PANEL_ICON_PATHS[0];
|
|
163
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
164
|
+
}
|
|
165
|
+
card.addEventListener('click', (ev) => {
|
|
166
|
+
// 克隆卡片会冒泡到官方“新标签页”容器的点击处理(被当成再开一个新标签页),
|
|
167
|
+
// 这里拦下冒泡只走面板打开
|
|
168
|
+
ev.stopPropagation();
|
|
169
|
+
const sc = sessionScope();
|
|
170
|
+
const open = findOpenSidePane();
|
|
171
|
+
if (!sc || !open) {
|
|
172
|
+
showToast(L.ntpNeedSession, 'error');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// 字段与官方市场页 selectSurface 的请求一致;serverName(资源的 MCP
|
|
177
|
+
// server 归属)缺失会让沙箱准备一直停在“正在加载插件界面…”
|
|
178
|
+
open({
|
|
179
|
+
parentSessionId: sc.sessionId,
|
|
180
|
+
pluginId: s.pluginId,
|
|
181
|
+
surfaceId: s.id,
|
|
182
|
+
serverName: s.server,
|
|
183
|
+
resourceUri: s.resourceUri,
|
|
184
|
+
remoteSessionId: null,
|
|
185
|
+
title,
|
|
186
|
+
workspacePath: sc.workspacePath,
|
|
187
|
+
});
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.warn('[zcodepro] 打开插件面板失败', e);
|
|
190
|
+
showToast(L.ntpNeedSession, 'error');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
grid.append(card);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function tryInject() {
|
|
198
|
+
if (!enabled || document.hidden) return;
|
|
199
|
+
const grid = findNewTabGrid();
|
|
200
|
+
if (!grid) return;
|
|
201
|
+
// React 重渲染可能冲掉注入的卡片:标记还在但卡片没了时清标记重注
|
|
202
|
+
if (grid.dataset.zcodeproPanels && !grid.querySelector('[data-zcodepro-panel]')) {
|
|
203
|
+
delete grid.dataset.zcodeproPanels;
|
|
204
|
+
}
|
|
205
|
+
void injectPanels(grid);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function startNewtabPanels() {
|
|
209
|
+
ensureStyle();
|
|
210
|
+
const refreshEnabled = () => {
|
|
211
|
+
void getConfig().then((cfg) => {
|
|
212
|
+
enabled = cfg.features ? cfg.features.newtabPanels !== false : true;
|
|
213
|
+
}).catch(() => {});
|
|
214
|
+
};
|
|
215
|
+
refreshEnabled();
|
|
216
|
+
// 设置里开关变化即时生效(保存时会派发 config-changed)
|
|
217
|
+
window.addEventListener('zcodepro:config-changed', refreshEnabled);
|
|
218
|
+
const observer = new MutationObserver(() => {
|
|
219
|
+
if (injectTimer) return;
|
|
220
|
+
injectTimer = setTimeout(() => { injectTimer = 0; tryInject(); }, 300);
|
|
221
|
+
});
|
|
222
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
223
|
+
setInterval(tryInject, 5000);
|
|
224
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// 插件界面沙箱回收:宿主为每个插件交互卡片(识图卡片、压缩记录、评审面板等)
|
|
2
|
+
// 保留一个沙箱,全局上限 64 个(主进程注册表与窗口页面管理器各一道闸),已卸下
|
|
3
|
+
// 的名额默认要等 6 小时 TTL 才自动回收——多卡片会话很快把名额耗尽,之后所有新
|
|
4
|
+
// 卡片报“插件界面数量已达上限”。本功能平时不动作,只在沙箱数量达到上限
|
|
5
|
+
//(sandboxReclaimBudget,默认 60,8–64;留少量余量,避免新卡片抢名额时恰好撞上
|
|
6
|
+
// 64 硬上限)时,调用官方桥 window.zcodePluginSandbox.disposeSandbox 释放最早
|
|
7
|
+
// 创建的沙箱,把数量压回上限以下:被释放的卡片位置退回占位(可能显示已停止
|
|
8
|
+
// 提示),重新打开所在会话即恢复,会话文本内容不受影响。
|
|
9
|
+
//
|
|
10
|
+
// 驱逐顺序:最早创建的优先,但可见的(正在查看的卡片)排最后——先在不可见
|
|
11
|
+
// 沙箱里挑最早的;全部可见时才动最早创建的可见沙箱。
|
|
12
|
+
//
|
|
13
|
+
// 观察手段均为宿主稳定标识,不依赖压缩后的内部变量名:
|
|
14
|
+
// 1) window 消息 zcode:plugin-sandbox-ports:沙箱端口建立时广播 {sandboxId, initId};
|
|
15
|
+
// 2) webview[data-zcode-retained-sandbox]:src 含 /instance/<sandboxId>/;
|
|
16
|
+
// 3) 可见性:webview 所在页根 div[data-sandbox-page-root] 的 opacity——展示中的
|
|
17
|
+
// 页根 opacity:1 且按卡片占位几何摆放;卡片切走/滚下后宿主把页根收进停靠区
|
|
18
|
+
// ([data-sandbox-page-parking] 下,opacity:0.001 + inert 或 display:none)。
|
|
19
|
+
// 页根是固定定位、DOM 挂载点与视觉位置解耦,不能按父容器判断可见性。
|
|
20
|
+
//
|
|
21
|
+
// 窗口最小化/后台不改变布局尺寸,可见性判定不受影响(最小化期间不驱逐)。
|
|
22
|
+
// 开关:features.sandboxReclaim(默认开)。
|
|
23
|
+
import { getConfig } from '../core.js';
|
|
24
|
+
|
|
25
|
+
const CHECK_MS = 30_000; // 巡检周期
|
|
26
|
+
const MIN_BUDGET = 8;
|
|
27
|
+
const MAX_BUDGET = 64;
|
|
28
|
+
const RETRY_COOLDOWN_MS = 10 * 60_000; // 同一沙箱释放失败后的重试冷却
|
|
29
|
+
|
|
30
|
+
const seen = new Map(); // sandboxId → { initId, firstSeen }
|
|
31
|
+
const cooldown = new Map(); // sandboxId → 释放失败冷却截止时刻
|
|
32
|
+
let disabledLogged = false;
|
|
33
|
+
|
|
34
|
+
function onPortsMessage(e) {
|
|
35
|
+
const d = e.data;
|
|
36
|
+
if (!d || d.type !== 'zcode:plugin-sandbox-ports' || typeof d.sandboxId !== 'string') return;
|
|
37
|
+
// 端口广播≈沙箱创建时刻;页面刷新后宿主按幂等键复用同一沙箱并再次广播,
|
|
38
|
+
// 创建时刻随之刷新(视作新名额重新排队)
|
|
39
|
+
seen.set(d.sandboxId, { initId: Number(d.initId) || 0, firstSeen: Date.now() });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractSandboxId(wv) {
|
|
43
|
+
const m = /instance\/(sb-[0-9a-f]+)\//.exec(wv.getAttribute('src') || '');
|
|
44
|
+
return m ? m[1] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 展示中的页根 opacity:1 且按卡片占位几何摆放;停靠保活的页根 opacity:0.001 +
|
|
48
|
+
// inert(或 display:none)。宿主用固定定位按占位几何摆放页根,DOM 挂载点不代表
|
|
49
|
+
// 视觉位置(可见卡片的页根也可能挂在停靠容器下),因此按 opacity 与布局判定
|
|
50
|
+
function isVisible(wv) {
|
|
51
|
+
const root = wv.closest('[data-sandbox-page-root]');
|
|
52
|
+
if (!root) return wv.getClientRects().length > 0;
|
|
53
|
+
if (!root.getClientRects().length) return false;
|
|
54
|
+
return Number(getComputedStyle(root).opacity) > 0.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function tick() {
|
|
58
|
+
const cfg = await getConfig();
|
|
59
|
+
if ((cfg.features || {}).sandboxReclaim === false) {
|
|
60
|
+
seen.clear();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// 沙箱桥只在宿主主窗口存在(guest 沙箱页与旧版宿主没有):不在时静默跳过
|
|
64
|
+
const bridge = window.zcodePluginSandbox;
|
|
65
|
+
if (!bridge || typeof bridge.disposeSandbox !== 'function') {
|
|
66
|
+
if (!disabledLogged) {
|
|
67
|
+
disabledLogged = true;
|
|
68
|
+
console.info('[zcodepro] sandbox-reclaim: 当前窗口无 zcodePluginSandbox 桥,沙箱回收跳过');
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let budget = Number(cfg.sandboxReclaimBudget);
|
|
73
|
+
if (!Number.isFinite(budget)) budget = 60;
|
|
74
|
+
budget = Math.min(MAX_BUDGET, Math.max(MIN_BUDGET, budget));
|
|
75
|
+
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const live = new Map(); // sandboxId → 是否可见
|
|
78
|
+
for (const wv of document.querySelectorAll('webview[data-zcode-retained-sandbox]')) {
|
|
79
|
+
const id = extractSandboxId(wv);
|
|
80
|
+
if (id) live.set(id, isVisible(wv));
|
|
81
|
+
}
|
|
82
|
+
// 端口广播登记过但 DOM 已无对应 webview 的条目(guest 已销毁):清掉,防 Map 无限增长
|
|
83
|
+
for (const id of [...seen.keys()]) {
|
|
84
|
+
if (!live.has(id)) seen.delete(id);
|
|
85
|
+
}
|
|
86
|
+
// 重注入后端口广播没赶上(Map 为空)的兜底:首次观察到时按现在起算创建时刻
|
|
87
|
+
for (const id of live.keys()) {
|
|
88
|
+
if (!seen.has(id)) seen.set(id, { initId: undefined, firstSeen: now });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while (live.size > budget) {
|
|
92
|
+
// 驱逐顺序:不可见的按创建时间最早的 → 全可见时最早的可见沙箱
|
|
93
|
+
const candidates = [...live.entries()]
|
|
94
|
+
.filter(([id]) => (cooldown.get(id) || 0) <= now)
|
|
95
|
+
.sort((a, b) => (a[1] - b[1]) || ((seen.get(a[0]) || {}).firstSeen || 0) - ((seen.get(b[0]) || {}).firstSeen || 0));
|
|
96
|
+
if (!candidates.length) break;
|
|
97
|
+
const [id] = candidates[0];
|
|
98
|
+
const entry = seen.get(id);
|
|
99
|
+
try {
|
|
100
|
+
await bridge.disposeSandbox(id, entry && entry.initId);
|
|
101
|
+
console.info('[zcodepro] sandbox-reclaim: 沙箱满 ' + live.size + ' 个,已驱逐最早的 ' + id);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn('[zcodepro] sandbox-reclaim: 释放失败 ' + id, err);
|
|
104
|
+
}
|
|
105
|
+
seen.delete(id);
|
|
106
|
+
live.delete(id);
|
|
107
|
+
cooldown.set(id, now + RETRY_COOLDOWN_MS);
|
|
108
|
+
}
|
|
109
|
+
if (cooldown.size > 256) {
|
|
110
|
+
for (const [k, v] of cooldown) {
|
|
111
|
+
if (v <= now) cooldown.delete(k);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function startSandboxReclaim() {
|
|
117
|
+
window.addEventListener('message', onPortsMessage);
|
|
118
|
+
const loop = () => { tick().catch(() => { /* 单次巡检失败不影响后续 */ }); };
|
|
119
|
+
loop();
|
|
120
|
+
setInterval(loop, CHECK_MS);
|
|
121
|
+
}
|