@modusensus/dsh-mneme 0.12.0 → 0.14.0-beta.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/README.md +51 -20
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +47 -6
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +788 -30
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +4627 -1409
- package/lib/config.js +166 -1
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/dream/decisions.js +95 -6
- package/lib/dream/sleep.js +760 -19
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +168 -21
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +22 -11
- package/lib/index.js +123 -3
- package/lib/inject.js +203 -20
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +98 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +268 -69
- package/lib/settings.js +128 -15
- package/lib/store.js +585 -26
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +282 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +145 -32
- package/package.json +39 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +788 -30
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +166 -1
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/dream/decisions.js +95 -6
- package/src/dream/sleep.js +760 -19
- package/src/dream/triage.js +139 -0
- package/src/dream.js +168 -21
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +22 -11
- package/src/index.js +123 -3
- package/src/inject.js +203 -20
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +98 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +268 -69
- package/src/settings.js +128 -15
- package/src/store.js +585 -26
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +282 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +145 -32
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +385 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +1051 -185
- package/test/conflict-freeze.test.js +45 -1
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/continuity.test.js +8 -1
- package/test/contrast.test.js +124 -12
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +7 -7
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +635 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +463 -1
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +128 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-t1.test.js +226 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// Labels, flag metadata and small formatting helpers shared by the panel pages.
|
|
2
|
+
export const TYPE_LABELS = {
|
|
3
|
+
preference: "偏好",
|
|
4
|
+
project: "项目",
|
|
5
|
+
decision: "决策",
|
|
6
|
+
history: "历史",
|
|
7
|
+
rejected_solution: "弃用方案",
|
|
8
|
+
pitfall: "教训",
|
|
9
|
+
constraint: "约束",
|
|
10
|
+
summary: "摘要",
|
|
11
|
+
document: "文档",
|
|
12
|
+
fact: "事实",
|
|
13
|
+
pattern: "模式",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 编辑器只提供常用的几类(写库层接受更多,但新建表单不必铺满)。
|
|
17
|
+
export const TYPE_OPTIONS = [
|
|
18
|
+
"preference",
|
|
19
|
+
"project",
|
|
20
|
+
"decision",
|
|
21
|
+
"constraint",
|
|
22
|
+
"pitfall",
|
|
23
|
+
"history",
|
|
24
|
+
"rejected_solution",
|
|
25
|
+
"summary",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// 筛选要覆盖写库层接受的全部类型(store.TYPES 的 10 个):只给 8 个的话,
|
|
29
|
+
// 库里已有的 pattern / document 条目在面板上筛不出来(评审 #11)。
|
|
30
|
+
export const FILTER_TYPE_OPTIONS = [
|
|
31
|
+
"preference",
|
|
32
|
+
"project",
|
|
33
|
+
"decision",
|
|
34
|
+
"history",
|
|
35
|
+
"summary",
|
|
36
|
+
"pattern",
|
|
37
|
+
"rejected_solution",
|
|
38
|
+
"pitfall",
|
|
39
|
+
"constraint",
|
|
40
|
+
"document",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const TYPE_TONES = {
|
|
44
|
+
preference: "info",
|
|
45
|
+
decision: "good",
|
|
46
|
+
constraint: "warn",
|
|
47
|
+
pitfall: "bad",
|
|
48
|
+
project: "info",
|
|
49
|
+
history: "muted",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const TYPE_ICONS = {
|
|
53
|
+
preference: "star",
|
|
54
|
+
project: "folder",
|
|
55
|
+
decision: "check",
|
|
56
|
+
constraint: "flag",
|
|
57
|
+
pitfall: "warning",
|
|
58
|
+
history: "clock",
|
|
59
|
+
rejected_solution: "remove",
|
|
60
|
+
summary: "note",
|
|
61
|
+
document: "book",
|
|
62
|
+
fact: "pin",
|
|
63
|
+
pattern: "repeat",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Entity types come from extraction, so this is a best-effort mapping with a
|
|
67
|
+
// neutral fallback rather than a closed enum.
|
|
68
|
+
const ENTITY_ICONS = {
|
|
69
|
+
person: "user",
|
|
70
|
+
people: "user",
|
|
71
|
+
agent: "robot",
|
|
72
|
+
project: "folder",
|
|
73
|
+
workspace: "workspace",
|
|
74
|
+
repo: "code",
|
|
75
|
+
technology: "code",
|
|
76
|
+
tech: "code",
|
|
77
|
+
tool: "wrench",
|
|
78
|
+
library: "puzzle",
|
|
79
|
+
concept: "lightbulb",
|
|
80
|
+
topic: "note",
|
|
81
|
+
event: "calendar",
|
|
82
|
+
place: "globe",
|
|
83
|
+
location: "globe",
|
|
84
|
+
organization: "team",
|
|
85
|
+
org: "team",
|
|
86
|
+
team: "team",
|
|
87
|
+
document: "book",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Entity types are free-form (the extractor invents them), so unknown ones fall
|
|
91
|
+
// back to what the store returned instead of being hidden.
|
|
92
|
+
const ENTITY_TYPE_LABELS = {
|
|
93
|
+
person: "人物",
|
|
94
|
+
people: "人物",
|
|
95
|
+
agent: "智能体",
|
|
96
|
+
project: "项目",
|
|
97
|
+
workspace: "工作区",
|
|
98
|
+
repo: "仓库",
|
|
99
|
+
technology: "技术",
|
|
100
|
+
tech: "技术",
|
|
101
|
+
tool: "工具",
|
|
102
|
+
library: "库",
|
|
103
|
+
concept: "概念",
|
|
104
|
+
topic: "主题",
|
|
105
|
+
event: "事件",
|
|
106
|
+
place: "地点",
|
|
107
|
+
location: "地点",
|
|
108
|
+
organization: "组织",
|
|
109
|
+
org: "组织",
|
|
110
|
+
team: "团队",
|
|
111
|
+
document: "文档",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const entityIcon = (type) => ENTITY_ICONS[String(type ?? "").toLowerCase()] ?? "link";
|
|
115
|
+
export const entityTypeLabel = (type) => ENTITY_TYPE_LABELS[String(type ?? "").toLowerCase()] ?? String(type ?? "实体");
|
|
116
|
+
|
|
117
|
+
export const typeLabel = (type) => TYPE_LABELS[type] ?? String(type ?? "其它");
|
|
118
|
+
export const typeTone = (type) => TYPE_TONES[type];
|
|
119
|
+
export const typeIcon = (type) => TYPE_ICONS[type];
|
|
120
|
+
|
|
121
|
+
const TIME = new Intl.DateTimeFormat("zh-CN", {
|
|
122
|
+
timeZone: "Asia/Shanghai",
|
|
123
|
+
month: "2-digit",
|
|
124
|
+
day: "2-digit",
|
|
125
|
+
hour: "2-digit",
|
|
126
|
+
minute: "2-digit",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export function fmtTime(iso) {
|
|
130
|
+
if (typeof iso !== "string" || iso === "") return "—";
|
|
131
|
+
const when = new Date(iso);
|
|
132
|
+
if (Number.isNaN(when.getTime())) return "—";
|
|
133
|
+
return TIME.format(when);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function clip(text, max) {
|
|
137
|
+
const str = typeof text === "string" ? text : String(text ?? "");
|
|
138
|
+
if (str.length <= max) return str;
|
|
139
|
+
return `${str.slice(0, max - 1)}…`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function splitTags(value) {
|
|
143
|
+
if (typeof value !== "string") return [];
|
|
144
|
+
return value
|
|
145
|
+
.split(/[,,]/u)
|
|
146
|
+
.map((t) => t.trim())
|
|
147
|
+
.filter((t) => t !== "");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function joinTags(tags) {
|
|
151
|
+
return Array.isArray(tags) ? tags.join(", ") : "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Error text short enough for a 32 KiB panel payload. */
|
|
155
|
+
export function errorText(error) {
|
|
156
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
157
|
+
return clip(raw, 600);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** True for a dotted flag key, used as a nested `data.flags.*` binding path. */
|
|
161
|
+
export const flagPath = (key) => `data.flags.${key}`;
|
|
162
|
+
|
|
163
|
+
/** One action id per switch: the app only tells us which action fired. */
|
|
164
|
+
export const flagAction = (key) => `flag.${key.replace(/[^A-Za-z0-9_-]/gu, "_")}`;
|
|
165
|
+
|
|
166
|
+
// Mirrors mneme's own settings page grouping (lib/client.js FEATURE_GROUPS),
|
|
167
|
+
// trimmed to the switches this panel can flip on a phone.
|
|
168
|
+
export const FLAG_GROUPS = [
|
|
169
|
+
{
|
|
170
|
+
key: "basic",
|
|
171
|
+
label: "常用",
|
|
172
|
+
note: "先动这几个总开关。",
|
|
173
|
+
items: [
|
|
174
|
+
["autoInject", "每轮自动带上相关记忆"],
|
|
175
|
+
["autoSummarize", "对话结束自动提炼成记忆"],
|
|
176
|
+
["autoDream", "后台自动合并、去重"],
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: "record",
|
|
181
|
+
label: "记录",
|
|
182
|
+
note: "它记住什么、写多细。",
|
|
183
|
+
items: [
|
|
184
|
+
["hotMemoryEnabled", "带上最近几轮原话"],
|
|
185
|
+
["injectTimePrefix", "注入当天日期"],
|
|
186
|
+
["injectGuidanceEnabled", "教 Agent 怎么用记忆"],
|
|
187
|
+
["continuityRescueEnabled", "压缩前先存快照"],
|
|
188
|
+
["memoryQualityFilter.enabled", "滤掉低质量条目"],
|
|
189
|
+
["sensitiveScanEnabled", "写入前扫描敏感内容"],
|
|
190
|
+
["documentMemoryEnabled", "长文档只存摘要卡"],
|
|
191
|
+
["profileFactsEnabled", "自动维护「Agent 眼中的你」"],
|
|
192
|
+
["codingRetrospect", "编码过程也沉淀"],
|
|
193
|
+
["distillRecords.enabled", "保留蒸馏原文"],
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
key: "write",
|
|
198
|
+
label: "写入",
|
|
199
|
+
note: "它什么时候主动写。",
|
|
200
|
+
items: [
|
|
201
|
+
["entityExtractionEnabled", "抽取人物与项目"],
|
|
202
|
+
["writeAdmission.enabled", "写入准入判定"],
|
|
203
|
+
["writeAdmission.enforce", "准入不通过就拦下"],
|
|
204
|
+
["preInjectGate.enabled", "注入前判定"],
|
|
205
|
+
["preInjectGate.enforce", "判定不通过就不注入"],
|
|
206
|
+
["profileExtractEnabled", "画像提取草稿"],
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: "workflow",
|
|
211
|
+
label: "工作流",
|
|
212
|
+
note: "后台整理与人格。",
|
|
213
|
+
items: [
|
|
214
|
+
["sleepModeEnabled", "空闲时整理记忆"],
|
|
215
|
+
["actionPipelineEnabled", "M.O. 动作图谱"],
|
|
216
|
+
["actionNudgeEnabled", "自动沉淀工作流"],
|
|
217
|
+
["personaEnabled", "注入人格卡"],
|
|
218
|
+
["personaStrictAddressing", "严格人格寻址"],
|
|
219
|
+
["llmAudit.enabled", "记录后台模型用量"],
|
|
220
|
+
["rerankEnabled", "召回结果精排"],
|
|
221
|
+
["chorographyEnabled", "代码库地志(chorography)"],
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: "retrieval",
|
|
226
|
+
label: "检索",
|
|
227
|
+
note: "它怎么把记忆找回来。",
|
|
228
|
+
items: [
|
|
229
|
+
["hybridInject", "关键词 + 语义两路"],
|
|
230
|
+
["selectiveInjectEnabled", "选择性注入"],
|
|
231
|
+
["adaptiveThresholdEnabled", "自适应阈值"],
|
|
232
|
+
["bm25SearchEnabled", "BM25 检索"],
|
|
233
|
+
["searchSemanticDedup", "语义去重"],
|
|
234
|
+
["heatEnabled", "热度衰减"],
|
|
235
|
+
["conflictFreezeEnabled", "矛盾记忆先冻结"],
|
|
236
|
+
["trustEpistemicWeighting", "可信度加权"],
|
|
237
|
+
["reflectionUpdateEnabled", "反思式更新"],
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
key: "scope",
|
|
242
|
+
label: "作用域",
|
|
243
|
+
note: "不同会话之间怎么隔离。",
|
|
244
|
+
items: [
|
|
245
|
+
["scopeEnabled", "按会话作用域标注"],
|
|
246
|
+
["strictScope", "严格按作用域过滤"],
|
|
247
|
+
["scopeState.enabled", "作用域级暂停"],
|
|
248
|
+
["scopeState.enforce", "暂停后真的不注入"],
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
key: "extra",
|
|
253
|
+
label: "其它",
|
|
254
|
+
// 上游不断加开关:这里按 /features 的 effective 动态列出「我们还没给中文名」
|
|
255
|
+
// 的布尔项,避免新功能在手机面板上静默消失(视图结构不能靠数据分支,所以
|
|
256
|
+
// 用 list + 数据行,点一行翻转一项)。
|
|
257
|
+
note: "上游新增的开关,面板还没有中文名;点一行即可切换。",
|
|
258
|
+
items: [],
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
// 枚举型旗标(不是开关,得用下拉):PUT /features 认字符串值。
|
|
263
|
+
export const ENUM_FLAGS = [
|
|
264
|
+
{
|
|
265
|
+
key: "chorographyInject",
|
|
266
|
+
label: "地志注入档位",
|
|
267
|
+
hint: "每轮往系统提示里塞多少代码库提纲:轻量只放前 6 个模块,完整放全部,关闭则一轮都不放。",
|
|
268
|
+
options: [
|
|
269
|
+
{ label: "轻量", value: "light" },
|
|
270
|
+
{ label: "完整", value: "full" },
|
|
271
|
+
{ label: "关闭", value: "off" },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
export const FLAG_LABELS = new Map();
|
|
277
|
+
for (const group of FLAG_GROUPS) {
|
|
278
|
+
for (const [key, label] of group.items) FLAG_LABELS.set(key, label);
|
|
279
|
+
}
|
|
280
|
+
for (const flag of ENUM_FLAGS) FLAG_LABELS.set(flag.key, flag.label);
|
|
281
|
+
|
|
282
|
+
export const MODE_OPTIONS = [
|
|
283
|
+
{ label: "标准", value: "standard" },
|
|
284
|
+
{ label: "轻量", value: "light" },
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
// ---- memory list filters (mirrors mneme's own filter bar) -------------------
|
|
288
|
+
export const RANGE_OPTIONS = [
|
|
289
|
+
{ label: "全部时间", value: "all" },
|
|
290
|
+
{ label: "近 7 天", value: "7d" },
|
|
291
|
+
{ label: "近 30 天", value: "30d" },
|
|
292
|
+
{ label: "近 90 天", value: "90d" },
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
export const MINIMP_OPTIONS = [
|
|
296
|
+
{ label: "不限重要度", value: "0" },
|
|
297
|
+
{ label: "★3 以上", value: "3" },
|
|
298
|
+
{ label: "★4 以上", value: "4" },
|
|
299
|
+
{ label: "仅 ★5", value: "5" },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
export const SCOPE_OPTIONS = [
|
|
303
|
+
{ label: "全部来源", value: "all" },
|
|
304
|
+
{ label: "仅已沉淀", value: "deposited" },
|
|
305
|
+
{ label: "仅已归档", value: "archived" },
|
|
306
|
+
{ label: "仅外部导入", value: "foreign" },
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
export const VIEW_OPTIONS = [
|
|
310
|
+
{ label: "平铺", value: "flat" },
|
|
311
|
+
{ label: "按月", value: "month" },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
export const RANGE_DAYS = { "7d": 7, "30d": 30, "90d": 90 };
|
|
315
|
+
|
|
316
|
+
const MONTH = new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit" });
|
|
317
|
+
const DAY = new Intl.DateTimeFormat("zh-CN", { timeZone: "Asia/Shanghai", month: "2-digit", day: "2-digit" });
|
|
318
|
+
|
|
319
|
+
/** `2026-11` group label for the month view. */
|
|
320
|
+
export function fmtMonth(iso) {
|
|
321
|
+
if (typeof iso !== "string" || iso === "") return "未知时间";
|
|
322
|
+
const when = new Date(iso);
|
|
323
|
+
if (Number.isNaN(when.getTime())) return "未知时间";
|
|
324
|
+
return MONTH.format(when).replace(/\//gu, "-");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** `07-12` for charts. */
|
|
328
|
+
export function fmtDay(iso) {
|
|
329
|
+
if (typeof iso !== "string" || iso === "") return "—";
|
|
330
|
+
const when = new Date(iso);
|
|
331
|
+
if (Number.isNaN(when.getTime())) return "—";
|
|
332
|
+
return DAY.format(when);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** 归属 line for a memory row (agent/workspace scope). */
|
|
336
|
+
export function scopeText(memo) {
|
|
337
|
+
const parts = [];
|
|
338
|
+
if (typeof memo?.agent_scope === "string" && memo.agent_scope !== "") parts.push(`Agent ${memo.agent_scope}`);
|
|
339
|
+
if (typeof memo?.workspace_scope === "string" && memo.workspace_scope !== "")
|
|
340
|
+
parts.push(`工作区 ${memo.workspace_scope}`);
|
|
341
|
+
return parts.length === 0 ? "全局" : parts.join(" · ");
|
|
342
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Small builders shared by every panel section.
|
|
2
|
+
export const b = (path) => ({ $: path });
|
|
3
|
+
export const text = (value, style) => ({ type: "text", text: value, ...(style === undefined ? {} : { style }) });
|
|
4
|
+
export const heading = (value) => text(value, { size: "subtitle", weight: "semibold" });
|
|
5
|
+
export const caption = (value) => text(value, { size: "caption" });
|
|
6
|
+
export const mono = (value) => text(value, { size: "mono" });
|
|
7
|
+
export const act = (label, id, args, icon) => ({
|
|
8
|
+
label,
|
|
9
|
+
...(icon === undefined ? {} : { icon }),
|
|
10
|
+
action: args === undefined ? { id } : { id, args },
|
|
11
|
+
});
|
|
12
|
+
export const actions = (...items) => ({ type: "actions", items });
|
|
13
|
+
export const kv = (items) => ({ type: "keyValue", items });
|
|
14
|
+
// 只接受两种来源:字面文案,或调用方显式构造好的绑定对象 { $: path }。
|
|
15
|
+
// 曾经把 "data.err" 这样的路径当字符串传进来,这里又原样当字面量下发,于是
|
|
16
|
+
// 面板把路径本身显示成一句提示(评审 #12)——绑定的构造统一由 b() 负责。
|
|
17
|
+
export const callout = (src, tone) => ({
|
|
18
|
+
type: "callout",
|
|
19
|
+
text: typeof src === "string" ? src : src,
|
|
20
|
+
tone,
|
|
21
|
+
});
|
|
22
|
+
export const fold = (label, children, expanded = false) => ({ type: "collapsible", label, expanded, children });
|
|
23
|
+
export const metric = (label, value, tone) => ({
|
|
24
|
+
type: "metric",
|
|
25
|
+
label,
|
|
26
|
+
value: b(value),
|
|
27
|
+
...(tone === undefined ? {} : { tone }),
|
|
28
|
+
});
|
|
29
|
+
export const divider = () => ({ type: "divider" });
|
|
30
|
+
// tone 只接受枚举值,不能绑定(schema 的 badgeV2.tone 是纯枚举),所以第二个
|
|
31
|
+
// 参数只能是字面色,不能是 "data.xxx" 这样的路径;文案同理,绑定要传 b(path)。
|
|
32
|
+
export const badge = (src, tone) => ({
|
|
33
|
+
type: "badge",
|
|
34
|
+
text: src,
|
|
35
|
+
...(tone === undefined ? {} : { tone }),
|
|
36
|
+
});
|
|
37
|
+
export const empty = (title, message, icon, action) => ({
|
|
38
|
+
type: "emptyState",
|
|
39
|
+
title,
|
|
40
|
+
...(message === undefined ? {} : { message }),
|
|
41
|
+
...(icon === undefined ? {} : { icon }),
|
|
42
|
+
...(action === undefined ? {} : { action }),
|
|
43
|
+
});
|
|
44
|
+
export const select = (label, bind, options, onChange) => ({
|
|
45
|
+
type: "select",
|
|
46
|
+
label,
|
|
47
|
+
bind,
|
|
48
|
+
options,
|
|
49
|
+
onChange: { id: onChange },
|
|
50
|
+
});
|
|
51
|
+
export const toggle = (label, bind, onChange) => ({ type: "toggle", label, bind, onChange: { id: onChange } });
|
|
52
|
+
|
|
53
|
+
const NOTICE_TONES = { ok: "good", warn: "warn", bad: "bad" };
|
|
54
|
+
|
|
55
|
+
export function notices(state) {
|
|
56
|
+
if (state.err) return [callout(b("data.err"), "bad")];
|
|
57
|
+
if (state.notice) return [callout(b("data.notice"), NOTICE_TONES[state.noticeTone] ?? "info")];
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const chart = (kind, categories, series, unit, captionText) => ({
|
|
62
|
+
type: "chart",
|
|
63
|
+
kind,
|
|
64
|
+
categories,
|
|
65
|
+
series,
|
|
66
|
+
...(unit === undefined ? {} : { unit }),
|
|
67
|
+
...(captionText === undefined ? {} : { caption: captionText }),
|
|
68
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 实体 section: entity catalogue with a search box, plus one entity's
|
|
2
|
+
// attributes and relations (mneme's graph ego network, without the SVG map).
|
|
3
|
+
import { act, actions, b, badge, caption, fold, heading, kv, metric, select, text } from "./common.js";
|
|
4
|
+
|
|
5
|
+
const DEPTH_OPTIONS = [
|
|
6
|
+
{ label: "只看直接关系", value: "1" },
|
|
7
|
+
{ label: "再看两层", value: "2" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function entityList() {
|
|
11
|
+
// 这里曾经按 state.entityTotal === 0 换成一块空状态:视图结构在首屏就
|
|
12
|
+
// 定死了,而数据是随后才推来的,于是实体已经有了、空状态块还挂在上面。
|
|
13
|
+
// 现在结构固定,空库的提示由 data.entityStatus 这一行文案承载。
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
type: "grid",
|
|
17
|
+
columns: 2,
|
|
18
|
+
children: [metric("实体总数", "data.entityTotalText"), metric("类型数", "data.entityTypeTotalText")],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: "form",
|
|
22
|
+
submit: { id: "entity-search" },
|
|
23
|
+
submitLabel: "搜索实体",
|
|
24
|
+
children: [{ type: "textField", label: "按名字找", bind: "data.entityQuery", placeholder: "留空显示全部" }],
|
|
25
|
+
},
|
|
26
|
+
{ type: "list", items: b("data.entities") },
|
|
27
|
+
caption(b("data.entityStatus")),
|
|
28
|
+
actions(act("刷新", "refresh", undefined, "refresh")),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function entityDetail(state) {
|
|
33
|
+
return [
|
|
34
|
+
actions(act("返回", "back", undefined, "reply"), act("刷新", "refresh", undefined, "refresh")),
|
|
35
|
+
{ type: "row", children: [heading(b("data.entityName")), badge(b("data.entityType"), "info")] },
|
|
36
|
+
caption(b("data.entityMentionText")),
|
|
37
|
+
select("关系深度", "data.entityDepth", DEPTH_OPTIONS, "entity-depth"),
|
|
38
|
+
fold("属性", [kv(b("data.entityAttrs"))], true),
|
|
39
|
+
fold("关系", [{ type: "list", items: b("data.entityRelations") }], true),
|
|
40
|
+
fold("提及它的记忆", [{ type: "list", items: b("data.entityMemories") }], false),
|
|
41
|
+
text(b("data.entityHint"), { size: "caption" }),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function entitySection(state) {
|
|
46
|
+
if (state.page === "entity") return entityDetail(state);
|
|
47
|
+
return entityList(state);
|
|
48
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// 记忆 section: list (search + filters + month view), detail, editor, and the
|
|
2
|
+
// conflict queue that mneme's own drawer keeps one click away from a memory.
|
|
3
|
+
import {
|
|
4
|
+
FILTER_TYPE_OPTIONS,
|
|
5
|
+
MINIMP_OPTIONS,
|
|
6
|
+
RANGE_OPTIONS,
|
|
7
|
+
SCOPE_OPTIONS,
|
|
8
|
+
TYPE_OPTIONS,
|
|
9
|
+
VIEW_OPTIONS,
|
|
10
|
+
typeLabel,
|
|
11
|
+
} from "../text.js";
|
|
12
|
+
import { act, actions, b, badge, caption, divider, empty, fold, heading, kv, select, text, toggle } from "./common.js";
|
|
13
|
+
|
|
14
|
+
function filterBar(state) {
|
|
15
|
+
return fold(
|
|
16
|
+
"筛选与视图",
|
|
17
|
+
[
|
|
18
|
+
select(
|
|
19
|
+
"类型",
|
|
20
|
+
"data.filter",
|
|
21
|
+
[
|
|
22
|
+
{ label: "全部类型", value: "all" },
|
|
23
|
+
...FILTER_TYPE_OPTIONS.map((type) => ({ label: typeLabel(type), value: type })),
|
|
24
|
+
],
|
|
25
|
+
"type-filter",
|
|
26
|
+
),
|
|
27
|
+
select("时间范围", "data.fRange", RANGE_OPTIONS, "set-range"),
|
|
28
|
+
select("重要度", "data.fMin", MINIMP_OPTIONS, "set-minimp"),
|
|
29
|
+
select("来源", "data.fScope", SCOPE_OPTIONS, "set-scope"),
|
|
30
|
+
select("视图", "data.fView", VIEW_OPTIONS, "set-view"),
|
|
31
|
+
toggle("语义检索", "data.fSemantic", "toggle-semantic"),
|
|
32
|
+
toggle("热度优先", "data.fHeat", "toggle-heat"),
|
|
33
|
+
],
|
|
34
|
+
state.filtersOpen,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function monthsView(state) {
|
|
39
|
+
const groups = Array.isArray(state.monthGroups) ? state.monthGroups : [];
|
|
40
|
+
if (groups.length === 0) return [{ type: "list", items: b("data.rows") }];
|
|
41
|
+
return groups.map((group) => fold(group.label, [{ type: "list", items: group.rows }], true));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function memoryList(state) {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
type: "form",
|
|
48
|
+
submit: { id: "search" },
|
|
49
|
+
submitLabel: "搜索",
|
|
50
|
+
children: [{ type: "textField", label: "搜索记忆", bind: "data.query", placeholder: "标题或正文,留空返回全部" }],
|
|
51
|
+
},
|
|
52
|
+
actions(act("新记忆", "new", undefined, "add"), act("刷新", "refresh", undefined, "refresh")),
|
|
53
|
+
filterBar(state),
|
|
54
|
+
...(state.viewMode === "month" ? monthsView(state) : [{ type: "list", items: b("data.rows") }]),
|
|
55
|
+
// 翻页用 App 原生的两段 segmented:每段最少 80dp、两段正好铺满一行,
|
|
56
|
+
// 不会像按钮组那样折成上下两行(面板只有约 297dp 宽)。
|
|
57
|
+
// 页码文案挂在 label 上(binding data.pageText),顺带替掉独立的说明行。
|
|
58
|
+
// 空列表也走同一条路:视图结构在首屏就定死了,按数据决定「要不要这块」
|
|
59
|
+
// 会留下错误的残影(列表已经推来 N 条,空状态块却还在),所以这里不放
|
|
60
|
+
// 任何依赖数据的结构分支,只把「没有匹配」写进页码文案里。
|
|
61
|
+
{
|
|
62
|
+
type: "segmented",
|
|
63
|
+
label: b("data.pageText"),
|
|
64
|
+
bind: "data.pagePick",
|
|
65
|
+
options: [
|
|
66
|
+
{ label: "上一页", value: "prev" },
|
|
67
|
+
{ label: "下一页", value: "next" },
|
|
68
|
+
],
|
|
69
|
+
onChange: { id: "page" },
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function memoryDetail(state) {
|
|
75
|
+
const archived = state.current?.archived === true;
|
|
76
|
+
return [
|
|
77
|
+
actions(
|
|
78
|
+
act("返回", "back", undefined, "reply"),
|
|
79
|
+
act("编辑", "edit", undefined, "edit"),
|
|
80
|
+
archived ? act("恢复", "unarchive", undefined, "refresh") : act("归档", "archive", undefined, "archive"),
|
|
81
|
+
),
|
|
82
|
+
text(b("data.detailTitle"), { size: "title", weight: "semibold" }),
|
|
83
|
+
{
|
|
84
|
+
type: "row",
|
|
85
|
+
children: [
|
|
86
|
+
badge(b("data.detailType"), state.detailTypeTone ?? "info"),
|
|
87
|
+
text(b("data.detailStars"), { size: "caption", weight: "medium" }),
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
...(state.detailConflict === true
|
|
91
|
+
? [{ type: "callout", text: "这条记忆已进入冲突队列,可在「设置 › 状态 › 冲突队列」里裁决。", tone: "warn" }]
|
|
92
|
+
: []),
|
|
93
|
+
kv(b("data.detailMeta")),
|
|
94
|
+
fold("正文", [text(b("data.detailContent"))], true),
|
|
95
|
+
fold("相关实体", [{ type: "list", items: b("data.detailEntities") }], false),
|
|
96
|
+
fold(
|
|
97
|
+
"来源与溯源",
|
|
98
|
+
[
|
|
99
|
+
actions(act("查看来源", "provenance", undefined, "history")),
|
|
100
|
+
text(b("data.provenanceText"), { size: "caption" }),
|
|
101
|
+
],
|
|
102
|
+
false,
|
|
103
|
+
),
|
|
104
|
+
actions(act("复制", "copy", undefined, "copy"), act("插入输入框", "insert", undefined, "paste")),
|
|
105
|
+
fold("删除这条记忆", [{ type: "confirm", id: "delete", confirmLabel: "删除", cancelLabel: "取消" }], false),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function memoryEditor(state) {
|
|
110
|
+
return [
|
|
111
|
+
actions(act("取消", "back")),
|
|
112
|
+
{
|
|
113
|
+
type: "form",
|
|
114
|
+
submit: { id: "save" },
|
|
115
|
+
submitLabel: "保存",
|
|
116
|
+
children: [
|
|
117
|
+
{ type: "textField", label: "标题", bind: "data.form.title", required: true, maxLength: 200 },
|
|
118
|
+
{ type: "textField", label: "正文", bind: "data.form.content", multiline: true, maxLines: 6, maxLength: 2000 },
|
|
119
|
+
...(state.form?.id === "" || state.form?.id === undefined
|
|
120
|
+
? [
|
|
121
|
+
select(
|
|
122
|
+
"类型",
|
|
123
|
+
"data.form.type",
|
|
124
|
+
TYPE_OPTIONS.map((type) => ({ label: typeLabel(type), value: type })),
|
|
125
|
+
"form-type",
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
: [text(`类型:${typeLabel(state.form?.type)}(保存后不能改)`, { size: "caption" })]),
|
|
129
|
+
{ type: "stepper", label: "重要度", bind: "data.form.importance", min: 1, max: 5 },
|
|
130
|
+
{ type: "textField", label: "标签", bind: "data.form.tags", placeholder: "逗号分隔" },
|
|
131
|
+
...(state.form?.id === "" || state.form?.id === undefined
|
|
132
|
+
? []
|
|
133
|
+
: [
|
|
134
|
+
{ type: "textField", label: "归属 Agent", bind: "data.form.agentScope", placeholder: "留空 = 全局" },
|
|
135
|
+
{ type: "textField", label: "归属工作区", bind: "data.form.workspaceScope", placeholder: "留空 = 全局" },
|
|
136
|
+
]),
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------------------------------------------------- conflict queue
|
|
143
|
+
export function conflictPage(state) {
|
|
144
|
+
const nodes = [
|
|
145
|
+
actions(act("返回", "back", undefined, "reply"), act("刷新", "refresh", undefined, "refresh")),
|
|
146
|
+
heading("冲突队列"),
|
|
147
|
+
caption("冻结的矛盾记忆,等待人工确认"),
|
|
148
|
+
];
|
|
149
|
+
if (state.conflictCount === 0) {
|
|
150
|
+
nodes.push(empty("没有待裁决的冲突", undefined, "check"));
|
|
151
|
+
return nodes;
|
|
152
|
+
}
|
|
153
|
+
nodes.push({ type: "list", items: b("data.conflictRows") });
|
|
154
|
+
if (state.conflictReady !== true) return nodes;
|
|
155
|
+
nodes.push(divider());
|
|
156
|
+
nodes.push(
|
|
157
|
+
fold(
|
|
158
|
+
"版本 A",
|
|
159
|
+
[
|
|
160
|
+
text(b("data.conflictATitle"), { size: "subtitle", weight: "semibold" }),
|
|
161
|
+
kv(b("data.conflictAMeta")),
|
|
162
|
+
text(b("data.conflictAContent")),
|
|
163
|
+
],
|
|
164
|
+
true,
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
nodes.push(
|
|
168
|
+
fold(
|
|
169
|
+
"版本 B",
|
|
170
|
+
[
|
|
171
|
+
text(b("data.conflictBTitle"), { size: "subtitle", weight: "semibold" }),
|
|
172
|
+
kv(b("data.conflictBMeta")),
|
|
173
|
+
text(b("data.conflictBContent")),
|
|
174
|
+
],
|
|
175
|
+
false,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
nodes.push(
|
|
179
|
+
actions(
|
|
180
|
+
act("采用 A", "conflict-pick", { winner: "a" }, "check"),
|
|
181
|
+
act("采用 B", "conflict-pick", { winner: "b" }, "check"),
|
|
182
|
+
act("都不采用", "conflict-pick", { winner: "none" }, "remove"),
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
if (state.conflictPick !== "") {
|
|
186
|
+
const label = state.conflictPick === "a" ? "采用 A" : state.conflictPick === "b" ? "采用 B" : "都不采用";
|
|
187
|
+
nodes.push({ type: "confirm", id: "conflict-win", confirmLabel: label, cancelLabel: "取消" });
|
|
188
|
+
nodes.push(caption("确认后:保留方正文追加已否决注记,另一方归档。"));
|
|
189
|
+
}
|
|
190
|
+
return nodes;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function memorySection(state) {
|
|
194
|
+
if (state.page === "conflict") return conflictPage(state);
|
|
195
|
+
if (state.page === "detail") return memoryDetail(state);
|
|
196
|
+
if (state.page === "editor") return memoryEditor(state);
|
|
197
|
+
return memoryList(state);
|
|
198
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// View builder — a phone-first rebuild of mneme's own panel.
|
|
2
|
+
//
|
|
3
|
+
// Top level: three sections in the app's own `tabs` bar (equal columns, one
|
|
4
|
+
// line, local switching). The app's tab bar needs 80dp per tab, so four tabs
|
|
5
|
+
// wrap on a phone-width panel; three fit. 运行状态 lives inside the settings tab,
|
|
6
|
+
// and so does the conflict queue (that is where its entry point sits).
|
|
7
|
+
// Because tabs switch locally, every section's data is loaded and pushed together.
|
|
8
|
+
import { entitySection } from "./entity.js";
|
|
9
|
+
import { memorySection } from "./memory.js";
|
|
10
|
+
import { settingsTab } from "./settings.js";
|
|
11
|
+
import { notices } from "./common.js";
|
|
12
|
+
|
|
13
|
+
export function buildView(state) {
|
|
14
|
+
return {
|
|
15
|
+
v: 2,
|
|
16
|
+
type: "stack",
|
|
17
|
+
accent: "app",
|
|
18
|
+
children: [
|
|
19
|
+
...notices(state),
|
|
20
|
+
{
|
|
21
|
+
type: "tabs",
|
|
22
|
+
items: [
|
|
23
|
+
{ label: "记忆", children: memorySection(state) },
|
|
24
|
+
{ label: "实体", children: entitySection(state) },
|
|
25
|
+
{ label: "设置", children: settingsTab(state) },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|