dsh-manage-session 0.5.4 → 0.5.7
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.ja.md +22 -0
- package/CHANGELOG.ko.md +22 -0
- package/CHANGELOG.md +22 -0
- package/README.en.md +13 -11
- package/README.md +11 -10
- package/dsh.plugin.json +2 -2
- package/lib/client.js +900 -386
- package/lib/index.d.ts +241 -16
- package/lib/index.js +526 -71
- package/package.json +15 -9
- package/src/client/card.tsx +14 -10
- package/src/client/index.ts +53 -15
- package/src/client/locales.ts +36 -0
- package/src/client/panel.tsx +26 -36
- package/src/client/search/host-api.ts +5 -0
- package/src/client/search/locales.ts +81 -0
- package/src/client/search/locate.ts +330 -0
- package/src/client/search/manage.tsx +6 -2
- package/src/client/search/panel.tsx +154 -29
- package/src/client/session-menu-legacy.ts +249 -0
- package/src/client/session-menu.tsx +96 -0
- package/src/config.ts +17 -0
- package/src/host/history/tombstones.ts +100 -0
- package/src/host/index/drs-source.ts +211 -0
- package/src/host/index/engine.ts +699 -571
- package/src/host/index/rebuild.ts +432 -368
- package/src/host/index/sync.ts +413 -286
- package/src/host/settings-bridge.ts +27 -0
- package/src/index.ts +1499 -1421
- package/src/client/history-panel.tsx +0 -319
package/src/config.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface StewardConfig {
|
|
|
21
21
|
healthCheck?: boolean
|
|
22
22
|
/** 搜索索引子域(`index-*` 路由与侧栏搜索入口)。关闭后索引子域整体停摆。 */
|
|
23
23
|
search?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* 直接删除入口(2026-10-20 用户定标):开启后搜索面板结果行出现行内
|
|
26
|
+
* 「删除」按钮(两步确认,走管理端同款 session-history-purge 链路)。
|
|
27
|
+
* **缺省关**——破坏性入口不默认铺开;管理端批量删除不受本开关限制。
|
|
28
|
+
*/
|
|
29
|
+
directDelete?: boolean
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** 缺省值。 */
|
|
@@ -29,6 +35,7 @@ export const DEFAULT_CONFIG: Required<StewardConfig> = {
|
|
|
29
35
|
historyFiles: true,
|
|
30
36
|
healthCheck: true,
|
|
31
37
|
search: true,
|
|
38
|
+
directDelete: false,
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
/** host 半身注册的设置命名空间(与 src/index.ts 保持一致)。 */
|
|
@@ -69,6 +76,14 @@ export interface SwitchSearchConfig {
|
|
|
69
76
|
archiveKeep?: number
|
|
70
77
|
/** 独立索引文件的可选绝对目录。 */
|
|
71
78
|
indexDir?: string
|
|
79
|
+
/**
|
|
80
|
+
* 语料源(T7.1):`'native'`=sessionQuery 服务 + 转录文件兜底(缺省);
|
|
81
|
+
* `'drs'`=dsh-reference-session-collector 投影库(docs 直插;DRS 不可达时整轮
|
|
82
|
+
* 回退 native 链路,水位语义切换的首轮会全量重灌一次)。
|
|
83
|
+
*/
|
|
84
|
+
corpusSource?: 'native' | 'drs'
|
|
85
|
+
/** DRS 投影库 API 基址;空=自动(首个 trustedHosts 条目,再退 http://127.0.0.1:8080)。 */
|
|
86
|
+
drsBaseUrl?: string
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/** 搜索子域缺省值。 */
|
|
@@ -79,4 +94,6 @@ export const SWITCH_DEFAULT_CONFIG: Required<SwitchSearchConfig> = {
|
|
|
79
94
|
syncIntervalMs: 30_000,
|
|
80
95
|
archiveKeep: 2,
|
|
81
96
|
indexDir: '',
|
|
97
|
+
corpusSource: 'native',
|
|
98
|
+
drsBaseUrl: '',
|
|
82
99
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 退场墓碑台账(2026-10-20 用户定标):任何 purge 成功路径都往
|
|
3
|
+
* `~/.dsh-switch-search/retired-tombstones.jsonl` 追加一行
|
|
4
|
+
* `{sessionId, retiredAt, source}`,作为离线清残脚本
|
|
5
|
+
* (scripts/cleanup-retired.mjs)的驱动清单。
|
|
6
|
+
*
|
|
7
|
+
* 为什么需要:宿主 `registry.deleteSession` 与 `purgeHistory` 各自覆盖 4 处
|
|
8
|
+
* 实体(转录目录/projcache/workspace.json 两处 id),但任一路径中途失败、
|
|
9
|
+
* 索引联动(onPurged)进程崩溃、或 collector 同步轮未跑,都会留下残行/残
|
|
10
|
+
* 目录。墓碑 = 「这个 id 已被判退场」的持久凭证,DSH 关闭后脚本据此兜底
|
|
11
|
+
* 清扫,不靠现场推断。
|
|
12
|
+
*
|
|
13
|
+
* 纪律:
|
|
14
|
+
* - **append-only、best-effort**:写台账失败绝不影响删除结果返回(try/catch
|
|
15
|
+
* 吞错并 log),台账是副账不是主账;
|
|
16
|
+
* - 每行一个 JSON(JSONL),读侧跳过畸形行——半行(进程崩在 append 中间)
|
|
17
|
+
* 不致命;
|
|
18
|
+
* - 幂等消费:脚本按 id 集合去重,重复墓碑无害;台账只增不删(体积可忽略,
|
|
19
|
+
* 每行 ~120B)。
|
|
20
|
+
*/
|
|
21
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { homedir } from 'node:os'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
|
|
25
|
+
/** 台账文件名(固定;脚本以默认路径发现)。 */
|
|
26
|
+
export const TOMBSTONE_FILE = 'retired-tombstones.jsonl'
|
|
27
|
+
|
|
28
|
+
/** 一条退场墓碑。 */
|
|
29
|
+
export interface RetiredTombstone {
|
|
30
|
+
sessionId: string
|
|
31
|
+
/** ISO 时间串(写入时刻)。 */
|
|
32
|
+
retiredAt: string
|
|
33
|
+
/** 落账来源:内存全痕删除 / 文件兜底删除 / 测试注入。 */
|
|
34
|
+
source: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 默认台账目录 = 索引缺省落点(与 rebuild.ts 的 ~/.dsh-switch-search 同根)。 */
|
|
38
|
+
export function defaultTombstoneDir(): string {
|
|
39
|
+
return join(homedir(), '.dsh-switch-search')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 台账文件绝对路径。 */
|
|
43
|
+
export function tombstonePathFor(dir: string): string {
|
|
44
|
+
return join(dir, TOMBSTONE_FILE)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 追加一批墓碑(一次 purge 成功的 id 集)。best-effort:目录不可建/盘不可写
|
|
49
|
+
* 只经 `log` 报一行,绝不抛——删除动作的成败判定先于本调用完成。
|
|
50
|
+
* @returns 实际写出的行数(0 = 静默失败或空入参)。
|
|
51
|
+
*/
|
|
52
|
+
export function appendRetiredTombstones(
|
|
53
|
+
sessionIds: readonly string[],
|
|
54
|
+
opts: { dir?: string; source: string; now?: () => Date; log?: (msg: string) => void },
|
|
55
|
+
): number {
|
|
56
|
+
const ids = [...new Set(sessionIds.filter((id) => typeof id === 'string' && id !== ''))]
|
|
57
|
+
if (ids.length === 0) return 0
|
|
58
|
+
const dir = opts.dir ?? defaultTombstoneDir()
|
|
59
|
+
const now = opts.now ?? (() => new Date())
|
|
60
|
+
try {
|
|
61
|
+
mkdirSync(dir, { recursive: true })
|
|
62
|
+
const stamp = now().toISOString()
|
|
63
|
+
const lines = ids.map((id) => JSON.stringify({ sessionId: id, retiredAt: stamp, source: opts.source } satisfies RetiredTombstone) + '\n')
|
|
64
|
+
appendFileSync(join(dir, TOMBSTONE_FILE), lines.join(''), 'utf8')
|
|
65
|
+
return lines.length
|
|
66
|
+
} catch (err) {
|
|
67
|
+
opts.log?.(`tombstone append failed (deletion itself succeeded): ${String(err instanceof Error ? err.message : err)}`)
|
|
68
|
+
return 0
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读台账(脚本与单测共用):畸形行/半行跳过,缺文件回空数组。
|
|
74
|
+
*/
|
|
75
|
+
export function readRetiredTombstones(file: string): RetiredTombstone[] {
|
|
76
|
+
let raw: string
|
|
77
|
+
try {
|
|
78
|
+
raw = readFileSync(file, 'utf8')
|
|
79
|
+
} catch {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
const out: RetiredTombstone[] = []
|
|
83
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
84
|
+
const trimmed = line.trim()
|
|
85
|
+
if (trimmed === '') continue
|
|
86
|
+
try {
|
|
87
|
+
const record = JSON.parse(trimmed) as Record<string, unknown>
|
|
88
|
+
if (typeof record['sessionId'] === 'string' && record['sessionId'] !== '') {
|
|
89
|
+
out.push({
|
|
90
|
+
sessionId: record['sessionId'],
|
|
91
|
+
retiredAt: typeof record['retiredAt'] === 'string' ? record['retiredAt'] : '',
|
|
92
|
+
source: typeof record['source'] === 'string' ? record['source'] : '',
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
/* 半行/脏行:跳过(append 撕裂的预期形态) */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRS 语料源:dsh-reference-session-collector 投影库的 HTTP 只读客户端(T7.1)。
|
|
3
|
+
*
|
|
4
|
+
* 语料契约 = DRS 单条 fenced 路由 `GET /api/dsh-reference-session-collector`:
|
|
5
|
+
* - `op=sessions&offset=&limit=` → `{ok, total, rows:[sessions 行]}`(分页);
|
|
6
|
+
* - `op=session&session=<id>&fields=search` → `{ok, session, rows:[检索臂]}`。
|
|
7
|
+
*
|
|
8
|
+
* 字段对照(T7.0,DRS wire snake → steward 引擎列):
|
|
9
|
+
* sessions.version ← artifact_mtime_ms(水位代理:日志重写必 bump mtime);
|
|
10
|
+
* sessions.updated_at ← last_time_ms;title/cwd/archived ← 同名列;
|
|
11
|
+
* docs.type ← kind;docs.surface ← on_surface(1='current'/0='shadowed');
|
|
12
|
+
* docs.time ← time;docs.text ← search_text;index_text 由本引擎
|
|
13
|
+
* segmentForIndex 重算(自有切分面不动)。
|
|
14
|
+
*
|
|
15
|
+
* 自调用信任面:DRS 的 Host 门是精确匹配(trustedHosts 全小写比对),本客户
|
|
16
|
+
* 端可带 hostHeader 显式回显首个 trustedHosts 条目,规避「条目省略端口而
|
|
17
|
+
* URL 带端口」的精确匹配失配;loopback + 无 trustedHosts 的老宿主天然放行。
|
|
18
|
+
*/
|
|
19
|
+
/** DRS API 路径(与 dsh-reference-session-collector 的 DRS_API_PATH 字面一致;不跨包 import)。 */
|
|
20
|
+
export const DRS_API_PATH = '/api/dsh-reference-session-collector'
|
|
21
|
+
|
|
22
|
+
/** 语料列表分页大小(百会话级一轮 1-2 页;DRS 侧 offset/limit 透传)。 */
|
|
23
|
+
export const DRS_CORPUS_PAGE = 500
|
|
24
|
+
|
|
25
|
+
/** 一个 DRS 会话头(watermark 代理 + 检索排序字段)。 */
|
|
26
|
+
export interface DrsCorpusHead {
|
|
27
|
+
sessionId: string
|
|
28
|
+
version: number
|
|
29
|
+
title: string
|
|
30
|
+
cwd: string
|
|
31
|
+
updatedAt: number
|
|
32
|
+
archived: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 一个 DRS 检索臂文档(surface 已折算成引擎列的字符串域)。 */
|
|
36
|
+
export interface DrsCorpusDoc {
|
|
37
|
+
seq: number
|
|
38
|
+
type: string
|
|
39
|
+
surface: 'current' | 'shadowed'
|
|
40
|
+
time: number
|
|
41
|
+
text: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 一个 DRS 会话的完整语料(头 + docs)。 */
|
|
45
|
+
export interface DrsCorpusSession {
|
|
46
|
+
head: DrsCorpusHead
|
|
47
|
+
docs: DrsCorpusDoc[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DrsCorpusSourceOptions {
|
|
51
|
+
/** DRS API 基址(http://host[:port];解析序见 resolveDrsBaseUrl)。 */
|
|
52
|
+
baseUrl: string
|
|
53
|
+
/** 注入面(单测);缺省全局 fetch。 */
|
|
54
|
+
fetchImpl?: typeof fetch
|
|
55
|
+
/** 显式 Host 头(trustedHosts 首条回显;缺省不覆盖)。 */
|
|
56
|
+
hostHeader?: string
|
|
57
|
+
log?: (message: string) => void
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 基址解析:显式配置 > 首个 trustedHosts 条目(补 http:// 前缀)>
|
|
62
|
+
* `http://127.0.0.1:8080`(宿主默认口;trustedHosts 缺席的老线 DRS 门本就放行)。
|
|
63
|
+
*/
|
|
64
|
+
export function resolveDrsBaseUrl(explicit: string | undefined, trustedHosts?: readonly string[]): string {
|
|
65
|
+
const trimmed = explicit?.trim() ?? ''
|
|
66
|
+
const pick = trimmed !== '' ? trimmed : trustedHosts?.find(entry => entry.trim() !== '')?.trim()
|
|
67
|
+
if (pick === undefined || pick === '') return 'http://127.0.0.1:8080'
|
|
68
|
+
return /^https?:\/\//u.test(pick) ? pick : `http://${pick}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class DrsCorpusSource {
|
|
72
|
+
private readonly fetchImpl: typeof fetch
|
|
73
|
+
|
|
74
|
+
constructor(private readonly options: DrsCorpusSourceOptions) {
|
|
75
|
+
this.fetchImpl = options.fetchImpl ?? fetch
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 一个 GET 的 JSON 包络(ok!==true 或非 2xx 一律抛,由调用方回退 native)。 */
|
|
79
|
+
private async getJson(query: string, atHost?: string): Promise<Record<string, unknown>> {
|
|
80
|
+
// atHost=同源转发臂(调用方请求的 Host 头):宿主端口动态分配时
|
|
81
|
+
// 配置缺省口(8080)猜不中——同源直连以请求 Host 优先。
|
|
82
|
+
const source = atHost !== undefined && atHost !== '' ? `http://${atHost}` : this.options.baseUrl
|
|
83
|
+
const base = source.replace(/\/+$/u, '')
|
|
84
|
+
const url = `${base}${DRS_API_PATH}${query}`
|
|
85
|
+
let response: Response
|
|
86
|
+
try {
|
|
87
|
+
response = await this.fetchImpl(url, {
|
|
88
|
+
headers: {
|
|
89
|
+
accept: 'application/json',
|
|
90
|
+
...(this.options.hostHeader === undefined ? {} : { host: this.options.hostHeader }),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
} catch (err) {
|
|
94
|
+
throw new Error(`DRS ${query}: ${String(err instanceof Error ? err.message : err)}`)
|
|
95
|
+
}
|
|
96
|
+
if (!response.ok) throw new Error(`DRS ${query} → HTTP ${String(response.status)}`)
|
|
97
|
+
const body = await response.json() as unknown
|
|
98
|
+
if (typeof body !== 'object' || body === null) throw new Error(`DRS ${query}: 响应非 JSON 对象`)
|
|
99
|
+
const record = body as Record<string, unknown>
|
|
100
|
+
if (record['ok'] !== true) {
|
|
101
|
+
throw new Error(`DRS ${query}: ok=false(${String(record['error'] ?? 'unknown')})`)
|
|
102
|
+
}
|
|
103
|
+
return record
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private static headOf(row: Record<string, unknown>, fallbackId: string): DrsCorpusHead {
|
|
107
|
+
return {
|
|
108
|
+
sessionId: String(row['session_id'] ?? fallbackId),
|
|
109
|
+
version: Number(row['artifact_mtime_ms'] ?? 0),
|
|
110
|
+
title: String(row['title'] ?? ''),
|
|
111
|
+
cwd: String(row['cwd'] ?? ''),
|
|
112
|
+
updatedAt: Number(row['last_time_ms'] ?? 0),
|
|
113
|
+
archived: Number(row['archived'] ?? 0) === 1,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 全量语料头(内部翻页;rows 空或凑满 total 即止)。 */
|
|
118
|
+
async listSessions(): Promise<DrsCorpusHead[]> {
|
|
119
|
+
const heads: DrsCorpusHead[] = []
|
|
120
|
+
let offset = 0
|
|
121
|
+
for (;;) {
|
|
122
|
+
const page = await this.getJson(`?op=sessions&offset=${String(offset)}&limit=${String(DRS_CORPUS_PAGE)}`)
|
|
123
|
+
const rows = Array.isArray(page['rows']) ? page['rows'] as Record<string, unknown>[] : []
|
|
124
|
+
for (const row of rows) {
|
|
125
|
+
const head = DrsCorpusSource.headOf(row, '')
|
|
126
|
+
if (head.sessionId !== '') heads.push(head)
|
|
127
|
+
}
|
|
128
|
+
offset += rows.length
|
|
129
|
+
const total = Number(page['total'] ?? heads.length)
|
|
130
|
+
if (rows.length === 0 || offset >= total) break
|
|
131
|
+
}
|
|
132
|
+
return heads
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 一个会话的检索臂 docs;DRS 无此会话(session:null)返回 undefined。 */
|
|
136
|
+
async readSession(sessionId: string): Promise<DrsCorpusSession | undefined> {
|
|
137
|
+
const body = await this.getJson(`?op=session&session=${encodeURIComponent(sessionId)}&fields=search`)
|
|
138
|
+
if (body['session'] === null || body['session'] === undefined) return undefined
|
|
139
|
+
const head = DrsCorpusSource.headOf(body['session'] as Record<string, unknown>, sessionId)
|
|
140
|
+
const docs = (Array.isArray(body['rows']) ? body['rows'] as Record<string, unknown>[] : []).map(row => ({
|
|
141
|
+
seq: Number(row['seq'] ?? 0),
|
|
142
|
+
type: String(row['kind'] ?? ''),
|
|
143
|
+
surface: Number(row['on_surface'] ?? 1) === 1 ? 'current' as const : 'shadowed' as const,
|
|
144
|
+
time: Number(row['time'] ?? 0),
|
|
145
|
+
text: String(row['search_text'] ?? ''),
|
|
146
|
+
}))
|
|
147
|
+
return { head, docs }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 分组内容搜索直连臂(op=search&group=session,DRS surface.searchGrouped 同构):
|
|
152
|
+
* 会话分组/加权/过滤/锚定节选全部在 DRS 侧完成,本方法只搬 wire →
|
|
153
|
+
* 引擎命中形状(SwitchSearchHit 字段同名)。失败(网络/非 2xx/包络)直抛,
|
|
154
|
+
* 调用侧按硬依赖语义显式报错——**不回退本地索引**。
|
|
155
|
+
*/
|
|
156
|
+
async searchGrouped(request: {
|
|
157
|
+
query: string
|
|
158
|
+
/** 原事件 type 列表(调用方展开粗类);空=DRS 缺省 user+assistant。 */
|
|
159
|
+
kinds?: readonly string[]
|
|
160
|
+
archived?: 'active' | 'archived' | 'all'
|
|
161
|
+
from?: number
|
|
162
|
+
to?: number
|
|
163
|
+
sortBy?: 'relevance' | 'time'
|
|
164
|
+
limit?: number
|
|
165
|
+
/** 命中展开(wire `expand=1`):同会话多命中各自成行;缺省聚合。 */
|
|
166
|
+
expand?: boolean
|
|
167
|
+
/** 同源 Host 覆盖(转发调用方请求头;优先于装配 baseUrl)。 */
|
|
168
|
+
atHost?: string
|
|
169
|
+
}): Promise<{
|
|
170
|
+
sessionId: string
|
|
171
|
+
title: string
|
|
172
|
+
seq: number
|
|
173
|
+
type: string
|
|
174
|
+
time: number
|
|
175
|
+
updatedAt: number
|
|
176
|
+
snippet: string
|
|
177
|
+
}[]> {
|
|
178
|
+
const params = new URLSearchParams()
|
|
179
|
+
params.set('op', 'search')
|
|
180
|
+
params.set('group', 'session')
|
|
181
|
+
params.set('q', request.query)
|
|
182
|
+
if (request.kinds !== undefined && request.kinds.length > 0) params.set('kinds', request.kinds.join(','))
|
|
183
|
+
if (request.archived !== undefined) params.set('archived', request.archived)
|
|
184
|
+
if (request.from !== undefined) params.set('from', String(request.from))
|
|
185
|
+
if (request.to !== undefined) params.set('to', String(request.to))
|
|
186
|
+
if (request.sortBy === 'time') params.set('sort', 'time')
|
|
187
|
+
if (request.limit !== undefined) params.set('limit', String(request.limit))
|
|
188
|
+
if (request.expand === true) params.set('expand', '1')
|
|
189
|
+
const body = await this.getJson(`?${params.toString()}`, request.atHost)
|
|
190
|
+
const hits = Array.isArray(body['hits']) ? body['hits'] as Record<string, unknown>[] : []
|
|
191
|
+
return hits.map(row => ({
|
|
192
|
+
sessionId: String(row['session_id'] ?? ''),
|
|
193
|
+
title: String(row['title'] ?? ''),
|
|
194
|
+
seq: Number(row['seq'] ?? 0),
|
|
195
|
+
type: String(row['type'] ?? ''),
|
|
196
|
+
time: Number(row['time'] ?? 0),
|
|
197
|
+
updatedAt: Number(row['updated_at'] ?? 0),
|
|
198
|
+
snippet: String(row['snippet'] ?? ''),
|
|
199
|
+
}))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 可性探针(op=status):硬依赖预检/降级提示用;失败返回 false。 */
|
|
203
|
+
async probe(atHost?: string): Promise<boolean> {
|
|
204
|
+
try {
|
|
205
|
+
await this.getJson('?op=status', atHost)
|
|
206
|
+
return true
|
|
207
|
+
} catch {
|
|
208
|
+
return false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|