minecodex 2.0.54 → 2.1.1

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.
Files changed (68) hide show
  1. package/README.md +90 -26
  2. package/apps/dashboard/app.js +20 -14
  3. package/apps/dashboard/assets/chevron-left.svg +13 -0
  4. package/apps/dashboard/assets/chevron-right.svg +13 -0
  5. package/apps/dashboard/assets/feature-showcase.js +185 -0
  6. package/apps/dashboard/assets/pause.svg +14 -0
  7. package/apps/dashboard/assets/play.svg +13 -0
  8. package/apps/dashboard/assets/previews/composer-expand.jpg +0 -0
  9. package/apps/dashboard/assets/previews/composer-expand.mp4 +0 -0
  10. package/apps/dashboard/assets/previews/file-navigation.jpg +0 -0
  11. package/apps/dashboard/assets/previews/file-navigation.mp4 +0 -0
  12. package/apps/dashboard/assets/previews/files-auto-refresh.jpg +0 -0
  13. package/apps/dashboard/assets/previews/files-auto-refresh.mp4 +0 -0
  14. package/apps/dashboard/assets/previews/images.jpg +0 -0
  15. package/apps/dashboard/assets/previews/images.mp4 +0 -0
  16. package/apps/dashboard/assets/previews/manifest.json +67 -1
  17. package/apps/dashboard/assets/previews/markdown-reading.jpg +0 -0
  18. package/apps/dashboard/assets/previews/markdown-reading.mp4 +0 -0
  19. package/apps/dashboard/assets/previews/mermaid-diagrams.jpg +0 -0
  20. package/apps/dashboard/assets/previews/mermaid-diagrams.mp4 +0 -0
  21. package/apps/dashboard/assets/previews/model-slider.jpg +0 -0
  22. package/apps/dashboard/assets/previews/model-slider.mp4 +0 -0
  23. package/apps/dashboard/assets/previews/notes.jpg +0 -0
  24. package/apps/dashboard/assets/previews/notes.mp4 +0 -0
  25. package/apps/dashboard/assets/previews/quick-chat.jpg +0 -0
  26. package/apps/dashboard/assets/previews/quick-chat.mp4 +0 -0
  27. package/apps/dashboard/assets/previews/session-status.jpg +0 -0
  28. package/apps/dashboard/assets/previews/session-status.mp4 +0 -0
  29. package/apps/dashboard/assets/previews/sidebar-focus.jpg +0 -0
  30. package/apps/dashboard/assets/previews/sidebar-focus.mp4 +0 -0
  31. package/apps/dashboard/assets/previews/sites.jpg +0 -0
  32. package/apps/dashboard/assets/previews/sites.mp4 +0 -0
  33. package/apps/dashboard/assets/previews/usage-meter.jpg +0 -0
  34. package/apps/dashboard/assets/previews/usage-meter.mp4 +0 -0
  35. package/apps/dashboard/index.html +26 -2
  36. package/apps/dashboard/styles.css +58 -39
  37. package/assets/readme/hero-dark.svg +50 -0
  38. package/assets/readme/hero.svg +36 -79
  39. package/features/images/src/adapters/codex/rspack-modules.mjs +9 -3
  40. package/features/images/src/library-prompt-source.mjs +2 -2
  41. package/features/mermaid-diagrams/assets/qv-template.json +1 -1
  42. package/features/mermaid-diagrams/src/mermaid/qv-polish.css +6 -2
  43. package/features/notes/codex-feature.json +1 -0
  44. package/features/quick-chat/src/adapters/codex/renderer.mjs +3 -1
  45. package/package.json +3 -3
  46. package/packages/cli/src/config.mjs +1 -2
  47. package/packages/cli/src/control-server.mjs +1 -1
  48. package/packages/plugin/assets/app.js +35 -35
  49. package/packages/plugin/assets/prompts.js +1 -1
  50. package/packages/runtime-host/src/codex-cdp.mjs +0 -7
  51. package/packages/runtime-host/src/codex-injection.mjs +50 -2114
  52. package/packages/runtime-host/src/codex-runtime.mjs +4 -203
  53. package/packages/runtime-host/src/feature-registry.mjs +10 -91
  54. package/packages/runtime-host/src/main.mjs +0 -1
  55. package/packages/ui/src/native/tokens.mjs +0 -28
  56. package/apps/dashboard/assets/feature-previews.js +0 -206
  57. package/features/images/src/adapters/codex/cloud-import.mjs +0 -52
  58. package/features/images/src/backend/cloud-library.mjs +0 -12
  59. package/features/images/src/chatgpt-image-source-page.mjs +0 -207
  60. package/features/images/src/chatgpt-image-source.mjs +0 -42
  61. package/features/images/src/http-server.mjs +0 -487
  62. package/features/images/src/main.mjs +0 -52
  63. package/features/images/src/save-as.mjs +0 -69
  64. package/features/images/web/app.js +0 -1780
  65. package/features/images/web/i18n.mjs +0 -132
  66. package/features/images/web/index.html +0 -187
  67. package/features/images/web/styles.css +0 -1114
  68. package/packages/runtime-host/src/host-actions.mjs +0 -221
package/README.md CHANGED
@@ -1,30 +1,63 @@
1
1
  <p align="center">
2
- <img src="./assets/readme/hero.svg" width="100%" alt="MineCodex:让 Codex 更顺手">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="./assets/readme/hero-dark.svg">
4
+ <img src="./assets/readme/hero.svg" width="100%" alt="MineCodex:让 Codex 更顺手。13 项按需开启的 Codex Desktop 交互增强,支持 macOS 与 Windows。">
5
+ </picture>
3
6
  </p>
4
7
 
5
- # MineCodex
8
+ <p align="center">
9
+ <a href="#安装">安装</a> ·
10
+ <a href="#13-项增强">全部功能</a> ·
11
+ <a href="#它如何工作">工作方式与隐私</a> ·
12
+ <a href="#常见问题">常见问题</a>
13
+ </p>
6
14
 
7
- 为 Codex Desktop 添加轻量扩展,按需开启,让日常使用更顺手。
15
+ **MineCodex 是一个给 Codex Desktop 用的增强工具箱。** 它在你熟悉的 Codex 界面里补上一些顺手的小功能:更快地切换模型、一键填入常用提示词、随时看到剩余额度、更舒服地阅读长文档和图表。每一项都可以单独开关,外观沿用 Codex 原生风格,看起来就像 Codex 本来就有。
8
16
 
9
- ## 功能
17
+ <table>
18
+ <tr>
19
+ <td width="50%"><img src="./apps/dashboard/assets/previews/model-slider.jpg" alt="模型窗口:在输入框旁直接选择模型与思考档位"></td>
20
+ <td width="50%"><img src="./apps/dashboard/assets/previews/usage-meter.jpg" alt="额度环:在侧栏查看五小时与每周剩余额度"></td>
21
+ </tr>
22
+ <tr>
23
+ <td><b>模型窗口</b> · 在输入框旁选模型和思考档位,常用模型可收藏</td>
24
+ <td><b>额度环</b> · 不打开菜单,也能看到五小时和每周额度</td>
25
+ </tr>
26
+ <tr>
27
+ <td><img src="./apps/dashboard/assets/previews/session-status.jpg" alt="会话状态:查看当前会话的 Token 用量、上下文与生成速度"></td>
28
+ <td><img src="./apps/dashboard/assets/previews/mermaid-diagrams.jpg" alt="Mermaid 图表增强:在 Markdown 预览中查看排版更清楚的图表"></td>
29
+ </tr>
30
+ <tr>
31
+ <td><b>会话状态</b> · 当前会话的用量、上下文和生成速度集中在一处</td>
32
+ <td><b>Mermaid 图表增强</b> · 复杂图表排得更清楚,可缩放、可下载 PNG</td>
33
+ </tr>
34
+ </table>
10
35
 
11
- | 插件 | 功能 |
12
- | --- | --- |
13
- | 模型窗口 | 切换模型和思考强度,搜索、收藏常用模型。 |
14
- | 提示词库 | 分类管理提示词,一键填入输入框,不自动发送。 |
15
- | 额度环 | 查看五小时与每周剩余额度,点击打开用量详情。 |
16
- | 会话状态 | 查看回复速度、Token 用量和上下文占用。 |
17
- | Markdown 阅读增强 | 改善文档排版,支持章节目录、表格展开和代码复制。 |
18
- | Mermaid 图表增强 | 独立开关,默认启用;支持图表缩放与 PNG 下载。 |
19
- | 文件级联浏览 | 点击文件路径,分列浏览目录并切换文件。 |
20
- | 图片集合 | 浏览本机生成图片,查看提示词,收藏到 Library。 |
21
- | 站点收藏 | 发现本地项目网页,收藏网址并快速打开。 |
22
- | 快捷聊天增强 | 恢复快捷键,发送后展开,支持历史与窗口固定。 |
23
- | 侧栏聚焦 | 收起其他任务分组,再次点击恢复布局。 |
36
+ ## 13 项增强
37
+
38
+ 全部功能都可以在控制台单独开关。控制台里每一项都配有真实录屏,可以先看效果再决定是否开启。
39
+
40
+ | 场景 | 功能 | 它帮你解决什么 |
41
+ | --- | --- | --- |
42
+ | 选模型与提示词 | **模型窗口** | 模型和思考档位放在一起选,常用模型可搜索、可收藏。 |
43
+ | | **提示词库** | 按分类管理常用提示词,一键填入输入框;不会自动发送,由你检查后再发。 |
44
+ | 掌握用量 | **额度环** | 侧栏常驻五小时与每周剩余额度,点击查看用量详情。 |
45
+ | | **会话状态** | 当前会话的 Token 用量、上下文占用和回复速度集中显示。 |
46
+ | 阅读文档与图表 | **Markdown 阅读增强** | 长文排版更舒服,支持章节目录、脚注跳转、表格展开和代码复制。 |
47
+ | | **Mermaid 图表增强** | 复杂图表布局更清楚,可缩放、可下载 PNG;关闭后恢复原生图表。 |
48
+ | 浏览项目文件 | **文件级联浏览** | 点击文件路径,分列浏览相邻目录并切换文件。 |
49
+ | | **Files 自动刷新** | Agent 新增、删除或重命名文件后,Files 列表自动更新,保留浏览位置。 |
50
+ | 管理素材与网页 | **图片集合** | 浏览本机生成的图片,查看对应提示词,收藏到 Library。 |
51
+ | | **站点收藏** | 自动发现正在运行的本地项目网页,收藏常用网址并快速打开。 |
52
+ | 写作与提问 | **长指令展开编辑** | 写长指令时展开成大尺寸输入框,收起后草稿保持不变。 |
53
+ | | **快捷聊天增强** | 恢复 Quick Chat 快捷键,发送后自动展开,支持历史与窗口固定。 |
54
+ | 整理界面 | **侧栏聚焦** | 一键收起其他任务分组,只看当前分组;再点一次恢复原样。 |
24
55
 
25
56
  ## 安装
26
57
 
27
- 支持 macOS 与 Windows。先安装 Codex Desktop 和 [Node.js](https://nodejs.org/) 22.5 或更高版本,然后在终端运行:
58
+ **准备:** macOS 或 Windows,已安装 Codex Desktop 和 [Node.js](https://nodejs.org/) 22.5 或更高版本。
59
+
60
+ 在终端依次运行:
28
61
 
29
62
  ```bash
30
63
  npm install -g minecodex
@@ -32,22 +65,53 @@ mcx install
32
65
  mcx open
33
66
  ```
34
67
 
35
- 安装后重启 Codex 即可使用。在控制台按需开关功能,修改后运行 `mcx restart` 应用设置。
68
+ `mcx install` 会安装后台服务,完成后询问是否立即重启 Codex;重启后增强即生效。`mcx open` 会在浏览器中打开控制台,你可以在这里按需开关功能,修改后运行 `mcx restart` 应用设置。
69
+
70
+ <details>
71
+ <summary><b>可选:在 Codex 里直接打开控制台和 Prompt 页面</b></summary>
72
+
73
+ <br>
74
+
75
+ 1. 先安装 [Codex CLI](https://developers.openai.com/codex/cli/),再运行:
36
76
 
37
- 如需在 Codex 内打开控制台和 Prompt 页面,先安装 [Codex CLI](https://developers.openai.com/codex/cli/),再运行 `mcx plugin install`。这一步独立于后台增强安装;更换全局包或源码安装位置后,再运行一次以更新插件路径。
77
+ ```bash
78
+ mcx plugin install
79
+ ```
38
80
 
39
- 注册后,在 Codex 的插件页点击刷新,再从 Explore/探索找到 MineCodex 和 Prompt。需要常驻 Navbar 时,分别点击它们旁边的“固定到侧边栏”。插件已安装、页面入口已发现、入口已固定是不同状态;未固定的入口不会常驻 Navbar。此路径已在 Codex 26.1013.4894.0 的 Windows 简体中文窗口验证。
81
+ 2. 在 Codex 的插件页点击刷新,从 Explore/探索找到 MineCodex 和 Prompt。
82
+ 3. 需要常驻侧边栏时,分别点击它们旁边的“固定到侧边栏”。
40
83
 
41
- ## 常用命令
84
+ </details>
85
+
86
+ ### 常用命令
42
87
 
43
88
  | 命令 | 作用 |
44
89
  | --- | --- |
45
90
  | `mcx open` | 打开控制台 |
46
- | `mcx update` | 更新到最新版本 |
47
91
  | `mcx restart` | 重启 Codex 并应用设置 |
48
- | `mcx status` | 查看运行状态 |
49
- | `mcx uninstall` | 卸载,保留个人数据 |
92
+ | `mcx status` | 查看安装与运行状态 |
93
+ | `mcx update` | 更新到最新稳定版 |
94
+ | `mcx uninstall` | 卸载,保留个人数据;加 `--purge` 同时删除数据 |
95
+
96
+ ## 它如何工作
97
+
98
+ MineCodex 在本机运行一个后台服务。它让 Codex 以只对本机开放(`127.0.0.1`)的调试端口启动,并在运行时把增强加载进 Codex 窗口。
99
+
100
+ - **不修改 Codex 安装文件。** 卸载 MineCodex 后重新打开 Codex,就是原本的样子。
101
+ - **数据留在本机。** 提示词、收藏和设置保存在你的电脑上;除了检查新版本时访问 npm,MineCodex 不上传任何内容。
102
+ - **单项失效不影响整体。** 某个增强出问题时,Codex 和其他增强照常运行。
103
+
104
+ ## 常见问题
105
+
106
+ **Codex 更新后某个增强不见了?**
107
+ MineCodex 依赖 Codex 的界面结构。Codex 改版后,个别增强可能暂时失效。先运行 `mcx update` 获取最新版本,再运行 `mcx restart`;问题仍在时,可以在控制台页尾复制诊断信息并[提交 Issue](https://github.com/Vontean/MineCodex/issues)。
108
+
109
+ **会影响我正在进行的会话吗?**
110
+ `mcx restart`(以及安装结束时你确认的重启)会重启 Codex。建议在没有正在运行的任务时操作。
111
+
112
+ **这是 OpenAI 官方产品吗?**
113
+ 不是。MineCodex 是非官方的独立项目,与 OpenAI 无关联。
50
114
 
51
115
  ## 许可
52
116
 
53
- [MIT](LICENSE)。MineCodex 是非官方独立扩展,与 OpenAI 无关联;第三方素材遵循各自的[权利说明](features/model-slider/THIRD_PARTY_NOTICES.md)。
117
+ [MIT](LICENSE)。第三方素材遵循各自的[权利说明](features/model-slider/THIRD_PARTY_NOTICES.md)。
@@ -1,4 +1,4 @@
1
- import {installFeaturePreviews} from "./assets/feature-previews.js";
1
+ import {installFeatureShowcase} from "./assets/feature-showcase.js";
2
2
  import {diagnosticReport} from "./assets/diagnostics.js";
3
3
  import {dashboardFetch} from "./transport.js";
4
4
  let savedLocale;
@@ -9,7 +9,8 @@ document.documentElement.lang = locale;
9
9
 
10
10
  const messages = {
11
11
  en: {
12
- plugins: "Your extensions",
12
+ plugins: "All enhancements",
13
+ pluginsHint: "Click a card to watch its demo above. Use the switch to turn it on or off.",
13
14
  restartTitle: "Restart Codex to apply changes",
14
15
  restartCopy: "Your plugin choices are saved. Restart when you are ready.",
15
16
  restart: "Restart Codex",
@@ -50,7 +51,8 @@ const messages = {
50
51
  failureDetail: "This extension could not be confirmed. Copy diagnostics to help investigate.",
51
52
  },
52
53
  "zh-CN": {
53
- plugins: "功能扩展",
54
+ plugins: "全部增强",
55
+ pluginsHint: "点击卡片在上方查看演示,开关控制是否启用。",
54
56
  restartTitle: "重启 Codex 以应用更改",
55
57
  restartCopy: "插件选择已经保存,可以在准备好后统一重启。",
56
58
  restart: "重启 Codex",
@@ -120,7 +122,7 @@ const elements = {
120
122
  let state = null;
121
123
  let toastTimer = null;
122
124
  const pendingFeatureIds = new Set();
123
- const previews = installFeaturePreviews(elements.pluginList, locale);
125
+ const showcase = installFeatureShowcase(document.querySelector("#feature-showcase"), locale);
124
126
 
125
127
  function applyStaticCopy() {
126
128
  document.querySelector("#brand-slogan").textContent = locale === "zh-CN" ? "让 Codex 更顺手。" : "Make Codex work better for you.";
@@ -136,6 +138,8 @@ function applyStaticCopy() {
136
138
  elements.restartCopy.textContent = copy.restartCopy;
137
139
  elements.restartButton.querySelector(".button-label").textContent = copy.restart;
138
140
  elements.pluginsHeading.textContent = copy.plugins;
141
+ document.querySelector("#plugins-hint").textContent = copy.pluginsHint;
142
+ renderEyebrow();
139
143
  elements.errorTitle.textContent = copy.loadError;
140
144
  elements.errorCopy.textContent = copy.loadErrorCopy;
141
145
  elements.retryButton.textContent = copy.retry;
@@ -208,7 +212,7 @@ function createPluginCard(feature) {
208
212
  pluginState.append(dot, stateLabel);
209
213
  heading.append(icon, label);
210
214
  card.append(heading, pluginCopy, pluginState);
211
- previews.attach(card);
215
+ showcase.attach(card);
212
216
  return card;
213
217
  }
214
218
 
@@ -230,8 +234,6 @@ function updatePluginCard(card, feature) {
230
234
  input.setAttribute("aria-label", feature.label);
231
235
  dot.className = `status-dot ${status.className}`;
232
236
  stateLabel.textContent = status.label;
233
- const previewEntry = card.querySelector(".preview-entry");
234
- previewEntry?.setAttribute("aria-label", `${locale === "zh-CN" ? "查看效果" : "See how it works"} · ${feature.label}`);
235
237
  }
236
238
 
237
239
  function findPluginCard(featureId) {
@@ -239,8 +241,6 @@ function findPluginCard(featureId) {
239
241
  }
240
242
 
241
243
  function renderPluginCards(features) {
242
- // Task 暂停接入,旧版服务返回的卡片也不展示。
243
- features = features.filter(feature => !["unread-inbox", "hide-upsell-banner"].includes(feature.id));
244
244
  for (const child of [...elements.pluginList.children]) {
245
245
  if (!child.dataset.featureId) child.remove();
246
246
  }
@@ -256,11 +256,19 @@ function renderPluginCards(features) {
256
256
  }
257
257
  for (const card of [...elements.pluginList.querySelectorAll("[data-feature-id]")]) {
258
258
  if (card.dataset.featureId && !featureIds.has(card.dataset.featureId)) {
259
- if (card.classList.contains("is-previewing")) previews.close();
259
+ showcase.detach(card);
260
260
  card.remove();
261
261
  }
262
262
  }
263
263
  elements.pluginList.setAttribute("aria-busy", "false");
264
+ showcase.setFeatures(features);
265
+ renderEyebrow();
266
+ }
267
+
268
+ function renderEyebrow() {
269
+ const total = state?.features?.length;
270
+ const base = locale === "zh-CN" ? "Codex 增强工具箱" : "Enhancements for Codex";
271
+ document.querySelector("#hero-eyebrow-text").textContent = total ? `${base} · ${total}${locale === "zh-CN" ? " 项" : ""}` : base;
264
272
  }
265
273
 
266
274
  function renderRuntime() {
@@ -309,7 +317,7 @@ async function load() {
309
317
  elements.pluginList.hidden = false;
310
318
  render();
311
319
  } catch {
312
- previews.close();
320
+ showcase.setFeatures([]);
313
321
  elements.pluginList.hidden = true;
314
322
  elements.loadError.hidden = false;
315
323
  }
@@ -429,7 +437,6 @@ function applyTheme() {
429
437
  let themeTransition = null;
430
438
  async function toggleTheme(button) {
431
439
  if (themeTransition) return;
432
- previews.close();
433
440
  selectedTheme = document.documentElement.dataset.theme === "dark" ? "light" : "dark";
434
441
  if (!window.minecodexMcpApp) {
435
442
  try { localStorage.setItem("minecodex-appearance", selectedTheme); } catch { /* 保留内存中的选择。 */ }
@@ -462,12 +469,11 @@ elements.languageToggle.addEventListener("click", (event) => {
462
469
  if (choice === locale) return;
463
470
  try { localStorage.setItem("minecodex-language", choice); } catch { return; }
464
471
  // 浏览器沿用重载;官方 sandbox 在当前上下文内切换,保留主题与通信。
465
- previews.close();
466
472
  if (!window.minecodexMcpApp) { location.reload(); return; }
467
473
  locale = choice;
468
474
  copy = messages[locale];
469
475
  document.documentElement.lang = locale;
470
- previews.setLocale(locale);
476
+ showcase.setLocale(locale);
471
477
  document.querySelector("#language-menu").hidePopover();
472
478
  applyStaticCopy();
473
479
  applyTheme();
@@ -0,0 +1,13 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="m15 18-6-6 6-6" />
13
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="m9 18 6-6-6-6" />
13
+ </svg>
@@ -0,0 +1,185 @@
1
+ import {previewContent} from './preview-content.js';
2
+
3
+ // 功能演示轮播:按卡片顺序自动播放各功能的真实录屏;点击卡片切换到对应功能。
4
+ export function installFeatureShowcase(root, locale) {
5
+ let zh = locale === 'zh-CN';
6
+ const t = (cn, en) => zh ? cn : en;
7
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)');
8
+ const $ = selector => root.querySelector(selector);
9
+ const media = $('.showcase-media'), video = $('.showcase-video'), poster = $('.showcase-poster'), status = $('.showcase-state');
10
+ const count = $('.showcase-count'), name = $('.showcase-name'), details = $('.showcase-details'), progress = $('.showcase-progress');
11
+ const prevButton = $('[data-showcase="prev"]'), nextButton = $('[data-showcase="next"]'), toggleButton = $('[data-showcase="toggle"]');
12
+ let features = [], index = -1, generation = 0, mediaAbort = null, advanceTimer = 0, frame = 0;
13
+ // userPaused 只由暂停按钮改变;inView / pageVisible 只暂停播放,不改变用户选择。
14
+ let userPaused = reduced.matches, inView = false, pageVisible = !document.hidden, manifest;
15
+ const cards = new Map();
16
+ const urls = new Map();
17
+
18
+ const loadManifest = () => manifest ??= window.minecodexPreviewMedia !== undefined
19
+ ? Promise.resolve(window.minecodexPreviewMedia)
20
+ : fetch('/assets/previews/manifest.json').then(r => r.ok ? r.json() : {}).then(value => value && typeof value === 'object' ? value : {}).catch(() => ({}));
21
+ async function resourceUrl(id, kind, value, signal) {
22
+ if (!window.minecodexMcpApp) return `/assets/previews/${value}`;
23
+ const key = `${id}/${kind}`;
24
+ if (urls.has(key)) return urls.get(key);
25
+ const response = await window.minecodexMcpApp.readServerResource({uri:`resource://minecodex/previews/${id}/${kind}`}, {signal});
26
+ const resource = response.contents?.[0];
27
+ if (!resource?.blob) throw new Error('Preview resource unavailable');
28
+ const url = `data:${resource.mimeType};base64,${resource.blob}`;
29
+ urls.set(key, url);
30
+ return url;
31
+ }
32
+
33
+ const shouldPlay = () => !userPaused && inView && pageVisible;
34
+ const current = () => features[index];
35
+
36
+ function renderControls() {
37
+ const playing = !userPaused;
38
+ toggleButton.querySelector('.ui-icon').className = `ui-icon ${playing ? 'pause-icon' : 'play-icon'}`;
39
+ const label = playing ? t('暂停演示', 'Pause demos') : t('播放演示', 'Play demos');
40
+ toggleButton.setAttribute('aria-label', label); toggleButton.setAttribute('data-minecodex-tooltip', label);
41
+ for (const [button, cn, en] of [[prevButton, '上一个功能', 'Previous feature'], [nextButton, '下一个功能', 'Next feature']]) {
42
+ button.setAttribute('aria-label', t(cn, en)); button.setAttribute('data-minecodex-tooltip', t(cn, en));
43
+ button.disabled = features.length < 2;
44
+ }
45
+ root.setAttribute('aria-label', t('功能演示', 'Feature demos'));
46
+ }
47
+ function renderInfo() {
48
+ const feature = current();
49
+ if (!feature) return;
50
+ const pad = n => String(n).padStart(2, '0');
51
+ count.textContent = `${pad(index + 1)} / ${pad(features.length)}`;
52
+ name.textContent = feature.label;
53
+ details.replaceChildren();
54
+ const labels = [t('出现位置', 'Where'), t('解决问题', 'Problem'), t('操作与效果', 'What it does')];
55
+ previewContent(feature.id, locale).forEach((value, i) => {
56
+ const dt = document.createElement('dt'); dt.textContent = labels[i];
57
+ const dd = document.createElement('dd'); dd.textContent = value;
58
+ details.append(dt, dd);
59
+ });
60
+ }
61
+ function renderProgress() {
62
+ if (progress.children.length !== features.length) {
63
+ progress.replaceChildren(...features.map(() => {
64
+ const segment = document.createElement('span'); segment.className = 'showcase-segment';
65
+ segment.append(Object.assign(document.createElement('span'), {className: 'showcase-fill'}));
66
+ return segment;
67
+ }));
68
+ }
69
+ [...progress.children].forEach((segment, i) => {
70
+ segment.classList.toggle('is-done', i < index);
71
+ segment.classList.toggle('is-current', i === index);
72
+ if (i !== index) segment.firstChild.style.transform = i < index ? 'scaleX(1)' : 'scaleX(0)';
73
+ });
74
+ }
75
+ // 当前段进度跟随视频时间;没有视频时停在起点。
76
+ function tick() {
77
+ cancelAnimationFrame(frame);
78
+ const fill = progress.children[index]?.firstChild;
79
+ if (fill && video.duration) fill.style.transform = `scaleX(${Math.min(1, video.currentTime / video.duration)})`;
80
+ if (!video.paused) frame = requestAnimationFrame(tick);
81
+ }
82
+
83
+ function releaseVideo() {
84
+ cancelAnimationFrame(frame); clearTimeout(advanceTimer);
85
+ video.pause(); video.removeAttribute('src'); video.load(); video.hidden = true;
86
+ }
87
+ function sync() {
88
+ clearTimeout(advanceTimer);
89
+ if (!video.getAttribute('src')) {
90
+ // 没有视频的功能只展示封面或说明,自动轮播时停留后前进。
91
+ if (shouldPlay() && features.length > 1 && media.dataset.state !== 'loading') advanceTimer = setTimeout(() => step(1), 6000);
92
+ return;
93
+ }
94
+ if (shouldPlay()) {
95
+ const ticket = generation;
96
+ video.play().catch(error => {
97
+ // 切换素材或暂停造成的中断,不代表用户选择暂停或宿主拒绝播放。
98
+ if (ticket !== generation || error.name !== 'NotAllowedError' || !shouldPlay()) return;
99
+ userPaused = true; renderControls();
100
+ });
101
+ } else video.pause();
102
+ }
103
+ async function select(nextIndex) {
104
+ if (!features.length) return;
105
+ index = (nextIndex + features.length) % features.length;
106
+ const ticket = ++generation;
107
+ mediaAbort?.abort(); mediaAbort = new AbortController();
108
+ const signal = mediaAbort.signal;
109
+ releaseVideo(); delete video.dataset.ready;
110
+ renderInfo(); renderProgress();
111
+ const feature = current();
112
+ media.dataset.state = 'loading'; status.textContent = t('正在加载演示…', 'Loading demo…');
113
+ poster.hidden = true; poster.removeAttribute('src');
114
+ const resources = (await loadManifest())?.[feature.id];
115
+ if (ticket !== generation) return;
116
+ if (!resources) { media.dataset.state = 'empty'; status.textContent = t('演示视频准备中', 'Demo video coming soon'); sync(); return; }
117
+ const [posterUrl, videoUrl] = await Promise.all(['poster', 'video'].map(kind => resources[kind] ? resourceUrl(feature.id, kind, resources[kind], signal).catch(() => null) : null));
118
+ if (ticket !== generation) return;
119
+ if (posterUrl) { poster.src = posterUrl; poster.hidden = false; }
120
+ media.dataset.state = posterUrl ? 'poster' : 'empty';
121
+ status.textContent = posterUrl ? '' : t('演示暂不可用', 'Demo unavailable');
122
+ if (videoUrl) {
123
+ video.src = videoUrl; video.hidden = false;
124
+ video.addEventListener('loadeddata', () => { if (ticket === generation) { video.dataset.ready = '1'; media.dataset.state = 'video'; poster.hidden = true; } }, {once: true});
125
+ }
126
+ sync();
127
+ }
128
+ const step = delta => select(index + delta);
129
+
130
+ video.addEventListener('play', tick);
131
+ video.addEventListener('ended', () => {
132
+ tick();
133
+ if (shouldPlay()) advanceTimer = setTimeout(() => step(1), 600);
134
+ });
135
+ video.addEventListener('error', () => {
136
+ if (!video.getAttribute('src')) return;
137
+ releaseVideo(); poster.hidden = !poster.getAttribute('src');
138
+ media.dataset.state = poster.hidden ? 'empty' : 'poster';
139
+ status.textContent = poster.hidden ? t('演示暂不可用', 'Demo unavailable') : '';
140
+ sync();
141
+ });
142
+ prevButton.addEventListener('click', () => step(-1));
143
+ nextButton.addEventListener('click', () => step(1));
144
+ toggleButton.addEventListener('click', () => {
145
+ userPaused = !userPaused; renderControls();
146
+ // 播完停在最后一帧时,继续播放从下一个功能开始。
147
+ if (!userPaused && video.ended) step(1); else sync();
148
+ });
149
+ new IntersectionObserver(([entry]) => { inView = entry.isIntersecting && entry.intersectionRatio >= 0.35; sync(); }, {threshold: 0.35}).observe(media);
150
+ document.addEventListener('visibilitychange', () => { pageVisible = !document.hidden; sync(); });
151
+ window.addEventListener('pagehide', () => { mediaAbort?.abort(); releaseVideo(); });
152
+
153
+ function attach(card) {
154
+ const entry = document.createElement('button');
155
+ entry.type = 'button'; entry.className = 'showcase-entry';
156
+ entry.setAttribute('aria-controls', root.id);
157
+ entry.addEventListener('click', () => {
158
+ const target = features.findIndex(feature => feature.id === card.dataset.featureId);
159
+ if (target < 0) return;
160
+ if (target !== index) void select(target);
161
+ root.scrollIntoView({block: 'start', behavior: reduced.matches ? 'auto' : 'smooth'});
162
+ });
163
+ card.append(entry);
164
+ cards.set(card.dataset.featureId, card);
165
+ }
166
+ function detach(card) { cards.delete(card.dataset.featureId); }
167
+ function labelEntries() {
168
+ for (const card of cards.values()) card.querySelector('.showcase-entry')?.setAttribute('aria-label', `${t('在演示中查看', 'Show in demo')} · ${card.querySelector('.plugin-name').textContent || card.dataset.featureId}`);
169
+ }
170
+ function setFeatures(next) {
171
+ const id = current()?.id;
172
+ features = next.map(({id, label}) => ({id, label}));
173
+ root.hidden = features.length === 0;
174
+ renderControls(); labelEntries();
175
+ const kept = features.findIndex(feature => feature.id === id);
176
+ if (kept >= 0) { index = kept; renderInfo(); renderProgress(); }
177
+ else void select(0);
178
+ }
179
+ function setLocale(next) {
180
+ locale = next; zh = locale === 'zh-CN';
181
+ renderControls(); renderInfo(); labelEntries();
182
+ }
183
+ renderControls();
184
+ return {attach, detach, setFeatures, setLocale};
185
+ }
@@ -0,0 +1,14 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <rect x="14" y="4" width="4" height="16" rx="1" />
13
+ <rect x="6" y="4" width="4" height="16" rx="1" />
14
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <polygon points="6 3 20 12 6 21 6 3" />
13
+ </svg>
@@ -1 +1,67 @@
1
- {}
1
+ {
2
+ "composer-expand": {
3
+ "video": "composer-expand.mp4",
4
+ "poster": "composer-expand.jpg",
5
+ "version": "26.1103.60933"
6
+ },
7
+ "file-navigation": {
8
+ "video": "file-navigation.mp4",
9
+ "poster": "file-navigation.jpg",
10
+ "version": "26.1103.60933"
11
+ },
12
+ "files-auto-refresh": {
13
+ "video": "files-auto-refresh.mp4",
14
+ "poster": "files-auto-refresh.jpg",
15
+ "version": "26.1103.60933"
16
+ },
17
+ "images": {
18
+ "video": "images.mp4",
19
+ "poster": "images.jpg",
20
+ "version": "26.1103.60933"
21
+ },
22
+ "markdown-reading": {
23
+ "video": "markdown-reading.mp4",
24
+ "poster": "markdown-reading.jpg",
25
+ "version": "26.1103.60933"
26
+ },
27
+ "mermaid-diagrams": {
28
+ "video": "mermaid-diagrams.mp4",
29
+ "poster": "mermaid-diagrams.jpg",
30
+ "version": "26.1103.60933"
31
+ },
32
+ "model-slider": {
33
+ "video": "model-slider.mp4",
34
+ "poster": "model-slider.jpg",
35
+ "version": "26.1103.60933"
36
+ },
37
+ "notes": {
38
+ "video": "notes.mp4",
39
+ "poster": "notes.jpg",
40
+ "version": "26.1103.60933"
41
+ },
42
+ "quick-chat": {
43
+ "video": "quick-chat.mp4",
44
+ "poster": "quick-chat.jpg",
45
+ "version": "26.1103.60933"
46
+ },
47
+ "session-status": {
48
+ "video": "session-status.mp4",
49
+ "poster": "session-status.jpg",
50
+ "version": "26.1103.60933"
51
+ },
52
+ "sidebar-focus": {
53
+ "video": "sidebar-focus.mp4",
54
+ "poster": "sidebar-focus.jpg",
55
+ "version": "26.1103.60933"
56
+ },
57
+ "sites": {
58
+ "video": "sites.mp4",
59
+ "poster": "sites.jpg",
60
+ "version": "26.1103.60933"
61
+ },
62
+ "usage-meter": {
63
+ "video": "usage-meter.mp4",
64
+ "poster": "usage-meter.jpg",
65
+ "version": "26.1103.60933"
66
+ }
67
+ }