dsh-tdai-memory 0.3.1 → 0.3.2
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/client.js +4 -3
- package/index.js +436 -436
- package/package.json +1 -1
- package/vendor/tdai/adapters/standalone/llm-runner.js +274 -274
- package/vendor/tdai/config.js +366 -366
- package/vendor/tdai/core/hooks/auto-recall.js +13 -13
- package/vendor/tdai/core/prompts/l1-dedup.js +64 -64
- package/vendor/tdai/core/prompts/l1-extraction.js +98 -98
- package/vendor/tdai/core/prompts/persona-generation.js +125 -125
- package/vendor/tdai/core/prompts/scene-extraction.js +224 -224
- package/vendor/tdai/core/scene/scene-navigation.js +3 -3
- package/vendor/tdai/core/seed/seed-runtime.js +318 -318
- package/vendor/tdai/core/store/sqlite.js +190 -190
- package/vendor/tdai/core/tdai-core.js +450 -450
- package/vendor/tdai/utils/session-header-fetch.js +88 -88
package/vendor/tdai/config.js
CHANGED
|
@@ -1,366 +1,366 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin configuration types and parser (v3).
|
|
3
|
-
*
|
|
4
|
-
* Config is organized into flat functional groups:
|
|
5
|
-
* capture, extraction, persona, pipeline, recall, embedding
|
|
6
|
-
*
|
|
7
|
-
* Minimal config (zero config): {} — all fields have sensible defaults.
|
|
8
|
-
*/
|
|
9
|
-
import { normalizeDisableThinking } from "./utils/no-think-fetch.js";
|
|
10
|
-
// ============================
|
|
11
|
-
// Parser
|
|
12
|
-
// ============================
|
|
13
|
-
/**
|
|
14
|
-
* Parse plugin config from raw user input.
|
|
15
|
-
* All fields have sensible defaults — minimal config is just {}.
|
|
16
|
-
*/
|
|
17
|
-
export function parseConfig(raw) {
|
|
18
|
-
const c = raw ?? {};
|
|
19
|
-
// --- Capture (L0) ---
|
|
20
|
-
const captureGroup = obj(c, "capture");
|
|
21
|
-
// --- Retention days validation (from capture.l0l1RetentionDays) ---
|
|
22
|
-
const rawRetentionDays = num(captureGroup, "l0l1RetentionDays") ?? 0;
|
|
23
|
-
const allowAggressiveCleanup = bool(captureGroup, "allowAggressiveCleanup") ?? false;
|
|
24
|
-
let retentionDays;
|
|
25
|
-
if (rawRetentionDays <= 0) {
|
|
26
|
-
retentionDays = undefined;
|
|
27
|
-
}
|
|
28
|
-
else if (rawRetentionDays >= 3) {
|
|
29
|
-
retentionDays = rawRetentionDays;
|
|
30
|
-
}
|
|
31
|
-
else if (allowAggressiveCleanup) {
|
|
32
|
-
retentionDays = rawRetentionDays;
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
retentionDays = undefined;
|
|
36
|
-
}
|
|
37
|
-
// --- Extraction (L1) ---
|
|
38
|
-
const extractionGroup = obj(c, "extraction");
|
|
39
|
-
// --- Persona (L2/L3) ---
|
|
40
|
-
const personaGroup = obj(c, "persona");
|
|
41
|
-
// --- Pipeline ---
|
|
42
|
-
const pipelineGroup = obj(c, "pipeline");
|
|
43
|
-
// --- Recall ---
|
|
44
|
-
const recallGroup = obj(c, "recall");
|
|
45
|
-
// --- Embedding ---
|
|
46
|
-
const embeddingGroup = obj(c, "embedding");
|
|
47
|
-
let embeddingConfigError;
|
|
48
|
-
// Embedding config: determine provider based on user input and apiKey availability
|
|
49
|
-
const embeddingApiKey = str(embeddingGroup, "apiKey") ?? "";
|
|
50
|
-
const embeddingBaseUrl = str(embeddingGroup, "baseUrl") ?? "";
|
|
51
|
-
const embeddingProviderRaw = str(embeddingGroup, "provider") ?? "none";
|
|
52
|
-
const embeddingModelRaw = str(embeddingGroup, "model") ?? "";
|
|
53
|
-
const embeddingDimensionsRaw = num(embeddingGroup, "dimensions");
|
|
54
|
-
const embeddingProxyUrl = str(embeddingGroup, "proxyUrl");
|
|
55
|
-
// provider="none" → embedding disabled (default for zero-config users)
|
|
56
|
-
// provider="local" → no longer exposed to users; treated as disabled at entry level
|
|
57
|
-
// provider="qclaw" → requires proxyUrl for local proxy forwarding
|
|
58
|
-
// Any other value → remote mode (requires apiKey, baseUrl, model, dimensions)
|
|
59
|
-
let embeddingProvider;
|
|
60
|
-
let embeddingEnabled = bool(embeddingGroup, "enabled") ?? true;
|
|
61
|
-
if (embeddingProviderRaw === "none") {
|
|
62
|
-
// Explicitly disabled (default): no embedding, no vector search
|
|
63
|
-
embeddingProvider = "none";
|
|
64
|
-
embeddingEnabled = false;
|
|
65
|
-
}
|
|
66
|
-
else if (embeddingProviderRaw === "local") {
|
|
67
|
-
// Local embedding is not exposed to users; treat as disabled at entry level.
|
|
68
|
-
// Internal LocalEmbeddingService code is preserved but not reachable from config.
|
|
69
|
-
embeddingProvider = "none";
|
|
70
|
-
embeddingEnabled = false;
|
|
71
|
-
embeddingConfigError =
|
|
72
|
-
"Local embedding provider is not available in user config. " +
|
|
73
|
-
"Please configure a remote embedding provider (e.g. openai, deepseek). Embedding has been disabled.";
|
|
74
|
-
}
|
|
75
|
-
else if (embeddingProviderRaw === "qclaw") {
|
|
76
|
-
// qclaw provider: requires proxyUrl for local proxy forwarding
|
|
77
|
-
const missingFields = [];
|
|
78
|
-
if (!embeddingProxyUrl)
|
|
79
|
-
missingFields.push("proxyUrl");
|
|
80
|
-
if (!embeddingBaseUrl)
|
|
81
|
-
missingFields.push("baseUrl");
|
|
82
|
-
if (!embeddingApiKey)
|
|
83
|
-
missingFields.push("apiKey");
|
|
84
|
-
if (!embeddingModelRaw)
|
|
85
|
-
missingFields.push("model");
|
|
86
|
-
if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
|
|
87
|
-
missingFields.push("dimensions");
|
|
88
|
-
if (missingFields.length > 0) {
|
|
89
|
-
const errorMsg = `Embedding provider 'qclaw' requires 'proxyUrl', 'baseUrl', 'apiKey', 'model', and 'dimensions' to be set. ` +
|
|
90
|
-
`Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
|
|
91
|
-
embeddingConfigError = errorMsg;
|
|
92
|
-
embeddingEnabled = false;
|
|
93
|
-
embeddingProvider = embeddingProviderRaw;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
embeddingProvider = embeddingProviderRaw;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
// Remote mode — validate all required fields
|
|
101
|
-
const missingFields = [];
|
|
102
|
-
if (!embeddingApiKey)
|
|
103
|
-
missingFields.push("apiKey");
|
|
104
|
-
if (!embeddingBaseUrl)
|
|
105
|
-
missingFields.push("baseUrl");
|
|
106
|
-
if (!embeddingModelRaw)
|
|
107
|
-
missingFields.push("model");
|
|
108
|
-
if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
|
|
109
|
-
missingFields.push("dimensions");
|
|
110
|
-
if (missingFields.length > 0) {
|
|
111
|
-
// Configuration error: disable embedding and log detailed error
|
|
112
|
-
// This does NOT throw — the plugin continues running without vector search
|
|
113
|
-
const errorMsg = `Remote embedding provider '${embeddingProviderRaw}' requires 'apiKey', 'baseUrl', 'model', and 'dimensions' to be set. ` +
|
|
114
|
-
`Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
|
|
115
|
-
// We store the error message so the caller (index.ts) can log it
|
|
116
|
-
embeddingConfigError = errorMsg;
|
|
117
|
-
embeddingEnabled = false;
|
|
118
|
-
embeddingProvider = embeddingProviderRaw; // preserve original for error context
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
embeddingProvider = embeddingProviderRaw;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
// When provider="none", dimensions=0 signals VectorStore to skip vec0 table
|
|
125
|
-
// creation entirely (deferred until a real embedding provider is configured).
|
|
126
|
-
// This avoids creating vec0 tables with a placeholder dimension that would
|
|
127
|
-
// mismatch if the user later enables a different-dimensional provider.
|
|
128
|
-
const defaultDimensions = embeddingProvider === "none" ? 0 :
|
|
129
|
-
embeddingDimensionsRaw ?? 0;
|
|
130
|
-
const defaultModel = embeddingProvider === "none" ? "" : embeddingModelRaw;
|
|
131
|
-
const cleanTime = normalizeCleanTime(str(captureGroup, "cleanTime")) ?? "03:00";
|
|
132
|
-
// --- BM25 (local @tencentdb-agent-memory/tcvdb-text encoder) ---
|
|
133
|
-
const bm25Group = obj(c, "bm25");
|
|
134
|
-
// --- Store backend ---
|
|
135
|
-
const storeBackendRaw = str(c, "storeBackend") ?? "sqlite";
|
|
136
|
-
const storeBackend = storeBackendRaw === "tcvdb" ? "tcvdb" : "sqlite";
|
|
137
|
-
// --- TCVDB config ---
|
|
138
|
-
const tcvdbGroup = obj(c, "tcvdb");
|
|
139
|
-
const memoryCleanup = {
|
|
140
|
-
retentionDays,
|
|
141
|
-
enabled: retentionDays != null,
|
|
142
|
-
cleanTime,
|
|
143
|
-
};
|
|
144
|
-
// --- Offload ---
|
|
145
|
-
const offloadGroup = obj(c, "offload");
|
|
146
|
-
const offloadMode = (() => {
|
|
147
|
-
const raw = optStr(offloadGroup, "mode");
|
|
148
|
-
if (raw === "local" || raw === "backend" || raw === "collect")
|
|
149
|
-
return raw;
|
|
150
|
-
return optStr(offloadGroup, "backendUrl") ? "backend" : "local";
|
|
151
|
-
})();
|
|
152
|
-
const offload = {
|
|
153
|
-
enabled: bool(offloadGroup, "enabled") ?? false,
|
|
154
|
-
mode: offloadMode,
|
|
155
|
-
model: optStr(offloadGroup, "model"),
|
|
156
|
-
temperature: num(offloadGroup, "temperature") ?? 0.2,
|
|
157
|
-
disableThinking: normalizeDisableThinking(boolOrStr(offloadGroup, "disableThinking")),
|
|
158
|
-
forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
|
|
159
|
-
dataDir: optStr(offloadGroup, "dataDir"),
|
|
160
|
-
defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
|
|
161
|
-
maxPairsPerBatch: num(offloadGroup, "maxPairsPerBatch") ?? 20,
|
|
162
|
-
l2NullThreshold: num(offloadGroup, "l2NullThreshold") ?? 4,
|
|
163
|
-
l2TimeoutSeconds: num(offloadGroup, "l2TimeoutSeconds") ?? 300,
|
|
164
|
-
mildOffloadRatio: num(offloadGroup, "mildOffloadRatio") ?? 0.5,
|
|
165
|
-
aggressiveCompressRatio: num(offloadGroup, "aggressiveCompressRatio") ?? 0.85,
|
|
166
|
-
mmdMaxTokenRatio: num(offloadGroup, "mmdMaxTokenRatio") ?? 0.2,
|
|
167
|
-
backendUrl: optStr(offloadGroup, "backendUrl"),
|
|
168
|
-
backendApiKey: optStr(offloadGroup, "backendApiKey"),
|
|
169
|
-
backendTimeoutMs: num(offloadGroup, "backendTimeoutMs") ?? 120000,
|
|
170
|
-
offloadRetentionDays: normalizeOffloadRetentionDays(num(offloadGroup, "offloadRetentionDays") ?? 0),
|
|
171
|
-
logMaxSizeMb: num(offloadGroup, "logMaxSizeMb") ?? 50,
|
|
172
|
-
userId: optStr(offloadGroup, "userId"),
|
|
173
|
-
};
|
|
174
|
-
return {
|
|
175
|
-
timezone: str(c, "timezone") ?? "system",
|
|
176
|
-
capture: {
|
|
177
|
-
enabled: bool(captureGroup, "enabled") ?? true,
|
|
178
|
-
excludeAgents: strArray(captureGroup, "excludeAgents") ?? [],
|
|
179
|
-
l0l1RetentionDays: retentionDays ?? 0,
|
|
180
|
-
allowAggressiveCleanup,
|
|
181
|
-
},
|
|
182
|
-
extraction: {
|
|
183
|
-
enabled: bool(extractionGroup, "enabled") ?? true,
|
|
184
|
-
enableDedup: bool(extractionGroup, "enableDedup") ?? true,
|
|
185
|
-
maxMemoriesPerSession: num(extractionGroup, "maxMemoriesPerSession") ?? 20,
|
|
186
|
-
model: optStr(extractionGroup, "model"),
|
|
187
|
-
},
|
|
188
|
-
persona: {
|
|
189
|
-
triggerEveryN: num(personaGroup, "triggerEveryN") ?? 50,
|
|
190
|
-
maxScenes: num(personaGroup, "maxScenes") ?? 15,
|
|
191
|
-
backupCount: num(personaGroup, "backupCount") ?? 3,
|
|
192
|
-
sceneBackupCount: num(personaGroup, "sceneBackupCount") ?? 10,
|
|
193
|
-
model: optStr(personaGroup, "model"),
|
|
194
|
-
},
|
|
195
|
-
pipeline: {
|
|
196
|
-
everyNConversations: num(pipelineGroup, "everyNConversations") ?? 5,
|
|
197
|
-
enableWarmup: bool(pipelineGroup, "enableWarmup") ?? true,
|
|
198
|
-
l1IdleTimeoutSeconds: num(pipelineGroup, "l1IdleTimeoutSeconds") ?? 600,
|
|
199
|
-
l2DelayAfterL1Seconds: num(pipelineGroup, "l2DelayAfterL1Seconds") ?? 10,
|
|
200
|
-
l2MinIntervalSeconds: num(pipelineGroup, "l2MinIntervalSeconds") ?? 900,
|
|
201
|
-
l2MaxIntervalSeconds: num(pipelineGroup, "l2MaxIntervalSeconds") ?? 3600,
|
|
202
|
-
sessionActiveWindowHours: num(pipelineGroup, "sessionActiveWindowHours") ?? 24,
|
|
203
|
-
},
|
|
204
|
-
recall: {
|
|
205
|
-
enabled: bool(recallGroup, "enabled") ?? true,
|
|
206
|
-
maxResults: num(recallGroup, "maxResults") ?? 5,
|
|
207
|
-
maxCharsPerMemory: num(recallGroup, "maxCharsPerMemory") ?? 0,
|
|
208
|
-
maxTotalRecallChars: num(recallGroup, "maxTotalRecallChars") ?? 0,
|
|
209
|
-
scoreThreshold: num(recallGroup, "scoreThreshold") ?? 0.3,
|
|
210
|
-
strategy: validateStrategy(str(recallGroup, "strategy")) ?? "hybrid",
|
|
211
|
-
timeoutMs: num(recallGroup, "timeoutMs") ?? 5000,
|
|
212
|
-
},
|
|
213
|
-
embedding: {
|
|
214
|
-
enabled: embeddingEnabled,
|
|
215
|
-
provider: embeddingProvider,
|
|
216
|
-
baseUrl: embeddingBaseUrl,
|
|
217
|
-
apiKey: embeddingApiKey,
|
|
218
|
-
model: str(embeddingGroup, "model") ?? defaultModel,
|
|
219
|
-
dimensions: num(embeddingGroup, "dimensions") ?? defaultDimensions,
|
|
220
|
-
sendDimensions: bool(embeddingGroup, "sendDimensions") ?? true,
|
|
221
|
-
conflictRecallTopK: num(embeddingGroup, "conflictRecallTopK") ?? 5,
|
|
222
|
-
proxyUrl: embeddingProxyUrl,
|
|
223
|
-
maxInputChars: num(embeddingGroup, "maxInputChars") ?? 5000,
|
|
224
|
-
timeoutMs: num(embeddingGroup, "timeoutMs") ?? 10_000,
|
|
225
|
-
recallTimeoutMs: num(embeddingGroup, "recallTimeoutMs") ?? undefined,
|
|
226
|
-
captureTimeoutMs: num(embeddingGroup, "captureTimeoutMs") ?? undefined,
|
|
227
|
-
modelCacheDir: optStr(embeddingGroup, "modelCacheDir"),
|
|
228
|
-
configError: embeddingConfigError,
|
|
229
|
-
},
|
|
230
|
-
storeBackend,
|
|
231
|
-
tcvdb: {
|
|
232
|
-
url: str(tcvdbGroup, "url") ?? "",
|
|
233
|
-
username: str(tcvdbGroup, "username") ?? "root",
|
|
234
|
-
apiKey: str(tcvdbGroup, "apiKey") ?? "",
|
|
235
|
-
database: str(tcvdbGroup, "database") ?? "",
|
|
236
|
-
alias: str(tcvdbGroup, "alias") ?? "",
|
|
237
|
-
embeddingModel: str(tcvdbGroup, "embeddingModel") ?? "bge-large-zh",
|
|
238
|
-
timeout: num(tcvdbGroup, "timeout") ?? 10000,
|
|
239
|
-
caPemPath: str(tcvdbGroup, "caPemPath") || undefined,
|
|
240
|
-
},
|
|
241
|
-
bm25: {
|
|
242
|
-
enabled: bool(bm25Group, "enabled") ?? true,
|
|
243
|
-
language: (str(bm25Group, "language") === "en" ? "en" : "zh"),
|
|
244
|
-
},
|
|
245
|
-
memoryCleanup,
|
|
246
|
-
report: {
|
|
247
|
-
enabled: bool(obj(c, "report"), "enabled") ?? false,
|
|
248
|
-
type: str(obj(c, "report"), "type") ?? "local",
|
|
249
|
-
},
|
|
250
|
-
llm: (() => {
|
|
251
|
-
const llmGroup = obj(c, "llm");
|
|
252
|
-
return {
|
|
253
|
-
enabled: bool(llmGroup, "enabled") ?? false,
|
|
254
|
-
baseUrl: str(llmGroup, "baseUrl") ?? "https://api.openai.com/v1",
|
|
255
|
-
apiKey: str(llmGroup, "apiKey") ?? "",
|
|
256
|
-
model: str(llmGroup, "model") ?? "gpt-4o",
|
|
257
|
-
maxTokens: num(llmGroup, "maxTokens") ?? 4096,
|
|
258
|
-
timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
|
|
259
|
-
disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
|
|
260
|
-
sendSessionHeader: bool(llmGroup, "sendSessionHeader") ?? true,
|
|
261
|
-
sessionHeaderName: str(llmGroup, "sessionHeaderName") ?? "x-opencode-session",
|
|
262
|
-
sessionId: str(llmGroup, "sessionId") ?? "",
|
|
263
|
-
};
|
|
264
|
-
})(),
|
|
265
|
-
offload,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
// ============================
|
|
269
|
-
// Helper functions
|
|
270
|
-
// ============================
|
|
271
|
-
/** Get sub-object by key, or empty object if missing. */
|
|
272
|
-
function obj(c, key) {
|
|
273
|
-
const v = c[key];
|
|
274
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
275
|
-
}
|
|
276
|
-
function str(src, key) {
|
|
277
|
-
const v = src[key];
|
|
278
|
-
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
279
|
-
}
|
|
280
|
-
function optStr(src, key) {
|
|
281
|
-
const v = src[key];
|
|
282
|
-
return typeof v === "string" ? v : undefined;
|
|
283
|
-
}
|
|
284
|
-
function num(src, key) {
|
|
285
|
-
const v = src[key];
|
|
286
|
-
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
287
|
-
}
|
|
288
|
-
function bool(src, key) {
|
|
289
|
-
const v = src[key];
|
|
290
|
-
return typeof v === "boolean" ? v : undefined;
|
|
291
|
-
}
|
|
292
|
-
/** Read a field that may be boolean or string. */
|
|
293
|
-
function boolOrStr(src, key) {
|
|
294
|
-
const v = src[key];
|
|
295
|
-
if (typeof v === "boolean")
|
|
296
|
-
return v;
|
|
297
|
-
if (typeof v === "string" && v.trim())
|
|
298
|
-
return v.trim();
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
function strArray(src, key) {
|
|
302
|
-
const v = src[key];
|
|
303
|
-
if (!Array.isArray(v))
|
|
304
|
-
return undefined;
|
|
305
|
-
return v.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
306
|
-
}
|
|
307
|
-
const VALID_STRATEGIES = ["embedding", "keyword", "hybrid"];
|
|
308
|
-
/**
|
|
309
|
-
* Validate recall strategy against whitelist.
|
|
310
|
-
* Returns the strategy if valid, undefined otherwise (caller falls back to default).
|
|
311
|
-
*/
|
|
312
|
-
function validateStrategy(value) {
|
|
313
|
-
if (!value)
|
|
314
|
-
return undefined;
|
|
315
|
-
return VALID_STRATEGIES.includes(value)
|
|
316
|
-
? value
|
|
317
|
-
: undefined;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Normalize a cleanup time string.
|
|
321
|
-
*
|
|
322
|
-
* The input must follow "HH:MM" or "H:MM" format (24-hour clock).
|
|
323
|
-
* If the time is valid, it returns the normalized format "HH:MM"
|
|
324
|
-
* with leading zeros added when necessary.
|
|
325
|
-
* If the format is invalid or the time is out of range
|
|
326
|
-
* (hour: 0–23, minute: 0–59), it returns undefined.
|
|
327
|
-
*
|
|
328
|
-
* Examples:
|
|
329
|
-
* normalizeCleanTime("3:05") -> "03:05"
|
|
330
|
-
* normalizeCleanTime("03:05") -> "03:05"
|
|
331
|
-
* normalizeCleanTime("23:59") -> "23:59"
|
|
332
|
-
*
|
|
333
|
-
* normalizeCleanTime("24:00") -> undefined // hour out of range
|
|
334
|
-
* normalizeCleanTime("12:60") -> undefined // minute out of range
|
|
335
|
-
* normalizeCleanTime("3:5") -> undefined // minute must have two digits
|
|
336
|
-
* normalizeCleanTime("abc") -> undefined // invalid format
|
|
337
|
-
*/
|
|
338
|
-
function normalizeCleanTime(input) {
|
|
339
|
-
if (!input)
|
|
340
|
-
return undefined;
|
|
341
|
-
const trimmed = input.trim();
|
|
342
|
-
const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
|
|
343
|
-
if (!m)
|
|
344
|
-
return undefined;
|
|
345
|
-
const hh = Number(m[1]);
|
|
346
|
-
const mm = Number(m[2]);
|
|
347
|
-
if (!Number.isInteger(hh) || !Number.isInteger(mm))
|
|
348
|
-
return undefined;
|
|
349
|
-
if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
|
|
350
|
-
return undefined;
|
|
351
|
-
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Normalize offload retention days.
|
|
355
|
-
*
|
|
356
|
-
* - `<= 0` → 0 (disabled)
|
|
357
|
-
* - `(0, 3)` → 0 (invalid, force disabled)
|
|
358
|
-
* - `>= 3` → as-is
|
|
359
|
-
*/
|
|
360
|
-
function normalizeOffloadRetentionDays(value) {
|
|
361
|
-
if (value <= 0)
|
|
362
|
-
return 0;
|
|
363
|
-
if (value < 3)
|
|
364
|
-
return 0;
|
|
365
|
-
return value;
|
|
366
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration types and parser (v3).
|
|
3
|
+
*
|
|
4
|
+
* Config is organized into flat functional groups:
|
|
5
|
+
* capture, extraction, persona, pipeline, recall, embedding
|
|
6
|
+
*
|
|
7
|
+
* Minimal config (zero config): {} — all fields have sensible defaults.
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeDisableThinking } from "./utils/no-think-fetch.js";
|
|
10
|
+
// ============================
|
|
11
|
+
// Parser
|
|
12
|
+
// ============================
|
|
13
|
+
/**
|
|
14
|
+
* Parse plugin config from raw user input.
|
|
15
|
+
* All fields have sensible defaults — minimal config is just {}.
|
|
16
|
+
*/
|
|
17
|
+
export function parseConfig(raw) {
|
|
18
|
+
const c = raw ?? {};
|
|
19
|
+
// --- Capture (L0) ---
|
|
20
|
+
const captureGroup = obj(c, "capture");
|
|
21
|
+
// --- Retention days validation (from capture.l0l1RetentionDays) ---
|
|
22
|
+
const rawRetentionDays = num(captureGroup, "l0l1RetentionDays") ?? 0;
|
|
23
|
+
const allowAggressiveCleanup = bool(captureGroup, "allowAggressiveCleanup") ?? false;
|
|
24
|
+
let retentionDays;
|
|
25
|
+
if (rawRetentionDays <= 0) {
|
|
26
|
+
retentionDays = undefined;
|
|
27
|
+
}
|
|
28
|
+
else if (rawRetentionDays >= 3) {
|
|
29
|
+
retentionDays = rawRetentionDays;
|
|
30
|
+
}
|
|
31
|
+
else if (allowAggressiveCleanup) {
|
|
32
|
+
retentionDays = rawRetentionDays;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
retentionDays = undefined;
|
|
36
|
+
}
|
|
37
|
+
// --- Extraction (L1) ---
|
|
38
|
+
const extractionGroup = obj(c, "extraction");
|
|
39
|
+
// --- Persona (L2/L3) ---
|
|
40
|
+
const personaGroup = obj(c, "persona");
|
|
41
|
+
// --- Pipeline ---
|
|
42
|
+
const pipelineGroup = obj(c, "pipeline");
|
|
43
|
+
// --- Recall ---
|
|
44
|
+
const recallGroup = obj(c, "recall");
|
|
45
|
+
// --- Embedding ---
|
|
46
|
+
const embeddingGroup = obj(c, "embedding");
|
|
47
|
+
let embeddingConfigError;
|
|
48
|
+
// Embedding config: determine provider based on user input and apiKey availability
|
|
49
|
+
const embeddingApiKey = str(embeddingGroup, "apiKey") ?? "";
|
|
50
|
+
const embeddingBaseUrl = str(embeddingGroup, "baseUrl") ?? "";
|
|
51
|
+
const embeddingProviderRaw = str(embeddingGroup, "provider") ?? "none";
|
|
52
|
+
const embeddingModelRaw = str(embeddingGroup, "model") ?? "";
|
|
53
|
+
const embeddingDimensionsRaw = num(embeddingGroup, "dimensions");
|
|
54
|
+
const embeddingProxyUrl = str(embeddingGroup, "proxyUrl");
|
|
55
|
+
// provider="none" → embedding disabled (default for zero-config users)
|
|
56
|
+
// provider="local" → no longer exposed to users; treated as disabled at entry level
|
|
57
|
+
// provider="qclaw" → requires proxyUrl for local proxy forwarding
|
|
58
|
+
// Any other value → remote mode (requires apiKey, baseUrl, model, dimensions)
|
|
59
|
+
let embeddingProvider;
|
|
60
|
+
let embeddingEnabled = bool(embeddingGroup, "enabled") ?? true;
|
|
61
|
+
if (embeddingProviderRaw === "none") {
|
|
62
|
+
// Explicitly disabled (default): no embedding, no vector search
|
|
63
|
+
embeddingProvider = "none";
|
|
64
|
+
embeddingEnabled = false;
|
|
65
|
+
}
|
|
66
|
+
else if (embeddingProviderRaw === "local") {
|
|
67
|
+
// Local embedding is not exposed to users; treat as disabled at entry level.
|
|
68
|
+
// Internal LocalEmbeddingService code is preserved but not reachable from config.
|
|
69
|
+
embeddingProvider = "none";
|
|
70
|
+
embeddingEnabled = false;
|
|
71
|
+
embeddingConfigError =
|
|
72
|
+
"Local embedding provider is not available in user config. " +
|
|
73
|
+
"Please configure a remote embedding provider (e.g. openai, deepseek). Embedding has been disabled.";
|
|
74
|
+
}
|
|
75
|
+
else if (embeddingProviderRaw === "qclaw") {
|
|
76
|
+
// qclaw provider: requires proxyUrl for local proxy forwarding
|
|
77
|
+
const missingFields = [];
|
|
78
|
+
if (!embeddingProxyUrl)
|
|
79
|
+
missingFields.push("proxyUrl");
|
|
80
|
+
if (!embeddingBaseUrl)
|
|
81
|
+
missingFields.push("baseUrl");
|
|
82
|
+
if (!embeddingApiKey)
|
|
83
|
+
missingFields.push("apiKey");
|
|
84
|
+
if (!embeddingModelRaw)
|
|
85
|
+
missingFields.push("model");
|
|
86
|
+
if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
|
|
87
|
+
missingFields.push("dimensions");
|
|
88
|
+
if (missingFields.length > 0) {
|
|
89
|
+
const errorMsg = `Embedding provider 'qclaw' requires 'proxyUrl', 'baseUrl', 'apiKey', 'model', and 'dimensions' to be set. ` +
|
|
90
|
+
`Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
|
|
91
|
+
embeddingConfigError = errorMsg;
|
|
92
|
+
embeddingEnabled = false;
|
|
93
|
+
embeddingProvider = embeddingProviderRaw;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
embeddingProvider = embeddingProviderRaw;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// Remote mode — validate all required fields
|
|
101
|
+
const missingFields = [];
|
|
102
|
+
if (!embeddingApiKey)
|
|
103
|
+
missingFields.push("apiKey");
|
|
104
|
+
if (!embeddingBaseUrl)
|
|
105
|
+
missingFields.push("baseUrl");
|
|
106
|
+
if (!embeddingModelRaw)
|
|
107
|
+
missingFields.push("model");
|
|
108
|
+
if (embeddingDimensionsRaw == null || embeddingDimensionsRaw <= 0)
|
|
109
|
+
missingFields.push("dimensions");
|
|
110
|
+
if (missingFields.length > 0) {
|
|
111
|
+
// Configuration error: disable embedding and log detailed error
|
|
112
|
+
// This does NOT throw — the plugin continues running without vector search
|
|
113
|
+
const errorMsg = `Remote embedding provider '${embeddingProviderRaw}' requires 'apiKey', 'baseUrl', 'model', and 'dimensions' to be set. ` +
|
|
114
|
+
`Missing: ${missingFields.join(", ")}. Embedding has been disabled.`;
|
|
115
|
+
// We store the error message so the caller (index.ts) can log it
|
|
116
|
+
embeddingConfigError = errorMsg;
|
|
117
|
+
embeddingEnabled = false;
|
|
118
|
+
embeddingProvider = embeddingProviderRaw; // preserve original for error context
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
embeddingProvider = embeddingProviderRaw;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// When provider="none", dimensions=0 signals VectorStore to skip vec0 table
|
|
125
|
+
// creation entirely (deferred until a real embedding provider is configured).
|
|
126
|
+
// This avoids creating vec0 tables with a placeholder dimension that would
|
|
127
|
+
// mismatch if the user later enables a different-dimensional provider.
|
|
128
|
+
const defaultDimensions = embeddingProvider === "none" ? 0 :
|
|
129
|
+
embeddingDimensionsRaw ?? 0;
|
|
130
|
+
const defaultModel = embeddingProvider === "none" ? "" : embeddingModelRaw;
|
|
131
|
+
const cleanTime = normalizeCleanTime(str(captureGroup, "cleanTime")) ?? "03:00";
|
|
132
|
+
// --- BM25 (local @tencentdb-agent-memory/tcvdb-text encoder) ---
|
|
133
|
+
const bm25Group = obj(c, "bm25");
|
|
134
|
+
// --- Store backend ---
|
|
135
|
+
const storeBackendRaw = str(c, "storeBackend") ?? "sqlite";
|
|
136
|
+
const storeBackend = storeBackendRaw === "tcvdb" ? "tcvdb" : "sqlite";
|
|
137
|
+
// --- TCVDB config ---
|
|
138
|
+
const tcvdbGroup = obj(c, "tcvdb");
|
|
139
|
+
const memoryCleanup = {
|
|
140
|
+
retentionDays,
|
|
141
|
+
enabled: retentionDays != null,
|
|
142
|
+
cleanTime,
|
|
143
|
+
};
|
|
144
|
+
// --- Offload ---
|
|
145
|
+
const offloadGroup = obj(c, "offload");
|
|
146
|
+
const offloadMode = (() => {
|
|
147
|
+
const raw = optStr(offloadGroup, "mode");
|
|
148
|
+
if (raw === "local" || raw === "backend" || raw === "collect")
|
|
149
|
+
return raw;
|
|
150
|
+
return optStr(offloadGroup, "backendUrl") ? "backend" : "local";
|
|
151
|
+
})();
|
|
152
|
+
const offload = {
|
|
153
|
+
enabled: bool(offloadGroup, "enabled") ?? false,
|
|
154
|
+
mode: offloadMode,
|
|
155
|
+
model: optStr(offloadGroup, "model"),
|
|
156
|
+
temperature: num(offloadGroup, "temperature") ?? 0.2,
|
|
157
|
+
disableThinking: normalizeDisableThinking(boolOrStr(offloadGroup, "disableThinking")),
|
|
158
|
+
forceTriggerThreshold: num(offloadGroup, "forceTriggerThreshold") ?? 4,
|
|
159
|
+
dataDir: optStr(offloadGroup, "dataDir"),
|
|
160
|
+
defaultContextWindow: num(offloadGroup, "defaultContextWindow") ?? 200000,
|
|
161
|
+
maxPairsPerBatch: num(offloadGroup, "maxPairsPerBatch") ?? 20,
|
|
162
|
+
l2NullThreshold: num(offloadGroup, "l2NullThreshold") ?? 4,
|
|
163
|
+
l2TimeoutSeconds: num(offloadGroup, "l2TimeoutSeconds") ?? 300,
|
|
164
|
+
mildOffloadRatio: num(offloadGroup, "mildOffloadRatio") ?? 0.5,
|
|
165
|
+
aggressiveCompressRatio: num(offloadGroup, "aggressiveCompressRatio") ?? 0.85,
|
|
166
|
+
mmdMaxTokenRatio: num(offloadGroup, "mmdMaxTokenRatio") ?? 0.2,
|
|
167
|
+
backendUrl: optStr(offloadGroup, "backendUrl"),
|
|
168
|
+
backendApiKey: optStr(offloadGroup, "backendApiKey"),
|
|
169
|
+
backendTimeoutMs: num(offloadGroup, "backendTimeoutMs") ?? 120000,
|
|
170
|
+
offloadRetentionDays: normalizeOffloadRetentionDays(num(offloadGroup, "offloadRetentionDays") ?? 0),
|
|
171
|
+
logMaxSizeMb: num(offloadGroup, "logMaxSizeMb") ?? 50,
|
|
172
|
+
userId: optStr(offloadGroup, "userId"),
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
timezone: str(c, "timezone") ?? "system",
|
|
176
|
+
capture: {
|
|
177
|
+
enabled: bool(captureGroup, "enabled") ?? true,
|
|
178
|
+
excludeAgents: strArray(captureGroup, "excludeAgents") ?? [],
|
|
179
|
+
l0l1RetentionDays: retentionDays ?? 0,
|
|
180
|
+
allowAggressiveCleanup,
|
|
181
|
+
},
|
|
182
|
+
extraction: {
|
|
183
|
+
enabled: bool(extractionGroup, "enabled") ?? true,
|
|
184
|
+
enableDedup: bool(extractionGroup, "enableDedup") ?? true,
|
|
185
|
+
maxMemoriesPerSession: num(extractionGroup, "maxMemoriesPerSession") ?? 20,
|
|
186
|
+
model: optStr(extractionGroup, "model"),
|
|
187
|
+
},
|
|
188
|
+
persona: {
|
|
189
|
+
triggerEveryN: num(personaGroup, "triggerEveryN") ?? 50,
|
|
190
|
+
maxScenes: num(personaGroup, "maxScenes") ?? 15,
|
|
191
|
+
backupCount: num(personaGroup, "backupCount") ?? 3,
|
|
192
|
+
sceneBackupCount: num(personaGroup, "sceneBackupCount") ?? 10,
|
|
193
|
+
model: optStr(personaGroup, "model"),
|
|
194
|
+
},
|
|
195
|
+
pipeline: {
|
|
196
|
+
everyNConversations: num(pipelineGroup, "everyNConversations") ?? 5,
|
|
197
|
+
enableWarmup: bool(pipelineGroup, "enableWarmup") ?? true,
|
|
198
|
+
l1IdleTimeoutSeconds: num(pipelineGroup, "l1IdleTimeoutSeconds") ?? 600,
|
|
199
|
+
l2DelayAfterL1Seconds: num(pipelineGroup, "l2DelayAfterL1Seconds") ?? 10,
|
|
200
|
+
l2MinIntervalSeconds: num(pipelineGroup, "l2MinIntervalSeconds") ?? 900,
|
|
201
|
+
l2MaxIntervalSeconds: num(pipelineGroup, "l2MaxIntervalSeconds") ?? 3600,
|
|
202
|
+
sessionActiveWindowHours: num(pipelineGroup, "sessionActiveWindowHours") ?? 24,
|
|
203
|
+
},
|
|
204
|
+
recall: {
|
|
205
|
+
enabled: bool(recallGroup, "enabled") ?? true,
|
|
206
|
+
maxResults: num(recallGroup, "maxResults") ?? 5,
|
|
207
|
+
maxCharsPerMemory: num(recallGroup, "maxCharsPerMemory") ?? 0,
|
|
208
|
+
maxTotalRecallChars: num(recallGroup, "maxTotalRecallChars") ?? 0,
|
|
209
|
+
scoreThreshold: num(recallGroup, "scoreThreshold") ?? 0.3,
|
|
210
|
+
strategy: validateStrategy(str(recallGroup, "strategy")) ?? "hybrid",
|
|
211
|
+
timeoutMs: num(recallGroup, "timeoutMs") ?? 5000,
|
|
212
|
+
},
|
|
213
|
+
embedding: {
|
|
214
|
+
enabled: embeddingEnabled,
|
|
215
|
+
provider: embeddingProvider,
|
|
216
|
+
baseUrl: embeddingBaseUrl,
|
|
217
|
+
apiKey: embeddingApiKey,
|
|
218
|
+
model: str(embeddingGroup, "model") ?? defaultModel,
|
|
219
|
+
dimensions: num(embeddingGroup, "dimensions") ?? defaultDimensions,
|
|
220
|
+
sendDimensions: bool(embeddingGroup, "sendDimensions") ?? true,
|
|
221
|
+
conflictRecallTopK: num(embeddingGroup, "conflictRecallTopK") ?? 5,
|
|
222
|
+
proxyUrl: embeddingProxyUrl,
|
|
223
|
+
maxInputChars: num(embeddingGroup, "maxInputChars") ?? 5000,
|
|
224
|
+
timeoutMs: num(embeddingGroup, "timeoutMs") ?? 10_000,
|
|
225
|
+
recallTimeoutMs: num(embeddingGroup, "recallTimeoutMs") ?? undefined,
|
|
226
|
+
captureTimeoutMs: num(embeddingGroup, "captureTimeoutMs") ?? undefined,
|
|
227
|
+
modelCacheDir: optStr(embeddingGroup, "modelCacheDir"),
|
|
228
|
+
configError: embeddingConfigError,
|
|
229
|
+
},
|
|
230
|
+
storeBackend,
|
|
231
|
+
tcvdb: {
|
|
232
|
+
url: str(tcvdbGroup, "url") ?? "",
|
|
233
|
+
username: str(tcvdbGroup, "username") ?? "root",
|
|
234
|
+
apiKey: str(tcvdbGroup, "apiKey") ?? "",
|
|
235
|
+
database: str(tcvdbGroup, "database") ?? "",
|
|
236
|
+
alias: str(tcvdbGroup, "alias") ?? "",
|
|
237
|
+
embeddingModel: str(tcvdbGroup, "embeddingModel") ?? "bge-large-zh",
|
|
238
|
+
timeout: num(tcvdbGroup, "timeout") ?? 10000,
|
|
239
|
+
caPemPath: str(tcvdbGroup, "caPemPath") || undefined,
|
|
240
|
+
},
|
|
241
|
+
bm25: {
|
|
242
|
+
enabled: bool(bm25Group, "enabled") ?? true,
|
|
243
|
+
language: (str(bm25Group, "language") === "en" ? "en" : "zh"),
|
|
244
|
+
},
|
|
245
|
+
memoryCleanup,
|
|
246
|
+
report: {
|
|
247
|
+
enabled: bool(obj(c, "report"), "enabled") ?? false,
|
|
248
|
+
type: str(obj(c, "report"), "type") ?? "local",
|
|
249
|
+
},
|
|
250
|
+
llm: (() => {
|
|
251
|
+
const llmGroup = obj(c, "llm");
|
|
252
|
+
return {
|
|
253
|
+
enabled: bool(llmGroup, "enabled") ?? false,
|
|
254
|
+
baseUrl: str(llmGroup, "baseUrl") ?? "https://api.openai.com/v1",
|
|
255
|
+
apiKey: str(llmGroup, "apiKey") ?? "",
|
|
256
|
+
model: str(llmGroup, "model") ?? "gpt-4o",
|
|
257
|
+
maxTokens: num(llmGroup, "maxTokens") ?? 4096,
|
|
258
|
+
timeoutMs: num(llmGroup, "timeoutMs") ?? 120_000,
|
|
259
|
+
disableThinking: normalizeDisableThinking(boolOrStr(llmGroup, "disableThinking")),
|
|
260
|
+
sendSessionHeader: bool(llmGroup, "sendSessionHeader") ?? true,
|
|
261
|
+
sessionHeaderName: str(llmGroup, "sessionHeaderName") ?? "x-opencode-session",
|
|
262
|
+
sessionId: str(llmGroup, "sessionId") ?? "",
|
|
263
|
+
};
|
|
264
|
+
})(),
|
|
265
|
+
offload,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
// ============================
|
|
269
|
+
// Helper functions
|
|
270
|
+
// ============================
|
|
271
|
+
/** Get sub-object by key, or empty object if missing. */
|
|
272
|
+
function obj(c, key) {
|
|
273
|
+
const v = c[key];
|
|
274
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
275
|
+
}
|
|
276
|
+
function str(src, key) {
|
|
277
|
+
const v = src[key];
|
|
278
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
279
|
+
}
|
|
280
|
+
function optStr(src, key) {
|
|
281
|
+
const v = src[key];
|
|
282
|
+
return typeof v === "string" ? v : undefined;
|
|
283
|
+
}
|
|
284
|
+
function num(src, key) {
|
|
285
|
+
const v = src[key];
|
|
286
|
+
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
287
|
+
}
|
|
288
|
+
function bool(src, key) {
|
|
289
|
+
const v = src[key];
|
|
290
|
+
return typeof v === "boolean" ? v : undefined;
|
|
291
|
+
}
|
|
292
|
+
/** Read a field that may be boolean or string. */
|
|
293
|
+
function boolOrStr(src, key) {
|
|
294
|
+
const v = src[key];
|
|
295
|
+
if (typeof v === "boolean")
|
|
296
|
+
return v;
|
|
297
|
+
if (typeof v === "string" && v.trim())
|
|
298
|
+
return v.trim();
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
function strArray(src, key) {
|
|
302
|
+
const v = src[key];
|
|
303
|
+
if (!Array.isArray(v))
|
|
304
|
+
return undefined;
|
|
305
|
+
return v.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
306
|
+
}
|
|
307
|
+
const VALID_STRATEGIES = ["embedding", "keyword", "hybrid"];
|
|
308
|
+
/**
|
|
309
|
+
* Validate recall strategy against whitelist.
|
|
310
|
+
* Returns the strategy if valid, undefined otherwise (caller falls back to default).
|
|
311
|
+
*/
|
|
312
|
+
function validateStrategy(value) {
|
|
313
|
+
if (!value)
|
|
314
|
+
return undefined;
|
|
315
|
+
return VALID_STRATEGIES.includes(value)
|
|
316
|
+
? value
|
|
317
|
+
: undefined;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Normalize a cleanup time string.
|
|
321
|
+
*
|
|
322
|
+
* The input must follow "HH:MM" or "H:MM" format (24-hour clock).
|
|
323
|
+
* If the time is valid, it returns the normalized format "HH:MM"
|
|
324
|
+
* with leading zeros added when necessary.
|
|
325
|
+
* If the format is invalid or the time is out of range
|
|
326
|
+
* (hour: 0–23, minute: 0–59), it returns undefined.
|
|
327
|
+
*
|
|
328
|
+
* Examples:
|
|
329
|
+
* normalizeCleanTime("3:05") -> "03:05"
|
|
330
|
+
* normalizeCleanTime("03:05") -> "03:05"
|
|
331
|
+
* normalizeCleanTime("23:59") -> "23:59"
|
|
332
|
+
*
|
|
333
|
+
* normalizeCleanTime("24:00") -> undefined // hour out of range
|
|
334
|
+
* normalizeCleanTime("12:60") -> undefined // minute out of range
|
|
335
|
+
* normalizeCleanTime("3:5") -> undefined // minute must have two digits
|
|
336
|
+
* normalizeCleanTime("abc") -> undefined // invalid format
|
|
337
|
+
*/
|
|
338
|
+
function normalizeCleanTime(input) {
|
|
339
|
+
if (!input)
|
|
340
|
+
return undefined;
|
|
341
|
+
const trimmed = input.trim();
|
|
342
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
|
|
343
|
+
if (!m)
|
|
344
|
+
return undefined;
|
|
345
|
+
const hh = Number(m[1]);
|
|
346
|
+
const mm = Number(m[2]);
|
|
347
|
+
if (!Number.isInteger(hh) || !Number.isInteger(mm))
|
|
348
|
+
return undefined;
|
|
349
|
+
if (hh < 0 || hh > 23 || mm < 0 || mm > 59)
|
|
350
|
+
return undefined;
|
|
351
|
+
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Normalize offload retention days.
|
|
355
|
+
*
|
|
356
|
+
* - `<= 0` → 0 (disabled)
|
|
357
|
+
* - `(0, 3)` → 0 (invalid, force disabled)
|
|
358
|
+
* - `>= 3` → as-is
|
|
359
|
+
*/
|
|
360
|
+
function normalizeOffloadRetentionDays(value) {
|
|
361
|
+
if (value <= 0)
|
|
362
|
+
return 0;
|
|
363
|
+
if (value < 3)
|
|
364
|
+
return 0;
|
|
365
|
+
return value;
|
|
366
|
+
}
|