dsh-recall-plugin 1.7.1 → 2.0.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/CHANGELOG.md +17 -0
- package/README.en.md +21 -18
- package/README.md +28 -19
- package/lib/client.js +266 -53
- package/lib/config.js +43 -2
- package/lib/index.js +153 -24
- package/lib/maintenance.js +263 -145
- package/lib/snapshots.js +73 -50
- package/package.json +15 -8
package/lib/client.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 这是 dsh.client bundle(exports["./client"]),由 client-modules 打包成
|
|
11
11
|
* /plugins/<pkg>/client.js 注入页面;factory 内 require("react") 由平台
|
|
12
|
-
*
|
|
12
|
+
* 模块表提供。当前为纯 JS + React.createElement 直写。
|
|
13
13
|
*/
|
|
14
14
|
window.__ModuleLoader__.load({
|
|
15
15
|
id: "dsh-recall-plugin",
|
|
@@ -73,6 +73,7 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
'.dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}',
|
|
74
74
|
'.dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
|
|
75
75
|
'.dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}',
|
|
76
|
+
'.dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}',
|
|
76
77
|
'.dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}',
|
|
77
78
|
'.dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}',
|
|
78
79
|
'.dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}',
|
|
@@ -175,7 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
175
176
|
// has:false 且不再重试,撤回按钮将永不出现。
|
|
176
177
|
// init 顺带下发插件行为开关(refillDraft 等),存进 pluginConfig
|
|
177
178
|
// 供撤回执行链读取——设置页改配置 + 重启后随下一次 init 刷新。
|
|
178
|
-
const pluginConfig = { refillDraft: true }
|
|
179
|
+
const pluginConfig = { refillDraft: true, archiveOriginal: true }
|
|
179
180
|
function ensureInit(sessionId) {
|
|
180
181
|
if (!sessionId) return Promise.resolve()
|
|
181
182
|
const cached = initMap.get(sessionId)
|
|
@@ -183,6 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
183
184
|
const done = api('init', { sessionId }).then((res) => {
|
|
184
185
|
if (res && res.config && typeof res.config === 'object') {
|
|
185
186
|
if (typeof res.config.refillDraft === 'boolean') pluginConfig.refillDraft = res.config.refillDraft
|
|
187
|
+
if (typeof res.config.archiveOriginal === 'boolean') pluginConfig.archiveOriginal = res.config.archiveOriginal
|
|
186
188
|
}
|
|
187
189
|
const notice = res && res.notice
|
|
188
190
|
if (notice && notice.unsupported) {
|
|
@@ -472,9 +474,36 @@ window.__ModuleLoader__.load({
|
|
|
472
474
|
if (recall.stage !== 'confirm') return
|
|
473
475
|
const changes = recall.changes || []
|
|
474
476
|
const previewCut = typeof recall.cutSeq === 'number' ? recall.cutSeq : null
|
|
477
|
+
// P0-3:携带预览摘要(total 是完整计数,与 Host 侧 diffFor 的
|
|
478
|
+
// total 对齐;changes 截断到 500 条,不能用来比对)。Host 端
|
|
479
|
+
// 据此在 execute 时校验「预览后文件集是否变化」,变了则返回
|
|
480
|
+
// STALE 拒绝执行,防止把工作区改到用户没看过的新状态。
|
|
481
|
+
const previewTotal = typeof recall.total === 'number' ? recall.total : changes.length
|
|
475
482
|
setRecall({ stage: 'executing', changes })
|
|
476
|
-
api('execute', { messageId, sessionId }).then(async (res) => {
|
|
483
|
+
api('execute', { messageId, sessionId, previewTotal, previewAt: Date.now() }).then(async (res) => {
|
|
477
484
|
if (!res || !res.ok) {
|
|
485
|
+
// STALE:预览后文件变了——自动重新拉一次最新清单回到确认
|
|
486
|
+
// 阶段让用户看新内容,而不是停在错误面板
|
|
487
|
+
if (res && res.code === 'STALE') {
|
|
488
|
+
setRecall({ stage: 'loading' })
|
|
489
|
+
api('preview', { messageId, sessionId }).then((res2) => {
|
|
490
|
+
if (!res2 || !res2.ok) {
|
|
491
|
+
setRecall({ stage: 'error', message: (res2 && (res2.message || res2.error)) || '无法获取快照' })
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
setRecall({
|
|
495
|
+
stage: 'confirm',
|
|
496
|
+
changes: res2.changes || [],
|
|
497
|
+
total: typeof res2.total === 'number' ? res2.total : (res2.changes || []).length,
|
|
498
|
+
truncated: Boolean(res2.truncated),
|
|
499
|
+
time: res2.time || null,
|
|
500
|
+
cutSeq: typeof res2.cutSeq === 'number' ? res2.cutSeq : null
|
|
501
|
+
})
|
|
502
|
+
}).catch((error) => {
|
|
503
|
+
setRecall({ stage: 'error', message: String(error) })
|
|
504
|
+
})
|
|
505
|
+
return
|
|
506
|
+
}
|
|
478
507
|
setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '回退失败' })
|
|
479
508
|
return
|
|
480
509
|
}
|
|
@@ -496,8 +525,10 @@ window.__ModuleLoader__.load({
|
|
|
496
525
|
if (typeof sessionsSvc.open === 'function') sessionsSvc.open(childId)
|
|
497
526
|
chatReverted = true
|
|
498
527
|
fillTarget = childId
|
|
499
|
-
//
|
|
500
|
-
|
|
528
|
+
// 回退前的原会话归档(可关,见设置页 archiveOriginal):只是从列表
|
|
529
|
+
// 隐藏、可恢复,避免侧栏出现两个近似会话;关闭时原会话
|
|
530
|
+
// 保留供对照
|
|
531
|
+
if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === 'function') {
|
|
501
532
|
workspacesSvc.archiveSession(sessionId).catch(() => {})
|
|
502
533
|
}
|
|
503
534
|
} else {
|
|
@@ -682,7 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
682
713
|
}, s))
|
|
683
714
|
),
|
|
684
715
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
685
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
716
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
686
717
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: !dirty || state.busy, onClick: discard }, '放弃修改'),
|
|
687
718
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', disabled: !dirty || state.busy, onClick: save }, '保存')
|
|
688
719
|
)
|
|
@@ -698,6 +729,21 @@ window.__ModuleLoader__.load({
|
|
|
698
729
|
const [usage, setUsage] = React.useState(null)
|
|
699
730
|
const [errors, setErrors] = React.useState(null)
|
|
700
731
|
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
732
|
+
// 快照全量计数与当前拉取上限(S1-2):Host 按 limit 切片返回,
|
|
733
|
+
// total 是全量——树默认折叠,全量加载到 2000 上限对 DOM 无压力,
|
|
734
|
+
// 分页与树形组装互斥,故用「加载更多」而非翻页。
|
|
735
|
+
const [limit, setLimit] = React.useState(200)
|
|
736
|
+
const [total, setTotal] = React.useState(0)
|
|
737
|
+
// 存储健康(S2-4):git 可用性 + home/降级分区计数,来自 usage
|
|
738
|
+
// 端点的扩展响应;git 不可用/降级是消息流一次性 toast 之外常驻
|
|
739
|
+
// 可见的排障信息。
|
|
740
|
+
const [health, setHealth] = React.useState(null)
|
|
741
|
+
// 树搜索关键字(S3-4):纯客户端过滤已加载条目,按工作区名/会话
|
|
742
|
+
// 标题/消息文本/快照 ID 匹配;只过滤当前已加载数据,与 S1-2 的
|
|
743
|
+
// limit 正交。
|
|
744
|
+
const [query, setQuery] = React.useState('')
|
|
745
|
+
// 最近错误展开态(S3-5):默认 5 条,可展开全部 20 条/收起
|
|
746
|
+
const [showAllErrors, setShowAllErrors] = React.useState(false)
|
|
701
747
|
// 冷会话标题在服务端要整日志解压(10 秒级),首屏不等它:列表
|
|
702
748
|
// 先出(live/缓存标题),拿到后对缺标题的会话异步补拉再合并。
|
|
703
749
|
const [titlesPending, setTitlesPending] = React.useState(false)
|
|
@@ -746,10 +792,12 @@ window.__ModuleLoader__.load({
|
|
|
746
792
|
}).catch(() => {})
|
|
747
793
|
}
|
|
748
794
|
|
|
749
|
-
function refresh() {
|
|
750
|
-
|
|
795
|
+
function refresh(overLimit) {
|
|
796
|
+
const useLimit = overLimit || limit
|
|
797
|
+
api('manage', { op: 'list', limit: useLimit }).then((res) => {
|
|
751
798
|
if (res && res.ok) {
|
|
752
799
|
setItems(res.items || [])
|
|
800
|
+
setTotal(typeof res.total === 'number' ? res.total : (res.items || []).length)
|
|
753
801
|
fetchTitles(res.items || [])
|
|
754
802
|
fetchMessages(res.items || [])
|
|
755
803
|
}
|
|
@@ -758,7 +806,10 @@ window.__ModuleLoader__.load({
|
|
|
758
806
|
// 渲染后,用户先看到树形内容,占用再异步补上(usage 不带
|
|
759
807
|
// sessionId,Host 汇总全部工作区)。
|
|
760
808
|
api('manage', { op: 'usage' }).then((res) => {
|
|
761
|
-
if (res && res.ok)
|
|
809
|
+
if (res && res.ok) {
|
|
810
|
+
setUsage(res.bytes || 0)
|
|
811
|
+
setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
|
|
812
|
+
}
|
|
762
813
|
}).catch(() => {})
|
|
763
814
|
api('status', {}).then((res) => {
|
|
764
815
|
if (res && res.ok) setErrors(res.errors || [])
|
|
@@ -766,7 +817,10 @@ window.__ModuleLoader__.load({
|
|
|
766
817
|
}).catch(() => {
|
|
767
818
|
// list 失败时仍尝试补 usage/status,避免整卡全空
|
|
768
819
|
api('manage', { op: 'usage' }).then((res) => {
|
|
769
|
-
if (res && res.ok)
|
|
820
|
+
if (res && res.ok) {
|
|
821
|
+
setUsage(res.bytes || 0)
|
|
822
|
+
setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
|
|
823
|
+
}
|
|
770
824
|
}).catch(() => {})
|
|
771
825
|
api('status', {}).then((res) => {
|
|
772
826
|
if (res && res.ok) setErrors(res.errors || [])
|
|
@@ -776,6 +830,13 @@ window.__ModuleLoader__.load({
|
|
|
776
830
|
|
|
777
831
|
React.useEffect(() => { refresh() }, [])
|
|
778
832
|
|
|
833
|
+
// S3-5:清空 Host 侧错误缓冲并同步本地状态;Host 已清后下次
|
|
834
|
+
// refresh 拉回为空,不重复请求。
|
|
835
|
+
function clearErrors() {
|
|
836
|
+
setErrors([])
|
|
837
|
+
api('status', { op: 'clear' }).catch(() => {})
|
|
838
|
+
}
|
|
839
|
+
|
|
779
840
|
function run(op, extra, doneText) {
|
|
780
841
|
if (state.busy) return
|
|
781
842
|
setState({ busy: true, message: '执行中…', error: false })
|
|
@@ -795,17 +856,17 @@ window.__ModuleLoader__.load({
|
|
|
795
856
|
function renderDeleteAllConfirm() {
|
|
796
857
|
if (!confirming || confirming.kind !== 'all') return null
|
|
797
858
|
return React.createElement('div', { className: 'dsh-recall-tree-confirm' },
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
859
|
+
'确认删除所有工作区的全部快照?此操作不可恢复。',
|
|
860
|
+
React.createElement('button', {
|
|
861
|
+
type: 'button',
|
|
862
|
+
className: 'dsh-recall-btn dsh-recall-btn-danger',
|
|
863
|
+
onClick: () => {
|
|
864
|
+
setConfirming(null)
|
|
865
|
+
run('deleteAll', {}, '已清空全部快照')
|
|
866
|
+
}
|
|
867
|
+
}, '确认全部删除'),
|
|
868
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirming(null) }, '取消')
|
|
869
|
+
)
|
|
809
870
|
}
|
|
810
871
|
|
|
811
872
|
// 树形管理:工作区 → 会话 → 快照三级。折叠状态用 Set<key> 记录,
|
|
@@ -843,7 +904,16 @@ window.__ModuleLoader__.load({
|
|
|
843
904
|
}
|
|
844
905
|
return wsList
|
|
845
906
|
}
|
|
846
|
-
const
|
|
907
|
+
const q = query.trim().toLowerCase()
|
|
908
|
+
const filteredItems = q
|
|
909
|
+
? (items || []).filter((it) =>
|
|
910
|
+
(it.workspace || '').toLowerCase().indexOf(q) >= 0 ||
|
|
911
|
+
(it.sessionTitle || '').toLowerCase().indexOf(q) >= 0 ||
|
|
912
|
+
(it.messageText || '').toLowerCase().indexOf(q) >= 0 ||
|
|
913
|
+
String(it.id || '').toLowerCase().indexOf(q) >= 0
|
|
914
|
+
)
|
|
915
|
+
: items
|
|
916
|
+
const tree = buildTree(filteredItems)
|
|
847
917
|
// 每个节点的删除按钮统一走 run();workspace/session 用新端点 scope
|
|
848
918
|
// 批量删,叶子沿用原单条删除。二次确认文案按节点层级区分。
|
|
849
919
|
function confirmDelete(kind, key, extra, text) {
|
|
@@ -948,21 +1018,63 @@ window.__ModuleLoader__.load({
|
|
|
948
1018
|
}
|
|
949
1019
|
const treeNodes = tree.map(renderWorkspace)
|
|
950
1020
|
|
|
1021
|
+
// 计数用 Host 返回的全量 total 而非已加载条数(S1-2):loaded 为
|
|
1022
|
+
// null 表示首屏加载中(显示 …),已加载但 total 超过当前 limit
|
|
1023
|
+
// 时提示「当前显示最新 X 条」与加载更多入口。
|
|
1024
|
+
const loaded = items ? items.length : null
|
|
1025
|
+
const countText = loaded === null
|
|
1026
|
+
? '共 … 条快照'
|
|
1027
|
+
: '共 ' + total + ' 条快照' + (limit < total ? '(当前显示最新 ' + loaded + ' 条)' : '')
|
|
1028
|
+
|
|
1029
|
+
// 加载更多:上限 2000 与 Host 端钳制一致;total 已含全量计数,
|
|
1030
|
+
// 直接按 total 一次拉满(再往上就是 2000 封顶)
|
|
1031
|
+
function loadMore() {
|
|
1032
|
+
const next = Math.min(Math.max(total, limit), 2000)
|
|
1033
|
+
if (next <= limit) return
|
|
1034
|
+
setLimit(next)
|
|
1035
|
+
refresh(next)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
951
1038
|
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
952
1039
|
React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照管理'),
|
|
953
1040
|
React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
954
1041
|
usage === null
|
|
955
|
-
?
|
|
956
|
-
:
|
|
1042
|
+
? countText + '。'
|
|
1043
|
+
: countText + ',全部工作区快照存储占用 ' + sizeText(usage) + '。'
|
|
957
1044
|
),
|
|
1045
|
+
health ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'health' },
|
|
1046
|
+
React.createElement('span', {
|
|
1047
|
+
className: health.gitAvailable ? '' : 'dsh-recall-ex-status-error'
|
|
1048
|
+
}, health.gitAvailable ? 'git 可用' : 'git 不可用(快照引擎依赖 git)'),
|
|
1049
|
+
' · 快照存储:home ' + health.homeStores + ' 个工作区' + (health.fallbackStores ? ',降级 ' + health.fallbackStores + ' 个' : '')
|
|
1050
|
+
) : null,
|
|
1051
|
+
React.createElement('input', {
|
|
1052
|
+
className: 'dsh-recall-ex-input',
|
|
1053
|
+
placeholder: '搜索工作区 / 会话标题 / 消息内容 / ID',
|
|
1054
|
+
value: query,
|
|
1055
|
+
spellCheck: false,
|
|
1056
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1057
|
+
}),
|
|
958
1058
|
treeNodes.length > 0 ? React.createElement('div', { className: 'dsh-recall-tree' }, ...treeNodes) : null,
|
|
1059
|
+
items && items.length === 0 && !q
|
|
1060
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'empty' }, '在任意工作区发送一条消息后,这里会出现快照。')
|
|
1061
|
+
: null,
|
|
1062
|
+
q && filteredItems && filteredItems.length === 0
|
|
1063
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'no-match' }, '无匹配快照')
|
|
1064
|
+
: null,
|
|
959
1065
|
renderDeleteAllConfirm(),
|
|
960
1066
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
961
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
1067
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
1068
|
+
limit < total ? React.createElement('button', {
|
|
1069
|
+
type: 'button',
|
|
1070
|
+
className: 'dsh-recall-btn',
|
|
1071
|
+
disabled: state.busy,
|
|
1072
|
+
onClick: loadMore
|
|
1073
|
+
}, '加载更多') : null,
|
|
962
1074
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy, onClick: refresh }, '刷新'),
|
|
963
1075
|
React.createElement('button', {
|
|
964
1076
|
type: 'button',
|
|
965
|
-
className: 'dsh-recall-btn',
|
|
1077
|
+
className: 'dsh-recall-btn dsh-recall-btn-danger',
|
|
966
1078
|
disabled: state.busy,
|
|
967
1079
|
title: '删除全部工作区的所有快照;会直接核对并删除 git tag(即使列表为空也可清理残留)',
|
|
968
1080
|
onClick: () => setConfirming({ kind: 'all' })
|
|
@@ -976,9 +1088,15 @@ window.__ModuleLoader__.load({
|
|
|
976
1088
|
}, '立即 gc')
|
|
977
1089
|
),
|
|
978
1090
|
errors && errors.length > 0
|
|
979
|
-
? React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
980
|
-
'
|
|
981
|
-
|
|
1091
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'errors' },
|
|
1092
|
+
React.createElement('div', { className: 'dsh-recall-ex-status' },
|
|
1093
|
+
'最近错误:',
|
|
1094
|
+
(showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement('div', { key: i, className: 'dsh-recall-ex-note' }, clockText(e.time) + ' ' + e.message))
|
|
1095
|
+
),
|
|
1096
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
1097
|
+
errors.length > 5 ? React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? '收起' : '展开全部 (' + errors.length + ')') : null,
|
|
1098
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: clearErrors }, '清空')
|
|
1099
|
+
)
|
|
982
1100
|
)
|
|
983
1101
|
: null
|
|
984
1102
|
)
|
|
@@ -1021,6 +1139,16 @@ window.__ModuleLoader__.load({
|
|
|
1021
1139
|
)
|
|
1022
1140
|
}
|
|
1023
1141
|
|
|
1142
|
+
// ConfigForm 的字节↔MB 换算:持久化与 schema 都是字节(config-get
|
|
1143
|
+
// 下发原值),只在 display/input 层换算成人工友好的 MB 小数;round
|
|
1144
|
+
// 2 位小数去尾零,避免默认值 104857600 裸奔成一长串数字
|
|
1145
|
+
// (plan-settings-ux S1-1)。
|
|
1146
|
+
function bytesToMb(bytes) {
|
|
1147
|
+
const n = Number(bytes)
|
|
1148
|
+
if (!Number.isFinite(n) || n <= 0) return ''
|
|
1149
|
+
return String(Math.round((n / 1048576) * 100) / 100)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1024
1152
|
// 插件配置表单:值经 Host 的 settings namespace「dsh-recall」读写
|
|
1025
1153
|
// (config-get / config-set 端点),保存即持久化进用户 settings 文档
|
|
1026
1154
|
// 并热生效(Host watch 链路原地更新 cfg),无需重启。只提交相对
|
|
@@ -1032,6 +1160,9 @@ window.__ModuleLoader__.load({
|
|
|
1032
1160
|
const [overridden, setOverridden] = React.useState({})
|
|
1033
1161
|
const [writable, setWritable] = React.useState(true)
|
|
1034
1162
|
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
1163
|
+
// 「高级:基础排除表」折叠态(S3-2):与 exclude.txt 同为 gitignore
|
|
1164
|
+
// 编辑器,叠放会造成认知负担——内置规则收进折叠,只留高频项在首屏
|
|
1165
|
+
const [showAdvanced, setShowAdvanced] = React.useState(false)
|
|
1035
1166
|
|
|
1036
1167
|
function load() {
|
|
1037
1168
|
api('config-get', {}).then((res) => {
|
|
@@ -1040,9 +1171,13 @@ window.__ModuleLoader__.load({
|
|
|
1040
1171
|
const next = {
|
|
1041
1172
|
gcSnaps: String(v.gcSnaps == null ? '' : v.gcSnaps),
|
|
1042
1173
|
gcHours: String(v.gcHours == null ? '' : v.gcHours),
|
|
1043
|
-
maxFileBytes:
|
|
1174
|
+
maxFileBytes: bytesToMb(v.maxFileBytes),
|
|
1175
|
+
maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? '' : v.maxSnapshotsPerWorkspace),
|
|
1044
1176
|
baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join('\n') : '',
|
|
1045
1177
|
refillDraft: v.refillDraft !== false,
|
|
1178
|
+
snapshotEnabled: v.snapshotEnabled !== false,
|
|
1179
|
+
archiveOriginal: v.archiveOriginal !== false,
|
|
1180
|
+
retentionDays: String(v.retentionDays == null ? '' : v.retentionDays),
|
|
1046
1181
|
}
|
|
1047
1182
|
setDraft(next)
|
|
1048
1183
|
setBaseline(next)
|
|
@@ -1064,7 +1199,7 @@ window.__ModuleLoader__.load({
|
|
|
1064
1199
|
function save() {
|
|
1065
1200
|
if (state.busy || !draft || !baseline) return
|
|
1066
1201
|
const patch = {}
|
|
1067
|
-
for (const key of ['gcSnaps', 'gcHours', 'maxFileBytes', 'baseExcludes', 'refillDraft']) {
|
|
1202
|
+
for (const key of ['gcSnaps', 'gcHours', 'maxFileBytes', 'maxSnapshotsPerWorkspace', 'baseExcludes', 'refillDraft', 'snapshotEnabled', 'archiveOriginal', 'retentionDays']) {
|
|
1068
1203
|
if (draft[key] !== baseline[key]) patch[key] = draft[key]
|
|
1069
1204
|
}
|
|
1070
1205
|
if (!Object.keys(patch).length) {
|
|
@@ -1083,11 +1218,25 @@ window.__ModuleLoader__.load({
|
|
|
1083
1218
|
clean.gcHours = n
|
|
1084
1219
|
}
|
|
1085
1220
|
if (patch.maxFileBytes !== undefined) {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1221
|
+
// display 层是 MB 小数,持久化仍是字节(Math.round 取整):
|
|
1222
|
+
// model 侧不变,config-get/config-set 往返零改动
|
|
1223
|
+
const mb = Number(patch.maxFileBytes)
|
|
1224
|
+
if (!Number.isFinite(mb) || mb < 0.01) { setState({ busy: false, message: '文件大小上限至少 0.01 MB', error: true }); return }
|
|
1225
|
+
clean.maxFileBytes = Math.round(mb * 1048576)
|
|
1226
|
+
}
|
|
1227
|
+
if (patch.maxSnapshotsPerWorkspace !== undefined) {
|
|
1228
|
+
const n = parseInt(patch.maxSnapshotsPerWorkspace, 10)
|
|
1229
|
+
if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '快照总量上限必须是 >= 0 的整数(0 表示不限制)', error: true }); return }
|
|
1230
|
+
clean.maxSnapshotsPerWorkspace = n
|
|
1089
1231
|
}
|
|
1090
1232
|
if (patch.refillDraft !== undefined) clean.refillDraft = Boolean(patch.refillDraft)
|
|
1233
|
+
if (patch.snapshotEnabled !== undefined) clean.snapshotEnabled = Boolean(patch.snapshotEnabled)
|
|
1234
|
+
if (patch.archiveOriginal !== undefined) clean.archiveOriginal = Boolean(patch.archiveOriginal)
|
|
1235
|
+
if (patch.retentionDays !== undefined) {
|
|
1236
|
+
const n = parseInt(patch.retentionDays, 10)
|
|
1237
|
+
if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '保留天数必须是 >= 0 的整数(0 表示不启用)', error: true }); return }
|
|
1238
|
+
clean.retentionDays = n
|
|
1239
|
+
}
|
|
1091
1240
|
if (patch.baseExcludes !== undefined) {
|
|
1092
1241
|
clean.baseExcludes = String(patch.baseExcludes).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
1093
1242
|
}
|
|
@@ -1102,7 +1251,7 @@ window.__ModuleLoader__.load({
|
|
|
1102
1251
|
}).catch((e) => setState({ busy: false, message: String(e), error: true }))
|
|
1103
1252
|
}
|
|
1104
1253
|
|
|
1105
|
-
function numRow(key, label, hint) {
|
|
1254
|
+
function numRow(key, label, hint, opts) {
|
|
1106
1255
|
const locked = Boolean(envLocks && envLocks[key])
|
|
1107
1256
|
const changed = Boolean(draft && baseline && draft[key] !== baseline[key])
|
|
1108
1257
|
return React.createElement('div', { className: 'dsh-recall-cfg-row', key: key },
|
|
@@ -1113,8 +1262,11 @@ window.__ModuleLoader__.load({
|
|
|
1113
1262
|
type: 'number',
|
|
1114
1263
|
value: draft ? draft[key] : '',
|
|
1115
1264
|
disabled: locked || !writable,
|
|
1265
|
+
min: opts && opts.min,
|
|
1266
|
+
step: opts && opts.step,
|
|
1116
1267
|
onChange: (e) => edit(key, e.target.value),
|
|
1117
1268
|
}),
|
|
1269
|
+
opts && opts.suffix ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, opts.suffix) : null,
|
|
1118
1270
|
changed && !locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1119
1271
|
overridden && overridden[key] !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null,
|
|
1120
1272
|
locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '环境变量锁定') : null
|
|
@@ -1123,18 +1275,49 @@ window.__ModuleLoader__.load({
|
|
|
1123
1275
|
)
|
|
1124
1276
|
}
|
|
1125
1277
|
|
|
1278
|
+
function resetDefaults() {
|
|
1279
|
+
if (state.busy || !writable) return
|
|
1280
|
+
setState({ busy: true, message: '恢复默认中…', error: false })
|
|
1281
|
+
api('config-reset', {}).then((res) => {
|
|
1282
|
+
if (res && res.ok) {
|
|
1283
|
+
load()
|
|
1284
|
+
setState({ busy: false, message: '已恢复默认值', error: false })
|
|
1285
|
+
} else {
|
|
1286
|
+
setState({ busy: false, message: (res && (res.message || res.error)) || '恢复默认失败', error: true })
|
|
1287
|
+
}
|
|
1288
|
+
}).catch((e) => setState({ busy: false, message: String(e), error: true }))
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1126
1291
|
if (!draft) {
|
|
1127
1292
|
return React.createElement('div', { className: 'dsh-recall-ex-note' }, state.message || '正在读取配置…')
|
|
1128
1293
|
}
|
|
1129
1294
|
|
|
1130
1295
|
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1296
|
+
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'snapshotEnabled' },
|
|
1297
|
+
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1298
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-snapshot' }, '启用快照'),
|
|
1299
|
+
React.createElement('input', {
|
|
1300
|
+
id: 'dsh-recall-cfg-snapshot',
|
|
1301
|
+
type: 'checkbox',
|
|
1302
|
+
checked: Boolean(draft.snapshotEnabled),
|
|
1303
|
+
disabled: !writable,
|
|
1304
|
+
onChange: (e) => edit('snapshotEnabled', e.target.checked),
|
|
1305
|
+
}),
|
|
1306
|
+
draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1307
|
+
overridden && overridden.snapshotEnabled !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
|
|
1308
|
+
),
|
|
1309
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '关闭后不再新建快照(已有快照仍可撤回),适合临时禁用快照的场合')
|
|
1310
|
+
),
|
|
1311
|
+
numRow('gcSnaps', 'gc 触发条数', '每积累多少条快照触发一次 git gc', { min: 1, step: 1 }),
|
|
1312
|
+
numRow('gcHours', 'gc 触发小时', '距上次 gc 超过多少小时触发(与条数先到先触发)', { min: 1, step: 1 }),
|
|
1313
|
+
numRow('maxFileBytes', '文件大小上限', '超过该大小的文件不进快照、不被回退触碰(单位 MB,支持小数)', { suffix: 'MB', min: 0.01, step: 0.5 }),
|
|
1314
|
+
numRow('maxSnapshotsPerWorkspace', '快照总量上限', '每个工作区保留的最大快照数,超限自动删除最旧的;填 0 表示不限制', { min: 0, step: 1 }),
|
|
1315
|
+
numRow('retentionDays', '快照保留天数', '按天数保留快照,超期自动删除最旧的;填 0 表示不启用(与快照总数上限各自生效)', { min: 0, step: 1 }),
|
|
1134
1316
|
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'refillDraft' },
|
|
1135
1317
|
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1136
|
-
React.createElement('label', { className: 'dsh-recall-cfg-label' }, '撤回后回填输入框'),
|
|
1318
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-refill' }, '撤回后回填输入框'),
|
|
1137
1319
|
React.createElement('input', {
|
|
1320
|
+
id: 'dsh-recall-cfg-refill',
|
|
1138
1321
|
type: 'checkbox',
|
|
1139
1322
|
checked: Boolean(draft.refillDraft),
|
|
1140
1323
|
disabled: !writable,
|
|
@@ -1145,7 +1328,23 @@ window.__ModuleLoader__.load({
|
|
|
1145
1328
|
),
|
|
1146
1329
|
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回成功后把被撤回的消息文本回填到输入框,方便修改后重新发送')
|
|
1147
1330
|
),
|
|
1148
|
-
React.createElement('div', { className: 'dsh-recall-cfg-row', key: '
|
|
1331
|
+
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'archiveOriginal' },
|
|
1332
|
+
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1333
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-archive' }, '撤回后归档原会话'),
|
|
1334
|
+
React.createElement('input', {
|
|
1335
|
+
id: 'dsh-recall-cfg-archive',
|
|
1336
|
+
type: 'checkbox',
|
|
1337
|
+
checked: Boolean(draft.archiveOriginal),
|
|
1338
|
+
disabled: !writable,
|
|
1339
|
+
onChange: (e) => edit('archiveOriginal', e.target.checked),
|
|
1340
|
+
}),
|
|
1341
|
+
draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1342
|
+
overridden && overridden.archiveOriginal !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
|
|
1343
|
+
),
|
|
1344
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回后原会话从列表归档隐藏(可从归档找回);关闭则保留在列表中,方便对照回退前后的上下文')
|
|
1345
|
+
),
|
|
1346
|
+
React.createElement(SectionToggle, { title: '高级:基础排除表', open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
|
|
1347
|
+
showAdvanced ? React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'baseExcludes' },
|
|
1149
1348
|
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1150
1349
|
React.createElement('label', { className: 'dsh-recall-cfg-label' }, '基础排除表'),
|
|
1151
1350
|
draft.baseExcludes !== baseline.baseExcludes ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
@@ -1158,11 +1357,18 @@ window.__ModuleLoader__.load({
|
|
|
1158
1357
|
disabled: !writable,
|
|
1159
1358
|
onChange: (e) => edit('baseExcludes', e.target.value),
|
|
1160
1359
|
}),
|
|
1161
|
-
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, 'gitignore
|
|
1162
|
-
),
|
|
1360
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '内置规则,每个工作区共享,建议保持默认;gitignore 语法每行一条,优先级低于「排除配置」里的 exclude.txt(S3-2 折叠)')
|
|
1361
|
+
) : null,
|
|
1163
1362
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
1164
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
1363
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
1165
1364
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, '放弃修改'),
|
|
1365
|
+
React.createElement('button', {
|
|
1366
|
+
type: 'button',
|
|
1367
|
+
className: 'dsh-recall-btn',
|
|
1368
|
+
disabled: state.busy || !writable,
|
|
1369
|
+
title: '把所有字段恢复到插件出厂默认值',
|
|
1370
|
+
onClick: resetDefaults
|
|
1371
|
+
}, '恢复默认'),
|
|
1166
1372
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: save }, '保存'),
|
|
1167
1373
|
!writable ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '只读设置源') : null
|
|
1168
1374
|
)
|
|
@@ -1229,16 +1435,23 @@ window.__ModuleLoader__.load({
|
|
|
1229
1435
|
// 整个插件;lowest renders,负值恰好覆盖默认渲染器实现撤回 UI。
|
|
1230
1436
|
// priority -1 也可能被别的机器上的插件占用(同样抛冲突),所以
|
|
1231
1437
|
// 递减重试三次——最坏情况只是撤回按钮不渲染,绝不让插件加载失败。
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1438
|
+
// chat.node 的 keyed key 与节点 UI 投影 kind 对齐:'user' 是常规
|
|
1439
|
+
// 用户消息;'steering' 是 agent 运行中插入的转向指令——官方仍按
|
|
1440
|
+
// 用户气泡回显,但 keyed 'user' 不命中,会落到默认渲染、撤回按钮
|
|
1441
|
+
// 缺失(实测存储层 role 一致、仅 UI 投影 kind 不同)。两个 key
|
|
1442
|
+
// 各自独立注册,互不抢占。
|
|
1443
|
+
for (const slotKey of ['user', 'steering']) {
|
|
1444
|
+
let mounted = false
|
|
1445
|
+
for (let priority = -1; priority >= -3 && !mounted; priority--) {
|
|
1446
|
+
try {
|
|
1447
|
+
slots.inject('conversation.chat.node', () => slots.register(
|
|
1448
|
+
{ name: 'conversation.chat.node', key: slotKey, priority },
|
|
1449
|
+
UserRecallNode
|
|
1450
|
+
))
|
|
1451
|
+
mounted = true
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (priority === -3) console.error('[dsh-recall-plugin] slot register failed (' + slotKey + '):', error)
|
|
1454
|
+
}
|
|
1242
1455
|
}
|
|
1243
1456
|
}
|
|
1244
1457
|
|
package/lib/config.js
CHANGED
|
@@ -19,18 +19,36 @@ export const Config = Schema.object({
|
|
|
19
19
|
gcSnaps: Schema.number().default(50).description('每积累多少条快照触发一次 git gc'),
|
|
20
20
|
gcHours: Schema.number().default(24).description('距上次 gc 超过多少小时触发(与条数先到先触发)'),
|
|
21
21
|
maxFileBytes: Schema.number().default(104857600).description('超过该字节数的文件不进快照、不被回退触碰'),
|
|
22
|
+
maxSnapshotsPerWorkspace: Schema.number().default(500).description('每个工作区保留的最大快照数,超限删除最旧的'),
|
|
22
23
|
// 排除表必须同时覆盖两种存储目录名:降级存储是项目内 .dsh-recall-snapshots/,
|
|
23
24
|
// 而 home 存储目录名是 dsh-recall-snapshots/(无点)——工作区 root 恰为
|
|
24
25
|
// HOME 时(容器 root=/root 等)它落在工作区内,漏排除会让 git add -A
|
|
25
26
|
// 把影子仓库自己吞进去、快照全部失败(issue #6)
|
|
26
27
|
baseExcludes: Schema.array(Schema.string()).default(['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']).description('基础排除表(gitignore 语法,优先级低于 exclude.txt)'),
|
|
27
28
|
refillDraft: Schema.boolean().default(true).description('撤回后把被撤回的消息文本回填到输入框'),
|
|
29
|
+
snapshotEnabled: Schema.boolean().default(true).description('启用消息快照(关闭后不再新建,已有快照仍可撤回)'),
|
|
30
|
+
archiveOriginal: Schema.boolean().default(true).description('撤回后归档原会话(关闭后原会话保留在列表中)'),
|
|
31
|
+
retentionDays: Schema.number().default(0).description('按天数保留快照,超期自动删除;0 表示不启用'),
|
|
28
32
|
})
|
|
29
33
|
|
|
30
34
|
// schema 默认值的运行时镜像:settings 服务未组装时 createConfig 直接以
|
|
31
|
-
// 入口 config 解析,这组兜底与 Config
|
|
35
|
+
// 入口 config 解析,这组兜底与 Config 保持一致(改默认值两处同步改)。
|
|
36
|
+
// DEFAULTS 同时供 config-reset 降级路径(settings.replace 不可用时的兜底,
|
|
37
|
+
// 见 index.js config-reset 端点)——默认值只此一份,避免重置与 schema 漂移。
|
|
32
38
|
const BASE_EXCLUDES = ['.git', 'node_modules/', '.dsh-recall-snapshots/', 'dsh-recall-snapshots/']
|
|
33
39
|
|
|
40
|
+
export const DEFAULTS = {
|
|
41
|
+
gcSnaps: 50,
|
|
42
|
+
gcHours: 24,
|
|
43
|
+
maxFileBytes: 104857600,
|
|
44
|
+
maxSnapshotsPerWorkspace: 500,
|
|
45
|
+
baseExcludes: BASE_EXCLUDES,
|
|
46
|
+
refillDraft: true,
|
|
47
|
+
snapshotEnabled: true,
|
|
48
|
+
archiveOriginal: true,
|
|
49
|
+
retentionDays: 0,
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
export function createConfig(raw) {
|
|
35
53
|
const cfg = raw && typeof raw === 'object' ? raw : {}
|
|
36
54
|
|
|
@@ -44,6 +62,13 @@ export function createConfig(raw) {
|
|
|
44
62
|
const gcSnaps = pickNumber(process.env.DSH_RECALL_GC_SNAPS, pickNumber(cfg.gcSnaps, 50, 1), 1)
|
|
45
63
|
const gcHours = pickNumber(process.env.DSH_RECALL_GC_HOURS, pickNumber(cfg.gcHours, 24, 1), 1)
|
|
46
64
|
const maxFileBytes = pickNumber(cfg.maxFileBytes, 104857600, 1024)
|
|
65
|
+
// 每工作区快照上限:0 或负值语义 = 不限制(给想全保留的用户出口);
|
|
66
|
+
// 非数值回退默认 500。默认 500 ≈ 重度使用一周量级,太小会静默丢历史
|
|
67
|
+
// 撤回点,太大失去防膨胀意义。
|
|
68
|
+
const rawMax = typeof cfg.maxSnapshotsPerWorkspace === 'number'
|
|
69
|
+
? cfg.maxSnapshotsPerWorkspace
|
|
70
|
+
: parseInt(String(cfg.maxSnapshotsPerWorkspace == null ? '' : cfg.maxSnapshotsPerWorkspace), 10)
|
|
71
|
+
const maxSnapshotsPerWorkspace = Number.isFinite(rawMax) ? Math.max(0, rawMax) : 500
|
|
47
72
|
|
|
48
73
|
const baseExcludes = Array.isArray(cfg.baseExcludes) && cfg.baseExcludes.length
|
|
49
74
|
? cfg.baseExcludes.filter((p) => typeof p === 'string' && p.trim())
|
|
@@ -51,5 +76,21 @@ export function createConfig(raw) {
|
|
|
51
76
|
|
|
52
77
|
const refillDraft = typeof cfg.refillDraft === 'boolean' ? cfg.refillDraft : true
|
|
53
78
|
|
|
54
|
-
|
|
79
|
+
// 快照总开关:false 冻结「新建」(session/event 短路,见 index.js),
|
|
80
|
+
// 已有快照的撤回链路不受影响——关闭只停增量,不销毁存量。
|
|
81
|
+
const snapshotEnabled = typeof cfg.snapshotEnabled === 'boolean' ? cfg.snapshotEnabled : true
|
|
82
|
+
|
|
83
|
+
// 撤回后是否归档原会话:关闭时原会话保留在侧栏(fork 新会话仍打开),
|
|
84
|
+
// 供用户对照回退前后上下文;默认开(归档只是隐藏、可恢复)。
|
|
85
|
+
const archiveOriginal = typeof cfg.archiveOriginal === 'boolean' ? cfg.archiveOriginal : true
|
|
86
|
+
|
|
87
|
+
// 按时间保留(S2-3):0 或负值 = 不启用(静默删历史撤回点必须显式
|
|
88
|
+
// opt-in);非数值回退 0。与 maxSnapshotsPerWorkspace(条数维度)并存,
|
|
89
|
+
// 各自独立触发——见 maintenance.enforceRetention。
|
|
90
|
+
const rawDays = typeof cfg.retentionDays === 'number'
|
|
91
|
+
? cfg.retentionDays
|
|
92
|
+
: parseInt(String(cfg.retentionDays == null ? '' : cfg.retentionDays), 10)
|
|
93
|
+
const retentionDays = Number.isFinite(rawDays) ? Math.max(0, rawDays) : 0
|
|
94
|
+
|
|
95
|
+
return { gcSnaps, gcHours, maxFileBytes, maxSnapshotsPerWorkspace, baseExcludes, refillDraft, snapshotEnabled, archiveOriginal, retentionDays }
|
|
55
96
|
}
|