@weibaohui/dsh-git-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/client/bundle.js +2356 -0
- package/client/index.js +2342 -0
- package/cordis.patch.yml +11 -0
- package/package.json +70 -0
- package/server/dist/api/v1.js +2029 -0
- package/server/dist/authx/password.js +43 -0
- package/server/dist/authx/um.js +157 -0
- package/server/dist/conf.js +323 -0
- package/server/dist/context.js +701 -0
- package/server/dist/db/actions.js +202 -0
- package/server/dist/db/db.js +838 -0
- package/server/dist/dshapi.js +1416 -0
- package/server/dist/editorconfig.js +104 -0
- package/server/dist/gitx/git.js +580 -0
- package/server/dist/gitx/http.js +232 -0
- package/server/dist/gitx/paths.js +28 -0
- package/server/dist/gitx/service.js +251 -0
- package/server/dist/gosprintf.js +167 -0
- package/server/dist/hook.js +53 -0
- package/server/dist/index.js +112 -0
- package/server/dist/lfsx.js +265 -0
- package/server/dist/mailer.js +127 -0
- package/server/dist/markup.js +317 -0
- package/server/dist/metrics.js +18 -0
- package/server/dist/mirror.js +203 -0
- package/server/dist/multipart.js +33 -0
- package/server/dist/repox.js +99 -0
- package/server/dist/router.js +185 -0
- package/server/dist/serv.js +43 -0
- package/server/dist/server.js +382 -0
- package/server/dist/sshx/dispatch.js +160 -0
- package/server/dist/sshx/lfs-transfer.js +289 -0
- package/server/dist/sshx/server.js +107 -0
- package/server/dist/toolx.js +108 -0
- package/server/dist/twofactor.js +160 -0
- package/server/dist/webapi.js +377 -0
- package/server/dist/webhook.js +128 -0
- package/server/package.json +34 -0
- package/server/public/img/avatar_default.png +0 -0
- package/server/tsconfig.json +19 -0
- package/server/vendored-conf/app.ini +538 -0
- package/server/vendored-conf/gitignore/Actionscript +11 -0
- package/server/vendored-conf/gitignore/Ada +5 -0
- package/server/vendored-conf/gitignore/Agda +1 -0
- package/server/vendored-conf/gitignore/Android +32 -0
- package/server/vendored-conf/gitignore/Anjuta +3 -0
- package/server/vendored-conf/gitignore/AppEngine +2 -0
- package/server/vendored-conf/gitignore/AppceleratorTitanium +3 -0
- package/server/vendored-conf/gitignore/ArchLinuxPackages +13 -0
- package/server/vendored-conf/gitignore/Archives +27 -0
- package/server/vendored-conf/gitignore/Autotools +14 -0
- package/server/vendored-conf/gitignore/BricxCC +4 -0
- package/server/vendored-conf/gitignore/C +32 -0
- package/server/vendored-conf/gitignore/C Sharp +108 -0
- package/server/vendored-conf/gitignore/C++ +28 -0
- package/server/vendored-conf/gitignore/CFWheels +12 -0
- package/server/vendored-conf/gitignore/CMake +6 -0
- package/server/vendored-conf/gitignore/CUDA +6 -0
- package/server/vendored-conf/gitignore/CakePHP +13 -0
- package/server/vendored-conf/gitignore/ChefCookbook +9 -0
- package/server/vendored-conf/gitignore/Cloud9 +3 -0
- package/server/vendored-conf/gitignore/CodeIgniter +6 -0
- package/server/vendored-conf/gitignore/CodeKit +3 -0
- package/server/vendored-conf/gitignore/CommonLisp +3 -0
- package/server/vendored-conf/gitignore/Composer +6 -0
- package/server/vendored-conf/gitignore/Concrete5 +3 -0
- package/server/vendored-conf/gitignore/Coq +3 -0
- package/server/vendored-conf/gitignore/CraftCMS +3 -0
- package/server/vendored-conf/gitignore/DM +5 -0
- package/server/vendored-conf/gitignore/Dart +15 -0
- package/server/vendored-conf/gitignore/DartEditor +2 -0
- package/server/vendored-conf/gitignore/Delphi +57 -0
- package/server/vendored-conf/gitignore/Dreamweaver +3 -0
- package/server/vendored-conf/gitignore/Drupal +36 -0
- package/server/vendored-conf/gitignore/EPiServer +4 -0
- package/server/vendored-conf/gitignore/Eagle +36 -0
- package/server/vendored-conf/gitignore/Eclipse +39 -0
- package/server/vendored-conf/gitignore/EiffelStudio +2 -0
- package/server/vendored-conf/gitignore/Elisp +5 -0
- package/server/vendored-conf/gitignore/Elixir +4 -0
- package/server/vendored-conf/gitignore/Emacs +32 -0
- package/server/vendored-conf/gitignore/Ensime +4 -0
- package/server/vendored-conf/gitignore/Erlang +10 -0
- package/server/vendored-conf/gitignore/Espresso +1 -0
- package/server/vendored-conf/gitignore/ExpressionEngine +19 -0
- package/server/vendored-conf/gitignore/ExtJs +4 -0
- package/server/vendored-conf/gitignore/Fancy +2 -0
- package/server/vendored-conf/gitignore/Finale +13 -0
- package/server/vendored-conf/gitignore/FlexBuilder +3 -0
- package/server/vendored-conf/gitignore/ForceDotCom +4 -0
- package/server/vendored-conf/gitignore/FuelPHP +2 -0
- package/server/vendored-conf/gitignore/GWT +28 -0
- package/server/vendored-conf/gitignore/Gcov +5 -0
- package/server/vendored-conf/gitignore/GitBook +16 -0
- package/server/vendored-conf/gitignore/Go +24 -0
- package/server/vendored-conf/gitignore/Gradle +8 -0
- package/server/vendored-conf/gitignore/Grails +33 -0
- package/server/vendored-conf/gitignore/Haskell +16 -0
- package/server/vendored-conf/gitignore/IGORPro +5 -0
- package/server/vendored-conf/gitignore/IPythonNotebook +2 -0
- package/server/vendored-conf/gitignore/Idris +2 -0
- package/server/vendored-conf/gitignore/JDeveloper +13 -0
- package/server/vendored-conf/gitignore/Java +12 -0
- package/server/vendored-conf/gitignore/Jboss +19 -0
- package/server/vendored-conf/gitignore/Jekyll +3 -0
- package/server/vendored-conf/gitignore/JetBrains +46 -0
- package/server/vendored-conf/gitignore/Joomla +525 -0
- package/server/vendored-conf/gitignore/KDevelop4 +2 -0
- package/server/vendored-conf/gitignore/Kate +3 -0
- package/server/vendored-conf/gitignore/KiCAD +13 -0
- package/server/vendored-conf/gitignore/Kohana +2 -0
- package/server/vendored-conf/gitignore/LabVIEW +16 -0
- package/server/vendored-conf/gitignore/Laravel +4 -0
- package/server/vendored-conf/gitignore/Lazarus +30 -0
- package/server/vendored-conf/gitignore/Leiningen +12 -0
- package/server/vendored-conf/gitignore/LemonStand +21 -0
- package/server/vendored-conf/gitignore/LibreOffice +2 -0
- package/server/vendored-conf/gitignore/Lilypond +6 -0
- package/server/vendored-conf/gitignore/Linux +7 -0
- package/server/vendored-conf/gitignore/Lithium +2 -0
- package/server/vendored-conf/gitignore/Lua +41 -0
- package/server/vendored-conf/gitignore/LyX +4 -0
- package/server/vendored-conf/gitignore/Magento +126 -0
- package/server/vendored-conf/gitignore/Matlab +16 -0
- package/server/vendored-conf/gitignore/Maven +9 -0
- package/server/vendored-conf/gitignore/Mercurial +6 -0
- package/server/vendored-conf/gitignore/Mercury +13 -0
- package/server/vendored-conf/gitignore/MetaProgrammingSystem +16 -0
- package/server/vendored-conf/gitignore/MicrosoftOffice +10 -0
- package/server/vendored-conf/gitignore/ModelSim +23 -0
- package/server/vendored-conf/gitignore/Momentics +8 -0
- package/server/vendored-conf/gitignore/MonoDevelop +8 -0
- package/server/vendored-conf/gitignore/Nanoc +10 -0
- package/server/vendored-conf/gitignore/NetBeans +8 -0
- package/server/vendored-conf/gitignore/Nim +1 -0
- package/server/vendored-conf/gitignore/Ninja +2 -0
- package/server/vendored-conf/gitignore/Node +28 -0
- package/server/vendored-conf/gitignore/NotepadPP +2 -0
- package/server/vendored-conf/gitignore/OCaml +20 -0
- package/server/vendored-conf/gitignore/Objective-C +43 -0
- package/server/vendored-conf/gitignore/Opa +13 -0
- package/server/vendored-conf/gitignore/OpenCart +11 -0
- package/server/vendored-conf/gitignore/OracleForms +8 -0
- package/server/vendored-conf/gitignore/Packer +5 -0
- package/server/vendored-conf/gitignore/Perl +20 -0
- package/server/vendored-conf/gitignore/Phalcon +2 -0
- package/server/vendored-conf/gitignore/PhpStorm +63 -0
- package/server/vendored-conf/gitignore/PlayFramework +17 -0
- package/server/vendored-conf/gitignore/Plone +18 -0
- package/server/vendored-conf/gitignore/Prestashop +25 -0
- package/server/vendored-conf/gitignore/Processing +7 -0
- package/server/vendored-conf/gitignore/Python +58 -0
- package/server/vendored-conf/gitignore/Qooxdoo +5 -0
- package/server/vendored-conf/gitignore/Qt +34 -0
- package/server/vendored-conf/gitignore/R +16 -0
- package/server/vendored-conf/gitignore/ROS +47 -0
- package/server/vendored-conf/gitignore/Rails +35 -0
- package/server/vendored-conf/gitignore/Redcar +1 -0
- package/server/vendored-conf/gitignore/Redis +3 -0
- package/server/vendored-conf/gitignore/RhodesRhomobile +9 -0
- package/server/vendored-conf/gitignore/Ruby +36 -0
- package/server/vendored-conf/gitignore/Rust +11 -0
- package/server/vendored-conf/gitignore/SBT +9 -0
- package/server/vendored-conf/gitignore/SCons +2 -0
- package/server/vendored-conf/gitignore/SVN +1 -0
- package/server/vendored-conf/gitignore/Sass +2 -0
- package/server/vendored-conf/gitignore/Scala +17 -0
- package/server/vendored-conf/gitignore/Scrivener +7 -0
- package/server/vendored-conf/gitignore/Sdcc +8 -0
- package/server/vendored-conf/gitignore/SeamGen +26 -0
- package/server/vendored-conf/gitignore/SketchUp +1 -0
- package/server/vendored-conf/gitignore/SlickEdit +11 -0
- package/server/vendored-conf/gitignore/Stella +12 -0
- package/server/vendored-conf/gitignore/SublimeText +14 -0
- package/server/vendored-conf/gitignore/SugarCRM +25 -0
- package/server/vendored-conf/gitignore/Swift +43 -0
- package/server/vendored-conf/gitignore/Symfony +37 -0
- package/server/vendored-conf/gitignore/SymphonyCMS +6 -0
- package/server/vendored-conf/gitignore/SynopsysVCS +36 -0
- package/server/vendored-conf/gitignore/Tags +14 -0
- package/server/vendored-conf/gitignore/TeX +136 -0
- package/server/vendored-conf/gitignore/TextMate +3 -0
- package/server/vendored-conf/gitignore/Textpattern +11 -0
- package/server/vendored-conf/gitignore/TortoiseGit +2 -0
- package/server/vendored-conf/gitignore/TurboGears2 +20 -0
- package/server/vendored-conf/gitignore/Typo3 +20 -0
- package/server/vendored-conf/gitignore/Umbraco +17 -0
- package/server/vendored-conf/gitignore/Unity +21 -0
- package/server/vendored-conf/gitignore/UnrealEngine +76 -0
- package/server/vendored-conf/gitignore/VVVV +6 -0
- package/server/vendored-conf/gitignore/Vagrant +1 -0
- package/server/vendored-conf/gitignore/Vim +6 -0
- package/server/vendored-conf/gitignore/VirtualEnv +10 -0
- package/server/vendored-conf/gitignore/VisualStudio +217 -0
- package/server/vendored-conf/gitignore/VisualStudioCode +2 -0
- package/server/vendored-conf/gitignore/Waf +3 -0
- package/server/vendored-conf/gitignore/WebMethods +14 -0
- package/server/vendored-conf/gitignore/WebStorm +63 -0
- package/server/vendored-conf/gitignore/Windows +18 -0
- package/server/vendored-conf/gitignore/WordPress +18 -0
- package/server/vendored-conf/gitignore/Xcode +23 -0
- package/server/vendored-conf/gitignore/XilinxISE +57 -0
- package/server/vendored-conf/gitignore/Xojo +11 -0
- package/server/vendored-conf/gitignore/Yeoman +6 -0
- package/server/vendored-conf/gitignore/Yii +6 -0
- package/server/vendored-conf/gitignore/ZendFramework +20 -0
- package/server/vendored-conf/gitignore/Zephir +26 -0
- package/server/vendored-conf/gitignore/macOS +25 -0
- package/server/vendored-conf/label/Default +7 -0
- package/server/vendored-conf/license/Abstyles License +10 -0
- package/server/vendored-conf/license/Academic Free License v1.1 +22 -0
- package/server/vendored-conf/license/Academic Free License v1.2 +27 -0
- package/server/vendored-conf/license/Academic Free License v2.0 +43 -0
- package/server/vendored-conf/license/Academic Free License v2.1 +44 -0
- package/server/vendored-conf/license/Academic Free License v3.0 +42 -0
- package/server/vendored-conf/license/Affero General Public License v1.0 +84 -0
- package/server/vendored-conf/license/Apache License 1.0 +19 -0
- package/server/vendored-conf/license/Apache License 1.1 +20 -0
- package/server/vendored-conf/license/Apache License 2.0 +72 -0
- package/server/vendored-conf/license/Artistic License 1.0 +48 -0
- package/server/vendored-conf/license/Artistic License 2.0 +84 -0
- package/server/vendored-conf/license/BSD 2-clause License +8 -0
- package/server/vendored-conf/license/BSD 3-clause License +10 -0
- package/server/vendored-conf/license/BSD 4-clause License +13 -0
- package/server/vendored-conf/license/Creative Commons CC0 1.0 Universal +39 -0
- package/server/vendored-conf/license/Eclipse Public License 1.0 +72 -0
- package/server/vendored-conf/license/Educational Community License v1.0 +22 -0
- package/server/vendored-conf/license/Educational Community License v2.0 +97 -0
- package/server/vendored-conf/license/GNU Affero General Public License v3.0 +241 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.1 +118 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.2 +129 -0
- package/server/vendored-conf/license/GNU Free Documentation License v1.3 +148 -0
- package/server/vendored-conf/license/GNU General Public License v1.0 +99 -0
- package/server/vendored-conf/license/GNU General Public License v2.0 +116 -0
- package/server/vendored-conf/license/GNU General Public License v3.0 +231 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v2.1 +174 -0
- package/server/vendored-conf/license/GNU Lesser General Public License v3.0 +70 -0
- package/server/vendored-conf/license/GNU Library General Public License v2.0 +173 -0
- package/server/vendored-conf/license/ISC license +6 -0
- package/server/vendored-conf/license/MIT License +8 -0
- package/server/vendored-conf/license/Mozilla Public License 1.0 +122 -0
- package/server/vendored-conf/license/Mozilla Public License 1.1 +142 -0
- package/server/vendored-conf/license/Mozilla Public License 2.0 +143 -0
- package/server/vendored-conf/readme/Default +3 -0
- package/src/dsh-host.js +168 -0
- package/src/index.js +682 -0
- package/src/runner.js +364 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @weibaohui/dsh-git-server — Host half(cordis 宿主接线)。
|
|
5
|
+
*
|
|
6
|
+
* 四件事:
|
|
7
|
+
* 1. settings 服务注册 schema(enabled/host/port/dataDir/authMode,3s 内
|
|
8
|
+
* 热生效——reconciler 指纹比对,变化才重启子进程);
|
|
9
|
+
* 2. 内嵌 ts-gogs 子进程生命周期(src/runner.js:写 app.ini → spawn →
|
|
10
|
+
* 就绪探测 → 崩溃自动拉起);
|
|
11
|
+
* 3. 同源路由 /dsh-git-server/api/*(status/settings)给宿主设置页用——
|
|
12
|
+
* 认证由宿主 web 门禁负责,此处不重复做;
|
|
13
|
+
* 4. 管理员密码管理:首次启动自动生成并持久化(settings 优先,token 文件
|
|
14
|
+
* 兜底),设置页可见可轮换。
|
|
15
|
+
*
|
|
16
|
+
* authMode:
|
|
17
|
+
* - gogs(默认):ts-gogs 自己的账号/令牌体系(管理员密码在状态里展示);
|
|
18
|
+
* - user-management:git HTTP Basic 与网页登录接受 user-management 的
|
|
19
|
+
* 用户名密码(映射到本库管理员),用户库缺失自动回退 gogs 模式。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs')
|
|
23
|
+
const path = require('node:path')
|
|
24
|
+
|
|
25
|
+
const engine = require('./runner')
|
|
26
|
+
|
|
27
|
+
const name = engine.PLUGIN_ID
|
|
28
|
+
const inject = ['webServer', 'settings']
|
|
29
|
+
const API_PREFIX = '/' + engine.PLUGIN_ID + '/api'
|
|
30
|
+
const SETTINGS_NS = engine.PLUGIN_ID
|
|
31
|
+
|
|
32
|
+
// ── schemastery 加载(settings 服务同源;缺席时仅 loader config 生效)──────
|
|
33
|
+
|
|
34
|
+
const { pathToFileURL } = require('node:url')
|
|
35
|
+
const os = require('node:os')
|
|
36
|
+
|
|
37
|
+
let schemaPromise = null
|
|
38
|
+
|
|
39
|
+
function hostCandidatePaths(pkgName, rel) {
|
|
40
|
+
const prefixes = [process.env.DSH_GLOBAL_PREFIX, os.homedir() + '/.local'].filter(Boolean)
|
|
41
|
+
return prefixes.map((prefix) =>
|
|
42
|
+
path.join(prefix, 'lib', 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', pkgName, rel),
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function startSchemaLoader() {
|
|
47
|
+
if (!schemaPromise) {
|
|
48
|
+
schemaPromise = (async () => {
|
|
49
|
+
const candidates = [
|
|
50
|
+
...hostCandidatePaths('schemastery', 'lib/index.cjs'),
|
|
51
|
+
'@deepseek-ai/schemastery',
|
|
52
|
+
]
|
|
53
|
+
const errors = []
|
|
54
|
+
for (const target of candidates) {
|
|
55
|
+
try {
|
|
56
|
+
const specifier = target.includes('/') && !target.startsWith('@') && target.includes('node_modules')
|
|
57
|
+
? pathToFileURL(target).href
|
|
58
|
+
: target
|
|
59
|
+
const mod = await import(specifier)
|
|
60
|
+
return mod
|
|
61
|
+
} catch (e) {
|
|
62
|
+
errors.push(`${target}: ${String((e && e.message) || e).slice(0, 160)}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new Error(errors.join(' | '))
|
|
66
|
+
})()
|
|
67
|
+
}
|
|
68
|
+
return schemaPromise
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function resolveSchema() {
|
|
72
|
+
try {
|
|
73
|
+
const mod = await startSchemaLoader()
|
|
74
|
+
if (!mod) return null
|
|
75
|
+
const Schema = mod.default || mod.Schema || (typeof mod === 'function' ? mod : null)
|
|
76
|
+
return Schema && typeof Schema.object === 'function' ? Schema : null
|
|
77
|
+
} catch {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function __seedSchema(Schema) {
|
|
83
|
+
schemaPromise = Promise.resolve(Schema ? { default: Schema } : null)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function settingsSchema(Schema) {
|
|
87
|
+
if (!Schema || typeof Schema.object !== 'function') return null
|
|
88
|
+
return Schema.object({
|
|
89
|
+
enabled: Schema.boolean().default(engine.DEFAULTS.enabled),
|
|
90
|
+
port: Schema.number().step(1).min(engine.NUM_RANGES.port[0]).max(engine.NUM_RANGES.port[1]).default(engine.DEFAULTS.port),
|
|
91
|
+
dataDir: Schema.string().default(engine.DEFAULTS.dataDir),
|
|
92
|
+
adminPassword: Schema.string().default(engine.DEFAULTS.adminPassword),
|
|
93
|
+
impersonateSecret: Schema.string().default(engine.DEFAULTS.impersonateSecret),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── 插件 ─────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
name,
|
|
101
|
+
inject,
|
|
102
|
+
version: '0.1.0',
|
|
103
|
+
__internals: { settingsSchema, resolveSchema, __seedSchema, API_PREFIX, SETTINGS_NS },
|
|
104
|
+
|
|
105
|
+
apply(ctx, config) {
|
|
106
|
+
// 宿主可能过滤插件 logger 输出;console.error 走 stderr 保底可见(launchd 下进 err.log)
|
|
107
|
+
const hostLogger = ctx.logger && typeof ctx.logger.warn === 'function' ? ctx.logger : null
|
|
108
|
+
const logger = {
|
|
109
|
+
info(m) { try { hostLogger && hostLogger.info && hostLogger.info(m) } catch {} console.error(m) },
|
|
110
|
+
warn(m) { try { hostLogger && hostLogger.warn && hostLogger.warn(m) } catch {} console.error(m) },
|
|
111
|
+
error(m) { try { hostLogger && hostLogger.error && hostLogger.error(m) } catch {} console.error(m) },
|
|
112
|
+
}
|
|
113
|
+
const webServer = ctx.webServer
|
|
114
|
+
|
|
115
|
+
const base = engine.normalizeConfig({ ...engine.DEFAULTS, ...(config || {}) })
|
|
116
|
+
let settingsScope = null
|
|
117
|
+
let memoryPatch = {}
|
|
118
|
+
let pwFileCache = null
|
|
119
|
+
|
|
120
|
+
const state = { handle: null, fingerprint: '', error: null, restarting: 0, deps: 'ok' }
|
|
121
|
+
|
|
122
|
+
const warnThrottledAt = new Map()
|
|
123
|
+
function warnThrottled(key, line, ttlMs = 60000) {
|
|
124
|
+
const t = Date.now()
|
|
125
|
+
if (t - (warnThrottledAt.get(key) || 0) < ttlMs) return
|
|
126
|
+
warnThrottledAt.set(key, t)
|
|
127
|
+
logger.warn(`dsh-git-server: ${line}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function passwordFile() {
|
|
131
|
+
return path.join(engine.resolveDataDir(base.dataDir || ''), 'custom', 'admin-password.txt')
|
|
132
|
+
}
|
|
133
|
+
function readPasswordFile() {
|
|
134
|
+
if (pwFileCache !== null) return pwFileCache
|
|
135
|
+
try { pwFileCache = fs.readFileSync(passwordFile(), 'utf8').trim() } catch { pwFileCache = '' }
|
|
136
|
+
return pwFileCache
|
|
137
|
+
}
|
|
138
|
+
function writePasswordFile(pw) {
|
|
139
|
+
try {
|
|
140
|
+
fs.mkdirSync(path.dirname(passwordFile()), { recursive: true })
|
|
141
|
+
fs.writeFileSync(passwordFile(), pw + '\n', { mode: 0o600 })
|
|
142
|
+
pwFileCache = pw
|
|
143
|
+
} catch (e) {
|
|
144
|
+
logger.warn(`dsh-git-server: 管理员密码文件写入失败: ${(e && e.message) || e}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 确保管理员密码存在:settings → 密码文件(已有的先采纳)→ 自动生成。 */
|
|
149
|
+
async function ensureAdminPassword(cfg) {
|
|
150
|
+
if (cfg.adminPassword) return
|
|
151
|
+
const fromFile = readPasswordFile()
|
|
152
|
+
if (fromFile) {
|
|
153
|
+
memoryPatch.adminPassword = fromFile
|
|
154
|
+
cfg.adminPassword = fromFile
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const pw = engine.generateSecret().slice(0, 20)
|
|
158
|
+
if (settingsScope && typeof settingsScope.update === 'function') {
|
|
159
|
+
try {
|
|
160
|
+
await settingsScope.update({ adminPassword: pw })
|
|
161
|
+
} catch (e) {
|
|
162
|
+
logger.warn(`dsh-git-server: 管理员密码写入 settings 失败,退回文件: ${(e && e.message) || e}`)
|
|
163
|
+
memoryPatch.adminPassword = pw
|
|
164
|
+
writePasswordFile(pw)
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
memoryPatch.adminPassword = pw
|
|
168
|
+
writePasswordFile(pw)
|
|
169
|
+
}
|
|
170
|
+
cfg.adminPassword = pw
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 用户桥(宿主侧全程无内核 HTTP):dsh 会话用户名 → 影子建号(宿主读
|
|
174
|
+
// UM users.json + 写共享 gogs.db)→ 铸个人令牌(直插 access_token 表)。
|
|
175
|
+
// 内核不再为此服务 HTTP;只保留 git HTTP/LFS 的 basic 认证。
|
|
176
|
+
const userTokens = new Map() // username → { pw, token }(子进程重启由 reconcile 清空)
|
|
177
|
+
async function kernelTokenFor(cfg, username) {
|
|
178
|
+
const hit = userTokens.get(username)
|
|
179
|
+
if (hit && hit.pw === cfg.adminPassword) return hit.token
|
|
180
|
+
const { ensureShadowHost, mintTokenHost } = require('./dsh-host')
|
|
181
|
+
await ensureShadowHost(cfg, username)
|
|
182
|
+
const token = await mintTokenHost(cfg, username)
|
|
183
|
+
userTokens.set(username, { pw: cfg.adminPassword, token })
|
|
184
|
+
return token
|
|
185
|
+
}
|
|
186
|
+
async function kernelApi(cfg, username, method, path, body) {
|
|
187
|
+
const token = await kernelTokenFor(cfg, username)
|
|
188
|
+
const headers = { Authorization: `token ${token}` }
|
|
189
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
|
190
|
+
const res = await fetch(`http://127.0.0.1:${cfg.port}/api/v1${path}`, {
|
|
191
|
+
method, headers, body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
192
|
+
})
|
|
193
|
+
const text = await res.text()
|
|
194
|
+
let json = null
|
|
195
|
+
try { json = JSON.parse(text) } catch {}
|
|
196
|
+
return { status: res.status, json }
|
|
197
|
+
}
|
|
198
|
+
function umUsernameOf(req) {
|
|
199
|
+
// 网关链路:um_session 由网关代理以 x-um-session 头转发(cookie 被其
|
|
200
|
+
// 替换为 dsh-auth);直连链路:cookie 里带 um_session。两者都查。
|
|
201
|
+
const hdr = req.headers['x-um-session']
|
|
202
|
+
const m = hdr ? [null, String(hdr)] : String(req.headers.cookie || '').match(/um_session=([^;]+)/)
|
|
203
|
+
if (!m) return null
|
|
204
|
+
return umUsernameForSession(decodeURIComponent(m[1]))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── ts-gogs 管理桥:用 root:adminPassword 铸 token,代理仓库管理 ──────
|
|
208
|
+
let adminTokenCache = null
|
|
209
|
+
async function adminToken(cfg) {
|
|
210
|
+
if (adminTokenCache && adminTokenCache.pw === cfg.adminPassword) return adminTokenCache.token
|
|
211
|
+
const auth = 'Basic ' + Buffer.from(`root:${cfg.adminPassword}`).toString('base64')
|
|
212
|
+
const res = await fetch(`http://127.0.0.1:${cfg.port}/api/v1/users/root/tokens`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: { 'Content-Type': 'application/json', Authorization: auth },
|
|
215
|
+
body: JSON.stringify({ name: 'dsh-settings-' + Date.now().toString(36) }),
|
|
216
|
+
})
|
|
217
|
+
if (res.status !== 200 && res.status !== 201) throw new Error(`铸管理令牌失败: ${res.status}`)
|
|
218
|
+
const token = ((await res.json()) || {}).sha1
|
|
219
|
+
adminTokenCache = { pw: cfg.adminPassword, token }
|
|
220
|
+
return token
|
|
221
|
+
}
|
|
222
|
+
async function gogsApi(cfg, method, path, body) {
|
|
223
|
+
const token = await adminToken(cfg)
|
|
224
|
+
const headers = { Authorization: `token ${token}` }
|
|
225
|
+
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
|
226
|
+
const res = await fetch(`http://127.0.0.1:${cfg.port}/api/v1${path}`, {
|
|
227
|
+
method, headers, body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
228
|
+
})
|
|
229
|
+
const text = await res.text()
|
|
230
|
+
let json = null
|
|
231
|
+
try { json = JSON.parse(text) } catch {}
|
|
232
|
+
return { status: res.status, json, text }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function impersonateFile() {
|
|
236
|
+
return path.join(engine.resolveDataDir(base.dataDir || ''), 'custom', 'impersonate-secret.txt')
|
|
237
|
+
}
|
|
238
|
+
function readImpersonateFile() {
|
|
239
|
+
try { return fs.readFileSync(impersonateFile(), 'utf8').trim() } catch { return '' }
|
|
240
|
+
}
|
|
241
|
+
function writeImpersonateFile(v) {
|
|
242
|
+
try {
|
|
243
|
+
fs.mkdirSync(path.dirname(impersonateFile()), { recursive: true })
|
|
244
|
+
fs.writeFileSync(impersonateFile(), v + '\n', { mode: 0o600 })
|
|
245
|
+
} catch (e) {
|
|
246
|
+
logger.warn(`dsh-git-server: impersonate secret 文件写入失败: ${(e && e.message) || e}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function ensureImpersonateSecret(cfg) {
|
|
250
|
+
if (cfg.impersonateSecret) return
|
|
251
|
+
const fromFile = readImpersonateFile()
|
|
252
|
+
const pw = fromFile || engine.generateSecret()
|
|
253
|
+
memoryPatch.impersonateSecret = pw
|
|
254
|
+
cfg.impersonateSecret = pw
|
|
255
|
+
if (fromFile) return
|
|
256
|
+
if (settingsScope && typeof settingsScope.update === 'function') {
|
|
257
|
+
try { await settingsScope.update({ impersonateSecret: pw }) } catch (e) {
|
|
258
|
+
logger.warn(`dsh-git-server: impersonate secret 写入 settings 失败,退回文件: ${(e && e.message) || e}`)
|
|
259
|
+
writeImpersonateFile(pw)
|
|
260
|
+
}
|
|
261
|
+
} else writeImpersonateFile(pw)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// dsh 会话(um_session cookie)→ user-management 用户名
|
|
265
|
+
let umSessionsCache = null
|
|
266
|
+
let umSessionsStamp = null
|
|
267
|
+
function umUsernameForSession(token) {
|
|
268
|
+
const file = path.join(engine.dshHome(), 'user-management', 'sessions.json')
|
|
269
|
+
let stamp = null
|
|
270
|
+
try { const st = fs.statSync(file); stamp = `${st.mtimeMs}:${st.size}` } catch { return null }
|
|
271
|
+
if (stamp !== umSessionsStamp || !umSessionsCache) {
|
|
272
|
+
try {
|
|
273
|
+
umSessionsCache = JSON.parse(fs.readFileSync(file, 'utf8')).tokens || {}
|
|
274
|
+
umSessionsStamp = stamp
|
|
275
|
+
} catch { umSessionsCache = {} }
|
|
276
|
+
}
|
|
277
|
+
const rec = umSessionsCache[token]
|
|
278
|
+
if (!rec || (rec.expiresAt && rec.expiresAt < Date.now())) return null
|
|
279
|
+
return rec.username || null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function umAvailability(cfg) {
|
|
283
|
+
const mod = engine.locateUmStore()
|
|
284
|
+
if (!mod) return 'missing'
|
|
285
|
+
try { fs.statSync(mod); return 'ok' } catch { return 'missing' }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function status() {
|
|
289
|
+
const cfg = engine.normalizeConfig(effective())
|
|
290
|
+
const lan = require('node:os').networkInterfaces()
|
|
291
|
+
let lanIp = null
|
|
292
|
+
for (const list of Object.values(lan)) {
|
|
293
|
+
for (const ni of list || []) {
|
|
294
|
+
if (ni.family === 'IPv4' && !ni.internal) { lanIp = ni.address; break }
|
|
295
|
+
}
|
|
296
|
+
if (lanIp) break
|
|
297
|
+
}
|
|
298
|
+
const openToLan = cfg.host === '0.0.0.0' || cfg.host === '::'
|
|
299
|
+
const urlHost = openToLan ? (lanIp || '127.0.0.1') : cfg.host
|
|
300
|
+
return {
|
|
301
|
+
ok: true,
|
|
302
|
+
running: Boolean(state.handle),
|
|
303
|
+
pid: state.handle ? state.handle.pid : null,
|
|
304
|
+
enabled: cfg.enabled,
|
|
305
|
+
error: state.error,
|
|
306
|
+
host: cfg.host,
|
|
307
|
+
port: cfg.port,
|
|
308
|
+
urlLocal: `http://127.0.0.1:${cfg.port}/`,
|
|
309
|
+
urlLan: openToLan && lanIp ? `http://${lanIp}:${cfg.port}/` : null,
|
|
310
|
+
urlDisplay: `http://${urlHost}:${cfg.port}/`,
|
|
311
|
+
dataDir: cfg.dataDir,
|
|
312
|
+
umAvailable: umAvailability(cfg),
|
|
313
|
+
adminUser: 'root',
|
|
314
|
+
adminPassword: cfg.adminPassword || readPasswordFile(),
|
|
315
|
+
crashes: state.restarting,
|
|
316
|
+
deps: state.deps,
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function effective() {
|
|
321
|
+
const fromSettings = settingsScope && typeof settingsScope.get === 'function'
|
|
322
|
+
? settingsScope.get()
|
|
323
|
+
: null
|
|
324
|
+
return engine.normalizeConfig({ ...base, ...(fromSettings || {}), ...memoryPatch })
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let restartTimer = null
|
|
328
|
+
/** 配置协调器:指纹变化才重启子进程;enabled=false 则停机。 */
|
|
329
|
+
async function reconcile() {
|
|
330
|
+
const cfg = engine.normalizeConfig(effective())
|
|
331
|
+
if (state.retryAt && Date.now() < state.retryAt) return status()
|
|
332
|
+
await ensureAdminPassword(cfg)
|
|
333
|
+
await ensureImpersonateSecret(cfg)
|
|
334
|
+
const fp = engine.serverFingerprint(cfg)
|
|
335
|
+
if (fp === state.fingerprint) return status()
|
|
336
|
+
|
|
337
|
+
if (!cfg.enabled) {
|
|
338
|
+
if (state.handle) { await state.handle.stop(); state.handle = null }
|
|
339
|
+
state.fingerprint = fp
|
|
340
|
+
state.error = null
|
|
341
|
+
logger.info('dsh-git-server: 已停用,git 服务器子进程关闭')
|
|
342
|
+
return status()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
state.fingerprint = fp // 先记账防并发重建;失败时错误落在 state.error
|
|
346
|
+
userTokens.clear()
|
|
347
|
+
if (state.handle) { await state.handle.stop(); state.handle = null }
|
|
348
|
+
try {
|
|
349
|
+
// 依赖自检/自动安装的日志始终可见;子进程自身输出仅在 verbose 模式转发
|
|
350
|
+
await engine.ensureDeps({ logger: (line) => logger.info(`dsh-git-server: ${line}`) })
|
|
351
|
+
state.deps = 'ok'
|
|
352
|
+
const handle = await engine.start(cfg, {
|
|
353
|
+
logger: (line) => { if (process.env.DSH_GIT_SERVER_VERBOSE) logger.info(`dsh-git-server: ${line}`) },
|
|
354
|
+
})
|
|
355
|
+
handle.onExit(({ code, stderrTail }) => {
|
|
356
|
+
// 意外退出:清指纹让 reconciler 的下一跳(3s)自动拉起
|
|
357
|
+
if (state.handle === handle) state.handle = null
|
|
358
|
+
state.fingerprint = ''
|
|
359
|
+
state.restarting++
|
|
360
|
+
logger.warn(`dsh-git-server: ts-gogs 意外退出 code=${code},3s 后自动拉起\n${(stderrTail || '').slice(-400)}`)
|
|
361
|
+
})
|
|
362
|
+
state.handle = handle
|
|
363
|
+
state.error = null
|
|
364
|
+
logger.info(
|
|
365
|
+
`dsh-git-server: Git 服务器 http://${cfg.host}:${cfg.port}/ ` +
|
|
366
|
+
`auth=${cfg.authMode === 'user-management' ? 'user-management' : 'gogs'} dataDir=${cfg.dataDir}`,
|
|
367
|
+
)
|
|
368
|
+
} catch (e) {
|
|
369
|
+
const msg = String((e && e.message) || e)
|
|
370
|
+
// 失败后清指纹 + 30s 退避,让心跳自动重试(in-flight 去重防并发装)
|
|
371
|
+
state.handle = null
|
|
372
|
+
state.fingerprint = ''
|
|
373
|
+
state.retryAt = Date.now() + 30000
|
|
374
|
+
if (/依赖自动安装/.test(msg)) {
|
|
375
|
+
state.deps = 'failed'
|
|
376
|
+
state.error = msg
|
|
377
|
+
logger.warn('dsh-git-server: ' + msg + '(30s 后自动重试)')
|
|
378
|
+
} else {
|
|
379
|
+
state.deps = 'ok'
|
|
380
|
+
state.error = msg
|
|
381
|
+
logger.error('dsh-git-server: ' + msg)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return status()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── settings 注册(异步;缺席不阻塞宿主半其余功能)───────────────────
|
|
388
|
+
;(async () => {
|
|
389
|
+
const Schema = await resolveSchema()
|
|
390
|
+
if (Schema && ctx.settings && typeof ctx.settings.register === 'function') {
|
|
391
|
+
try {
|
|
392
|
+
settingsScope = ctx.settings.register(SETTINGS_NS, settingsSchema(Schema), { base })
|
|
393
|
+
logger.info('dsh-git-server: settings-registered')
|
|
394
|
+
} catch (e) {
|
|
395
|
+
logger.warn(`dsh-git-server: settings register 失败(仅 loader config 生效): ${(e && e.message) || e}`)
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
logger.warn(
|
|
399
|
+
`dsh-git-server: settings 不可用(Schema=${Boolean(Schema)}, ctx.settings=${Boolean(ctx.settings)})——配置退回进程内兜底`,
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
await reconcile()
|
|
403
|
+
})().catch((e) => logger.error(`dsh-git-server: init: ${(e && e.message) || e}`))
|
|
404
|
+
|
|
405
|
+
// ── 同源 API:status / settings ─────────────────────────────────────
|
|
406
|
+
function repoCard(x, actor) {
|
|
407
|
+
return {
|
|
408
|
+
id: x.id, name: x.name, owner: (x.owner && (x.owner.login || x.owner.username)) || '',
|
|
409
|
+
fullName: x.full_name, private: !!x.private,
|
|
410
|
+
stars: x.stars_count || 0, issues: x.open_issues_count || 0, forks: x.forks_count || 0,
|
|
411
|
+
description: x.description || '',
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function errText(r) {
|
|
415
|
+
const j = r.json
|
|
416
|
+
if (j && j.message) return String(j.message)
|
|
417
|
+
if (Array.isArray(j) && j[0] && j[0].message) return j[0].message
|
|
418
|
+
return 'kernel ' + r.status
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function sendJson(res, statusCode, payloadOut) {
|
|
422
|
+
const body = JSON.stringify(payloadOut)
|
|
423
|
+
res.writeHead(statusCode, {
|
|
424
|
+
'content-type': 'application/json; charset=utf-8',
|
|
425
|
+
'content-length': Buffer.byteLength(body),
|
|
426
|
+
'cache-control': 'no-store',
|
|
427
|
+
})
|
|
428
|
+
res.end(body)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function readBody(req) {
|
|
432
|
+
return new Promise((resolve, reject) => {
|
|
433
|
+
const chunks = []
|
|
434
|
+
let size = 0
|
|
435
|
+
req.on('data', (c) => {
|
|
436
|
+
size += c.length
|
|
437
|
+
if (size > 64 * 1024) {
|
|
438
|
+
reject(new Error('body too large'))
|
|
439
|
+
req.destroy()
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
chunks.push(c)
|
|
443
|
+
})
|
|
444
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
445
|
+
req.on('error', reject)
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (webServer && typeof webServer.register === 'function') {
|
|
450
|
+
ctx.effect(() => {
|
|
451
|
+
webServer.register({
|
|
452
|
+
kind: 'prefix',
|
|
453
|
+
path: API_PREFIX,
|
|
454
|
+
handler: async (req, res) => {
|
|
455
|
+
const url = new URL(req.url || '/', 'http://dsh.local')
|
|
456
|
+
const rest = url.pathname.slice(API_PREFIX.length)
|
|
457
|
+
try {
|
|
458
|
+
if (req.method === 'GET' && (rest === '' || rest === '/' || rest === '/status')) {
|
|
459
|
+
sendJson(res, 200, status())
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
const cfg = engine.normalizeConfig(effective())
|
|
463
|
+
const actor = umUsernameOf(req) // dsh 会话 → 操作者本人
|
|
464
|
+
if (!actor) { sendJson(res, 401, { ok: false, error: '需要 dsh 登录' }); return }
|
|
465
|
+
const parts = rest.split('/').filter(Boolean) // dsh|repos,o,r,what...
|
|
466
|
+
|
|
467
|
+
// 未启用/未运行时,数据面路由返回清晰提示(而不是去 fetch 内核撞
|
|
468
|
+
// ECONNREFUSED 变成一句莫名其妙的 "fetch failed")。PUT /settings 例外
|
|
469
|
+
// 放行——那是启用入口,必须可用;GET /status 在上面 459 行已先行返回。
|
|
470
|
+
const isSettingsWrite = req.method === 'PUT' && rest === '/settings'
|
|
471
|
+
if (!isSettingsWrite) {
|
|
472
|
+
if (!cfg.enabled) {
|
|
473
|
+
sendJson(res, 503, { ok: false, disabled: true, enabled: false, error: 'git 服务器未启用:请到「设置 → Git 服务器」勾选启用后重试' })
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
if (!state.handle) {
|
|
477
|
+
sendJson(res, 503, { ok: false, starting: true, enabled: true, running: false, error: 'git 服务器正在启动或异常重启中,请稍候重试' })
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// GET /me — 本人 + 仓库列表
|
|
483
|
+
if (req.method === 'GET' && (rest === '/me' || rest === '' || rest === '/')) {
|
|
484
|
+
const me = await kernelApi(cfg, actor, 'GET', '/user')
|
|
485
|
+
const repos = await kernelApi(cfg, actor, 'GET', '/user/repos')
|
|
486
|
+
sendJson(res, 200, {
|
|
487
|
+
ok: true,
|
|
488
|
+
user: { name: actor, isAdmin: !!(me.json && me.json.is_admin) },
|
|
489
|
+
repos: Array.isArray(repos.json) ? repos.json.map((x) => repoCard(x)) : [],
|
|
490
|
+
})
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
// POST /repos — 建仓(本人身份)
|
|
494
|
+
if (req.method === 'POST' && rest === '/repos') {
|
|
495
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
496
|
+
const name = String(body.name || '').trim()
|
|
497
|
+
if (!name) { sendJson(res, 400, { ok: false, error: '仓库名不能为空' }); return }
|
|
498
|
+
const r = await kernelApi(cfg, actor, 'POST', '/user/repos',
|
|
499
|
+
{ name, private: !!body.private, auto_init: body.autoInit !== false, readme: 'Default' })
|
|
500
|
+
sendJson(res, r.status === 201 ? 200 : r.status,
|
|
501
|
+
r.status === 201 ? { ok: true, repo: repoCard(r.json, actor) } : { ok: false, error: errText(r) })
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
// DELETE /repos/:owner/:name
|
|
505
|
+
const delM = req.method === 'DELETE' && rest.startsWith('/repos/') && rest.split('/').length === 4
|
|
506
|
+
if (delM) {
|
|
507
|
+
const [, , owner, name] = rest.split('/')
|
|
508
|
+
const r = await kernelApi(cfg, actor, 'DELETE', `/repos/${owner}/${name}`)
|
|
509
|
+
sendJson(res, [200, 204].includes(r.status) ? 200 : r.status,
|
|
510
|
+
[200, 204].includes(r.status) ? { ok: true } : { ok: false, error: '删除失败(无权限或不存在)' })
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
// GET /repos/:o/:r/tree|raw|commits|branches|issues...
|
|
514
|
+
if (req.method === 'GET' && parts[0] === 'repos' && parts.length >= 4) {
|
|
515
|
+
const [, owner, repo, ...tail] = parts
|
|
516
|
+
const what = tail.join('/')
|
|
517
|
+
const q = (n) => url.searchParams.get(n) || ''
|
|
518
|
+
if (what === 'tree') {
|
|
519
|
+
const pth = q('path') ? '?ref=' + encodeURIComponent(q('ref') || '') + '&path=' + encodeURIComponent(q('path'))
|
|
520
|
+
: '?ref=' + encodeURIComponent(q('ref') || '')
|
|
521
|
+
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/contents${pth}`)
|
|
522
|
+
sendJson(res, r.status, Array.isArray(r.json)
|
|
523
|
+
? { ok: true, entries: r.json.map((e) => ({ name: e.name, type: e.type, path: e.path, size: e.size })) }
|
|
524
|
+
: { ok: false, error: errText(r) })
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
if (what === 'raw') {
|
|
528
|
+
const r = await kernelApi(cfg, actor, 'GET',
|
|
529
|
+
`/repos/${owner}/${repo}/raw/${encodeURIComponent(q('ref'))}/${q('path').split('/').map(encodeURIComponent).join('/')}`)
|
|
530
|
+
// raw 端点返回文本(非 JSON)
|
|
531
|
+
const res2 = await fetch(`http://127.0.0.1:${cfg.port}/api/v1/repos/${owner}/${repo}/raw/${encodeURIComponent(q('ref'))}/${q('path').split('/').map(encodeURIComponent).join('/')}`,
|
|
532
|
+
{ headers: { Authorization: 'token ' + await kernelTokenFor(cfg, actor) } })
|
|
533
|
+
const text = await res2.text()
|
|
534
|
+
sendJson(res, 200, { ok: res2.status === 200, text: res2.status === 200 ? text.slice(0, 512 * 1024) : '', error: res2.status === 200 ? null : 'not found' })
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
if (what === 'commits') {
|
|
538
|
+
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/commits${q('ref') ? '?sha=' + encodeURIComponent(q('ref')) : ''}`)
|
|
539
|
+
const list = Array.isArray(r.json) ? r.json : (r.json && r.json.data) || []
|
|
540
|
+
sendJson(res, r.status, { ok: Array.isArray(list), commits: (Array.isArray(list) ? list : []).slice(0, 30).map((c) => ({
|
|
541
|
+
sha: (c.sha || '').slice(0, 10), message: (c.commit && c.commit.message) || c.message || '',
|
|
542
|
+
author: (c.author && (c.author.login || c.author.name)) || (c.commit && c.commit.author && c.commit.author.name) || '',
|
|
543
|
+
date: (c.commit && c.commit.author && c.commit.author.date) || c.created_at || '',
|
|
544
|
+
})) })
|
|
545
|
+
return
|
|
546
|
+
}
|
|
547
|
+
if (what === 'branches') {
|
|
548
|
+
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/branches`)
|
|
549
|
+
sendJson(res, r.status, { ok: Array.isArray(r.json), branches: (Array.isArray(r.json) ? r.json : []).map((b) => ({ name: b.name, sha: (b.commit && b.commit.id) || b.sha })) })
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
if (what === 'issues') {
|
|
553
|
+
const state = q('state') || 'open'
|
|
554
|
+
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/issues?state=${state}&type=issues`)
|
|
555
|
+
sendJson(res, r.status, { ok: Array.isArray(r.json), issues: (Array.isArray(r.json) ? r.json : []).map((i) => ({
|
|
556
|
+
number: i.number, title: i.title, state: i.state, user: i.user && i.user.login,
|
|
557
|
+
comments: i.comments, updatedAt: i.updated_at,
|
|
558
|
+
})) })
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
if (what.startsWith('issues/') && tail.length === 3) {
|
|
562
|
+
// issues/:idx/comments
|
|
563
|
+
const idx = tail[1]
|
|
564
|
+
if (tail[2] === 'comments') {
|
|
565
|
+
const r = await kernelApi(cfg, actor, 'GET', `/repos/${owner}/${repo}/issues/${idx}/comments`)
|
|
566
|
+
sendJson(res, r.status, { ok: Array.isArray(r.json), comments: (Array.isArray(r.json) ? r.json : []).map((c) => ({
|
|
567
|
+
id: c.id, body: c.body, user: c.user && c.user.login, created: c.created_at,
|
|
568
|
+
})) })
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// POST /repos/:o/:r/issues — 建工单
|
|
574
|
+
if (req.method === 'POST' && parts[0] === 'repos' && parts[3] === 'issues' && parts.length === 4) {
|
|
575
|
+
const [, owner, repo] = parts
|
|
576
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
577
|
+
const r = await kernelApi(cfg, actor, 'POST', `/repos/${owner}/${repo}/issues`, {
|
|
578
|
+
title: String(body.title || ''), body: String(body.body || ''),
|
|
579
|
+
...(Array.isArray(body.labels) ? { labels: body.labels } : {}),
|
|
580
|
+
...(body.milestone ? { milestone: body.milestone } : {}),
|
|
581
|
+
...(body.assignee ? { assignee: body.assignee } : {}),
|
|
582
|
+
})
|
|
583
|
+
sendJson(res, r.status === 201 ? 200 : r.status, r.status === 201 ? { ok: true } : { ok: false, error: errText(r) })
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
// POST /repos/:o/:r/issues/:idx/comments
|
|
587
|
+
if (req.method === 'POST' && parts[0] === 'repos' && parts[3] === 'issues' && parts[5] === 'comments') {
|
|
588
|
+
const [, owner, repo, , idx] = parts
|
|
589
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
590
|
+
const r = await kernelApi(cfg, actor, 'POST', `/repos/${owner}/${repo}/issues/${idx}/comments`, { body: String(body.body || '') })
|
|
591
|
+
sendJson(res, r.status === 201 ? 200 : r.status, r.status === 201 ? { ok: true } : { ok: false, error: errText(r) })
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
// /dsh/*:宿主进程内直接执行内核 dshapi(省铸令牌+HTTP 跳转)。
|
|
595
|
+
// 异常/未匹配路由回退到原来的 HTTP 转发,保证任何内核端点仍可到达。
|
|
596
|
+
if (parts && parts[0] === 'dsh') {
|
|
597
|
+
try {
|
|
598
|
+
const token = await kernelTokenFor(cfg, actor)
|
|
599
|
+
const { dispatch } = require('./dsh-host')
|
|
600
|
+
const handled = await dispatch(cfg, actor, req.method, rest, req, res, token)
|
|
601
|
+
if (handled) return
|
|
602
|
+
} catch (e) {
|
|
603
|
+
sendJson(res, 502, { ok: false, error: 'in-process dispatch failed: ' + String((e && e.message) || e) })
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
// 未匹配(理论上不会发生——dshapi 的路由全注册了)则回退透传
|
|
607
|
+
const kernelPath = '/api' + (rest.startsWith('/') ? rest : '/' + rest) + (url.search || '')
|
|
608
|
+
try {
|
|
609
|
+
const token = await kernelTokenFor(cfg, actor)
|
|
610
|
+
const headers = { Authorization: 'token ' + token }
|
|
611
|
+
let bodyStr
|
|
612
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
613
|
+
bodyStr = await readBody(req)
|
|
614
|
+
headers['Content-Type'] = 'application/json'
|
|
615
|
+
}
|
|
616
|
+
const upstream = await fetch(`http://127.0.0.1:${cfg.port}${kernelPath}`, {
|
|
617
|
+
method: req.method, headers, body: bodyStr,
|
|
618
|
+
})
|
|
619
|
+
const text = await upstream.text()
|
|
620
|
+
let json = null
|
|
621
|
+
try { json = JSON.parse(text) } catch { json = { ok: false, error: text.slice(0, 200) } }
|
|
622
|
+
sendJson(res, upstream.status, json)
|
|
623
|
+
} catch (e) {
|
|
624
|
+
sendJson(res, 502, { ok: false, error: String((e && e.message) || e) })
|
|
625
|
+
}
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// PATCH /repos/:o/:r/issues/:idx — 开/关工单
|
|
630
|
+
if (req.method === 'PATCH' && parts[0] === 'repos' && parts[3] === 'issues' && parts.length === 5) {
|
|
631
|
+
const [, owner, repo, , idx] = parts
|
|
632
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
633
|
+
const r = await kernelApi(cfg, actor, 'PATCH', `/repos/${owner}/${repo}/issues/${idx}`, { state: body.state === 'closed' ? 'closed' : 'open' })
|
|
634
|
+
sendJson(res, r.status === 201 || r.status === 200 ? 200 : r.status, { ok: [200, 201].includes(r.status) })
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (req.method === 'PUT' && rest === '/settings') {
|
|
639
|
+
let patchBody = null
|
|
640
|
+
try {
|
|
641
|
+
patchBody = JSON.parse((await readBody(req)) || '{}')
|
|
642
|
+
} catch {
|
|
643
|
+
sendJson(res, 400, { ok: false, error: '请求体不是合法 JSON' })
|
|
644
|
+
return
|
|
645
|
+
}
|
|
646
|
+
const patch = engine.sanitizePatch(patchBody, effective())
|
|
647
|
+
memoryPatch = { ...memoryPatch, ...patch }
|
|
648
|
+
if (typeof patch.adminPassword === 'string' && patch.adminPassword) writePasswordFile(patch.adminPassword)
|
|
649
|
+
if (settingsScope && typeof settingsScope.update === 'function') {
|
|
650
|
+
try {
|
|
651
|
+
await settingsScope.update(patch)
|
|
652
|
+
} catch (e) {
|
|
653
|
+
logger.warn(`dsh-git-server: settings update 失败(仅本次进程生效): ${(e && e.message) || e}`)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
await reconcile()
|
|
657
|
+
sendJson(res, 200, status())
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
sendJson(res, 404, { ok: false, error: 'not found' })
|
|
661
|
+
} catch (e) {
|
|
662
|
+
try { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }) } catch {}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
})
|
|
666
|
+
}, 'dsh-git-server: api route')
|
|
667
|
+
} else {
|
|
668
|
+
logger.warn('dsh-git-server: webServer 不可用,设置 API 未注册')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ── 协调器心跳:兜住手改 settings.yaml / 意外退出的自动拉起 ───────────
|
|
672
|
+
ctx.effect(() => {
|
|
673
|
+
restartTimer = setInterval(() => {
|
|
674
|
+
reconcile().catch((e) => logger.error(`dsh-git-server: reconcile: ${(e && e.message) || e}`))
|
|
675
|
+
}, 3000)
|
|
676
|
+
return () => {
|
|
677
|
+
clearInterval(restartTimer)
|
|
678
|
+
if (state.handle) state.handle.stop().catch(() => {})
|
|
679
|
+
}
|
|
680
|
+
}, 'dsh-git-server: reconciler + child process')
|
|
681
|
+
},
|
|
682
|
+
}
|