dsh-offpeak 3.1.6 → 3.1.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/README.en.md +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +9 -9
- package/lib/client.js +32 -4
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/model-list.ts +116 -116
- package/src/client/model-panel.tsx +185 -185
- package/src/client/status-badge.tsx +114 -100
- package/src/client/styles.ts +19 -4
- package/src/settings.js +183 -183
|
@@ -1,100 +1,114 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-offpeak — 状态徽标(**徽标即按钮**,fail-open)。
|
|
3
|
-
*
|
|
4
|
-
* 轮询 host 的 `/session-guard/status`(全局阶段 + 拦截列表规模)与
|
|
5
|
-
* `/session-guard/state`(本会话畅跑任务),显示四态文案(v3.1.6):
|
|
6
|
-
* - 高峰 + 有畅跑任务 → `高峰 · 畅跑中`;
|
|
7
|
-
* - 高峰 + 有拦截任务 → `高峰 · 拦截中`;
|
|
8
|
-
* - 高峰 + 都没任务 → `高峰`;
|
|
9
|
-
* - 非高峰 → `谷时` / `周末`。
|
|
10
|
-
*
|
|
11
|
-
* 点开面板 = **畅跑任务区(最上方)+ 高峰拦截列表**(布局按用户定案);**每个阶段都是同一个
|
|
12
|
-
* 按钮**(类名见 `badgeClassNames`),只有文案与配色变,不因不是高峰就退化成展示元素。
|
|
13
|
-
* 本线(0.1.7)客户端没有 settingsScope,拦截列表的写入口是 `setModelPatterns`(写配置文件)。
|
|
14
|
-
* 不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
|
|
15
|
-
*/
|
|
16
|
-
import { useEffect, useState } from 'react'
|
|
17
|
-
import { badgeClassNames, badgeTitle, peakLabel, type Status } from './badge-text'
|
|
18
|
-
import { FreeRunPanel } from './free-run-button'
|
|
19
|
-
import { EMPTY_FREE_RUN, readFreeRun, type FreeRunView } from './free-run-button-text'
|
|
20
|
-
import { ModelListPanel } from './model-panel'
|
|
21
|
-
import { injectClientCss } from './styles'
|
|
22
|
-
|
|
23
|
-
/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */
|
|
24
|
-
export interface StatusBadgeProps {
|
|
25
|
-
sessionId?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const POLL_MS = 15_000
|
|
29
|
-
|
|
30
|
-
/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
|
|
31
|
-
export function StatusBadge({ sessionId }: StatusBadgeProps) {
|
|
32
|
-
const [status, setStatus] = useState<Status | null>(null)
|
|
33
|
-
const [freeRun, setFreeRun] = useState<FreeRunView>(EMPTY_FREE_RUN)
|
|
34
|
-
const [open, setOpen] = useState(false)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-offpeak — 状态徽标(**徽标即按钮**,fail-open)。
|
|
3
|
+
*
|
|
4
|
+
* 轮询 host 的 `/session-guard/status`(全局阶段 + 拦截列表规模)与
|
|
5
|
+
* `/session-guard/state`(本会话畅跑任务),显示四态文案(v3.1.6):
|
|
6
|
+
* - 高峰 + 有畅跑任务 → `高峰 · 畅跑中`;
|
|
7
|
+
* - 高峰 + 有拦截任务 → `高峰 · 拦截中`;
|
|
8
|
+
* - 高峰 + 都没任务 → `高峰`;
|
|
9
|
+
* - 非高峰 → `谷时` / `周末`。
|
|
10
|
+
*
|
|
11
|
+
* 点开面板 = **畅跑任务区(最上方)+ 高峰拦截列表**(布局按用户定案);**每个阶段都是同一个
|
|
12
|
+
* 按钮**(类名见 `badgeClassNames`),只有文案与配色变,不因不是高峰就退化成展示元素。
|
|
13
|
+
* 本线(0.1.7)客户端没有 settingsScope,拦截列表的写入口是 `setModelPatterns`(写配置文件)。
|
|
14
|
+
* 不做任何队列/会话动作;冻结按钮由 input-traffic 经桥接管(D6/D8)。
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useRef, useState } from 'react'
|
|
17
|
+
import { badgeClassNames, badgeTitle, peakLabel, type Status } from './badge-text'
|
|
18
|
+
import { FreeRunPanel } from './free-run-button'
|
|
19
|
+
import { EMPTY_FREE_RUN, readFreeRun, type FreeRunView } from './free-run-button-text'
|
|
20
|
+
import { ModelListPanel } from './model-panel'
|
|
21
|
+
import { injectClientCss } from './styles'
|
|
22
|
+
|
|
23
|
+
/** slot 运行时注入的会话级 props(sessionId 由 dsh 的 SessionStandardProps 提供)。 */
|
|
24
|
+
export interface StatusBadgeProps {
|
|
25
|
+
sessionId?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const POLL_MS = 15_000
|
|
29
|
+
|
|
30
|
+
/** 状态徽标:轮询全局阶段,显示 高峰/谷时/周末(enabled 关闭或请求失败时静默隐藏)。 */
|
|
31
|
+
export function StatusBadge({ sessionId }: StatusBadgeProps) {
|
|
32
|
+
const [status, setStatus] = useState<Status | null>(null)
|
|
33
|
+
const [freeRun, setFreeRun] = useState<FreeRunView>(EMPTY_FREE_RUN)
|
|
34
|
+
const [open, setOpen] = useState(false)
|
|
35
|
+
const rootRef = useRef<HTMLDivElement | null>(null)
|
|
36
|
+
|
|
37
|
+
injectClientCss()
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!sessionId) return
|
|
41
|
+
let cancelled = false
|
|
42
|
+
const poll = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch('/session-guard/status')
|
|
45
|
+
const body = await res.json().catch(() => null)
|
|
46
|
+
if (!cancelled && body?.ok && body.status) setStatus(body.status)
|
|
47
|
+
} catch {
|
|
48
|
+
// fail-open:路由/网络不可达 → 静默,徽标隐藏。
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(`/session-guard/state?session=${encodeURIComponent(sessionId)}`)
|
|
52
|
+
const body = await res.json().catch(() => null)
|
|
53
|
+
if (!cancelled && body !== null) setFreeRun(readFreeRun(body))
|
|
54
|
+
} catch {
|
|
55
|
+
// fail-open:畅跑状态取不到 → 保持上一次。
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
void poll()
|
|
59
|
+
const timer = setInterval(poll, POLL_MS)
|
|
60
|
+
return () => {
|
|
61
|
+
cancelled = true
|
|
62
|
+
clearInterval(timer)
|
|
63
|
+
}
|
|
64
|
+
}, [sessionId])
|
|
65
|
+
|
|
66
|
+
// 点击面板之外的区域 → 自动收回(与再次点击徽标收起的效果一致)。
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open) return
|
|
69
|
+
const onPointerDown = (ev: MouseEvent) => {
|
|
70
|
+
const root = rootRef.current
|
|
71
|
+
const target = ev.target
|
|
72
|
+
if (root === null || !(target instanceof Node)) return
|
|
73
|
+
if (!root.contains(target)) setOpen(false)
|
|
74
|
+
}
|
|
75
|
+
document.addEventListener('mousedown', onPointerDown)
|
|
76
|
+
return () => document.removeEventListener('mousedown', onPointerDown)
|
|
77
|
+
}, [open])
|
|
78
|
+
|
|
79
|
+
if (!status || !status.enabled) return null
|
|
80
|
+
|
|
81
|
+
const tasks = {
|
|
82
|
+
intercepting: (status.modelPatterns ?? 0) > 0,
|
|
83
|
+
freeRun: freeRun.windows.length > 0,
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<div className="sg-status-root" ref={rootRef}>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={badgeClassNames(status)}
|
|
90
|
+
title={`${badgeTitle(status)} — 点击展开/收起高峰面板`}
|
|
91
|
+
aria-expanded={open}
|
|
92
|
+
data-sg-phase={status.phase}
|
|
93
|
+
data-sg-provider-guard={status.providerGuard === true ? 'on' : 'off'}
|
|
94
|
+
data-sg-step-held={String(status.stepHeld ?? 0)}
|
|
95
|
+
onClick={() => setOpen(!open)}
|
|
96
|
+
>
|
|
97
|
+
{peakLabel(status, tasks)}
|
|
98
|
+
</button>
|
|
99
|
+
{open && (
|
|
100
|
+
<div className="sg-status-panel" role="dialog" aria-label="高峰面板">
|
|
101
|
+
{/* 畅跑任务区在最上方(v3.1.6:独立的「畅跑」按钮已去掉,UI 内嵌到这里) */}
|
|
102
|
+
<FreeRunPanel sessionId={sessionId} />
|
|
103
|
+
<div className="sgMlHead">
|
|
104
|
+
<span className="sg-fr-title">高峰拦截列表(按模型名)</span>
|
|
105
|
+
<button type="button" className="sg-fr-closeicon" onClick={() => setOpen(false)} aria-label="关闭">
|
|
106
|
+
×
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
<ModelListPanel />
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
package/src/client/styles.ts
CHANGED
|
@@ -31,8 +31,8 @@ export const CLIENT_CSS = `
|
|
|
31
31
|
/* 「高峰拦截列表」面板(.sgMl* 由状态徽标与设置表单共用同一份样式) */
|
|
32
32
|
.sgMlBody{margin-top:8px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:8px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
33
33
|
.sgMlBody *{box-sizing:border-box}
|
|
34
|
-
.sgMlFilterRow{display:flex;align-items:center;gap:6px;padding-bottom:6px}
|
|
35
|
-
.sgMlInput{flex:1;min-width:0;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:var(--dsw-alias-bg-layer-2,transparent);color:inherit;font:inherit;font-size:12px}
|
|
34
|
+
.sgMlFilterRow{display:flex;align-items:center;gap:6px;padding-bottom:6px;flex-wrap:wrap}
|
|
35
|
+
.sgMlInput{flex:1 1 160px;min-width:0;height:24px;padding:0 8px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:var(--dsw-alias-bg-layer-2,transparent);color:inherit;font:inherit;font-size:12px}
|
|
36
36
|
.sgMlInput:disabled{opacity:.6;cursor:not-allowed}
|
|
37
37
|
.sgMlError{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:12px;line-height:18px;margin:6px 0 0;word-break:break-word}
|
|
38
38
|
.sgMlPatterns{display:flex;align-items:center;flex-wrap:wrap;gap:6px;padding-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.08))}
|
|
@@ -62,8 +62,8 @@ export const CLIENT_CSS = `
|
|
|
62
62
|
.sgMlModel{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
63
63
|
.sgMlTag{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.08));border-radius:4px;padding:0 4px}
|
|
64
64
|
.sgMlHint{color:var(--dsw-alias-label-tertiary,#6b7280);font-size:12px;line-height:18px;margin:6px 0 0}
|
|
65
|
-
.sg-fr-panel,.sg-status-panel{position:absolute;bottom:calc(100% + 8px);right:0;z-index:40;width:448px
|
|
66
|
-
.sg-status-panel{width:380px;z-index:45}
|
|
65
|
+
.sg-fr-panel,.sg-status-panel{position:absolute;bottom:calc(100% + 8px);right:0;z-index:40;width:min(448px,calc(100vw - 24px));max-height:min(70vh,560px);overflow:auto;overscroll-behavior:contain;box-sizing:border-box;padding:10px 12px 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:10px;background:var(--dsw-specific-input-major,#fff);box-shadow:var(--dsw-elevation-panel,0 6px 24px rgba(0,0,0,.16));color:var(--dsw-alias-label-primary,#111);font-size:12px;line-height:1.5}
|
|
66
|
+
.sg-status-panel{width:min(380px,calc(100vw - 24px));z-index:45}
|
|
67
67
|
.sg-status-panel *{box-sizing:border-box}
|
|
68
68
|
.sg-fr-panel *{box-sizing:border-box}
|
|
69
69
|
.sg-fr-head{display:flex;align-items:baseline;gap:8px;font-size:13px;font-weight:600;margin-bottom:8px}
|
|
@@ -137,6 +137,21 @@ export const CLIENT_CSS = `
|
|
|
137
137
|
.sg-fr-actions{display:flex;justify-content:flex-end;gap:6px;margin-top:4px}
|
|
138
138
|
.sg-fr-actions>button{height:24px;padding:0 10px;border:1px solid var(--dsw-alias-border-l3,rgba(0,0,0,.12));border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:12px;cursor:pointer}
|
|
139
139
|
.sg-fr-resume{border-color:var(--dsw-alias-state-success-primary,#30a46c)!important;color:var(--dsw-alias-state-success-primary,#30a46c)!important}
|
|
140
|
+
/* ── 响应式:窄窗口不裁切、日历改弹性列(v3.1.7)── */
|
|
141
|
+
.sg-dr-months{flex-wrap:wrap;justify-content:center;gap:10px}
|
|
142
|
+
.sg-dr-month{width:auto;flex:1 1 168px;min-width:0}
|
|
143
|
+
.sg-dr-panel{max-width:calc(100vw - 24px);box-sizing:border-box}
|
|
144
|
+
.sg-fr-list,.sgMlList{overflow-x:hidden}
|
|
145
|
+
@media (max-width:520px){
|
|
146
|
+
/* 单月:第二个日历换行后依然可读,但优先只显示一个(隐藏第二个) */
|
|
147
|
+
.sg-dr-month+.sg-dr-month{display:none}
|
|
148
|
+
.sg-dr-month{flex:1 1 100%;max-width:280px}
|
|
149
|
+
.sg-fr-panel,.sg-status-panel{right:auto;left:0;bottom:calc(100% + 6px)}
|
|
150
|
+
.sg-fr-bar{flex-wrap:wrap}
|
|
151
|
+
.sg-fr-bar-btn{flex:1 1 30%}
|
|
152
|
+
.sg-fr-hours{flex-wrap:wrap}
|
|
153
|
+
.sg-fr-hour{flex:1 1 100%}
|
|
154
|
+
}
|
|
140
155
|
`
|
|
141
156
|
|
|
142
157
|
/** 注入一次(幂等;无 document 时静默跳过)。 */
|
package/src/settings.js
CHANGED
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-offpeak — 设置 schema(0.1.7 线:**声明式 volatile 设置**)。
|
|
3
|
-
*
|
|
4
|
-
* **本线的 `settings` 服务仍在**(类 `SettingsForms`,有 `describe()` / `configure()`),
|
|
5
|
-
* 但 `dsh-settings` **已没有 `register()` 与 `get()`** —— 这(而非「服务缺席」)才是设置面
|
|
6
|
-
* 改用官方推荐声明式形态的原因:
|
|
7
|
-
* - `src/index.js` 导出 `export const Config = SettingsSchema`,dsh 依据 schema 里的
|
|
8
|
-
* `.volatile()` 字段**自动生成设置表单**;
|
|
9
|
-
* - 运行时经 `apply(ctx, config)` 的组合条目读取(volatile 值是 live ref,
|
|
10
|
-
* 用 `.get()` 解引),见 `src/index.js` 的 `resolveEntryConfig()`;
|
|
11
|
-
* - **不再有任何 register 调用**(没有可调用的 `register`),插件也**不 inject `settings`**
|
|
12
|
-
* —— 它不从该服务消费任何东西,少一个依赖更干净。
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ 真正会让应用 **web boot 致命失败**的是**客户端**缺失的 `settingsScope`(写成静态
|
|
15
|
-
* `inject` 会让客户端条目永远 pending),与宿主的 `settings` 服务无关 —— 见
|
|
16
|
-
* `src/client/index.ts` 的兼容性注记。
|
|
17
|
-
*
|
|
18
|
-
* 本文件只保留纯数据 + 纯校验:
|
|
19
|
-
* - `DEFAULT_SETTINGS`:核心逻辑依赖的默认值;
|
|
20
|
-
* - `SettingsSchema`:字段级 `.default()` + `.volatile()`;
|
|
21
|
-
* - `effectiveDefaults` / `canonicalSettings` / `settingsBaseFor`:把
|
|
22
|
-
* `config/offpeak.json` 的值并进默认层并用 schema 校验(坏值回退内置默认,
|
|
23
|
-
* 绝不因配置文件写错而让守卫静默失效)。
|
|
24
|
-
*/
|
|
25
|
-
import z from '@deepseek-ai/schemastery'
|
|
26
|
-
import { DEFAULT_RETRY } from './retry.js'
|
|
27
|
-
import { DEFAULT_WEEKEND_DAYS, WEEKEND_MODE_OFF_PEAK } from './time-policy.js'
|
|
28
|
-
|
|
29
|
-
/** 设置命名空间(设置 → 插件 → session-guard)。 */
|
|
30
|
-
export const NS = 'offpeak'
|
|
31
|
-
|
|
32
|
-
/** 默认配置(核心逻辑依赖;设置不可用时即用此值)。 */
|
|
33
|
-
export const DEFAULT_SETTINGS = Object.freeze({
|
|
34
|
-
enabled: true, // 高峰自动处理开关(简单开关)
|
|
35
|
-
offPeakAutoResume: true, // 低谷自动恢复开关:低峰时段自动恢复被高峰暂停的会话
|
|
36
|
-
weekendMode: true, // 周末模式开关:周末整日视为 off-peak(简单开关)
|
|
37
|
-
timezone: 'Asia/Shanghai', // 全部判定的时区(spec §六.3)
|
|
38
|
-
weekendDays: [...DEFAULT_WEEKEND_DAYS], // 哪几天算周末(spec §一 weekendPolicy.days)
|
|
39
|
-
weekendPolicyMode: WEEKEND_MODE_OFF_PEAK, // 周末策略模式(目前仅 offPeak)
|
|
40
|
-
// 峰窗口:name 仅用于展示/日志,days 省略或为空 = 每天生效(spec §六.1)。
|
|
41
|
-
// v0.3.0 起不再硬编码——全部来自 config/offpeak.json。
|
|
42
|
-
peakWindows: [
|
|
43
|
-
{ name: 'morning', start: '09:00', end: '12:00', days: ['mon', 'tue', 'wed', 'thu', 'fri'] },
|
|
44
|
-
{ name: 'afternoon', start: '14:00', end: '18:00', days: ['mon', 'tue', 'wed', 'thu', 'fri'] },
|
|
45
|
-
],
|
|
46
|
-
peakTimezone: '', // 仅覆盖峰窗口判定的时区;'' = 跟随 timezone(spec §六.3)
|
|
47
|
-
pauseMode: 'safe', // 透传 taskControl.pause mode
|
|
48
|
-
pauseReason: 'wait', // 透传 taskControl.pause reason
|
|
49
|
-
stepLevelPause: true, // step 级门控(agent/pre-step):高峰在下一个 step 前拉门
|
|
50
|
-
stepGateTimeoutMs: 300_000, // step 门控超时(防死锁;到期升级为 turn 级 force 暂停)
|
|
51
|
-
queueFallback: true, // 无会话门时回退锁等待队列(简单开关)
|
|
52
|
-
retryEnabled: false, // 自动重试开关(后端,D9;默认关,保守)
|
|
53
|
-
retryText: DEFAULT_RETRY.retryText,
|
|
54
|
-
retryGraceMs: DEFAULT_RETRY.retryGraceMs,
|
|
55
|
-
retryCooldownMs: DEFAULT_RETRY.retryCooldownMs,
|
|
56
|
-
retryBackoffFactor: DEFAULT_RETRY.retryBackoffFactor,
|
|
57
|
-
retryBackoffMaxMs: DEFAULT_RETRY.retryBackoffMaxMs,
|
|
58
|
-
retryMaxConsecutive: DEFAULT_RETRY.retryMaxConsecutive,
|
|
59
|
-
// ── 官方 provider 二维判定(高峰 × 目标源)──
|
|
60
|
-
providerGuard: true, // 二维判定总开关;关掉 = 现有纯时间判定
|
|
61
|
-
officialProviders: [], // 追加的官方 provider id(精确匹配,最高优先级)
|
|
62
|
-
// 官方**模型名**名单(精确匹配、大小写不敏感;v3.1.6 起没有关键字/正则的包含匹配):
|
|
63
|
-
// 默认空 —— 不勾不拦;面板勾选 / 配置文件写入的就是这份名单。
|
|
64
|
-
officialModelPatterns: [],
|
|
65
|
-
officialBaseURLs: ['api.deepseek.com'], // 官方端点 host 名单
|
|
66
|
-
deferredResume: true, // 退峰自动继续被延后的请求/会话
|
|
67
|
-
deferredResumeText: '继续(高峰已过,自动继续)', // error 模式退峰 followup 文案
|
|
68
|
-
deferredMode: 'hold', // hold(挂起不报错)/ error(抛错 + 记延后)
|
|
69
|
-
deferredMaxHoldMs: 6 * 60 * 60 * 1000, // 挂起上限(6h),到期转 error
|
|
70
|
-
guardSubagents: true, // 是否纳入子代理请求
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 设置 schema(schemastery 原生命令,zod 变体):
|
|
75
|
-
* - 枚举用 z.union([z.const(...)]);默认值用字段级 .default()。
|
|
76
|
-
* - **每个字段都标 `.volatile()`**:0.1.7 线用它自动生成设置表单。
|
|
77
|
-
* - peakWindows 的 name/days 为 v0.3.0 新增;未提供时 default 兜底,
|
|
78
|
-
* 因此升级后已存的旧形态(只有 start/end)仍然合法。
|
|
79
|
-
*/
|
|
80
|
-
export const SettingsSchema = z.object({
|
|
81
|
-
enabled: z.boolean().default(DEFAULT_SETTINGS.enabled).volatile(),
|
|
82
|
-
offPeakAutoResume: z.boolean().default(DEFAULT_SETTINGS.offPeakAutoResume).volatile(),
|
|
83
|
-
weekendMode: z.boolean().default(DEFAULT_SETTINGS.weekendMode).volatile(),
|
|
84
|
-
timezone: z.string().default(DEFAULT_SETTINGS.timezone).volatile(),
|
|
85
|
-
weekendDays: z.array(z.string()).default([...DEFAULT_SETTINGS.weekendDays]).volatile(),
|
|
86
|
-
weekendPolicyMode: z
|
|
87
|
-
.union([z.const(WEEKEND_MODE_OFF_PEAK)])
|
|
88
|
-
.default(DEFAULT_SETTINGS.weekendPolicyMode)
|
|
89
|
-
.volatile(),
|
|
90
|
-
peakWindows: z
|
|
91
|
-
.array(
|
|
92
|
-
z.object({
|
|
93
|
-
name: z.string().default(''),
|
|
94
|
-
start: z.string(),
|
|
95
|
-
end: z.string(),
|
|
96
|
-
days: z.array(z.string()).default([]),
|
|
97
|
-
}),
|
|
98
|
-
)
|
|
99
|
-
.default(DEFAULT_SETTINGS.peakWindows)
|
|
100
|
-
.volatile(),
|
|
101
|
-
peakTimezone: z.string().default(DEFAULT_SETTINGS.peakTimezone).volatile(),
|
|
102
|
-
pauseMode: z.union([z.const('safe'), z.const('force')]).default(DEFAULT_SETTINGS.pauseMode).volatile(),
|
|
103
|
-
pauseReason: z.union([z.const('wait'), z.const('stop')]).default(DEFAULT_SETTINGS.pauseReason).volatile(),
|
|
104
|
-
stepLevelPause: z.boolean().default(DEFAULT_SETTINGS.stepLevelPause).volatile(),
|
|
105
|
-
stepGateTimeoutMs: z.number().min(0).default(DEFAULT_SETTINGS.stepGateTimeoutMs).volatile(),
|
|
106
|
-
queueFallback: z.boolean().default(DEFAULT_SETTINGS.queueFallback).volatile(),
|
|
107
|
-
retryEnabled: z.boolean().default(DEFAULT_SETTINGS.retryEnabled).volatile(),
|
|
108
|
-
retryText: z.string().default(DEFAULT_SETTINGS.retryText).volatile(),
|
|
109
|
-
retryGraceMs: z.number().min(0).default(DEFAULT_SETTINGS.retryGraceMs).volatile(),
|
|
110
|
-
retryCooldownMs: z.number().min(0).default(DEFAULT_SETTINGS.retryCooldownMs).volatile(),
|
|
111
|
-
retryBackoffFactor: z.number().min(1).default(DEFAULT_SETTINGS.retryBackoffFactor).volatile(),
|
|
112
|
-
retryBackoffMaxMs: z.number().min(0).default(DEFAULT_SETTINGS.retryBackoffMaxMs).volatile(),
|
|
113
|
-
retryMaxConsecutive: z.number().min(0).default(DEFAULT_SETTINGS.retryMaxConsecutive).volatile(),
|
|
114
|
-
providerGuard: z.boolean().default(DEFAULT_SETTINGS.providerGuard).volatile(),
|
|
115
|
-
officialProviders: z.array(z.string()).default(DEFAULT_SETTINGS.officialProviders).volatile(),
|
|
116
|
-
officialModelPatterns: z.array(z.string()).default(DEFAULT_SETTINGS.officialModelPatterns).volatile(),
|
|
117
|
-
officialBaseURLs: z.array(z.string()).default(DEFAULT_SETTINGS.officialBaseURLs).volatile(),
|
|
118
|
-
deferredResume: z.boolean().default(DEFAULT_SETTINGS.deferredResume).volatile(),
|
|
119
|
-
deferredResumeText: z.string().default(DEFAULT_SETTINGS.deferredResumeText).volatile(),
|
|
120
|
-
deferredMode: z.union([z.const('hold'), z.const('error')]).default(DEFAULT_SETTINGS.deferredMode).volatile(),
|
|
121
|
-
deferredMaxHoldMs: z.number().min(0).default(DEFAULT_SETTINGS.deferredMaxHoldMs).volatile(),
|
|
122
|
-
guardSubagents: z.boolean().default(DEFAULT_SETTINGS.guardSubagents).volatile(),
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 把 base 过一遍 schema,得到**规范化形态**(补齐 schema 的字段级默认值)。
|
|
127
|
-
*
|
|
128
|
-
* 注意:因为字段都标了 `.volatile()`,`SettingsSchema(base)` 解析出来的是 **live ref**
|
|
129
|
-
* (每个字段一个带 `.get()` 的对象,dsh 的自动表单与实时读取依赖这个形态)。
|
|
130
|
-
* 本函数负责把 ref **解引成普通值**,供默认层合并与比较使用。
|
|
131
|
-
*
|
|
132
|
-
* 坏值不抛给调用方,而是带回 `error` 说明。
|
|
133
|
-
*
|
|
134
|
-
* @param {object} base
|
|
135
|
-
* @returns {{value:object, error:string|null}}
|
|
136
|
-
*/
|
|
137
|
-
export function canonicalSettings(base) {
|
|
138
|
-
try {
|
|
139
|
-
const resolved = SettingsSchema(base)
|
|
140
|
-
const value = {}
|
|
141
|
-
for (const [key, v] of Object.entries(resolved)) {
|
|
142
|
-
if (v !== null && typeof v === 'object' && typeof v.get === 'function') {
|
|
143
|
-
try {
|
|
144
|
-
value[key] = v.get()
|
|
145
|
-
} catch {
|
|
146
|
-
// 坏 ref → 视为未设置,交给默认层兜底(绝不让它把整份配置拖挂)。
|
|
147
|
-
value[key] = undefined
|
|
148
|
-
}
|
|
149
|
-
} else {
|
|
150
|
-
value[key] = v
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return { value, error: null }
|
|
154
|
-
} catch (e) {
|
|
155
|
-
return { value: base, error: String((e && e.message) || e) }
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* 算出用于默认层的 base,以及与之配套的**规范化快照**。
|
|
161
|
-
*
|
|
162
|
-
* 若文件值过不了 schema(例如某个字段类型写错),这里回退内置默认 —— 设置表单保住,
|
|
163
|
-
* 坏值被忽略(而不是让整个守卫静默失效)。
|
|
164
|
-
*
|
|
165
|
-
* @param {object} [fileConfig]
|
|
166
|
-
* @returns {{base:object, canonical:object, error:string|null}}
|
|
167
|
-
*/
|
|
168
|
-
export function settingsBaseFor(fileConfig) {
|
|
169
|
-
const fileBase = effectiveDefaults(fileConfig)
|
|
170
|
-
const checked = canonicalSettings(fileBase)
|
|
171
|
-
if (checked.error === null) return { base: fileBase, canonical: checked.value, error: null }
|
|
172
|
-
const fallback = { ...DEFAULT_SETTINGS }
|
|
173
|
-
return { base: fallback, canonical: canonicalSettings(fallback).value, error: checked.error }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 由配置文件算出「有效默认值」(DEFAULT_SETTINGS + 文件覆盖)。
|
|
178
|
-
* @param {object} [fileConfig]
|
|
179
|
-
* @returns {object}
|
|
180
|
-
*/
|
|
181
|
-
export function effectiveDefaults(fileConfig) {
|
|
182
|
-
return { ...DEFAULT_SETTINGS, ...(fileConfig !== null && typeof fileConfig === 'object' ? fileConfig : {}) }
|
|
183
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* dsh-offpeak — 设置 schema(0.1.7 线:**声明式 volatile 设置**)。
|
|
3
|
+
*
|
|
4
|
+
* **本线的 `settings` 服务仍在**(类 `SettingsForms`,有 `describe()` / `configure()`),
|
|
5
|
+
* 但 `dsh-settings` **已没有 `register()` 与 `get()`** —— 这(而非「服务缺席」)才是设置面
|
|
6
|
+
* 改用官方推荐声明式形态的原因:
|
|
7
|
+
* - `src/index.js` 导出 `export const Config = SettingsSchema`,dsh 依据 schema 里的
|
|
8
|
+
* `.volatile()` 字段**自动生成设置表单**;
|
|
9
|
+
* - 运行时经 `apply(ctx, config)` 的组合条目读取(volatile 值是 live ref,
|
|
10
|
+
* 用 `.get()` 解引),见 `src/index.js` 的 `resolveEntryConfig()`;
|
|
11
|
+
* - **不再有任何 register 调用**(没有可调用的 `register`),插件也**不 inject `settings`**
|
|
12
|
+
* —— 它不从该服务消费任何东西,少一个依赖更干净。
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ 真正会让应用 **web boot 致命失败**的是**客户端**缺失的 `settingsScope`(写成静态
|
|
15
|
+
* `inject` 会让客户端条目永远 pending),与宿主的 `settings` 服务无关 —— 见
|
|
16
|
+
* `src/client/index.ts` 的兼容性注记。
|
|
17
|
+
*
|
|
18
|
+
* 本文件只保留纯数据 + 纯校验:
|
|
19
|
+
* - `DEFAULT_SETTINGS`:核心逻辑依赖的默认值;
|
|
20
|
+
* - `SettingsSchema`:字段级 `.default()` + `.volatile()`;
|
|
21
|
+
* - `effectiveDefaults` / `canonicalSettings` / `settingsBaseFor`:把
|
|
22
|
+
* `config/offpeak.json` 的值并进默认层并用 schema 校验(坏值回退内置默认,
|
|
23
|
+
* 绝不因配置文件写错而让守卫静默失效)。
|
|
24
|
+
*/
|
|
25
|
+
import z from '@deepseek-ai/schemastery'
|
|
26
|
+
import { DEFAULT_RETRY } from './retry.js'
|
|
27
|
+
import { DEFAULT_WEEKEND_DAYS, WEEKEND_MODE_OFF_PEAK } from './time-policy.js'
|
|
28
|
+
|
|
29
|
+
/** 设置命名空间(设置 → 插件 → session-guard)。 */
|
|
30
|
+
export const NS = 'offpeak'
|
|
31
|
+
|
|
32
|
+
/** 默认配置(核心逻辑依赖;设置不可用时即用此值)。 */
|
|
33
|
+
export const DEFAULT_SETTINGS = Object.freeze({
|
|
34
|
+
enabled: true, // 高峰自动处理开关(简单开关)
|
|
35
|
+
offPeakAutoResume: true, // 低谷自动恢复开关:低峰时段自动恢复被高峰暂停的会话
|
|
36
|
+
weekendMode: true, // 周末模式开关:周末整日视为 off-peak(简单开关)
|
|
37
|
+
timezone: 'Asia/Shanghai', // 全部判定的时区(spec §六.3)
|
|
38
|
+
weekendDays: [...DEFAULT_WEEKEND_DAYS], // 哪几天算周末(spec §一 weekendPolicy.days)
|
|
39
|
+
weekendPolicyMode: WEEKEND_MODE_OFF_PEAK, // 周末策略模式(目前仅 offPeak)
|
|
40
|
+
// 峰窗口:name 仅用于展示/日志,days 省略或为空 = 每天生效(spec §六.1)。
|
|
41
|
+
// v0.3.0 起不再硬编码——全部来自 config/offpeak.json。
|
|
42
|
+
peakWindows: [
|
|
43
|
+
{ name: 'morning', start: '09:00', end: '12:00', days: ['mon', 'tue', 'wed', 'thu', 'fri'] },
|
|
44
|
+
{ name: 'afternoon', start: '14:00', end: '18:00', days: ['mon', 'tue', 'wed', 'thu', 'fri'] },
|
|
45
|
+
],
|
|
46
|
+
peakTimezone: '', // 仅覆盖峰窗口判定的时区;'' = 跟随 timezone(spec §六.3)
|
|
47
|
+
pauseMode: 'safe', // 透传 taskControl.pause mode
|
|
48
|
+
pauseReason: 'wait', // 透传 taskControl.pause reason
|
|
49
|
+
stepLevelPause: true, // step 级门控(agent/pre-step):高峰在下一个 step 前拉门
|
|
50
|
+
stepGateTimeoutMs: 300_000, // step 门控超时(防死锁;到期升级为 turn 级 force 暂停)
|
|
51
|
+
queueFallback: true, // 无会话门时回退锁等待队列(简单开关)
|
|
52
|
+
retryEnabled: false, // 自动重试开关(后端,D9;默认关,保守)
|
|
53
|
+
retryText: DEFAULT_RETRY.retryText,
|
|
54
|
+
retryGraceMs: DEFAULT_RETRY.retryGraceMs,
|
|
55
|
+
retryCooldownMs: DEFAULT_RETRY.retryCooldownMs,
|
|
56
|
+
retryBackoffFactor: DEFAULT_RETRY.retryBackoffFactor,
|
|
57
|
+
retryBackoffMaxMs: DEFAULT_RETRY.retryBackoffMaxMs,
|
|
58
|
+
retryMaxConsecutive: DEFAULT_RETRY.retryMaxConsecutive,
|
|
59
|
+
// ── 官方 provider 二维判定(高峰 × 目标源)──
|
|
60
|
+
providerGuard: true, // 二维判定总开关;关掉 = 现有纯时间判定
|
|
61
|
+
officialProviders: [], // 追加的官方 provider id(精确匹配,最高优先级)
|
|
62
|
+
// 官方**模型名**名单(精确匹配、大小写不敏感;v3.1.6 起没有关键字/正则的包含匹配):
|
|
63
|
+
// 默认空 —— 不勾不拦;面板勾选 / 配置文件写入的就是这份名单。
|
|
64
|
+
officialModelPatterns: [],
|
|
65
|
+
officialBaseURLs: ['api.deepseek.com'], // 官方端点 host 名单
|
|
66
|
+
deferredResume: true, // 退峰自动继续被延后的请求/会话
|
|
67
|
+
deferredResumeText: '继续(高峰已过,自动继续)', // error 模式退峰 followup 文案
|
|
68
|
+
deferredMode: 'hold', // hold(挂起不报错)/ error(抛错 + 记延后)
|
|
69
|
+
deferredMaxHoldMs: 6 * 60 * 60 * 1000, // 挂起上限(6h),到期转 error
|
|
70
|
+
guardSubagents: true, // 是否纳入子代理请求
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 设置 schema(schemastery 原生命令,zod 变体):
|
|
75
|
+
* - 枚举用 z.union([z.const(...)]);默认值用字段级 .default()。
|
|
76
|
+
* - **每个字段都标 `.volatile()`**:0.1.7 线用它自动生成设置表单。
|
|
77
|
+
* - peakWindows 的 name/days 为 v0.3.0 新增;未提供时 default 兜底,
|
|
78
|
+
* 因此升级后已存的旧形态(只有 start/end)仍然合法。
|
|
79
|
+
*/
|
|
80
|
+
export const SettingsSchema = z.object({
|
|
81
|
+
enabled: z.boolean().default(DEFAULT_SETTINGS.enabled).volatile(),
|
|
82
|
+
offPeakAutoResume: z.boolean().default(DEFAULT_SETTINGS.offPeakAutoResume).volatile(),
|
|
83
|
+
weekendMode: z.boolean().default(DEFAULT_SETTINGS.weekendMode).volatile(),
|
|
84
|
+
timezone: z.string().default(DEFAULT_SETTINGS.timezone).volatile(),
|
|
85
|
+
weekendDays: z.array(z.string()).default([...DEFAULT_SETTINGS.weekendDays]).volatile(),
|
|
86
|
+
weekendPolicyMode: z
|
|
87
|
+
.union([z.const(WEEKEND_MODE_OFF_PEAK)])
|
|
88
|
+
.default(DEFAULT_SETTINGS.weekendPolicyMode)
|
|
89
|
+
.volatile(),
|
|
90
|
+
peakWindows: z
|
|
91
|
+
.array(
|
|
92
|
+
z.object({
|
|
93
|
+
name: z.string().default(''),
|
|
94
|
+
start: z.string(),
|
|
95
|
+
end: z.string(),
|
|
96
|
+
days: z.array(z.string()).default([]),
|
|
97
|
+
}),
|
|
98
|
+
)
|
|
99
|
+
.default(DEFAULT_SETTINGS.peakWindows)
|
|
100
|
+
.volatile(),
|
|
101
|
+
peakTimezone: z.string().default(DEFAULT_SETTINGS.peakTimezone).volatile(),
|
|
102
|
+
pauseMode: z.union([z.const('safe'), z.const('force')]).default(DEFAULT_SETTINGS.pauseMode).volatile(),
|
|
103
|
+
pauseReason: z.union([z.const('wait'), z.const('stop')]).default(DEFAULT_SETTINGS.pauseReason).volatile(),
|
|
104
|
+
stepLevelPause: z.boolean().default(DEFAULT_SETTINGS.stepLevelPause).volatile(),
|
|
105
|
+
stepGateTimeoutMs: z.number().min(0).default(DEFAULT_SETTINGS.stepGateTimeoutMs).volatile(),
|
|
106
|
+
queueFallback: z.boolean().default(DEFAULT_SETTINGS.queueFallback).volatile(),
|
|
107
|
+
retryEnabled: z.boolean().default(DEFAULT_SETTINGS.retryEnabled).volatile(),
|
|
108
|
+
retryText: z.string().default(DEFAULT_SETTINGS.retryText).volatile(),
|
|
109
|
+
retryGraceMs: z.number().min(0).default(DEFAULT_SETTINGS.retryGraceMs).volatile(),
|
|
110
|
+
retryCooldownMs: z.number().min(0).default(DEFAULT_SETTINGS.retryCooldownMs).volatile(),
|
|
111
|
+
retryBackoffFactor: z.number().min(1).default(DEFAULT_SETTINGS.retryBackoffFactor).volatile(),
|
|
112
|
+
retryBackoffMaxMs: z.number().min(0).default(DEFAULT_SETTINGS.retryBackoffMaxMs).volatile(),
|
|
113
|
+
retryMaxConsecutive: z.number().min(0).default(DEFAULT_SETTINGS.retryMaxConsecutive).volatile(),
|
|
114
|
+
providerGuard: z.boolean().default(DEFAULT_SETTINGS.providerGuard).volatile(),
|
|
115
|
+
officialProviders: z.array(z.string()).default(DEFAULT_SETTINGS.officialProviders).volatile(),
|
|
116
|
+
officialModelPatterns: z.array(z.string()).default(DEFAULT_SETTINGS.officialModelPatterns).volatile(),
|
|
117
|
+
officialBaseURLs: z.array(z.string()).default(DEFAULT_SETTINGS.officialBaseURLs).volatile(),
|
|
118
|
+
deferredResume: z.boolean().default(DEFAULT_SETTINGS.deferredResume).volatile(),
|
|
119
|
+
deferredResumeText: z.string().default(DEFAULT_SETTINGS.deferredResumeText).volatile(),
|
|
120
|
+
deferredMode: z.union([z.const('hold'), z.const('error')]).default(DEFAULT_SETTINGS.deferredMode).volatile(),
|
|
121
|
+
deferredMaxHoldMs: z.number().min(0).default(DEFAULT_SETTINGS.deferredMaxHoldMs).volatile(),
|
|
122
|
+
guardSubagents: z.boolean().default(DEFAULT_SETTINGS.guardSubagents).volatile(),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 把 base 过一遍 schema,得到**规范化形态**(补齐 schema 的字段级默认值)。
|
|
127
|
+
*
|
|
128
|
+
* 注意:因为字段都标了 `.volatile()`,`SettingsSchema(base)` 解析出来的是 **live ref**
|
|
129
|
+
* (每个字段一个带 `.get()` 的对象,dsh 的自动表单与实时读取依赖这个形态)。
|
|
130
|
+
* 本函数负责把 ref **解引成普通值**,供默认层合并与比较使用。
|
|
131
|
+
*
|
|
132
|
+
* 坏值不抛给调用方,而是带回 `error` 说明。
|
|
133
|
+
*
|
|
134
|
+
* @param {object} base
|
|
135
|
+
* @returns {{value:object, error:string|null}}
|
|
136
|
+
*/
|
|
137
|
+
export function canonicalSettings(base) {
|
|
138
|
+
try {
|
|
139
|
+
const resolved = SettingsSchema(base)
|
|
140
|
+
const value = {}
|
|
141
|
+
for (const [key, v] of Object.entries(resolved)) {
|
|
142
|
+
if (v !== null && typeof v === 'object' && typeof v.get === 'function') {
|
|
143
|
+
try {
|
|
144
|
+
value[key] = v.get()
|
|
145
|
+
} catch {
|
|
146
|
+
// 坏 ref → 视为未设置,交给默认层兜底(绝不让它把整份配置拖挂)。
|
|
147
|
+
value[key] = undefined
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
value[key] = v
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { value, error: null }
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return { value: base, error: String((e && e.message) || e) }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 算出用于默认层的 base,以及与之配套的**规范化快照**。
|
|
161
|
+
*
|
|
162
|
+
* 若文件值过不了 schema(例如某个字段类型写错),这里回退内置默认 —— 设置表单保住,
|
|
163
|
+
* 坏值被忽略(而不是让整个守卫静默失效)。
|
|
164
|
+
*
|
|
165
|
+
* @param {object} [fileConfig]
|
|
166
|
+
* @returns {{base:object, canonical:object, error:string|null}}
|
|
167
|
+
*/
|
|
168
|
+
export function settingsBaseFor(fileConfig) {
|
|
169
|
+
const fileBase = effectiveDefaults(fileConfig)
|
|
170
|
+
const checked = canonicalSettings(fileBase)
|
|
171
|
+
if (checked.error === null) return { base: fileBase, canonical: checked.value, error: null }
|
|
172
|
+
const fallback = { ...DEFAULT_SETTINGS }
|
|
173
|
+
return { base: fallback, canonical: canonicalSettings(fallback).value, error: checked.error }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 由配置文件算出「有效默认值」(DEFAULT_SETTINGS + 文件覆盖)。
|
|
178
|
+
* @param {object} [fileConfig]
|
|
179
|
+
* @returns {object}
|
|
180
|
+
*/
|
|
181
|
+
export function effectiveDefaults(fileConfig) {
|
|
182
|
+
return { ...DEFAULT_SETTINGS, ...(fileConfig !== null && typeof fileConfig === 'object' ? fileConfig : {}) }
|
|
183
|
+
}
|