@fengye_nobug/dsh-qq-bot 0.2.6 → 0.2.8
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/CHANGELOG.md +13 -0
- package/DESIGN.md +570 -1
- package/README.md +60 -605
- package/client/client.js +165 -13
- package/cordis.patch.yml +3 -1
- package/index.js +407 -2
- package/onebot.js +1 -1
- package/package.json +1 -1
- package/updates.js +135 -82
package/updates.js
CHANGED
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
// 安装成功后,若 profile、home 或插件自己的 cordis.patch.yml 里 qq-bridge 的 name
|
|
11
11
|
// 仍是旧的无作用域名,会改成磁盘上 package.json 里的包名。不改 config,不改设置,不改 NapCat。
|
|
12
12
|
// 安装超过三分钟还没结束时,停掉这次拉起的进程组,并清掉已经退出的安装记录。
|
|
13
|
-
//
|
|
13
|
+
// 超时后若磁盘上已是目标版本,仍改包名,并安排重启。
|
|
14
|
+
// 重启会读本进程的 cgroup:认得出具体服务就用 systemd-run 只重启那个服务
|
|
15
|
+
// (挂在 user@ 下面的用 systemctl --user)。systemd-run 失败,或只看见用户会话、
|
|
16
|
+
// 认不出单独服务时,不重启整个登录,改为按当前命令行再拉起一次。
|
|
17
|
+
// 两条都做不到,才提示自己重启。
|
|
14
18
|
|
|
15
19
|
import { spawn } from 'node:child_process'
|
|
16
20
|
import { chmodSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
@@ -237,7 +241,7 @@ export function assessUpdate({ current, releases, repo = UPDATE_REPO, channel =
|
|
|
237
241
|
kind: 'no-release',
|
|
238
242
|
current: parsedCurrent.raw,
|
|
239
243
|
updateAvailable: false,
|
|
240
|
-
message: 'GitHub 上还没有可用的正式版,没法对比新版本。',
|
|
244
|
+
message: channel === 'npm' ? 'npm 上还没有可用的正式版,没法对比新版本。' : 'GitHub 上还没有可用的正式版,没法对比新版本。',
|
|
241
245
|
}
|
|
242
246
|
}
|
|
243
247
|
stable.sort((left, right) => compareSemver(right.parsed, left.parsed) ?? 0)
|
|
@@ -259,21 +263,21 @@ export function assessUpdate({ current, releases, repo = UPDATE_REPO, channel =
|
|
|
259
263
|
url,
|
|
260
264
|
}
|
|
261
265
|
if (compared > 0) {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return { ...base, kind: 'update', updateAvailable: true, message: lines.join('\n') }
|
|
266
|
+
const where = useNpm ? 'npm' : 'GitHub'
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
kind: 'update',
|
|
270
|
+
updateAvailable: true,
|
|
271
|
+
message: `有新版本 ${latest.parsed.raw}(现在是 ${parsedCurrent.raw}),在 ${where} 上。这里只检查,还没安装。`,
|
|
272
|
+
}
|
|
270
273
|
}
|
|
271
274
|
if (compared < 0) {
|
|
275
|
+
const where = useNpm ? 'npm' : 'GitHub'
|
|
272
276
|
return {
|
|
273
277
|
...base,
|
|
274
278
|
kind: 'ahead',
|
|
275
279
|
updateAvailable: false,
|
|
276
|
-
message: `你现在的 ${parsedCurrent.raw} 比
|
|
280
|
+
message: `你现在的 ${parsedCurrent.raw} 比 ${where} 上最新正式版 ${latest.parsed.raw} 还新,不用往回装。`,
|
|
277
281
|
}
|
|
278
282
|
}
|
|
279
283
|
return {
|
|
@@ -439,19 +443,6 @@ export async function queryNpmVersions(input = {}) {
|
|
|
439
443
|
return { ok: false, code: 'network' }
|
|
440
444
|
}
|
|
441
445
|
|
|
442
|
-
function noteGithubFallback(assessed) {
|
|
443
|
-
if (!assessed || assessed.updateAvailable !== true) return assessed
|
|
444
|
-
const message = String(assessed.message ?? '')
|
|
445
|
-
if (message.includes('还没发到 npm') || !message.includes('也可以在设置卡片里点')) return assessed
|
|
446
|
-
return {
|
|
447
|
-
...assessed,
|
|
448
|
-
message: message.replace(
|
|
449
|
-
'也可以在设置卡片里点',
|
|
450
|
-
'这一版还没发到 npm,一键更新会改从 GitHub 下载。\n也可以在设置卡片里点',
|
|
451
|
-
),
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
446
|
function lookupFailure(npmCode, githubError, applying) {
|
|
456
447
|
const raw = String(githubError ?? '')
|
|
457
448
|
if (npmCode === 'network' && /连不上 GitHub/.test(raw)) {
|
|
@@ -490,7 +481,7 @@ async function findUpdate(input) {
|
|
|
490
481
|
if (wanted !== '') return { ok: false, error: '这次检查到的正式版里没有这个版本,没有安装。' }
|
|
491
482
|
return { ok: true, channel: 'npm', releases, assessed }
|
|
492
483
|
}
|
|
493
|
-
const ghAssessed =
|
|
484
|
+
const ghAssessed = assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' })
|
|
494
485
|
if (ghAssessed.ok === false) return { ok: false, error: ghAssessed.message }
|
|
495
486
|
const npmLatest = assessed.latest || '0.0.0'
|
|
496
487
|
const githubNewer = ghAssessed.updateAvailable === true && (compareSemver(ghAssessed.latest, npmLatest) ?? 0) > 0
|
|
@@ -506,7 +497,7 @@ async function findUpdate(input) {
|
|
|
506
497
|
}
|
|
507
498
|
const gh = await queryGithubReleases(input.fetchImpl, { repo, timeoutMs: input.timeoutMs })
|
|
508
499
|
if (!gh.ok) return { ok: false, error: lookupFailure(npm.code, gh.error, input.applying === true) }
|
|
509
|
-
const assessed =
|
|
500
|
+
const assessed = assessUpdate({ current: input.current, releases: gh.releases, repo, channel: 'github' })
|
|
510
501
|
if (assessed.ok === false) return { ok: false, error: assessed.message }
|
|
511
502
|
return { ok: true, channel: 'github', releases: gh.releases, assessed }
|
|
512
503
|
}
|
|
@@ -627,9 +618,6 @@ function installError(code, command, extra = {}) {
|
|
|
627
618
|
const which = Number.isSafeInteger(pid) && pid > 1 ? `(进程 ${pid})` : ''
|
|
628
619
|
return `已经有一个安装进程还在跑${which},这次没有再启动。等它结束再点「更新」。如果确认它已经卡死,先停掉那个进程,再点一次。`
|
|
629
620
|
}
|
|
630
|
-
if (code === 'timeout' && extra.packageName) {
|
|
631
|
-
return `安装超时,已经停掉卡住的进程。新版本的文件已经在磁盘上,qq-bridge 的包名也改成了 ${extra.packageName}。请重启 dsh-web,设置页才会出来。`
|
|
632
|
-
}
|
|
633
621
|
if (code === 'timeout' && extra.leftover) {
|
|
634
622
|
return `安装超时,卡住的安装进程还没停干净。请先停掉 dsh 和 pnpm,再点一次「更新」。${hint}`
|
|
635
623
|
}
|
|
@@ -639,13 +627,12 @@ function installError(code, command, extra = {}) {
|
|
|
639
627
|
if (code === 'wrong-name') return '下到的安装包不是这个插件,没有安装。'
|
|
640
628
|
if (code === 'wrong-version') return '下到的版本和发布标签对不上,没有安装。'
|
|
641
629
|
if (code === 'bad-package') return '下到的安装包读不懂,没有安装。'
|
|
642
|
-
|
|
643
|
-
if (extra.packageName) return `${base}Cordis 里 qq-bridge 的包名已经改成实际安装的 ${extra.packageName}。请重启 dsh-web,设置页才会出来。`
|
|
644
|
-
return base
|
|
630
|
+
return `安装没有成功。${hint}`
|
|
645
631
|
}
|
|
646
632
|
|
|
647
|
-
function restartError() {
|
|
648
|
-
|
|
633
|
+
function restartError(channel) {
|
|
634
|
+
const via = channel === 'npm' ? '从 npm ' : channel === 'github' ? '从 GitHub ' : ''
|
|
635
|
+
return `新版本已经${via}装上,但没能自动重启面板。请自己重启 dsh-web,然后刷新页面。登录口令可能会变。`
|
|
649
636
|
}
|
|
650
637
|
|
|
651
638
|
function headerValue(response, name) {
|
|
@@ -1538,46 +1525,75 @@ setTimeout(() => {
|
|
|
1538
1525
|
}, delay)
|
|
1539
1526
|
`.trim()
|
|
1540
1527
|
|
|
1528
|
+
/** 具体服务挂在 user@UID.service 下面时,重启要走用户实例,不能重启整个登录会话。 */
|
|
1529
|
+
function systemdUserManager(text) {
|
|
1530
|
+
return /\/user@\d+\.service(?:\/|$)/.test(String(text ?? ''))
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function systemdRunArgs(unit, user) {
|
|
1534
|
+
const scope = user ? ['--user'] : []
|
|
1535
|
+
return [
|
|
1536
|
+
...scope,
|
|
1537
|
+
'--quiet',
|
|
1538
|
+
'--collect',
|
|
1539
|
+
'--on-active=3s',
|
|
1540
|
+
'--timer-property=AccuracySec=1s',
|
|
1541
|
+
'--',
|
|
1542
|
+
'systemctl',
|
|
1543
|
+
...scope,
|
|
1544
|
+
'restart',
|
|
1545
|
+
unit,
|
|
1546
|
+
]
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function cgroupText(input) {
|
|
1550
|
+
if (input.cgroup != null) return String(input.cgroup)
|
|
1551
|
+
const read = typeof input.readCgroup === 'function' ? input.readCgroup : readOwnCgroup
|
|
1552
|
+
try {
|
|
1553
|
+
return String(read() ?? '')
|
|
1554
|
+
} catch {
|
|
1555
|
+
return ''
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function reexecPlan(input) {
|
|
1560
|
+
return {
|
|
1561
|
+
mode: 'reexec',
|
|
1562
|
+
execPath: input.execPath || process.execPath,
|
|
1563
|
+
argv: Array.isArray(input.argv) ? input.argv : process.argv.slice(1),
|
|
1564
|
+
pid: input.pid || process.pid,
|
|
1565
|
+
cwd: input.cwd || process.cwd(),
|
|
1566
|
+
delayMs: RESTART_DELAY_MS,
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1541
1570
|
/**
|
|
1542
1571
|
* 重启计划。systemd 里只重启 cgroup 里那个具体服务,用 systemd-run 把动作放到服务外面。
|
|
1543
|
-
*
|
|
1572
|
+
* 没传入 cgroup 时读本进程的 /proc/self/cgroup。用户服务加 --user。
|
|
1573
|
+
* 认不出单独服务时计划是 manual:不重启整个登录会话。其余情况按当前进程的命令行再拉起一次。
|
|
1544
1574
|
*/
|
|
1545
1575
|
export function planPanelRestart(input = {}) {
|
|
1546
1576
|
const platform = input.platform || process.platform
|
|
1547
1577
|
const env = input.env || process.env
|
|
1548
1578
|
const invocation = platform !== 'win32' && Boolean(env && (env.INVOCATION_ID || env.NOTIFY_SOCKET))
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1579
|
+
const cgroup = invocation ? cgroupText(input) : ''
|
|
1580
|
+
const unit = invocation ? systemdUnitFromCgroup(cgroup) : null
|
|
1551
1581
|
if (unit) {
|
|
1582
|
+
const user = systemdUserManager(cgroup)
|
|
1552
1583
|
return {
|
|
1553
1584
|
mode: 'systemd',
|
|
1554
1585
|
command: 'systemd-run',
|
|
1555
|
-
args:
|
|
1586
|
+
args: systemdRunArgs(unit, user),
|
|
1556
1587
|
unit,
|
|
1588
|
+
scope: user ? 'user' : 'system',
|
|
1557
1589
|
}
|
|
1558
1590
|
}
|
|
1559
|
-
return {
|
|
1560
|
-
|
|
1561
|
-
execPath: input.execPath || process.execPath,
|
|
1562
|
-
argv: Array.isArray(input.argv) ? input.argv : process.argv.slice(1),
|
|
1563
|
-
pid: input.pid || process.pid,
|
|
1564
|
-
cwd: input.cwd || process.cwd(),
|
|
1565
|
-
delayMs: RESTART_DELAY_MS,
|
|
1566
|
-
}
|
|
1591
|
+
if (invocation) return { mode: 'manual' }
|
|
1592
|
+
return reexecPlan(input)
|
|
1567
1593
|
}
|
|
1568
1594
|
|
|
1569
|
-
|
|
1570
|
-
* @param {{ plan?: ReturnType<typeof planPanelRestart>, spawnImpl?: typeof spawn, env?: NodeJS.ProcessEnv, platform?: string, cgroup?: string, execPath?: string, argv?: string[], pid?: number, cwd?: string }} input
|
|
1571
|
-
*/
|
|
1572
|
-
export async function schedulePanelRestart(input = {}) {
|
|
1573
|
-
const plan = input.plan || planPanelRestart(input)
|
|
1574
|
-
if (plan.mode === 'manual') return { ok: false, mode: 'manual', error: restartError() }
|
|
1595
|
+
async function spawnReexec(plan, input) {
|
|
1575
1596
|
const spawnImpl = input.spawnImpl || spawn
|
|
1576
|
-
if (plan.mode === 'systemd') {
|
|
1577
|
-
const ran = await runChild(spawnImpl, plan.command, plan.args, { timeoutMs: 20000, stdio: 'ignore' })
|
|
1578
|
-
if (!ran.ok) return { ok: false, mode: 'systemd', error: restartError() }
|
|
1579
|
-
return { ok: true, mode: 'systemd' }
|
|
1580
|
-
}
|
|
1581
1597
|
const payload = JSON.stringify({
|
|
1582
1598
|
pid: plan.pid,
|
|
1583
1599
|
cwd: plan.cwd,
|
|
@@ -1607,6 +1623,29 @@ export async function schedulePanelRestart(input = {}) {
|
|
|
1607
1623
|
return { ok: true, mode: 'reexec' }
|
|
1608
1624
|
}
|
|
1609
1625
|
|
|
1626
|
+
/**
|
|
1627
|
+
* 先按计划重启。systemd-run 失败,或认不出单独服务时,改为重启当前进程,不重启整个登录。
|
|
1628
|
+
* 两条都做不到才返回失败。
|
|
1629
|
+
* @param {{ plan?: ReturnType<typeof planPanelRestart>, spawnImpl?: typeof spawn, env?: NodeJS.ProcessEnv, platform?: string, cgroup?: string, readCgroup?: () => string, execPath?: string, argv?: string[], pid?: number, cwd?: string }} input
|
|
1630
|
+
*/
|
|
1631
|
+
export async function schedulePanelRestart(input = {}) {
|
|
1632
|
+
const plan = input.plan || planPanelRestart(input)
|
|
1633
|
+
const spawnImpl = input.spawnImpl || spawn
|
|
1634
|
+
if (plan.mode === 'systemd') {
|
|
1635
|
+
const ran = await runChild(spawnImpl, plan.command, plan.args, { timeoutMs: 20000, stdio: 'ignore' })
|
|
1636
|
+
if (ran.ok) return { ok: true, mode: 'systemd', unit: plan.unit, scope: plan.scope }
|
|
1637
|
+
const fallback = await spawnReexec(reexecPlan(input), { ...input, spawnImpl })
|
|
1638
|
+
if (fallback.ok) return fallback
|
|
1639
|
+
return { ok: false, mode: 'manual', error: restartError() }
|
|
1640
|
+
}
|
|
1641
|
+
if (plan.mode === 'manual') {
|
|
1642
|
+
const fallback = await spawnReexec(reexecPlan(input), { ...input, spawnImpl })
|
|
1643
|
+
if (fallback.ok) return fallback
|
|
1644
|
+
return { ok: false, mode: 'manual', error: restartError() }
|
|
1645
|
+
}
|
|
1646
|
+
return spawnReexec(plan, { ...input, spawnImpl })
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1610
1649
|
const CORDIS_ID = 'qq-bridge'
|
|
1611
1650
|
|
|
1612
1651
|
function isWritablePackageName(name) {
|
|
@@ -1783,10 +1822,8 @@ function alignNote(input, expectedVersion) {
|
|
|
1783
1822
|
}
|
|
1784
1823
|
|
|
1785
1824
|
function successMessage(tag, channel, note = '') {
|
|
1786
|
-
const via = channel === 'npm'
|
|
1787
|
-
|
|
1788
|
-
: '这一版在 npm 上还没有,所以从 GitHub 装上。'
|
|
1789
|
-
const base = `已经装上 ${tag},面板即将重启。请刷新页面;登录口令可能会变,用新的面板地址再打开。${via}现有设置还在,没有改动 NapCat。`
|
|
1825
|
+
const via = channel === 'npm' ? 'npm' : 'GitHub'
|
|
1826
|
+
const base = `已经从 ${via} 装上 ${tag},面板即将重启。`
|
|
1790
1827
|
return note ? `${base}${note}` : base
|
|
1791
1828
|
}
|
|
1792
1829
|
|
|
@@ -1810,25 +1847,31 @@ function installRunSpec(input, extra) {
|
|
|
1810
1847
|
}
|
|
1811
1848
|
}
|
|
1812
1849
|
|
|
1813
|
-
/**
|
|
1814
|
-
function
|
|
1850
|
+
/** 磁盘上的已安装包是不是这次要装的版本。只读 package.json,不改文件。 */
|
|
1851
|
+
function installedMatches(input, version) {
|
|
1852
|
+
const profileDir = profileDirOf(input)
|
|
1853
|
+
if (profileDir === '' || !version) return false
|
|
1854
|
+
const scoped = readInstalledManifest(join(profileDir, 'node_modules', '@fengye_nobug', 'dsh-qq-bot'))
|
|
1855
|
+
const legacy = readInstalledManifest(join(profileDir, 'node_modules', 'dsh-qq-bot'))
|
|
1856
|
+
return [scoped, legacy].some((item) => item && compareSemver(item.version, version) === 0)
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* 安装命令没返回成功时:进程已经停干净,并且磁盘上已经是目标版本,就改包名并安排重启。
|
|
1861
|
+
* 进程还在,或版本对不上,不重启。
|
|
1862
|
+
*/
|
|
1863
|
+
async function failInstall(input, decision, ran, command, install, channel) {
|
|
1815
1864
|
const code = ran?.code
|
|
1816
1865
|
const leftover = ran?.leftover === true
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
if (aligned.changed.length > 0) packageName = aligned.name
|
|
1827
|
-
} catch {
|
|
1828
|
-
// 对不齐就只说明安装失败。
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
const message = installError(code, command, { packageName, leftover, pid: ran?.pid })
|
|
1866
|
+
const via = channel === 'npm' || channel === 'github' ? channel : decision.channel
|
|
1867
|
+
if (!leftover && (code === 'timeout' || code === 'exit' || code === 'error') && installedMatches(input, decision.version)) {
|
|
1868
|
+
return finishApply(
|
|
1869
|
+
input,
|
|
1870
|
+
{ ...decision, command: command || decision.command, install: install || decision.install },
|
|
1871
|
+
via === 'npm' ? 'npm' : 'github',
|
|
1872
|
+
)
|
|
1873
|
+
}
|
|
1874
|
+
const message = installError(code, command, { leftover, pid: ran?.pid })
|
|
1832
1875
|
return {
|
|
1833
1876
|
ok: false,
|
|
1834
1877
|
stage: 'install',
|
|
@@ -1891,7 +1934,7 @@ export async function applyPluginUpdate(input = {}) {
|
|
|
1891
1934
|
ran = { ok: false, code: 'error' }
|
|
1892
1935
|
}
|
|
1893
1936
|
if (ran?.ok === true) return finishApply(input, decision, 'npm')
|
|
1894
|
-
if (ran?.code !== 'unpublished') return failInstall(input, decision, ran, decision.command, decision.install)
|
|
1937
|
+
if (ran?.code !== 'unpublished') return failInstall(input, decision, ran, decision.command, decision.install, 'npm')
|
|
1895
1938
|
channel = 'github'
|
|
1896
1939
|
}
|
|
1897
1940
|
const shown = shownInstall(channel, decision.version, repo)
|
|
@@ -1933,7 +1976,7 @@ export async function applyPluginUpdate(input = {}) {
|
|
|
1933
1976
|
rmSync(dir, { recursive: true, force: true })
|
|
1934
1977
|
}
|
|
1935
1978
|
if (!ran || ran.ok !== true) {
|
|
1936
|
-
return failInstall(input, { ...decision, install: shown.install, command: shown.command }, ran, shown.command, shown.install)
|
|
1979
|
+
return failInstall(input, { ...decision, install: shown.install, command: shown.command }, ran, shown.command, shown.install, 'github')
|
|
1937
1980
|
}
|
|
1938
1981
|
return finishApply(input, { ...decision, install: shown.install, command: shown.command }, 'github')
|
|
1939
1982
|
}
|
|
@@ -1943,12 +1986,22 @@ async function finishApply(input, decision, channel) {
|
|
|
1943
1986
|
let restart
|
|
1944
1987
|
try {
|
|
1945
1988
|
const schedule = input.scheduleRestart || schedulePanelRestart
|
|
1946
|
-
restart = await schedule({
|
|
1989
|
+
restart = await schedule({
|
|
1990
|
+
spawnImpl: input.spawnImpl,
|
|
1991
|
+
platform: input.platform,
|
|
1992
|
+
env: input.env,
|
|
1993
|
+
cgroup: input.cgroup,
|
|
1994
|
+
readCgroup: input.readCgroup,
|
|
1995
|
+
execPath: input.execPath,
|
|
1996
|
+
argv: input.argv,
|
|
1997
|
+
pid: input.pid,
|
|
1998
|
+
cwd: input.cwd,
|
|
1999
|
+
})
|
|
1947
2000
|
} catch {
|
|
1948
2001
|
restart = { ok: false, error: restartError() }
|
|
1949
2002
|
}
|
|
1950
2003
|
if (!restart || restart.ok !== true) {
|
|
1951
|
-
const message = `${
|
|
2004
|
+
const message = `${restartError(channel)}${note}`
|
|
1952
2005
|
return {
|
|
1953
2006
|
ok: false,
|
|
1954
2007
|
stage: 'restart',
|