dsh-session-guard 3.0.0 → 3.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/LICENSE +21 -21
- package/README.md +5 -4
- package/cordis.patch.yml +10 -10
- package/dsh.plugin.json +1 -1
- package/lib/client.js +276 -656
- package/lib/client.js.map +1 -1
- package/package.json +109 -107
- package/src/bridge.js +136 -136
- package/src/client/index.ts +46 -66
- package/src/client/pause-button-text.ts +60 -60
- package/src/client/pause-button.tsx +129 -129
- package/src/client/styles.ts +30 -30
- package/src/detect.js +27 -27
- package/src/gate.js +93 -93
- package/src/index.js +22 -7
- package/src/pause-store.js +101 -101
- package/src/scheduler.js +42 -42
- package/src/settings.js +28 -52
- package/src/step-gate.js +399 -399
- package/src/store.js +77 -77
- package/src/client/settings-card.tsx +0 -391
package/src/pause-store.js
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 自研会话门暂停状态持久化(脱离 dsh-task-control)。
|
|
3
|
-
*
|
|
4
|
-
* 与 queueLock(src/store.js,队列锁)分离:这里是「真暂停」的持久化快照,
|
|
5
|
-
* 仿 task-control 的状态模型,但独立子目录 /pause 避免与队列锁文件互相污染。
|
|
6
|
-
*
|
|
7
|
-
* 暂停/锁定状态仍**不写 session log**(harness 持久化 reader 只有已知事件集,
|
|
8
|
-
* 自定义 `session-guard/*` 事件会导致重启后会话无法加载),走插件自有 JSON 文件:
|
|
9
|
-
* `$DSH_HOME/.dsh/session-guard/pause/<sessionId>.json`(`DSH_SESSION_GUARD_PAUSE_DIR`
|
|
10
|
-
* 可覆盖根目录)。原子写:tmp + rename。
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
-
import { dirname, join } from 'node:path'
|
|
14
|
-
import { storeRoot } from './store.js'
|
|
15
|
-
|
|
16
|
-
/** 会话 id → 安全文件名(与队列锁同规则)。 */
|
|
17
|
-
export function encodeSessionId(id) {
|
|
18
|
-
return String(id).replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 自研暂停状态根目录(独立于队列锁)。 */
|
|
22
|
-
export function pauseStateRoot() {
|
|
23
|
-
return (
|
|
24
|
-
process.env.DSH_SESSION_GUARD_PAUSE_DIR ||
|
|
25
|
-
join(storeRoot(), 'pause')
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 每会话暂停状态文件路径。 */
|
|
30
|
-
export function pauseFilePath(id) {
|
|
31
|
-
return join(pauseStateRoot(), `${encodeSessionId(id)}.json`)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 未暂停基线快照。 */
|
|
35
|
-
export function idlePauseState(id) {
|
|
36
|
-
return {
|
|
37
|
-
sessionId: String(id),
|
|
38
|
-
paused: false,
|
|
39
|
-
resumeContent: null,
|
|
40
|
-
forced: false,
|
|
41
|
-
interruptedTool: null,
|
|
42
|
-
deferredTools: null,
|
|
43
|
-
updatedAt: null,
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 创建暂停状态存储(内存缓存 + 磁盘持久化,原子写)。
|
|
49
|
-
* 独立于队列锁 store,字段形态仿 task-control(paused/forced/interruptedTool/deferredTools)。
|
|
50
|
-
*/
|
|
51
|
-
export function createPauseStore() {
|
|
52
|
-
const cache = new Map()
|
|
53
|
-
|
|
54
|
-
function read(id) {
|
|
55
|
-
if (cache.has(id)) return cache.get(id)
|
|
56
|
-
try {
|
|
57
|
-
const f = pauseFilePath(id)
|
|
58
|
-
if (!existsSync(f)) return null
|
|
59
|
-
const v = JSON.parse(readFileSync(f, 'utf8'))
|
|
60
|
-
cache.set(id, v)
|
|
61
|
-
return v
|
|
62
|
-
} catch {
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
/** 读一会话暂停态;无记录返回 null(调用方 fallback idlePauseState)。 */
|
|
69
|
-
get(id) {
|
|
70
|
-
return read(id)
|
|
71
|
-
},
|
|
72
|
-
/** 原子写。 */
|
|
73
|
-
set(id, value) {
|
|
74
|
-
cache.set(id, value)
|
|
75
|
-
try {
|
|
76
|
-
const f = pauseFilePath(id)
|
|
77
|
-
mkdirSync(dirname(f), { recursive: true })
|
|
78
|
-
const tmp = `${f}.tmp`
|
|
79
|
-
writeFileSync(tmp, JSON.stringify(value, null, 2))
|
|
80
|
-
renameSync(tmp, f)
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// 落盘失败仅影响重启恢复,内存态仍可用。
|
|
83
|
-
console.error(`[session-guard] pause store write failed: ${String(e && e.message || e)}`)
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
/** 清除(resume/cancel 后)。 */
|
|
87
|
-
clear(id) {
|
|
88
|
-
cache.delete(id)
|
|
89
|
-
try {
|
|
90
|
-
rmSync(pauseFilePath(id), { force: true })
|
|
91
|
-
} catch {
|
|
92
|
-
/* ignore */
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
/** 当前有效快照(无记录时 idle 基线)。 */
|
|
96
|
-
current(id) {
|
|
97
|
-
const v = read(id)
|
|
98
|
-
return v !== null && typeof v === 'object' ? v : idlePauseState(id)
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 自研会话门暂停状态持久化(脱离 dsh-task-control)。
|
|
3
|
+
*
|
|
4
|
+
* 与 queueLock(src/store.js,队列锁)分离:这里是「真暂停」的持久化快照,
|
|
5
|
+
* 仿 task-control 的状态模型,但独立子目录 /pause 避免与队列锁文件互相污染。
|
|
6
|
+
*
|
|
7
|
+
* 暂停/锁定状态仍**不写 session log**(harness 持久化 reader 只有已知事件集,
|
|
8
|
+
* 自定义 `session-guard/*` 事件会导致重启后会话无法加载),走插件自有 JSON 文件:
|
|
9
|
+
* `$DSH_HOME/.dsh/session-guard/pause/<sessionId>.json`(`DSH_SESSION_GUARD_PAUSE_DIR`
|
|
10
|
+
* 可覆盖根目录)。原子写:tmp + rename。
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { storeRoot } from './store.js'
|
|
15
|
+
|
|
16
|
+
/** 会话 id → 安全文件名(与队列锁同规则)。 */
|
|
17
|
+
export function encodeSessionId(id) {
|
|
18
|
+
return String(id).replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 自研暂停状态根目录(独立于队列锁)。 */
|
|
22
|
+
export function pauseStateRoot() {
|
|
23
|
+
return (
|
|
24
|
+
process.env.DSH_SESSION_GUARD_PAUSE_DIR ||
|
|
25
|
+
join(storeRoot(), 'pause')
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 每会话暂停状态文件路径。 */
|
|
30
|
+
export function pauseFilePath(id) {
|
|
31
|
+
return join(pauseStateRoot(), `${encodeSessionId(id)}.json`)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 未暂停基线快照。 */
|
|
35
|
+
export function idlePauseState(id) {
|
|
36
|
+
return {
|
|
37
|
+
sessionId: String(id),
|
|
38
|
+
paused: false,
|
|
39
|
+
resumeContent: null,
|
|
40
|
+
forced: false,
|
|
41
|
+
interruptedTool: null,
|
|
42
|
+
deferredTools: null,
|
|
43
|
+
updatedAt: null,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 创建暂停状态存储(内存缓存 + 磁盘持久化,原子写)。
|
|
49
|
+
* 独立于队列锁 store,字段形态仿 task-control(paused/forced/interruptedTool/deferredTools)。
|
|
50
|
+
*/
|
|
51
|
+
export function createPauseStore() {
|
|
52
|
+
const cache = new Map()
|
|
53
|
+
|
|
54
|
+
function read(id) {
|
|
55
|
+
if (cache.has(id)) return cache.get(id)
|
|
56
|
+
try {
|
|
57
|
+
const f = pauseFilePath(id)
|
|
58
|
+
if (!existsSync(f)) return null
|
|
59
|
+
const v = JSON.parse(readFileSync(f, 'utf8'))
|
|
60
|
+
cache.set(id, v)
|
|
61
|
+
return v
|
|
62
|
+
} catch {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
/** 读一会话暂停态;无记录返回 null(调用方 fallback idlePauseState)。 */
|
|
69
|
+
get(id) {
|
|
70
|
+
return read(id)
|
|
71
|
+
},
|
|
72
|
+
/** 原子写。 */
|
|
73
|
+
set(id, value) {
|
|
74
|
+
cache.set(id, value)
|
|
75
|
+
try {
|
|
76
|
+
const f = pauseFilePath(id)
|
|
77
|
+
mkdirSync(dirname(f), { recursive: true })
|
|
78
|
+
const tmp = `${f}.tmp`
|
|
79
|
+
writeFileSync(tmp, JSON.stringify(value, null, 2))
|
|
80
|
+
renameSync(tmp, f)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// 落盘失败仅影响重启恢复,内存态仍可用。
|
|
83
|
+
console.error(`[session-guard] pause store write failed: ${String(e && e.message || e)}`)
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
/** 清除(resume/cancel 后)。 */
|
|
87
|
+
clear(id) {
|
|
88
|
+
cache.delete(id)
|
|
89
|
+
try {
|
|
90
|
+
rmSync(pauseFilePath(id), { force: true })
|
|
91
|
+
} catch {
|
|
92
|
+
/* ignore */
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
/** 当前有效快照(无记录时 idle 基线)。 */
|
|
96
|
+
current(id) {
|
|
97
|
+
const v = read(id)
|
|
98
|
+
return v !== null && typeof v === 'object' ? v : idlePauseState(id)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/scheduler.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-session-guard — 纯状态机(可单测,零依赖)。
|
|
3
|
-
*
|
|
4
|
-
* 状态:NORMAL ↔ PAUSED_PEAK。
|
|
5
|
-
* 副作用(暂停/恢复具体会话)由 host 的 tick 执行,这里只做判定与迁移检测。
|
|
6
|
-
*/
|
|
7
|
-
import { shouldPause } from './time.js'
|
|
8
|
-
|
|
9
|
-
export const STATES = {
|
|
10
|
-
NORMAL: 'NORMAL',
|
|
11
|
-
PAUSED_PEAK: 'PAUSED_PEAK',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 计算当前状态。
|
|
16
|
-
* @param {object} settings 同 time.shouldPause 的 settings
|
|
17
|
-
* @param {Date|number} now
|
|
18
|
-
* @returns {{state:string, reason:string, at:number}}
|
|
19
|
-
*/
|
|
20
|
-
export function computeState(settings, now) {
|
|
21
|
-
const date = now instanceof Date ? now : new Date(now)
|
|
22
|
-
const d = shouldPause(settings, date)
|
|
23
|
-
return {
|
|
24
|
-
state: d.pause ? STATES.PAUSED_PEAK : STATES.NORMAL,
|
|
25
|
-
reason: d.reason,
|
|
26
|
-
at: date.getTime(),
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 迁移检测:从 prev 到 next 是否发生了 入峰(enter) / 退峰(leave)。
|
|
32
|
-
* @param {{state:string}|null} prev
|
|
33
|
-
* @param {{state:string}} next
|
|
34
|
-
* @returns {{enter:boolean, leave:boolean}}
|
|
35
|
-
*/
|
|
36
|
-
export function transition(prev, next) {
|
|
37
|
-
const was = prev && prev.state
|
|
38
|
-
return {
|
|
39
|
-
enter: next.state === STATES.PAUSED_PEAK && was !== STATES.PAUSED_PEAK,
|
|
40
|
-
leave: was === STATES.PAUSED_PEAK && next.state !== STATES.PAUSED_PEAK,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-session-guard — 纯状态机(可单测,零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 状态:NORMAL ↔ PAUSED_PEAK。
|
|
5
|
+
* 副作用(暂停/恢复具体会话)由 host 的 tick 执行,这里只做判定与迁移检测。
|
|
6
|
+
*/
|
|
7
|
+
import { shouldPause } from './time.js'
|
|
8
|
+
|
|
9
|
+
export const STATES = {
|
|
10
|
+
NORMAL: 'NORMAL',
|
|
11
|
+
PAUSED_PEAK: 'PAUSED_PEAK',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 计算当前状态。
|
|
16
|
+
* @param {object} settings 同 time.shouldPause 的 settings
|
|
17
|
+
* @param {Date|number} now
|
|
18
|
+
* @returns {{state:string, reason:string, at:number}}
|
|
19
|
+
*/
|
|
20
|
+
export function computeState(settings, now) {
|
|
21
|
+
const date = now instanceof Date ? now : new Date(now)
|
|
22
|
+
const d = shouldPause(settings, date)
|
|
23
|
+
return {
|
|
24
|
+
state: d.pause ? STATES.PAUSED_PEAK : STATES.NORMAL,
|
|
25
|
+
reason: d.reason,
|
|
26
|
+
at: date.getTime(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 迁移检测:从 prev 到 next 是否发生了 入峰(enter) / 退峰(leave)。
|
|
32
|
+
* @param {{state:string}|null} prev
|
|
33
|
+
* @param {{state:string}} next
|
|
34
|
+
* @returns {{enter:boolean, leave:boolean}}
|
|
35
|
+
*/
|
|
36
|
+
export function transition(prev, next) {
|
|
37
|
+
const was = prev && prev.state
|
|
38
|
+
return {
|
|
39
|
+
enter: next.state === STATES.PAUSED_PEAK && was !== STATES.PAUSED_PEAK,
|
|
40
|
+
leave: was === STATES.PAUSED_PEAK && next.state !== STATES.PAUSED_PEAK,
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/settings.js
CHANGED
|
@@ -54,33 +54,34 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
54
54
|
* - 枚举用 z.union([z.const(...)]);默认值用字段级 .default()。
|
|
55
55
|
*/
|
|
56
56
|
export const SettingsSchema = z.object({
|
|
57
|
-
enabled: z.boolean().default(DEFAULT_SETTINGS.enabled),
|
|
58
|
-
offPeakAutoResume: z.boolean().default(DEFAULT_SETTINGS.offPeakAutoResume),
|
|
59
|
-
weekendMode: z.boolean().default(DEFAULT_SETTINGS.weekendMode),
|
|
60
|
-
timezone: z.string().default(DEFAULT_SETTINGS.timezone),
|
|
57
|
+
enabled: z.boolean().default(DEFAULT_SETTINGS.enabled).volatile(),
|
|
58
|
+
offPeakAutoResume: z.boolean().default(DEFAULT_SETTINGS.offPeakAutoResume).volatile(),
|
|
59
|
+
weekendMode: z.boolean().default(DEFAULT_SETTINGS.weekendMode).volatile(),
|
|
60
|
+
timezone: z.string().default(DEFAULT_SETTINGS.timezone).volatile(),
|
|
61
61
|
peakWindows: z
|
|
62
62
|
.array(z.object({ start: z.string(), end: z.string() }))
|
|
63
|
-
.default(DEFAULT_SETTINGS.peakWindows)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
.default(DEFAULT_SETTINGS.peakWindows)
|
|
64
|
+
.volatile(),
|
|
65
|
+
pauseMode: z.union([z.const('safe'), z.const('force')]).default(DEFAULT_SETTINGS.pauseMode).volatile(),
|
|
66
|
+
pauseReason: z.union([z.const('wait'), z.const('stop')]).default(DEFAULT_SETTINGS.pauseReason).volatile(),
|
|
67
|
+
stepLevelPause: z.boolean().default(DEFAULT_SETTINGS.stepLevelPause).volatile(),
|
|
68
|
+
stepGateTimeoutMs: z.number().min(0).default(DEFAULT_SETTINGS.stepGateTimeoutMs).volatile(),
|
|
69
|
+
queueFallback: z.boolean().default(DEFAULT_SETTINGS.queueFallback).volatile(),
|
|
70
|
+
retryEnabled: z.boolean().default(DEFAULT_SETTINGS.retryEnabled).volatile(),
|
|
71
|
+
retryText: z.string().default(DEFAULT_SETTINGS.retryText).volatile(),
|
|
72
|
+
retryGraceMs: z.number().min(0).default(DEFAULT_SETTINGS.retryGraceMs).volatile(),
|
|
73
|
+
retryCooldownMs: z.number().min(0).default(DEFAULT_SETTINGS.retryCooldownMs).volatile(),
|
|
74
|
+
retryBackoffFactor: z.number().min(1).default(DEFAULT_SETTINGS.retryBackoffFactor).volatile(),
|
|
75
|
+
retryBackoffMaxMs: z.number().min(0).default(DEFAULT_SETTINGS.retryBackoffMaxMs).volatile(),
|
|
76
|
+
retryMaxConsecutive: z.number().min(0).default(DEFAULT_SETTINGS.retryMaxConsecutive).volatile(),
|
|
77
|
+
providerGuard: z.boolean().default(DEFAULT_SETTINGS.providerGuard).volatile(),
|
|
78
|
+
officialProviders: z.array(z.string()).default(DEFAULT_SETTINGS.officialProviders).volatile(),
|
|
79
|
+
officialBaseURLs: z.array(z.string()).default(DEFAULT_SETTINGS.officialBaseURLs).volatile(),
|
|
80
|
+
deferredResume: z.boolean().default(DEFAULT_SETTINGS.deferredResume).volatile(),
|
|
81
|
+
deferredResumeText: z.string().default(DEFAULT_SETTINGS.deferredResumeText).volatile(),
|
|
82
|
+
deferredMode: z.union([z.const('hold'), z.const('error')]).default(DEFAULT_SETTINGS.deferredMode).volatile(),
|
|
83
|
+
deferredMaxHoldMs: z.number().min(0).default(DEFAULT_SETTINGS.deferredMaxHoldMs).volatile(),
|
|
84
|
+
guardSubagents: z.boolean().default(DEFAULT_SETTINGS.guardSubagents).volatile(),
|
|
84
85
|
})
|
|
85
86
|
|
|
86
87
|
/**
|
|
@@ -88,31 +89,6 @@ export const SettingsSchema = z.object({
|
|
|
88
89
|
* 插件不 value-import `@deepseek-ai/dsh-settings`,只通过这些形状在注入面调用。
|
|
89
90
|
*/
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
/** @typedef {{ inject(deps: readonly string[], fn: (s: { settings: SettingsServiceLike; effect(cb: () => (() => void) | void, label?: string): void }) => void): void }} SettingsAwareCtx */
|
|
92
|
+
// 0.1.7:设置面改声明式 —— SettingsSchema 的 volatile 字段即自动表单,
|
|
93
|
+
// 不再有任何 register 调用;运行时经 apply 的组合条目(live ref)读取。
|
|
94
94
|
|
|
95
|
-
/**
|
|
96
|
-
* 注册 设置 → 插件 → session-guard 子板块(简单开关)。
|
|
97
|
-
* - 走 `settings` 注入面,`base` 层叠组合配置;对 runtime 调用方经 `ctx.settings.get(NS)` 读取。
|
|
98
|
-
* - 任何失败(settings 服务缺失 / 注入异常)→ 返回 false,静默降级用默认配置。
|
|
99
|
-
* @param {object} ctx - host context(应含 cordis `settings` 注入面)。
|
|
100
|
-
* @returns {boolean} 注册成功 true;设置服务不可用时 false(fail-open)。
|
|
101
|
-
*/
|
|
102
|
-
export function registerSettings(ctx) {
|
|
103
|
-
try {
|
|
104
|
-
// 本插件顶层 `inject` 已声明 `settings`(见 src/index.js 的
|
|
105
|
-
// `export const inject`),所以 apply 时 `ctx.settings` 已是完整
|
|
106
|
-
// SettingsProvider(带 .register),直接注册即可 —— 无需再 `ctx.inject`
|
|
107
|
-
// 二次动态注入(对已在 fiber 上解析的服务做二次注入,回调作为异步插件
|
|
108
|
-
// apply 排队,`registerSettings` 同步返回 true 会掩盖实际操作未生效)。
|
|
109
|
-
// 与 dsh-thinking-levels / dsh-context 的区别仅在于它们顶层未声明
|
|
110
|
-
// settings,才必须动态注入;这里已声明,直接用最可靠。
|
|
111
|
-
const svc = /** @type {{ register(ns: string, schema: unknown, options?: { base?: unknown }): unknown }} */ (ctx.settings)
|
|
112
|
-
// base 用副本:DEFAULT_SETTINGS 被 Object.freeze,直接当 base 可能被写。
|
|
113
|
-
svc.register(NS, SettingsSchema, { base: { ...DEFAULT_SETTINGS } })
|
|
114
|
-
return true
|
|
115
|
-
} catch {
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
}
|