dsh-messager 0.1.4

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +86 -0
  3. package/README.md +241 -0
  4. package/assets/icon.png +0 -0
  5. package/cordis.patch.yml +11 -0
  6. package/lib/channels/feishu.d.ts +58 -0
  7. package/lib/channels/feishu.d.ts.map +1 -0
  8. package/lib/channels/feishu.js +77 -0
  9. package/lib/channels/feishu.js.map +1 -0
  10. package/lib/channels/system.d.ts +26 -0
  11. package/lib/channels/system.d.ts.map +1 -0
  12. package/lib/channels/system.js +73 -0
  13. package/lib/channels/system.js.map +1 -0
  14. package/lib/client.js +2001 -0
  15. package/lib/client.js.map +1 -0
  16. package/lib/config-route.d.ts +55 -0
  17. package/lib/config-route.d.ts.map +1 -0
  18. package/lib/config-route.js +170 -0
  19. package/lib/config-route.js.map +1 -0
  20. package/lib/config-shared.d.ts +28 -0
  21. package/lib/config-shared.d.ts.map +1 -0
  22. package/lib/config-shared.js +7 -0
  23. package/lib/config-shared.js.map +1 -0
  24. package/lib/config.d.ts +82 -0
  25. package/lib/config.d.ts.map +1 -0
  26. package/lib/config.js +59 -0
  27. package/lib/config.js.map +1 -0
  28. package/lib/index.d.ts +15 -0
  29. package/lib/index.d.ts.map +1 -0
  30. package/lib/index.js +152 -0
  31. package/lib/index.js.map +1 -0
  32. package/lib/notify.d.ts +75 -0
  33. package/lib/notify.d.ts.map +1 -0
  34. package/lib/notify.js +157 -0
  35. package/lib/notify.js.map +1 -0
  36. package/lib/settings.d.ts +23 -0
  37. package/lib/settings.d.ts.map +1 -0
  38. package/lib/settings.js +23 -0
  39. package/lib/settings.js.map +1 -0
  40. package/lib/signals.d.ts +57 -0
  41. package/lib/signals.d.ts.map +1 -0
  42. package/lib/signals.js +52 -0
  43. package/lib/signals.js.map +1 -0
  44. package/lib/templates.d.ts +40 -0
  45. package/lib/templates.d.ts.map +1 -0
  46. package/lib/templates.js +107 -0
  47. package/lib/templates.js.map +1 -0
  48. package/lib/types/client/card-controller.d.ts +171 -0
  49. package/lib/types/client/card-controller.d.ts.map +1 -0
  50. package/lib/types/client/config.d.ts +20 -0
  51. package/lib/types/client/config.d.ts.map +1 -0
  52. package/lib/types/client/diff.d.ts +26 -0
  53. package/lib/types/client/diff.d.ts.map +1 -0
  54. package/lib/types/client/fetch-scope.d.ts +29 -0
  55. package/lib/types/client/fetch-scope.d.ts.map +1 -0
  56. package/lib/types/client/index.d.ts +24 -0
  57. package/lib/types/client/index.d.ts.map +1 -0
  58. package/lib/types/client/locales.d.ts +92 -0
  59. package/lib/types/client/locales.d.ts.map +1 -0
  60. package/lib/types/client/section.d.ts +16 -0
  61. package/lib/types/client/section.d.ts.map +1 -0
  62. package/lib/types/client/settings-form.d.ts +22 -0
  63. package/lib/types/client/settings-form.d.ts.map +1 -0
  64. package/lib/types/config-shared.d.ts +28 -0
  65. package/lib/types/config-shared.d.ts.map +1 -0
  66. package/lib/types/config.d.ts +82 -0
  67. package/lib/types/config.d.ts.map +1 -0
  68. package/package.json +96 -0
package/lib/client.js ADDED
@@ -0,0 +1,2001 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-messager",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react_jsx_runtime = require("react/jsx-runtime");
8
+ //#region node_modules/.pnpm/@deepseek-ai+cosmokit@1.8.2/node_modules/@deepseek-ai/cosmokit/lib/index.js
9
+ /** Return true when a value is `null` or `undefined`. */
10
+ function isNullable(value) {
11
+ return value === null || value === void 0;
12
+ }
13
+ /** Return true for non-array object values. */
14
+ function isPlainObject(data) {
15
+ return data && typeof data === "object" && !Array.isArray(data);
16
+ }
17
+ /** Filter object entries and return a new object. */
18
+ function filterKeys(object, filter) {
19
+ return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
20
+ }
21
+ /** Map object values while preserving the original key set. */
22
+ function mapValues(object, transform) {
23
+ return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
24
+ }
25
+ /** Pick selected keys from an object, optionally including `undefined` values. */
26
+ function pick(source, keys, forced) {
27
+ if (!keys) return { ...source };
28
+ const result = {};
29
+ for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
30
+ return result;
31
+ }
32
+ /** Test values using `instanceof` with a `toStringTag` fallback. */
33
+ function is(type, value) {
34
+ if (arguments.length === 1) return (value) => is(type, value);
35
+ return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
36
+ }
37
+ function isArrayBufferLike(value) {
38
+ return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
39
+ }
40
+ function isArrayBufferSource(value) {
41
+ return isArrayBufferLike(value) || ArrayBuffer.isView(value);
42
+ }
43
+ /** Binary source detection and base64/hex conversion helpers. */
44
+ var Binary;
45
+ (function(Binary) {
46
+ Binary.is = isArrayBufferLike;
47
+ Binary.isSource = isArrayBufferSource;
48
+ function fromSource(source) {
49
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
50
+ else return source;
51
+ }
52
+ Binary.fromSource = fromSource;
53
+ function toBase64(source) {
54
+ source = fromSource(source);
55
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
56
+ let binary = "";
57
+ const bytes = new Uint8Array(source);
58
+ for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
59
+ return btoa(binary);
60
+ }
61
+ Binary.toBase64 = toBase64;
62
+ function fromBase64(source) {
63
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
64
+ return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
65
+ }
66
+ Binary.fromBase64 = fromBase64;
67
+ function toHex(source) {
68
+ source = fromSource(source);
69
+ if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
70
+ return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
71
+ }
72
+ Binary.toHex = toHex;
73
+ function fromHex(source) {
74
+ if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
75
+ const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
76
+ const buffer = [];
77
+ for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
78
+ return Uint8Array.from(buffer).buffer;
79
+ }
80
+ Binary.fromHex = fromHex;
81
+ })(Binary || (Binary = {}));
82
+ Binary.fromBase64;
83
+ Binary.toBase64;
84
+ Binary.fromHex;
85
+ Binary.toHex;
86
+ /** Deep-clone common JavaScript values while preserving prototypes and cycles. */
87
+ function clone(source, refs = /* @__PURE__ */ new Map()) {
88
+ if (!source || typeof source !== "object") return source;
89
+ if (is("Date", source)) return new Date(source.valueOf());
90
+ if (is("RegExp", source)) return new RegExp(source.source, source.flags);
91
+ if (isArrayBufferLike(source)) return source.slice(0);
92
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
93
+ const cached = refs.get(source);
94
+ if (cached) return cached;
95
+ if (Array.isArray(source)) {
96
+ const result = [];
97
+ refs.set(source, result);
98
+ source.forEach((value, index) => {
99
+ result[index] = Reflect.apply(clone, null, [value, refs]);
100
+ });
101
+ return result;
102
+ }
103
+ const result = Object.create(Object.getPrototypeOf(source));
104
+ refs.set(source, result);
105
+ for (const key of Reflect.ownKeys(source)) {
106
+ const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
107
+ if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
108
+ Reflect.defineProperty(result, key, descriptor);
109
+ }
110
+ return result;
111
+ }
112
+ /** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
113
+ function deepEqual(a, b, strict) {
114
+ if (a === b) return true;
115
+ if (!strict && isNullable(a) && isNullable(b)) return true;
116
+ if (typeof a !== typeof b) return false;
117
+ if (typeof a !== "object") return false;
118
+ if (!a || !b) return false;
119
+ function check(test, then) {
120
+ return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
121
+ }
122
+ return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
123
+ if (a.byteLength !== b.byteLength) return false;
124
+ const viewA = new Uint8Array(a);
125
+ const viewB = new Uint8Array(b);
126
+ for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
127
+ return true;
128
+ }) ?? Object.keys({
129
+ ...a,
130
+ ...b
131
+ }).every((key) => deepEqual(a[key], b[key], strict));
132
+ }
133
+ /** Time constants plus parsing and formatting helpers. */
134
+ var Time;
135
+ (function(Time) {
136
+ Time.millisecond = 1;
137
+ Time.second = 1e3;
138
+ Time.minute = Time.second * 60;
139
+ Time.hour = Time.minute * 60;
140
+ Time.day = Time.hour * 24;
141
+ Time.week = Time.day * 7;
142
+ let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
143
+ function setTimezoneOffset(offset) {
144
+ timezoneOffset = offset;
145
+ }
146
+ Time.setTimezoneOffset = setTimezoneOffset;
147
+ function getTimezoneOffset() {
148
+ return timezoneOffset;
149
+ }
150
+ Time.getTimezoneOffset = getTimezoneOffset;
151
+ function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
152
+ if (typeof date === "number") date = new Date(date);
153
+ if (offset === void 0) offset = timezoneOffset;
154
+ return Math.floor((date.valueOf() / Time.minute - offset) / 1440);
155
+ }
156
+ Time.getDateNumber = getDateNumber;
157
+ function fromDateNumber(value, offset) {
158
+ const date = new Date(value * Time.day);
159
+ if (offset === void 0) offset = timezoneOffset;
160
+ return new Date(+date + offset * Time.minute);
161
+ }
162
+ Time.fromDateNumber = fromDateNumber;
163
+ const numeric = /\d+(?:\.\d+)?/.source;
164
+ const timeRegExp = new RegExp(`^${[
165
+ "w(?:eek(?:s)?)?",
166
+ "d(?:ay(?:s)?)?",
167
+ "h(?:our(?:s)?)?",
168
+ "m(?:in(?:ute)?(?:s)?)?",
169
+ "s(?:ec(?:ond)?(?:s)?)?"
170
+ ].map((unit) => `(${numeric}${unit})?`).join("")}$`);
171
+ function parseTime(source) {
172
+ const capture = timeRegExp.exec(source);
173
+ if (!capture) return 0;
174
+ return (parseFloat(capture[1]) * Time.week || 0) + (parseFloat(capture[2]) * Time.day || 0) + (parseFloat(capture[3]) * Time.hour || 0) + (parseFloat(capture[4]) * Time.minute || 0) + (parseFloat(capture[5]) * Time.second || 0);
175
+ }
176
+ Time.parseTime = parseTime;
177
+ function parseDate(date) {
178
+ const parsed = parseTime(date);
179
+ if (parsed) date = Date.now() + parsed;
180
+ else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
181
+ else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
182
+ return date ? new Date(date) : /* @__PURE__ */ new Date();
183
+ }
184
+ Time.parseDate = parseDate;
185
+ function format(ms) {
186
+ const abs = Math.abs(ms);
187
+ if (abs >= Time.day - Time.hour / 2) return Math.round(ms / Time.day) + "d";
188
+ else if (abs >= Time.hour - Time.minute / 2) return Math.round(ms / Time.hour) + "h";
189
+ else if (abs >= Time.minute - Time.second / 2) return Math.round(ms / Time.minute) + "m";
190
+ else if (abs >= Time.second) return Math.round(ms / Time.second) + "s";
191
+ return ms + "ms";
192
+ }
193
+ Time.format = format;
194
+ function toDigits(source, length = 2) {
195
+ return source.toString().padStart(length, "0");
196
+ }
197
+ Time.toDigits = toDigits;
198
+ function template(template, time = /* @__PURE__ */ new Date()) {
199
+ return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
200
+ }
201
+ Time.template = template;
202
+ })(Time || (Time = {}));
203
+ //#endregion
204
+ //#region node_modules/.pnpm/@deepseek-ai+schemastery@3.18.1/node_modules/@deepseek-ai/schemastery/lib/index.mjs
205
+ const kSchema = Symbol.for("schemastery");
206
+ const kValidationError = Symbol.for("ValidationError");
207
+ globalThis.__schemastery_index__ ??= 0;
208
+ globalThis.__schemastery_refs__ = void 0;
209
+ var ValidationError = class extends TypeError {
210
+ options;
211
+ name = "ValidationError";
212
+ constructor(message, options) {
213
+ let prefix = "$";
214
+ for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
215
+ else if (typeof segment === "number") prefix += "[" + segment + "]";
216
+ else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
217
+ if (prefix.startsWith(".")) prefix = prefix.slice(1);
218
+ super((prefix === "$" ? "" : `${prefix} `) + message);
219
+ this.options = options;
220
+ }
221
+ static is(error) {
222
+ return !!error?.[kValidationError];
223
+ }
224
+ };
225
+ Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
226
+ const Schema = function(options) {
227
+ const schema = function(data, options = {}) {
228
+ return Schema.resolve(data, schema, options)[0];
229
+ };
230
+ if (options.refs) {
231
+ const refs = mapValues(options.refs, (options) => new Schema(options));
232
+ const getRef = (uid) => refs[uid];
233
+ for (const key in refs) {
234
+ const options = refs[key];
235
+ options.sKey = getRef(options.sKey);
236
+ options.inner = getRef(options.inner);
237
+ options.list = options.list && options.list.map(getRef);
238
+ options.dict = options.dict && mapValues(options.dict, getRef);
239
+ }
240
+ return refs[options.uid];
241
+ }
242
+ Object.assign(schema, options);
243
+ if (typeof schema.callback === "string") try {
244
+ schema.callback = new Function("return " + schema.callback)();
245
+ } catch {}
246
+ Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
247
+ Object.setPrototypeOf(schema, Schema.prototype);
248
+ schema.meta ||= {};
249
+ schema.toString = schema.toString.bind(schema);
250
+ return schema;
251
+ };
252
+ Schema.prototype = Object.create(Function.prototype);
253
+ Schema.prototype[kSchema] = true;
254
+ Object.defineProperty(Schema.prototype, "~standard", { get() {
255
+ return {
256
+ version: 1,
257
+ vendor: "schemastery",
258
+ validate: (value) => {
259
+ try {
260
+ return { value: Schema.resolve(value, this, {})[0] };
261
+ } catch (error) {
262
+ if (ValidationError.is(error)) return { issues: [{
263
+ message: error.message,
264
+ path: error.options.path
265
+ }] };
266
+ throw error;
267
+ }
268
+ }
269
+ };
270
+ } });
271
+ Schema.ValidationError = ValidationError;
272
+ Schema.prototype.toJSON = function toJSON() {
273
+ if (globalThis.__schemastery_refs__) {
274
+ globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
275
+ return this.uid;
276
+ }
277
+ globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
278
+ globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
279
+ const result = {
280
+ uid: this.uid,
281
+ refs: globalThis.__schemastery_refs__
282
+ };
283
+ globalThis.__schemastery_refs__ = void 0;
284
+ return result;
285
+ };
286
+ Schema.prototype.set = function set(key, value) {
287
+ this.dict[key] = value;
288
+ return this;
289
+ };
290
+ Schema.prototype.push = function push(value) {
291
+ this.list.push(value);
292
+ return this;
293
+ };
294
+ function mergeDesc(original, messages) {
295
+ const result = typeof original === "string" ? { "": original } : { ...original };
296
+ for (const locale in messages) {
297
+ const value = messages[locale];
298
+ if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
299
+ else if (typeof value === "string") result[locale] = value;
300
+ }
301
+ return result;
302
+ }
303
+ function getInner(value) {
304
+ return value?.$value ?? value?.$inner;
305
+ }
306
+ function extractKeys(data) {
307
+ return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
308
+ }
309
+ Schema.prototype.i18n = function i18n(messages) {
310
+ const schema = Schema(this);
311
+ const desc = mergeDesc(schema.meta.description, messages);
312
+ if (Object.keys(desc).length) schema.meta.description = desc;
313
+ if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
314
+ return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
315
+ });
316
+ if (schema.list) schema.list = schema.list.map((inner, index) => {
317
+ return inner.i18n(mapValues(messages, (data = {}) => {
318
+ if (Array.isArray(getInner(data))) return getInner(data)[index];
319
+ if (Array.isArray(data)) return data[index];
320
+ return extractKeys(data);
321
+ }));
322
+ });
323
+ if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
324
+ if (getInner(data)) return getInner(data);
325
+ return extractKeys(data);
326
+ }));
327
+ if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
328
+ return schema;
329
+ };
330
+ Schema.prototype.extra = function extra(key, value) {
331
+ const schema = Schema(this);
332
+ schema.meta = {
333
+ ...schema.meta,
334
+ [key]: value
335
+ };
336
+ return schema;
337
+ };
338
+ for (const key of [
339
+ "required",
340
+ "disabled",
341
+ "collapse",
342
+ "hidden",
343
+ "loose"
344
+ ]) Object.assign(Schema.prototype, { [key](value = true) {
345
+ const schema = Schema(this);
346
+ schema.meta = {
347
+ ...schema.meta,
348
+ [key]: value
349
+ };
350
+ return schema;
351
+ } });
352
+ Schema.prototype.deprecated = function deprecated() {
353
+ const schema = Schema(this);
354
+ schema.meta.badges ||= [];
355
+ schema.meta.badges.push({
356
+ text: "deprecated",
357
+ type: "danger"
358
+ });
359
+ return schema;
360
+ };
361
+ Schema.prototype.experimental = function experimental() {
362
+ const schema = Schema(this);
363
+ schema.meta.badges ||= [];
364
+ schema.meta.badges.push({
365
+ text: "experimental",
366
+ type: "warning"
367
+ });
368
+ return schema;
369
+ };
370
+ Schema.prototype.pattern = function pattern(regexp) {
371
+ const schema = Schema(this);
372
+ const pattern = pick(regexp, ["source", "flags"]);
373
+ schema.meta = {
374
+ ...schema.meta,
375
+ pattern
376
+ };
377
+ return schema;
378
+ };
379
+ Schema.prototype.simplify = function simplify(value) {
380
+ if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
381
+ if (isNullable(value)) return value;
382
+ if (this.type === "object" || this.type === "dict") {
383
+ const result = {};
384
+ for (const key in value) {
385
+ const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
386
+ if (this.type === "dict" || !isNullable(item)) result[key] = item;
387
+ }
388
+ if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
389
+ return result;
390
+ } else if (this.type === "array" || this.type === "tuple") {
391
+ const result = [];
392
+ value.forEach((value, index) => {
393
+ const schema = this.type === "array" ? this.inner : this.list[index];
394
+ const item = schema ? schema.simplify(value) : value;
395
+ result.push(item);
396
+ });
397
+ return result;
398
+ } else if (this.type === "intersect") {
399
+ const result = {};
400
+ for (const item of this.list) Object.assign(result, item.simplify(value));
401
+ return result;
402
+ } else if (this.type === "union") for (const schema of this.list) try {
403
+ Schema.resolve(value, schema, {});
404
+ return schema.simplify(value);
405
+ } catch {}
406
+ return value;
407
+ };
408
+ Schema.prototype.toString = function toString(inline) {
409
+ return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
410
+ };
411
+ Schema.prototype.role = function role(role, extra) {
412
+ const schema = Schema(this);
413
+ schema.meta = {
414
+ ...schema.meta,
415
+ role,
416
+ extra
417
+ };
418
+ return schema;
419
+ };
420
+ for (const key of [
421
+ "default",
422
+ "link",
423
+ "comment",
424
+ "description",
425
+ "max",
426
+ "min",
427
+ "step"
428
+ ]) Object.assign(Schema.prototype, { [key](value) {
429
+ const schema = Schema(this);
430
+ schema.meta = {
431
+ ...schema.meta,
432
+ [key]: value
433
+ };
434
+ return schema;
435
+ } });
436
+ const resolvers = {};
437
+ Schema.extend = function extend(type, resolve) {
438
+ resolvers[type] = resolve;
439
+ };
440
+ Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
441
+ if (!schema) return [data];
442
+ if (options.ignore?.(data, schema)) return [data];
443
+ if (isNullable(data) && schema.type !== "lazy") {
444
+ if (schema.meta.required) throw new ValidationError(`missing required value`, options);
445
+ let current = schema;
446
+ let fallback = schema.meta.default;
447
+ while (current?.type === "intersect" && isNullable(fallback)) {
448
+ current = current.list[0];
449
+ fallback = current?.meta.default;
450
+ }
451
+ if (isNullable(fallback)) return [data];
452
+ data = clone(fallback);
453
+ }
454
+ const callback = resolvers[schema.type];
455
+ if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
456
+ try {
457
+ return callback(data, schema, options, strict);
458
+ } catch (error) {
459
+ if (!schema.meta.loose) throw error;
460
+ return [schema.meta.default];
461
+ }
462
+ };
463
+ Schema.from = function from(source) {
464
+ if (isNullable(source)) return Schema.any();
465
+ else if ([
466
+ "string",
467
+ "number",
468
+ "boolean"
469
+ ].includes(typeof source)) return Schema.const(source).required();
470
+ else if (source[kSchema]) return source;
471
+ else if (typeof source === "function") switch (source) {
472
+ case String: return Schema.string().required();
473
+ case Number: return Schema.number().required();
474
+ case Boolean: return Schema.boolean().required();
475
+ case Function: return Schema.function().required();
476
+ default: return Schema.is(source).required();
477
+ }
478
+ else throw new TypeError(`cannot infer schema from ${source}`);
479
+ };
480
+ Schema.lazy = function lazy(builder) {
481
+ const toJSON = () => {
482
+ if (!schema.inner[kSchema]) {
483
+ schema.inner = schema.builder();
484
+ schema.inner.meta = {
485
+ ...schema.meta,
486
+ ...schema.inner.meta
487
+ };
488
+ }
489
+ return schema.inner.toJSON();
490
+ };
491
+ const schema = new Schema({
492
+ type: "lazy",
493
+ builder,
494
+ inner: { toJSON }
495
+ });
496
+ return schema;
497
+ };
498
+ Schema.natural = function natural() {
499
+ return Schema.number().step(1).min(0);
500
+ };
501
+ Schema.percent = function percent() {
502
+ return Schema.number().step(.01).min(0).max(1).role("slider");
503
+ };
504
+ Schema.date = function date() {
505
+ return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
506
+ const date = new Date(value);
507
+ if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
508
+ return date;
509
+ }, true)]);
510
+ };
511
+ Schema.regExp = function regExp(flag = "") {
512
+ return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
513
+ try {
514
+ return new RegExp(value, flag);
515
+ } catch (e) {
516
+ throw new ValidationError(e.message, options);
517
+ }
518
+ }, true)]);
519
+ };
520
+ Schema.arrayBuffer = function arrayBuffer(encoding) {
521
+ return Schema.union([
522
+ Schema.is(ArrayBuffer),
523
+ Schema.is(SharedArrayBuffer),
524
+ Schema.transform(Schema.any(), (value, options) => {
525
+ if (Binary.isSource(value)) return Binary.fromSource(value);
526
+ throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
527
+ }, true),
528
+ ...encoding ? [Schema.transform(Schema.string(), (value, options) => {
529
+ try {
530
+ return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
531
+ } catch (e) {
532
+ throw new ValidationError(e.message, options);
533
+ }
534
+ }, true)] : []
535
+ ]);
536
+ };
537
+ Schema.extend("lazy", (data, schema, options, strict) => {
538
+ if (!schema.inner[kSchema]) {
539
+ schema.inner = schema.builder();
540
+ schema.inner.meta = {
541
+ ...schema.meta,
542
+ ...schema.inner.meta
543
+ };
544
+ }
545
+ return Schema.resolve(data, schema.inner, options, strict);
546
+ });
547
+ Schema.extend("any", (data) => {
548
+ return [data];
549
+ });
550
+ Schema.extend("never", (data, _, options) => {
551
+ throw new ValidationError(`expected nullable but got ${data}`, options);
552
+ });
553
+ Schema.extend("const", (data, { value }, options) => {
554
+ if (deepEqual(data, value)) return [value];
555
+ throw new ValidationError(`expected ${value} but got ${data}`, options);
556
+ });
557
+ function checkWithinRange(data, meta, description, options, skipMin = false) {
558
+ const { max = Infinity, min = -Infinity } = meta;
559
+ if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
560
+ if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
561
+ }
562
+ Schema.extend("string", (data, { meta }, options) => {
563
+ if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
564
+ if (meta.pattern) {
565
+ const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
566
+ if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
567
+ }
568
+ checkWithinRange(data.length, meta, "string length", options);
569
+ return [data];
570
+ });
571
+ function decimalShift(data, digits) {
572
+ const str = data.toString();
573
+ if (str.includes("e")) return data * Math.pow(10, digits);
574
+ const index = str.indexOf(".");
575
+ if (index === -1) return data * Math.pow(10, digits);
576
+ const frac = str.slice(index + 1);
577
+ const integer = str.slice(0, index);
578
+ if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
579
+ return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
580
+ }
581
+ function isMultipleOf(data, min, step) {
582
+ step = Math.abs(step);
583
+ if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
584
+ const index = step.toString().indexOf(".");
585
+ const digits = step.toString().slice(index + 1).length;
586
+ return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
587
+ }
588
+ Schema.extend("number", (data, { meta }, options) => {
589
+ if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
590
+ checkWithinRange(data, meta, "number", options);
591
+ const { step } = meta;
592
+ if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
593
+ return [data];
594
+ });
595
+ Schema.extend("boolean", (data, _, options) => {
596
+ if (typeof data === "boolean") return [data];
597
+ throw new ValidationError(`expected boolean but got ${data}`, options);
598
+ });
599
+ Schema.extend("bitset", (data, { bits, meta }, options) => {
600
+ let value = 0, keys = [];
601
+ if (typeof data === "number") {
602
+ value = data;
603
+ for (const key in bits) if (data & bits[key]) keys.push(key);
604
+ } else if (Array.isArray(data)) {
605
+ keys = data;
606
+ for (const key of keys) {
607
+ if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
608
+ if (key in bits) value |= bits[key];
609
+ }
610
+ } else throw new ValidationError(`expected number or array but got ${data}`, options);
611
+ if (value === meta.default) return [value];
612
+ return [value, keys];
613
+ });
614
+ Schema.extend("function", (data, _, options) => {
615
+ if (typeof data === "function") return [data];
616
+ throw new ValidationError(`expected function but got ${data}`, options);
617
+ });
618
+ Schema.extend("is", (data, { constructor }, options) => {
619
+ if (typeof constructor === "function") {
620
+ if (data instanceof constructor) return [data];
621
+ throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
622
+ } else {
623
+ if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
624
+ let prototype = Object.getPrototypeOf(data);
625
+ while (prototype) {
626
+ if (prototype.constructor?.name === constructor) return [data];
627
+ prototype = Object.getPrototypeOf(prototype);
628
+ }
629
+ throw new ValidationError(`expected ${constructor} but got ${data}`, options);
630
+ }
631
+ });
632
+ function property(data, key, schema, options) {
633
+ try {
634
+ const [value, adapted] = Schema.resolve(data[key], schema, {
635
+ ...options,
636
+ path: [...options.path || [], key]
637
+ });
638
+ if (adapted !== void 0) data[key] = adapted;
639
+ return value;
640
+ } catch (e) {
641
+ if (!options?.autofix) throw e;
642
+ delete data[key];
643
+ return schema.meta.default;
644
+ }
645
+ }
646
+ Schema.extend("array", (data, { inner, meta }, options) => {
647
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
648
+ checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
649
+ return [data.map((_, index) => property(data, index, inner, options))];
650
+ });
651
+ Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
652
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
653
+ const result = {};
654
+ for (const key in data) {
655
+ let rKey;
656
+ try {
657
+ rKey = Schema.resolve(key, sKey, options)[0];
658
+ } catch (error) {
659
+ if (strict) continue;
660
+ throw error;
661
+ }
662
+ result[rKey] = property(data, key, inner, options);
663
+ data[rKey] = data[key];
664
+ if (key !== rKey) delete data[key];
665
+ }
666
+ return [result];
667
+ });
668
+ Schema.extend("tuple", (data, { list }, options, strict) => {
669
+ if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
670
+ const result = list.map((inner, index) => property(data, index, inner, options));
671
+ if (strict) return [result];
672
+ result.push(...data.slice(list.length));
673
+ return [result];
674
+ });
675
+ function merge(result, data) {
676
+ for (const key in data) {
677
+ if (key in result) continue;
678
+ result[key] = data[key];
679
+ }
680
+ }
681
+ Schema.extend("object", (data, { dict }, options, strict) => {
682
+ if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
683
+ const result = {};
684
+ for (const key in dict) {
685
+ const value = property(data, key, dict[key], options);
686
+ if (!isNullable(value) || key in data) result[key] = value;
687
+ }
688
+ if (!strict) merge(result, data);
689
+ return [result];
690
+ });
691
+ Schema.extend("union", (data, { list, toString }, options, strict) => {
692
+ const messages = [];
693
+ for (const inner of list) try {
694
+ return Schema.resolve(data, inner, options, strict);
695
+ } catch (error) {
696
+ messages.push(error);
697
+ }
698
+ throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
699
+ });
700
+ Schema.extend("intersect", (data, { list, toString }, options, strict) => {
701
+ if (!list.length) return [data];
702
+ let result;
703
+ for (const inner of list) {
704
+ const value = Schema.resolve(data, inner, options, true)[0];
705
+ if (isNullable(value)) continue;
706
+ if (isNullable(result)) result = value;
707
+ else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
708
+ else if (typeof value === "object") merge(result ??= {}, value);
709
+ else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
710
+ }
711
+ if (!strict && isPlainObject(data)) merge(result, data);
712
+ return [result];
713
+ });
714
+ Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
715
+ const [result, adapted = data] = Schema.resolve(data, inner, options, true);
716
+ if (preserve) return [callback(result)];
717
+ else return [callback(result), callback(adapted)];
718
+ });
719
+ const formatters = {};
720
+ function defineMethod(name, keys, format) {
721
+ formatters[name] = format;
722
+ Object.assign(Schema, { [name](...args) {
723
+ const schema = new Schema({ type: name });
724
+ keys.forEach((key, index) => {
725
+ switch (key) {
726
+ case "sKey":
727
+ schema.sKey = args[index] ?? Schema.string();
728
+ break;
729
+ case "inner":
730
+ schema.inner = Schema.from(args[index]);
731
+ break;
732
+ case "list":
733
+ schema.list = args[index].map(Schema.from);
734
+ break;
735
+ case "dict":
736
+ schema.dict = mapValues(args[index], Schema.from);
737
+ break;
738
+ case "bits":
739
+ schema.bits = {};
740
+ for (const key in args[index]) {
741
+ if (typeof args[index][key] !== "number") continue;
742
+ schema.bits[key] = args[index][key];
743
+ }
744
+ break;
745
+ case "callback": {
746
+ const callback = schema.callback = args[index];
747
+ callback["toJSON"] ||= () => callback.toString();
748
+ break;
749
+ }
750
+ case "constructor": {
751
+ const constructor = schema.constructor = args[index];
752
+ if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
753
+ break;
754
+ }
755
+ default: schema[key] = args[index];
756
+ }
757
+ });
758
+ if (name === "object" || name === "dict") schema.meta.default = {};
759
+ else if (name === "array" || name === "tuple") schema.meta.default = [];
760
+ else if (name === "bitset") schema.meta.default = 0;
761
+ return schema;
762
+ } });
763
+ }
764
+ defineMethod("is", ["constructor"], ({ constructor }) => {
765
+ if (typeof constructor === "function") return constructor.name;
766
+ else return constructor;
767
+ });
768
+ defineMethod("any", [], () => "any");
769
+ defineMethod("never", [], () => "never");
770
+ defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
771
+ defineMethod("string", [], () => "string");
772
+ defineMethod("number", [], () => "number");
773
+ defineMethod("boolean", [], () => "boolean");
774
+ defineMethod("bitset", ["bits"], () => "bitset");
775
+ defineMethod("function", [], () => "function");
776
+ defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
777
+ defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
778
+ defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
779
+ defineMethod("object", ["dict"], ({ dict }) => {
780
+ if (Object.keys(dict).length === 0) return "{}";
781
+ return `{ ${Object.entries(dict).map(([key, inner]) => {
782
+ return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
783
+ }).join(", ")} }`;
784
+ });
785
+ defineMethod("union", ["list"], ({ list }, inline) => {
786
+ const result = list.map(({ toString: format }) => format()).join(" | ");
787
+ return inline ? `(${result})` : result;
788
+ });
789
+ defineMethod("intersect", ["list"], ({ list }) => {
790
+ return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
791
+ });
792
+ defineMethod("transform", [
793
+ "inner",
794
+ "callback",
795
+ "preserve"
796
+ ], ({ inner }, isInner) => inner.toString(isInner));
797
+ //#endregion
798
+ //#region src/config.ts
799
+ /**
800
+ * dsh-messager 配置模型。
801
+ *
802
+ * 本 schema 同时承担两个角色:
803
+ * - host 端的 Loader config(cordis.yml 中该插件行的 `config:`,成为 settings 命名空间的 base 层);
804
+ * - `ctx.settings.register('messager', Config, { base })` 的命名空间 schema,
805
+ * 因此 Web 设置页会自动渲染配置表单,用户层可覆盖 base。
806
+ *
807
+ * 有效值优先级:schema 默认值 → base(cordis.yml)→ 用户层(设置页)。
808
+ */
809
+ const verbosity = Schema.union([
810
+ "minimal",
811
+ "normal",
812
+ "detailed"
813
+ ]);
814
+ const Config = Schema.object({
815
+ triggers: Schema.object({
816
+ interaction: Schema.boolean().default(true),
817
+ completed: Schema.boolean().default(true),
818
+ error: Schema.boolean().default(true)
819
+ }),
820
+ system: Schema.object({
821
+ enabled: Schema.boolean().default(true),
822
+ icon: Schema.string(),
823
+ verbosity: verbosity.default("normal")
824
+ }),
825
+ browser: Schema.object({
826
+ enabled: Schema.boolean().default(true),
827
+ icon: Schema.string(),
828
+ onlyWhenHidden: Schema.boolean().default(true),
829
+ verbosity: verbosity.default("normal")
830
+ }),
831
+ feishu: Schema.object({
832
+ enabled: Schema.boolean().default(false),
833
+ webhookUrl: Schema.string(),
834
+ secret: Schema.string().role("secret"),
835
+ timeoutMs: Schema.number().default(5e3),
836
+ verbosity: verbosity.default("normal")
837
+ }),
838
+ dedup: Schema.object({
839
+ interactionCooldownMs: Schema.number().default(1e4),
840
+ completedDebounceMs: Schema.number().default(1e3),
841
+ perChannelPerMinute: Schema.number().default(20)
842
+ }),
843
+ message: Schema.object({
844
+ titlePrefix: Schema.string(),
845
+ includeSessionTitle: Schema.boolean().default(true),
846
+ guiUrl: Schema.string().default("http://127.0.0.1:3080")
847
+ })
848
+ });
849
+ /**
850
+ * 用 schema 默认值解析一份配置(输入可省略任意字段)。
851
+ * 与 cordis Loader 的校验同源,保证“默认值 → base → 用户层”的解析一致。
852
+ */
853
+ function resolveConfig(input = {}) {
854
+ return Config(input);
855
+ }
856
+ //#endregion
857
+ //#region src/config-shared.ts
858
+ /**
859
+ * 配置路由的跨端共享类型(host 与浏览器 client 共用,不得 import 任何
860
+ * Node/浏览器专属模块)。
861
+ */
862
+ /** 配置路由路径(挂在 DSH webServer 上,同源访问)。 */
863
+ const CONFIG_PATH = "/dsh-messager/config";
864
+ //#endregion
865
+ //#region src/client/config.ts
866
+ /**
867
+ * 客户端配置读取:经 host 配置路由(GET /dsh-messager/config)拉取 messager
868
+ * 命名空间有效值,并通过 `settings/document-updated` 失效后重拉。
869
+ * 路由不受 Web 设置白名单门控,发行版同样可用;拉取失败回退 schema 默认值。
870
+ */
871
+ var ClientConfig = class {
872
+ current = resolveConfig({});
873
+ listeners = /* @__PURE__ */ new Set();
874
+ get() {
875
+ return this.current;
876
+ }
877
+ subscribe(listener) {
878
+ this.listeners.add(listener);
879
+ return () => {
880
+ this.listeners.delete(listener);
881
+ };
882
+ }
883
+ /** 拉取 host 配置视图(首次与收到 document-updated 后调用)。 */
884
+ async refresh() {
885
+ try {
886
+ const response = await fetch(CONFIG_PATH, { headers: { accept: "application/json" } });
887
+ if (!response.ok) return;
888
+ const view = await response.json();
889
+ if (view.status !== "ready" || view.value === void 0) return;
890
+ this.current = resolveConfig(view.value);
891
+ for (const listener of [...this.listeners]) listener();
892
+ } catch {}
893
+ }
894
+ };
895
+ //#endregion
896
+ //#region src/client/diff.ts
897
+ /**
898
+ * 对比两次列表快照。
899
+ * 首次出现的会话只建立基线、不通知;被移除的会话不通知。
900
+ * @param previous - 上一次快照的 byId。
901
+ * @param next - 本次快照的 byId。
902
+ * @param current - 本次快照的当前选中会话(选中会话的完成不打扰)。
903
+ */
904
+ function diffSessionSummaries(previous, next, current) {
905
+ const notices = [];
906
+ for (const id of Object.keys(next)) {
907
+ const summary = next[id];
908
+ if (summary === void 0) continue;
909
+ const before = previous[id];
910
+ if (before === void 0) continue;
911
+ if (summary.pendingInteraction !== void 0 && before.pendingInteraction === void 0) notices.push({
912
+ kind: "interaction",
913
+ sessionId: id,
914
+ interaction: summary.pendingInteraction,
915
+ title: summary.displayTitle
916
+ });
917
+ if (before.running && !summary.running && id !== current) notices.push({
918
+ kind: "completed",
919
+ sessionId: id,
920
+ title: summary.displayTitle
921
+ });
922
+ }
923
+ return notices;
924
+ }
925
+ //#endregion
926
+ //#region src/client/card-controller.ts
927
+ /** 字段键:`group.field`。 */
928
+ function fieldKey(group, field) {
929
+ return `${group}.${field}`;
930
+ }
931
+ /**
932
+ * 渲染层门控判断(与控制器 gatedOff 语义一致):
933
+ * hiddenUnless 指定的开关字段当前(含草稿)不为 true 时,该字段不渲染。
934
+ */
935
+ function isFieldGated(spec, fields) {
936
+ const gate = spec.hiddenUnless;
937
+ if (gate === void 0) return false;
938
+ return fields[fieldKey(gate.group, gate.field)]?.text !== "true";
939
+ }
940
+ /** 把草稿解析为组内值;undefined 表示非法(阻止保存)。 */
941
+ function parseDraft(spec, text) {
942
+ const trimmed = text.trim();
943
+ switch (spec.kind) {
944
+ case "toggle": return trimmed === "true" ? true : trimmed === "false" ? false : void 0;
945
+ case "select": return spec.options?.includes(trimmed) ? trimmed : void 0;
946
+ case "number": {
947
+ if (trimmed === "") return void 0;
948
+ const parsed = Number(trimmed);
949
+ return Number.isFinite(parsed) ? parsed : void 0;
950
+ }
951
+ case "text": return trimmed;
952
+ }
953
+ }
954
+ /** 把存储值格式化为草稿文本。 */
955
+ function formatValue(spec, value) {
956
+ if (spec.secret === true) return "";
957
+ if (value === void 0 || value === null) return "";
958
+ return String(value);
959
+ }
960
+ var MessagerCardController = class {
961
+ scope;
962
+ fields;
963
+ staged = /* @__PURE__ */ new Map();
964
+ listeners = /* @__PURE__ */ new Set();
965
+ saving = false;
966
+ failed = false;
967
+ cache = null;
968
+ constructor(scope, fields) {
969
+ this.scope = scope;
970
+ this.fields = fields;
971
+ scope.subscribe(() => this.invalidate());
972
+ }
973
+ /** 槽位注入面。 */
974
+ inject() {
975
+ return {
976
+ hooks: { messagerCard: {
977
+ getSnapshot: () => this.getSnapshot(),
978
+ subscribe: (listener) => {
979
+ this.listeners.add(listener);
980
+ return () => {
981
+ this.listeners.delete(listener);
982
+ };
983
+ }
984
+ } },
985
+ edit: (group, field, text) => this.stage(group, field, {
986
+ text,
987
+ clear: false
988
+ }),
989
+ reset: (group, field) => {
990
+ const spec = this.spec(group, field);
991
+ this.stage(group, field, {
992
+ text: formatValue(spec, this.baseValue(group, field)),
993
+ clear: true
994
+ });
995
+ },
996
+ save: () => {
997
+ this.save();
998
+ },
999
+ discard: () => {
1000
+ if (this.staged.size === 0 && !this.failed) return;
1001
+ this.staged.clear();
1002
+ this.failed = false;
1003
+ this.invalidate();
1004
+ },
1005
+ t: (key) => key
1006
+ };
1007
+ }
1008
+ /**
1009
+ * 读取缓存的快照。
1010
+ * 注意:useSyncExternalStore 要求 getSnapshot 在状态未变化时返回**同一引用**,
1011
+ * 否则 React 会以 #185(最大更新深度)崩溃 —— 因此快照按变更点惰性计算并缓存,
1012
+ * 只有 invalidate() 之后才重建。
1013
+ */
1014
+ getSnapshot() {
1015
+ return this.cache ??= this.compute();
1016
+ }
1017
+ compute() {
1018
+ const snapshot = this.scope.getSnapshot();
1019
+ const available = snapshot.status === "ready";
1020
+ const fields = {};
1021
+ for (const spec of this.fields) {
1022
+ const key = fieldKey(spec.group, spec.field);
1023
+ const staged = this.staged.get(key);
1024
+ const effective = this.effectiveValue(spec);
1025
+ if (staged === void 0) {
1026
+ fields[key] = {
1027
+ text: formatValue(spec, effective),
1028
+ overridden: this.userHas(spec),
1029
+ invalid: false
1030
+ };
1031
+ continue;
1032
+ }
1033
+ fields[key] = {
1034
+ text: staged.text,
1035
+ overridden: staged.clear ? false : this.userHas(spec),
1036
+ invalid: staged.clear ? false : parseDraft(spec, staged.text) === void 0
1037
+ };
1038
+ }
1039
+ const plan = this.plan();
1040
+ return {
1041
+ available,
1042
+ status: snapshot.status,
1043
+ mode: snapshot.mode ?? "",
1044
+ writable: snapshot.writable,
1045
+ dirty: plan.length > 0,
1046
+ invalid: plan.some((item) => item.run === void 0),
1047
+ saving: this.saving,
1048
+ failed: this.failed,
1049
+ fields
1050
+ };
1051
+ }
1052
+ effectiveValue(spec) {
1053
+ const group = this.groupValue(spec.group);
1054
+ if (group === void 0) return void 0;
1055
+ return group[spec.field];
1056
+ }
1057
+ groupValue(group) {
1058
+ const value = this.scope.getSnapshot().value;
1059
+ if (typeof value !== "object" || value === null) return void 0;
1060
+ const section = value[group];
1061
+ return typeof section === "object" && section !== null ? section : void 0;
1062
+ }
1063
+ baseValue(group, field) {
1064
+ const base = this.scope.getSnapshot().base;
1065
+ if (typeof base !== "object" || base === null) return void 0;
1066
+ const section = base[group];
1067
+ if (typeof section !== "object" || section === null) return void 0;
1068
+ return section[field];
1069
+ }
1070
+ userHas(spec) {
1071
+ const user = this.scope.getSnapshot().user;
1072
+ if (typeof user !== "object" || user === null) return false;
1073
+ const section = user[spec.group];
1074
+ return typeof section === "object" && section !== null && Object.hasOwn(section, spec.field);
1075
+ }
1076
+ stage(group, field, edit) {
1077
+ this.staged.set(fieldKey(group, field), edit);
1078
+ this.failed = false;
1079
+ this.invalidate();
1080
+ }
1081
+ spec(group, field) {
1082
+ const spec = this.fields.find((candidate) => candidate.group === group && candidate.field === field);
1083
+ if (spec === void 0) throw new Error(`messager card has no field ${group}.${field}`);
1084
+ return spec;
1085
+ }
1086
+ /**
1087
+ * 计算保存计划:把每个有效草稿翻译为一条嵌套路径写操作。
1088
+ * 逐字段而非整组合并:密钥字段只有被显式填写/重置时才生成 op,
1089
+ * 其余字段互不牵连(mutate 的 set 是整组替换,合并写会抹掉未回显的密钥)。
1090
+ */
1091
+ plan() {
1092
+ const ops = [];
1093
+ for (const [key, staged] of this.staged) {
1094
+ const [group, field] = key.split(".");
1095
+ const spec = this.spec(group, field);
1096
+ if (this.gatedOff(spec)) continue;
1097
+ if (spec.secret === true && staged.text.trim() === "" && !staged.clear) continue;
1098
+ if (staged.clear) {
1099
+ if (!spec.secret && !this.userHas(spec)) continue;
1100
+ ops.push({
1101
+ op: "unset",
1102
+ path: [group, field]
1103
+ });
1104
+ continue;
1105
+ }
1106
+ const desired = parseDraft(spec, staged.text);
1107
+ if (desired === void 0) return [{ run: void 0 }];
1108
+ if (spec.secret !== true && deepEqualJson(desired, this.effectiveValue(spec))) continue;
1109
+ ops.push({
1110
+ op: "set",
1111
+ path: [group, field],
1112
+ value: desired
1113
+ });
1114
+ }
1115
+ if (ops.length === 0) return [];
1116
+ const scope = this.scope;
1117
+ return [{ run: async () => {
1118
+ return await scope.writeOps(ops);
1119
+ } }];
1120
+ }
1121
+ /**
1122
+ * 门控判断:hiddenUnless 指定的开关字段当前(含草稿覆盖)不为 true 时,
1123
+ * 该字段不可见、草稿不参与保存。
1124
+ */
1125
+ gatedOff(spec) {
1126
+ const gate = spec.hiddenUnless;
1127
+ if (gate === void 0) return false;
1128
+ const gateKey = fieldKey(gate.group, gate.field);
1129
+ const gateSpec = this.spec(gate.group, gate.field);
1130
+ const staged = this.staged.get(gateKey);
1131
+ let gateValue;
1132
+ if (staged !== void 0 && !staged.clear) gateValue = parseDraft(gateSpec, staged.text);
1133
+ else gateValue = this.effectiveValue(gateSpec);
1134
+ return gateValue !== true;
1135
+ }
1136
+ async save() {
1137
+ const plan = this.plan();
1138
+ if (plan.length === 0 || this.saving || plan.some((item) => item.run === void 0)) return;
1139
+ this.saving = true;
1140
+ this.failed = false;
1141
+ this.invalidate();
1142
+ let landed = true;
1143
+ for (const item of plan) {
1144
+ if (item.run === void 0) continue;
1145
+ landed = await item.run() && landed;
1146
+ }
1147
+ if (landed) this.staged.clear();
1148
+ this.saving = false;
1149
+ this.failed = !landed;
1150
+ this.invalidate();
1151
+ }
1152
+ /** 失效缓存并通知订阅者(getSnapshot 下一次调用时重建)。 */
1153
+ invalidate() {
1154
+ this.cache = null;
1155
+ for (const listener of [...this.listeners]) listener();
1156
+ }
1157
+ };
1158
+ /** 深比较 JSON 形状数据(组对象比较)。 */
1159
+ function deepEqualJson(a, b) {
1160
+ if (a === b) return true;
1161
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1162
+ if (Array.isArray(a) || Array.isArray(b)) {
1163
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
1164
+ return a.every((entry, index) => deepEqualJson(entry, b[index]));
1165
+ }
1166
+ const left = a;
1167
+ const right = b;
1168
+ const keys = Object.keys(left);
1169
+ if (keys.length !== Object.keys(right).length) return false;
1170
+ return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
1171
+ }
1172
+ /**
1173
+ * 设置分区展示的字段清单。
1174
+ * label/hint 为**翻译键**(见 src/client/locales.ts),渲染层经 t() 取当前语言;
1175
+ * 键名与字典一致,缺键时 t() 原样返回键名(fail loud)。
1176
+ */
1177
+ const CARD_FIELDS = [
1178
+ {
1179
+ group: "triggers",
1180
+ field: "interaction",
1181
+ kind: "toggle",
1182
+ label: "field.triggers.interaction"
1183
+ },
1184
+ {
1185
+ group: "triggers",
1186
+ field: "completed",
1187
+ kind: "toggle",
1188
+ label: "field.triggers.completed"
1189
+ },
1190
+ {
1191
+ group: "triggers",
1192
+ field: "error",
1193
+ kind: "toggle",
1194
+ label: "field.triggers.error"
1195
+ },
1196
+ {
1197
+ group: "system",
1198
+ field: "enabled",
1199
+ kind: "toggle",
1200
+ label: "field.system.enabled"
1201
+ },
1202
+ {
1203
+ group: "system",
1204
+ field: "verbosity",
1205
+ kind: "select",
1206
+ label: "field.system.verbosity",
1207
+ options: [
1208
+ "minimal",
1209
+ "normal",
1210
+ "detailed"
1211
+ ]
1212
+ },
1213
+ {
1214
+ group: "system",
1215
+ field: "icon",
1216
+ kind: "text",
1217
+ label: "field.system.icon",
1218
+ hint: "hint.system.icon"
1219
+ },
1220
+ {
1221
+ group: "browser",
1222
+ field: "enabled",
1223
+ kind: "toggle",
1224
+ label: "field.browser.enabled"
1225
+ },
1226
+ {
1227
+ group: "browser",
1228
+ field: "onlyWhenHidden",
1229
+ kind: "toggle",
1230
+ label: "field.browser.onlyWhenHidden"
1231
+ },
1232
+ {
1233
+ group: "browser",
1234
+ field: "verbosity",
1235
+ kind: "select",
1236
+ label: "field.browser.verbosity",
1237
+ options: [
1238
+ "minimal",
1239
+ "normal",
1240
+ "detailed"
1241
+ ]
1242
+ },
1243
+ {
1244
+ group: "browser",
1245
+ field: "icon",
1246
+ kind: "text",
1247
+ label: "field.browser.icon"
1248
+ },
1249
+ {
1250
+ group: "feishu",
1251
+ field: "enabled",
1252
+ kind: "toggle",
1253
+ label: "field.feishu.enabled"
1254
+ },
1255
+ {
1256
+ group: "feishu",
1257
+ field: "webhookUrl",
1258
+ kind: "text",
1259
+ label: "field.feishu.webhookUrl",
1260
+ hiddenUnless: {
1261
+ group: "feishu",
1262
+ field: "enabled"
1263
+ }
1264
+ },
1265
+ {
1266
+ group: "feishu",
1267
+ field: "secret",
1268
+ kind: "text",
1269
+ label: "field.feishu.secret",
1270
+ hint: "hint.feishu.secret",
1271
+ secret: true,
1272
+ hiddenUnless: {
1273
+ group: "feishu",
1274
+ field: "enabled"
1275
+ }
1276
+ },
1277
+ {
1278
+ group: "feishu",
1279
+ field: "timeoutMs",
1280
+ kind: "number",
1281
+ label: "field.feishu.timeoutMs",
1282
+ hiddenUnless: {
1283
+ group: "feishu",
1284
+ field: "enabled"
1285
+ }
1286
+ },
1287
+ {
1288
+ group: "feishu",
1289
+ field: "verbosity",
1290
+ kind: "select",
1291
+ label: "field.feishu.verbosity",
1292
+ options: [
1293
+ "minimal",
1294
+ "normal",
1295
+ "detailed"
1296
+ ],
1297
+ hiddenUnless: {
1298
+ group: "feishu",
1299
+ field: "enabled"
1300
+ }
1301
+ },
1302
+ {
1303
+ group: "message",
1304
+ field: "titlePrefix",
1305
+ kind: "text",
1306
+ label: "field.message.titlePrefix",
1307
+ hint: "hint.message.titlePrefix"
1308
+ },
1309
+ {
1310
+ group: "message",
1311
+ field: "includeSessionTitle",
1312
+ kind: "toggle",
1313
+ label: "field.message.includeSessionTitle"
1314
+ },
1315
+ {
1316
+ group: "message",
1317
+ field: "guiUrl",
1318
+ kind: "text",
1319
+ label: "field.message.guiUrl"
1320
+ }
1321
+ ];
1322
+ //#endregion
1323
+ //#region src/client/fetch-scope.ts
1324
+ /** 创建 fetch 版 scope。 */
1325
+ function createFetchScope(fetcher) {
1326
+ let view = {
1327
+ status: "loading",
1328
+ value: void 0,
1329
+ user: void 0,
1330
+ base: void 0,
1331
+ writable: false,
1332
+ mode: "host"
1333
+ };
1334
+ const listeners = /* @__PURE__ */ new Set();
1335
+ const notify = () => {
1336
+ for (const listener of [...listeners]) listener();
1337
+ };
1338
+ const refresh = async () => {
1339
+ let next;
1340
+ try {
1341
+ next = await fetcher.get();
1342
+ } catch {
1343
+ view = {
1344
+ ...view,
1345
+ status: "unavailable"
1346
+ };
1347
+ notify();
1348
+ return;
1349
+ }
1350
+ view = {
1351
+ status: next.status,
1352
+ value: next.value,
1353
+ user: next.user,
1354
+ base: next.base,
1355
+ writable: next.writable,
1356
+ mode: next.mode,
1357
+ revision: next.revision
1358
+ };
1359
+ notify();
1360
+ };
1361
+ const scope = {
1362
+ getSnapshot: () => view,
1363
+ subscribe: (listener) => {
1364
+ listeners.add(listener);
1365
+ return () => {
1366
+ listeners.delete(listener);
1367
+ };
1368
+ },
1369
+ async set(field, value) {
1370
+ await scope.writeOps([{
1371
+ op: "set",
1372
+ path: [field],
1373
+ value
1374
+ }]);
1375
+ },
1376
+ async unset(field) {
1377
+ await scope.writeOps([{
1378
+ op: "unset",
1379
+ path: [field]
1380
+ }]);
1381
+ },
1382
+ async writeOps(ops) {
1383
+ const result = await fetcher.write({ ops });
1384
+ if (result.ok) await refresh();
1385
+ return result.ok;
1386
+ }
1387
+ };
1388
+ return {
1389
+ scope,
1390
+ refresh
1391
+ };
1392
+ }
1393
+ //#endregion
1394
+ //#region src/client/settings-form.tsx
1395
+ /** 分组标题的翻译键(键见 locales.ts;导出供字典一致性测试)。 */
1396
+ const GROUP_TITLE_KEYS = {
1397
+ triggers: "group.triggers",
1398
+ system: "group.system",
1399
+ browser: "group.browser",
1400
+ feishu: "group.feishu",
1401
+ message: "group.message"
1402
+ };
1403
+ const FIELD_STYLE = {
1404
+ display: "flex",
1405
+ flexDirection: "column",
1406
+ gap: 6,
1407
+ padding: "12px 0"
1408
+ };
1409
+ const FIELD_HEAD_STYLE = {
1410
+ display: "flex",
1411
+ alignItems: "center",
1412
+ gap: 8
1413
+ };
1414
+ const FIELD_LABEL_STYLE = {
1415
+ flex: 1,
1416
+ minWidth: 0,
1417
+ fontSize: 13,
1418
+ fontWeight: 500,
1419
+ lineHeight: 1.5,
1420
+ color: "var(--dsw-alias-label-primary)"
1421
+ };
1422
+ const BADGE_STYLE = {
1423
+ borderRadius: 999,
1424
+ padding: "1px 8px",
1425
+ fontSize: 11,
1426
+ lineHeight: "17px",
1427
+ whiteSpace: "nowrap",
1428
+ fontWeight: 500,
1429
+ background: "var(--dsw-alias-bg-module-platform)",
1430
+ color: "var(--dsw-alias-label-secondary)"
1431
+ };
1432
+ const RESET_STYLE = {
1433
+ border: "none",
1434
+ background: "none",
1435
+ padding: 0,
1436
+ font: "inherit",
1437
+ fontSize: 12,
1438
+ lineHeight: 1.5,
1439
+ color: "var(--dsw-alias-label-secondary)",
1440
+ cursor: "pointer"
1441
+ };
1442
+ const INPUT_STYLE = {
1443
+ height: 34,
1444
+ padding: "0 12px",
1445
+ border: "1px solid var(--dsw-alias-border-l2)",
1446
+ borderRadius: 8,
1447
+ background: "var(--dsw-alias-bg-layer-3)",
1448
+ font: "inherit",
1449
+ fontSize: 13,
1450
+ lineHeight: 1.5,
1451
+ color: "var(--dsw-alias-label-primary)"
1452
+ };
1453
+ const HINT_STYLE = {
1454
+ margin: 0,
1455
+ fontSize: 12,
1456
+ lineHeight: 1.5,
1457
+ color: "var(--dsw-alias-label-tertiary)"
1458
+ };
1459
+ const INVALID_STYLE = {
1460
+ ...HINT_STYLE,
1461
+ color: "var(--dsw-alias-label-error)"
1462
+ };
1463
+ const GROUP_HEADING_STYLE = {
1464
+ margin: "12px 0 0",
1465
+ fontSize: 12,
1466
+ fontWeight: 600,
1467
+ lineHeight: 1.5,
1468
+ color: "var(--dsw-alias-label-tertiary)"
1469
+ };
1470
+ const READ_ONLY_STYLE = {
1471
+ margin: "12px 0 0",
1472
+ fontSize: 12,
1473
+ lineHeight: 1.5,
1474
+ color: "var(--dsw-alias-label-tertiary)"
1475
+ };
1476
+ const FOOTER_STYLE = {
1477
+ display: "flex",
1478
+ alignItems: "center",
1479
+ justifyContent: "flex-end",
1480
+ gap: 8,
1481
+ padding: "12px 0 4px",
1482
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
1483
+ marginTop: 12
1484
+ };
1485
+ const FAILED_STYLE = {
1486
+ flex: 1,
1487
+ minWidth: 0,
1488
+ margin: 0,
1489
+ fontSize: 12,
1490
+ lineHeight: 1.5,
1491
+ color: "var(--dsw-alias-label-error)"
1492
+ };
1493
+ const BUTTON_BASE = {
1494
+ appearance: "none",
1495
+ border: "1px solid transparent",
1496
+ borderRadius: 8,
1497
+ padding: "5px 14px",
1498
+ font: "inherit",
1499
+ fontSize: 13,
1500
+ lineHeight: 1.5,
1501
+ cursor: "pointer"
1502
+ };
1503
+ const DISCARD_STYLE = {
1504
+ ...BUTTON_BASE,
1505
+ borderColor: "var(--dsw-alias-border-l2)",
1506
+ background: "none",
1507
+ color: "var(--dsw-alias-label-secondary)"
1508
+ };
1509
+ const SAVE_STYLE = {
1510
+ ...BUTTON_BASE,
1511
+ background: "var(--dsw-alias-label-primary)",
1512
+ color: "var(--dsw-alias-bg-layer-3)"
1513
+ };
1514
+ /** 渲染一个字段行(DSH ValueField 同款;toggle 标签与勾选框同一行)。 */
1515
+ function FieldRow({ spec, state, actions, t, disabled }) {
1516
+ const id = `dsh-messager-${spec.group}-${spec.field}`;
1517
+ const label = t(spec.label);
1518
+ const overriddenBadge = state.overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1519
+ style: {
1520
+ display: "inline-flex",
1521
+ alignItems: "center",
1522
+ gap: 8
1523
+ },
1524
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1525
+ style: BADGE_STYLE,
1526
+ children: t("badge.overridden")
1527
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1528
+ type: "button",
1529
+ style: RESET_STYLE,
1530
+ disabled,
1531
+ onClick: () => actions.reset(spec.group, spec.field),
1532
+ children: t("action.reset")
1533
+ })]
1534
+ });
1535
+ if (spec.kind === "toggle") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1536
+ style: {
1537
+ ...FIELD_STYLE,
1538
+ flexDirection: "row",
1539
+ alignItems: "center",
1540
+ flexWrap: "wrap",
1541
+ gap: 8,
1542
+ minHeight: 34
1543
+ },
1544
+ children: [
1545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1546
+ style: {
1547
+ ...FIELD_LABEL_STYLE,
1548
+ flex: "1 1 auto",
1549
+ cursor: disabled ? "default" : "pointer"
1550
+ },
1551
+ htmlFor: id,
1552
+ children: label
1553
+ }),
1554
+ overriddenBadge,
1555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1556
+ id,
1557
+ type: "checkbox",
1558
+ checked: state.text === "true",
1559
+ disabled,
1560
+ onChange: (event) => actions.edit(spec.group, spec.field, String(event.target.checked)),
1561
+ style: {
1562
+ flex: "none",
1563
+ accentColor: "var(--dsw-alias-brand-primary)"
1564
+ }
1565
+ }),
1566
+ spec.hint !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1567
+ style: {
1568
+ ...HINT_STYLE,
1569
+ flexBasis: "100%"
1570
+ },
1571
+ children: t(spec.hint)
1572
+ })
1573
+ ]
1574
+ });
1575
+ let control;
1576
+ if (spec.kind === "select") control = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1577
+ id,
1578
+ value: state.text,
1579
+ disabled,
1580
+ onChange: (event) => actions.edit(spec.group, spec.field, event.target.value),
1581
+ style: {
1582
+ ...INPUT_STYLE,
1583
+ cursor: "pointer"
1584
+ },
1585
+ children: spec.options?.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1586
+ value: option,
1587
+ children: option
1588
+ }, option))
1589
+ });
1590
+ else control = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1591
+ id,
1592
+ type: "text",
1593
+ inputMode: spec.kind === "number" ? "numeric" : void 0,
1594
+ value: state.text,
1595
+ placeholder: spec.secret === true ? t("hint.feishu.secret") : void 0,
1596
+ disabled,
1597
+ "aria-invalid": state.invalid,
1598
+ onChange: (event) => actions.edit(spec.group, spec.field, event.target.value),
1599
+ style: state.invalid ? {
1600
+ ...INPUT_STYLE,
1601
+ borderColor: "var(--dsw-alias-label-error)"
1602
+ } : INPUT_STYLE
1603
+ });
1604
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1605
+ style: FIELD_STYLE,
1606
+ children: [
1607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1608
+ style: FIELD_HEAD_STYLE,
1609
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1610
+ style: FIELD_LABEL_STYLE,
1611
+ htmlFor: id,
1612
+ children: label
1613
+ }), overriddenBadge]
1614
+ }),
1615
+ control,
1616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1617
+ style: state.invalid ? INVALID_STYLE : HINT_STYLE,
1618
+ children: state.invalid ? t("status.invalidField") : spec.hint === void 0 ? "" : t(spec.hint)
1619
+ })
1620
+ ]
1621
+ });
1622
+ }
1623
+ /** key → spec 查找表(渲染时用)。 */
1624
+ const FIELD_LOOKUP = Object.fromEntries(CARD_FIELDS.map((spec) => [`${spec.group}.${spec.field}`, spec]));
1625
+ /**
1626
+ * 设置表单体:全部字段分组 + 底部操作栏。
1627
+ * @param state - 控制器快照(useMessagerCard 的返回)。
1628
+ * @param actions - 控制器动作(edit/reset/save/discard)。
1629
+ * @param t - 翻译函数(键 → 当前语言文案)。
1630
+ */
1631
+ function MessagerSettingsForm({ state, actions, t }) {
1632
+ const groups = [...new Set(Object.keys(state.fields).map((key) => key.split(".")[0]))];
1633
+ const actionsDisabled = !state.writable || state.saving;
1634
+ const blocked = !state.dirty || state.invalid || state.saving;
1635
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1636
+ !state.writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1637
+ style: READ_ONLY_STYLE,
1638
+ role: "status",
1639
+ children: t("status.readOnly")
1640
+ }),
1641
+ groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1642
+ style: GROUP_HEADING_STYLE,
1643
+ children: t(GROUP_TITLE_KEYS[group] ?? group)
1644
+ }), Object.entries(state.fields).filter(([key]) => key.startsWith(`${group}.`)).map(([key, field]) => {
1645
+ key.slice(key.indexOf(".") + 1);
1646
+ const spec = FIELD_LOOKUP[key];
1647
+ if (spec === void 0) return null;
1648
+ if (isFieldGated(spec, state.fields)) return null;
1649
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FieldRow, {
1650
+ spec,
1651
+ state: field,
1652
+ actions,
1653
+ t,
1654
+ disabled: actionsDisabled
1655
+ }, key);
1656
+ })] }, group)),
1657
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1658
+ style: FOOTER_STYLE,
1659
+ children: [
1660
+ state.failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1661
+ style: FAILED_STYLE,
1662
+ role: "status",
1663
+ children: t("status.saveFailed")
1664
+ }),
1665
+ state.invalid && !state.failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1666
+ style: FAILED_STYLE,
1667
+ role: "status",
1668
+ children: t("status.invalidInput")
1669
+ }),
1670
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1671
+ type: "button",
1672
+ style: {
1673
+ ...DISCARD_STYLE,
1674
+ ...!state.dirty && !state.failed || state.saving ? {
1675
+ opacity: .4,
1676
+ cursor: "default"
1677
+ } : {}
1678
+ },
1679
+ disabled: !state.dirty || state.saving,
1680
+ onClick: actions.discard,
1681
+ children: t("action.discard")
1682
+ }),
1683
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1684
+ type: "button",
1685
+ style: {
1686
+ ...SAVE_STYLE,
1687
+ ...blocked ? {
1688
+ opacity: .4,
1689
+ cursor: "default"
1690
+ } : {}
1691
+ },
1692
+ disabled: blocked,
1693
+ onClick: actions.save,
1694
+ children: state.saving ? t("action.saving") : t("action.save")
1695
+ })
1696
+ ]
1697
+ })
1698
+ ] });
1699
+ }
1700
+ //#endregion
1701
+ //#region src/client/section.tsx
1702
+ const SECTION_STYLE = {
1703
+ display: "flex",
1704
+ flexDirection: "column",
1705
+ gap: 12,
1706
+ maxWidth: 640
1707
+ };
1708
+ const TITLE_STYLE = {
1709
+ margin: 0,
1710
+ fontSize: 18,
1711
+ fontWeight: 600,
1712
+ lineHeight: 1.4,
1713
+ color: "var(--dsw-alias-label-primary)"
1714
+ };
1715
+ const DESCRIPTION_STYLE = {
1716
+ margin: 0,
1717
+ fontSize: 13,
1718
+ lineHeight: 1.6,
1719
+ color: "var(--dsw-alias-label-tertiary)"
1720
+ };
1721
+ const UNAVAILABLE_STYLE = {
1722
+ margin: 0,
1723
+ fontSize: 13,
1724
+ lineHeight: 1.6,
1725
+ color: "var(--dsw-alias-label-tertiary)"
1726
+ };
1727
+ /**
1728
+ * dsh-messager 设置分区(设置页「通知&信使」)。
1729
+ * @param props - 槽位 owner props + 注入面(useMessagerCard + 动作 + t)。
1730
+ */
1731
+ function MessagerSection(props) {
1732
+ const state = props.useMessagerCard((snapshot) => snapshot);
1733
+ const { edit, reset, save, discard, t } = props;
1734
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1735
+ style: SECTION_STYLE,
1736
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1737
+ style: TITLE_STYLE,
1738
+ children: t("nav")
1739
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1740
+ style: DESCRIPTION_STYLE,
1741
+ children: t("section.description")
1742
+ })] }), state.available ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessagerSettingsForm, {
1743
+ state,
1744
+ actions: {
1745
+ edit,
1746
+ reset,
1747
+ save,
1748
+ discard
1749
+ },
1750
+ t
1751
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1752
+ style: UNAVAILABLE_STYLE,
1753
+ role: "status",
1754
+ children: t("status.unavailable")
1755
+ })]
1756
+ });
1757
+ }
1758
+ //#endregion
1759
+ //#region src/client/locales.ts
1760
+ const zh = {
1761
+ nav: "通知&信使",
1762
+ "section.description": "会话交互 / 任务完成 / 出错时的通知推送:系统通知、浏览器通知、飞书机器人。",
1763
+ "group.triggers": "触发时机",
1764
+ "group.system": "系统通知",
1765
+ "group.browser": "浏览器通知",
1766
+ "group.feishu": "第三方推送",
1767
+ "group.message": "消息内容",
1768
+ "field.triggers.interaction": "需要交互时通知",
1769
+ "field.triggers.completed": "任务完成时通知",
1770
+ "field.triggers.error": "任务出错时通知",
1771
+ "field.system.enabled": "启用系统通知",
1772
+ "field.system.icon": "图标路径",
1773
+ "field.system.verbosity": "内容繁复度",
1774
+ "field.browser.enabled": "启用浏览器通知",
1775
+ "field.browser.icon": "图标 URL",
1776
+ "field.browser.onlyWhenHidden": "仅页面隐藏时通知",
1777
+ "field.browser.verbosity": "内容繁复度",
1778
+ "field.feishu.enabled": "飞书机器人",
1779
+ "field.feishu.webhookUrl": "Webhook 地址",
1780
+ "field.feishu.secret": "签名密钥",
1781
+ "field.feishu.timeoutMs": "请求超时(毫秒)",
1782
+ "field.feishu.verbosity": "内容繁复度",
1783
+ "field.message.titlePrefix": "标题前缀",
1784
+ "field.message.includeSessionTitle": "正文包含会话标题",
1785
+ "field.message.guiUrl": "打开链接地址",
1786
+ "hint.system.icon": "node-notifier 需要文件绝对路径",
1787
+ "hint.feishu.secret": "留空不修改;重置可清除已存密钥",
1788
+ "hint.message.titlePrefix": "如 [DSH]",
1789
+ "action.save": "保存",
1790
+ "action.saving": "保存中…",
1791
+ "action.discard": "放弃修改",
1792
+ "action.reset": "重置",
1793
+ "badge.overridden": "已覆盖",
1794
+ "status.readOnly": "设置文档当前为只读。",
1795
+ "status.saveFailed": "保存失败,请重试。",
1796
+ "status.invalidInput": "存在无效输入,请修正后再保存。",
1797
+ "status.invalidField": "无效输入(数字/选项),请修正后再保存。",
1798
+ "status.unavailable": "配置通道不可用:配置路由未就绪(host 插件未加载或 webServer 服务缺失)。"
1799
+ };
1800
+ const en = {
1801
+ nav: "Messenger",
1802
+ "section.description": "Notifications for interaction, task completion and errors: system toast, browser notification, Feishu bot.",
1803
+ "group.triggers": "Triggers",
1804
+ "group.system": "System",
1805
+ "group.browser": "Browser",
1806
+ "group.feishu": "Third-party",
1807
+ "group.message": "Message",
1808
+ "field.triggers.interaction": "Notify when interaction is needed",
1809
+ "field.triggers.completed": "Notify when a task completes",
1810
+ "field.triggers.error": "Notify when a task errors",
1811
+ "field.system.enabled": "Enable system notifications",
1812
+ "field.system.icon": "Icon path",
1813
+ "field.system.verbosity": "Verbosity",
1814
+ "field.browser.enabled": "Enable browser notifications",
1815
+ "field.browser.icon": "Icon URL",
1816
+ "field.browser.onlyWhenHidden": "Notify only when the page is hidden",
1817
+ "field.browser.verbosity": "Verbosity",
1818
+ "field.feishu.enabled": "Feishu bot",
1819
+ "field.feishu.webhookUrl": "Webhook URL",
1820
+ "field.feishu.secret": "Signing secret",
1821
+ "field.feishu.timeoutMs": "Request timeout (ms)",
1822
+ "field.feishu.verbosity": "Verbosity",
1823
+ "field.message.titlePrefix": "Title prefix",
1824
+ "field.message.includeSessionTitle": "Include session title in the body",
1825
+ "field.message.guiUrl": "Open link URL",
1826
+ "hint.system.icon": "node-notifier requires an absolute file path",
1827
+ "hint.feishu.secret": "Leave blank to keep the stored secret; Reset clears it",
1828
+ "hint.message.titlePrefix": "e.g. [DSH]",
1829
+ "action.save": "Save",
1830
+ "action.saving": "Saving…",
1831
+ "action.discard": "Discard",
1832
+ "action.reset": "Reset",
1833
+ "badge.overridden": "Overridden",
1834
+ "status.readOnly": "The settings document is read-only.",
1835
+ "status.saveFailed": "Save failed, please retry.",
1836
+ "status.invalidInput": "There are invalid inputs, fix them before saving.",
1837
+ "status.invalidField": "Invalid input (number/option), fix it before saving.",
1838
+ "status.unavailable": "Config channel unavailable: the config route is not ready (host plugin not loaded or webServer service missing)."
1839
+ };
1840
+ //#endregion
1841
+ //#region src/client/index.ts
1842
+ const name = "dsh-messager";
1843
+ /** locale 字典命名空间(与 section 的 locale 声明一致)。 */
1844
+ const LOCALE_NS = "dsh-messager";
1845
+ /** 依赖的客户端服务:会话列表、远程事件(document-updated)、槽位、locale。 */
1846
+ const inject = [
1847
+ "sessions",
1848
+ "remote",
1849
+ "slots",
1850
+ "locale"
1851
+ ];
1852
+ /** localStorage 跨标签页去重键前缀。 */
1853
+ const STORAGE_PREFIX = "dsh-messager:notified:";
1854
+ /** 浏览器通知投递器。 */
1855
+ var BrowserNotifier = class {
1856
+ config;
1857
+ /** `${kind}:${sessionId}` → 上次投递时间戳(内存冷却)。 */
1858
+ cooldowns = /* @__PURE__ */ new Map();
1859
+ constructor(config) {
1860
+ this.config = config;
1861
+ }
1862
+ dispose() {
1863
+ this.cooldowns.clear();
1864
+ }
1865
+ /** 列表快照变化 → 通知。 */
1866
+ onListChange(previous, next) {
1867
+ const config = this.config.get();
1868
+ if (!config.browser.enabled) return;
1869
+ if (typeof Notification === "undefined") return;
1870
+ if (Notification.permission !== "granted") return;
1871
+ if (config.browser.onlyWhenHidden && document.visibilityState !== "hidden") return;
1872
+ const notices = diffSessionSummaries(previous.byId, next.byId, next.current);
1873
+ for (const notice of notices) {
1874
+ if (!this.allow(notice, config)) continue;
1875
+ this.show(notice, config);
1876
+ }
1877
+ }
1878
+ /** 冷却 + 跨标签页去重。 */
1879
+ allow(notice, config) {
1880
+ const now = Date.now();
1881
+ const key = `${notice.kind}:${notice.sessionId}`;
1882
+ const cooldownMs = config.dedup.interactionCooldownMs;
1883
+ const last = this.cooldowns.get(key);
1884
+ if (last !== void 0 && now - last < cooldownMs) return false;
1885
+ try {
1886
+ const storageKey = STORAGE_PREFIX + key;
1887
+ const raw = window.localStorage.getItem(storageKey);
1888
+ if (raw !== null && now - Number(raw) < cooldownMs) return false;
1889
+ window.localStorage.setItem(storageKey, String(now));
1890
+ } catch {}
1891
+ this.cooldowns.set(key, now);
1892
+ return true;
1893
+ }
1894
+ show(notice, config) {
1895
+ const verbosity = config.browser.verbosity;
1896
+ const notification = new Notification(this.titleOf(notice, config), {
1897
+ ...verbosity === "minimal" ? {} : { body: this.bodyOf(notice, config, verbosity) },
1898
+ ...config.browser.icon === void 0 ? {} : { icon: config.browser.icon },
1899
+ tag: `dsh-messager:${notice.kind}:${notice.sessionId}`
1900
+ });
1901
+ notification.onclick = () => {
1902
+ window.focus();
1903
+ notification.close();
1904
+ };
1905
+ }
1906
+ titleOf(notice, config) {
1907
+ let base;
1908
+ if (notice.kind === "interaction") base = notice.interaction === "approval" ? "需要交互:等待审批" : notice.interaction === "plan-review" ? "需要交互:计划待审" : "需要交互:等待回答";
1909
+ else base = "任务完成";
1910
+ const prefix = config.message.titlePrefix;
1911
+ return prefix === void 0 || prefix === "" ? base : `${prefix} ${base}`;
1912
+ }
1913
+ bodyOf(notice, config, verbosity) {
1914
+ const lines = [];
1915
+ if (verbosity === "minimal") return "";
1916
+ if (config.message.includeSessionTitle && notice.title !== void 0) lines.push(`会话:${notice.title}`);
1917
+ if (verbosity === "detailed") lines.push(`打开:${config.message.guiUrl}`);
1918
+ return lines.join("\n");
1919
+ }
1920
+ };
1921
+ function apply(ctx) {
1922
+ const config = new ClientConfig();
1923
+ config.refresh();
1924
+ const fetchScope = createFetchScope({
1925
+ get: async () => {
1926
+ const response = await fetch(CONFIG_PATH, { headers: { accept: "application/json" } });
1927
+ if (!response.ok) throw new Error(`config route responded ${response.status}`);
1928
+ return await response.json();
1929
+ },
1930
+ write: async (body) => {
1931
+ try {
1932
+ const response = await fetch(CONFIG_PATH, {
1933
+ method: "POST",
1934
+ headers: { "content-type": "application/json" },
1935
+ body: JSON.stringify(body)
1936
+ });
1937
+ const result = await response.json();
1938
+ return {
1939
+ ok: response.ok && result.ok === true,
1940
+ error: result.error
1941
+ };
1942
+ } catch (error) {
1943
+ return {
1944
+ ok: false,
1945
+ error: error instanceof Error ? error.message : String(error)
1946
+ };
1947
+ }
1948
+ }
1949
+ });
1950
+ fetchScope.refresh();
1951
+ const offRemote = ctx.remote.$on("settings/document-updated", () => {
1952
+ config.refresh();
1953
+ fetchScope.refresh();
1954
+ });
1955
+ ctx.effect(() => () => offRemote?.(), "dsh-messager: settings invalidation");
1956
+ const notifier = new BrowserNotifier(config);
1957
+ ctx.effect(() => () => notifier.dispose(), "dsh-messager: browser notifier");
1958
+ let previous = ctx.sessions.list.getSnapshot();
1959
+ const offList = ctx.sessions.list.subscribe(() => {
1960
+ const next = ctx.sessions.list.getSnapshot();
1961
+ notifier.onListChange(previous, next);
1962
+ previous = next;
1963
+ });
1964
+ ctx.effect(() => () => offList(), "dsh-messager: sessions subscription");
1965
+ const controller = new MessagerCardController(fetchScope.scope, CARD_FIELDS);
1966
+ ctx.effect(() => ctx.locale.register(LOCALE_NS, {
1967
+ zh,
1968
+ en
1969
+ }), "dsh-messager: locale dictionaries");
1970
+ const t = ctx.locale.bind(LOCALE_NS);
1971
+ ctx.slots.inject("settings.section", function* () {
1972
+ const existing = ctx.slots.entries("settings.section");
1973
+ const agentPresets = existing.find((entry) => entry.options.id === "agent-presets");
1974
+ const maxOrder = existing.reduce((max, entry) => Math.max(max, entry.options.order ?? 0), 0);
1975
+ const order = agentPresets !== void 0 ? (agentPresets.options.order ?? 0) + 1 : existing.length > 0 ? maxOrder + 1 : 1e3;
1976
+ const face = controller.inject();
1977
+ yield ctx.slots.register({
1978
+ name: "settings.section",
1979
+ id: "dsh-messager",
1980
+ order,
1981
+ label: () => t("nav"),
1982
+ locale: LOCALE_NS,
1983
+ inject: () => ({
1984
+ ...face,
1985
+ t
1986
+ })
1987
+ }, MessagerSection);
1988
+ });
1989
+ if (typeof Notification !== "undefined" && Notification.permission === "default") Notification.requestPermission();
1990
+ ctx.logger.info("[dsh-messager] client loaded");
1991
+ }
1992
+ //#endregion
1993
+ exports.LOCALE_NS = LOCALE_NS;
1994
+ exports.apply = apply;
1995
+ exports.inject = inject;
1996
+ exports.name = name;
1997
+ return module.exports;
1998
+ }
1999
+ });
2000
+
2001
+ //# sourceMappingURL=client.js.map