dsh-plugin-image-workbench 0.0.0-stage → 1.1.6

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 (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +258 -0
  3. package/README.md +260 -2
  4. package/client.js +241 -0
  5. package/cordis.patch.yml +14 -0
  6. package/host.js +692 -0
  7. package/install.mjs +333 -0
  8. package/package.json +58 -4
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -0
  11. package/server/access.py +26 -0
  12. package/server/app.py +1329 -0
  13. package/server/catalog.json +1 -0
  14. package/server/catalog.py +147 -0
  15. package/server/composer.py +391 -0
  16. package/server/drafts.py +158 -0
  17. package/server/history.py +575 -0
  18. package/server/imaging.py +172 -0
  19. package/server/pool.py +1335 -0
  20. package/server/pool_admin.py +921 -0
  21. package/server/pool_service.py +1073 -0
  22. package/server/requirements.txt +6 -0
  23. package/server/smoke_check.py +402 -0
  24. package/server/static/access.html +202 -0
  25. package/server/static/index.html +4725 -0
  26. package/vendor/chatgpt2api/README.md +13 -0
  27. package/vendor/chatgpt2api/SOURCE.json +11 -0
  28. package/vendor/chatgpt2api/runtime/uv.exe +0 -0
  29. package/vendor/chatgpt2api/source/.env.example +82 -0
  30. package/vendor/chatgpt2api/source/.python-version +1 -0
  31. package/vendor/chatgpt2api/source/LICENSE +21 -0
  32. package/vendor/chatgpt2api/source/README.md +391 -0
  33. package/vendor/chatgpt2api/source/VERSION +1 -0
  34. package/vendor/chatgpt2api/source/api/__init__.py +2 -0
  35. package/vendor/chatgpt2api/source/api/accounts.py +537 -0
  36. package/vendor/chatgpt2api/source/api/ai.py +217 -0
  37. package/vendor/chatgpt2api/source/api/app.py +62 -0
  38. package/vendor/chatgpt2api/source/api/errors.py +46 -0
  39. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
  40. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
  41. package/vendor/chatgpt2api/source/api/support.py +134 -0
  42. package/vendor/chatgpt2api/source/api/system.py +365 -0
  43. package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
  44. package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
  45. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
  46. package/vendor/chatgpt2api/source/config.default.json +108 -0
  47. package/vendor/chatgpt2api/source/main.py +9 -0
  48. package/vendor/chatgpt2api/source/pyproject.toml +27 -0
  49. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
  50. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
  51. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
  52. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
  53. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
  54. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
  55. package/vendor/chatgpt2api/source/services/__init__.py +0 -0
  56. package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
  57. package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
  58. package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
  59. package/vendor/chatgpt2api/source/services/config.py +667 -0
  60. package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
  61. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
  62. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
  63. package/vendor/chatgpt2api/source/services/image_service.py +376 -0
  64. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
  65. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
  66. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
  67. package/vendor/chatgpt2api/source/services/log_service.py +337 -0
  68. package/vendor/chatgpt2api/source/services/model_service.py +182 -0
  69. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
  70. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
  71. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
  72. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
  73. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
  74. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
  75. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
  76. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
  77. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
  78. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
  79. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
  80. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
  81. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
  82. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
  83. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
  84. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
  85. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
  86. package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
  87. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
  88. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
  89. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
  90. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
  91. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
  92. package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
  93. package/vendor/chatgpt2api/source/utils/helper.py +469 -0
  94. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
  95. package/vendor/chatgpt2api/source/utils/log.py +110 -0
  96. package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
  97. package/vendor/chatgpt2api/source/utils/pow.py +207 -0
  98. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
  99. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
  100. package/vendor/chatgpt2api/source/uv.lock +904 -0
  101. package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
  102. package/vendor/chatgpt2api/source/web/components.json +24 -0
  103. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
  104. package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
  105. package/vendor/chatgpt2api/source/web/package.json +58 -0
  106. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
  107. package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
  108. package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
  109. package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
  110. package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
  111. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
  112. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
  113. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
  114. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
  115. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
  116. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
  117. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
  118. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
  119. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
  120. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
  121. package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
  122. package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
  123. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
  124. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
  125. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
  126. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
  127. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
  128. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
  129. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
  130. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
  131. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
  133. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
  134. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
  135. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
  136. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
  137. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
  138. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
  139. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
  140. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
  141. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
  142. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
  143. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
  144. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
  145. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
  146. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
  147. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
  148. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
  149. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
  150. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
  151. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
  152. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
  153. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
  154. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
  155. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
  156. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
  157. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
  158. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
  159. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
  160. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
  161. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
  162. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
  163. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
  164. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
  165. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
  166. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
  167. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
  168. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
  169. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
  170. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
  171. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
  172. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
  173. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
  174. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
  175. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
  176. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
  177. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
  178. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
  179. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
  180. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
  181. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
  182. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
  183. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
  184. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
  185. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
  186. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
package/install.mjs ADDED
@@ -0,0 +1,333 @@
1
+ /**
2
+ * 把插件挂进当前 DSH desktop profile。
3
+ *
4
+ * 用法:
5
+ * node install.mjs 安装(幂等,可在任意目录重复执行)
6
+ * node install.mjs --uninstall 从 profile 配置里摘掉插件并断开目录联接
7
+ *
8
+ * 改动范围:
9
+ * 1. profile/node_modules 下建指向插件源码目录的目录联接(父目录自动创建);
10
+ * 2. profile/package.json 的 dependencies 与 dsh.profile.bundles(改前备份);
11
+ * 3. pnpm-lock.yaml 里直接补/删 importer 记录,不联网、不动 node_modules。
12
+ * 不会执行完整 pnpm install(避免重拉已装的其他插件),也不会重启 DSH。
13
+ *
14
+ * 顺序是「先建联接、后写配置」:联接建不起来时 profile 保持原样,DSH 照常启动,
15
+ * 不会出现「配置已注册、目录联接没建」的半装状态。这一点在全新 profile
16
+ * (没有 node_modules、没有 pnpm-lock.yaml)上尤其重要。
17
+ *
18
+ * 号池已经并入工作台:安装时顺手把旧的独立号池插件 dsh-plugin-image-pool
19
+ * 从 profile 摘掉(只改配置,不删它的源码目录和账号数据),侧栏就只剩一个
20
+ * 工作台入口。旧插件下次 DSH 启动不再加载,号池改由工作台后端自己拉起。
21
+ *
22
+ * 整包自包含:插件自带 vendor/chatgpt2api(号池服务端源码 + uv)与 server/
23
+ * (工作台后端代码)。开发机上仍优先用工作区里的 image-local/,把这个插件
24
+ * 单独拷到另一台机器时会自动落到 server/,不要求对方装任何第三方插件。
25
+ */
26
+ import fs from 'node:fs';
27
+ import path from 'node:path';
28
+ import os from 'node:os';
29
+ import { fileURLToPath } from 'node:url';
30
+ import { fetchStyleImages as fetchSamples } from './scripts/fetch-style-images.mjs';
31
+
32
+ const NAME = 'dsh-plugin-image-workbench';
33
+ const LEGACY = 'dsh-plugin-image-pool';
34
+ const PLUGIN_DIR = path.resolve(fileURLToPath(new URL('.', import.meta.url)));
35
+ const PROFILE_DIR = path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'desktop');
36
+ const PKG_FILE = path.join(PROFILE_DIR, 'package.json');
37
+ const LOCK_FILE = path.join(PROFILE_DIR, 'pnpm-lock.yaml');
38
+ const LINK_PATH = path.join(PROFILE_DIR, 'node_modules', NAME);
39
+ const uninstall = process.argv.includes('--uninstall');
40
+
41
+ /* Node 16 及更早没有全局 fetch,脚本会在改完配置之后才报错,留下半装状态。
42
+ 这里先拦一道,报错时 profile 一个字节都没动。 */
43
+ const nodeMajor = Number(String(process.versions.node).split('.')[0]);
44
+ if (nodeMajor < 18) {
45
+ console.error('当前 Node 版本为 ' + process.versions.node + ',本安装器需要 Node 18 及以上(建议 20+)。');
46
+ console.error('升级 Node 后重新执行;profile 未被修改。');
47
+ process.exit(1);
48
+ }
49
+
50
+ if (!fs.existsSync(PKG_FILE)) {
51
+ console.error('找不到 DSH profile:' + PKG_FILE);
52
+ console.error('请确认 DSH 桌面端已至少启动过一次,或用 DSH_HOME 指定数据目录。');
53
+ process.exit(1);
54
+ }
55
+
56
+ const raw = fs.readFileSync(PKG_FILE, 'utf8');
57
+ const pkg = JSON.parse(raw);
58
+ const deps = pkg.dependencies || (pkg.dependencies = {});
59
+ const bundles = pkg.dsh?.profile?.bundles;
60
+ if (!Array.isArray(bundles)) {
61
+ console.error('profile/package.json 缺少 dsh.profile.bundles,已停止,不做任何修改。');
62
+ process.exit(1);
63
+ }
64
+
65
+ /* ---------- 目录联接 ---------- */
66
+
67
+ /** 判定当前联接状态:missing 不存在 / dir 是真实目录 / ok 有效联接 / dead 失效联接 */
68
+ function linkState() {
69
+ let st = null;
70
+ try { st = fs.lstatSync(LINK_PATH); } catch { return 'missing'; }
71
+ if (!st.isSymbolicLink()) return 'dir';
72
+ try { fs.realpathSync(LINK_PATH); return 'ok'; } catch { return 'dead'; }
73
+ }
74
+
75
+ function removeLink() {
76
+ try { fs.unlinkSync(LINK_PATH); return true; } catch (_) { /* 试下面的 */ }
77
+ try { fs.rmdirSync(LINK_PATH); return true; } catch (_) { return false; }
78
+ }
79
+
80
+ /**
81
+ * 建立(或修正)目录联接。
82
+ * 三种非理想状态都必须处理,否则会出现「装上了但其实指向旧目录」的静默错误:
83
+ * - 父目录不存在(全新 profile) -> 自动创建
84
+ * - 指向别的路径(换目录重装) -> 删除后重建
85
+ * - 死链(旧目录已被删) -> 删除后重建
86
+ */
87
+ function ensureLink() {
88
+ const parent = path.dirname(LINK_PATH);
89
+ const createdParent = !fs.existsSync(parent);
90
+ fs.mkdirSync(parent, { recursive: true });
91
+ if (createdParent) console.log('已创建目录 -> ' + parent);
92
+
93
+ const st = linkState();
94
+ if (st === 'dir') {
95
+ console.error('node_modules/' + NAME + ' 是一个真实目录,不是目录联接。');
96
+ console.error('为避免误删你的文件,安装器不做改动。请先手动移走它,再重新执行。');
97
+ process.exit(1);
98
+ }
99
+ if (st === 'ok') {
100
+ const cur = fs.realpathSync(LINK_PATH);
101
+ if (path.resolve(cur).toLowerCase() === path.resolve(PLUGIN_DIR).toLowerCase()) {
102
+ console.log('目录联接已存在,指向本目录 -> ' + LINK_PATH);
103
+ return;
104
+ }
105
+ console.log('目录联接原指向 ' + cur + ',与本次安装目录不同,正在重建。');
106
+ if (!removeLink()) {
107
+ console.error('无法删除旧联接:' + LINK_PATH);
108
+ process.exit(1);
109
+ }
110
+ } else if (st === 'dead') {
111
+ console.log('发现失效的目录联接(目标已不存在),重建中。');
112
+ if (!removeLink()) {
113
+ console.error('无法删除失效联接:' + LINK_PATH);
114
+ process.exit(1);
115
+ }
116
+ }
117
+
118
+ try {
119
+ fs.symlinkSync(PLUGIN_DIR, LINK_PATH, 'junction');
120
+ } catch (e) {
121
+ console.error('建立目录联接失败:' + (e && e.message || e));
122
+ console.error('常见原因:profile 目录无写入权限,或系统禁用了目录联接。profile 未被修改。');
123
+ process.exit(1);
124
+ }
125
+ console.log('已建立目录联接 -> ' + LINK_PATH);
126
+ }
127
+
128
+ /* ---------- 包完整性 ---------- */
129
+
130
+ function checkAssets() {
131
+ const critical = ['host.js', 'client.js', 'cordis.patch.yml',
132
+ 'server/app.py', 'server/static/index.html', 'server/catalog.json'];
133
+ const optional = ['vendor/chatgpt2api/runtime/uv.exe', 'server/requirements.txt'];
134
+ const missing = critical.filter(f => !fs.existsSync(path.join(PLUGIN_DIR, f)));
135
+ if (missing.length) {
136
+ console.error('插件包不完整,缺少:' + missing.join('、'));
137
+ console.error('这通常是解压不完整或下载中断。请重新获取完整包后重试。');
138
+ process.exit(1);
139
+ }
140
+ for (const f of optional) {
141
+ if (!fs.existsSync(path.join(PLUGIN_DIR, f)))
142
+ console.warn('提醒:未找到 ' + f + ',首次启动时将改用系统 Python 或自动下载运行时。');
143
+ }
144
+ }
145
+
146
+ /* ---------- profile 配置 ---------- */
147
+
148
+ let removedLegacy = false;
149
+
150
+ /** 写入 dependencies 与 bundles,仅在确有变化时备份并落盘。 */
151
+ function applyConfig() {
152
+ let changed = false;
153
+ if (uninstall) {
154
+ if (deps[NAME]) { delete deps[NAME]; changed = true; }
155
+ const i = bundles.indexOf(NAME);
156
+ if (i >= 0) { bundles.splice(i, 1); changed = true; }
157
+ } else {
158
+ const linkSpec = 'link:' + PLUGIN_DIR.replace(/\\/g, '/');
159
+ if (deps[NAME] !== linkSpec) { deps[NAME] = linkSpec; changed = true; }
160
+ if (!bundles.includes(NAME)) { bundles.push(NAME); changed = true; }
161
+ // 旧独立号池插件只从 bundle / 依赖里摘掉,文件与账号数据一律保留
162
+ if (deps[LEGACY]) { delete deps[LEGACY]; changed = true; removedLegacy = true; }
163
+ const j = bundles.indexOf(LEGACY);
164
+ if (j >= 0) { bundles.splice(j, 1); changed = true; removedLegacy = true; }
165
+ }
166
+
167
+ if (!changed) { console.log('profile 配置已经是目标状态,无需修改。'); return; }
168
+
169
+ // 写文件之前再确认一次目标文件没被别的进程改掉,降低覆盖别人改动的风险。
170
+ if (fs.readFileSync(PKG_FILE, 'utf8') !== raw) {
171
+ console.error('profile/package.json 在安装过程中被其他进程修改,已停止以免覆盖。');
172
+ console.error('请关闭 DSH 后重新执行,或手动核对配置。');
173
+ process.exit(1);
174
+ }
175
+
176
+ const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
177
+ const backup = PKG_FILE + '.bak_' + stamp;
178
+ fs.writeFileSync(backup, raw, 'utf8');
179
+ fs.writeFileSync(PKG_FILE, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
180
+ console.log('已备份原配置 -> ' + backup);
181
+ console.log('已更新 -> ' + PKG_FILE);
182
+ }
183
+
184
+ /**
185
+ * 直接补/删 lock 的 importer 记录,不调用 pnpm install。
186
+ * link 依赖在 lock v9 里的标准写法(已实测,跨盘符时 specifier 与 version 都是绝对路径)。
187
+ * lock 不存在时跳过——DSH 下次执行 pnpm install 会自行生成。
188
+ */
189
+ function dropLockEntry(text, name) {
190
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
191
+ return text.replace(new RegExp(' ' + escaped + ':\\n(?: [^\\n]*\\n){2}'), '');
192
+ }
193
+
194
+ function syncLock(linkSpec) {
195
+ if (!fs.existsSync(LOCK_FILE)) return;
196
+ const before = fs.readFileSync(LOCK_FILE, 'utf8');
197
+ const entry = ' ' + NAME + ':\n specifier: ' + linkSpec + '\n version: ' + linkSpec + '\n';
198
+ let after = before;
199
+ if (uninstall) after = dropLockEntry(after, NAME);
200
+ if (removedLegacy) after = dropLockEntry(after, LEGACY);
201
+ if (!uninstall && !before.includes(' ' + NAME + ':')) {
202
+ const marker = '\npackages:\n';
203
+ const at = after.indexOf(marker);
204
+ if (at < 0) { console.warn('lock 结构不符合预期,跳过(不影响插件加载)。'); return; }
205
+ after = after.slice(0, at) + entry + after.slice(at);
206
+ }
207
+ if (after === before) return;
208
+ const backup = LOCK_FILE + '.bak_' + new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
209
+ fs.writeFileSync(backup, before, 'utf8');
210
+ fs.writeFileSync(LOCK_FILE, after, 'utf8');
211
+ console.log('已备份并同步 -> ' + LOCK_FILE);
212
+ }
213
+
214
+ /* ---------- 后端代码同步 ---------- */
215
+
216
+ /**
217
+ * 把开发目录 image-local/ 的纯代码同步进插件 server/。
218
+ * 只覆盖代码,绝不删目标里已有的图片、配置、数据库等运行态数据。
219
+ * 发布后的插件包旁边没有 image-local/,直接跳过,用包内自带的 server/。
220
+ */
221
+ function syncServer() {
222
+ const src = path.resolve(PLUGIN_DIR, '..', 'image-local');
223
+ const dst = path.join(PLUGIN_DIR, 'server');
224
+ if (!fs.existsSync(path.join(src, 'app.py'))) {
225
+ console.log('旁边没有 image-local/,使用插件包内自带的 server/。');
226
+ return;
227
+ }
228
+ const skipDirs = new Set(['.venv', '__pycache__', 'images', 'thumbs', 'refs',
229
+ 'repo_images', 'workbench', '.pytest_cache']);
230
+ const skipFiles = new Set(['config.local.json', 'app.db', 'history.db', '.DS_Store']);
231
+ const junk = n => skipFiles.has(n) || /\.(log|pyc|db|vps_original)$/.test(n)
232
+ || /\.bak_/.test(n);
233
+ // 开发机上的源文件可能是 CRLF,仓库里存的是 LF。不归一的话每次同步
234
+ // 都会把整个文件标成改动,commit 里几千行假 diff,历史没法看。
235
+ const textExt = new Set(['.py', '.html', '.js', '.mjs', '.json', '.txt',
236
+ '.md', '.css', '.yml', '.yaml']);
237
+ const syncFile = (from, to) => {
238
+ if (!textExt.has(path.extname(from).toLowerCase())) {
239
+ fs.copyFileSync(from, to);
240
+ return;
241
+ }
242
+ const s = fs.readFileSync(from, 'utf8').replace(/\r\n?/g, '\n');
243
+ fs.writeFileSync(to, s, 'utf8');
244
+ };
245
+ let copied = 0;
246
+ const walk = (from, to) => {
247
+ fs.mkdirSync(to, { recursive: true });
248
+ for (const e of fs.readdirSync(from, { withFileTypes: true })) {
249
+ const a = path.join(from, e.name), b = path.join(to, e.name);
250
+ if (e.isDirectory()) { if (!skipDirs.has(e.name)) walk(a, b); continue; }
251
+ if (!e.isFile() || junk(e.name)) continue;
252
+ syncFile(a, b);
253
+ copied += 1;
254
+ }
255
+ };
256
+ walk(src, dst);
257
+ fs.writeFileSync(path.join(dst, 'requirements.txt'),
258
+ 'fastapi\nuvicorn\nhttpx\npydantic\npillow\npython-multipart\n', 'utf8');
259
+ console.log('已同步工作台后端代码 -> server/(' + copied + ' 个文件,不含账号/图片/配置)。');
260
+ }
261
+
262
+ /* ---------- 样图下载 ---------- */
263
+
264
+ /**
265
+ * 安装时补齐真实样图(风格 / 图型 / 主题色预览)。
266
+ * 实现与 host.js 共用 scripts/fetch-style-images.mjs —— 一份逻辑两处调用,
267
+ * 免得「插件启动时补图」和「安装时补图」两套代码各自漂移。
268
+ */
269
+ async function fetchStyleImages() {
270
+ const r = await fetchSamples({
271
+ serverDir: path.join(PLUGIN_DIR, 'server'),
272
+ log: text => console.log(text),
273
+ });
274
+ if (r.error) console.log('样图下载跳过:' + r.error);
275
+ if (r.fail) console.log(' ' + r.fail + ' 张失败(多为网络受限)。重跑 node install.mjs 可补齐,已存在的会自动跳过。');
276
+ }
277
+
278
+ /* ---------- 安装后自检 ---------- */
279
+
280
+ function verify() {
281
+ const problems = [];
282
+ if (!bundles.includes(NAME)) problems.push('bundles 中未包含 ' + NAME);
283
+ if (!deps[NAME]) problems.push('dependencies 中未包含 ' + NAME);
284
+ if (linkState() !== 'ok') problems.push('目录联接不有效:' + LINK_PATH);
285
+ const viaLink = path.join(LINK_PATH, 'host.js');
286
+ if (!fs.existsSync(viaLink)) problems.push('通过目录联接读不到 host.js');
287
+ if (problems.length) {
288
+ console.error('自检未通过:');
289
+ for (const p of problems) console.error(' - ' + p);
290
+ return false;
291
+ }
292
+ console.log('自检通过:配置已注册,目录联接有效,插件文件可读。');
293
+ return true;
294
+ }
295
+
296
+ /* ---------- 主流程 ---------- */
297
+
298
+ async function install() {
299
+ checkAssets();
300
+ syncServer();
301
+ ensureLink(); // 先建联接:失败则 profile 原样,不留半装
302
+ applyConfig(); // 再改配置
303
+ syncLock('link:' + PLUGIN_DIR.replace(/\\/g, '/'));
304
+ const ok = verify();
305
+ if (removedLegacy)
306
+ console.log('已摘除旧独立号池插件 ' + LEGACY + '(源码与账号数据保留,未删除)。');
307
+ await fetchStyleImages();
308
+ if (!ok) {
309
+ console.error('安装未完全成功,请根据上面的提示处理后重跑 node install.mjs。');
310
+ process.exit(1);
311
+ }
312
+ console.log('安装完成。多数情况下 DSH 会直接加载新 bundle;若侧栏未出现入口,完全退出 DSH 再启动一次。');
313
+ console.log('首次打开工作台会自动初始化后端与账号池,通常 1 分钟内就绪。');
314
+ }
315
+
316
+ if (!uninstall) {
317
+ install().catch(e => {
318
+ console.error('安装过程中发生异常:' + (e && e.stack || e));
319
+ process.exit(1);
320
+ });
321
+ } else {
322
+ applyConfig();
323
+ syncLock('link:' + PLUGIN_DIR.replace(/\\/g, '/'));
324
+ // 断开联接:只删联接本身,不会碰插件源码目录与账号数据。
325
+ const st = linkState();
326
+ if (st === 'ok' || st === 'dead') {
327
+ console.log(removeLink() ? '已断开目录联接 -> ' + LINK_PATH : '警告:目录联接未能删除,可手动移除。');
328
+ } else if (st === 'dir') {
329
+ console.log('node_modules/' + NAME + ' 是真实目录,非联接,未做改动。');
330
+ }
331
+ console.log('已从 profile 摘除插件;插件源码目录与 ~/.dsh/chatgpt2api-image 数据保留。');
332
+ console.log('完全退出 DSH 再启动,侧栏入口即消失。确认不再需要时可自行删除源码目录。');
333
+ }
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "dsh-plugin-image-workbench",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.6",
4
+ "type": "module",
5
+ "description": "Bluxe手绘工坊 · 生图工作台(图文 / 摄影):DSH 侧栏图像生成工作台,默认使用内置本地账号池,亦支持任意 OpenAI 兼容接口(Base URL / API Key / 模型);可自定义工作台名称与图片存储目录。",
6
+ "main": "./host.js",
7
+ "exports": {
8
+ ".": "./host.js",
9
+ "./client": "./client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "bundle": {
14
+ "patch": "./cordis.patch.yml"
15
+ },
16
+ "client": {
17
+ "platform": "web",
18
+ "inject": [
19
+ "@deepseek-ai/dsh-client-ui-sidebar",
20
+ "@deepseek-ai/dsh-client-ui-layout"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "host.js",
26
+ "client.js",
27
+ "cordis.patch.yml",
28
+ "install.mjs",
29
+ "server/*.py",
30
+ "server/catalog.json",
31
+ "server/requirements.txt",
32
+ "server/static",
33
+ "vendor",
34
+ "scripts/vendor-snapshot.py",
35
+ "scripts/fetch-style-images.mjs"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "publishConfig": {
41
+ "registry": "https://registry.npmjs.org/",
42
+ "access": "public"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/guohwa/dsh-plugin-image-workbench.git"
47
+ },
48
+ "homepage": "https://github.com/guohwa/dsh-plugin-image-workbench#readme",
49
+ "bugs": {
50
+ "url": "https://github.com/guohwa/dsh-plugin-image-workbench/issues"
51
+ },
52
+ "keywords": [
53
+ "dsh",
54
+ "dsh-plugin",
55
+ "image-generation",
56
+ "openai-compatible",
57
+ "gpt-image",
58
+ "workbench"
59
+ ]
60
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 样图下载器(风格 / 图型 / 主题色三类预览图)。
3
+ *
4
+ * 为什么要有这个文件:repo_images/ 里的 574 张样例图不进 npm 包(包体积会从
5
+ * 几百 KB 涨到十几 MB,而且图片本来就该放 CDN)。但工作台后端的 /img/{path}
6
+ * 在文件缺失时会返回一张渐变 SVG 占位图,于是全新安装打开「配色库 / 风格库」
7
+ * 全是色块 —— 看起来像装坏了。安装包里必须自带一次「把样图拉下来」的动作,
8
+ * 光靠 README 让用户手跑 `node install.mjs` 是不够的。
9
+ *
10
+ * 约定:
11
+ * - 由 host.js 在插件启动时后台调用(不阻塞工作台启动),失败只写日志;
12
+ * - install.mjs 也复用它,安装完顺手补齐,两处逻辑只有这一份;
13
+ * - 幂等:已存在且非空的文件直接跳过,重跑只补缺失项;
14
+ * - 只接受 image/* 响应,避免把 Gitee 的 302 登录页 HTML 写成 .webp 坏图。
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ /** 样图源,按「CDN 单文件 → Gitee 镜像 → GitHub raw」顺序尝试。 */
20
+ export const IMAGE_SOURCES = [
21
+ 'https://img.handraw-style.com',
22
+ 'https://gitee.com/yang02010/handraw-style/raw/master',
23
+ 'https://raw.githubusercontent.com/yang0/handraw-style/main',
24
+ ];
25
+
26
+ /** catalog.json 里 img 字段形如 images/individual/FA/FA-001.webp,落到 repo_images/ 时去掉 images/ 前缀。 */
27
+ export function localPathOf(repoImages, rel) {
28
+ return path.join(repoImages, String(rel).replace(/^images\//, ''));
29
+ }
30
+
31
+ /** 从 catalog.json 收集需要下载的样图相对路径(去重)。 */
32
+ export function collectRels(serverDir) {
33
+ const file = path.join(serverDir, 'catalog.json');
34
+ const rels = [];
35
+ const cat = JSON.parse(fs.readFileSync(file, 'utf8'));
36
+ for (const key of ['styles', 'layouts', 'colors']) {
37
+ for (const item of cat[key] || []) if (item && item.img) rels.push(item.img);
38
+ }
39
+ return [...new Set(rels)];
40
+ }
41
+
42
+ /**
43
+ * 下载缺失样图。
44
+ * @param {{serverDir:string, log?:Function, concurrency?:number, timeoutMs?:number, sources?:string[]}} opts
45
+ * @returns {Promise<{ok:boolean, total:number, done:number, skip:number, fail:number, error?:string}>}
46
+ */
47
+ export async function fetchStyleImages(opts = {}) {
48
+ const serverDir = opts.serverDir;
49
+ const log = opts.log || (() => {});
50
+ const concurrency = Math.max(1, Math.min(24, opts.concurrency || 12));
51
+ const timeoutMs = opts.timeoutMs || 15000;
52
+ const sources = opts.sources || IMAGE_SOURCES;
53
+ if (!serverDir) return { ok: false, total: 0, done: 0, skip: 0, fail: 0, error: '缺少 serverDir' };
54
+
55
+ const repoImages = path.join(serverDir, 'repo_images');
56
+ let rels = [];
57
+ try {
58
+ rels = collectRels(serverDir);
59
+ } catch (e) {
60
+ return { ok: false, total: 0, done: 0, skip: 0, fail: 0,
61
+ error: '读取 catalog.json 失败:' + (e && e.message || e) };
62
+ }
63
+ if (!rels.length) return { ok: true, total: 0, done: 0, skip: 0, fail: 0 };
64
+
65
+ fs.mkdirSync(repoImages, { recursive: true });
66
+ const exists = p => { try { return fs.statSync(p).size > 0; } catch { return false; } };
67
+
68
+ const missing = rels.filter(rel => !exists(localPathOf(repoImages, rel)));
69
+ const skip = rels.length - missing.length;
70
+ if (!missing.length) return { ok: true, total: rels.length, done: 0, skip, fail: 0 };
71
+
72
+ log(`正在下载缺失样图 ${missing.length} 张(共 ${rels.length} 张,已存在 ${skip} 张)…`);
73
+ let done = 0, fail = 0;
74
+
75
+ const getOne = async rel => {
76
+ const dst = localPathOf(repoImages, rel);
77
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
78
+ for (const base of sources) {
79
+ const url = base.replace(/\/$/, '') + '/' + String(rel).replace(/^\/+/, '');
80
+ try {
81
+ const ac = new AbortController();
82
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
83
+ const res = await fetch(url, { signal: ac.signal, redirect: 'follow' });
84
+ clearTimeout(timer);
85
+ if (!res.ok) continue;
86
+ const type = (res.headers.get('content-type') || '').toLowerCase();
87
+ if (!type.startsWith('image/')) continue;
88
+ const buf = Buffer.from(await res.arrayBuffer());
89
+ if (buf.length < 200) continue;
90
+ fs.writeFileSync(dst, buf);
91
+ done += 1;
92
+ return;
93
+ } catch { /* 换下一个源 */ }
94
+ }
95
+ fail += 1;
96
+ };
97
+
98
+ for (let i = 0; i < missing.length; i += concurrency) {
99
+ await Promise.all(missing.slice(i, i + concurrency).map(getOne));
100
+ }
101
+
102
+ log(`样图下载结束:已下 ${done} 张,跳过 ${skip} 张,失败 ${fail} 张`);
103
+ return { ok: fail === 0, total: rels.length, done, skip, fail };
104
+ }
@@ -0,0 +1,154 @@
1
+ """把本机一份 ChatGPT2API 号池源码,做成插件自带的 vendor 快照。
2
+
3
+ 用途:工作台插件发布到别的机器时,不再要求对方先装任何第三方号池插件。
4
+ 这个脚本只在「升级号池源码」时手动跑一次,日常运行完全不依赖它。
5
+
6
+ 用法:
7
+ python vendor-snapshot.py [源号池 home 目录]
8
+
9
+ 默认源:~/.dsh/chatgpt2api-image
10
+ 输出: <插件目录>/vendor/chatgpt2api/
11
+
12
+ 严格排除:账号库 data/、虚拟环境 .venv/、日志、备份、缓存、明文密钥与代理配置。
13
+ config.json 会被清洗成 config.default.json(auth-key / proxy / 云存储凭据一律置空)。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import os
19
+ import shutil
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ PLUGIN_DIR = Path(__file__).resolve().parent.parent
24
+ OUT = PLUGIN_DIR / "vendor" / "chatgpt2api"
25
+
26
+ # 只带运行需要的代码与静态资源,不带文档 / 测试 / 容器配置
27
+ DIRS = ["api", "services", "utils", "web", "assets", "scripts"]
28
+ FILES = ["main.py", "pyproject.toml", "uv.lock", ".python-version",
29
+ ".env.example", "LICENSE", "README.md", "VERSION"]
30
+
31
+ EXCLUDE_NAMES = {"__pycache__", ".git", ".codex", ".pytest_cache", ".mypy_cache"}
32
+ EXCLUDE_SUFFIX = (".pyc", ".pyo", ".log", ".db", ".db-shm", ".db-wal", ".enc")
33
+
34
+
35
+ def keep(p: Path) -> bool:
36
+ n = p.name
37
+ if n in EXCLUDE_NAMES or n.startswith(".dsh-"):
38
+ return False
39
+ if n.endswith(EXCLUDE_SUFFIX) or ".bak" in n or n.endswith(".tmp"):
40
+ return False
41
+ return True
42
+
43
+
44
+ def clean_config(raw: dict) -> dict:
45
+ """出厂默认配置:任何凭据、代理、本机状态都不带进插件包。"""
46
+ d = json.loads(json.dumps(raw)) # 深拷贝
47
+ d["auth-key"] = ""
48
+ d["proxy"] = ""
49
+ pr = d.get("proxy_runtime") or {}
50
+ pr["enabled"] = False
51
+ pr["proxy_url"] = ""
52
+ pr["resource_proxy_url"] = ""
53
+ cl = pr.get("clearance") or {}
54
+ cl["cf_cookies"] = ""
55
+ cl["cf_clearance"] = ""
56
+ d["backup"] = {**(d.get("backup") or {}),
57
+ "enabled": False, "access_key_id": "",
58
+ "secret_access_key": "", "passphrase": ""}
59
+ d["ai_review"] = {**(d.get("ai_review") or {}),
60
+ "enabled": False, "base_url": "", "api_key": ""}
61
+ img = d.get("image_storage") or {}
62
+ img.update({"enabled": False, "webdav_username": "", "webdav_password": ""})
63
+ return d
64
+
65
+
66
+ def main() -> int:
67
+ home = Path(sys.argv[1]) if len(sys.argv) > 1 else (
68
+ Path(os.environ.get("DSH_HOME", Path.home() / ".dsh")) / "chatgpt2api-image")
69
+ src = home / "source"
70
+ if not (src / "main.py").is_file():
71
+ print("找不到号池源码:" + str(src))
72
+ return 1
73
+ if OUT.resolve() == src.resolve():
74
+ print("源目录不能是 vendor 输出目录")
75
+ return 1
76
+
77
+ if OUT.exists():
78
+ shutil.rmtree(OUT)
79
+ (OUT / "source").mkdir(parents=True)
80
+ (OUT / "runtime").mkdir(parents=True)
81
+
82
+ copied = 0
83
+ for d in DIRS:
84
+ s = src / d
85
+ if not s.is_dir():
86
+ continue
87
+ shutil.copytree(s, OUT / "source" / d, ignore=lambda _, names: [
88
+ n for n in names if not keep(Path(n))], dirs_exist_ok=True)
89
+ copied += sum(1 for _ in (OUT / "source" / d).rglob("*") if _.is_file())
90
+ for f in FILES:
91
+ s = src / f
92
+ if s.is_file() and keep(s):
93
+ shutil.copy2(s, OUT / "source" / f)
94
+ copied += 1
95
+
96
+ cfg = src / "config.json"
97
+ if cfg.is_file():
98
+ try:
99
+ data = clean_config(json.loads(cfg.read_text(encoding="utf-8")))
100
+ (OUT / "source" / "config.default.json").write_text(
101
+ json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
102
+ copied += 1
103
+ except Exception as e:
104
+ print("警告:config.json 清洗失败,未生成默认配置:" + str(e))
105
+
106
+ uv = ""
107
+ for c in (home / "runtime" / "uv.exe", home / "runtime" / "uv",
108
+ shutil.which("uv") or ""):
109
+ if c and Path(c).is_file():
110
+ uv = str(c)
111
+ break
112
+ if uv:
113
+ shutil.copy2(uv, OUT / "runtime" / Path(uv).name)
114
+ copied += 1
115
+ else:
116
+ print("警告:没找到 uv.exe,全新机器初始化时会退回系统 Python + pip")
117
+
118
+ note = """# 内置号池运行时
119
+
120
+ 本目录是 ChatGPT2API 号池服务端的**代码快照**,随「Bluxe手绘工坊 · 生图工作台」插件一起分发,
121
+ 安装后由工作台后端(pool_service.py)释放到本机数据目录并自建虚拟环境。
122
+
123
+ * 后端上游:[basketikun/chatgpt2api](https://github.com/basketikun/chatgpt2api),`source/VERSION` 与 `SOURCE.json` 记录快照版本;DSH 插件封装:[yang0/chatgpt2api-dsh](https://github.com/yang0/chatgpt2api-dsh)。
124
+ * 许可:MIT License,Copyright (c) 2026 kunkun,LICENSE 原文见 `source/LICENSE`。本目录仅快照后端源码,不含 DSH 插件外壳与任何账号数据。
125
+ * 已包含本工作台需要的两处补丁:round-robin 游标持久化、限流号排除;首次初始化时还会幂等复打一次。
126
+ * **不含**任何账号、token、密钥、代理配置或图片数据。`source/config.default.json` 是清洗后的出厂默认配置。
127
+ * 升级方式:改完本机号池源码后,重跑 `scripts/vendor-snapshot.py`,再重新发布插件。
128
+
129
+ 分工:工作台插件负责「拉起并守护」号池进程;本目录只提供「能跑起来的代码」。
130
+ 两者都不需要安装任何第三方 DSH 插件。
131
+ """
132
+ (OUT / "README.md").write_text(note, encoding="utf-8")
133
+ (OUT / "SOURCE.json").write_text(json.dumps({
134
+ "name": "chatgpt2api", "license": "MIT",
135
+ "version": (src / "VERSION").read_text(encoding="utf-8").strip()
136
+ if (src / "VERSION").is_file() else "",
137
+ "python": (src / ".python-version").read_text(encoding="utf-8").strip()
138
+ if (src / ".python-version").is_file() else "",
139
+ "files": copied,
140
+ "version_source": "source/VERSION",
141
+ "copyright": "Copyright (c) 2026 kunkun",
142
+ "upstream_backend": "https://github.com/basketikun/chatgpt2api",
143
+ "upstream_dsh_plugin": "https://github.com/yang0/chatgpt2api-dsh",
144
+ }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
145
+
146
+ size = sum(f.stat().st_size for f in OUT.rglob("*") if f.is_file())
147
+ print("vendor 已生成:" + str(OUT))
148
+ print("文件数 %d,体积 %.1f MB" % (copied, size / 1024 / 1024))
149
+ print("含 uv 运行时:" + ("是" if uv else "否"))
150
+ return 0
151
+
152
+
153
+ if __name__ == "__main__":
154
+ raise SystemExit(main())