Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3E63DD?style=flat-square" alt="license"></a>
<a href="https://github.com/slow-stack/mneme/actions"><img src="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI" alt="CI"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-22%2B-3E63DD?style=flat-square&logo=nodedotjs&logoColor=white" alt="node"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1620%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1627%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://codecov.io/gh/slow-stack/mneme"><img src="https://img.shields.io/codecov/c/github/slow-stack/mneme/main?style=flat-square" alt="coverage"></a>
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome"></a>
</p>
Expand Down Expand Up @@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790

```bash
cd dsh-mneme && npm install
npm test # 1620 个测试
npm test # 1627 个测试
npm run stress # 三轴线压测
npm run sync # src → lib 同步
```
Expand Down Expand Up @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo

```bash
cd dsh-mneme && npm install
npm test # 1620 tests
npm test # 1627 tests
npm run stress # three-axis stress test
npm run sync # src → lib sync
```
Expand Down
4 changes: 4 additions & 0 deletions dsh-mneme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

## 🆕 新增

- **面板手动新增记忆(issue #386 第 5 条)**:面板此前只能读、改、删、导入,**没有任何手写入口**——想记一条只能切到对话里让模型调 `memory_save`,而模型看不见的东西(一次现场结论、一条口头约定)恰恰最需要手动落库。「更多操作」菜单新增「新增记忆…」,弹层收类型 / 标题 / 正文 / 重要性 / 标签,字段与 `memory_save` 工具对齐。类型下拉**刻意不含 `document`**:那是 agent 注册长文档的指针行,铸造口唯一(`registerDocument`,#230),列出来只会让人点了才被拒。**表单不放 scope / sensitivity**:显式归属走详情抽屉那条路,它带「放宽可见性」二次确认;在新建表单里顺手盖章会绕开那道确认。新增后端路由 `POST /api/dsh-mneme/create`,落库走 `service.saveWithDedupe`——去重键仍是 `(type, title, scope, sensitivity)`,所以**同类型同标题会并入既有条目而不是长出第二行**,服务端回 `action: "created" | "merged"` 决定提示语说「已新建」还是「已并入」:两者对用户是不同的事实,都报「已保存」会让人以为自己新写了一条、实际旧行被改。三处校验比 standalone 的 `POST /memories` 收紧(口径向 `/update` 看齐):`content` 要 trim 非空(面板是人工输入,空正文只可能是误操作)、`document` 提前 400、scope/sensitivity 不接受。写入准入(`write-admission.js`)对面板路径不生效——它无会话身份即放行——但路由仍显式处理 `action === "denied"` → 422,免得日后面板带上会话身份时静默吞掉拒绝。回归锁六条进 `test/api-routes-gaps.test.js`(①鉴权 401 且 GET 落 404;②六种 400 校验且库内零写入;③201 created 且 `source` 为 `panel`;④200 merged 且 `count() === 1`;⑤`denied` → 422 且理由随行;⑥落库异常 500),面板侧另有一条 `test/client.test.js` 锁「入口真的挂在树上」(菜单项确实调 `setCreateOpen(true)`、弹层确实被条件挂载、`onCreated` 确实刷新列表并选中新行、请求体逐字只有 type/title/content/importance/tags、表单不得长出 scope/sensitivity 输入框、类型下拉不得含 `document`)。评审期补的两处:**成功路径不得重置 `busy`**——提示要停 900ms 才关窗,那段时间按钮若重新可点,第二次同 type+title 会落进同一行并由 `appendContent` 把正文再追加一遍,提示还会从「已新建」翻成「已并入」,一次误触就写坏一条记忆;因此整个弹层只允许错误路径重置 `busy`,按钮文案也随之改成 `busy && !msg`,免得写完了还写着「保存中…」。另把字段锁从「整包里包含这些词」改成在 `CreateDialog` 源码切片上匹配真实请求体写法(在整包上断言 `clientSource.includes("title")` 恒真,等于没锁)。两侧都做了变异检验:面板/后端逐条把锁打回坏状态都转红(含「菜单项变哑」「弹层摘掉」「成功路径放回 setBusy(false)」「按钮文案只看 busy」「表单长回 scope」「不 trim title」「打错路由」这类只有肉眼才能发现的退化)。

## 🐛 修复

- **面板文字对比度全面达标 WCAG AA(issue #178)**:面板有 270 条 `color` 声明,此前 **70 条**在浅色或深色主题下达不到 AA(正文 4.5:1、大字号 3:1;浅色 65 条、深色 5 条,最差的两条 `.mneme-statfoot` 只有 1.26 / 1.64)。根因是面板直接拿宿主的**标签层** token 当正文色(`label-tertiary` 50 处、`label-dimmed` 4 处):这些是给分隔线/占位符用的低对比层,不是正文层。另有三处**图形**同病(不在文字口径内,按 3:1 复核):热度条与热度点的冷档用 `label-dimmed` 压在热度轨道上只有 1.14:1、关系图连线描边 1.26:1,等于把三档热度画成同一个看不见的灰。修法不是逐条换颜色,而是在四个渲染根上定义**面板自己的语义色层**:`.mneme-x,.mneme-overlay,.mneme-triggerwrap,.mneme-topentry{…}` 六条 `--mneme-*`(`fg-dim` / `graphic-dim` / `accent` / `danger` / `warn` / `ok`),每条都用 `color-mix()` 从宿主 token 混出——`fg-dim` = `label-primary` 60% + 透明(浅 4.94 / 深 6.30),其余同法。**四个根一个都不能少**:`.mneme-overlay` 与 `.mneme-topentry` 是 `createPortal` 出来的独立子树,`.mneme-triggerwrap` 挂在宿主 sidebar footer 插槽,只挂 `.mneme-x` 的话这三个根拿不到变量、静默退回旧色。顺带修掉 **5 个幻影 token**(`--dsw-alias-state-error` 19 处、`state-warning` 14 处、`state-success` 3 处、`border` 2 处、`state-warn` 1 处):这些名字宿主从未定义过,一直靠 CSS 兜底字面量在跑,宿主改配色时面板不会跟——现全部换成真名(`state-error-primary` / `static-amber-600` / `border-l1` 等)。另需分清:`label-dimmed`(4 处)与 `label-tertiary`(50 处)**都是真实存在的宿主 token**,问题不在名字而在用法——宿主把它们定位成分隔线/占位符的低对比层,面板却拿来当正文与图形色。三处 knockout 配色一并修:`.mneme-conflict-primary` 浅色主题下白字压品牌蓝 `#4176e6` 只有 4.23:1,底色换成 `--mneme-accent`(浅 5.32 / 深 7.67);`.mneme-entrybadge` 深色主题下 `bg-layer-1` 压 `#c33` 只有 3.06:1,底色换成 `--mneme-danger`;`.mneme-btndangerconfirm` 底色同步换成 `--mneme-danger`,并把写死的 `#fff` 换成 `var(--dsw-alias-bg-layer-1)`——它旧的写死白字只在旧底色上侥幸过关,底色一变浅就会在深色主题掉到 2.67:1。回归锁 `test/contrast.test.js` 五条:①把宿主 token 的浅/深取值快照硬编码进测试(CI 上没有宿主包,读不到 asar),逐条重算全部 `color` 声明的对比度,两主题都必须过,**解析不出的颜色记失败而不是跳过**(否则改成 `var(--mneme-unknown)` 就能静默溜过);②面板引用的每个 `--dsw-*` 必须在快照表里存在(幻影名回归锁);③语义色层的选择器必须恰好是那四个根(少了根主锁照样全绿——主锁只算颜色,不知道变量有没有继承到);④图形色(`background` / `stroke`,非 `color`)单独按 3:1 复核——热度条、热度点、打卡点、关系图连线都不在主锁的视野里;⑤虚线关系边不得靠 `opacity` 区分(无 `memory_id` 的边同时命中 `.mneme-gedge` 与 `.mneme-gedge-dashed`,后者的 `opacity:.7` 把 50% 描边压到浅色 2.28,而图形锁只读单条规则、看不见这种叠加)。变异检验七条:改淡 `fg-dim`、一处正文用回宿主标签色、塞回幻影名、渲染根少挂一个、热度冷档改回宿主 `label-dimmed`、一条 `color` 改成解析不出的 `var(--mneme-unknown)`、虚线边加回 `opacity:.7`,七条锁分别转红。
Expand Down
6 changes: 3 additions & 3 deletions dsh-mneme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
[![tests](https://img.shields.io/badge/tests-1620%20passed-success)](https://github.com/slow-stack/mneme)
[![tests](https://img.shields.io/badge/tests-1627%20passed-success)](https://github.com/slow-stack/mneme)
[![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
[![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
[![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
Expand Down Expand Up @@ -613,7 +613,7 @@ src/
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
└── index.js # 插件接线
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
test/ # 1620 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
test/ # 1627 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
```

Expand All @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
```bash
cd dsh-mneme
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
npm test # 运行 1620 个测试
npm test # 运行 1627 个测试
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
```
Expand Down
91 changes: 90 additions & 1 deletion dsh-mneme/lib/api.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { URL } from "node:url";
import { timingSafeEqual, randomBytes } from "node:crypto";
import { FEATURE_FLAG_SPEC } from "./settings.js";
import { TYPES } from "./store.js";
import { TYPE_FILE, renderMirrorText, renderFileHeader, parseHumanEdits, MIRROR_READONLY_TYPES } from "./mirror.js";
import { langOf } from "./lang.js";
import { computeHeat } from "./heat.js";
Expand Down Expand Up @@ -602,6 +603,94 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
}
});

// --- 新建一条记忆(面板写路径,与 /update、/delete 对称命名)---------------
// #386 第 5 条:CLI `add` 与 standalone `POST /memories` 都能建行,面板此前只
// 能看/改/删/导入。字段与 memory_save 工具对齐(type/title/content/importance/
// tags),走同一条 saveWithDedupe 路径——去重键仍是 (type, title, scope,
// sensitivity),所以「面板建」与「工具建」同标题会并入同一行,不会长出两份。
//
// 三处刻意的口径差异(都写在代码里,免得下一个人以为是抄漏):
// 1. content 要求 trim 后非空,与 /update 一致而非 standalone 的宽松版:
// 面板是人工输入,空正文只可能是误操作;standalone 面对的是程序化调用,
// 放宽是为了兼容「先建壳再补内容」的既有客户端。
// 2. document 直接 400:行铸造口唯一(registerDocument,#230),saveWithDedupe
// 会 throw,这里提前给干净错误码而不是让它落成 500。
// 3. 不接受 scope/sensitivity 字段——面板的 scope 编辑在详情抽屉里(带放宽
// 确认的安全阀,见 lib/client.js 的 MemoryDrawer),新建时就盖章 explicit
// 会绕过那道确认。传了也不报错,只是不生效(与 /update 的「字段出现即校验」
// 不同,因为这里根本没有该字段的语义)。
register({
kind: "exact",
path: "/api/dsh-mneme/create",
handler(req, res) {
try {
if (req.method !== "POST") {
sendJson(res, 404, { error: "not-found" });
return;
}
if (!requireAuth(req, res, apiToken)) return;
return readBody(req).then((text) => {
const body = parseBody(text);
if (!TYPES.has(body.type)) {
sendJson(res, 400, { error: "invalid-type" });
return;
}
if (body.type === "document") {
sendJson(res, 400, { error: "document-requires-register" });
return;
}
if (typeof body.title !== "string" || !body.title.trim()) {
sendJson(res, 400, { error: "invalid-title" });
return;
}
if (typeof body.content !== "string" || !body.content.trim()) {
sendJson(res, 400, { error: "invalid-content" });
return;
}
if (body.importance !== undefined
&& (!Number.isInteger(body.importance) || body.importance < 1 || body.importance > 5)) {
sendJson(res, 400, { error: "invalid-importance" });
return;
}
if (body.tags !== undefined && (!Array.isArray(body.tags) || !body.tags.every((tag) => typeof tag === "string"))) {
sendJson(res, 400, { error: "invalid-tags" });
return;
}
try {
const result = service.saveWithDedupe({
type: body.type,
title: body.title.trim(),
content: body.content.trim(),
importance: body.importance,
tags: body.tags ?? [],
// 面板不是工具也不是外部程序,来源要能在详情抽屉里一眼认出是谁写的。
source: "panel"
});
// #254 写入准入在无会话身份时放行(write-admission.js 首行早退),面板
// 路由恰好不带 _sessionKey,所以今天到不了这个分支。仍然显式写出来:
// 将来若把会话身份接进面板,落到 toApiList([null]) 上会变成 500,把
// 「被拒」伪装成「服务器出错」——两者的处置完全不同。
if (result.action === "denied") {
sendJson(res, 422, { error: "write-rejected", reason: result.reason });
return;
}
// action 随行透出(created/merged):面板据此提示「已并入同标题的既有条目」
// 还是「新建成功」——两者对用户是不同的事实,不能都报「已保存」。
sendJson(res, result.action === "created" ? 201 : 200, {
...service.toApiList([result.memory])[0],
action: result.action
});
} catch (error) {
ctx.logger?.warn?.(`[dsh-mneme] panel create failed: ${String(error)}`);
sendJson(res, 500, { error: "internal" });
}
});
} catch {
sendJson(res, 500, { error: "internal" });
}
}
});

// --- rules ---
register({
kind: "exact",
Expand Down Expand Up @@ -1429,7 +1518,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
});

return {
routes: 32,
routes: 33,
dispose: () => {
for (const dispose of disposers) dispose();
}
Expand Down
Loading
Loading