stratagate-dsh 0.2.25 → 0.2.26
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 +6 -0
- package/dist/client.js +87 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.26 - 2026-08-25
|
|
4
|
+
|
|
5
|
+
- Size Knowledge Graph nodes by their long-term importance using supporting Events, active relationships, sustained recent activity, and current-workspace affinity.
|
|
6
|
+
- Keep node sizing stable across search and type filters by deriving importance from the complete graph snapshot.
|
|
7
|
+
- Preserve selection as an independent outline and glow treatment instead of temporarily enlarging the selected node.
|
|
8
|
+
|
|
3
9
|
## 0.2.25 - 2026-08-24
|
|
4
10
|
|
|
5
11
|
- Send every sealed conversation Block through its current decayed L0–L5 representation instead of a fixed L0/L1/L2 checkpoint.
|
package/dist/client.js
CHANGED
|
@@ -207,7 +207,88 @@ window.__ModuleLoader__.load({
|
|
|
207
207
|
h('div', { className: 'sg-node-bubble-foot' }, h('span', null, eventCount + ' 条相关事件'), h('button', { type: 'button', onClick: onViewDetails }, '查看详情 →')))
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
const GRAPH_NODE_RADIUS = { peripheral: 30, normal: 38, important: 46, core: 54 }
|
|
211
|
+
const GRAPH_IMPORTANCE_TEXT = { peripheral: '边缘节点', normal: '普通节点', important: '重要节点', core: '核心节点' }
|
|
212
|
+
|
|
213
|
+
function graphNameKey(value) {
|
|
214
|
+
return String(value || '').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function graphTimestamp(value) {
|
|
218
|
+
const timestamp = Date.parse(String(value || ''))
|
|
219
|
+
return Number.isFinite(timestamp) ? timestamp : 0
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Importance is derived from the complete persisted graph snapshot. The caller
|
|
223
|
+
// deliberately computes this before search/type filters are applied, so a view
|
|
224
|
+
// change never changes the meaning of a node's size.
|
|
225
|
+
function graphNodeImportance(nodes, edges, project) {
|
|
226
|
+
const activeEdges = edges.filter((edge) => edge.status === 'active')
|
|
227
|
+
const activeSupportingEvents = (node) => (node.supportingEvents || []).filter((event) => event.status !== 'forgotten' && event.status !== 'archived')
|
|
228
|
+
const relationCounts = new Map(nodes.map((node) => [node.id, 0]))
|
|
229
|
+
const neighbors = new Map(nodes.map((node) => [node.id, new Set()]))
|
|
230
|
+
for (const edge of activeEdges) {
|
|
231
|
+
relationCounts.set(edge.fromNodeId, (relationCounts.get(edge.fromNodeId) || 0) + 1)
|
|
232
|
+
relationCounts.set(edge.toNodeId, (relationCounts.get(edge.toNodeId) || 0) + 1)
|
|
233
|
+
neighbors.get(edge.fromNodeId)?.add(edge.toNodeId)
|
|
234
|
+
neighbors.get(edge.toNodeId)?.add(edge.fromNodeId)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const projectKey = graphNameKey(project)
|
|
238
|
+
const directWorkspaceNodes = new Set(nodes.filter((node) => projectKey && [node.name, ...(node.aliases || [])].some((name) => {
|
|
239
|
+
const key = graphNameKey(name)
|
|
240
|
+
return key === projectKey || (key.length >= 5 && projectKey.length >= 5 && (key.includes(projectKey) || projectKey.includes(key)))
|
|
241
|
+
})).map((node) => node.id))
|
|
242
|
+
const workspaceAffinity = new Map(nodes.map((node) => [node.id, directWorkspaceNodes.has(node.id) ? 1 : 0]))
|
|
243
|
+
for (const nodeId of directWorkspaceNodes) {
|
|
244
|
+
for (const neighborId of neighbors.get(nodeId) || []) {
|
|
245
|
+
workspaceAffinity.set(neighborId, Math.max(workspaceAffinity.get(neighborId) || 0, .55))
|
|
246
|
+
for (const secondHopId of neighbors.get(neighborId) || []) {
|
|
247
|
+
workspaceAffinity.set(secondHopId, Math.max(workspaceAffinity.get(secondHopId) || 0, .25))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const supportCounts = new Map(nodes.map((node) => {
|
|
253
|
+
const supportingEvents = activeSupportingEvents(node)
|
|
254
|
+
return [node.id, supportingEvents.length ? new Set(supportingEvents.map((event) => event.id)).size : new Set(node.sourceEventIds || []).size]
|
|
255
|
+
}))
|
|
256
|
+
const maxSupport = Math.max(1, ...supportCounts.values())
|
|
257
|
+
const maxRelations = Math.max(1, ...relationCounts.values())
|
|
258
|
+
let latestActivity = 0
|
|
259
|
+
for (const node of nodes) {
|
|
260
|
+
latestActivity = Math.max(latestActivity, graphTimestamp(node.updatedAt))
|
|
261
|
+
for (const event of activeSupportingEvents(node)) latestActivity = Math.max(latestActivity, graphTimestamp(event.updatedAt || event.createdAt))
|
|
262
|
+
}
|
|
263
|
+
for (const edge of activeEdges) latestActivity = Math.max(latestActivity, graphTimestamp(edge.updatedAt))
|
|
264
|
+
const halfLife = 90 * 24 * 60 * 60 * 1000
|
|
265
|
+
const recency = (timestamp) => timestamp && latestActivity ? Math.exp(-Math.max(0, latestActivity - timestamp) / halfLife) : 0
|
|
266
|
+
|
|
267
|
+
const scored = nodes.map((node) => {
|
|
268
|
+
const supportingEvents = activeSupportingEvents(node)
|
|
269
|
+
const sustainedMentions = supportingEvents.map((event) => recency(graphTimestamp(event.updatedAt || event.createdAt))).sort((left, right) => right - left).slice(0, 3)
|
|
270
|
+
const recentScore = .25 * recency(graphTimestamp(node.updatedAt)) + .75 * sustainedMentions.reduce((sum, value) => sum + value, 0) / 3
|
|
271
|
+
const supportScore = Math.log1p(supportCounts.get(node.id) || 0) / Math.log1p(maxSupport)
|
|
272
|
+
const relationScore = Math.log1p(relationCounts.get(node.id) || 0) / Math.log1p(maxRelations)
|
|
273
|
+
const score = .42 * supportScore + .28 * relationScore + .15 * recentScore + .15 * (workspaceAffinity.get(node.id) || 0)
|
|
274
|
+
return { id: node.id, score }
|
|
275
|
+
})
|
|
276
|
+
const ordered = scored.map(({ score }) => score).sort((left, right) => left - right)
|
|
277
|
+
const quantile = (ratio) => ordered[Math.ceil(Math.max(0, ordered.length - 1) * ratio)] || 0
|
|
278
|
+
const spread = (ordered[ordered.length - 1] || 0) - (ordered[0] || 0)
|
|
279
|
+
const thresholds = { normal: quantile(.25), important: quantile(.6), core: quantile(.85) }
|
|
280
|
+
return new Map(scored.map(({ id, score }) => {
|
|
281
|
+
let tier
|
|
282
|
+
if (spread < .02) tier = score >= .65 ? 'core' : score >= .38 ? 'important' : 'normal'
|
|
283
|
+
else if (score >= thresholds.core) tier = 'core'
|
|
284
|
+
else if (score >= thresholds.important) tier = 'important'
|
|
285
|
+
else if (score >= thresholds.normal) tier = 'normal'
|
|
286
|
+
else tier = 'peripheral'
|
|
287
|
+
return [id, { score, tier, radius: GRAPH_NODE_RADIUS[tier] }]
|
|
288
|
+
}))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function GraphCanvas({ nodes, edges, importance, selectedId, onSelect, showSummary = false, onViewDetails }) {
|
|
211
292
|
if (!nodes.length) return h(Empty, { title: '知识图谱正在形成', copy: 'Event 会在后台分批投影为节点与关系。' })
|
|
212
293
|
const width = 760; const height = 560; const cx = width / 2; const cy = height / 2
|
|
213
294
|
const placed = nodes.slice(0, 40).map((node, index) => {
|
|
@@ -230,8 +311,9 @@ window.__ModuleLoader__.load({
|
|
|
230
311
|
}),
|
|
231
312
|
placed.map(({ node, x, y }) => {
|
|
232
313
|
const meta = NODE_META[node.type] || ['实体', '#64748b', '•']; const selected = node.id === selectedId
|
|
233
|
-
|
|
234
|
-
|
|
314
|
+
const visualImportance = importance.get(node.id) || { tier: 'normal', radius: GRAPH_NODE_RADIUS.normal }
|
|
315
|
+
return h('g', { key: node.id, className: 'sg-graph-node ' + (selected ? 'selected' : ''), 'data-importance': visualImportance.tier, onClick: () => onSelect(node.id), onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onSelect(node.id) } }, role: 'button', tabIndex: 0, 'aria-label': node.name + ',' + meta[0] + ',' + GRAPH_IMPORTANCE_TEXT[visualImportance.tier] },
|
|
316
|
+
h('circle', { cx: x, cy: y, r: visualImportance.radius, fill: meta[1], fillOpacity: '.28', stroke: meta[1], strokeWidth: selected ? '3' : '1.5' }),
|
|
235
317
|
h('text', { x, y: y - 7, textAnchor: 'middle', className: 'sg-node-name' }, node.name.slice(0, 14)),
|
|
236
318
|
h('text', { x, y: y + 14, textAnchor: 'middle', className: 'sg-node-type' }, meta[0]))
|
|
237
319
|
})),
|
|
@@ -325,6 +407,7 @@ window.__ModuleLoader__.load({
|
|
|
325
407
|
const [filtersOpen, setFiltersOpen] = React.useState(false)
|
|
326
408
|
const [fullScreen, setFullScreen] = React.useState(false)
|
|
327
409
|
const nodes = graph.nodes || []; const edges = graph.edges || []; const normalized = query.trim().toLocaleLowerCase()
|
|
410
|
+
const nodeImportance = React.useMemo(() => graphNodeImportance(nodes, edges, project), [nodes, edges, project])
|
|
328
411
|
React.useEffect(() => {
|
|
329
412
|
if (!fullScreen) return undefined
|
|
330
413
|
const previous = document.body.style.overflow
|
|
@@ -362,7 +445,7 @@ window.__ModuleLoader__.load({
|
|
|
362
445
|
filtersOpen ? filterControls : null,
|
|
363
446
|
mode === 'graph'
|
|
364
447
|
? h('div', { className: 'sg-long-layout ' + (fullScreen ? '' : 'sg-summary-layout') },
|
|
365
|
-
h(GraphCanvas, { nodes: visibleNodes, edges, selectedId: selectedNodeId, onSelect: setSelectedNodeId, showSummary: !fullScreen, onViewDetails: openExplorer }),
|
|
448
|
+
h(GraphCanvas, { nodes: visibleNodes, edges, importance: nodeImportance, selectedId: selectedNodeId, onSelect: setSelectedNodeId, showSummary: !fullScreen, onViewDetails: openExplorer }),
|
|
366
449
|
fullScreen ? h(NodeDetailPanel, { node: nodes.find((node) => node.id === selectedNodeId), nodes, edges, events, onEvent: selectEvent }) : null)
|
|
367
450
|
: h('div', { className: 'sg-long-layout ' + (fullScreen ? 'sg-timeline-layout' : 'sg-summary-layout') },
|
|
368
451
|
h(TimelineList, { events, nodes, query, filters: eventFilters, onSelect: setSelectedEventId, selectedId: selectedEventId, floating: !fullScreen, onNode: selectNode, openSource: openEvent }),
|