dsh-memory-cloud 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-memory-cloud contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,113 @@
1
- # Temporary Holding Version
1
+ # dsh-memory-cloud
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/dsh-memory-cloud.svg)](https://www.npmjs.com/package/dsh-memory-cloud)
4
+ [![license](https://img.shields.io/npm/l/dsh-memory-cloud.svg)](./LICENSE)
5
+
6
+ 把当前 DSH(DeepSeek Harness)的**全部会话记忆**聚合成一幅可交互的 **3D 星云图**,并提供「神经元信号」皮肤切换。
7
+
8
+ DSH 没有独立的记忆库:每一条持久记忆都是 `$DSH_HOME/sessions` 里某个会话日志中的一条已提交事件。本插件冷读所有 `session.v4.jsonl.zstd`(concatenated zstd frames 包裹的 JSONL),把每个会话折叠成一个记忆星云团,再依据 fork 血缘、共同话题、同工作区、共享工具与时间邻近计算会话之间的关联边,最后用纯 Canvas 2D 自实现的 3D 投影渲染。
9
+
10
+ ## 功能
11
+
12
+ - **记忆聚合**:zstd 分帧解码 → 提取标题、真实工作目录、消息碎片、工具集、fork 血缘、关键词;过滤运行时上下文/技能目录/纯标点等注入文本。
13
+ - **关联图**:TF-IDF 加权的话题余弦相似度 + 共享工具 + 同工作区 + 时间邻近 + fork 血缘,输出带 `kind`/权重/共享词的加权边;每节点与全局上限抑制"万能 hub"。
14
+ - **两种皮肤**
15
+ - **星云**(默认):不规则絮状气体云(无球核、无硬边界,参考 NASA 星系云团),团间用细电弧连接;电流靠**沿线明暗渐变**表现传递,暗区纯黑、余晖自然衰减。
16
+ - **神经元**:细胞体 + 递归分叉的树状树突 + 突触终扣 + 弯曲轴突,共用同一套信号波与流速。
17
+ - **规模映射真实数据**:云团凝聚核数、光斑数、纤维/尘埃密度、记忆星数量、树突主干数与分叉深度,全部按该会话的实际碎片数/事件数归一化缩放。
18
+ - **分级细节(LOD)**:缩放即钻取,每放大一级就多露出一层记忆——
19
+ | 等级 | 徽章 | 触发缩放 | 显示内容 |
20
+ |---|---|---|---|
21
+ | 0 | 全景 | < 1.5× | 云团 + 会话标题 |
22
+ | 1 | 星团 | ≥ 1.5× | 标题 + 记忆条数/对话数/工具数 |
23
+ | 2 | 记忆摘要 | ≥ 2.8× | 自动聚焦最近星团并浮现碎片摘要 + 话题关键词 |
24
+ | 3 | 记忆全文 | ≥ 5× | 按需拉取该会话**完整未截断**碎片(含全文标记) |
25
+
26
+ 放大到 2 级以上时视角会平滑"飞入"目标星团,其余云团退到四周雾中;缩回后自动释放焦点。双击星团可直接钻到底。
27
+ - **交互**:拖拽/单指旋转、滚轮/双指捏合缩放、悬停 tooltip、点击云团展开该会话记忆、按标题/正文/工作区搜索、点击图例聚焦工作区。
28
+ - **移动端适配**:`viewport-fit=cover` + safe-area、横向图例、全宽底部抽屉、触摸命中放大、dpr 上限 1.25。
29
+ - **性能**:离屏渐变精灵缓存、按颜色/深度批量绘制;电流相位由真实时钟连续推进(不量化到固定帧率),渐变采样点随链路屏幕长度自适应(约每 6px 一个色标),因此波峰是亚色标级平滑移动。降级策略优先**减少采样精度**而非降低帧率,只有在持续掉帧时才回落到 30fps;移动端 dpr 上限 1.25。
30
+
31
+ ## 安装
32
+
33
+ 已发布到 npm:**<https://www.npmjs.com/package/dsh-memory-cloud>**
34
+
35
+ ```bash
36
+ # 方式一:DSH CLI
37
+ dsh plugin --profile web add dsh-memory-cloud
38
+
39
+ # 方式二:在 DSH 对话里让 agent 安装
40
+ plugin_manager install_bundle dsh-memory-cloud
41
+ ```
42
+
43
+ 也可以在 **Settings → Plugins** 里安装。安装后 bundle 会在当前 profile 全局启用。
44
+
45
+ 本地开发时改成安装目录即可(该方式为软链接,改动即时生效):
46
+
47
+ ```bash
48
+ plugin_manager install_bundle D:\dsh-memory-cloud
49
+ ```
50
+
51
+ 安装成功后,从左侧栏底部的「记忆星云」按钮打开;也可直接访问:
52
+
53
+ ```
54
+ http://127.0.0.1:3080/memory-cloud/
55
+ ```
56
+
57
+ > 该 URL 无需 DSH Web GUI 的启动 token(插件路由自带 Host 头校验,仅允许 localhost)。
58
+
59
+ ## 卸载
60
+
61
+ ```bash
62
+ dsh plugin --profile web remove dsh-memory-cloud
63
+ ```
64
+
65
+ ## 结构
66
+
67
+ | 文件 | 作用 |
68
+ |---|---|
69
+ | `index.js` | Host 半:扫描/解码会话日志、聚合记忆与关联、注册 `/memory-cloud/*` HTTP 路由 |
70
+ | `client.js` | Client 半:`sidebar.footer.action` 入口按钮 + `shell.overlay` 全屏层 + locale |
71
+ | `nebula.js` | 3D 投影、两种皮肤渲染、信号波、交互与命中测试 |
72
+ | `nebula.html` / `nebula.css` | 页面骨架与响应式深色样式 |
73
+ | `cordis.patch.yml` | Loader patch,插入插件行并声明配置 |
74
+ | `locale/*.json` | Plugin Manager 显示用的中英文标题/描述 |
75
+ | `icon.svg` / `assets/icon.svg` | 插件图标 |
76
+
77
+ ## 配置
78
+
79
+ `cordis.patch.yml` 中可调整:
80
+
81
+ ```yaml
82
+ sessionsDir: !!js dshHomePath('sessions') # 会话日志目录
83
+ maxSessions: 400 # 最多聚合多少个会话
84
+ maxFragments: 28 # 每个会话抽样的记忆碎片上限
85
+ maxFragmentChars: 220 # 单条碎片字数上限
86
+ trustedHosts: [] # 额外允许访问插件路由的 Host
87
+ ```
88
+
89
+ ## 数据接口
90
+
91
+ `GET /memory-cloud/api/memories` —— 聚合摘要(每会话最多抽样 `maxFragments` 条碎片):
92
+
93
+ ```json
94
+ {
95
+ "generatedAt": 0,
96
+ "sessions": [{ "id", "title", "workspaceLabel", "counts", "fragments", "topics", "tools", "parentSession", "createdAt", "updatedAt" }],
97
+ "workspaces": [{ "id", "label", "sessions" }],
98
+ "relations": [{ "source", "target", "weight", "kind", "kinds", "shared" }],
99
+ "stats": { "sessions", "relations", "fragments", "events", "user", "assistant", "tools", "turns", "oldest", "newest" }
100
+ }
101
+ ```
102
+
103
+ `GET /memory-cloud/api/session/:id` —— 单个会话的**完整**碎片(LOD 3 按需拉取,按文件 mtime 缓存;未知 id 返回 404):
104
+
105
+ ```json
106
+ { "id", "title", "workspaceLabel", "createdAt", "updatedAt", "counts", "fragments": [{ "kind", "text", "truncated", "time", "seq" }] }
107
+ ```
108
+
109
+ 插件对会话日志与 profile **只读**,不写入任何状态;唯一的持久化是浏览器 `localStorage` 里的皮肤选择。
110
+
111
+ ## 许可
112
+
113
+ MIT
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <defs>
3
+ <radialGradient id="core" cx="50%" cy="50%" r="50%">
4
+ <stop offset="0%" stop-color="#fff7d6"/>
5
+ <stop offset="35%" stop-color="#ffb45e"/>
6
+ <stop offset="70%" stop-color="#b455d8" stop-opacity="0.75"/>
7
+ <stop offset="100%" stop-color="#3b2a80" stop-opacity="0"/>
8
+ </radialGradient>
9
+ <radialGradient id="glow" cx="50%" cy="50%" r="50%">
10
+ <stop offset="0%" stop-color="#7c5cff" stop-opacity="0.55"/>
11
+ <stop offset="100%" stop-color="#7c5cff" stop-opacity="0"/>
12
+ </radialGradient>
13
+ </defs>
14
+ <rect width="64" height="64" rx="14" fill="#0d1030"/>
15
+ <circle cx="32" cy="32" r="27" fill="url(#glow)"/>
16
+ <g fill="none" stroke-linecap="round">
17
+ <path d="M11 36c8-14 22-20 40-13" stroke="#66d9ff" stroke-width="2.4" opacity="0.85"/>
18
+ <path d="M14 43c10-9 23-11 36-4" stroke="#b98bff" stroke-width="2.2" opacity="0.8"/>
19
+ <path d="M17 25c9-8 20-9 31-3" stroke="#ff9bc6" stroke-width="1.8" opacity="0.7"/>
20
+ </g>
21
+ <circle cx="32" cy="32" r="12" fill="url(#core)"/>
22
+ <g fill="#ffffff">
23
+ <circle cx="14" cy="35" r="1.5" opacity="0.9"/>
24
+ <circle cx="50" cy="23" r="1.7" opacity="0.95"/>
25
+ <circle cx="20" cy="18" r="1.1" opacity="0.8"/>
26
+ <circle cx="47" cy="45" r="1.3" opacity="0.85"/>
27
+ <circle cx="27" cy="50" r="1" opacity="0.75"/>
28
+ <circle cx="38" cy="14" r="1" opacity="0.7"/>
29
+ <circle cx="9" cy="47" r="0.9" opacity="0.7"/>
30
+ <circle cx="55" cy="35" r="1" opacity="0.8"/>
31
+ </g>
32
+ </svg>
package/client.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Client half of the Memory Nebula bundle.
3
+ *
4
+ * Two additive slot registrations share one tiny observable store:
5
+ * - `sidebar.footer.action`: a button beside Settings that opens the nebula.
6
+ * - `shell.overlay`: the full-frame layer that hosts the nebula iframe.
7
+ *
8
+ * The iframe loads a same-origin page served by the Host half. The nebula is
9
+ * dark-only by design, so this half only bridges open/close and Escape — it
10
+ * never forwards the host's light/dark theme. No Harness Client UI package is
11
+ * imported as a module.
12
+ */
13
+ window.__ModuleLoader__.load({
14
+ id: 'dsh-memory-cloud',
15
+ factory(require) {
16
+ const React = require('react')
17
+ const h = React.createElement
18
+ const { useEffect, useSyncExternalStore } = React
19
+
20
+ const NS = 'dsh-memory-cloud'
21
+ const DICTS = {
22
+ en: { title: 'Memory nebula', hint: 'Explore all DSH memories as a 3D nebula' },
23
+ zh: { title: '记忆星云', hint: '以 3D 星云图探索全部 DSH 记忆' },
24
+ }
25
+
26
+ // Locale-aware translation for the sidebar button. The nebula iframe page
27
+ // carries its own fixed copy; here we follow the host's active locale.
28
+ let localeApi = null
29
+ function useT() {
30
+ const snap = useSyncExternalStore(
31
+ fn => localeApi ? localeApi.subscribe(fn) : () => {},
32
+ () => localeApi ? localeApi.getSnapshot().revision : 0,
33
+ () => 0,
34
+ )
35
+ void snap
36
+ return key => {
37
+ const active = localeApi ? localeApi.getLocale().active : 'en'
38
+ const dict = DICTS[active] || DICTS.en
39
+ return dict[key] || DICTS.en[key] || key
40
+ }
41
+ }
42
+
43
+ // --- shared open/close store -------------------------------------------
44
+ let open = false
45
+ const listeners = new Set()
46
+ const store = {
47
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn) },
48
+ getSnapshot() { return open },
49
+ getServerSnapshot() { return false },
50
+ set(value) {
51
+ if (value === open) return
52
+ open = value
53
+ for (const fn of listeners) fn()
54
+ },
55
+ }
56
+
57
+ // --- nebula glyph ------------------------------------------------------
58
+ function NebulaGlyph(size) {
59
+ return h('svg', {
60
+ width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
61
+ 'aria-hidden': true, style: { display: 'block' },
62
+ },
63
+ h('circle', { cx: 12, cy: 12, r: 3.2, fill: '#ffd591' }),
64
+ h('circle', { cx: 12, cy: 12, r: 6.5, stroke: 'currentColor', strokeWidth: 1, opacity: 0.55 }),
65
+ h('path', { d: 'M3 12c3-5 7-7 12-6', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round', opacity: 0.9 }),
66
+ h('path', { d: 'M4 15c4-3 9-3 15-1', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', opacity: 0.8 }),
67
+ h('path', { d: 'M6 8c4-3 8-3 12-1', stroke: 'currentColor', strokeWidth: 1, strokeLinecap: 'round', opacity: 0.7 }),
68
+ h('circle', { cx: 4, cy: 13, r: 0.9, fill: 'currentColor' }),
69
+ h('circle', { cx: 20, cy: 9, r: 1, fill: 'currentColor' }),
70
+ h('circle', { cx: 18, cy: 18, r: 0.9, fill: 'currentColor' }),
71
+ h('circle', { cx: 8, cy: 5, r: 0.8, fill: 'currentColor' }),
72
+ )
73
+ }
74
+
75
+ // --- footer action button ---------------------------------------------
76
+ function NebulaFooterButton(props) {
77
+ const wide = props && props.wide
78
+ const active = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)
79
+ const t = useT()
80
+ return h('button', {
81
+ type: 'button',
82
+ onClick: () => store.set(!active),
83
+ title: t('hint'),
84
+ 'aria-label': t('title'),
85
+ 'aria-pressed': active,
86
+ style: {
87
+ display: 'flex', alignItems: 'center', gap: 8, width: '100%',
88
+ padding: '7px 8px', border: 0, borderRadius: 8, cursor: 'pointer',
89
+ background: active ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
90
+ color: active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)',
91
+ font: 'inherit', fontSize: 13, textAlign: 'left',
92
+ },
93
+ onMouseEnter: e => { if (!active) e.currentTarget.style.background = 'var(--dsw-alias-bg-layer-1)' },
94
+ onMouseLeave: e => { if (!active) e.currentTarget.style.background = 'transparent' },
95
+ },
96
+ h('span', { style: { flex: '0 0 auto', display: 'grid', placeItems: 'center', width: 18, height: 18 } }, NebulaGlyph(18)),
97
+ wide ? h('span', { style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, t('title')) : null,
98
+ )
99
+ }
100
+
101
+ // --- full-frame overlay ------------------------------------------------
102
+ function NebulaOverlay() {
103
+ const isOpen = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot)
104
+
105
+ useEffect(() => {
106
+ if (!isOpen) return undefined
107
+ const onMessage = event => {
108
+ if (event.origin !== location.origin || event.data?.source !== 'dsh-memory-cloud') return
109
+ if (event.data.type === 'close') store.set(false)
110
+ }
111
+ const onKeyDown = event => { if (event.key === 'Escape') store.set(false) }
112
+ window.addEventListener('message', onMessage)
113
+ window.addEventListener('keydown', onKeyDown)
114
+ return () => {
115
+ window.removeEventListener('message', onMessage)
116
+ window.removeEventListener('keydown', onKeyDown)
117
+ }
118
+ }, [isOpen])
119
+
120
+ if (!isOpen) return null
121
+ return h('div', {
122
+ style: {
123
+ position: 'fixed', inset: 0, zIndex: 1200, pointerEvents: 'auto',
124
+ // Dark-only surface: never inherit a light host background.
125
+ background: '#070a1c',
126
+ },
127
+ role: 'dialog', 'aria-modal': true, 'aria-label': '记忆星云',
128
+ },
129
+ h('iframe', {
130
+ id: 'dsh-memory-cloud-frame',
131
+ title: '记忆星云 · Memory nebula',
132
+ src: '/memory-cloud/',
133
+ style: { display: 'block', width: '100%', height: '100%', border: 0 },
134
+ }),
135
+ )
136
+ }
137
+
138
+ return {
139
+ inject: ['slots', 'locale'],
140
+ apply(ctx) {
141
+ localeApi = ctx.locale
142
+ const disposers = []
143
+ for (const [id, dict] of Object.entries(DICTS)) {
144
+ try { disposers.push(ctx.locale.register(NS, id, dict)) } catch (_) { /* namespace already owned on re-apply */ }
145
+ }
146
+ const label = () => {
147
+ const active = ctx.locale.getLocale().active
148
+ return (DICTS[active] || DICTS.en).title
149
+ }
150
+ disposers.push(ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({
151
+ name: 'sidebar.footer.action',
152
+ id: 'dsh-memory-cloud',
153
+ order: 60,
154
+ label,
155
+ }, NebulaFooterButton)))
156
+ disposers.push(ctx.slots.inject('shell.overlay', () => ctx.slots.register({
157
+ name: 'shell.overlay',
158
+ id: 'dsh-memory-cloud',
159
+ order: 100,
160
+ label,
161
+ }, NebulaOverlay)))
162
+ ctx.effect(() => () => {
163
+ for (const dispose of disposers) { try { dispose() } catch (_) {} }
164
+ localeApi = null
165
+ }, 'memory-cloud: ui registrations')
166
+ },
167
+ }
168
+ },
169
+ })
@@ -0,0 +1,12 @@
1
+ - insert:
2
+ - id: dsh-memory-cloud
3
+ name: 'dsh-memory-cloud'
4
+ config:
5
+ # All durable DSH memories live in the session logs under $DSH_HOME.
6
+ sessionsDir: !!js dshHomePath('sessions')
7
+ # Upper bounds keep the nebula responsive on profiles with long histories.
8
+ maxSessions: 400
9
+ maxFragments: 28
10
+ maxFragmentChars: 220
11
+ # Extra authorities the Host-header check accepts (host or host:port).
12
+ trustedHosts: []
package/icon.svg ADDED
@@ -0,0 +1,32 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <defs>
3
+ <radialGradient id="core" cx="50%" cy="50%" r="50%">
4
+ <stop offset="0%" stop-color="#fff7d6"/>
5
+ <stop offset="35%" stop-color="#ffb45e"/>
6
+ <stop offset="70%" stop-color="#b455d8" stop-opacity="0.75"/>
7
+ <stop offset="100%" stop-color="#3b2a80" stop-opacity="0"/>
8
+ </radialGradient>
9
+ <radialGradient id="glow" cx="50%" cy="50%" r="50%">
10
+ <stop offset="0%" stop-color="#7c5cff" stop-opacity="0.55"/>
11
+ <stop offset="100%" stop-color="#7c5cff" stop-opacity="0"/>
12
+ </radialGradient>
13
+ </defs>
14
+ <rect width="64" height="64" rx="14" fill="#0d1030"/>
15
+ <circle cx="32" cy="32" r="27" fill="url(#glow)"/>
16
+ <g fill="none" stroke-linecap="round">
17
+ <path d="M11 36c8-14 22-20 40-13" stroke="#66d9ff" stroke-width="2.4" opacity="0.85"/>
18
+ <path d="M14 43c10-9 23-11 36-4" stroke="#b98bff" stroke-width="2.2" opacity="0.8"/>
19
+ <path d="M17 25c9-8 20-9 31-3" stroke="#ff9bc6" stroke-width="1.8" opacity="0.7"/>
20
+ </g>
21
+ <circle cx="32" cy="32" r="12" fill="url(#core)"/>
22
+ <g fill="#ffffff">
23
+ <circle cx="14" cy="35" r="1.5" opacity="0.9"/>
24
+ <circle cx="50" cy="23" r="1.7" opacity="0.95"/>
25
+ <circle cx="20" cy="18" r="1.1" opacity="0.8"/>
26
+ <circle cx="47" cy="45" r="1.3" opacity="0.85"/>
27
+ <circle cx="27" cy="50" r="1" opacity="0.75"/>
28
+ <circle cx="38" cy="14" r="1" opacity="0.7"/>
29
+ <circle cx="9" cy="47" r="0.9" opacity="0.7"/>
30
+ <circle cx="55" cy="35" r="1" opacity="0.8"/>
31
+ </g>
32
+ </svg>