dsh-wechat-remote 1.7.8 → 1.7.9
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 +10 -8
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +9 -10
- package/bin/dsh-discovery.mjs +18 -18
- package/bin/native-recovery.mjs +44 -44
- package/bin/release-selection.mjs +2 -2
- package/bin/setup.mjs +110 -94
- package/lib/install-control.js +52 -28
- package/lib/install-profile.js +2 -1
- package/lib/native-recovery.js +144 -144
- package/lib/update-download.js +162 -36
- package/lib/update-policy.js +9 -1
- package/lib/update-worker.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent远程管理助手 · DSH 插件安装器
|
|
2
2
|
|
|
3
|
-
当前版本为 `1.7.
|
|
3
|
+
当前版本为 `1.7.9`,内嵌同版本正式插件。
|
|
4
4
|
|
|
5
5
|
首次安装和升级旧插件使用同一个命令:
|
|
6
6
|
|
|
@@ -8,20 +8,22 @@
|
|
|
8
8
|
npx -y dsh-wechat-remote@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
在与 DSH 相同的系统用户下运行即可。无需改成全局安装 DSH;使用 `npx @deepseek-ai/dsh web` 的用户也使用同一条安装命令。需要 Node.js 22.13 或更高版本,无需另装 pnpm 或 Git。Windows CMD / PowerShell 推荐使用 `npx.cmd -y dsh-wechat-remote@latest`,不必修改脚本执行策略。
|
|
11
|
+
在与 DSH 相同的系统用户下运行即可。无需改成全局安装 DSH;使用 `npx @deepseek-ai/dsh web` 的用户也使用同一条安装命令。需要 Node.js 22.13 或更高版本,无需另装 pnpm 或 Git。Windows CMD / PowerShell 推荐使用 `npx.cmd -y dsh-wechat-remote@latest`,不必修改脚本执行策略。
|
|
12
12
|
|
|
13
|
-
安装器保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。DSH 已关闭时会尝试启动;若启动失败,按提示查看启动日志,不要删除原有数据。
|
|
13
|
+
安装器保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。DSH 已关闭时会尝试启动;若启动失败,按提示查看启动日志,不要删除原有数据。
|
|
14
14
|
|
|
15
15
|
DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验证记录而被拒绝安装。安装器仍检查必要运行能力、安装包完整性和已知不兼容问题,并保留失败恢复所需的备份;无法确认恢复结果时会提示保留数据。这不代表所有宿主组合均已通过功能验证。
|
|
16
16
|
|
|
17
|
-
本版沿用此前已验证的跨平台安装方案,具体发行包与宿主组合以验收记录为准。普通 Node 启动、独立的 macOS launchd 和 Linux 用户级 systemd 服务有对应的自动恢复方式;无法确认启动方式时不自动停止主机。
|
|
17
|
+
本版沿用此前已验证的跨平台安装方案,具体发行包与宿主组合以验收记录为准。普通 Node 启动、独立的 macOS launchd 和 Linux 用户级 systemd 服务有对应的自动恢复方式;无法确认启动方式时不自动停止主机。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
安装过程中请等待完成,勿重复执行命令。
|
|
20
|
+
|
|
21
|
+
本版安装器内置完整插件,安装当前版本不依赖 GitHub 附件下载。安装后,WebUI 一键更新在 GitHub 网络下载失败时可以从已核验的 npm 安装包中取回同一插件。该能力不回写旧插件;旧插件遇到 GitHub 下载失败时,先使用上面的 npm 命令升级即可。
|
|
20
22
|
|
|
21
23
|
旧插件的一键更新如被版本或架构判断拦截,可用上述安装命令升级一次。从插件 1.7.6 起,未登记的 CPU 架构或 DSH 版本不再影响更新入口;执行前仍会检查实际安装与重启条件,无需因此降低 DSH 版本或删除节点。
|
|
22
24
|
|
|
23
25
|
默认使用 `web` 配置和原来的 DSH 数据目录。自定义数据目录时,在新终端设置相同的 `DSH_HOME`,或附加 `--home "数据目录"`;使用其他配置时附加 `--profile 配置名称`。WSL、容器和其他系统用户内的 DSH,需要在对应环境中执行安装,不能跨环境替换。
|
|
24
26
|
|
|
25
|
-
DSH 正在运行时,优先连接它自己的安装接口;未运行时,优先使用当前终端默认的 DSH,再检查全局、当前项目和 NPX 缓存。只有存在多份缓存且没有默认选择时才询问。也可用 `--dsh-cli "DSH的lib/bin.js路径"` 明确指定。
|
|
26
|
-
|
|
27
|
-
已运行的同版本插件卡在更新验证、尚未就绪时,重新执行命令会尝试修复。DSH 已退出或怀疑插件文件损坏时,附加 `--repair` 主动重装,不会自动降级。宿主本体或其他插件的故障仍可能需要另行处理;安装日志会保留,不必先删除会话或重新配对。
|
|
27
|
+
DSH 正在运行时,优先连接它自己的安装接口;未运行时,优先使用当前终端默认的 DSH,再检查全局、当前项目和 NPX 缓存。只有存在多份缓存且没有默认选择时才询问。也可用 `--dsh-cli "DSH的lib/bin.js路径"` 明确指定。
|
|
28
|
+
|
|
29
|
+
已运行的同版本插件卡在更新验证、尚未就绪时,重新执行命令会尝试修复。DSH 已退出或怀疑插件文件损坏时,附加 `--repair` 主动重装,不会自动降级。宿主本体或其他插件的故障仍可能需要另行处理;安装日志会保留,不必先删除会话或重新配对。
|
package/assets/plugin.tgz
CHANGED
|
Binary file
|
package/assets/release.json
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.7.
|
|
2
|
+
"version": "1.7.9",
|
|
3
3
|
"catalog": {
|
|
4
4
|
"schemaVersion": 1,
|
|
5
|
-
"revision": "installer-1.7.
|
|
6
|
-
"issuedAt":
|
|
7
|
-
"expiresAt":
|
|
5
|
+
"revision": "installer-1.7.9",
|
|
6
|
+
"issuedAt": 1790053952064,
|
|
7
|
+
"expiresAt": 1792473152064,
|
|
8
8
|
"releases": [
|
|
9
9
|
{
|
|
10
|
-
"version": "1.7.
|
|
10
|
+
"version": "1.7.9",
|
|
11
11
|
"channel": "stable",
|
|
12
12
|
"dsh": [
|
|
13
13
|
"0.1.5-rc.1"
|
|
14
14
|
],
|
|
15
15
|
"platforms": [
|
|
16
16
|
"windows",
|
|
17
|
-
"macos"
|
|
18
|
-
"linux"
|
|
17
|
+
"macos"
|
|
19
18
|
],
|
|
20
19
|
"architectures": [
|
|
21
20
|
"x64"
|
|
22
21
|
],
|
|
23
22
|
"asset": {
|
|
24
|
-
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.
|
|
25
|
-
"sha256": "
|
|
26
|
-
"bytes":
|
|
23
|
+
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.9/harness-remote-dsh-wechat-remote-1.7.9.tgz",
|
|
24
|
+
"sha256": "c0b592f90d67e63af3b4f03592bf1365828c7d404f0b5d3a812ca4a1a8439cda",
|
|
25
|
+
"bytes": 10361441
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
],
|
package/bin/dsh-discovery.mjs
CHANGED
|
@@ -10,8 +10,8 @@ import { createInterface } from 'node:readline/promises'
|
|
|
10
10
|
|
|
11
11
|
const execute = promisify(execFile)
|
|
12
12
|
const PACKAGE = '@deepseek-ai/dsh'
|
|
13
|
-
const clean = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, '')
|
|
14
|
-
const environmentPath = env => env[Object.keys(env).find(key => key.toLowerCase() === 'path')] || ''
|
|
13
|
+
const clean = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, '')
|
|
14
|
+
const environmentPath = env => env[Object.keys(env).find(key => key.toLowerCase() === 'path')] || ''
|
|
15
15
|
|
|
16
16
|
export function validateDshCli(filename) {
|
|
17
17
|
const cli = fs.realpathSync(path.resolve(filename))
|
|
@@ -37,7 +37,7 @@ export function resolveHome(value, fallback = os.homedir()) {
|
|
|
37
37
|
async function npmLocation(key, env, cwd) {
|
|
38
38
|
let npm = env.npm_execpath
|
|
39
39
|
if (!npm || !fs.existsSync(npm)) {
|
|
40
|
-
const roots = [path.dirname(process.execPath), ...environmentPath(env).split(path.delimiter)]
|
|
40
|
+
const roots = [path.dirname(process.execPath), ...environmentPath(env).split(path.delimiter)]
|
|
41
41
|
npm = roots.flatMap(dir => [path.join(dir, 'node_modules/npm/bin/npm-cli.js'),
|
|
42
42
|
path.join(dir, '../lib/node_modules/npm/bin/npm-cli.js')]).find(file => fs.existsSync(file))
|
|
43
43
|
}
|
|
@@ -59,7 +59,7 @@ export async function discoverDsh({ env = process.env, cwd = process.cwd(), cach
|
|
|
59
59
|
if (!found.has(item.cli)) found.set(item.cli, { ...item, source })
|
|
60
60
|
} catch { /* A missing/broken cache entry is not a usable DSH. */ }
|
|
61
61
|
}
|
|
62
|
-
for (const dir of environmentPath(env).split(path.delimiter).filter(Boolean)) {
|
|
62
|
+
for (const dir of environmentPath(env).split(path.delimiter).filter(Boolean)) {
|
|
63
63
|
add(path.join(dir, 'dsh'), 'PATH') // POSIX symlink, never execute a Windows shim.
|
|
64
64
|
add(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
65
65
|
add(path.join(dir, '../lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
@@ -81,9 +81,9 @@ export async function discoverDsh({ env = process.env, cwd = process.cwd(), cach
|
|
|
81
81
|
add(path.join(locations[1], 'lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npm-global')
|
|
82
82
|
}
|
|
83
83
|
const cacheRoot = locations[0]
|
|
84
|
-
// Cached NPX versions cannot supersede a normal default executable. Avoid
|
|
85
|
-
// probing a large/unreadable cache when the terminal already has that default.
|
|
86
|
-
if (cacheRoot && found.size === 0) {
|
|
84
|
+
// Cached NPX versions cannot supersede a normal default executable. Avoid
|
|
85
|
+
// probing a large/unreadable cache when the terminal already has that default.
|
|
86
|
+
if (cacheRoot && found.size === 0) {
|
|
87
87
|
try {
|
|
88
88
|
const entries = fs.readdirSync(path.join(cacheRoot, '_npx'), { withFileTypes: true })
|
|
89
89
|
if (entries.length > 512) throw new Error('npx 缓存较多,请先启动要使用的 DSH,再运行安装命令。')
|
|
@@ -97,17 +97,17 @@ export async function discoverDsh({ env = process.env, cwd = process.cwd(), cach
|
|
|
97
97
|
|
|
98
98
|
export async function chooseDsh(options = {}) {
|
|
99
99
|
if (options.cli) return validateDshCli(options.cli).cli
|
|
100
|
-
const candidates = await discoverDsh(options)
|
|
101
|
-
if (!candidates.length) throw new Error('未找到 DSH。请先按原来的方式启动 DSH WebUI,再运行本命令。')
|
|
102
|
-
// A stopped host has no handshake yet. Follow the terminal's ordinary DSH
|
|
103
|
-
// resolution before consulting old NPX cache entries; no version sorting or
|
|
104
|
-
// downloading. This fallback never overrides an already running host.
|
|
105
|
-
const normal = candidates.find(item => item.source === 'PATH')
|
|
106
|
-
|| candidates.find(item => item.source === 'npm-global')
|
|
107
|
-
|| candidates.find(item => item.source === 'project' || item.source === 'source')
|
|
108
|
-
if (normal) return normal.cli
|
|
109
|
-
if (candidates.length === 1) return candidates[0].cli
|
|
110
|
-
// Multiple cache-only versions are not evidence of the user's choice.
|
|
100
|
+
const candidates = await discoverDsh(options)
|
|
101
|
+
if (!candidates.length) throw new Error('未找到 DSH。请先按原来的方式启动 DSH WebUI,再运行本命令。')
|
|
102
|
+
// A stopped host has no handshake yet. Follow the terminal's ordinary DSH
|
|
103
|
+
// resolution before consulting old NPX cache entries; no version sorting or
|
|
104
|
+
// downloading. This fallback never overrides an already running host.
|
|
105
|
+
const normal = candidates.find(item => item.source === 'PATH')
|
|
106
|
+
|| candidates.find(item => item.source === 'npm-global')
|
|
107
|
+
|| candidates.find(item => item.source === 'project' || item.source === 'source')
|
|
108
|
+
if (normal) return normal.cli
|
|
109
|
+
if (candidates.length === 1) return candidates[0].cli
|
|
110
|
+
// Multiple cache-only versions are not evidence of the user's choice.
|
|
111
111
|
// A live host normally bypasses this fallback via its native handshake.
|
|
112
112
|
const prompt = options.prompt || (process.stdin.isTTY && process.stdout.isTTY ? async message => {
|
|
113
113
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
package/bin/native-recovery.mjs
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
/** Verify a restored, plugin-free DSH through the same native control adapter.
|
|
2
|
-
* Only the first-install rollback uses this; ordinary updates probe the plugin.
|
|
3
|
-
* Bundled outside the profile so neither a failed add nor rollback removes it. */
|
|
4
|
-
import fs from 'node:fs'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { randomBytes } from 'node:crypto'
|
|
7
|
-
import { attachControl, waitForJson } from './native-control.mjs'
|
|
8
|
-
|
|
9
|
-
export async function verifyNativeRestore(job, start, sessionIds, readableIds) {
|
|
10
|
-
const directory = path.join(job.home, 'harness-remote-updates', randomBytes(16).toString('hex'))
|
|
11
|
-
fs.mkdirSync(directory, { mode: 0o700 })
|
|
12
|
-
fs.writeFileSync(path.join(directory, 'package.json'), '{"type":"module"}\n', { mode: 0o600 })
|
|
13
|
-
const helper = path.join(directory, 'install-control.js'), token = randomBytes(24).toString('hex')
|
|
14
|
-
fs.copyFileSync(path.join(job.directory, 'install-control.js'), helper)
|
|
15
|
-
const remove = attachControl(job.profile, helper, { directory, token, pnpm: job.pnpm })
|
|
16
|
-
let ref
|
|
17
|
-
const request = async (method, payload = {}) => {
|
|
18
|
-
const response = await fetch(ref.origin + '/' + method, { method: 'POST', headers: {
|
|
19
|
-
authorization: `Bearer ${token}`, 'content-type': 'application/json',
|
|
20
|
-
}, body: JSON.stringify(payload), redirect: 'error', signal: AbortSignal.timeout(12000) })
|
|
21
|
-
const value = await response.json()
|
|
22
|
-
if (!response.ok) throw new Error('原 DSH 恢复检查未通过。')
|
|
23
|
-
return value
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const child = start()
|
|
27
|
-
ref = await waitForJson(path.join(directory, 'control-ready.json'), value =>
|
|
28
|
-
Number.isInteger(value.pid) && value.pid > 0 && (!child || value.pid === child.pid)
|
|
29
|
-
&& /^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(value.origin), 180000)
|
|
30
|
-
const host = await request('describe')
|
|
31
|
-
if (host.pid !== ref.pid || host.home !== job.home || host.profile !== job.profile
|
|
32
|
-
|| host.cli !== job.cli || host.dshVersion !== job.dshVersion || host.pluginVersion !== '0.0.0') {
|
|
33
|
-
throw new Error('恢复后的 DSH 身份不匹配。')
|
|
34
|
-
}
|
|
35
|
-
const items = (await request('read', { method: 'session.list' })).items
|
|
36
|
-
if (!Array.isArray(items) || JSON.stringify(items.map(s => s.sessionId).sort()) !== JSON.stringify(sessionIds)) {
|
|
37
|
-
throw new Error('恢复后的会话列表不一致。')
|
|
38
|
-
}
|
|
39
|
-
for (const sessionId of readableIds) await request('read', { method: 'session.history', payload: { sessionId, maxMessages: 1 } })
|
|
40
|
-
} finally {
|
|
41
|
-
try { remove() }
|
|
42
|
-
finally { if (ref) { try { await request('close') } catch {} } }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
1
|
+
/** Verify a restored, plugin-free DSH through the same native control adapter.
|
|
2
|
+
* Only the first-install rollback uses this; ordinary updates probe the plugin.
|
|
3
|
+
* Bundled outside the profile so neither a failed add nor rollback removes it. */
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { randomBytes } from 'node:crypto'
|
|
7
|
+
import { attachControl, waitForJson } from './native-control.mjs'
|
|
8
|
+
|
|
9
|
+
export async function verifyNativeRestore(job, start, sessionIds, readableIds) {
|
|
10
|
+
const directory = path.join(job.home, 'harness-remote-updates', randomBytes(16).toString('hex'))
|
|
11
|
+
fs.mkdirSync(directory, { mode: 0o700 })
|
|
12
|
+
fs.writeFileSync(path.join(directory, 'package.json'), '{"type":"module"}\n', { mode: 0o600 })
|
|
13
|
+
const helper = path.join(directory, 'install-control.js'), token = randomBytes(24).toString('hex')
|
|
14
|
+
fs.copyFileSync(path.join(job.directory, 'install-control.js'), helper)
|
|
15
|
+
const remove = attachControl(job.profile, helper, { directory, token, pnpm: job.pnpm })
|
|
16
|
+
let ref
|
|
17
|
+
const request = async (method, payload = {}) => {
|
|
18
|
+
const response = await fetch(ref.origin + '/' + method, { method: 'POST', headers: {
|
|
19
|
+
authorization: `Bearer ${token}`, 'content-type': 'application/json',
|
|
20
|
+
}, body: JSON.stringify(payload), redirect: 'error', signal: AbortSignal.timeout(12000) })
|
|
21
|
+
const value = await response.json()
|
|
22
|
+
if (!response.ok) throw new Error('原 DSH 恢复检查未通过。')
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const child = start()
|
|
27
|
+
ref = await waitForJson(path.join(directory, 'control-ready.json'), value =>
|
|
28
|
+
Number.isInteger(value.pid) && value.pid > 0 && (!child || value.pid === child.pid)
|
|
29
|
+
&& /^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(value.origin), 180000)
|
|
30
|
+
const host = await request('describe')
|
|
31
|
+
if (host.pid !== ref.pid || host.home !== job.home || host.profile !== job.profile
|
|
32
|
+
|| host.cli !== job.cli || host.dshVersion !== job.dshVersion || host.pluginVersion !== '0.0.0') {
|
|
33
|
+
throw new Error('恢复后的 DSH 身份不匹配。')
|
|
34
|
+
}
|
|
35
|
+
const items = (await request('read', { method: 'session.list' })).items
|
|
36
|
+
if (!Array.isArray(items) || JSON.stringify(items.map(s => s.sessionId).sort()) !== JSON.stringify(sessionIds)) {
|
|
37
|
+
throw new Error('恢复后的会话列表不一致。')
|
|
38
|
+
}
|
|
39
|
+
for (const sessionId of readableIds) await request('read', { method: 'session.history', payload: { sessionId, maxMessages: 1 } })
|
|
40
|
+
} finally {
|
|
41
|
+
try { remove() }
|
|
42
|
+
finally { if (ref) { try { await request('close') } catch {} } }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -4,7 +4,7 @@ import { validateCatalog, compareVersions, trustedReleaseAsset } from '../lib/up
|
|
|
4
4
|
* Tested DSH/platform/architecture lists describe evidence, not admission.
|
|
5
5
|
* Keep trusted artifacts, explicit withdrawals and the no-downgrade floor.
|
|
6
6
|
*/
|
|
7
|
-
export function selectInstallTarget(pinned, remote, current, now = Date.now()) {
|
|
7
|
+
export function selectInstallTarget(pinned, remote, current, now = Date.now(), bundledOnly = false) {
|
|
8
8
|
const local = validateCatalog(pinned.catalog)
|
|
9
9
|
// Choosing an explicit prerelease installer authorizes its exact embedded
|
|
10
10
|
// preview, not arbitrary remote previews or a change to stable recommendations.
|
|
@@ -21,7 +21,7 @@ export function selectInstallTarget(pinned, remote, current, now = Date.now()) {
|
|
|
21
21
|
|
|
22
22
|
// An installer never silently substitutes an older plugin than its own bundle.
|
|
23
23
|
// Keep the bundled bytes/hash for the same version even if remote metadata differs.
|
|
24
|
-
const candidates = [bundled, ...(online?.releases || []).filter(r => r.channel === 'stable'
|
|
24
|
+
const candidates = [bundled, ...(bundledOnly ? [] : online?.releases || []).filter(r => r.channel === 'stable'
|
|
25
25
|
&& compareVersions(r.version, bundled.version) > 0 && trustedReleaseAsset(r.asset, r.version))]
|
|
26
26
|
.sort((a, b) => compareVersions(b.version, a.version))
|
|
27
27
|
const rules = [...local.blocked, ...(online?.blocked || [])]
|
package/bin/setup.mjs
CHANGED
|
@@ -6,11 +6,11 @@ import { spawn } from 'node:child_process'
|
|
|
6
6
|
import { randomBytes, createHash } from 'node:crypto'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { attachControl, waitForJson, waitForInstallControl } from './native-control.mjs'
|
|
9
|
-
import { resolveInstallRuntime, verifyInstallRuntime } from '../lib/install-runtime.js'
|
|
10
|
-
import { installProfile, backupProfile, NativeInstallError } from '../lib/install-profile.js'
|
|
9
|
+
import { resolveInstallRuntime, verifyInstallRuntime } from '../lib/install-runtime.js'
|
|
10
|
+
import { installProfile, backupProfile, NativeInstallError } from '../lib/install-profile.js'
|
|
11
11
|
import { compareVersions } from '../lib/update-policy.js'
|
|
12
12
|
import { selectInstallTarget } from './release-selection.mjs'
|
|
13
|
-
import { boundedFetch, downloadRelease, auditArchive } from '../lib/update-download.js'
|
|
13
|
+
import { boundedFetch, downloadRelease, auditArchive, DownloadUnavailableError } from '../lib/update-download.js'
|
|
14
14
|
import { writePrivateJsonAtomic } from '../lib/secure-file.js'
|
|
15
15
|
import { control, validateJob, releaseOwnedUpdateLock } from '../lib/update-worker.js'
|
|
16
16
|
import { chooseDsh, validateDshCli, resolveHome, mayHaveRunningDsh, assertInstallTarget, installHostArgv } from './dsh-discovery.mjs'
|
|
@@ -25,8 +25,8 @@ function portBusy(port) {
|
|
|
25
25
|
socket.setTimeout(500, () => end(false)); socket.once('connect', () => end(true)); socket.once('error', () => end(false))
|
|
26
26
|
})
|
|
27
27
|
}
|
|
28
|
-
export async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false,
|
|
29
|
-
fetchCatalog = () => boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024)) {
|
|
28
|
+
export async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false,
|
|
29
|
+
fetchCatalog = () => boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024), fetchPackage = fetch) {
|
|
30
30
|
const current = { agentKind: 'dsh', agentVersion: host.dshVersion, pluginVersion: host.pluginVersion,
|
|
31
31
|
platform: { win32: 'windows', darwin: 'macos', linux: 'linux' }[host.platform], arch: host.arch }
|
|
32
32
|
const pinned = JSON.parse(fs.readFileSync(path.join(assetsRoot, 'release.json'), 'utf8'))
|
|
@@ -34,64 +34,80 @@ export async function selectRelease(host, assetsRoot = path.join(root, 'assets')
|
|
|
34
34
|
try {
|
|
35
35
|
remote = JSON.parse((await fetchCatalog()).toString('utf8'))
|
|
36
36
|
} catch { /* Authenticated npm package retains its bundled release. */ }
|
|
37
|
-
const
|
|
37
|
+
const selectionTime = Date.now()
|
|
38
|
+
let release = selectInstallTarget(pinned, remote, current, selectionTime)
|
|
38
39
|
if (host.pluginVersion !== '0.0.0' && (compareVersions(host.pluginVersion, release.version) > 0
|
|
39
40
|
|| (compareVersions(host.pluginVersion, release.version) === 0 && !repair))) return { release, archive: null }
|
|
40
41
|
let archive
|
|
41
42
|
if (release.version === pinned.version) archive = fs.readFileSync(path.join(assetsRoot, 'plugin.tgz'))
|
|
42
|
-
else
|
|
43
|
+
else {
|
|
44
|
+
try { archive = await downloadRelease(release, fetchPackage) }
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (!(error instanceof DownloadUnavailableError)) throw error
|
|
47
|
+
// Keep BOTH local and valid online withdrawals when falling back. Dropping
|
|
48
|
+
// the catalog here would accidentally reinstall a known-broken bundle.
|
|
49
|
+
// Retain rules accepted at selection even if their catalog expires while
|
|
50
|
+
// a slow download is in flight; expiry must not erase a known withdrawal.
|
|
51
|
+
const bundled = selectInstallTarget(pinned, remote, current, selectionTime, true)
|
|
52
|
+
if (host.pluginVersion !== '0.0.0' && compareVersions(host.pluginVersion, bundled.version) > 0) throw error
|
|
53
|
+
release = bundled
|
|
54
|
+
console.log(`较新版本暂时无法下载,使用安装器内置的 ${release.version};未升级到线上最新版本。`)
|
|
55
|
+
if (host.pluginVersion === release.version && !repair) return { release, archive: null }
|
|
56
|
+
archive = fs.readFileSync(path.join(assetsRoot, 'plugin.tgz'))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
43
59
|
auditArchive(archive, release)
|
|
44
|
-
return { release, archive }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** With no running host, native plugin add needs neither HMR nor healthy third-
|
|
48
|
-
* party plugins. Reuse the same installation core; only live hosts need disposal. */
|
|
49
|
-
async function installStopped({ cli, home, profile, profileName, directory, id, runtime, assetsRoot, repair, open }) {
|
|
50
|
-
const dsh = validateDshCli(cli)
|
|
51
|
-
let version = '0.0.0'
|
|
52
|
-
try { version = JSON.parse(fs.readFileSync(path.join(profile, 'node_modules', packageName, 'package.json'), 'utf8')).version } catch {}
|
|
53
|
-
const selected = await selectRelease({ dshVersion: dsh.version, pluginVersion: version, platform: process.platform, arch: process.arch }, assetsRoot, repair)
|
|
54
|
-
fs.mkdirSync(profile, { recursive: true, mode: 0o700 })
|
|
55
|
-
if (fs.realpathSync(profile) !== profile) throw new Error('无法确认 DSH 配置目录的实际位置。')
|
|
56
|
-
const lock = path.join(profile, '.harness-remote-update.lock')
|
|
57
|
-
const fd = fs.openSync(lock, 'wx', 0o600); fs.writeFileSync(fd, id); fs.closeSync(fd)
|
|
58
|
-
let modified = false, keepLock = false
|
|
59
|
-
try {
|
|
60
|
-
// Recheck immediately before writing: a host may have started during npm
|
|
61
|
-
// discovery/download. Never mutate its dependencies underneath that process.
|
|
62
|
-
if (await mayHaveRunningDsh()) throw new Error('DSH 已开始运行,请重新执行安装命令以连接该实例。')
|
|
63
|
-
if (selected.archive) {
|
|
64
|
-
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
65
|
-
backupProfile(profile, path.join(directory, 'profile-before'))
|
|
66
|
-
console.log(`正在通过 DSH 原生方式安装插件 ${selected.release.version}…`)
|
|
67
|
-
modified = true
|
|
68
|
-
await installProfile({ profile, directory, cli: dsh.cli, targetVersion: selected.release.version, runtime })
|
|
69
|
-
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
keepLock = error instanceof NativeInstallError && error.mayStillBeRunning
|
|
72
|
-
if (modified && !keepLock) {
|
|
73
|
-
fs.renameSync(profile, path.join(directory, 'profile-failed'))
|
|
74
|
-
fs.renameSync(path.join(directory, 'profile-before'), profile)
|
|
75
|
-
}
|
|
76
|
-
throw error
|
|
77
|
-
} finally { if (!keepLock) releaseOwnedUpdateLock(lock, id) }
|
|
78
|
-
// Use DSH's own browser opening behavior. Startup failure does not undo an
|
|
79
|
-
// otherwise successful native add; an unrelated plugin can fail to boot.
|
|
80
|
-
const logFile = path.join(directory, 'startup.log'), log = fs.openSync(logFile, 'a', 0o600)
|
|
81
|
-
const env = { ...process.env, DSH_HOME: home }
|
|
82
|
-
delete env.HARNESS_REMOTE_UPDATE_JOB
|
|
83
|
-
const child = spawn(process.execPath, [dsh.cli, '--profile', profileName, ...(open ? [] : ['--no-open'])],
|
|
84
|
-
{ cwd: process.cwd(), env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
85
|
-
fs.closeSync(log)
|
|
86
|
-
let launchError
|
|
87
|
-
child.on('error', error => { launchError = error }); child.unref()
|
|
88
|
-
await sleep(1200)
|
|
89
|
-
const starting = !launchError && child.pid && child.exitCode === null && child.signalCode === null
|
|
90
|
-
const installedVersion = selected.archive ? selected.release.version : version
|
|
91
|
-
console.log(`插件 ${installedVersion} 已安装。${starting ? '已启动 DSH,请等待 WebUI 就绪。' : `DSH 未能启动,请查看:${logFile}`}`)
|
|
92
|
-
return { version: installedVersion, changed: Boolean(selected.archive), starting: Boolean(starting) }
|
|
93
|
-
}
|
|
94
|
-
export async function install({ profileName = 'web', cli, home: configuredHome, assetsRoot, open = true, repair = false } = {}) {
|
|
60
|
+
return { release, archive }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** With no running host, native plugin add needs neither HMR nor healthy third-
|
|
64
|
+
* party plugins. Reuse the same installation core; only live hosts need disposal. */
|
|
65
|
+
async function installStopped({ cli, home, profile, profileName, directory, id, runtime, assetsRoot, repair, open }) {
|
|
66
|
+
const dsh = validateDshCli(cli)
|
|
67
|
+
let version = '0.0.0'
|
|
68
|
+
try { version = JSON.parse(fs.readFileSync(path.join(profile, 'node_modules', packageName, 'package.json'), 'utf8')).version } catch {}
|
|
69
|
+
const selected = await selectRelease({ dshVersion: dsh.version, pluginVersion: version, platform: process.platform, arch: process.arch }, assetsRoot, repair)
|
|
70
|
+
fs.mkdirSync(profile, { recursive: true, mode: 0o700 })
|
|
71
|
+
if (fs.realpathSync(profile) !== profile) throw new Error('无法确认 DSH 配置目录的实际位置。')
|
|
72
|
+
const lock = path.join(profile, '.harness-remote-update.lock')
|
|
73
|
+
const fd = fs.openSync(lock, 'wx', 0o600); fs.writeFileSync(fd, id); fs.closeSync(fd)
|
|
74
|
+
let modified = false, keepLock = false
|
|
75
|
+
try {
|
|
76
|
+
// Recheck immediately before writing: a host may have started during npm
|
|
77
|
+
// discovery/download. Never mutate its dependencies underneath that process.
|
|
78
|
+
if (await mayHaveRunningDsh()) throw new Error('DSH 已开始运行,请重新执行安装命令以连接该实例。')
|
|
79
|
+
if (selected.archive) {
|
|
80
|
+
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
81
|
+
backupProfile(profile, path.join(directory, 'profile-before'))
|
|
82
|
+
console.log(`正在通过 DSH 原生方式安装插件 ${selected.release.version}…`)
|
|
83
|
+
modified = true
|
|
84
|
+
await installProfile({ profile, directory, cli: dsh.cli, targetVersion: selected.release.version, runtime })
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
keepLock = error instanceof NativeInstallError && error.mayStillBeRunning
|
|
88
|
+
if (modified && !keepLock) {
|
|
89
|
+
fs.renameSync(profile, path.join(directory, 'profile-failed'))
|
|
90
|
+
fs.renameSync(path.join(directory, 'profile-before'), profile)
|
|
91
|
+
}
|
|
92
|
+
throw error
|
|
93
|
+
} finally { if (!keepLock) releaseOwnedUpdateLock(lock, id) }
|
|
94
|
+
// Use DSH's own browser opening behavior. Startup failure does not undo an
|
|
95
|
+
// otherwise successful native add; an unrelated plugin can fail to boot.
|
|
96
|
+
const logFile = path.join(directory, 'startup.log'), log = fs.openSync(logFile, 'a', 0o600)
|
|
97
|
+
const env = { ...process.env, DSH_HOME: home }
|
|
98
|
+
delete env.HARNESS_REMOTE_UPDATE_JOB
|
|
99
|
+
const child = spawn(process.execPath, [dsh.cli, '--profile', profileName, ...(open ? [] : ['--no-open'])],
|
|
100
|
+
{ cwd: process.cwd(), env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
101
|
+
fs.closeSync(log)
|
|
102
|
+
let launchError
|
|
103
|
+
child.on('error', error => { launchError = error }); child.unref()
|
|
104
|
+
await sleep(1200)
|
|
105
|
+
const starting = !launchError && child.pid && child.exitCode === null && child.signalCode === null
|
|
106
|
+
const installedVersion = selected.archive ? selected.release.version : version
|
|
107
|
+
console.log(`插件 ${installedVersion} 已安装。${starting ? '已启动 DSH,请等待 WebUI 就绪。' : `DSH 未能启动,请查看:${logFile}`}`)
|
|
108
|
+
return { version: installedVersion, changed: Boolean(selected.archive), starting: Boolean(starting) }
|
|
109
|
+
}
|
|
110
|
+
export async function install({ profileName = 'web', cli, home: configuredHome, assetsRoot, open = true, repair = false } = {}) {
|
|
95
111
|
if (!/^[A-Za-z0-9_-]{1,80}$/.test(profileName)) throw new Error('无效的 profile 名称。')
|
|
96
112
|
if (cli) cli = validateDshCli(cli).cli
|
|
97
113
|
const runtime = resolveInstallRuntime(root); await verifyInstallRuntime(runtime)
|
|
@@ -103,16 +119,16 @@ export async function install({ profileName = 'web', cli, home: configuredHome,
|
|
|
103
119
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
104
120
|
let remove, ref, job, locked = false, authorized = false, launched
|
|
105
121
|
const lockFile = path.join(profile, '.harness-remote-update.lock')
|
|
106
|
-
console.log('正在检查 DSH 与插件…')
|
|
107
|
-
try {
|
|
108
|
-
if (!await mayHaveRunningDsh()) {
|
|
109
|
-
cli ||= await chooseDsh()
|
|
110
|
-
return await installStopped({ cli, home, profile, profileName, directory, id, runtime, assetsRoot, repair, open })
|
|
111
|
-
}
|
|
112
|
-
// A live host must identify the same home/profile; never synthesize a second
|
|
113
|
-
// profile just because this terminal inherited different environment values.
|
|
114
|
-
if (!fs.existsSync(path.join(profile, 'package.json'))) {
|
|
115
|
-
throw new Error('当前目录下没有此 DSH 配置,但检测到 DSH 正在运行。请使用相同的 DSH_HOME/--home 和 --profile,未修改正在运行的实例。')
|
|
122
|
+
console.log('正在检查 DSH 与插件…')
|
|
123
|
+
try {
|
|
124
|
+
if (!await mayHaveRunningDsh()) {
|
|
125
|
+
cli ||= await chooseDsh()
|
|
126
|
+
return await installStopped({ cli, home, profile, profileName, directory, id, runtime, assetsRoot, repair, open })
|
|
127
|
+
}
|
|
128
|
+
// A live host must identify the same home/profile; never synthesize a second
|
|
129
|
+
// profile just because this terminal inherited different environment values.
|
|
130
|
+
if (!fs.existsSync(path.join(profile, 'package.json'))) {
|
|
131
|
+
throw new Error('当前目录下没有此 DSH 配置,但检测到 DSH 正在运行。请使用相同的 DSH_HOME/--home 和 --profile,未修改正在运行的实例。')
|
|
116
132
|
}
|
|
117
133
|
const fd = fs.openSync(lockFile, 'wx', 0o600); fs.writeFileSync(fd, id); fs.closeSync(fd); locked = true
|
|
118
134
|
const helper = path.join(directory, 'install-control.js')
|
|
@@ -155,28 +171,28 @@ export async function install({ profileName = 'web', cli, home: configuredHome,
|
|
|
155
171
|
assertInstallTarget(host, { home, profile, cli, pid: launched?.pid || ref.pid })
|
|
156
172
|
remove(); remove = undefined
|
|
157
173
|
await sleep(600)
|
|
158
|
-
let selected = await selectRelease(host, assetsRoot, repair)
|
|
159
|
-
if (!selected.archive && host.pluginVersion === selected.release.version) {
|
|
160
|
-
// A version string on disk is not proof that a prior restart completed.
|
|
161
|
-
// Re-running the normal command must also repair a stuck installed copy.
|
|
162
|
-
let ready = false
|
|
163
|
-
try {
|
|
164
|
-
const response = await fetch(`http://127.0.0.1:${host.localPort}/gate/status`, { signal: AbortSignal.timeout(5000), redirect: 'error' })
|
|
165
|
-
await response.arrayBuffer(); ready = response.ok
|
|
166
|
-
} catch {}
|
|
167
|
-
if (!ready) {
|
|
168
|
-
console.log('检测到已安装插件尚未就绪,正在重新安装修复…')
|
|
169
|
-
selected = await selectRelease(host, assetsRoot, true)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
174
|
+
let selected = await selectRelease(host, assetsRoot, repair)
|
|
175
|
+
if (!selected.archive && host.pluginVersion === selected.release.version) {
|
|
176
|
+
// A version string on disk is not proof that a prior restart completed.
|
|
177
|
+
// Re-running the normal command must also repair a stuck installed copy.
|
|
178
|
+
let ready = false
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(`http://127.0.0.1:${host.localPort}/gate/status`, { signal: AbortSignal.timeout(5000), redirect: 'error' })
|
|
181
|
+
await response.arrayBuffer(); ready = response.ok
|
|
182
|
+
} catch {}
|
|
183
|
+
if (!ready) {
|
|
184
|
+
console.log('检测到已安装插件尚未就绪,正在重新安装修复…')
|
|
185
|
+
selected = await selectRelease(host, assetsRoot, true)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
172
188
|
if (!selected.archive) { console.log(`插件 ${host.pluginVersion} 无需更新。`); return { version: host.pluginVersion, changed: false } }
|
|
173
189
|
console.log(`正在安装插件 ${selected.release.version},保留原配对与会话…`)
|
|
174
190
|
job = { ...host, argv: installHostArgv(host), id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
|
|
175
191
|
previousVersion: host.pluginVersion, statusToken: token, controlOrigin: ref.origin }
|
|
176
192
|
validateJob(job)
|
|
177
193
|
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
178
|
-
fs.copyFileSync(path.join(root, 'lib/update-worker.js'), path.join(directory, 'update-worker.js'))
|
|
179
|
-
if (host.pluginVersion === '0.0.0') fs.copyFileSync(path.join(root, 'lib/native-recovery.js'), path.join(directory, 'native-recovery.js'))
|
|
194
|
+
fs.copyFileSync(path.join(root, 'lib/update-worker.js'), path.join(directory, 'update-worker.js'))
|
|
195
|
+
if (host.pluginVersion === '0.0.0') fs.copyFileSync(path.join(root, 'lib/native-recovery.js'), path.join(directory, 'native-recovery.js'))
|
|
180
196
|
writePrivateJsonAtomic(path.join(directory, 'job.json'), job)
|
|
181
197
|
await control(job, 'launch')
|
|
182
198
|
const ready = await waitForJson(path.join(directory, 'worker-ready.json'), v => v.id === id)
|
|
@@ -184,9 +200,9 @@ export async function install({ profileName = 'web', cli, home: configuredHome,
|
|
|
184
200
|
writePrivateJsonAtomic(path.join(home, 'harness-remote-updates', `profile-${scope}.json`), { jobId: id, statusOrigin: ready.origin, statusToken: token })
|
|
185
201
|
writePrivateJsonAtomic(path.join(directory, 'authorized.json'), { id }); authorized = true
|
|
186
202
|
let previous = '', result
|
|
187
|
-
// Native download/install has its own bounded deadline; allow its restart
|
|
188
|
-
// and possible rollback to finish before reporting an unknown outcome.
|
|
189
|
-
const until = Date.now() + 1200000
|
|
203
|
+
// Native download/install has its own bounded deadline; allow its restart
|
|
204
|
+
// and possible rollback to finish before reporting an unknown outcome.
|
|
205
|
+
const until = Date.now() + 1200000
|
|
190
206
|
while (Date.now() < until) {
|
|
191
207
|
try { result = JSON.parse(fs.readFileSync(path.join(directory, 'result.json'), 'utf8')) } catch {}
|
|
192
208
|
if (result?.phase !== previous && result?.phase) { console.log(`${result.progress}% ${result.message}`); previous = result.phase }
|
|
@@ -205,12 +221,12 @@ export async function install({ profileName = 'web', cli, home: configuredHome,
|
|
|
205
221
|
} catch { console.log('插件已更新;请返回原 DSH WebUI 查看。') }
|
|
206
222
|
}
|
|
207
223
|
return { version: selected.release.version, changed: true }
|
|
208
|
-
} finally {
|
|
209
|
-
try { remove?.() }
|
|
210
|
-
finally {
|
|
211
|
-
if (ref && !authorized) { try { await control({ controlOrigin: ref.origin, statusToken: token }, 'close') } catch {} }
|
|
212
|
-
if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
|
|
213
|
-
}
|
|
224
|
+
} finally {
|
|
225
|
+
try { remove?.() }
|
|
226
|
+
finally {
|
|
227
|
+
if (ref && !authorized) { try { await control({ controlOrigin: ref.origin, statusToken: token }, 'close') } catch {} }
|
|
228
|
+
if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
|
|
229
|
+
}
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
232
|
export function parseArguments(args) {
|
|
@@ -228,6 +244,6 @@ export function parseArguments(args) {
|
|
|
228
244
|
}
|
|
229
245
|
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
230
246
|
const args = process.argv.slice(2)
|
|
231
|
-
if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n支持全局安装与 npx 使用的 DSH,已启动或关闭均可;请在相同系统账号下执行。Windows 可使用 npx.cmd。\n可选:--profile <名称>(默认 web);--home <DSH 数据目录>;--dsh-cli <DSH 的 lib/bin.js>;--repair(重新安装,不降级)')
|
|
247
|
+
if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n支持全局安装与 npx 使用的 DSH,已启动或关闭均可;请在相同系统账号下执行。Windows 可使用 npx.cmd。\n可选:--profile <名称>(默认 web);--home <DSH 数据目录>;--dsh-cli <DSH 的 lib/bin.js>;--repair(重新安装,不降级)')
|
|
232
248
|
else Promise.resolve().then(() => install(parseArguments(args))).catch(error => { console.error(error.message); process.exitCode = 1 })
|
|
233
249
|
}
|
package/lib/install-control.js
CHANGED
|
@@ -912,41 +912,65 @@ async function historyValue(gateway, request, signal) {
|
|
|
912
912
|
const sessionId = typeof request.sessionId === "string" ? request.sessionId : "";
|
|
913
913
|
if (!sessionId) throw new Error("session.history requires sessionId");
|
|
914
914
|
const maxMessages = Number.isSafeInteger(request.maxMessages) ? Math.max(1, Math.min(30, Number(request.maxMessages))) : 8;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (Number.isSafeInteger(request.beforeSeq)) {
|
|
915
|
+
return createHistoryPageReader(gateway, sessionId, signal)({
|
|
916
|
+
maxMessages,
|
|
917
|
+
...Number.isSafeInteger(request.beforeSeq) ? { beforeSeq: Number(request.beforeSeq) } : {}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function createHistoryPageReader(gateway, sessionId, signal) {
|
|
921
|
+
let address;
|
|
922
|
+
let opening;
|
|
923
|
+
const readPage = async (request, throughSeq) => {
|
|
924
|
+
const resolved = await (address ??= resolveDshSessionAddress(gateway, sessionId, signal));
|
|
925
|
+
signal.throwIfAborted();
|
|
927
926
|
const page = recordOf(await gateway.invoke({
|
|
928
927
|
namespace: "session",
|
|
929
928
|
method: "page",
|
|
930
|
-
args: {
|
|
931
|
-
request: {
|
|
932
|
-
address,
|
|
933
|
-
throughSeq: first.cursor,
|
|
934
|
-
beforeSeq: request.beforeSeq,
|
|
935
|
-
maxMessages
|
|
936
|
-
}
|
|
937
|
-
},
|
|
929
|
+
args: { request: { address: resolved, throughSeq, beforeSeq: request.beforeSeq, maxMessages: request.maxMessages } },
|
|
938
930
|
signal
|
|
939
931
|
})) ?? {};
|
|
932
|
+
signal.throwIfAborted();
|
|
933
|
+
return { events: historyEvents(page.records), hasMore: page.hasMore === true };
|
|
934
|
+
};
|
|
935
|
+
return async (request) => {
|
|
936
|
+
signal.throwIfAborted();
|
|
937
|
+
if (!opening && request.beforeSeq !== void 0) {
|
|
938
|
+
return readPage(request, request.beforeSeq - 1);
|
|
939
|
+
}
|
|
940
|
+
if (!opening) {
|
|
941
|
+
opening = (async () => {
|
|
942
|
+
const resolved = await (address ??= resolveDshSessionAddress(gateway, sessionId, signal));
|
|
943
|
+
signal.throwIfAborted();
|
|
944
|
+
const maxMessages2 = request.maxMessages;
|
|
945
|
+
const first2 = recordOf(await firstStreamFrame(gateway, "session", "follow", {
|
|
946
|
+
request: { address: resolved, maxMessages: maxMessages2 }
|
|
947
|
+
}, signal));
|
|
948
|
+
signal.throwIfAborted();
|
|
949
|
+
if (first2?.type !== "snapshot" || !Number.isSafeInteger(first2.cursor) || Number(first2.cursor) < -1) {
|
|
950
|
+
throw new Error("session/follow returned an invalid opening snapshot");
|
|
951
|
+
}
|
|
952
|
+
return { first: first2, maxMessages: maxMessages2 };
|
|
953
|
+
})();
|
|
954
|
+
}
|
|
955
|
+
const { first, maxMessages } = await opening;
|
|
956
|
+
signal.throwIfAborted();
|
|
957
|
+
const latest = request.beforeSeq === void 0;
|
|
958
|
+
if (latest && request.maxMessages === maxMessages) {
|
|
959
|
+
return {
|
|
960
|
+
events: historyEvents(first.records),
|
|
961
|
+
hasMore: first.hasMore === true,
|
|
962
|
+
projections: withPresentationProjections(first.projections),
|
|
963
|
+
historyEndSeq: first.cursor
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
const page = await readPage(request, Number(first.cursor));
|
|
940
967
|
return {
|
|
941
|
-
|
|
942
|
-
|
|
968
|
+
...page,
|
|
969
|
+
...latest ? {
|
|
970
|
+
projections: withPresentationProjections(first.projections),
|
|
971
|
+
historyEndSeq: first.cursor
|
|
972
|
+
} : {}
|
|
943
973
|
};
|
|
944
|
-
}
|
|
945
|
-
return {
|
|
946
|
-
events: historyEvents(first.records),
|
|
947
|
-
hasMore: first.hasMore === true,
|
|
948
|
-
projections: withPresentationProjections(first.projections),
|
|
949
|
-
historyEndSeq: first.cursor
|
|
950
974
|
};
|
|
951
975
|
}
|
|
952
976
|
async function workspaceValue(gateway, signal) {
|