dsh-memento 0.5.12 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README-es.md +21 -7
- package/README-hi.md +22 -8
- package/README-pt.md +21 -7
- package/README-zh.md +21 -7
- package/README.md +20 -7
- package/client/client.js +204 -19
- package/client/globals.d.ts +27 -0
- package/index.mjs +113 -38
- package/package.json +14 -7
- package/types.d.ts +8 -6
package/client/client.js
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
if (typeof window === 'undefined' || !window.__ModuleLoader__ || !window.__ModuleLoader__.load) return
|
|
19
19
|
window.__ModuleLoader__.load({
|
|
20
20
|
id: 'dsh-memento',
|
|
21
|
+
/**
|
|
22
|
+
* 物化回调。require 是宿主模块系统(react 等平台内置模块)——本仓包里没有
|
|
23
|
+
* 这些模块的类型面,故按不透明函数面声明,返回值交给消费点自行收窄。
|
|
24
|
+
* @param {(id: string) => any} require - 宿主模块解析函数。
|
|
25
|
+
* @returns {{name: string, inject: string[], apply: (ctx: any) => void}} 客户端插件定义。
|
|
26
|
+
*/
|
|
21
27
|
factory: function (require) {
|
|
22
28
|
const react = require('react')
|
|
23
29
|
/** createElement 简写(宿主平台内置 react,无需构建期 JSX 编译)。 */
|
|
@@ -26,9 +32,14 @@
|
|
|
26
32
|
const PANEL_ID = 'dsh-memento-panel'
|
|
27
33
|
|
|
28
34
|
/** 本页已渲染的悬浮入口按钮引用(设置页 panel.enabled 开关即时切换用)。 */
|
|
35
|
+
/** @type {HTMLElement | null} */
|
|
29
36
|
let panelOpenButton = null
|
|
30
37
|
|
|
31
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* 悬浮窗开关的即时生效面:切显隐;开启时按钮缺失(上次探测为关)则重建。
|
|
40
|
+
* @param {boolean} visible - 目标可见性。
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
32
43
|
function setPanelButtonVisible(visible) {
|
|
33
44
|
if (panelOpenButton === null) panelOpenButton = document.getElementById('mem-open')
|
|
34
45
|
if (panelOpenButton !== null) {
|
|
@@ -38,7 +49,27 @@ function setPanelButtonVisible(visible) {
|
|
|
38
49
|
}
|
|
39
50
|
}
|
|
40
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 面板文案模板。
|
|
54
|
+
* @typedef {object} PanelStrings
|
|
55
|
+
* @property {string} open
|
|
56
|
+
* @property {string} title
|
|
57
|
+
* @property {string} refresh
|
|
58
|
+
* @property {string} close
|
|
59
|
+
* @property {string} filter
|
|
60
|
+
* @property {string} empty
|
|
61
|
+
* @property {(shown: number, total: number) => string} truncated
|
|
62
|
+
* @property {(n: number) => string} groupCount
|
|
63
|
+
* @property {string} budgets
|
|
64
|
+
* @property {string} audit
|
|
65
|
+
* @property {string} loading
|
|
66
|
+
* @property {string} auditEmpty
|
|
67
|
+
* @property {string} proposals
|
|
68
|
+
* @property {string} proposalsEmpty
|
|
69
|
+
* @property {(message: string) => string} loadFailed
|
|
70
|
+
*/
|
|
41
71
|
/** 面板文案(en 源文 / zh 译文;语言来自 /api/memento/entries 响应的 language 字段,缺省 en)。 */
|
|
72
|
+
/** @type {Record<string, PanelStrings>} */
|
|
42
73
|
const STRINGS = {
|
|
43
74
|
en: {
|
|
44
75
|
open: '🧠 Memory',
|
|
@@ -76,6 +107,46 @@ const STRINGS = {
|
|
|
76
107
|
},
|
|
77
108
|
}
|
|
78
109
|
|
|
110
|
+
/**
|
|
111
|
+
* 面板数据行。只声明面板真正消费的字段:服务端返回更宽的条目行,
|
|
112
|
+
* 面板只读展示 track/scope/text 并经预算表读 used/limit。
|
|
113
|
+
* @typedef {object} PanelEntryRow
|
|
114
|
+
* @property {string} id
|
|
115
|
+
* @property {string} track
|
|
116
|
+
* @property {string} scope
|
|
117
|
+
* @property {string} text
|
|
118
|
+
* @property {string} [workspaceKey]
|
|
119
|
+
* @property {string} [agentKey]
|
|
120
|
+
* @property {number} [createdAt]
|
|
121
|
+
* @property {number} [recallCount]
|
|
122
|
+
*/
|
|
123
|
+
/**
|
|
124
|
+
* 预算用量行(/api/memento/entries 的 budgets[])。
|
|
125
|
+
* @typedef {object} PanelBudgetRow
|
|
126
|
+
* @property {string} track
|
|
127
|
+
* @property {string} scope
|
|
128
|
+
* @property {number} used
|
|
129
|
+
* @property {number} limit
|
|
130
|
+
*/
|
|
131
|
+
/**
|
|
132
|
+
* 审计行(/api/memento/audit 的 rows[])。
|
|
133
|
+
* @typedef {object} PanelAuditRow
|
|
134
|
+
* @property {number} ts
|
|
135
|
+
* @property {string} action
|
|
136
|
+
* @property {string} [track]
|
|
137
|
+
* @property {string} [scope]
|
|
138
|
+
* @property {string} [outcome]
|
|
139
|
+
* @property {string} [source]
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* 待审批提案(/api/memento/proposals 的 proposals[])。
|
|
143
|
+
* @typedef {object} PanelProposalRow
|
|
144
|
+
* @property {string} id
|
|
145
|
+
* @property {string} track
|
|
146
|
+
* @property {string} scope
|
|
147
|
+
* @property {string} text
|
|
148
|
+
* @property {number} createdAt
|
|
149
|
+
*/
|
|
79
150
|
/**
|
|
80
151
|
* 启动探测:panel.enabled=false 时入口按钮不渲染(设置面板可随时改回);
|
|
81
152
|
* 探测失败按开启处理,行为与未引入开关前的版本一致。
|
|
@@ -102,6 +173,11 @@ async function bootPanel() {
|
|
|
102
173
|
installPanel(state)
|
|
103
174
|
}
|
|
104
175
|
|
|
176
|
+
/**
|
|
177
|
+
* 挂载面板(入口按钮 + 抽屉)。
|
|
178
|
+
* @param {{enabled: boolean, language: string}} state - 启动探测结果。
|
|
179
|
+
* @returns {void}
|
|
180
|
+
*/
|
|
105
181
|
function installPanel(state) {
|
|
106
182
|
if (document.getElementById(PANEL_ID)) return
|
|
107
183
|
let S = STRINGS[state.language] ?? STRINGS.en
|
|
@@ -155,13 +231,17 @@ function installPanel(state) {
|
|
|
155
231
|
document.head.appendChild(style)
|
|
156
232
|
|
|
157
233
|
const body = document.getElementById('mem-body')
|
|
158
|
-
const filter = document.getElementById('mem-filter')
|
|
234
|
+
const filter = /** @type {HTMLInputElement} */ (document.getElementById('mem-filter'))
|
|
159
235
|
const titleLabel = document.getElementById('mem-title')
|
|
160
236
|
const refreshBtn = document.getElementById('mem-refresh')
|
|
161
237
|
const closeBtn = document.getElementById('mem-close')
|
|
162
|
-
const filterInput = document.getElementById('mem-filter')
|
|
238
|
+
const filterInput = /** @type {HTMLInputElement} */ (document.getElementById('mem-filter'))
|
|
163
239
|
|
|
164
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* 按服务端 language 切换文案并刷新静态标签(语言随配置,运行期不变)。
|
|
242
|
+
* @param {string} language - 服务端 language 字段。
|
|
243
|
+
* @returns {void}
|
|
244
|
+
*/
|
|
165
245
|
const applyLanguage = (language) => {
|
|
166
246
|
S = STRINGS[language] ?? STRINGS.en
|
|
167
247
|
openBtn.textContent = S.open
|
|
@@ -172,6 +252,7 @@ function installPanel(state) {
|
|
|
172
252
|
filterInput.placeholder = S.filter
|
|
173
253
|
}
|
|
174
254
|
|
|
255
|
+
/** @type {PanelEntryRow[]} */
|
|
175
256
|
let entries = []
|
|
176
257
|
let lastFilter = ''
|
|
177
258
|
let lastTotal = 0
|
|
@@ -203,6 +284,11 @@ function installPanel(state) {
|
|
|
203
284
|
body.innerHTML = html
|
|
204
285
|
}
|
|
205
286
|
|
|
287
|
+
/**
|
|
288
|
+
* 渲染预算条与两个异步槽位骨架。
|
|
289
|
+
* @param {PanelBudgetRow[]} budgets - 预算用量行。
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
206
292
|
const renderBudget = (budgets) => {
|
|
207
293
|
if (!Array.isArray(budgets) || budgets.length === 0) return
|
|
208
294
|
let html = `<div class="mem-group">${S.budgets}</div>`
|
|
@@ -217,6 +303,11 @@ function installPanel(state) {
|
|
|
217
303
|
body.insertAdjacentHTML('beforeend', html)
|
|
218
304
|
}
|
|
219
305
|
|
|
306
|
+
/**
|
|
307
|
+
* 渲染审计槽位。
|
|
308
|
+
* @param {PanelAuditRow[]} rows - 审计行。
|
|
309
|
+
* @returns {void}
|
|
310
|
+
*/
|
|
220
311
|
const renderAudit = (rows) => {
|
|
221
312
|
const slot = document.getElementById('mem-audit-slot')
|
|
222
313
|
if (slot === null) return
|
|
@@ -228,6 +319,11 @@ function installPanel(state) {
|
|
|
228
319
|
`<div class="mem-audit">${new Date(row.ts).toLocaleString()} ${escapeHtml(row.action)}${row.track ? ` ${escapeHtml(row.track)}/${escapeHtml(row.scope)}` : ''} · ${escapeHtml(row.outcome ?? '')} · ${escapeHtml(row.source ?? '')}</div>`).join('')
|
|
229
320
|
}
|
|
230
321
|
|
|
322
|
+
/**
|
|
323
|
+
* 渲染待审批提案槽位。
|
|
324
|
+
* @param {PanelProposalRow[]} proposals - 待审批提案。
|
|
325
|
+
* @returns {void}
|
|
326
|
+
*/
|
|
231
327
|
const renderProposals = (proposals) => {
|
|
232
328
|
const slot = document.getElementById('mem-proposal-slot')
|
|
233
329
|
if (slot === null) return
|
|
@@ -276,12 +372,18 @@ function installPanel(state) {
|
|
|
276
372
|
})
|
|
277
373
|
}
|
|
278
374
|
|
|
375
|
+
/**
|
|
376
|
+
* HTML 转义(面板所有插值文本都经此,避免记忆内容当标记注入)。
|
|
377
|
+
* @param {unknown} value - 待转义值(非字符串先 String())。
|
|
378
|
+
* @returns {string} 转义后的文本。
|
|
379
|
+
*/
|
|
279
380
|
function escapeHtml(value) {
|
|
280
381
|
return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"')
|
|
281
382
|
}
|
|
282
383
|
|
|
283
384
|
// ── 宿主设置页(settings.section 一级项,id = dsh-memento)──────────────
|
|
284
385
|
/** 卡片文案(en 源文 / zh 译文;语言跟随 namespace value.language,保存后即时切换)。 */
|
|
386
|
+
/** @type {Record<string, Record<string, string>>} */
|
|
285
387
|
const CARD_STRINGS = {
|
|
286
388
|
en: {
|
|
287
389
|
title: 'dsh-memento memory',
|
|
@@ -392,9 +494,14 @@ function escapeHtml(value) {
|
|
|
392
494
|
/** 顶层字段名(保存按顶层聚合:scope.set(topField, 合并值),不依赖点路径写入面)。 */
|
|
393
495
|
const POLICIES = ['ask', 'auto', 'off']
|
|
394
496
|
|
|
395
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* 简版快照 store(useSyncExternalStore 形状:{getSnapshot, subscribe}+set)。
|
|
499
|
+
* @param {Record<string, any>} initial - 顶层设置值。
|
|
500
|
+
* @returns {{getSnapshot: () => Record<string, any>, subscribe: (fn: () => void) => () => void, set: (next: Record<string, any>) => void}} store。
|
|
501
|
+
*/
|
|
396
502
|
function createSnapshotStore(initial) {
|
|
397
503
|
let snapshot = initial
|
|
504
|
+
/** @type {Set<() => void>} */
|
|
398
505
|
const listeners = new Set()
|
|
399
506
|
return {
|
|
400
507
|
getSnapshot() { return snapshot },
|
|
@@ -403,7 +510,12 @@ function escapeHtml(value) {
|
|
|
403
510
|
}
|
|
404
511
|
}
|
|
405
512
|
|
|
406
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* 读点路径('a.b' → obj?.a?.b)。
|
|
515
|
+
* @param {any} obj - 起点对象。
|
|
516
|
+
* @param {string} path - 点路径。
|
|
517
|
+
* @returns {any} 路径值(缺失为 undefined)。
|
|
518
|
+
*/
|
|
407
519
|
function pathValue(obj, path) {
|
|
408
520
|
let cursor = obj
|
|
409
521
|
for (const key of path.split('.')) {
|
|
@@ -413,7 +525,12 @@ function escapeHtml(value) {
|
|
|
413
525
|
return cursor
|
|
414
526
|
}
|
|
415
527
|
|
|
416
|
-
/**
|
|
528
|
+
/**
|
|
529
|
+
* 判定子字段是否被用户层覆盖(user 层子路径 hasOwn)。
|
|
530
|
+
* @param {any} user - 设置用户层。
|
|
531
|
+
* @param {string} path - 点路径。
|
|
532
|
+
* @returns {boolean} 用户层是否显式写了该子字段。
|
|
533
|
+
*/
|
|
417
534
|
function pathStored(user, path) {
|
|
418
535
|
const keys = path.split('.')
|
|
419
536
|
const last = keys.pop()
|
|
@@ -425,7 +542,12 @@ function escapeHtml(value) {
|
|
|
425
542
|
return cursor !== null && typeof cursor === 'object' && Object.hasOwn(cursor, last)
|
|
426
543
|
}
|
|
427
544
|
|
|
428
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* 深合并(草稿子树 → 当前顶层值;数组与标量直接替换)。
|
|
547
|
+
* @param {any} base - 当前值。
|
|
548
|
+
* @param {any} patch - 草稿子树。
|
|
549
|
+
* @returns {any} 合并结果。
|
|
550
|
+
*/
|
|
429
551
|
function deepMerge(base, patch) {
|
|
430
552
|
if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) return patch
|
|
431
553
|
const out = { ...(base !== null && typeof base === 'object' && !Array.isArray(base) ? base : {}) }
|
|
@@ -463,7 +585,13 @@ function escapeHtml(value) {
|
|
|
463
585
|
const SPEC_BY_PATH = new Map(FIELD_SPECS.map((spec) => [spec.path, spec]))
|
|
464
586
|
const RELOAD_PATHS = new Set(['snapshotOrder'])
|
|
465
587
|
|
|
466
|
-
/**
|
|
588
|
+
/**
|
|
589
|
+
* 草稿文本 → 顶层字段写入计划;无法解析的草稿返回 undefined(阻塞保存)。
|
|
590
|
+
* @param {{path: string, kind: string, key: string, choices?: string[]}} spec - 子字段规格。
|
|
591
|
+
* @param {string} text - 控件里的草稿文本。
|
|
592
|
+
* @param {any} currentValue - 当前快照值(text 类字段用于回退判定)。
|
|
593
|
+
* @returns {{ok: true, value: any} | {ok: false}} 解析结果。
|
|
594
|
+
*/
|
|
467
595
|
function parseDraftText(spec, text, currentValue) {
|
|
468
596
|
if (spec.kind === 'bool') {
|
|
469
597
|
if (text === 'true') return { ok: true, value: true }
|
|
@@ -479,6 +607,7 @@ function escapeHtml(value) {
|
|
|
479
607
|
return trimmed !== '' && Number.isFinite(parsed) && Number.isInteger(parsed) ? { ok: true, value: parsed } : { ok: false }
|
|
480
608
|
}
|
|
481
609
|
if (spec.kind === 'policies') {
|
|
610
|
+
/** @type {Record<string, string>} */
|
|
482
611
|
const next = {}
|
|
483
612
|
for (const rawLine of text.split('\n')) {
|
|
484
613
|
const line = rawLine.trim()
|
|
@@ -497,14 +626,22 @@ function escapeHtml(value) {
|
|
|
497
626
|
return { ok: true, value: text }
|
|
498
627
|
}
|
|
499
628
|
|
|
500
|
-
/**
|
|
629
|
+
/**
|
|
630
|
+
* 子字段格式化(快照值 → 控件文本)。
|
|
631
|
+
* @param {any} value - 快照值。
|
|
632
|
+
* @returns {string} 控件文本。
|
|
633
|
+
*/
|
|
501
634
|
function formatValue(value) {
|
|
502
635
|
if (value === undefined || value === null) return ''
|
|
503
636
|
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
504
637
|
return String(value)
|
|
505
638
|
}
|
|
506
639
|
|
|
507
|
-
/**
|
|
640
|
+
/**
|
|
641
|
+
* policies 顶层对象的控件文本(每行 key=policy)。
|
|
642
|
+
* @param {any} value - policies 顶层对象。
|
|
643
|
+
* @returns {string} 多行文本。
|
|
644
|
+
*/
|
|
508
645
|
function formatPolicies(value) {
|
|
509
646
|
if (value === null || typeof value !== 'object') return ''
|
|
510
647
|
return Object.entries(value).map(([key, policy]) => `${key}=${policy}`).join('\n')
|
|
@@ -515,6 +652,10 @@ function escapeHtml(value) {
|
|
|
515
652
|
* 顶层聚合:同顶层字段的多个子字段草稿一次 scope.set(top, 合并值)。
|
|
516
653
|
*/
|
|
517
654
|
class CardForm {
|
|
655
|
+
/**
|
|
656
|
+
* @param {any} scope - 宿主 settingsScope 句柄。
|
|
657
|
+
* @param {(tops: Map<string, object>) => void} onLanded - 落盘成功回调(panel 显隐同步用)。
|
|
658
|
+
*/
|
|
518
659
|
constructor(scope, onLanded) {
|
|
519
660
|
this.scope = scope
|
|
520
661
|
/** @type {((tops: Map<string, object>) => void) | undefined} 落盘成功回调(panel 显隐同步用)。 */
|
|
@@ -527,12 +668,23 @@ function escapeHtml(value) {
|
|
|
527
668
|
scope.subscribe(() => this.publish())
|
|
528
669
|
}
|
|
529
670
|
|
|
671
|
+
/**
|
|
672
|
+
* 绑定读取投影。
|
|
673
|
+
* @param {() => Record<string, any>} project - 快照投影(每次读当前 scope)。
|
|
674
|
+
* @returns {{getSnapshot: () => Record<string, any>, subscribe: (fn: () => void) => () => void, set: (next: Record<string, any>) => void}} store。
|
|
675
|
+
*/
|
|
530
676
|
bind(project) {
|
|
531
677
|
const store = createSnapshotStore(project())
|
|
532
678
|
this.listeners.add(() => store.set(project()))
|
|
533
679
|
return store
|
|
534
680
|
}
|
|
535
681
|
|
|
682
|
+
/**
|
|
683
|
+
* 暂存一个子字段草稿。
|
|
684
|
+
* @param {string} path - 子字段点路径。
|
|
685
|
+
* @param {string} text - 控件文本。
|
|
686
|
+
* @returns {void}
|
|
687
|
+
*/
|
|
536
688
|
stage(path, text) {
|
|
537
689
|
this.staged.set(path, text)
|
|
538
690
|
this.failed = false
|
|
@@ -562,7 +714,18 @@ function escapeHtml(value) {
|
|
|
562
714
|
const base = tops.get(top) ?? (snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value[top] : undefined)
|
|
563
715
|
const merged = keys.length === 1
|
|
564
716
|
? parsed.value
|
|
565
|
-
: deepMerge(base, (() => {
|
|
717
|
+
: deepMerge(base, (() => {
|
|
718
|
+
/** @type {Record<string, any>} */
|
|
719
|
+
const out = {}
|
|
720
|
+
/** @type {Record<string, any>} */
|
|
721
|
+
let cursor = out
|
|
722
|
+
for (let i = 1; i < keys.length - 1; i++) {
|
|
723
|
+
cursor[keys[i]] = {}
|
|
724
|
+
cursor = cursor[keys[i]]
|
|
725
|
+
}
|
|
726
|
+
cursor[keys[keys.length - 1]] = parsed.value
|
|
727
|
+
return out
|
|
728
|
+
})())
|
|
566
729
|
tops.set(top, merged)
|
|
567
730
|
}
|
|
568
731
|
if (!valid || this.staged.size === 0) { this.publish(); return }
|
|
@@ -584,6 +747,11 @@ function escapeHtml(value) {
|
|
|
584
747
|
this.publish()
|
|
585
748
|
}
|
|
586
749
|
|
|
750
|
+
/**
|
|
751
|
+
* 单个子字段的控件状态(草稿或快照文本 / 是否被用户层覆盖 / 草稿是否非法)。
|
|
752
|
+
* @param {string} path - 子字段点路径。
|
|
753
|
+
* @returns {{text: string, overridden: boolean, invalid: boolean}} 控件状态。
|
|
754
|
+
*/
|
|
587
755
|
fieldState(path) {
|
|
588
756
|
const spec = SPEC_BY_PATH.get(path)
|
|
589
757
|
const snapshot = this.scope.getSnapshot()
|
|
@@ -664,7 +832,11 @@ function escapeHtml(value) {
|
|
|
664
832
|
.memsec-btn:disabled { opacity: 0.5; cursor: default; }
|
|
665
833
|
`
|
|
666
834
|
|
|
667
|
-
/**
|
|
835
|
+
/**
|
|
836
|
+
* 单字段控件行(label + input/checkbox/select + override 徽标 + reset + hint/invalid)。
|
|
837
|
+
* @param {{t: (key: string) => string, spec: {path: string, kind: string, key: string, choices?: string[]}, state: {text: string, invalid: boolean, overridden: boolean}, disabled: boolean, onEdit: (text: string) => void, onReset: () => void}} props - 组件属性。
|
|
838
|
+
* @returns {unknown} react 元素。
|
|
839
|
+
*/
|
|
668
840
|
function FieldRow(props) {
|
|
669
841
|
const { t, spec, state, disabled, onEdit, onReset } = props
|
|
670
842
|
const label = t(spec.key ?? spec.path)
|
|
@@ -674,25 +846,25 @@ function escapeHtml(value) {
|
|
|
674
846
|
? jsx('input', {
|
|
675
847
|
type: 'checkbox', disabled,
|
|
676
848
|
checked: state.text === 'true',
|
|
677
|
-
onChange: (event) => onEdit(event.target.checked ? 'true' : 'false'),
|
|
849
|
+
onChange: (/** @type {{target: {checked: boolean}}} */ event) => onEdit(event.target.checked ? 'true' : 'false'),
|
|
678
850
|
})
|
|
679
851
|
: spec.kind === 'choice'
|
|
680
852
|
? jsx('select', {
|
|
681
853
|
className: 'memcard-input', disabled, value: state.text,
|
|
682
|
-
onChange: (event) => onEdit(event.target.value),
|
|
854
|
+
onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
|
|
683
855
|
}, spec.choices.map((choice) => jsx('option', { key: choice, value: choice }, choice)))
|
|
684
856
|
: spec.kind === 'policies'
|
|
685
857
|
? jsx('textarea', {
|
|
686
858
|
className: 'memcard-input memcard-textarea', disabled,
|
|
687
859
|
value: state.text, 'aria-invalid': state.invalid,
|
|
688
|
-
onChange: (event) => onEdit(event.target.value),
|
|
860
|
+
onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
|
|
689
861
|
})
|
|
690
862
|
: jsx('input', {
|
|
691
863
|
type: spec.kind === 'number' ? 'text' : 'text',
|
|
692
864
|
inputMode: spec.kind === 'number' ? 'numeric' : undefined,
|
|
693
865
|
className: 'memcard-input', disabled,
|
|
694
866
|
value: state.text, 'aria-invalid': state.invalid,
|
|
695
|
-
onChange: (event) => onEdit(event.target.value),
|
|
867
|
+
onChange: (/** @type {{target: {value: string}}} */ event) => onEdit(event.target.value),
|
|
696
868
|
})
|
|
697
869
|
return jsx('div', { className: 'memcard-field' },
|
|
698
870
|
jsx('div', { className: 'memcard-row' },
|
|
@@ -707,10 +879,16 @@ function escapeHtml(value) {
|
|
|
707
879
|
)
|
|
708
880
|
}
|
|
709
881
|
|
|
710
|
-
/**
|
|
882
|
+
/**
|
|
883
|
+
* 设置页组件(settings.section 渲染入口;hooks share: mementoCard → useMementoCard)。
|
|
884
|
+
* @param {{useMementoCard: (select: (snapshot: any) => any) => any, discard: () => void, save: () => void, edit: (path: string, text: string) => void, base: any}} props - 宿主注入的组件属性。
|
|
885
|
+
* @returns {unknown} react 元素。
|
|
886
|
+
*/
|
|
711
887
|
function MementoSection(props) {
|
|
712
888
|
const state = props.useMementoCard((snapshot) => snapshot)
|
|
713
889
|
const language = state.language === 'zh' ? 'zh' : 'en'
|
|
890
|
+
/** 文案取值(缺键回退键名);语言取「所选」语言。 */
|
|
891
|
+
/** @type {(key: string) => string} */
|
|
714
892
|
const t = (key) => CARD_STRINGS[language][key] ?? key
|
|
715
893
|
const blocked = !state.dirty || state.saving || state.invalid
|
|
716
894
|
return jsx('div', { className: 'memsec' },
|
|
@@ -732,7 +910,7 @@ function escapeHtml(value) {
|
|
|
732
910
|
return jsx(FieldRow, {
|
|
733
911
|
key: path, t, spec, disabled: !state.writable,
|
|
734
912
|
state: state.fields[path],
|
|
735
|
-
onEdit: (text) => props.edit(path, text),
|
|
913
|
+
onEdit: (/** @type {string} */ text) => props.edit(path, text),
|
|
736
914
|
onReset: () => {
|
|
737
915
|
const baseValue = pathValue(props.base, path)
|
|
738
916
|
props.edit(path, spec.kind === 'policies' ? formatPolicies(baseValue) : formatValue(baseValue))
|
|
@@ -746,6 +924,7 @@ function escapeHtml(value) {
|
|
|
746
924
|
|
|
747
925
|
/** 控制器:scope → 暂存表单 → 设置页快照。保存落盘后同步本页悬浮按钮显隐。 */
|
|
748
926
|
class MementoCardController {
|
|
927
|
+
/** @param {any} scope - 宿主 settingsScope 句柄(namespace = dsh-memento)。 */
|
|
749
928
|
constructor(scope) {
|
|
750
929
|
this.scope = scope
|
|
751
930
|
this.form = new CardForm(scope, (tops) => {
|
|
@@ -767,6 +946,7 @@ function escapeHtml(value) {
|
|
|
767
946
|
inject() {
|
|
768
947
|
return {
|
|
769
948
|
hooks: { mementoCard: this.store },
|
|
949
|
+
/** @param {string} path - 子字段点路径。 @param {string} text - 草稿文本。 @returns {void} */
|
|
770
950
|
edit: (path, text) => this.form.stage(path, text),
|
|
771
951
|
discard: () => this.form.discard(),
|
|
772
952
|
save: () => { void this.form.save() },
|
|
@@ -774,6 +954,11 @@ function escapeHtml(value) {
|
|
|
774
954
|
}
|
|
775
955
|
}
|
|
776
956
|
|
|
957
|
+
/**
|
|
958
|
+
* 客户端插件挂载:面板 + 设置页一级项。
|
|
959
|
+
* @param {any} ctx - 宿主客户端上下文(slots/settingsScope)。
|
|
960
|
+
* @returns {void}
|
|
961
|
+
*/
|
|
777
962
|
function apply(ctx) {
|
|
778
963
|
void bootPanel()
|
|
779
964
|
if (!styleInstalled && typeof document !== 'undefined') {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// client/globals.d.ts — 客户端面的环境声明(浏览器全局 + 宿主平台模块)。
|
|
2
|
+
//
|
|
3
|
+
// 本文件是**脚本**声明(无 import/export):`declare module` 只有在脚本里才是
|
|
4
|
+
// 环境模块声明(在模块文件里会变成"对已存在模块的增强",而本仓并不依赖 react)。
|
|
5
|
+
//
|
|
6
|
+
// client.js 不是 Node 模块:宿主把它当 classic script 注入页面,执行时经
|
|
7
|
+
// window.__ModuleLoader__.load 注册 factory,factory 拿到的 require 是宿主
|
|
8
|
+
// 模块系统(react 等平台内置模块)。这里声明这两件事,让客户端类型门
|
|
9
|
+
// (tsconfig.check.client.json)量到真实契约,而不是靠 any 蒙过去。
|
|
10
|
+
|
|
11
|
+
interface Window {
|
|
12
|
+
/** 宿主注入的模块表:load 注册 factory,同 id 重复 load 会留孤儿工厂。 */
|
|
13
|
+
__ModuleLoader__?: {
|
|
14
|
+
load(entry: { id: string, factory: (require: (id: string) => any) => unknown }): void
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 宿主平台内置的 react。本插件只用 createElement(不引 JSX 编译),
|
|
20
|
+
* 因此这里只声明登记到的那些面;其余用法一律应为类型错误。
|
|
21
|
+
*/
|
|
22
|
+
declare module 'react' {
|
|
23
|
+
const react: {
|
|
24
|
+
createElement: (type: unknown, props?: unknown, ...children: unknown[]) => unknown
|
|
25
|
+
}
|
|
26
|
+
export = react
|
|
27
|
+
}
|