dsh-plugin-image-workbench 1.1.5 → 1.1.7

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 (173) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +5 -3
  3. package/README.md +7 -3
  4. package/client.js +241 -241
  5. package/cordis.patch.yml +14 -14
  6. package/host.js +51 -0
  7. package/install.mjs +333 -387
  8. package/package.json +3 -2
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -154
  11. package/server/access.py +26 -26
  12. package/server/app.py +1371 -1329
  13. package/server/catalog.py +147 -147
  14. package/server/composer.py +391 -391
  15. package/server/drafts.py +158 -158
  16. package/server/history.py +575 -575
  17. package/server/imaging.py +172 -172
  18. package/server/pool.py +1335 -1335
  19. package/server/pool_admin.py +921 -921
  20. package/server/pool_service.py +1073 -1073
  21. package/server/requirements.txt +6 -6
  22. package/server/smoke_check.py +402 -402
  23. package/server/static/access.html +202 -202
  24. package/server/static/index.html +15 -0
  25. package/vendor/chatgpt2api/source/.env.example +82 -82
  26. package/vendor/chatgpt2api/source/.python-version +1 -1
  27. package/vendor/chatgpt2api/source/LICENSE +21 -21
  28. package/vendor/chatgpt2api/source/README.md +391 -391
  29. package/vendor/chatgpt2api/source/VERSION +1 -1
  30. package/vendor/chatgpt2api/source/api/__init__.py +2 -2
  31. package/vendor/chatgpt2api/source/api/accounts.py +537 -537
  32. package/vendor/chatgpt2api/source/api/ai.py +217 -217
  33. package/vendor/chatgpt2api/source/api/app.py +62 -62
  34. package/vendor/chatgpt2api/source/api/errors.py +46 -46
  35. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
  36. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
  37. package/vendor/chatgpt2api/source/api/support.py +134 -134
  38. package/vendor/chatgpt2api/source/api/system.py +365 -365
  39. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
  40. package/vendor/chatgpt2api/source/config.default.json +108 -108
  41. package/vendor/chatgpt2api/source/main.py +9 -9
  42. package/vendor/chatgpt2api/source/pyproject.toml +27 -27
  43. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
  44. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
  45. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
  46. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
  47. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
  48. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
  49. package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
  50. package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
  51. package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
  52. package/vendor/chatgpt2api/source/services/config.py +667 -667
  53. package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
  54. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
  55. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
  56. package/vendor/chatgpt2api/source/services/image_service.py +376 -376
  57. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
  58. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
  59. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
  60. package/vendor/chatgpt2api/source/services/log_service.py +337 -337
  61. package/vendor/chatgpt2api/source/services/model_service.py +182 -182
  62. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
  63. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
  64. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
  65. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
  66. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
  67. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
  68. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
  69. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
  70. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
  71. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
  72. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
  73. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
  74. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
  75. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
  76. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
  77. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
  78. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
  79. package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
  80. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
  81. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
  82. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
  83. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
  84. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
  85. package/vendor/chatgpt2api/source/utils/helper.py +469 -469
  86. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
  87. package/vendor/chatgpt2api/source/utils/log.py +110 -110
  88. package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
  89. package/vendor/chatgpt2api/source/utils/pow.py +207 -207
  90. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
  91. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
  92. package/vendor/chatgpt2api/source/uv.lock +904 -904
  93. package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
  94. package/vendor/chatgpt2api/source/web/components.json +24 -24
  95. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
  96. package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
  97. package/vendor/chatgpt2api/source/web/package.json +58 -58
  98. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
  99. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
  100. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
  101. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
  102. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
  103. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
  104. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
  105. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
  106. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
  107. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
  108. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
  109. package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
  110. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
  111. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
  112. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
  113. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
  114. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
  115. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
  116. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
  117. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
  118. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
  119. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
  120. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
  121. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
  122. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
  123. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
  124. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
  125. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
  126. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
  127. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
  128. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
  129. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
  130. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
  133. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
  134. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
  135. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
  136. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
  137. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
  138. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
  139. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
  140. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
  141. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
  142. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
  143. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
  144. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
  145. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
  146. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
  147. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
  148. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
  149. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
  150. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
  151. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
  152. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
  153. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
  154. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
  155. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
  156. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
  157. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
  158. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
  159. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
  160. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
  161. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
  162. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
  163. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
  164. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
  165. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
  166. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
  167. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
  168. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
  169. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
  170. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
  171. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
  172. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
  173. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 guohwa
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 guohwa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md CHANGED
@@ -79,8 +79,10 @@ Open "Plugins -> Add Plugin" and use any one of these:
79
79
  | Method | Enter | Notes |
80
80
  | --- | --- | --- |
81
81
  | npm package (preferred) | `dsh-plugin-image-workbench` | Served from a CDN, fast and interruption-free; the plugin manager probes npmjs and npmmirror in parallel and takes whichever answers first |
82
- | Gitee mirror | `https://gitee.com/guohwa/dsh-plugin-image-workbench` | Reachable directly from mainland China, shallow clone takes about 9 seconds |
83
- | GitHub upstream | `https://github.com/guohwa/dsh-plugin-image-workbench` | Source of truth; cloning often stalls on the 31 MB uv runtime |
82
+ | Gitee mirror | `https://gitee.com/guohwa/dsh-plugin-image-workbench.git` | Reachable directly from mainland China, shallow clone takes about 9 seconds |
83
+ | GitHub upstream | `https://github.com/guohwa/dsh-plugin-image-workbench.git` | Source of truth; cloning often stalls on the 31 MB uv runtime |
84
+
85
+ > Git URLs **must end with `.git`**. The plugin manager installs through pnpm; without the suffix pnpm treats the URL as a tarball link, fetches the HTML page and fails with `ERR_PNPM_FETCH_403` plus "No authorization header was set" — that is about URL form, not permissions.
84
86
 
85
87
  All three carry the same content: every push and tag on GitHub is mirrored to Gitee, and tags also trigger an npm release. The plugin ships a roughly 31 MB uv runtime, which is exactly the part that stalls or times out when cloning from GitHub — so prefer the package name or the Gitee URL in mainland China.
86
88
 
@@ -209,7 +211,7 @@ Principal backend routes are listed below. The complete definitions are in `serv
209
211
  | Symptom | Resolution |
210
212
  | --- | --- |
211
213
  | No sidebar entry | Bundles load only at DSH startup; quit DSH completely and start it again |
212
- | Plugin manager reports a GitHub connection timeout | Switch to the Gitee URL `https://gitee.com/guohwa/dsh-plugin-image-workbench`, reachable directly from mainland China |
214
+ | Plugin manager reports a GitHub connection timeout | Switch to the Gitee URL `https://gitee.com/guohwa/dsh-plugin-image-workbench.git` (**with `.git`**), reachable directly from mainland China |
213
215
  | Install fails with `ERR_PNPM_EPERM ... rename '...dsh-plugin-image-workbench_tmp_...'` | The previous version is still running and the backend process holds the plugin directory, so Windows refuses the rename. Quit DSH completely (tray menu -> Quit), end any leftover `python.exe` in Task Manager, delete `node_modules\dsh-plugin-image-workbench`, then install again. From 1.1.4 on, installs under `node_modules` relocate their runtime to `~/.dsh/image-workbench`, so this no longer happens |
214
216
  | Code stops taking effect after moving the installation directory | Re-run `node install.mjs`; the directory junction is rebuilt to the current path |
215
217
  | "Built-in pool not ready" | Start the pool under "Endpoint & Pool -> Local Pool"; allow time for first-run environment initialization |
package/README.md CHANGED
@@ -78,8 +78,10 @@ flowchart LR
78
78
  | 方式 | 填什么 | 说明 |
79
79
  | --- | --- | --- |
80
80
  | npm 包名(首选) | `dsh-plugin-image-workbench` | 走 CDN,装得快且不会中途断;插件管理器会同时探测 npmjs 与 npmmirror,取先响应的那个 |
81
- | Gitee 镜像 | `https://gitee.com/guohwa/dsh-plugin-image-workbench` | 国内直连,浅克隆实测 9 秒左右 |
82
- | GitHub 原仓库 | `https://github.com/guohwa/dsh-plugin-image-workbench` | 内容源头,国内常卡在 31 MB 的 uv 运行时传输上 |
81
+ | Gitee 镜像 | `https://gitee.com/guohwa/dsh-plugin-image-workbench.git` | 国内直连,浅克隆实测 9 秒左右 |
82
+ | GitHub 原仓库 | `https://github.com/guohwa/dsh-plugin-image-workbench.git` | 内容源头,国内常卡在 31 MB 的 uv 运行时传输上 |
83
+
84
+ > 填 Git 地址时**结尾必须带 `.git`**。插件管理器调用 pnpm,不带后缀时 pnpm 会把它当作 tarball 链接去抓页面,报 `ERR_PNPM_FETCH_403` 并附一句「No authorization header was set」——那跟权限无关,只是地址形式不对。
83
85
 
84
86
  三处是同源内容:GitHub 上每次 push 和打 tag 会自动镜像到 Gitee,npm 也由 tag 触发发布。插件里带一个约 31 MB 的 uv 运行时,这正是从 GitHub 克隆时最容易超时的那一块,所以国内优先用包名或 Gitee 地址。
85
87
 
@@ -113,6 +115,8 @@ node install.mjs
113
115
 
114
116
  首次启动会自动准备后端与账号池的运行环境,通常 1 分钟内完成,无需手动操作;环境就绪后账号池会自动拉起,也可在「接口与号池 → 本地号池」手动启动。
115
117
 
118
+ 首次启动还会在后台补齐风格、版式与配色的预览样图(共 574 张,约 33 MB,从 CDN 拉取,失败自动换源)。下载完成前,风格库与配色库里显示的是渐变占位块,刷新页面即可看到真图;进度写在 `~/.dsh/image-workbench/samples.log`。样图不进 npm 包,也不参与升级同步,所以只在第一次装好后下一遍。
119
+
116
120
  插件以 `node_modules` 形式安装时,后端代码会同步到 `~/.dsh/image-workbench/server`,号池源码快照同步到同级的 `vendor/`,Python 环境、生成图片、账号与历史数据都留在这个目录里。插件包本身保持只读,升级插件不会冲掉环境和图片。用 `link:` 指向源码目录(开发场景)时不复制,直接用源码目录。
117
121
 
118
122
  ## 接口接入与账号管理
@@ -209,7 +213,7 @@ python -m venv .venv
209
213
  | 现象 | 处理方式 |
210
214
  | --- | --- |
211
215
  | 侧栏未出现入口 | bundle 仅在 DSH 启动时加载,完全退出 DSH 再启动 |
212
- | 插件管理器提示连接 GitHub 超时 | 换成 Gitee 地址 `https://gitee.com/guohwa/dsh-plugin-image-workbench`,国内直连 |
216
+ | 插件管理器提示连接 GitHub 超时 | 换成 Gitee 地址 `https://gitee.com/guohwa/dsh-plugin-image-workbench.git`(**带 `.git`**),国内直连 |
213
217
  | 安装报 `ERR_PNPM_EPERM ... rename '...dsh-plugin-image-workbench_tmp_...'` | 上一个版本还在运行,后端进程把插件目录锁住了,Windows 不允许重命名。完全退出 DSH(托盘菜单 → 退出),在任务管理器结束残留的 `python.exe`,删掉 `node_modules\dsh-plugin-image-workbench` 后重装。1.1.4 起装在 `node_modules` 里时运行时会自动搬到 `~/.dsh/image-workbench`,不再出现 |
214
218
  | 移动过插件目录后代码不再生效 | 重跑 `node install.mjs`,安装器会把目录联接重建到当前路径 |
215
219
  | 提示「工作台后端未运行」 | 面板会直接显示原因:多为本机没有 Python 3.10+ 且自动下载失败。装一个 Python 3.13 后点「立即启动并重试」;有代理时插件会自动识别并使用 |
package/client.js CHANGED
@@ -1,241 +1,241 @@
1
- /**
2
- * DSH Client 插件:把 127.0.0.1:8092 的生图工作台嵌进侧栏面板。
3
- * 页面本体是 image-local/static/index.html,浏览器直连与插件内看到的是同一个工作台。
4
- * 号池是工作台自带的进程,所以侧栏只注册这一个面板,不再有独立号池入口。
5
- */
6
- window.__ModuleLoader__.load({
7
- id: 'dsh-plugin-image-workbench',
8
- factory(require) {
9
- const { createElement: h, useState, useEffect, useCallback } = require('react');
10
- const PANEL = 'image-workbench';
11
- const STATUS_API = '/api/image-workbench.status';
12
- const CSS = `
13
- .iw-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%;background:#e7eaee;color:#1c1e21;font:14px/1.5 "PingFang SC","Microsoft YaHei",system-ui,sans-serif;box-sizing:border-box}
14
- .iw-root *{box-sizing:border-box}
15
- .iw-bar{display:flex;align-items:center;justify-content:flex-start;gap:12px;flex:none;padding:10px 18px;background:#fff;border-bottom:1px solid #dee3e9;flex-wrap:wrap}
16
- .iw-title{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:#0a1317;flex-wrap:wrap}
17
- .iw-title small{font-weight:400;color:#8595a4;font-size:12px}
18
- .iw-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}
19
- .iw-btn{border:1px solid #d7dde5;background:#fff;color:#1c1e21;border-radius:7px;padding:5px 11px;font-size:12px;cursor:pointer;text-decoration:none;font-family:inherit;line-height:1.5}
20
- .iw-btn:hover{border-color:#0064e0;color:#0064e0}
21
- .iw-btn.primary{background:#0064e0;border-color:#0064e0;color:#fff}
22
- .iw-btn.primary:hover{background:#0053bb;color:#fff}
23
- .iw-btn:disabled{opacity:.5;cursor:default}
24
- .iw-chip{display:inline-flex;align-items:center;gap:6px;border-radius:99px;padding:3px 10px;font-size:11px;background:#eef1f5;color:#5d6c7b}
25
- .iw-chip.on{background:#e6f6ec;color:#1d7a3c}
26
- .iw-chip.off{background:#fff0ed;color:#b3402f}
27
- .iw-chip.warn{background:#fff5e2;color:#9a6700}
28
- .iw-dot{width:7px;height:7px;border-radius:50%;background:currentColor}
29
- .iw-frame{flex:1;min-height:0;width:100%;border:0;background:#e7eaee;display:block}
30
- .iw-empty{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px;text-align:center;color:#5d6c7b}
31
- .iw-empty h2{margin:0;font-size:18px;color:#1c1e21}
32
- .iw-empty p{margin:0;max-width:560px;font-size:13px;line-height:1.8}
33
- .iw-empty code{background:#dfe4ea;padding:2px 6px;border-radius:4px;font-size:12px}
34
- .iw-error{margin:8px 18px 0;padding:9px 12px;border-radius:7px;background:#fff3ef;border:1px solid #f3cfc4;color:#a13c28;font-size:12px;flex:none}
35
- .iw-empty .iw-why{margin:0;max-width:560px;padding:9px 12px;border-radius:7px;background:#fff3ef;
36
- border:1px solid #f3cfc4;color:#a13c28;font-size:12.5px;line-height:1.7;text-align:left}
37
- .iw-empty .iw-why .iw-tail{margin:6px 0 0;padding:8px;max-height:150px;overflow:auto;background:#fff;
38
- border:1px solid #f0d8d1;border-radius:5px;color:#6b3a30;font-size:11.5px;line-height:1.55;
39
- white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,Consolas,monospace}
40
- .iw-note{flex:0 1 auto;min-width:0;max-width:min(420px,38vw);padding:4px 10px;border-radius:7px;
41
- font-size:12px;line-height:1.55;background:#eef4ff;border:1px solid #cddffb;color:#1b4d8f;
42
- white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
43
- .iw-note.bad{background:#fff3ef;border-color:#f3cfc4;color:#a13c28}
44
- /* 窄一点先把次要的自述说明收起来,优先保证提示条和按钮不换行 */
45
- @media(max-width:1180px){.iw-title small{display:none}}
46
- @media(max-width:820px){.iw-bar{padding:8px 12px}}
47
- `;
48
-
49
- function Button(label, onClick, opts = {}) {
50
- return h('button', { type: 'button', className: 'iw-btn' + (opts.primary ? ' primary' : ''), onClick, disabled: !!opts.disabled, ...(opts.extra || {}) }, label);
51
- }
52
-
53
- function Workbench() {
54
- const [status, setStatus] = useState(null);
55
- const [error, setError] = useState('');
56
- const [notice, setNotice] = useState(null);
57
- const [busy, setBusy] = useState('');
58
- const [nonce, setNonce] = useState(0);
59
-
60
- const call = useCallback(async (action) => {
61
- setBusy(action || '刷新');
62
- setError('');
63
- try {
64
- const response = action
65
- ? await fetch(STATUS_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) })
66
- : await fetch(STATUS_API, { cache: 'no-store' });
67
- if (!response.ok) throw new Error('宿主接口返回 HTTP ' + response.status);
68
- const data = await response.json();
69
- setStatus(data);
70
- if (data.running && action === 'start') setNonce(n => n + 1);
71
- if (!data.running && action === 'start') setError('后端仍未就绪,可查看日志:' + (data.log || '~/.dsh/image-workbench/backend.log'));
72
-
73
- // 每个动作都要能看见结果:以前点了像没反应,就是因为什么都不回显
74
- // 文案压短,完整信息放 hint(渲染成 title),否则窄一点就把按钮挤到第二行
75
- if (action === 'open') {
76
- const done = data.lastAction || {};
77
- setNotice(done.ok
78
- ? { ok: true, text: '已用系统浏览器打开', hint: '已在系统默认浏览器打开 ' + (data.url || '') }
79
- : { ok: false, text: '打开浏览器失败', hint: '原因:' + (done.error || '未知') + '。也可以手动访问 ' + (data.url || '') });
80
- } else if (action === 'refresh') {
81
- setNotice(data.running
82
- ? { ok: true, text: '后端正常 · 端口 ' + (data.port || 8092), hint: '已检查:工作台后端在跑,端口正常响应' }
83
- : { ok: false, text: '端口 ' + (data.port || 8092) + ' 无响应', hint: '已检查:后端没在跑,点「启动后端」' });
84
- }
85
- } catch (e) {
86
- setError(e.message || '状态查询失败');
87
- } finally {
88
- setBusy('');
89
- }
90
- }, []);
91
-
92
- // 提示条自动消失,避免一直占着页面顶部
93
- useEffect(() => {
94
- if (!notice) return undefined;
95
- const timer = setTimeout(() => setNotice(null), notice.ok ? 6000 : 10000);
96
- return () => clearTimeout(timer);
97
- }, [notice]);
98
-
99
- const copyUrl = useCallback(async () => {
100
- const url = status?.url || 'http://127.0.0.1:8092/';
101
- try {
102
- await navigator.clipboard.writeText(url);
103
- setNotice({ ok: true, text: '地址已复制', hint: '已复制到剪贴板,可直接粘到浏览器打开:' + url });
104
- } catch {
105
- setNotice({ ok: false, text: '复制失败', hint: '复制被环境拦下了,手动复制这个地址:' + url });
106
- }
107
- }, [status]);
108
-
109
- // 不做定时轮询:iframe 一旦重载,正在编辑的工程会被打断。
110
- useEffect(() => { call(); }, [call]);
111
-
112
- // 后端没起来时(首次装环境、刚被拉起)轻量轮询,起来就自动切成 iframe。
113
- // 只在没有 iframe 的时候轮询,所以不会打断正在编辑的内容;最多盯 2 分钟。
114
- useEffect(() => {
115
- if (Boolean(status?.running)) return undefined;
116
- let tick = 0;
117
- const timer = setInterval(() => {
118
- tick += 1;
119
- if (tick > 40) { clearInterval(timer); return; }
120
- void call();
121
- }, 3000);
122
- return () => clearInterval(timer);
123
- }, [status?.running, call]);
124
-
125
- // 后端起不来时,把真实原因复制出来,不用让用户去翻日志文件
126
- const copyDiag = useCallback(async () => {
127
- const d = {
128
- time: new Date().toISOString(),
129
- running: status?.running, port: status?.port,
130
- serverDir: status?.serverDir, python: status?.python,
131
- venv: status?.venv, lastError: status?.lastError,
132
- watchdog: status?.watchdog, log: status?.log,
133
- };
134
- try {
135
- await navigator.clipboard.writeText(JSON.stringify(d, null, 2));
136
- setNotice({ ok: true, text: '诊断信息已复制', hint: '已复制到剪贴板,可直接发给作者' });
137
- } catch {
138
- setNotice({ ok: false, text: '复制失败', hint: JSON.stringify(d) });
139
- }
140
- }, [status]);
141
-
142
- const running = Boolean(status?.running);
143
- const appUrl = status?.url || 'http://127.0.0.1:8092/';
144
- const guard = status?.watchdog;
145
- const guardText = !status ? '检查中'
146
- : !guard ? '守护未加载 · 需重启 DSH'
147
- : !guard.enabled ? '守护已关闭'
148
- : '守护已开' + (guard.restarts ? ' · 已自动拉起 ' + guard.restarts + ' 次' : '')
149
- + (guard.consecutiveFailures ? ' · 拉起失败 ' + guard.consecutiveFailures + ' 次' : '');
150
- const guardClass = !status ? '' : !guard ? 'warn' : guard.enabled && !guard.consecutiveFailures ? 'on' : 'off';
151
- // 号池是工作台自带的第二个进程,状态由后端上报;不再有独立号池插件
152
- const bp = status?.pool;
153
- const poolText = !status ? '' : !bp ? '内置号池读取中'
154
- : bp.online ? `内置号池 ${bp.active}/${bp.total} · 剩 ${bp.quota} 次`
155
- : '内置号池未就绪';
156
- const poolClass = !bp ? 'warn' : bp.online ? 'on' : 'off';
157
-
158
- return h('div', { className: 'iw-root' },
159
- h('style', null, CSS),
160
- h('header', { className: 'iw-bar' },
161
- h('div', { className: 'iw-title' },
162
- 'Bluxe手绘工坊 · 生图工作台',
163
- h('small', null, '号池运行时随插件自带 · 可独立运行'),
164
- h('span', { className: 'iw-chip ' + guardClass, title: '后端端口掉线时由 DSH 宿主自动拉起;修改插件后需重启 DSH 才会加载新代码' },
165
- h('span', { className: 'iw-dot' }), guardText),
166
- h('span', { className: 'iw-chip ' + poolClass, title: '号池服务端代码与 uv 随插件自带,全新机器首次启动自动初始化虚拟环境;由工作台后端自己启动与守护,不需要任何第三方号池插件' },
167
- h('span', { className: 'iw-dot' }), poolText)),
168
- // 提示放在标题右侧、按钮左侧这条空档里,不额外占一行高度
169
- notice && h('div', { className: 'iw-note' + (notice.ok ? '' : ' bad'), role: 'status', title: notice.hint || notice.text }, notice.text),
170
- h('div', { className: 'iw-actions' },
171
- Button(busy === 'refresh' ? '检查中…' : '刷新状态', () => call('refresh'), { disabled: !!busy }),
172
- // 只有在没跑的时候才给「启动后端」,跑着的时候这个按钮点了也没意义
173
- !running && Button(busy === 'start' ? '启动中…' : '启动后端', () => call('start'), { disabled: !!busy, primary: true }),
174
- Button(busy === 'open' ? '正在打开…' : '用系统浏览器打开', () => call('open'), { disabled: !!busy }),
175
- // 原来的「新窗口打开」是 <a target="_blank">,DSH 里既开不了新窗口又会静默失败,换成复制地址
176
- Button('复制地址', copyUrl))),
177
- error && h('div', { className: 'iw-error', role: 'alert' }, error),
178
- running
179
- ? h('iframe', {
180
- key: 'frame-' + nonce,
181
- className: 'iw-frame',
182
- src: appUrl + (appUrl.includes('?') ? '&' : '?') + 'dsh=' + nonce,
183
- title: '生图工作台(图文 / 摄影)',
184
- allow: 'clipboard-read; clipboard-write; fullscreen',
185
- })
186
- : h('div', { className: 'iw-empty' },
187
- h('h2', null, status ? '工作台后端未运行' : '正在连接工作台'),
188
- h('p', null,
189
- '插件会自动拉起本机 image-local 工作台(uvicorn,端口 ',
190
- h('code', null, String(status?.port || 8092)),
191
- ')。若这里一直是空白,请确认 DSH 已重启加载插件,或点「用系统浏览器打开」直接访问。'),
192
- status?.serverDir && h('p', null, '后端目录:', h('code', null, status.serverDir)),
193
- // 初始化中 / 失败原因:以前这里只说「未运行」,用户完全看不出卡在哪一步
194
- !status ? null
195
- : (status.venv?.message || status.lastError)
196
- ? h('div', { className: 'iw-why' },
197
- h('div', null, '原因:' + (status.venv?.message || status.lastError)),
198
- status.backendTail
199
- ? h('pre', { className: 'iw-tail' }, status.backendTail)
200
- : null)
201
- : (status.venv?.tried && !status.venv?.ready)
202
- ? h('p', null, '正在初始化运行环境(首次启动约 1 分钟),装好后这里会自动刷新。')
203
- : null,
204
- h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
205
- Button(busy ? '正在处理' : '立即启动并重试', () => call('start'), { primary: true, disabled: !!busy }),
206
- Button('复制诊断信息', copyDiag),
207
- status?.venv?.log && h('span', {
208
- style: { fontSize: '12px', color: '#6b7280', alignSelf: 'center' },
209
- title: status.venv.log,
210
- }, '日志:' + status.venv.log)),
211
- ),
212
- );
213
- }
214
-
215
- function HeaderIcon({ size = 20 }) {
216
- return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, 'aria-hidden': true },
217
- h('rect', { x: 3, y: 3, width: 18, height: 18, rx: 2 }),
218
- h('circle', { cx: 9, cy: 9, r: 2.2 }),
219
- h('path', { d: 'M21 15l-5-5L5 21' }));
220
- }
221
-
222
- function apply(ctx) {
223
- const slots = ctx?.slots;
224
- if (!slots?.inject) return;
225
- const register = () => {
226
- const disposers = [];
227
- disposers.push(slots.inject('sidebar.panellist', () => slots.register(
228
- { name: 'sidebar.panellist', id: PANEL, order: 73, label: () => 'Bluxe手绘工坊 · 生图工作台' },
229
- HeaderIcon)));
230
- disposers.push(slots.inject('main', () => slots.register(
231
- { name: 'main', key: PANEL },
232
- Workbench)));
233
- return () => { for (const fn of disposers.reverse()) if (typeof fn === 'function') fn(); };
234
- };
235
- if (typeof ctx.effect === 'function') ctx.effect(register, 'image-workbench: panel registrations');
236
- else register();
237
- }
238
-
239
- return { name: PANEL, inject: ['slots'], apply };
240
- },
241
- });
1
+ /**
2
+ * DSH Client 插件:把 127.0.0.1:8092 的生图工作台嵌进侧栏面板。
3
+ * 页面本体是 image-local/static/index.html,浏览器直连与插件内看到的是同一个工作台。
4
+ * 号池是工作台自带的进程,所以侧栏只注册这一个面板,不再有独立号池入口。
5
+ */
6
+ window.__ModuleLoader__.load({
7
+ id: 'dsh-plugin-image-workbench',
8
+ factory(require) {
9
+ const { createElement: h, useState, useEffect, useCallback } = require('react');
10
+ const PANEL = 'image-workbench';
11
+ const STATUS_API = '/api/image-workbench.status';
12
+ const CSS = `
13
+ .iw-root{display:flex;flex-direction:column;height:100%;min-height:0;width:100%;background:#e7eaee;color:#1c1e21;font:14px/1.5 "PingFang SC","Microsoft YaHei",system-ui,sans-serif;box-sizing:border-box}
14
+ .iw-root *{box-sizing:border-box}
15
+ .iw-bar{display:flex;align-items:center;justify-content:flex-start;gap:12px;flex:none;padding:10px 18px;background:#fff;border-bottom:1px solid #dee3e9;flex-wrap:wrap}
16
+ .iw-title{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:#0a1317;flex-wrap:wrap}
17
+ .iw-title small{font-weight:400;color:#8595a4;font-size:12px}
18
+ .iw-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}
19
+ .iw-btn{border:1px solid #d7dde5;background:#fff;color:#1c1e21;border-radius:7px;padding:5px 11px;font-size:12px;cursor:pointer;text-decoration:none;font-family:inherit;line-height:1.5}
20
+ .iw-btn:hover{border-color:#0064e0;color:#0064e0}
21
+ .iw-btn.primary{background:#0064e0;border-color:#0064e0;color:#fff}
22
+ .iw-btn.primary:hover{background:#0053bb;color:#fff}
23
+ .iw-btn:disabled{opacity:.5;cursor:default}
24
+ .iw-chip{display:inline-flex;align-items:center;gap:6px;border-radius:99px;padding:3px 10px;font-size:11px;background:#eef1f5;color:#5d6c7b}
25
+ .iw-chip.on{background:#e6f6ec;color:#1d7a3c}
26
+ .iw-chip.off{background:#fff0ed;color:#b3402f}
27
+ .iw-chip.warn{background:#fff5e2;color:#9a6700}
28
+ .iw-dot{width:7px;height:7px;border-radius:50%;background:currentColor}
29
+ .iw-frame{flex:1;min-height:0;width:100%;border:0;background:#e7eaee;display:block}
30
+ .iw-empty{flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px;text-align:center;color:#5d6c7b}
31
+ .iw-empty h2{margin:0;font-size:18px;color:#1c1e21}
32
+ .iw-empty p{margin:0;max-width:560px;font-size:13px;line-height:1.8}
33
+ .iw-empty code{background:#dfe4ea;padding:2px 6px;border-radius:4px;font-size:12px}
34
+ .iw-error{margin:8px 18px 0;padding:9px 12px;border-radius:7px;background:#fff3ef;border:1px solid #f3cfc4;color:#a13c28;font-size:12px;flex:none}
35
+ .iw-empty .iw-why{margin:0;max-width:560px;padding:9px 12px;border-radius:7px;background:#fff3ef;
36
+ border:1px solid #f3cfc4;color:#a13c28;font-size:12.5px;line-height:1.7;text-align:left}
37
+ .iw-empty .iw-why .iw-tail{margin:6px 0 0;padding:8px;max-height:150px;overflow:auto;background:#fff;
38
+ border:1px solid #f0d8d1;border-radius:5px;color:#6b3a30;font-size:11.5px;line-height:1.55;
39
+ white-space:pre-wrap;word-break:break-all;font-family:ui-monospace,Consolas,monospace}
40
+ .iw-note{flex:0 1 auto;min-width:0;max-width:min(420px,38vw);padding:4px 10px;border-radius:7px;
41
+ font-size:12px;line-height:1.55;background:#eef4ff;border:1px solid #cddffb;color:#1b4d8f;
42
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
43
+ .iw-note.bad{background:#fff3ef;border-color:#f3cfc4;color:#a13c28}
44
+ /* 窄一点先把次要的自述说明收起来,优先保证提示条和按钮不换行 */
45
+ @media(max-width:1180px){.iw-title small{display:none}}
46
+ @media(max-width:820px){.iw-bar{padding:8px 12px}}
47
+ `;
48
+
49
+ function Button(label, onClick, opts = {}) {
50
+ return h('button', { type: 'button', className: 'iw-btn' + (opts.primary ? ' primary' : ''), onClick, disabled: !!opts.disabled, ...(opts.extra || {}) }, label);
51
+ }
52
+
53
+ function Workbench() {
54
+ const [status, setStatus] = useState(null);
55
+ const [error, setError] = useState('');
56
+ const [notice, setNotice] = useState(null);
57
+ const [busy, setBusy] = useState('');
58
+ const [nonce, setNonce] = useState(0);
59
+
60
+ const call = useCallback(async (action) => {
61
+ setBusy(action || '刷新');
62
+ setError('');
63
+ try {
64
+ const response = action
65
+ ? await fetch(STATUS_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) })
66
+ : await fetch(STATUS_API, { cache: 'no-store' });
67
+ if (!response.ok) throw new Error('宿主接口返回 HTTP ' + response.status);
68
+ const data = await response.json();
69
+ setStatus(data);
70
+ if (data.running && action === 'start') setNonce(n => n + 1);
71
+ if (!data.running && action === 'start') setError('后端仍未就绪,可查看日志:' + (data.log || '~/.dsh/image-workbench/backend.log'));
72
+
73
+ // 每个动作都要能看见结果:以前点了像没反应,就是因为什么都不回显
74
+ // 文案压短,完整信息放 hint(渲染成 title),否则窄一点就把按钮挤到第二行
75
+ if (action === 'open') {
76
+ const done = data.lastAction || {};
77
+ setNotice(done.ok
78
+ ? { ok: true, text: '已用系统浏览器打开', hint: '已在系统默认浏览器打开 ' + (data.url || '') }
79
+ : { ok: false, text: '打开浏览器失败', hint: '原因:' + (done.error || '未知') + '。也可以手动访问 ' + (data.url || '') });
80
+ } else if (action === 'refresh') {
81
+ setNotice(data.running
82
+ ? { ok: true, text: '后端正常 · 端口 ' + (data.port || 8092), hint: '已检查:工作台后端在跑,端口正常响应' }
83
+ : { ok: false, text: '端口 ' + (data.port || 8092) + ' 无响应', hint: '已检查:后端没在跑,点「启动后端」' });
84
+ }
85
+ } catch (e) {
86
+ setError(e.message || '状态查询失败');
87
+ } finally {
88
+ setBusy('');
89
+ }
90
+ }, []);
91
+
92
+ // 提示条自动消失,避免一直占着页面顶部
93
+ useEffect(() => {
94
+ if (!notice) return undefined;
95
+ const timer = setTimeout(() => setNotice(null), notice.ok ? 6000 : 10000);
96
+ return () => clearTimeout(timer);
97
+ }, [notice]);
98
+
99
+ const copyUrl = useCallback(async () => {
100
+ const url = status?.url || 'http://127.0.0.1:8092/';
101
+ try {
102
+ await navigator.clipboard.writeText(url);
103
+ setNotice({ ok: true, text: '地址已复制', hint: '已复制到剪贴板,可直接粘到浏览器打开:' + url });
104
+ } catch {
105
+ setNotice({ ok: false, text: '复制失败', hint: '复制被环境拦下了,手动复制这个地址:' + url });
106
+ }
107
+ }, [status]);
108
+
109
+ // 不做定时轮询:iframe 一旦重载,正在编辑的工程会被打断。
110
+ useEffect(() => { call(); }, [call]);
111
+
112
+ // 后端没起来时(首次装环境、刚被拉起)轻量轮询,起来就自动切成 iframe。
113
+ // 只在没有 iframe 的时候轮询,所以不会打断正在编辑的内容;最多盯 2 分钟。
114
+ useEffect(() => {
115
+ if (Boolean(status?.running)) return undefined;
116
+ let tick = 0;
117
+ const timer = setInterval(() => {
118
+ tick += 1;
119
+ if (tick > 40) { clearInterval(timer); return; }
120
+ void call();
121
+ }, 3000);
122
+ return () => clearInterval(timer);
123
+ }, [status?.running, call]);
124
+
125
+ // 后端起不来时,把真实原因复制出来,不用让用户去翻日志文件
126
+ const copyDiag = useCallback(async () => {
127
+ const d = {
128
+ time: new Date().toISOString(),
129
+ running: status?.running, port: status?.port,
130
+ serverDir: status?.serverDir, python: status?.python,
131
+ venv: status?.venv, lastError: status?.lastError,
132
+ watchdog: status?.watchdog, log: status?.log,
133
+ };
134
+ try {
135
+ await navigator.clipboard.writeText(JSON.stringify(d, null, 2));
136
+ setNotice({ ok: true, text: '诊断信息已复制', hint: '已复制到剪贴板,可直接发给作者' });
137
+ } catch {
138
+ setNotice({ ok: false, text: '复制失败', hint: JSON.stringify(d) });
139
+ }
140
+ }, [status]);
141
+
142
+ const running = Boolean(status?.running);
143
+ const appUrl = status?.url || 'http://127.0.0.1:8092/';
144
+ const guard = status?.watchdog;
145
+ const guardText = !status ? '检查中'
146
+ : !guard ? '守护未加载 · 需重启 DSH'
147
+ : !guard.enabled ? '守护已关闭'
148
+ : '守护已开' + (guard.restarts ? ' · 已自动拉起 ' + guard.restarts + ' 次' : '')
149
+ + (guard.consecutiveFailures ? ' · 拉起失败 ' + guard.consecutiveFailures + ' 次' : '');
150
+ const guardClass = !status ? '' : !guard ? 'warn' : guard.enabled && !guard.consecutiveFailures ? 'on' : 'off';
151
+ // 号池是工作台自带的第二个进程,状态由后端上报;不再有独立号池插件
152
+ const bp = status?.pool;
153
+ const poolText = !status ? '' : !bp ? '内置号池读取中'
154
+ : bp.online ? `内置号池 ${bp.active}/${bp.total} · 剩 ${bp.quota} 次`
155
+ : '内置号池未就绪';
156
+ const poolClass = !bp ? 'warn' : bp.online ? 'on' : 'off';
157
+
158
+ return h('div', { className: 'iw-root' },
159
+ h('style', null, CSS),
160
+ h('header', { className: 'iw-bar' },
161
+ h('div', { className: 'iw-title' },
162
+ 'Bluxe手绘工坊 · 生图工作台',
163
+ h('small', null, '号池运行时随插件自带 · 可独立运行'),
164
+ h('span', { className: 'iw-chip ' + guardClass, title: '后端端口掉线时由 DSH 宿主自动拉起;修改插件后需重启 DSH 才会加载新代码' },
165
+ h('span', { className: 'iw-dot' }), guardText),
166
+ h('span', { className: 'iw-chip ' + poolClass, title: '号池服务端代码与 uv 随插件自带,全新机器首次启动自动初始化虚拟环境;由工作台后端自己启动与守护,不需要任何第三方号池插件' },
167
+ h('span', { className: 'iw-dot' }), poolText)),
168
+ // 提示放在标题右侧、按钮左侧这条空档里,不额外占一行高度
169
+ notice && h('div', { className: 'iw-note' + (notice.ok ? '' : ' bad'), role: 'status', title: notice.hint || notice.text }, notice.text),
170
+ h('div', { className: 'iw-actions' },
171
+ Button(busy === 'refresh' ? '检查中…' : '刷新状态', () => call('refresh'), { disabled: !!busy }),
172
+ // 只有在没跑的时候才给「启动后端」,跑着的时候这个按钮点了也没意义
173
+ !running && Button(busy === 'start' ? '启动中…' : '启动后端', () => call('start'), { disabled: !!busy, primary: true }),
174
+ Button(busy === 'open' ? '正在打开…' : '用系统浏览器打开', () => call('open'), { disabled: !!busy }),
175
+ // 原来的「新窗口打开」是 <a target="_blank">,DSH 里既开不了新窗口又会静默失败,换成复制地址
176
+ Button('复制地址', copyUrl))),
177
+ error && h('div', { className: 'iw-error', role: 'alert' }, error),
178
+ running
179
+ ? h('iframe', {
180
+ key: 'frame-' + nonce,
181
+ className: 'iw-frame',
182
+ src: appUrl + (appUrl.includes('?') ? '&' : '?') + 'dsh=' + nonce,
183
+ title: '生图工作台(图文 / 摄影)',
184
+ allow: 'clipboard-read; clipboard-write; fullscreen',
185
+ })
186
+ : h('div', { className: 'iw-empty' },
187
+ h('h2', null, status ? '工作台后端未运行' : '正在连接工作台'),
188
+ h('p', null,
189
+ '插件会自动拉起本机 image-local 工作台(uvicorn,端口 ',
190
+ h('code', null, String(status?.port || 8092)),
191
+ ')。若这里一直是空白,请确认 DSH 已重启加载插件,或点「用系统浏览器打开」直接访问。'),
192
+ status?.serverDir && h('p', null, '后端目录:', h('code', null, status.serverDir)),
193
+ // 初始化中 / 失败原因:以前这里只说「未运行」,用户完全看不出卡在哪一步
194
+ !status ? null
195
+ : (status.venv?.message || status.lastError)
196
+ ? h('div', { className: 'iw-why' },
197
+ h('div', null, '原因:' + (status.venv?.message || status.lastError)),
198
+ status.backendTail
199
+ ? h('pre', { className: 'iw-tail' }, status.backendTail)
200
+ : null)
201
+ : (status.venv?.tried && !status.venv?.ready)
202
+ ? h('p', null, '正在初始化运行环境(首次启动约 1 分钟),装好后这里会自动刷新。')
203
+ : null,
204
+ h('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
205
+ Button(busy ? '正在处理' : '立即启动并重试', () => call('start'), { primary: true, disabled: !!busy }),
206
+ Button('复制诊断信息', copyDiag),
207
+ status?.venv?.log && h('span', {
208
+ style: { fontSize: '12px', color: '#6b7280', alignSelf: 'center' },
209
+ title: status.venv.log,
210
+ }, '日志:' + status.venv.log)),
211
+ ),
212
+ );
213
+ }
214
+
215
+ function HeaderIcon({ size = 20 }) {
216
+ return h('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, 'aria-hidden': true },
217
+ h('rect', { x: 3, y: 3, width: 18, height: 18, rx: 2 }),
218
+ h('circle', { cx: 9, cy: 9, r: 2.2 }),
219
+ h('path', { d: 'M21 15l-5-5L5 21' }));
220
+ }
221
+
222
+ function apply(ctx) {
223
+ const slots = ctx?.slots;
224
+ if (!slots?.inject) return;
225
+ const register = () => {
226
+ const disposers = [];
227
+ disposers.push(slots.inject('sidebar.panellist', () => slots.register(
228
+ { name: 'sidebar.panellist', id: PANEL, order: 73, label: () => 'Bluxe手绘工坊 · 生图工作台' },
229
+ HeaderIcon)));
230
+ disposers.push(slots.inject('main', () => slots.register(
231
+ { name: 'main', key: PANEL },
232
+ Workbench)));
233
+ return () => { for (const fn of disposers.reverse()) if (typeof fn === 'function') fn(); };
234
+ };
235
+ if (typeof ctx.effect === 'function') ctx.effect(register, 'image-workbench: panel registrations');
236
+ else register();
237
+ }
238
+
239
+ return { name: PANEL, inject: ['slots'], apply };
240
+ },
241
+ });
package/cordis.patch.yml CHANGED
@@ -1,14 +1,14 @@
1
- # DSH profile patch: register the workbench as a loader entry.
2
- # All paths are resolved at runtime by host.js; no absolute paths are stored here.
3
- # The DSH host manages the workbench backend on port 8092 only.
4
- # The account-pool process on port 38651 is managed by server/pool_service.py.
5
- - insert:
6
- - id: image-workbench
7
- name: dsh-plugin-image-workbench
8
- config:
9
- port: 8092
10
- poolPort: 38651
11
- autoStart: true
12
- # 宿主侧守护:探测失败时自动重启后端,不重载面板。设为 false 并重启 DSH 可关闭。
13
- watchdog: true
14
- watchdogIntervalMs: 15000
1
+ # DSH profile patch: register the workbench as a loader entry.
2
+ # All paths are resolved at runtime by host.js; no absolute paths are stored here.
3
+ # The DSH host manages the workbench backend on port 8092 only.
4
+ # The account-pool process on port 38651 is managed by server/pool_service.py.
5
+ - insert:
6
+ - id: image-workbench
7
+ name: dsh-plugin-image-workbench
8
+ config:
9
+ port: 8092
10
+ poolPort: 38651
11
+ autoStart: true
12
+ # 宿主侧守护:探测失败时自动重启后端,不重载面板。设为 false 并重启 DSH 可关闭。
13
+ watchdog: true
14
+ watchdogIntervalMs: 15000