dsh-instruction-memory 1.2.0 → 1.3.1
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 +77 -0
- package/README.md +37 -0
- package/lib/client.js +416 -8
- package/lib/index.js +345 -72
- package/memory.schema.json +6 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -153,6 +153,26 @@ window.__ModuleLoader__.load({
|
|
|
153
153
|
.im-backup { display:flex; flex-direction:column; gap:8px; }
|
|
154
154
|
.im-backup-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
155
155
|
.im-backup-row .im-input { flex:1; min-width:200px; }
|
|
156
|
+
/* ---- F12 诊断与安全状态 ---- */
|
|
157
|
+
.im-hostwarn { display:flex; gap:8px; align-items:flex-start; padding:9px 12px; border-radius:8px; font-size:12px;
|
|
158
|
+
color:var(--dsw-alias-state-warn-primary,#d97706); border:1px solid var(--dsw-alias-state-warn-primary,#d97706);
|
|
159
|
+
background:var(--dsw-alias-state-warn-bg,rgba(217,119,6,.12)); font-weight:500; }
|
|
160
|
+
.im-diag { display:flex; flex-direction:column; gap:8px; }
|
|
161
|
+
.im-diag-group { display:flex; flex-direction:column; gap:3px; padding:8px 10px; border-radius:8px;
|
|
162
|
+
border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1)); background:var(--dsw-alias-bg-base,#fff); }
|
|
163
|
+
.im-diag-sub { color:var(--dsw-alias-label-secondary,#8a9099); font-size:11.5px; }
|
|
164
|
+
.im-diag-fallback { display:flex; flex-direction:column; gap:5px; }
|
|
165
|
+
.im-diag-fallback .im-textarea { min-height:170px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; }
|
|
166
|
+
/* ---- F7 临时静音 ---- */
|
|
167
|
+
.im-master-col { display:flex; flex-direction:column; align-items:flex-end; gap:6px; }
|
|
168
|
+
.im-pause-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-content:flex-end; }
|
|
169
|
+
.im-pause-menu { display:flex; flex-direction:column; gap:4px; padding:8px 10px; border-radius:8px;
|
|
170
|
+
border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.18)); background:var(--dsw-alias-bg-base,#fff);
|
|
171
|
+
box-shadow:0 4px 14px rgba(0,0,0,.1); }
|
|
172
|
+
.im-pause-bar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-content:flex-end;
|
|
173
|
+
padding:6px 10px; border-radius:8px; font-size:12px;
|
|
174
|
+
border:1px solid var(--dsw-alias-state-warn-primary,#d97706);
|
|
175
|
+
background:var(--dsw-alias-state-warn-bg,rgba(217,119,6,.12)); color:var(--dsw-alias-state-warn-primary,#d97706); }
|
|
156
176
|
@media (max-width:640px) { .im-grid2 { grid-template-columns:1fr; } }
|
|
157
177
|
`
|
|
158
178
|
|
|
@@ -315,6 +335,50 @@ window.__ModuleLoader__.load({
|
|
|
315
335
|
'sets.copySuffix': ' 副本',
|
|
316
336
|
'sets.deleteConfirm': '确定删除配置集「{name}」?其中的 {entries} 条指令与 {profiles} 个用户形象会一并删除,且无法撤销。',
|
|
317
337
|
'sets.deleteActive': '当前激活的配置集不能删除;请先切换到其他配置集。',
|
|
338
|
+
'sets.draftsClosed': '已关闭切换前打开的编辑草稿',
|
|
339
|
+
// ---- F12 诊断与安全状态 ----
|
|
340
|
+
'diag.section': '诊断信息',
|
|
341
|
+
'diag.show': '展开诊断',
|
|
342
|
+
'diag.hide': '收起诊断',
|
|
343
|
+
'diag.copy': '复制诊断信息',
|
|
344
|
+
'diag.copied': '诊断信息已复制到剪贴板',
|
|
345
|
+
'diag.copyManual': '自动复制不可用;请手动全选复制下方文本。',
|
|
346
|
+
'diag.hostWarn': '安全提示:设置路由当前绑定在非回环地址({host}),同一网络内的设备可以无鉴权访问此页面,请勿在不可信网络暴露;如需对外发布,请阅读 README 的「经反向代理对外发布」。',
|
|
347
|
+
'diag.groupStorage': '存储',
|
|
348
|
+
'diag.groupInjection': '注入',
|
|
349
|
+
'diag.groupBackup': '备份与历史',
|
|
350
|
+
'diag.groupIssues': '错误与警告',
|
|
351
|
+
'diag.none': '(无)',
|
|
352
|
+
'diag.storagePath': '存储文件',
|
|
353
|
+
'diag.onDisk': '存储文件已在磁盘',
|
|
354
|
+
'diag.loadedFrom': '载入来源',
|
|
355
|
+
'diag.savedAt': '最近保存',
|
|
356
|
+
'diag.rev': '修订号(rev)',
|
|
357
|
+
'diag.registered': '注入已注册',
|
|
358
|
+
'diag.yes': '是',
|
|
359
|
+
'diag.no': '否',
|
|
360
|
+
'diag.pulls': '注入拉取次数(自本次插件启动计数)',
|
|
361
|
+
'diag.chars': '当前注入字符数 / 上限',
|
|
362
|
+
'diag.backupDir': '备份位置',
|
|
363
|
+
'diag.backupDefault': '默认(与存储文件同目录)',
|
|
364
|
+
'diag.backupPath': '覆盖副本(.bak)',
|
|
365
|
+
'diag.historyDir': '版本历史目录',
|
|
366
|
+
'diag.historyCount': '历史快照数',
|
|
367
|
+
'diag.schemaFile': 'Schema 文件',
|
|
368
|
+
'diag.issueStorage': '存储错误',
|
|
369
|
+
'diag.issueWarning': '存储警告',
|
|
370
|
+
'diag.issueHistory': '历史错误',
|
|
371
|
+
'diag.issueSection': '注入错误',
|
|
372
|
+
// ---- F7 临时静音 ----
|
|
373
|
+
'mute.menu': '暂停注入…',
|
|
374
|
+
'mute.30m': '30 分钟',
|
|
375
|
+
'mute.1h': '1 小时',
|
|
376
|
+
'mute.today': '到今天结束',
|
|
377
|
+
'mute.resume': '立即恢复',
|
|
378
|
+
'mute.active': '注入已暂停,剩余 {remaining}',
|
|
379
|
+
'mute.hint': '暂停期间不注入任何指令或用户形象,到期后自动恢复;此状态跨重启保留。',
|
|
380
|
+
'mute.remainingMs': '{m} 分 {s} 秒',
|
|
381
|
+
'mute.remainingH': '{h} 小时 {m} 分',
|
|
318
382
|
// ---- F14 版本历史 ----
|
|
319
383
|
'history.section': '版本历史',
|
|
320
384
|
'history.subtitle': '每次保存都会留一份带时间戳的快照(每台设备最多保留 {max} 份,逐出最旧的)。可在此查看并一键回滚。',
|
|
@@ -522,6 +586,50 @@ window.__ModuleLoader__.load({
|
|
|
522
586
|
'sets.copySuffix': ' copy',
|
|
523
587
|
'sets.deleteConfirm': 'Delete the set “{name}”? Its {entries} instructions and {profiles} personas are deleted with it. This cannot be undone.',
|
|
524
588
|
'sets.deleteActive': 'The active set cannot be deleted; switch to another set first.',
|
|
589
|
+
'sets.draftsClosed': 'The editor draft open before the switch was closed',
|
|
590
|
+
// ---- F12 diagnostics & security panel ----
|
|
591
|
+
'diag.section': 'Diagnostics',
|
|
592
|
+
'diag.show': 'Show diagnostics',
|
|
593
|
+
'diag.hide': 'Hide diagnostics',
|
|
594
|
+
'diag.copy': 'Copy diagnostics',
|
|
595
|
+
'diag.copied': 'Diagnostics copied to the clipboard',
|
|
596
|
+
'diag.copyManual': 'Automatic copy is unavailable; select and copy the text below manually.',
|
|
597
|
+
'diag.hostWarn': 'Security notice: the settings route is bound to a non-loopback address ({host}); other devices on the same network can reach this page without authentication. Do not expose it on an untrusted network — for public deployments read “Publishing behind a reverse proxy” in the README.',
|
|
598
|
+
'diag.groupStorage': 'Storage',
|
|
599
|
+
'diag.groupInjection': 'Injection',
|
|
600
|
+
'diag.groupBackup': 'Backup & history',
|
|
601
|
+
'diag.groupIssues': 'Errors & warnings',
|
|
602
|
+
'diag.none': '(none)',
|
|
603
|
+
'diag.storagePath': 'Store file',
|
|
604
|
+
'diag.onDisk': 'Store file on disk',
|
|
605
|
+
'diag.loadedFrom': 'Loaded from',
|
|
606
|
+
'diag.savedAt': 'Last saved',
|
|
607
|
+
'diag.rev': 'Revision (rev)',
|
|
608
|
+
'diag.registered': 'Section registered',
|
|
609
|
+
'diag.yes': 'yes',
|
|
610
|
+
'diag.no': 'no',
|
|
611
|
+
'diag.pulls': 'Prompt pulls (counted since this plugin started)',
|
|
612
|
+
'diag.chars': 'Injected chars / budget',
|
|
613
|
+
'diag.backupDir': 'Backup location',
|
|
614
|
+
'diag.backupDefault': 'default (next to the store file)',
|
|
615
|
+
'diag.backupPath': 'Rolling copy (.bak)',
|
|
616
|
+
'diag.historyDir': 'History directory',
|
|
617
|
+
'diag.historyCount': 'History snapshots',
|
|
618
|
+
'diag.schemaFile': 'Schema file',
|
|
619
|
+
'diag.issueStorage': 'Storage error',
|
|
620
|
+
'diag.issueWarning': 'Storage warning',
|
|
621
|
+
'diag.issueHistory': 'History error',
|
|
622
|
+
'diag.issueSection': 'Injection error',
|
|
623
|
+
// ---- F7 temporary mute ----
|
|
624
|
+
'mute.menu': 'Pause injection…',
|
|
625
|
+
'mute.30m': '30 minutes',
|
|
626
|
+
'mute.1h': '1 hour',
|
|
627
|
+
'mute.today': 'Until the end of today',
|
|
628
|
+
'mute.resume': 'Resume now',
|
|
629
|
+
'mute.active': 'Injection paused — {remaining} left',
|
|
630
|
+
'mute.hint': 'While paused, neither instructions nor personas are injected. It resumes automatically and survives a restart.',
|
|
631
|
+
'mute.remainingMs': '{m}m {s}s',
|
|
632
|
+
'mute.remainingH': '{h}h {m}m',
|
|
525
633
|
// ---- F14 version history ----
|
|
526
634
|
'history.section': 'Version history',
|
|
527
635
|
'history.subtitle': 'Every save leaves a timestamped snapshot ({max} kept per device, oldest evicted first). Review them here and roll back with one click.',
|
|
@@ -924,6 +1032,71 @@ window.__ModuleLoader__.load({
|
|
|
924
1032
|
return { budget, used, omitted, truncated, percent, tokens }
|
|
925
1033
|
}
|
|
926
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* F12: serialize a diagnostic snapshot into plain text for an issue report.
|
|
1037
|
+
* Pinned as a pure export so verify.mjs can hold the contract: the dump
|
|
1038
|
+
* carries the operational fields (store shape, storage, injection,
|
|
1039
|
+
* security, load origin) plus the reporter's environment, and NEVER
|
|
1040
|
+
* carries user text — no entry titles, no entry or persona bodies. Paths
|
|
1041
|
+
* (including the absolute backup directory) are machine-local but are the
|
|
1042
|
+
* whole point of a support dump, so they stay.
|
|
1043
|
+
*/
|
|
1044
|
+
function diagnosticsText(view, env) {
|
|
1045
|
+
const sec = (root, name) => (root !== null && typeof root === 'object'
|
|
1046
|
+
&& root[name] !== null && typeof root[name] === 'object' ? root[name] : {})
|
|
1047
|
+
const data = sec(view, 'data')
|
|
1048
|
+
const storage = sec(view, 'storage')
|
|
1049
|
+
const injection = sec(view, 'injection')
|
|
1050
|
+
const security = sec(view, 'security')
|
|
1051
|
+
const diagnostics = sec(view, 'diagnostics')
|
|
1052
|
+
const environment = env !== null && typeof env === 'object' ? env : {}
|
|
1053
|
+
const v = (value) => (value === null || value === undefined ? '' : String(value))
|
|
1054
|
+
const count = (value) => (Array.isArray(value) ? String(value.length) : '0')
|
|
1055
|
+
return [
|
|
1056
|
+
'dsh-instruction-memory diagnostics',
|
|
1057
|
+
'--- store ---',
|
|
1058
|
+
'version: ' + v(data.version),
|
|
1059
|
+
'rev: ' + v(data.rev),
|
|
1060
|
+
'enabled: ' + v(data.enabled),
|
|
1061
|
+
'budgetChars: ' + v(data.budgetChars),
|
|
1062
|
+
'disabledUntil: ' + v(data.disabledUntil),
|
|
1063
|
+
'sets: ' + count(data.sets),
|
|
1064
|
+
'entries(active set): ' + count(data.entries),
|
|
1065
|
+
'profiles(active set): ' + count(data.profiles),
|
|
1066
|
+
'--- storage ---',
|
|
1067
|
+
'path: ' + v(storage.path),
|
|
1068
|
+
'onDisk: ' + v(storage.onDisk),
|
|
1069
|
+
'loadedFrom: ' + v(diagnostics.loadedFrom),
|
|
1070
|
+
'rev: ' + v(diagnostics.rev),
|
|
1071
|
+
'savedAt: ' + (typeof storage.savedAt === 'number' && storage.savedAt > 0 ? new Date(storage.savedAt).toISOString() : ''),
|
|
1072
|
+
'migratedFrom: ' + v(storage.migratedFrom),
|
|
1073
|
+
'backupDir: ' + v(storage.backupDir),
|
|
1074
|
+
'isDefaultBackupDir: ' + v(storage.isDefaultBackupDir),
|
|
1075
|
+
'backupPath: ' + v(storage.backupPath),
|
|
1076
|
+
'historyDir: ' + v(storage.historyDir),
|
|
1077
|
+
'historyCount: ' + v(storage.historyCount),
|
|
1078
|
+
'schemaFile: ' + v(storage.schemaFile),
|
|
1079
|
+
'--- injection ---',
|
|
1080
|
+
'registered: ' + v(injection.registered),
|
|
1081
|
+
'muted: ' + v(injection.muted),
|
|
1082
|
+
'error: ' + v(injection.error),
|
|
1083
|
+
'chars: ' + v(injection.chars),
|
|
1084
|
+
'budget: ' + v(injection.budget),
|
|
1085
|
+
'pulls(since boot): ' + v(injection.pulls),
|
|
1086
|
+
'--- security ---',
|
|
1087
|
+
'hostGuarded: ' + v(security.hostGuarded),
|
|
1088
|
+
'boundHost: ' + v(security.boundHost),
|
|
1089
|
+
'--- errors/warnings ---',
|
|
1090
|
+
'storage.error: ' + v(storage.error),
|
|
1091
|
+
'storage.warning: ' + v(storage.warning),
|
|
1092
|
+
'historyError: ' + v(storage.historyError),
|
|
1093
|
+
'--- environment ---',
|
|
1094
|
+
'userAgent: ' + v(environment.userAgent),
|
|
1095
|
+
'language: ' + v(environment.language),
|
|
1096
|
+
'time: ' + v(environment.now),
|
|
1097
|
+
].join('\n')
|
|
1098
|
+
}
|
|
1099
|
+
|
|
927
1100
|
/* ---------------- F17: local relevance preview ---------------- */
|
|
928
1101
|
|
|
929
1102
|
/** The most terms one query may be split into; keeps matching cheap. */
|
|
@@ -1318,6 +1491,16 @@ window.__ModuleLoader__.load({
|
|
|
1318
1491
|
// draft: editing an instruction never closes an open persona and vice
|
|
1319
1492
|
// versa is handled by closing one when opening the other (see handlers).
|
|
1320
1493
|
const [profileDraft, setProfileDraft] = React.useState(null)
|
|
1494
|
+
// Latest live values of the two editor drafts. switchSet's success
|
|
1495
|
+
// callback is a closure created at click time; between the click and the
|
|
1496
|
+
// response landing, a draft can still be opened (the busy flag only
|
|
1497
|
+
// re-renders on the next pass), so the callback must read the drafts
|
|
1498
|
+
// through refs to see what is REALLY open when the switch lands — a
|
|
1499
|
+
// stale `draft === null` would leave the old set's form on screen.
|
|
1500
|
+
const draftRef = React.useRef(null)
|
|
1501
|
+
draftRef.current = draft
|
|
1502
|
+
const profileDraftRef = React.useRef(null)
|
|
1503
|
+
profileDraftRef.current = profileDraft
|
|
1321
1504
|
const [budgetText, setBudgetText] = React.useState('')
|
|
1322
1505
|
// Latest live value of the draft, because the SSE onmessage closure below
|
|
1323
1506
|
// is created once and must not read a stale first-render budgetText.
|
|
@@ -1354,6 +1537,17 @@ window.__ModuleLoader__.load({
|
|
|
1354
1537
|
const [historyOpen, setHistoryOpen] = React.useState(false)
|
|
1355
1538
|
const [history, setHistory] = React.useState(null)
|
|
1356
1539
|
const [historyBusy, setHistoryBusy] = React.useState(false)
|
|
1540
|
+
// F12: the diagnostics card is collapsed by default; a failed automatic
|
|
1541
|
+
// copy parks the dump here so it can be selected manually instead of
|
|
1542
|
+
// pretending the copy happened.
|
|
1543
|
+
const [diagOpen, setDiagOpen] = React.useState(false)
|
|
1544
|
+
const [diagFallback, setDiagFallback] = React.useState(null)
|
|
1545
|
+
// F7: the pause menu (null-state boolean) and a 1s clock tick that exists
|
|
1546
|
+
// ONLY while a pause is live (its effect cleans up after itself). The
|
|
1547
|
+
// tick drives nothing but the remaining-time text; authority stays with
|
|
1548
|
+
// the host snapshot (injection.muted / data.disabledUntil).
|
|
1549
|
+
const [pauseMenuOpen, setPauseMenuOpen] = React.useState(false)
|
|
1550
|
+
const [nowTick, setNowTick] = React.useState(Date.now())
|
|
1357
1551
|
// F18: the backup-location draft (focused-input protection like budget).
|
|
1358
1552
|
const [backupText, setBackupText] = React.useState('')
|
|
1359
1553
|
const backupTextRef = React.useRef('')
|
|
@@ -1396,6 +1590,27 @@ window.__ModuleLoader__.load({
|
|
|
1396
1590
|
return () => clearTimeout(timer)
|
|
1397
1591
|
}, [undo])
|
|
1398
1592
|
|
|
1593
|
+
// F7: tick once per second while a pause is live so the remaining-time
|
|
1594
|
+
// text counts down. The interval exists only while muted — the cleanup
|
|
1595
|
+
// clears it the moment the snapshot says otherwise. This effect sits
|
|
1596
|
+
// BEFORE the early returns (a hook skipped by a conditional return would
|
|
1597
|
+
// desync the hook order); it reads the snapshot through `view` because
|
|
1598
|
+
// the derived `injection` does not exist yet up here.
|
|
1599
|
+
const mutedNow = view !== null && view.injection !== null && typeof view.injection === 'object'
|
|
1600
|
+
&& view.injection.muted === true
|
|
1601
|
+
React.useEffect(() => {
|
|
1602
|
+
if (!mutedNow) return undefined
|
|
1603
|
+
// A pause started from ANOTHER window replaces the menu row; the open
|
|
1604
|
+
// flag must not resurface when the row returns after expiry.
|
|
1605
|
+
setPauseMenuOpen(false)
|
|
1606
|
+
// Snapshot the clock the moment the pause lands: nowTick may be up to
|
|
1607
|
+
// a second stale from a PREVIOUS pause, which would render the first
|
|
1608
|
+
// frame with an inflated remaining time.
|
|
1609
|
+
setNowTick(Date.now())
|
|
1610
|
+
const tick = setInterval(() => setNowTick(Date.now()), 1000)
|
|
1611
|
+
return () => clearInterval(tick)
|
|
1612
|
+
}, [mutedNow])
|
|
1613
|
+
|
|
1399
1614
|
/**
|
|
1400
1615
|
* F16 live validation, debounced: parsing on every keystroke would stall
|
|
1401
1616
|
* the panel on a multi-megabyte paste, and 250 ms is below the threshold
|
|
@@ -1566,6 +1781,22 @@ window.__ModuleLoader__.load({
|
|
|
1566
1781
|
if (result !== null && result.ok === true) {
|
|
1567
1782
|
setSelected([])
|
|
1568
1783
|
setUndo(null)
|
|
1784
|
+
// Batch mode exists to act on listed ids; after a switch it would
|
|
1785
|
+
// act on ids from the wrong set, so it leaves with the selection.
|
|
1786
|
+
setBatchMode(false)
|
|
1787
|
+
// An editor left open across a switch is a form full of the OLD
|
|
1788
|
+
// set's content that would save into the NEW set. The server-side
|
|
1789
|
+
// cross-set guard means no data is lost, but the form itself is a
|
|
1790
|
+
// lie — so it closes with the switch, and the user is told. The
|
|
1791
|
+
// refs (not the closure state) say whether anything was open.
|
|
1792
|
+
setDraft(null)
|
|
1793
|
+
setProfileDraft(null)
|
|
1794
|
+
if (draftRef.current !== null || profileDraftRef.current !== null) {
|
|
1795
|
+
const base = typeof result.message === 'string' && result.message !== ''
|
|
1796
|
+
? result.message + ';'
|
|
1797
|
+
: ''
|
|
1798
|
+
setNotice(base + t('sets.draftsClosed'))
|
|
1799
|
+
}
|
|
1569
1800
|
}
|
|
1570
1801
|
})
|
|
1571
1802
|
}
|
|
@@ -1729,6 +1960,55 @@ window.__ModuleLoader__.load({
|
|
|
1729
1960
|
}
|
|
1730
1961
|
}
|
|
1731
1962
|
|
|
1963
|
+
// ---- F12 diagnostics copy ----
|
|
1964
|
+
|
|
1965
|
+
function copyDiagnostics() {
|
|
1966
|
+
if (view === null) return
|
|
1967
|
+
const text = diagnosticsText(view, {
|
|
1968
|
+
userAgent: typeof navigator !== 'undefined' && navigator.userAgent !== undefined ? String(navigator.userAgent) : '',
|
|
1969
|
+
language: typeof navigator !== 'undefined' && navigator.language !== undefined ? String(navigator.language) : '',
|
|
1970
|
+
now: new Date().toISOString(),
|
|
1971
|
+
})
|
|
1972
|
+
const clipboard = typeof navigator !== 'undefined' && navigator.clipboard
|
|
1973
|
+
&& typeof navigator.clipboard.writeText === 'function' ? navigator.clipboard : null
|
|
1974
|
+
if (clipboard !== null) {
|
|
1975
|
+
clipboard.writeText(text).then(() => setNotice(t('diag.copied')), () => fallbackCopyDiagnostics(text))
|
|
1976
|
+
return
|
|
1977
|
+
}
|
|
1978
|
+
fallbackCopyDiagnostics(text)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/**
|
|
1982
|
+
* The webview may ship neither the async clipboard API nor
|
|
1983
|
+
* document.execCommand; then the only honest move is to surface the dump
|
|
1984
|
+
* in a selectable textarea instead of pretending the copy happened.
|
|
1985
|
+
*/
|
|
1986
|
+
function fallbackCopyDiagnostics(text) {
|
|
1987
|
+
try {
|
|
1988
|
+
const area = document.createElement('textarea')
|
|
1989
|
+
area.value = text
|
|
1990
|
+
area.setAttribute('readonly', '')
|
|
1991
|
+
const host = document.body || document.head
|
|
1992
|
+
if (host && typeof host.appendChild === 'function') host.appendChild(area)
|
|
1993
|
+
if (typeof area.select === 'function') area.select()
|
|
1994
|
+
// execCommand may THROW (not merely return false); the detached
|
|
1995
|
+
// helper must leave the DOM on every path.
|
|
1996
|
+
let ok = false
|
|
1997
|
+
try {
|
|
1998
|
+
ok = typeof document.execCommand === 'function' && document.execCommand('copy') === true
|
|
1999
|
+
} finally {
|
|
2000
|
+
if (host && typeof host.removeChild === 'function' && area.parentNode === host) host.removeChild(area)
|
|
2001
|
+
}
|
|
2002
|
+
if (ok) {
|
|
2003
|
+
setNotice(t('diag.copied'))
|
|
2004
|
+
return
|
|
2005
|
+
}
|
|
2006
|
+
throw new Error('copy rejected')
|
|
2007
|
+
} catch {
|
|
2008
|
+
setDiagFallback(text)
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1732
2012
|
async function doExport() {
|
|
1733
2013
|
try {
|
|
1734
2014
|
const result = await callHost('export-data', null)
|
|
@@ -1854,6 +2134,11 @@ window.__ModuleLoader__.load({
|
|
|
1854
2134
|
// it here keeps the effect's dependency list down to the text itself.
|
|
1855
2135
|
sourceCeilingRef.current = typeof limits.maxBodyChars === 'number' ? limits.maxBodyChars : 0
|
|
1856
2136
|
const injection = view.injection || { registered: false, chars: 0, error: null }
|
|
2137
|
+
// F12: the security block is optional (older Host) and then means "no
|
|
2138
|
+
// known problem" — an absent field must not manufacture a warning.
|
|
2139
|
+
const security = view.security !== null && typeof view.security === 'object'
|
|
2140
|
+
? view.security
|
|
2141
|
+
: { hostGuarded: true, boundHost: '' }
|
|
1857
2142
|
// List order is display-only and never changes what the Host injects
|
|
1858
2143
|
// (the injection order is decided host-side). The default mirrors the
|
|
1859
2144
|
// previous fixed order; updated-time and title are the other sorts.
|
|
@@ -1972,20 +2257,90 @@ window.__ModuleLoader__.load({
|
|
|
1972
2257
|
: (injection.registered
|
|
1973
2258
|
? t('status.mounted', { chars: injection.chars })
|
|
1974
2259
|
: (activeCount === 0 ? t('status.notMountedEmpty') : t('status.notMounted')))
|
|
2260
|
+
// F12: display-only rows. Every value comes straight from the snapshot;
|
|
2261
|
+
// a missing field (older Host) degrades to (无) instead of throwing
|
|
2262
|
+
// mid-render.
|
|
2263
|
+
const diagValue = (value) => (value === null || value === undefined || value === '' ? t('diag.none') : String(value))
|
|
2264
|
+
const diagYesNo = (value) => (value === true ? t('diag.yes') : t('diag.no'))
|
|
2265
|
+
const diagRow = (label, value) => h('div', { className: 'im-meta' }, label + ':' + value)
|
|
2266
|
+
const diagLoadedFrom = view.diagnostics !== null && typeof view.diagnostics === 'object'
|
|
2267
|
+
? view.diagnostics.loadedFrom
|
|
2268
|
+
: null
|
|
2269
|
+
const diagRev = view.diagnostics !== null && typeof view.diagnostics === 'object'
|
|
2270
|
+
&& typeof view.diagnostics.rev === 'number'
|
|
2271
|
+
? view.diagnostics.rev
|
|
2272
|
+
: (data.rev || 0)
|
|
2273
|
+
// F7: the pause decision itself belongs to the host (injection.muted);
|
|
2274
|
+
// the countdown below is display-only. At zero the bar yields back to
|
|
2275
|
+
// the normal control (the host's own timer is delivering a fresh
|
|
2276
|
+
// snapshot at that moment in any case).
|
|
2277
|
+
const muted = injection.muted === true
|
|
2278
|
+
const disabledUntil = typeof data.disabledUntil === 'number' && Number.isFinite(data.disabledUntil)
|
|
2279
|
+
? data.disabledUntil
|
|
2280
|
+
: 0
|
|
2281
|
+
const muteRemainingMs = muted ? Math.max(0, disabledUntil - nowTick) : 0
|
|
2282
|
+
const muteRemainingText = (ms) => {
|
|
2283
|
+
const totalSec = Math.max(0, Math.round(ms / 1000))
|
|
2284
|
+
const hh = Math.floor(totalSec / 3600)
|
|
2285
|
+
const mm = Math.floor((totalSec % 3600) / 60)
|
|
2286
|
+
const ss = totalSec % 60
|
|
2287
|
+
return hh > 0 ? t('mute.remainingH', { h: hh, m: mm }) : t('mute.remainingMs', { m: mm, s: ss })
|
|
2288
|
+
}
|
|
2289
|
+
const endOfToday = () => {
|
|
2290
|
+
const d = new Date()
|
|
2291
|
+
d.setHours(24, 0, 0, 0)
|
|
2292
|
+
return d.getTime()
|
|
2293
|
+
}
|
|
2294
|
+
function pauseFor(ms) {
|
|
2295
|
+
if (busy || typeof ms !== 'number' || !(ms > 0)) return
|
|
2296
|
+
setPauseMenuOpen(false)
|
|
2297
|
+
void send('set-options', { disabledUntil: Date.now() + ms, baseRev: revOf() })
|
|
2298
|
+
}
|
|
2299
|
+
function resumePause() {
|
|
2300
|
+
if (busy) return
|
|
2301
|
+
setPauseMenuOpen(false)
|
|
2302
|
+
void send('set-options', { disabledUntil: 0, baseRev: revOf() })
|
|
2303
|
+
}
|
|
1975
2304
|
|
|
1976
2305
|
const header = h('div', { className: 'im-card', key: 'head' },
|
|
2306
|
+
// F12: the non-loopback warning sits at the TOP of the panel — not
|
|
2307
|
+
// inside the collapsible diagnostics card — so it cannot be missed.
|
|
2308
|
+
security.hostGuarded === false
|
|
2309
|
+
? h('div', { className: 'im-hostwarn', key: 'hostwarn' },
|
|
2310
|
+
t('diag.hostWarn', { host: security.boundHost !== '' ? security.boundHost : '?' }))
|
|
2311
|
+
: null,
|
|
1977
2312
|
h('div', { className: 'im-row' },
|
|
1978
2313
|
h('div', null,
|
|
1979
2314
|
h('h3', { className: 'im-h' }, t('panel.title')),
|
|
1980
2315
|
h('p', { className: 'im-sub' }, t('panel.subtitle'))),
|
|
1981
|
-
h('
|
|
1982
|
-
h('
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2316
|
+
h('div', { className: 'im-master-col' },
|
|
2317
|
+
h('label', { className: 'im-switch' },
|
|
2318
|
+
h('input', {
|
|
2319
|
+
type: 'checkbox',
|
|
2320
|
+
checked: data.enabled === true,
|
|
2321
|
+
disabled: busy,
|
|
2322
|
+
onChange: () => send('set-options', { enabled: !data.enabled, baseRev: revOf() }),
|
|
2323
|
+
}),
|
|
2324
|
+
t('panel.masterSwitch')),
|
|
2325
|
+
// F7 临时静音:暂停期间提示词与「未安装插件」逐字节一致;到期由宿主定时器
|
|
2326
|
+
// 自动恢复,无需用户操作、不写盘。倒计时只是展示,权威在快照。
|
|
2327
|
+
muted && muteRemainingMs > 0
|
|
2328
|
+
? h('div', { className: 'im-pause-bar' },
|
|
2329
|
+
h('span', null, t('mute.active', { remaining: muteRemainingText(muteRemainingMs) })),
|
|
2330
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: resumePause }, t('mute.resume')))
|
|
2331
|
+
: h('div', { className: 'im-pause-row' },
|
|
2332
|
+
h('button', {
|
|
2333
|
+
className: 'im-btn',
|
|
2334
|
+
disabled: busy,
|
|
2335
|
+
onClick: () => setPauseMenuOpen(!pauseMenuOpen),
|
|
2336
|
+
}, t('mute.menu')),
|
|
2337
|
+
pauseMenuOpen
|
|
2338
|
+
? h('div', { className: 'im-pause-menu' },
|
|
2339
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: () => pauseFor(30 * 60000) }, t('mute.30m')),
|
|
2340
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: () => pauseFor(60 * 60000) }, t('mute.1h')),
|
|
2341
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: () => pauseFor(endOfToday() - Date.now()) }, t('mute.today')),
|
|
2342
|
+
h('div', { className: 'im-meta' }, t('mute.hint')))
|
|
2343
|
+
: null))),
|
|
1989
2344
|
h('div', { className: 'im-status' },
|
|
1990
2345
|
h('span', { className: dotClass }),
|
|
1991
2346
|
h('span', { className: injection.error ? 'im-err' : 'im-meta' }, dotText)),
|
|
@@ -2410,6 +2765,58 @@ window.__ModuleLoader__.load({
|
|
|
2410
2765
|
history !== null && history.dir !== ''
|
|
2411
2766
|
? h('div', { className: 'im-meta' }, t('history.dir', { dir: history.dir }))
|
|
2412
2767
|
: null),
|
|
2768
|
+
// ---- F12 诊断与安全状态(默认收起;纯展示,无写路由) ----
|
|
2769
|
+
h('div', { key: 'diag', className: 'im-diag' },
|
|
2770
|
+
h('div', { className: 'im-profiles-head' },
|
|
2771
|
+
h('span', { className: 'im-section-title' }, t('diag.section')),
|
|
2772
|
+
h('span', { className: 'im-spacer' }),
|
|
2773
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: () => setDiagOpen(!diagOpen) },
|
|
2774
|
+
diagOpen ? t('diag.hide') : t('diag.show')),
|
|
2775
|
+
h('button', { className: 'im-btn', disabled: busy, onClick: copyDiagnostics }, t('diag.copy'))),
|
|
2776
|
+
diagOpen
|
|
2777
|
+
? h('div', null,
|
|
2778
|
+
h('div', { className: 'im-diag-group' },
|
|
2779
|
+
h('span', { className: 'im-section-title im-diag-sub' }, t('diag.groupStorage')),
|
|
2780
|
+
diagRow(t('diag.storagePath'), diagValue(storage.path)),
|
|
2781
|
+
diagRow(t('diag.onDisk'), diagYesNo(storage.onDisk === true)),
|
|
2782
|
+
diagRow(t('diag.loadedFrom'), diagValue(diagLoadedFrom)),
|
|
2783
|
+
diagRow(t('diag.savedAt'), typeof storage.savedAt === 'number' && storage.savedAt > 0
|
|
2784
|
+
? new Date(storage.savedAt).toLocaleString()
|
|
2785
|
+
: t('diag.none')),
|
|
2786
|
+
diagRow(t('diag.rev'), String(diagRev))),
|
|
2787
|
+
h('div', { className: 'im-diag-group' },
|
|
2788
|
+
h('span', { className: 'im-section-title im-diag-sub' }, t('diag.groupInjection')),
|
|
2789
|
+
diagRow(t('diag.registered'), diagYesNo(injection.registered === true)),
|
|
2790
|
+
diagRow(t('diag.pulls'), String(typeof injection.pulls === 'number' ? injection.pulls : 0)),
|
|
2791
|
+
diagRow(t('diag.chars'),
|
|
2792
|
+
String(typeof injection.chars === 'number' ? injection.chars : 0) + ' / '
|
|
2793
|
+
+ String(typeof injection.budget === 'number' ? injection.budget : data.budgetChars))),
|
|
2794
|
+
h('div', { className: 'im-diag-group' },
|
|
2795
|
+
h('span', { className: 'im-section-title im-diag-sub' }, t('diag.groupBackup')),
|
|
2796
|
+
diagRow(t('diag.backupDir'), typeof storage.backupDir === 'string' && storage.backupDir !== ''
|
|
2797
|
+
? storage.backupDir
|
|
2798
|
+
: t('diag.backupDefault')),
|
|
2799
|
+
diagRow(t('diag.backupPath'), diagValue(storage.backupPath)),
|
|
2800
|
+
diagRow(t('diag.historyDir'), diagValue(storage.historyDir)),
|
|
2801
|
+
diagRow(t('diag.historyCount'), String(typeof storage.historyCount === 'number' ? storage.historyCount : 0)),
|
|
2802
|
+
diagRow(t('diag.schemaFile'), diagValue(storage.schemaFile))),
|
|
2803
|
+
h('div', { className: 'im-diag-group' },
|
|
2804
|
+
h('span', { className: 'im-section-title im-diag-sub' }, t('diag.groupIssues')),
|
|
2805
|
+
diagRow(t('diag.issueStorage'), diagValue(storage.error)),
|
|
2806
|
+
diagRow(t('diag.issueWarning'), diagValue(storage.warning)),
|
|
2807
|
+
diagRow(t('diag.issueHistory'), diagValue(storage.historyError)),
|
|
2808
|
+
diagRow(t('diag.issueSection'), diagValue(injection.error))),
|
|
2809
|
+
diagFallback !== null
|
|
2810
|
+
? h('div', { className: 'im-diag-fallback' },
|
|
2811
|
+
h('div', { className: 'im-meta' }, t('diag.copyManual')),
|
|
2812
|
+
h('textarea', {
|
|
2813
|
+
className: 'im-textarea',
|
|
2814
|
+
readOnly: true,
|
|
2815
|
+
value: diagFallback,
|
|
2816
|
+
onFocus: (ev) => { if (typeof ev.target.select === 'function') ev.target.select() },
|
|
2817
|
+
}))
|
|
2818
|
+
: null)
|
|
2819
|
+
: null),
|
|
2413
2820
|
error !== '' ? h('div', { className: 'im-err' }, error) : null,
|
|
2414
2821
|
error === '' && notice !== '' ? h('div', { className: 'im-ok' }, notice) : null,
|
|
2415
2822
|
error === '' && notice === '' && storage.error ? h('div', { className: 'im-err' }, storage.error) : null,
|
|
@@ -2717,6 +3124,7 @@ window.__ModuleLoader__.load({
|
|
|
2717
3124
|
exports.__relevanceTerms = relevanceTerms
|
|
2718
3125
|
exports.__relevancePreview = relevancePreview
|
|
2719
3126
|
exports.__sourceParseVerdict = sourceParseVerdict
|
|
3127
|
+
exports.__diagnosticsText = diagnosticsText
|
|
2720
3128
|
exports.__strings = STRINGS
|
|
2721
3129
|
exports.__ns = NS
|
|
2722
3130
|
exports.__dictionaryT = dictionaryT
|