stratagate-dsh 0.2.1 → 0.2.16
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 +89 -0
- package/README.md +43 -3
- package/cordis.patch.yml +2 -2
- package/dist/client.js +351 -180
- package/dist/index.js +1130 -276
- package/dist/index.js.map +1 -1
- package/docs/README.zh-CN.md +152 -0
- package/package.json +12 -2
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// StrataGate Memory UI
|
|
1
|
+
// StrataGate Memory UI - read-only DSH settings page.
|
|
2
2
|
window.__ModuleLoader__.load({
|
|
3
3
|
id: 'stratagate-dsh',
|
|
4
4
|
factory: (require) => {
|
|
@@ -11,29 +11,46 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
const STAR_DISMISSED_KEY = 'stratagate.starPrompt.dismissed.v1'
|
|
12
12
|
const STAR_PROMPT_USAGE_THRESHOLD = 3
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// The prompt can still be hidden for this render when storage is unavailable.
|
|
14
|
+
const css = `
|
|
15
|
+
.sg-memory {
|
|
16
|
+
color-scheme:light dark;
|
|
17
|
+
--sg-page:var(--dsh-color-background,var(--color-background,#fff));
|
|
18
|
+
--sg-surface:var(--dsh-color-surface,var(--color-surface,#fff));
|
|
19
|
+
--sg-soft:var(--dsh-color-surface-secondary,var(--color-fill-secondary,#f6f7f9));
|
|
20
|
+
--sg-text:var(--dsh-color-text,var(--color-text,#1c2028));
|
|
21
|
+
--sg-muted:var(--dsh-color-text-secondary,var(--color-text-secondary,#707782));
|
|
22
|
+
--sg-border:var(--dsh-color-border,var(--color-border,#e2e5e9));
|
|
23
|
+
--sg-accent:var(--dsh-color-primary,var(--color-primary,#2563d9));
|
|
24
|
+
--sg-accent-soft:var(--dsh-color-primary-soft,#edf3ff);
|
|
25
|
+
--sg-good:var(--dsh-color-success,#16835b);
|
|
26
|
+
--sg-good-soft:var(--dsh-color-success-soft,#eaf8f1);
|
|
27
|
+
--sg-warn:var(--dsh-color-warning,#ad6200);
|
|
28
|
+
--sg-warn-soft:var(--dsh-color-warning-soft,#fff5e5);
|
|
29
|
+
--sg-danger:var(--dsh-color-danger,#c73b36);
|
|
30
|
+
--sg-danger-soft:var(--dsh-color-danger-soft,#fff0ef);
|
|
31
|
+
box-sizing:border-box;width:100%;max-width:680px;min-width:0;margin:0 auto;padding:16px 18px 32px;
|
|
32
|
+
background:var(--sg-page);color:var(--sg-text);font:14px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
|
|
33
|
+
letter-spacing:0;overflow-wrap:anywhere;
|
|
35
34
|
}
|
|
36
|
-
|
|
35
|
+
.sg-memory *{box-sizing:border-box;letter-spacing:0}.sg-memory button,.sg-memory input,.sg-memory select{font:inherit;color:inherit}
|
|
36
|
+
.sg-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}.sg-brand{display:flex;align-items:center;gap:9px;min-width:0}.sg-logo{width:30px;height:30px;display:grid;place-items:center;flex:0 0 auto;border-radius:7px;background:var(--sg-accent);color:#fff;font-size:17px;font-weight:750}.sg-brand-name{font-size:15px;font-weight:720;white-space:nowrap}
|
|
37
|
+
.sg-icon-button,.sg-back,.sg-quiet-button{border:0;background:transparent;cursor:pointer}.sg-icon-button{width:32px;height:32px;border-radius:6px;font-size:20px}.sg-icon-button:hover,.sg-back:hover,.sg-quiet-button:hover{background:var(--sg-soft)}
|
|
38
|
+
.sg-project{display:flex;align-items:center;gap:7px;min-width:0;margin:0 0 9px;color:var(--sg-muted);font-size:12px}.sg-project-label{flex:0 0 auto}.sg-project-select{min-width:0;max-width:100%;padding:3px 22px 3px 5px;border:0;border-radius:5px;background:transparent;color:var(--sg-text);font-weight:620;cursor:pointer;text-overflow:ellipsis}
|
|
39
|
+
.sg-tabs{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--sg-border);margin-bottom:20px}.sg-tab{position:relative;min-width:0;padding:10px 4px;border:0;background:transparent;color:var(--sg-muted);cursor:pointer;white-space:nowrap}.sg-tab.active{color:var(--sg-accent);font-weight:700}.sg-tab.active:after{content:"";position:absolute;left:18%;right:18%;bottom:-1px;height:2px;border-radius:2px;background:var(--sg-accent)}
|
|
40
|
+
.sg-alert{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;width:100%;padding:11px 12px;margin:0 0 18px;border:1px solid color-mix(in srgb,var(--sg-warn) 28%,var(--sg-border));border-radius:8px;background:var(--sg-warn-soft);text-align:left;cursor:pointer}.sg-alert-mark{color:var(--sg-warn);font-size:17px}.sg-alert-title{font-weight:700}.sg-alert-copy{color:var(--sg-muted);font-size:12px}.sg-chevron{color:var(--sg-muted);font-size:18px}.sg-processing-alert{display:flex;align-items:center;gap:9px;width:100%;padding:11px 12px;margin:0 0 18px;border:1px solid color-mix(in srgb,var(--sg-danger) 34%,var(--sg-border));border-radius:8px;background:var(--sg-danger-soft);color:var(--sg-danger)}.sg-processing-icon{display:inline-grid;place-items:center;width:20px;height:20px;flex:0 0 auto;font-size:19px;font-weight:700;line-height:1;animation:sg-spin 1s linear infinite}.sg-processing-title{display:block;font-weight:720}.sg-processing-copy{display:block;margin-top:2px;color:var(--sg-muted);font-size:12px}@keyframes sg-spin{to{transform:rotate(360deg)}}
|
|
41
|
+
.sg-intro{margin-bottom:15px}.sg-intro h2,.sg-detail-title{margin:0;font-size:18px;line-height:1.35;font-weight:740}.sg-intro p,.sg-detail-subtitle{margin:5px 0 0;color:var(--sg-muted);font-size:13px}.sg-search{position:relative;margin-bottom:5px}.sg-search-mark{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--sg-muted);font-size:16px;pointer-events:none}.sg-search input{width:100%;height:39px;padding:0 11px 0 35px;border:1px solid var(--sg-border);border-radius:7px;background:var(--sg-surface);outline:0}.sg-search input:focus{border-color:var(--sg-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--sg-accent) 14%,transparent)}
|
|
42
|
+
.sg-feed{border-top:1px solid var(--sg-border)}.sg-entry{position:relative;width:100%;min-width:0;padding:17px 2px;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left}.sg-entry-button{cursor:pointer}.sg-entry-button:hover .sg-entry-title{color:var(--sg-accent)}.sg-entry-title{padding-right:22px;font-size:15px;line-height:1.45;font-weight:720}.sg-entry-summary{margin-top:5px;color:var(--sg-text);white-space:pre-wrap}.sg-entry-chevron{position:absolute;right:2px;top:18px;color:var(--sg-muted);font-size:18px}.sg-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:9px;color:var(--sg-muted);font-size:12px}.sg-meta-sep:before{content:"·";margin-right:7px}.sg-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}.sg-tag{max-width:100%;padding:3px 8px;border:1px solid var(--sg-border);border-radius:999px;background:var(--sg-soft);color:var(--sg-text);font-size:12px;line-height:1.45;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.sg-tag-button{cursor:pointer}.sg-tag-button:hover{border-color:var(--sg-accent);color:var(--sg-accent)}
|
|
43
|
+
.sg-status{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:5px;font-size:12px;font-weight:650}.sg-status:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.sg-status.organized{color:var(--sg-good);background:var(--sg-good-soft)}.sg-status.processing{color:var(--sg-accent);background:var(--sg-accent-soft)}.sg-status.waiting{color:var(--sg-muted);background:var(--sg-soft)}.sg-status.failed{color:var(--sg-warn);background:var(--sg-warn-soft)}
|
|
44
|
+
.sg-backbar{display:flex;align-items:center;min-height:35px;margin:-4px 0 13px}.sg-back{display:inline-flex;align-items:center;gap:6px;margin-left:-7px;padding:6px 7px;border-radius:6px;font-weight:650}.sg-detail-header{padding-bottom:16px;border-bottom:1px solid var(--sg-border)}.sg-detail-section{padding:18px 0;border-bottom:1px solid var(--sg-border)}.sg-detail-section:last-child{border-bottom:0}.sg-section-title{margin:0 0 11px;font-size:14px;font-weight:730}.sg-prose{margin:0;white-space:pre-wrap}.sg-facts{margin:0;padding-left:20px}.sg-facts li+li{margin-top:7px}.sg-related-list{display:flex;flex-direction:column}.sg-related{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left;cursor:pointer}.sg-related:last-child{border-bottom:0}.sg-related-name{color:var(--sg-accent)}.sg-related-time{flex:0 0 auto;color:var(--sg-muted);font-size:12px}
|
|
45
|
+
.sg-source-label{display:flex;align-items:center;gap:8px}.sg-source-icon{color:var(--sg-muted)}.sg-tech{margin-top:13px}.sg-tech summary{color:var(--sg-muted);font-size:12px;cursor:pointer}.sg-tech-body{margin-top:10px;padding:11px;border-radius:7px;background:var(--sg-soft);font-size:12px}.sg-tech-row{display:grid;grid-template-columns:88px minmax(0,1fr);gap:9px;padding:3px 0}.sg-code{font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.sg-raw-message{padding-top:10px;margin-top:10px;border-top:1px solid var(--sg-border)}
|
|
46
|
+
.sg-result-count{margin:0 0 9px;color:var(--sg-muted);font-size:12px}.sg-result-event{padding:8px 0;border-bottom:1px solid var(--sg-border)}.sg-result-event:last-child{border-bottom:0}.sg-pipeline{display:flex;flex-direction:column}.sg-stage{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:9px 0;border-bottom:1px solid var(--sg-border)}.sg-stage:last-child{border-bottom:0}.sg-stage-value{font-size:12px}.sg-stage-value.done{color:var(--sg-good)}.sg-stage-value.failed{color:var(--sg-danger)}.sg-stage-value.waiting{color:var(--sg-muted)}.sg-safe-note{padding:11px 12px;margin-bottom:12px;border-radius:7px;background:var(--sg-good-soft);color:var(--sg-good);font-weight:650}.sg-error-note{margin:8px 0 0;color:var(--sg-muted);font-size:12px}
|
|
47
|
+
.sg-menu{border-top:1px solid var(--sg-border)}.sg-menu-row{display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:9px;width:100%;padding:14px 2px;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left;cursor:pointer}.sg-menu-row:hover .sg-menu-title{color:var(--sg-accent)}.sg-menu-icon{width:28px;height:28px;display:grid;place-items:center;border-radius:6px;background:var(--sg-soft);color:var(--sg-muted)}.sg-menu-title{font-weight:680}.sg-menu-subtitle{color:var(--sg-muted);font-size:12px}.sg-counts{display:flex;gap:22px;padding:5px 0 18px;border-bottom:1px solid var(--sg-border)}.sg-count-value{font-size:22px;font-weight:740}.sg-count-label{color:var(--sg-muted);font-size:12px}.sg-structured-group{padding-top:17px}.sg-raw-group{padding:12px 0;border-bottom:1px solid var(--sg-border)}.sg-raw-group summary{cursor:pointer;font-weight:680}.sg-raw-json{max-height:360px;padding:11px;margin:10px 0 0;overflow:auto;border-radius:7px;background:var(--sg-soft)}
|
|
48
|
+
.sg-audit{padding:14px 0;border-bottom:1px solid var(--sg-border)}.sg-audit summary{cursor:pointer}.sg-audit-body{margin-top:10px}.sg-audit-evidence{margin-top:9px}.sg-star{padding:14px 0;margin-top:14px;border-top:1px solid var(--sg-border)}.sg-star-actions{display:flex;gap:10px;align-items:center;margin-top:8px}.sg-link{color:var(--sg-accent);text-decoration:none}.sg-quiet-button{padding:5px 7px;border-radius:5px;color:var(--sg-muted);font-size:12px}
|
|
49
|
+
.sg-loading{padding:32px 0}.sg-skeleton{height:12px;margin:10px 0;border-radius:4px;background:var(--sg-soft)}.sg-skeleton:nth-child(2){width:72%}.sg-empty{padding:34px 14px;text-align:center;color:var(--sg-muted)}.sg-empty strong{display:block;margin-bottom:5px;color:var(--sg-text)}.sg-error{padding:13px;margin-bottom:16px;border:1px solid color-mix(in srgb,var(--sg-danger) 28%,var(--sg-border));border-radius:8px;background:var(--sg-danger-soft)}.sg-error-title{font-weight:700;color:var(--sg-danger)}.sg-error details{margin-top:7px;font-size:12px}
|
|
50
|
+
.sg-muted{color:var(--sg-muted);font-size:12px}
|
|
51
|
+
@media (prefers-color-scheme:dark){.sg-memory{--sg-page:var(--dsh-color-background,var(--color-background,#15171b));--sg-surface:var(--dsh-color-surface,var(--color-surface,#191c21));--sg-soft:var(--dsh-color-surface-secondary,var(--color-fill-secondary,#22262d));--sg-text:var(--dsh-color-text,var(--color-text,#edf0f4));--sg-muted:var(--dsh-color-text-secondary,var(--color-text-secondary,#9da5b0));--sg-border:var(--dsh-color-border,var(--color-border,#343942));--sg-accent:var(--dsh-color-primary,var(--color-primary,#78a7ff));--sg-accent-soft:var(--dsh-color-primary-soft,#192842);--sg-good:var(--dsh-color-success,#67ce9a);--sg-good-soft:var(--dsh-color-success-soft,#162a22);--sg-warn:var(--dsh-color-warning,#f0ad58);--sg-warn-soft:var(--dsh-color-warning-soft,#302519);--sg-danger:var(--dsh-color-danger,#ff8179);--sg-danger-soft:var(--dsh-color-danger-soft,#321e1f)}}
|
|
52
|
+
@media (max-width:440px){.sg-memory{padding:12px 12px 26px}.sg-brand-name{font-size:14px}.sg-tabs{margin-left:-2px;margin-right:-2px}.sg-tab{padding-left:0;padding-right:0}.sg-alert{grid-template-columns:auto minmax(0,1fr)}.sg-alert>.sg-chevron{display:none}.sg-tech-row{grid-template-columns:1fr;gap:1px}.sg-counts{gap:16px}.sg-entry-title{font-size:14px}}
|
|
53
|
+
`
|
|
37
54
|
|
|
38
55
|
function api(path, params) {
|
|
39
56
|
const query = new URLSearchParams(params || {})
|
|
@@ -44,190 +61,344 @@ window.__ModuleLoader__.load({
|
|
|
44
61
|
}))
|
|
45
62
|
}
|
|
46
63
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
function projectName(namespace) {
|
|
65
|
+
const value = String(namespace || '')
|
|
66
|
+
const marker = ':project:'
|
|
67
|
+
if (value.includes(marker)) return value.slice(value.indexOf(marker) + marker.length) || '当前项目'
|
|
68
|
+
if (value.includes(':global:')) return value.split(':global:').pop() || '全局记忆'
|
|
69
|
+
if (value.includes(':session:')) return '当前对话'
|
|
70
|
+
return value || '当前项目'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTime(value) {
|
|
74
|
+
if (!value) return '时间未知'
|
|
75
|
+
const date = new Date(value)
|
|
76
|
+
if (Number.isNaN(date.getTime())) return String(value)
|
|
77
|
+
const seconds = Math.round((Date.now() - date.getTime()) / 1000)
|
|
78
|
+
if (seconds >= 0 && seconds < 60) return '刚刚'
|
|
79
|
+
if (seconds >= 60 && seconds < 3600) return Math.floor(seconds / 60) + ' 分钟前'
|
|
80
|
+
const timeZone = 'Asia/Shanghai'
|
|
81
|
+
const dayKey = (input) => new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(input)
|
|
82
|
+
const clock = (input) => new Intl.DateTimeFormat('zh-CN', { timeZone, hour: '2-digit', minute: '2-digit', hour12: false }).format(input)
|
|
83
|
+
const now = new Date()
|
|
84
|
+
if (dayKey(date) === dayKey(now)) return '今天 ' + clock(date)
|
|
85
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
|
86
|
+
if (dayKey(date) === dayKey(yesterday)) return '昨天 ' + clock(date)
|
|
87
|
+
return new Intl.DateTimeFormat('zh-CN', { timeZone, month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(date)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function statusText(status) {
|
|
91
|
+
if (status === 'organized') return '已整理'
|
|
92
|
+
if (status === 'failed') return '整理失败'
|
|
93
|
+
if (status === 'waiting') return '等待整理'
|
|
94
|
+
return '整理中'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function wasStarPromptDismissed() { try { return window.localStorage?.getItem(STAR_DISMISSED_KEY) === '1' } catch { return false } }
|
|
98
|
+
function rememberStarPromptDismissal() { try { window.localStorage?.setItem(STAR_DISMISSED_KEY, '1') } catch { /* no-op */ } }
|
|
99
|
+
|
|
100
|
+
function Loading() {
|
|
101
|
+
return h('div', { className: 'sg-loading', role: 'status' }, h('div', { className: 'sg-skeleton' }), h('div', { className: 'sg-skeleton' }), h('div', { className: 'sg-skeleton' }))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function Empty({ title, copy }) {
|
|
105
|
+
return h('div', { className: 'sg-empty' }, h('strong', null, title), h('span', null, copy))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function BackBar({ label, onBack }) {
|
|
109
|
+
return h('div', { className: 'sg-backbar' }, h('button', { className: 'sg-back', onClick: onBack, title: '返回' }, h('span', { 'aria-hidden': 'true' }, '←'), label))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function FailureAlert({ count, onOpen }) {
|
|
113
|
+
if (!count) return null
|
|
114
|
+
return h('button', { className: 'sg-alert', onClick: onOpen },
|
|
115
|
+
h('span', { className: 'sg-alert-mark', 'aria-hidden': 'true' }, '⚠'),
|
|
116
|
+
h('span', null, h('span', { className: 'sg-alert-title' }, count + ' 条最近记忆尚未整理完成'), h('br'), h('span', { className: 'sg-alert-copy' }, '原始内容已经保存,不会丢失。')),
|
|
117
|
+
h('span', { className: 'sg-chevron', 'aria-hidden': 'true' }, '›'))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ProcessingAlert({ visible }) {
|
|
121
|
+
if (!visible) return null
|
|
122
|
+
return h('div', { className: 'sg-processing-alert', role: 'status', 'aria-live': 'polite' },
|
|
123
|
+
h('span', { className: 'sg-processing-icon', 'aria-hidden': 'true' }, '↻'),
|
|
124
|
+
h('span', null,
|
|
125
|
+
h('span', { className: 'sg-processing-title' }, '正在触发记忆整理'),
|
|
126
|
+
h('span', { className: 'sg-processing-copy' }, 'Block、Event 和 Element 正在生成,请稍候。')))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function SearchBox({ value, onChange }) {
|
|
130
|
+
return h('div', { className: 'sg-search' }, h('span', { className: 'sg-search-mark', 'aria-hidden': 'true' }, '⌕'), h('input', { value, onChange: (event) => onChange(event.target.value), placeholder: '搜索记忆、人物、项目、概念…', 'aria-label': '搜索长期记忆' }))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ElementTags({ elements, onOpen }) {
|
|
134
|
+
if (!Array.isArray(elements) || !elements.length) return null
|
|
135
|
+
return h('div', { className: 'sg-tags', 'aria-label': '相关事物' }, elements.map((element) => h('button', { key: element.id, className: 'sg-tag sg-tag-button', onClick: (event) => { event.stopPropagation(); onOpen(element.id) } }, element.name)))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function LongTermPage({ events, elements, project, query, setQuery, openEvent, openElement }) {
|
|
139
|
+
const normalized = query.trim().toLocaleLowerCase()
|
|
140
|
+
const visible = [...events]
|
|
141
|
+
.filter((event) => event.status !== 'forgotten' && event.status !== 'archived')
|
|
142
|
+
.filter((event) => !normalized || JSON.stringify([event.title, event.summary, event.tags, event.relatedElements, project]).toLocaleLowerCase().includes(normalized))
|
|
143
|
+
.sort((a, b) => String(b.updatedAt || b.createdAt).localeCompare(String(a.updatedAt || a.createdAt)))
|
|
144
|
+
const elementMap = new Map(elements.map((element) => [element.id, element]))
|
|
145
|
+
return h(React.Fragment, null,
|
|
146
|
+
h('div', { className: 'sg-intro' }, h('h2', null, 'AI 已形成的长期记忆'), h('p', null, '围绕当前项目整理出的经历与相关事物')),
|
|
147
|
+
h(SearchBox, { value: query, onChange: setQuery }),
|
|
148
|
+
visible.length ? h('div', { className: 'sg-feed' }, visible.map((event) => h('article', { key: event.id, className: 'sg-entry' },
|
|
149
|
+
h('button', { className: 'sg-entry sg-entry-button', style: { padding: 0, borderBottom: 0 }, onClick: () => openEvent(event) },
|
|
150
|
+
h('div', { className: 'sg-entry-title' }, event.title || '未命名记忆'),
|
|
151
|
+
event.summary ? h('div', { className: 'sg-entry-summary' }, event.summary) : null,
|
|
152
|
+
h('span', { className: 'sg-entry-chevron', 'aria-hidden': 'true' }, '›')),
|
|
153
|
+
h('div', { className: 'sg-meta' }, h('span', null, formatTime(event.updatedAt || event.createdAt)), h('span', { className: 'sg-meta-sep' }, project)),
|
|
154
|
+
h(ElementTags, { elements: (event.relatedElements || []).map((item) => elementMap.get(item.id) || item), onOpen: openElement }))))
|
|
155
|
+
: h(Empty, { title: normalized ? '没有找到匹配的长期记忆' : '还没有形成长期记忆', copy: normalized ? '换一个关键词试试。' : '近期内容完成整理后会出现在这里。' }))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function RecentPage({ blocks, project, openBlock }) {
|
|
159
|
+
const visible = [...blocks].sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
160
|
+
return h(React.Fragment, null,
|
|
161
|
+
h('div', { className: 'sg-intro' }, h('h2', null, '最近记忆'), h('p', null, 'AI 最近完整记下、正在整理的内容')),
|
|
162
|
+
visible.length ? h('div', { className: 'sg-feed' }, visible.map((block) => h('button', { key: block.id, className: 'sg-entry sg-entry-button', onClick: () => openBlock(block) },
|
|
163
|
+
h('div', { className: 'sg-entry-title' }, block.title || '近期记忆'),
|
|
164
|
+
block.summary ? h('div', { className: 'sg-entry-summary' }, block.summary) : null,
|
|
165
|
+
h('div', { className: 'sg-meta' }, h('span', null, formatTime(block.createdAt)), h('span', { className: 'sg-meta-sep' }, project), h('span', { className: 'sg-status ' + block.status }, statusText(block.status))),
|
|
166
|
+
block.status === 'organized' ? h('div', { className: 'sg-meta' }, '形成 ' + (block.relatedEvents || []).length + ' 条长期记忆,识别 ' + (block.relatedElements || []).length + ' 个相关事物') : null,
|
|
167
|
+
block.status === 'failed' ? h('div', { className: 'sg-error-note' }, '原始记忆已保存,不会丢失。') : null,
|
|
168
|
+
h('span', { className: 'sg-entry-chevron', 'aria-hidden': 'true' }, '›'))))
|
|
169
|
+
: h(Empty, { title: '还没有最近记忆', copy: '完成一些对话后,AI 完整记下的内容会出现在这里。' }))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function SourceDetails({ item, source, kind }) {
|
|
173
|
+
const messages = source?.messages || []
|
|
174
|
+
return h('div', { className: 'sg-detail-section' },
|
|
175
|
+
h('h3', { className: 'sg-section-title' }, '来源'),
|
|
176
|
+
h('div', { className: 'sg-source-label' }, h('span', { className: 'sg-source-icon', 'aria-hidden': 'true' }, '▣'), h('span', null, '当前 DeepSeek 对话')),
|
|
177
|
+
h('details', { className: 'sg-tech' }, h('summary', null, '技术详情'), h('div', { className: 'sg-tech-body' },
|
|
178
|
+
h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, kind === 'block' ? 'Block ID' : kind === 'event' ? 'Event ID' : 'Element ID'), h('span', { className: 'sg-code' }, item.id)),
|
|
179
|
+
kind === 'block' && item.turnRange ? h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, 'Turn'), h('span', null, item.turnRange.join(' - '))) : null,
|
|
180
|
+
messages.length ? messages.map((message) => h('div', { key: message.id, className: 'sg-raw-message' }, h('div', { className: 'sg-muted' }, String(message.role || '') + ' · ' + formatTime(message.createdAt)), h('div', { className: 'sg-code' }, String(message.content || '')))) : h('div', { className: 'sg-muted' }, '当前数据中没有可显示的来源消息。'))))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function EventDetail({ event, project, source, openElement, onBack, backLabel }) {
|
|
184
|
+
return h(React.Fragment, null,
|
|
185
|
+
h(BackBar, { label: backLabel, onBack }),
|
|
186
|
+
h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, event.title || '记忆详情'), event.summary ? h('p', { className: 'sg-detail-subtitle' }, event.summary) : null, h('div', { className: 'sg-meta' }, h('span', null, formatTime(event.updatedAt || event.createdAt)), h('span', { className: 'sg-meta-sep' }, project))),
|
|
187
|
+
event.narrative ? h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, 'AI 对这段经历的理解'), h('p', { className: 'sg-prose' }, event.narrative)) : null,
|
|
188
|
+
h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '相关事物'), h(ElementTags, { elements: event.relatedElements || [], onOpen: openElement })),
|
|
189
|
+
h(SourceDetails, { item: event, source, kind: 'event' }))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ElementDetail({ element, events, source, openEvent, onBack, backLabel }) {
|
|
193
|
+
const facts = []
|
|
194
|
+
if (element.currentState) facts.push(element.currentState)
|
|
195
|
+
for (const fact of element.facts || []) {
|
|
196
|
+
const value = Array.isArray(fact.value) ? fact.value.join('、') : fact.value
|
|
197
|
+
if (value && !facts.includes(String(value))) facts.push(String(value))
|
|
198
|
+
}
|
|
199
|
+
const related = events.filter((event) => (element.sourceEventIds || []).includes(event.id)).sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
200
|
+
return h(React.Fragment, null,
|
|
201
|
+
h(BackBar, { label: backLabel, onBack }),
|
|
202
|
+
h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, element.name || '相关事物'), h('p', { className: 'sg-detail-subtitle' }, 'AI 关于它目前知道这些'), h('div', { className: 'sg-meta' }, h('span', null, '相关经历 ' + related.length + ' 条'), h('span', { className: 'sg-meta-sep' }, '最近更新 ' + formatTime(element.updatedAt)))),
|
|
203
|
+
h('div', { className: 'sg-detail-section' }, facts.length ? h('ul', { className: 'sg-facts' }, facts.map((fact, index) => h('li', { key: index }, fact))) : h('div', { className: 'sg-muted' }, '暂时没有形成可展示的认识。')),
|
|
204
|
+
h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '相关经历'), related.length ? h('div', { className: 'sg-related-list' }, related.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', { className: 'sg-related-name' }, event.title), h('span', { className: 'sg-related-time' }, formatTime(event.updatedAt || event.createdAt))))) : h('div', { className: 'sg-muted' }, '没有关联经历。')),
|
|
205
|
+
h(SourceDetails, { item: element, source, kind: 'element' }))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function BlockDetail({ block, project, source, openEvent, openElement, onBack }) {
|
|
209
|
+
const events = source?.events?.length ? source.events : (block.relatedEvents || [])
|
|
210
|
+
const elements = source?.elements?.length ? source.elements : (block.relatedElements || [])
|
|
211
|
+
return h(React.Fragment, null,
|
|
212
|
+
h(BackBar, { label: '最近记忆', onBack }),
|
|
213
|
+
h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, '记忆内容'), h('p', { className: 'sg-detail-subtitle sg-prose' }, block.summary || block.title || '暂无可显示内容'), h('div', { className: 'sg-meta' }, h('span', null, formatTime(block.createdAt)), h('span', { className: 'sg-meta-sep' }, project))),
|
|
214
|
+
h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '整理结果'), block.status === 'failed' ? h('div', { className: 'sg-safe-note' }, '原始记忆已保存,不会丢失。') : null, h('div', { className: 'sg-status ' + block.status }, statusText(block.status)),
|
|
215
|
+
events.length ? h(React.Fragment, null, h('p', { className: 'sg-result-count' }, '形成经历 ' + events.length + ' 条'), h('div', null, events.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', { className: 'sg-related-name' }, event.title), h('span', { className: 'sg-chevron' }, '›'))))) : null,
|
|
216
|
+
elements.length ? h(React.Fragment, null, h('p', { className: 'sg-result-count', style: { marginTop: '15px' } }, '识别相关事物 ' + elements.length + ' 个'), h(ElementTags, { elements, onOpen: openElement })) : null,
|
|
217
|
+
block.status === 'processing' && !events.length ? h('p', { className: 'sg-error-note' }, '后续结构正在生成,完成后会显示在这里。') : null),
|
|
218
|
+
h(SourceDetails, { item: block, source, kind: 'block' }))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ProcessingStatus({ overview, blocks, onBack, refresh }) {
|
|
222
|
+
const failures = overview.failedJobDetails || []
|
|
223
|
+
const failedBlocks = blocks.filter((block) => block.status === 'failed')
|
|
224
|
+
const first = failures[0]
|
|
225
|
+
return h(React.Fragment, null,
|
|
226
|
+
h(BackBar, { label: '返回', onBack }),
|
|
227
|
+
h('div', { className: 'sg-intro' }, h('h2', null, '处理状态'), h('p', null, failures.length + ' 个任务需要继续处理')),
|
|
228
|
+
h('div', { className: 'sg-safe-note' }, '原始记忆已保存,不会丢失。'),
|
|
229
|
+
h('div', { className: 'sg-detail-section' }, h('div', { className: 'sg-pipeline' },
|
|
230
|
+
h('div', { className: 'sg-stage' }, h('span', null, '记忆保存'), h('span', { className: 'sg-stage-value done' }, '✓ 已完成')),
|
|
231
|
+
h('div', { className: 'sg-stage' }, h('span', null, '事件提取'), h('span', { className: 'sg-stage-value ' + (first?.kind === 'event-extraction' ? 'failed' : 'done') }, first?.kind === 'event-extraction' ? '× 失败' : '✓ 已完成')),
|
|
232
|
+
h('div', { className: 'sg-stage' }, h('span', null, '元素投影'), h('span', { className: 'sg-stage-value ' + (first?.kind === 'element-projection' ? 'failed' : first?.kind === 'event-extraction' ? 'waiting' : 'done') }, first?.kind === 'element-projection' ? '× 失败' : first?.kind === 'event-extraction' ? '— 等待' : '✓ 已完成')))),
|
|
233
|
+
failedBlocks.length ? h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '受影响的最近记忆'), failedBlocks.map((block) => h('div', { key: block.id, className: 'sg-result-event' }, block.title || block.summary || '近期记忆'))) : null,
|
|
234
|
+
h('button', { className: 'sg-alert', onClick: refresh, style: { marginTop: '16px' } }, h('span', { className: 'sg-alert-mark' }, '↻'), h('span', null, h('span', { className: 'sg-alert-title' }, '重新检查状态'), h('br'), h('span', { className: 'sg-alert-copy' }, '任务会沿用现有重试机制继续处理。')), h('span', { className: 'sg-chevron' }, '›')),
|
|
235
|
+
h('details', { className: 'sg-tech' }, h('summary', null, '技术错误详情'), h('pre', { className: 'sg-tech-body sg-code' }, first?.lastErrorFull || first?.lastError || '没有记录技术错误。')))
|
|
51
236
|
}
|
|
52
237
|
|
|
53
238
|
function StarPrompt({ usageRecords }) {
|
|
54
239
|
const [dismissed, setDismissed] = React.useState(wasStarPromptDismissed)
|
|
55
240
|
if (dismissed || Number(usageRecords || 0) < STAR_PROMPT_USAGE_THRESHOLD) return null
|
|
241
|
+
const dismiss = () => { rememberStarPromptDismissal(); setDismissed(true) }
|
|
242
|
+
return h('div', { className: 'sg-star', 'data-testid': 'stratagate-star-prompt' }, h('strong', null, 'StrataGate 已在当前项目中帮助使用记忆 ', usageRecords, ' 次。'), h('div', { className: 'sg-star-actions' }, h('a', { className: 'sg-link', href: STAR_REPOSITORY_URL, target: '_blank', rel: 'noopener noreferrer', onClick: dismiss }, '在 GitHub 支持项目'), h('button', { className: 'sg-quiet-button', onClick: dismiss }, '不再提示')))
|
|
243
|
+
}
|
|
56
244
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
245
|
+
function MoreHome({ selected, setView }) {
|
|
246
|
+
const rows = [
|
|
247
|
+
['structure', '◇', '记忆结构', '浏览经历与相关事物'],
|
|
248
|
+
['system', '✓', '系统状态', '处理任务和最近整理时间'],
|
|
249
|
+
['audit', '↗', '使用记录', '长期记忆何时被使用'],
|
|
250
|
+
['raw', '{}', '原始数据', 'Block、Event、Element 与模型响应'],
|
|
251
|
+
['settings', '⚙', '高级设置', 'Schema、提取间隔与项目空间'],
|
|
252
|
+
]
|
|
253
|
+
return h(React.Fragment, null, h('div', { className: 'sg-intro' }, h('h2', null, '更多'), h('p', null, '高级信息与工程视图')), h('div', { className: 'sg-menu' }, rows.map(([id, icon, title, subtitle]) => h('button', { key: id, className: 'sg-menu-row', onClick: () => setView({ name: id }) }, h('span', { className: 'sg-menu-icon', 'aria-hidden': 'true' }, icon), h('span', null, h('span', { className: 'sg-menu-title' }, title), h('br'), h('span', { className: 'sg-menu-subtitle' }, subtitle)), h('span', { className: 'sg-chevron' }, '›')))), h(StarPrompt, { usageRecords: selected.usageReceipts }))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function StructurePage({ events, elements, openEvent, openElement, onBack }) {
|
|
257
|
+
return h(React.Fragment, null,
|
|
258
|
+
h(BackBar, { label: '更多', onBack }),
|
|
259
|
+
h('div', { className: 'sg-intro' }, h('h2', null, '记忆结构'), h('p', null, '完整浏览现有结构化记忆')),
|
|
260
|
+
h('div', { className: 'sg-counts' },
|
|
261
|
+
h('div', null, h('div', { className: 'sg-count-value' }, events.length), h('div', { className: 'sg-count-label' }, '经历 / Event')),
|
|
262
|
+
h('div', null, h('div', { className: 'sg-count-value' }, elements.length), h('div', { className: 'sg-count-label' }, '相关事物 / Element'))),
|
|
263
|
+
h('div', { className: 'sg-structured-group' },
|
|
264
|
+
h('h3', { className: 'sg-section-title' }, '经历'),
|
|
265
|
+
events.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', null, event.title), h('span', { className: 'sg-related-time' }, formatTime(event.updatedAt))))),
|
|
266
|
+
h('div', { className: 'sg-structured-group' },
|
|
267
|
+
h('h3', { className: 'sg-section-title' }, '相关事物'),
|
|
268
|
+
elements.map((element) => h('button', { key: element.id, className: 'sg-related', onClick: () => openElement(element.id) }, h('span', null, element.name), h('span', { className: 'sg-related-time' }, (element.sourceEventIds || []).length + ' 条经历')))))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function SystemPage({ selected, blocks, onBack, refresh }) {
|
|
272
|
+
const processing = blocks.filter((block) => block.status === 'processing').length
|
|
273
|
+
return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '系统状态'), h('p', null, '仅在这里展示记忆处理的工程状态')), h('div', { className: 'sg-pipeline' },
|
|
274
|
+
[['Block 数量', selected.blocks], ['待处理任务', processing], ['失败任务', selected.failedJobs], ['最近整理时间', formatTime(selected.lastActivityAt)], ['Event 提取', selected.failedJobDetails?.some((item) => item.kind === 'event-extraction') ? '有失败' : '正常'], ['Element 投影', selected.failedJobDetails?.some((item) => item.kind === 'element-projection') ? '有失败' : '正常']].map(([label, value]) => h('div', { key: label, className: 'sg-stage' }, h('span', null, label), h('span', { className: 'sg-stage-value ' + (label === '失败任务' && value ? 'failed' : 'waiting') }, String(value))))), h('button', { className: 'sg-quiet-button', onClick: refresh, style: { marginTop: '14px' } }, '↻ 重新检查'))
|
|
275
|
+
}
|
|
61
276
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
function SourceMessages({ messages }) {
|
|
79
|
-
if (!Array.isArray(messages) || messages.length === 0) return h('div', { style: muted }, '没有可显示的原始消息。')
|
|
80
|
-
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, messages.map((message) =>
|
|
81
|
-
h('div', { key: message.id, style: { ...card, borderLeft: '3px solid #5794f2' } },
|
|
82
|
-
h('div', { style: { ...row, justifyContent: 'space-between' } },
|
|
83
|
-
h('strong', null, String(message.role)),
|
|
84
|
-
h('span', { style: muted }, String(message.createdAt || '') + (message.blockId ? ' · ' + message.blockId : ' · open tail'))),
|
|
85
|
-
h('pre', { style: { ...code, whiteSpace: 'pre-wrap', margin: '8px 0 0', overflowWrap: 'anywhere' } }, String(message.content || '')),
|
|
86
|
-
message.toolCalls && message.toolCalls.length
|
|
87
|
-
? h('pre', { style: { ...code, whiteSpace: 'pre-wrap', margin: '8px 0 0', overflowWrap: 'anywhere', opacity: 0.82 } }, JSON.stringify(message.toolCalls, null, 2))
|
|
88
|
-
: null)))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function Detail({ detail, onClose }) {
|
|
92
|
-
if (!detail) return null
|
|
93
|
-
return h('div', { style: { ...card, borderColor: '#5794f2' } },
|
|
94
|
-
h('div', { style: { ...row, justifyContent: 'space-between' } },
|
|
95
|
-
h('strong', null, '来源证据'),
|
|
96
|
-
h('button', { onClick: onClose }, '关闭')),
|
|
97
|
-
Array.isArray(detail.events) && detail.events.length
|
|
98
|
-
? h('div', { style: { margin: '10px 0' } }, detail.events.map((event) =>
|
|
99
|
-
h('div', { key: event.id, style: { marginBottom: '8px' } },
|
|
100
|
-
h('div', { style: { fontWeight: 600 } }, event.title),
|
|
101
|
-
h('div', { style: muted }, event.summary))))
|
|
102
|
-
: null,
|
|
103
|
-
h(SourceMessages, { messages: detail.messages || detail.sourceMessages }))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function MemoryItem({ item, kind, openSource }) {
|
|
107
|
-
const title = kind === 'events' ? item.title : kind === 'elements' ? item.name : item.title
|
|
108
|
-
const body = kind === 'events' ? item.summary : kind === 'elements' ? item.currentState : item.summary
|
|
109
|
-
const sourceParams = kind === 'events' ? { eventId: item.id } : kind === 'elements' ? { elementId: item.id } : { blockId: item.id }
|
|
110
|
-
return h('div', { style: card },
|
|
111
|
-
h('div', { style: { ...row, justifyContent: 'space-between' } },
|
|
112
|
-
h('strong', null, String(title || item.id)),
|
|
113
|
-
h('button', { onClick: () => openSource(sourceParams) }, '查看原始证据')),
|
|
114
|
-
h('div', { style: { marginTop: '6px', whiteSpace: 'pre-wrap' } }, String(body || '')),
|
|
115
|
-
h('div', { style: { ...muted, marginTop: '8px' } },
|
|
116
|
-
kind === 'events'
|
|
117
|
-
? [item.status, item.scope, item.criticality, 'confidence ' + item.confidence].join(' · ')
|
|
118
|
-
: kind === 'elements'
|
|
119
|
-
? [item.type, (item.facts || []).length + ' facts', (item.sourceEventIds || []).length + ' events'].join(' · ')
|
|
120
|
-
: ['turns ' + (item.turnRange || []).join('–'), item.sourceMessages + ' messages', 'L' + item.currentLevel].join(' · ')))
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function AuditItem({ item }) {
|
|
124
|
-
const audit = item.audit || {}
|
|
125
|
-
return h('details', { style: card },
|
|
126
|
-
h('summary', { style: { cursor: 'pointer' } },
|
|
127
|
-
h('strong', null, audit.sessionId ? '回答 · Session ' + audit.sessionId + (audit.turn !== undefined ? ' · Turn ' + audit.turn : '') : item.id),
|
|
128
|
-
h('span', { style: { ...muted, marginLeft: '8px' } }, String(item.createdAt || ''))),
|
|
129
|
-
h('div', { style: { marginTop: '10px' } },
|
|
130
|
-
h('div', { style: muted }, [audit.verdict || 'legacy receipt', audit.batchId, audit.nextStrategy].filter(Boolean).join(' · ')),
|
|
131
|
-
audit.fit ? h('p', null, h('strong', null, 'Fit: '), audit.fit) : null,
|
|
132
|
-
h('div', { style: code }, 'Evidence: ' + ((audit.evidenceRefs || []).join(', ') || 'legacy IDs only')),
|
|
133
|
-
h('div', { style: { ...muted, margin: '8px 0' } },
|
|
134
|
-
(item.events || []).length + ' events · ' + (item.elements || []).length + ' elements · ' + (item.sourceMessages || []).length + ' source messages'),
|
|
135
|
-
h('div', { style: { fontWeight: 600, margin: '10px 0 6px' } }, '采用的 Memory'),
|
|
136
|
-
[...(item.events || []), ...(item.elements || [])].map((memory) =>
|
|
137
|
-
h('div', { key: memory.id, style: { ...card, marginBottom: '6px' } },
|
|
138
|
-
h('div', { style: { ...row, justifyContent: 'space-between' } },
|
|
139
|
-
h('strong', null, memory.title || memory.name || memory.id),
|
|
140
|
-
h('span', { style: code }, memory.id)),
|
|
141
|
-
h('div', { style: muted }, memory.summary || memory.currentState || ''))),
|
|
142
|
-
h('div', { style: { fontWeight: 600, margin: '10px 0 6px' } }, '追溯到的原始证据'),
|
|
143
|
-
h(SourceMessages, { messages: item.sourceMessages })))
|
|
277
|
+
function AuditPage({ audit, onBack }) {
|
|
278
|
+
return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '使用记录'), h('p', null, '长期记忆被使用的时间与来源')), audit.length ? audit.map((item) => { const value = item.audit || {}; return h('details', { key: item.id, className: 'sg-audit' }, h('summary', null, h('strong', null, '使用了 ' + ((item.events || []).length + (item.elements || []).length) + ' 条记忆'), h('span', { className: 'sg-muted' }, ' · ' + formatTime(item.createdAt))), h('div', { className: 'sg-audit-body' }, h('div', null, (item.events || []).map((event) => event.title).join('、') || (item.elements || []).map((element) => element.name).join('、') || '旧版使用记录'), h('div', { className: 'sg-tech-row sg-audit-evidence' }, h('span', { className: 'sg-muted' }, '来源会话'), h('span', { className: 'sg-code' }, value.sessionId || '未记录')), value.turn !== undefined ? h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, 'Turn'), h('span', null, value.turn)) : null)) }) : h(Empty, { title: '还没有使用记录', copy: 'AI 在回答中采用长期记忆后会记录在这里。' }))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function RawPage({ data, selected, onBack }) {
|
|
282
|
+
const groups = [['Block raw', data.blocks], ['Event raw', data.events], ['Element raw', data.elements], ['Usage raw', data.audit], ['模型响应', selected.successfulModelResponses || []]]
|
|
283
|
+
return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '原始数据'), h('p', null, '供排查问题使用的内部字段与 JSON')), groups.map(([label, value]) => h('details', { key: label, className: 'sg-raw-group' }, h('summary', null, label + ' (' + value.length + ')'), h('pre', { className: 'sg-raw-json sg-code' }, JSON.stringify(value, null, 2)))))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function SettingsPage({ selected, namespace, onBack }) {
|
|
287
|
+
const rows = [['Schema 版本', 'v' + selected.schemaVersion], ['提取间隔', '每 ' + selected.blockTurnSize + ' 轮形成一个 Block'], ['模型', '由 DSH 当前模型配置提供'], ['项目空间 ID', namespace], ['已处理轮次', selected.currentTurn]]
|
|
288
|
+
return h(React.Fragment, null,
|
|
289
|
+
h(BackBar, { label: '更多', onBack }),
|
|
290
|
+
h('div', { className: 'sg-intro' }, h('h2', null, '高级设置'), h('p', null, '当前记忆空间的只读运行参数')),
|
|
291
|
+
h('div', { className: 'sg-pipeline' }, rows.map(([label, value]) => h('div', { key: label, className: 'sg-stage' }, h('span', null, label), h('span', { className: label === '项目空间 ID' ? 'sg-stage-value sg-code' : 'sg-stage-value' }, String(value))))))
|
|
144
292
|
}
|
|
145
293
|
|
|
146
294
|
function MemoryPage() {
|
|
147
295
|
const [overview, setOverview] = React.useState({ namespaces: [] })
|
|
148
296
|
const [namespace, setNamespace] = React.useState('')
|
|
149
|
-
const [
|
|
150
|
-
const [
|
|
297
|
+
const [section, setSection] = React.useState('long')
|
|
298
|
+
const [view, setView] = React.useState({ name: 'root' })
|
|
299
|
+
const [data, setData] = React.useState({ events: [], elements: [], blocks: [], audit: [] })
|
|
151
300
|
const [query, setQuery] = React.useState('')
|
|
152
|
-
const [
|
|
301
|
+
const [source, setSource] = React.useState(null)
|
|
153
302
|
const [loading, setLoading] = React.useState(true)
|
|
154
303
|
const [error, setError] = React.useState('')
|
|
155
304
|
|
|
156
|
-
const
|
|
157
|
-
setLoading(true)
|
|
305
|
+
const loadOverview = React.useCallback(() => {
|
|
158
306
|
setError('')
|
|
159
|
-
return api('overview').then((
|
|
160
|
-
|
|
161
|
-
if (!namespace && data.namespaces && data.namespaces[0]) setNamespace(data.namespaces[0].namespace)
|
|
162
|
-
}).catch((err) => setError(String(err.message || err))).finally(() => setLoading(false))
|
|
163
|
-
}, [namespace])
|
|
307
|
+
return api('overview').then((next) => { setOverview(next); setNamespace((current) => next.namespaces?.some((item) => item.namespace === current) ? current : (next.namespaces?.[0]?.namespace || '')); return next }).catch((reason) => { setError(String(reason.message || reason)); return null })
|
|
308
|
+
}, [])
|
|
164
309
|
|
|
165
|
-
React.
|
|
310
|
+
const loadMemoryData = React.useCallback((activeNamespace, options = {}) => {
|
|
311
|
+
const background = options.background === true
|
|
312
|
+
if (!activeNamespace) { setData({ events: [], elements: [], blocks: [], audit: [] }); if (!background) setLoading(false); return Promise.resolve() }
|
|
313
|
+
if (!background) setLoading(true)
|
|
314
|
+
return Promise.allSettled([
|
|
315
|
+
api('memories', { namespace: activeNamespace, kind: 'events', limit: '200' }),
|
|
316
|
+
api('memories', { namespace: activeNamespace, kind: 'elements', limit: '200' }),
|
|
317
|
+
api('memories', { namespace: activeNamespace, kind: 'blocks', limit: '200' }),
|
|
318
|
+
api('audit', { namespace: activeNamespace, limit: '100' }),
|
|
319
|
+
]).then((results) => {
|
|
320
|
+
const [events, elements, blocks, audit] = results
|
|
321
|
+
setData((previous) => ({
|
|
322
|
+
events: events.status === 'fulfilled' ? events.value.items || [] : previous.events,
|
|
323
|
+
elements: elements.status === 'fulfilled' ? elements.value.items || [] : previous.elements,
|
|
324
|
+
blocks: blocks.status === 'fulfilled' ? blocks.value.items || [] : previous.blocks,
|
|
325
|
+
audit: audit.status === 'fulfilled' ? audit.value.items || [] : previous.audit,
|
|
326
|
+
}))
|
|
327
|
+
const failure = results.find((result) => result.status === 'rejected')
|
|
328
|
+
if (failure && failure.status === 'rejected') setError(String(failure.reason?.message || failure.reason))
|
|
329
|
+
else setError('')
|
|
330
|
+
}).finally(() => { if (!background) setLoading(false) })
|
|
331
|
+
}, [])
|
|
166
332
|
|
|
333
|
+
React.useEffect(() => { setLoading(true); void loadOverview().finally(() => setLoading(false)) }, [])
|
|
167
334
|
React.useEffect(() => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}, [namespace,
|
|
177
|
-
|
|
178
|
-
const openSource = (params) => {
|
|
179
|
-
setError('')
|
|
180
|
-
api('sources', { namespace, ...params }).then(setDetail).catch((err) => setError(String(err.message || err)))
|
|
181
|
-
}
|
|
335
|
+
setView({ name: 'root' }); setSource(null); setData({ events: [], elements: [], blocks: [], audit: [] }); void loadMemoryData(namespace)
|
|
336
|
+
}, [namespace])
|
|
337
|
+
React.useEffect(() => {
|
|
338
|
+
if (!namespace) return undefined
|
|
339
|
+
const timer = window.setInterval(() => {
|
|
340
|
+
void Promise.all([loadOverview(), loadMemoryData(namespace, { background: true })])
|
|
341
|
+
}, 2500)
|
|
342
|
+
return () => window.clearInterval(timer)
|
|
343
|
+
}, [namespace, loadOverview, loadMemoryData])
|
|
182
344
|
|
|
183
345
|
const selected = (overview.namespaces || []).find((item) => item.namespace === namespace)
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
346
|
+
const project = projectName(namespace)
|
|
347
|
+
const failedCount = Number(selected?.failedJobs || 0)
|
|
348
|
+
const processing = !error && (Number(selected?.processingJobs || 0) > 0
|
|
349
|
+
|| data.blocks.some((block) => block.status === 'processing'))
|
|
350
|
+
const goSection = (next) => { setSection(next); setView({ name: 'root' }); setSource(null) }
|
|
351
|
+
const sourceParams = (kind, item) => kind === 'event' ? { eventId: item.id } : kind === 'element' ? { elementId: item.id } : { blockId: item.id }
|
|
352
|
+
const loadSource = (kind, item) => {
|
|
353
|
+
setSource(null)
|
|
354
|
+
void api('sources', { namespace, ...sourceParams(kind, item) }).then(setSource).catch((reason) => setError(String(reason.message || reason)))
|
|
355
|
+
}
|
|
356
|
+
const openWithSource = (kind, item) => {
|
|
357
|
+
setView((current) => ({ name: kind, item, back: current })); loadSource(kind, item)
|
|
358
|
+
}
|
|
359
|
+
const openEvent = (event) => openWithSource('event', data.events.find((item) => item.id === event.id) || event)
|
|
360
|
+
const openElement = (id) => { const element = data.elements.find((item) => item.id === id); if (element) openWithSource('element', element) }
|
|
361
|
+
const openBlock = (block) => openWithSource('block', block)
|
|
362
|
+
const goBack = () => {
|
|
363
|
+
const previous = view.back || { name: 'root' }
|
|
364
|
+
setView(previous)
|
|
365
|
+
if (previous.item && ['event', 'element', 'block'].includes(previous.name)) loadSource(previous.name, previous.item)
|
|
366
|
+
else setSource(null)
|
|
367
|
+
}
|
|
368
|
+
const backLabel = view.back?.name === 'block' ? '最近记忆' : view.back?.name === 'element' ? '相关事物' : view.back?.name === 'event' ? '长期记忆' : view.back?.name === 'structure' ? '记忆结构' : section === 'recent' ? '最近记忆' : '长期记忆'
|
|
369
|
+
const refresh = () => Promise.all([loadOverview(), loadMemoryData(namespace)])
|
|
370
|
+
const moreBack = () => setView({ name: 'root' })
|
|
371
|
+
|
|
372
|
+
let content = null
|
|
373
|
+
if (loading && !selected) content = h(Loading)
|
|
374
|
+
else if (!selected) content = h(Empty, { title: '还没有记忆', copy: '完成一些 DSH 对话后,长期记忆和最近记忆会出现在这里。' })
|
|
375
|
+
else if (view.name === 'event') content = h(EventDetail, { event: view.item, project, source, openElement, onBack: goBack, backLabel })
|
|
376
|
+
else if (view.name === 'element') content = h(ElementDetail, { element: view.item, events: data.events, source, openEvent, onBack: goBack, backLabel })
|
|
377
|
+
else if (view.name === 'block') content = h(BlockDetail, { block: view.item, project, source, openEvent, openElement, onBack: goBack })
|
|
378
|
+
else if (view.name === 'status') content = h(ProcessingStatus, { overview: selected, blocks: data.blocks, onBack: () => setView({ name: 'root' }), refresh })
|
|
379
|
+
else if (view.name === 'structure') content = h(StructurePage, { events: data.events, elements: data.elements, openEvent, openElement, onBack: moreBack })
|
|
380
|
+
else if (view.name === 'system') content = h(SystemPage, { selected, blocks: data.blocks, onBack: moreBack, refresh })
|
|
381
|
+
else if (view.name === 'audit') content = h(AuditPage, { audit: data.audit, onBack: moreBack })
|
|
382
|
+
else if (view.name === 'raw') content = h(RawPage, { data, selected, onBack: moreBack })
|
|
383
|
+
else if (view.name === 'settings') content = h(SettingsPage, { selected, namespace, onBack: moreBack })
|
|
384
|
+
else content = h(React.Fragment, null,
|
|
385
|
+
h(FailureAlert, { count: failedCount, onOpen: () => setView({ name: 'status' }) }),
|
|
386
|
+
loading ? h(Loading) : section === 'long' ? h(LongTermPage, { events: data.events, elements: data.elements, project, query, setQuery, openEvent, openElement }) : section === 'recent' ? h(RecentPage, { blocks: data.blocks, project, openBlock }) : h(MoreHome, { selected, setView }))
|
|
387
|
+
|
|
388
|
+
return h('main', { className: 'sg-memory', 'data-testid': 'stratagate-memory-ui' },
|
|
389
|
+
h('style', null, css),
|
|
390
|
+
h('header', { className: 'sg-header' }, h('div', { className: 'sg-brand' }, h('div', { className: 'sg-logo', 'aria-hidden': 'true' }, '◎'), h('div', { className: 'sg-brand-name' }, 'StrataGate Memory')), h('button', { className: 'sg-icon-button', title: '重新加载', onClick: refresh, 'aria-label': '重新加载' }, '↻')),
|
|
391
|
+
h('div', { className: 'sg-project' }, h('span', { className: 'sg-project-label' }, '当前项目:'), h('select', { className: 'sg-project-select', value: namespace, onChange: (event) => setNamespace(event.target.value), 'aria-label': '当前项目' }, (overview.namespaces || []).map((item) => h('option', { key: item.namespace, value: item.namespace }, projectName(item.namespace))))),
|
|
392
|
+
h('nav', { className: 'sg-tabs', 'aria-label': '记忆视图' }, [['long', '长期记忆'], ['recent', '最近记忆'], ['more', '更多']].map(([id, label]) => h('button', { key: id, className: 'sg-tab ' + (section === id ? 'active' : ''), onClick: () => goSection(id) }, label))),
|
|
393
|
+
error ? h('div', { className: 'sg-error' }, h('div', { className: 'sg-error-title' }, '暂时无法读取完整记忆'), h('div', null, '已显示能够读取的内容,请稍后重新加载。'), h('details', null, h('summary', null, '技术详情'), h('div', { className: 'sg-code' }, error))) : null,
|
|
394
|
+
h(ProcessingAlert, { visible: processing }),
|
|
395
|
+
content)
|
|
222
396
|
}
|
|
223
397
|
|
|
224
398
|
function apply(ctx) {
|
|
225
399
|
const slots = ctx.get('slots')
|
|
226
400
|
if (!slots) return
|
|
227
|
-
slots.inject('settings.section', () => slots.register(
|
|
228
|
-
{ name: 'settings.section', id: 'stratagate-memory', order: 32, label: () => 'StrataGate Memory' },
|
|
229
|
-
() => h(MemoryPage, null),
|
|
230
|
-
))
|
|
401
|
+
slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'stratagate-memory', order: 32, label: () => 'StrataGate Memory' }, () => h(MemoryPage, null)))
|
|
231
402
|
}
|
|
232
403
|
|
|
233
404
|
exports.name = 'stratagate-dsh'
|