dsh-plugin-image-workbench 0.0.0-stage → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +256 -0
- package/README.md +256 -2
- package/client.js +241 -0
- package/cordis.patch.yml +14 -0
- package/host.js +641 -0
- package/install.mjs +387 -0
- package/package.json +57 -4
- package/scripts/vendor-snapshot.py +154 -0
- package/server/access.py +26 -0
- package/server/app.py +1329 -0
- package/server/catalog.json +1 -0
- package/server/catalog.py +147 -0
- package/server/composer.py +391 -0
- package/server/drafts.py +158 -0
- package/server/history.py +575 -0
- package/server/imaging.py +172 -0
- package/server/pool.py +1335 -0
- package/server/pool_admin.py +921 -0
- package/server/pool_service.py +1073 -0
- package/server/requirements.txt +6 -0
- package/server/smoke_check.py +402 -0
- package/server/static/access.html +202 -0
- package/server/static/index.html +4725 -0
- package/vendor/chatgpt2api/README.md +13 -0
- package/vendor/chatgpt2api/SOURCE.json +11 -0
- package/vendor/chatgpt2api/runtime/uv.exe +0 -0
- package/vendor/chatgpt2api/source/.env.example +82 -0
- package/vendor/chatgpt2api/source/.python-version +1 -0
- package/vendor/chatgpt2api/source/LICENSE +21 -0
- package/vendor/chatgpt2api/source/README.md +391 -0
- package/vendor/chatgpt2api/source/VERSION +1 -0
- package/vendor/chatgpt2api/source/api/__init__.py +2 -0
- package/vendor/chatgpt2api/source/api/accounts.py +537 -0
- package/vendor/chatgpt2api/source/api/ai.py +217 -0
- package/vendor/chatgpt2api/source/api/app.py +62 -0
- package/vendor/chatgpt2api/source/api/errors.py +46 -0
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
- package/vendor/chatgpt2api/source/api/support.py +134 -0
- package/vendor/chatgpt2api/source/api/system.py +365 -0
- package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
- package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
- package/vendor/chatgpt2api/source/config.default.json +108 -0
- package/vendor/chatgpt2api/source/main.py +9 -0
- package/vendor/chatgpt2api/source/pyproject.toml +27 -0
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
- package/vendor/chatgpt2api/source/services/__init__.py +0 -0
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
- package/vendor/chatgpt2api/source/services/config.py +667 -0
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
- package/vendor/chatgpt2api/source/services/image_service.py +376 -0
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
- package/vendor/chatgpt2api/source/services/log_service.py +337 -0
- package/vendor/chatgpt2api/source/services/model_service.py +182 -0
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
- package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
- package/vendor/chatgpt2api/source/utils/helper.py +469 -0
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
- package/vendor/chatgpt2api/source/utils/log.py +110 -0
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
- package/vendor/chatgpt2api/source/utils/pow.py +207 -0
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
- package/vendor/chatgpt2api/source/uv.lock +904 -0
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
- package/vendor/chatgpt2api/source/web/components.json +24 -0
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
- package/vendor/chatgpt2api/source/web/package.json +58 -0
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
- package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
- package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
- package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
package/install.mjs
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
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
|
+
|
|
31
|
+
const NAME = 'dsh-plugin-image-workbench';
|
|
32
|
+
const LEGACY = 'dsh-plugin-image-pool';
|
|
33
|
+
const PLUGIN_DIR = path.resolve(fileURLToPath(new URL('.', import.meta.url)));
|
|
34
|
+
const PROFILE_DIR = path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'desktop');
|
|
35
|
+
const PKG_FILE = path.join(PROFILE_DIR, 'package.json');
|
|
36
|
+
const LOCK_FILE = path.join(PROFILE_DIR, 'pnpm-lock.yaml');
|
|
37
|
+
const LINK_PATH = path.join(PROFILE_DIR, 'node_modules', NAME);
|
|
38
|
+
const uninstall = process.argv.includes('--uninstall');
|
|
39
|
+
|
|
40
|
+
/* Node 16 及更早没有全局 fetch,脚本会在改完配置之后才报错,留下半装状态。
|
|
41
|
+
这里先拦一道,报错时 profile 一个字节都没动。 */
|
|
42
|
+
const nodeMajor = Number(String(process.versions.node).split('.')[0]);
|
|
43
|
+
if (nodeMajor < 18) {
|
|
44
|
+
console.error('当前 Node 版本为 ' + process.versions.node + ',本安装器需要 Node 18 及以上(建议 20+)。');
|
|
45
|
+
console.error('升级 Node 后重新执行;profile 未被修改。');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(PKG_FILE)) {
|
|
50
|
+
console.error('找不到 DSH profile:' + PKG_FILE);
|
|
51
|
+
console.error('请确认 DSH 桌面端已至少启动过一次,或用 DSH_HOME 指定数据目录。');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const raw = fs.readFileSync(PKG_FILE, 'utf8');
|
|
56
|
+
const pkg = JSON.parse(raw);
|
|
57
|
+
const deps = pkg.dependencies || (pkg.dependencies = {});
|
|
58
|
+
const bundles = pkg.dsh?.profile?.bundles;
|
|
59
|
+
if (!Array.isArray(bundles)) {
|
|
60
|
+
console.error('profile/package.json 缺少 dsh.profile.bundles,已停止,不做任何修改。');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---------- 目录联接 ---------- */
|
|
65
|
+
|
|
66
|
+
/** 判定当前联接状态:missing 不存在 / dir 是真实目录 / ok 有效联接 / dead 失效联接 */
|
|
67
|
+
function linkState() {
|
|
68
|
+
let st = null;
|
|
69
|
+
try { st = fs.lstatSync(LINK_PATH); } catch { return 'missing'; }
|
|
70
|
+
if (!st.isSymbolicLink()) return 'dir';
|
|
71
|
+
try { fs.realpathSync(LINK_PATH); return 'ok'; } catch { return 'dead'; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function removeLink() {
|
|
75
|
+
try { fs.unlinkSync(LINK_PATH); return true; } catch (_) { /* 试下面的 */ }
|
|
76
|
+
try { fs.rmdirSync(LINK_PATH); return true; } catch (_) { return false; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 建立(或修正)目录联接。
|
|
81
|
+
* 三种非理想状态都必须处理,否则会出现「装上了但其实指向旧目录」的静默错误:
|
|
82
|
+
* - 父目录不存在(全新 profile) -> 自动创建
|
|
83
|
+
* - 指向别的路径(换目录重装) -> 删除后重建
|
|
84
|
+
* - 死链(旧目录已被删) -> 删除后重建
|
|
85
|
+
*/
|
|
86
|
+
function ensureLink() {
|
|
87
|
+
const parent = path.dirname(LINK_PATH);
|
|
88
|
+
const createdParent = !fs.existsSync(parent);
|
|
89
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
90
|
+
if (createdParent) console.log('已创建目录 -> ' + parent);
|
|
91
|
+
|
|
92
|
+
const st = linkState();
|
|
93
|
+
if (st === 'dir') {
|
|
94
|
+
console.error('node_modules/' + NAME + ' 是一个真实目录,不是目录联接。');
|
|
95
|
+
console.error('为避免误删你的文件,安装器不做改动。请先手动移走它,再重新执行。');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
if (st === 'ok') {
|
|
99
|
+
const cur = fs.realpathSync(LINK_PATH);
|
|
100
|
+
if (path.resolve(cur).toLowerCase() === path.resolve(PLUGIN_DIR).toLowerCase()) {
|
|
101
|
+
console.log('目录联接已存在,指向本目录 -> ' + LINK_PATH);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
console.log('目录联接原指向 ' + cur + ',与本次安装目录不同,正在重建。');
|
|
105
|
+
if (!removeLink()) {
|
|
106
|
+
console.error('无法删除旧联接:' + LINK_PATH);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
} else if (st === 'dead') {
|
|
110
|
+
console.log('发现失效的目录联接(目标已不存在),重建中。');
|
|
111
|
+
if (!removeLink()) {
|
|
112
|
+
console.error('无法删除失效联接:' + LINK_PATH);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
fs.symlinkSync(PLUGIN_DIR, LINK_PATH, 'junction');
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error('建立目录联接失败:' + (e && e.message || e));
|
|
121
|
+
console.error('常见原因:profile 目录无写入权限,或系统禁用了目录联接。profile 未被修改。');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
console.log('已建立目录联接 -> ' + LINK_PATH);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ---------- 包完整性 ---------- */
|
|
128
|
+
|
|
129
|
+
function checkAssets() {
|
|
130
|
+
const critical = ['host.js', 'client.js', 'cordis.patch.yml',
|
|
131
|
+
'server/app.py', 'server/static/index.html', 'server/catalog.json'];
|
|
132
|
+
const optional = ['vendor/chatgpt2api/runtime/uv.exe', 'server/requirements.txt'];
|
|
133
|
+
const missing = critical.filter(f => !fs.existsSync(path.join(PLUGIN_DIR, f)));
|
|
134
|
+
if (missing.length) {
|
|
135
|
+
console.error('插件包不完整,缺少:' + missing.join('、'));
|
|
136
|
+
console.error('这通常是解压不完整或下载中断。请重新获取完整包后重试。');
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
for (const f of optional) {
|
|
140
|
+
if (!fs.existsSync(path.join(PLUGIN_DIR, f)))
|
|
141
|
+
console.warn('提醒:未找到 ' + f + ',首次启动时将改用系统 Python 或自动下载运行时。');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ---------- profile 配置 ---------- */
|
|
146
|
+
|
|
147
|
+
let removedLegacy = false;
|
|
148
|
+
|
|
149
|
+
/** 写入 dependencies 与 bundles,仅在确有变化时备份并落盘。 */
|
|
150
|
+
function applyConfig() {
|
|
151
|
+
let changed = false;
|
|
152
|
+
if (uninstall) {
|
|
153
|
+
if (deps[NAME]) { delete deps[NAME]; changed = true; }
|
|
154
|
+
const i = bundles.indexOf(NAME);
|
|
155
|
+
if (i >= 0) { bundles.splice(i, 1); changed = true; }
|
|
156
|
+
} else {
|
|
157
|
+
const linkSpec = 'link:' + PLUGIN_DIR.replace(/\\/g, '/');
|
|
158
|
+
if (deps[NAME] !== linkSpec) { deps[NAME] = linkSpec; changed = true; }
|
|
159
|
+
if (!bundles.includes(NAME)) { bundles.push(NAME); changed = true; }
|
|
160
|
+
// 旧独立号池插件只从 bundle / 依赖里摘掉,文件与账号数据一律保留
|
|
161
|
+
if (deps[LEGACY]) { delete deps[LEGACY]; changed = true; removedLegacy = true; }
|
|
162
|
+
const j = bundles.indexOf(LEGACY);
|
|
163
|
+
if (j >= 0) { bundles.splice(j, 1); changed = true; removedLegacy = true; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!changed) { console.log('profile 配置已经是目标状态,无需修改。'); return; }
|
|
167
|
+
|
|
168
|
+
// 写文件之前再确认一次目标文件没被别的进程改掉,降低覆盖别人改动的风险。
|
|
169
|
+
if (fs.readFileSync(PKG_FILE, 'utf8') !== raw) {
|
|
170
|
+
console.error('profile/package.json 在安装过程中被其他进程修改,已停止以免覆盖。');
|
|
171
|
+
console.error('请关闭 DSH 后重新执行,或手动核对配置。');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
176
|
+
const backup = PKG_FILE + '.bak_' + stamp;
|
|
177
|
+
fs.writeFileSync(backup, raw, 'utf8');
|
|
178
|
+
fs.writeFileSync(PKG_FILE, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
179
|
+
console.log('已备份原配置 -> ' + backup);
|
|
180
|
+
console.log('已更新 -> ' + PKG_FILE);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 直接补/删 lock 的 importer 记录,不调用 pnpm install。
|
|
185
|
+
* link 依赖在 lock v9 里的标准写法(已实测,跨盘符时 specifier 与 version 都是绝对路径)。
|
|
186
|
+
* lock 不存在时跳过——DSH 下次执行 pnpm install 会自行生成。
|
|
187
|
+
*/
|
|
188
|
+
function dropLockEntry(text, name) {
|
|
189
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
190
|
+
return text.replace(new RegExp(' ' + escaped + ':\\n(?: [^\\n]*\\n){2}'), '');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function syncLock(linkSpec) {
|
|
194
|
+
if (!fs.existsSync(LOCK_FILE)) return;
|
|
195
|
+
const before = fs.readFileSync(LOCK_FILE, 'utf8');
|
|
196
|
+
const entry = ' ' + NAME + ':\n specifier: ' + linkSpec + '\n version: ' + linkSpec + '\n';
|
|
197
|
+
let after = before;
|
|
198
|
+
if (uninstall) after = dropLockEntry(after, NAME);
|
|
199
|
+
if (removedLegacy) after = dropLockEntry(after, LEGACY);
|
|
200
|
+
if (!uninstall && !before.includes(' ' + NAME + ':')) {
|
|
201
|
+
const marker = '\npackages:\n';
|
|
202
|
+
const at = after.indexOf(marker);
|
|
203
|
+
if (at < 0) { console.warn('lock 结构不符合预期,跳过(不影响插件加载)。'); return; }
|
|
204
|
+
after = after.slice(0, at) + entry + after.slice(at);
|
|
205
|
+
}
|
|
206
|
+
if (after === before) return;
|
|
207
|
+
const backup = LOCK_FILE + '.bak_' + new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
208
|
+
fs.writeFileSync(backup, before, 'utf8');
|
|
209
|
+
fs.writeFileSync(LOCK_FILE, after, 'utf8');
|
|
210
|
+
console.log('已备份并同步 -> ' + LOCK_FILE);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ---------- 后端代码同步 ---------- */
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 把开发目录 image-local/ 的纯代码同步进插件 server/。
|
|
217
|
+
* 只覆盖代码,绝不删目标里已有的图片、配置、数据库等运行态数据。
|
|
218
|
+
* 发布后的插件包旁边没有 image-local/,直接跳过,用包内自带的 server/。
|
|
219
|
+
*/
|
|
220
|
+
function syncServer() {
|
|
221
|
+
const src = path.resolve(PLUGIN_DIR, '..', 'image-local');
|
|
222
|
+
const dst = path.join(PLUGIN_DIR, 'server');
|
|
223
|
+
if (!fs.existsSync(path.join(src, 'app.py'))) {
|
|
224
|
+
console.log('旁边没有 image-local/,使用插件包内自带的 server/。');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const skipDirs = new Set(['.venv', '__pycache__', 'images', 'thumbs', 'refs',
|
|
228
|
+
'repo_images', 'workbench', '.pytest_cache']);
|
|
229
|
+
const skipFiles = new Set(['config.local.json', 'app.db', 'history.db', '.DS_Store']);
|
|
230
|
+
const junk = n => skipFiles.has(n) || /\.(log|pyc|db|vps_original)$/.test(n)
|
|
231
|
+
|| /\.bak_/.test(n);
|
|
232
|
+
// 开发机上的源文件可能是 CRLF,仓库里存的是 LF。不归一的话每次同步
|
|
233
|
+
// 都会把整个文件标成改动,commit 里几千行假 diff,历史没法看。
|
|
234
|
+
const textExt = new Set(['.py', '.html', '.js', '.mjs', '.json', '.txt',
|
|
235
|
+
'.md', '.css', '.yml', '.yaml']);
|
|
236
|
+
const syncFile = (from, to) => {
|
|
237
|
+
if (!textExt.has(path.extname(from).toLowerCase())) {
|
|
238
|
+
fs.copyFileSync(from, to);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const s = fs.readFileSync(from, 'utf8').replace(/\r\n?/g, '\n');
|
|
242
|
+
fs.writeFileSync(to, s, 'utf8');
|
|
243
|
+
};
|
|
244
|
+
let copied = 0;
|
|
245
|
+
const walk = (from, to) => {
|
|
246
|
+
fs.mkdirSync(to, { recursive: true });
|
|
247
|
+
for (const e of fs.readdirSync(from, { withFileTypes: true })) {
|
|
248
|
+
const a = path.join(from, e.name), b = path.join(to, e.name);
|
|
249
|
+
if (e.isDirectory()) { if (!skipDirs.has(e.name)) walk(a, b); continue; }
|
|
250
|
+
if (!e.isFile() || junk(e.name)) continue;
|
|
251
|
+
syncFile(a, b);
|
|
252
|
+
copied += 1;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
walk(src, dst);
|
|
256
|
+
fs.writeFileSync(path.join(dst, 'requirements.txt'),
|
|
257
|
+
'fastapi\nuvicorn\nhttpx\npydantic\npillow\npython-multipart\n', 'utf8');
|
|
258
|
+
console.log('已同步工作台后端代码 -> server/(' + copied + ' 个文件,不含账号/图片/配置)。');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ---------- 样图下载 ---------- */
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 安装时自动下载真实样图(用 Node 直接拉,不依赖 Python 虚拟环境)。
|
|
265
|
+
*
|
|
266
|
+
* 分层来源:
|
|
267
|
+
* 1) CDN 单文件 https://img.handraw-style.com —— 国内国外都通,最小最快
|
|
268
|
+
* 2) Gitee 镜像 raw https://gitee.com/yang02010/handraw-style/raw/master
|
|
269
|
+
* 3) GitHub raw https://raw.githubusercontent.com/yang0/handraw-style/main
|
|
270
|
+
* 失败不阻断安装,但会明确提示;重跑 `node install.mjs` 只补缺失项,已下的自动跳过。
|
|
271
|
+
*/
|
|
272
|
+
async function fetchStyleImages() {
|
|
273
|
+
const repoImages = path.join(PLUGIN_DIR, 'server', 'repo_images');
|
|
274
|
+
fs.mkdirSync(repoImages, { recursive: true });
|
|
275
|
+
|
|
276
|
+
let rels = [];
|
|
277
|
+
const catFile = path.join(PLUGIN_DIR, 'server', 'catalog.json');
|
|
278
|
+
try {
|
|
279
|
+
const cat = JSON.parse(fs.readFileSync(catFile, 'utf8'));
|
|
280
|
+
for (const k of ['styles', 'layouts', 'colors'])
|
|
281
|
+
for (const it of (cat[k] || [])) if (it.img) rels.push(it.img);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
console.warn('读取 catalog.json 失败(' + (e && e.message || e) + '),跳过样图下载。');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
rels = [...new Set(rels)];
|
|
287
|
+
if (!rels.length) { console.log('catalog.json 中没有可下载的样图清单,跳过。'); return; }
|
|
288
|
+
|
|
289
|
+
const total = rels.length;
|
|
290
|
+
const CDN = 'https://img.handraw-style.com';
|
|
291
|
+
const GITEE = 'https://gitee.com/yang02010/handraw-style/raw/master';
|
|
292
|
+
const GITHUB = 'https://raw.githubusercontent.com/yang0/handraw-style/main';
|
|
293
|
+
const sourcesFor = rel => [CDN + '/' + rel, GITEE + '/' + rel, GITHUB + '/' + rel];
|
|
294
|
+
const localOf = rel => path.join(repoImages, rel.replace(/^images\//, ''));
|
|
295
|
+
const exist = p => { try { return fs.statSync(p).size > 0; } catch { return false; } };
|
|
296
|
+
|
|
297
|
+
let done = 0, skip = 0, fail = 0;
|
|
298
|
+
const CONC = 12;
|
|
299
|
+
const getOne = async (rel) => {
|
|
300
|
+
const dst = localOf(rel);
|
|
301
|
+
if (exist(dst)) { skip++; return; }
|
|
302
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
303
|
+
for (const url of sourcesFor(rel)) {
|
|
304
|
+
try {
|
|
305
|
+
const ac = new AbortController();
|
|
306
|
+
const t = setTimeout(() => ac.abort(), 15000);
|
|
307
|
+
const r = await fetch(url, { signal: ac.signal });
|
|
308
|
+
clearTimeout(t);
|
|
309
|
+
if (!r.ok) continue;
|
|
310
|
+
// 只接受真正的图片响应:gitee raw 会 302 跳到登录页(text/html),
|
|
311
|
+
// 若不拦截会把 HTML 写成 .webp 坏文件。
|
|
312
|
+
const ct = (r.headers.get('content-type') || '').toLowerCase();
|
|
313
|
+
if (!ct.startsWith('image/')) continue;
|
|
314
|
+
const buf = Buffer.from(await r.arrayBuffer());
|
|
315
|
+
if (buf.length < 200) continue;
|
|
316
|
+
fs.writeFileSync(dst, buf);
|
|
317
|
+
done++; return;
|
|
318
|
+
} catch { /* 换下一个源 */ }
|
|
319
|
+
}
|
|
320
|
+
fail++;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
console.log('正在自动下载真实样图(风格 / 图型 / 主题色,共 ' + total + ' 张)…');
|
|
324
|
+
for (let i = 0; i < rels.length; i += CONC)
|
|
325
|
+
await Promise.all(rels.slice(i, i + CONC).map(getOne));
|
|
326
|
+
|
|
327
|
+
console.log(`样图下载完成:共 ${total} 张,已下 ${done},跳过(已存在) ${skip},失败 ${fail}。`);
|
|
328
|
+
if (fail)
|
|
329
|
+
console.log(` ${fail} 张失败(多为网络受限)。重跑 \`node install.mjs\` 可补齐,已存在的会自动跳过。`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* ---------- 安装后自检 ---------- */
|
|
333
|
+
|
|
334
|
+
function verify() {
|
|
335
|
+
const problems = [];
|
|
336
|
+
if (!bundles.includes(NAME)) problems.push('bundles 中未包含 ' + NAME);
|
|
337
|
+
if (!deps[NAME]) problems.push('dependencies 中未包含 ' + NAME);
|
|
338
|
+
if (linkState() !== 'ok') problems.push('目录联接不有效:' + LINK_PATH);
|
|
339
|
+
const viaLink = path.join(LINK_PATH, 'host.js');
|
|
340
|
+
if (!fs.existsSync(viaLink)) problems.push('通过目录联接读不到 host.js');
|
|
341
|
+
if (problems.length) {
|
|
342
|
+
console.error('自检未通过:');
|
|
343
|
+
for (const p of problems) console.error(' - ' + p);
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
console.log('自检通过:配置已注册,目录联接有效,插件文件可读。');
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ---------- 主流程 ---------- */
|
|
351
|
+
|
|
352
|
+
async function install() {
|
|
353
|
+
checkAssets();
|
|
354
|
+
syncServer();
|
|
355
|
+
ensureLink(); // 先建联接:失败则 profile 原样,不留半装
|
|
356
|
+
applyConfig(); // 再改配置
|
|
357
|
+
syncLock('link:' + PLUGIN_DIR.replace(/\\/g, '/'));
|
|
358
|
+
const ok = verify();
|
|
359
|
+
if (removedLegacy)
|
|
360
|
+
console.log('已摘除旧独立号池插件 ' + LEGACY + '(源码与账号数据保留,未删除)。');
|
|
361
|
+
await fetchStyleImages();
|
|
362
|
+
if (!ok) {
|
|
363
|
+
console.error('安装未完全成功,请根据上面的提示处理后重跑 node install.mjs。');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
console.log('安装完成。多数情况下 DSH 会直接加载新 bundle;若侧栏未出现入口,完全退出 DSH 再启动一次。');
|
|
367
|
+
console.log('首次打开工作台会自动初始化后端与账号池,通常 1 分钟内就绪。');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!uninstall) {
|
|
371
|
+
install().catch(e => {
|
|
372
|
+
console.error('安装过程中发生异常:' + (e && e.stack || e));
|
|
373
|
+
process.exit(1);
|
|
374
|
+
});
|
|
375
|
+
} else {
|
|
376
|
+
applyConfig();
|
|
377
|
+
syncLock('link:' + PLUGIN_DIR.replace(/\\/g, '/'));
|
|
378
|
+
// 断开联接:只删联接本身,不会碰插件源码目录与账号数据。
|
|
379
|
+
const st = linkState();
|
|
380
|
+
if (st === 'ok' || st === 'dead') {
|
|
381
|
+
console.log(removeLink() ? '已断开目录联接 -> ' + LINK_PATH : '警告:目录联接未能删除,可手动移除。');
|
|
382
|
+
} else if (st === 'dir') {
|
|
383
|
+
console.log('node_modules/' + NAME + ' 是真实目录,非联接,未做改动。');
|
|
384
|
+
}
|
|
385
|
+
console.log('已从 profile 摘除插件;插件源码目录与 ~/.dsh/chatgpt2api-image 数据保留。');
|
|
386
|
+
console.log('完全退出 DSH 再启动,侧栏入口即消失。确认不再需要时可自行删除源码目录。');
|
|
387
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-image-workbench",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.1.5",
|
|
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
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org/",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/guohwa/dsh-plugin-image-workbench.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/guohwa/dsh-plugin-image-workbench#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/guohwa/dsh-plugin-image-workbench/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"dsh",
|
|
53
|
+
"dsh-plugin",
|
|
54
|
+
"image-generation",
|
|
55
|
+
"openai-compatible",
|
|
56
|
+
"gpt-image",
|
|
57
|
+
"workbench"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -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())
|
package/server/access.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""本地版访问控制:单机 127.0.0.1 单人使用,不需要验证码审批。
|
|
2
|
+
|
|
3
|
+
保留与 VPS 版相同的接口签名,让 app.py 一行都不用改就能加载。
|
|
4
|
+
VPS 原件已备份为 access.py.vps_original(其顶层 import fcntl 在 Windows 不存在)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
SESSION_TTL = 7 * 86400
|
|
9
|
+
LOCAL_MODE = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def check_session(token: str) -> bool:
|
|
13
|
+
"""本地版没有会话概念,永远放行。"""
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def request_code(email: str, ip: str, ua: str = "") -> dict:
|
|
18
|
+
return {"ok": True, "msg": "本地版无需申请,请直接返回工作台"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_code(email: str, code: str, ip: str) -> dict:
|
|
22
|
+
return {"ok": True, "msg": "本地版无需验证码", "token": "local"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def master_login(password: str, ip: str) -> dict:
|
|
26
|
+
return {"ok": True, "msg": "本地版无需口令", "token": "local"}
|