@modusensus/dsh-mneme 0.9.3 → 0.9.5
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 +56 -10
- package/bin/cli.mjs +215 -2
- package/docs/CONFIGURATION.md +10 -2
- package/docs/DAEMON.md +9 -2
- package/docs/DISTILL.md +66 -4
- package/docs/SHARE.md +552 -0
- package/docs/SLEEP.md +1 -1
- package/docs/STORAGE.md +1 -1
- package/lib/api-standalone.js +286 -8
- package/lib/api.js +254 -3
- package/lib/client.js +1000 -50
- package/lib/config.js +50 -1
- package/lib/document.js +15 -2
- package/lib/dream/decisions.js +88 -3
- package/lib/dream/sleep.js +63 -13
- package/lib/dream.js +66 -10
- package/lib/index.js +6 -1
- package/lib/inject.js +10 -25
- package/lib/lang.js +48 -6
- package/lib/memory-source.js +9 -0
- package/lib/organize.js +9 -3
- package/lib/pipeline.js +62 -1
- package/lib/profile-extract.js +161 -0
- package/lib/provenance-http.js +126 -0
- package/lib/serve.js +13 -0
- package/lib/service.js +70 -6
- package/lib/settings.js +17 -13
- package/lib/share-http.js +281 -0
- package/lib/share-import.js +234 -0
- package/lib/share.js +335 -0
- package/lib/store.js +272 -11
- package/lib/summarize.js +118 -8
- package/lib/tools.js +240 -1
- package/lib/user-settings.js +78 -0
- package/package.json +1 -1
- package/src/api-standalone.js +286 -8
- package/src/api.js +254 -3
- package/src/config.js +50 -1
- package/src/document.js +15 -2
- package/src/dream/decisions.js +88 -3
- package/src/dream/sleep.js +63 -13
- package/src/dream.js +66 -10
- package/src/index.js +6 -1
- package/src/inject.js +10 -25
- package/src/lang.js +48 -6
- package/src/memory-source.js +9 -0
- package/src/organize.js +9 -3
- package/src/pipeline.js +62 -1
- package/src/profile-extract.js +161 -0
- package/src/provenance-http.js +126 -0
- package/src/serve.js +13 -0
- package/src/service.js +70 -6
- package/src/settings.js +17 -13
- package/src/share-http.js +281 -0
- package/src/share-import.js +234 -0
- package/src/share.js +335 -0
- package/src/store.js +272 -11
- package/src/summarize.js +118 -8
- package/src/tools.js +240 -1
- package/src/user-settings.js +78 -0
- package/test/api.test.js +384 -3
- package/test/audit.test.js +2 -2
- package/test/cli-receive.test.js +390 -0
- package/test/cli-share.test.js +437 -0
- package/test/client-share.test.js +225 -0
- package/test/client.test.js +215 -6
- package/test/dedupe-candidate-window.test.js +180 -0
- package/test/dream-candidate-hybrid.test.js +2 -2
- package/test/dream-fence.test.js +164 -0
- package/test/dream.test.js +87 -5
- package/test/helpers/dream-mock.js +6 -2
- package/test/lang.test.js +11 -0
- package/test/llm-message-source.test.js +1 -1
- package/test/mint-source.test.js +1 -1
- package/test/organize-tool.test.js +232 -0
- package/test/persona-addressing.test.js +83 -0
- package/test/persona-settings.test.js +30 -8
- package/test/pipeline-r1r2.test.js +129 -0
- package/test/policy-epoch.test.js +2 -2
- package/test/profile-extract.test.js +269 -0
- package/test/provenance-http.test.js +184 -0
- package/test/readme-claims.test.js +45 -0
- package/test/reasoning-effort.test.js +10 -10
- package/test/scope-candidates.test.js +7 -4
- package/test/scope-state.test.js +130 -1
- package/test/serve.test.js +62 -0
- package/test/share-foreign-rows.test.js +144 -0
- package/test/share-http.test.js +144 -0
- package/test/share-import.test.js +577 -0
- package/test/share-imported-guard.test.js +311 -0
- package/test/share-imported-identity.test.js +111 -0
- package/test/share-pack.test.js +511 -0
- package/test/share-receive-http.test.js +756 -0
- package/test/share-routes.test.js +337 -0
- package/test/share-store.test.js +133 -0
- package/test/share-validate.test.js +101 -0
- package/test/standalone-api.test.js +524 -5
- package/test/summarize-parse-diagnostics.test.js +75 -0
- package/test/summarize.test.js +240 -2
- package/test/tools-dto-schema-matrix.test.js +48 -5
- package/test/tools.test.js +6 -1
- package/test/user-settings-preview.test.js +127 -0
package/lib/api.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
5
6
|
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
6
7
|
import { TYPES } from "./store.js";
|
|
@@ -8,11 +9,17 @@ import { TYPE_FILE, renderMirrorText, renderFileHeader, parseHumanEdits, MIRROR_
|
|
|
8
9
|
import { langOf } from "./lang.js";
|
|
9
10
|
import { computeHeat } from "./heat.js";
|
|
10
11
|
import { EFFORT_REJECT_RE, describeStreamFailure, resolveRoute, withEffortFallback } from "./dream.js";
|
|
12
|
+
// Issue #521:面板要按归一化原因显示 skipped,归一化只有一份实现(住在
|
|
13
|
+
// decisions.js,与产生这些 error 的校验器同一文件,避免措辞漂移)。
|
|
14
|
+
import { summarizeSkipped } from "./dream/decisions.js";
|
|
11
15
|
import { getInjectionSnapshot } from "./inject.js";
|
|
16
|
+
import { renderUserSettingsBlock } from "./user-settings.js";
|
|
17
|
+
import { parseProvenanceQuery, provenanceReply } from "./provenance-http.js";
|
|
12
18
|
import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
|
|
13
19
|
import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
|
|
14
20
|
import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
|
|
15
21
|
import { parseReclaimQuery, parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
|
|
22
|
+
import { parseShareBody, shareReply, shareFilename, parseReceiveBody, receiveReply, parseUndoBody, undoReply } from "./share-http.js";
|
|
16
23
|
import { readPipelineDoc, loadPipelines, projectPipelineDir } from "./pipeline.js";
|
|
17
24
|
import { loadRunsById, runsDir, RUNS_SUFFIX } from "./pipeline-runs.js";
|
|
18
25
|
import { observationDir, loadObservationsById } from "./pipeline-observations.js";
|
|
@@ -182,7 +189,7 @@ function parseBody(text) {
|
|
|
182
189
|
// maintenance 是第 9 个位置参数,默认 null:既有调用点(测试里的 14 处)全是位置传参、
|
|
183
190
|
// 不传它,必须继续工作——不传时 /maintenance/reclaim 回 503 maintenance-unavailable,
|
|
184
191
|
// 而不是把「没接线」伪装成崩溃(#386 第 4 条)。
|
|
185
|
-
export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "", config = null, maintenance = null) {
|
|
192
|
+
export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "", config = null, maintenance = null, profileExtract = null) {
|
|
186
193
|
const disposers = [];
|
|
187
194
|
|
|
188
195
|
// #182 注入状态探测:面板状态页的「极简模式下注入按宿主设计关闭」提示需要
|
|
@@ -386,6 +393,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
386
393
|
...m,
|
|
387
394
|
archived: rows[i].archived === true || rows[i].archived === 1,
|
|
388
395
|
quality_score: rows[i].quality_score ?? null,
|
|
396
|
+
// 外来区的包号:抽屉里的「收起这个包」按 pack_id 整批删,寻址键必须由行自己带上。
|
|
397
|
+
// 与 heat 同一个做法——**缺省就不带键**,面板的撤销按钮因此天然自门控。
|
|
398
|
+
// /search 那一面为什么没有这一格,别说错原因:召回侧的候选行内存里带着
|
|
399
|
+
// share_pack_id(searchMemories 只是把 source 盖成渠道名),toApiList 也从不透出这一列;
|
|
400
|
+
// 真正的事实是这层扩展只长在 /list 的答复上,搜索面没有一处代码去补它。所以搜索态
|
|
401
|
+
// 按不了撤销,那条代价写在 docs/SHARE.md 的车道 D 一节。
|
|
402
|
+
// 扩展点留在 HTTP 层而不是 toApiList:模型工具严格复用后者(src/service.js:2094 与
|
|
403
|
+
// MEMORY_ITEM_SCHEMA),给它们加一格没有第二个消费者,却要多改一处 schema 声明。
|
|
404
|
+
...(rows[i].share_pack_id ? { share_pack_id: rows[i].share_pack_id } : {}),
|
|
389
405
|
...(heatOn ? { heat: computeHeat(rows[i], Date.now(), config ?? {}) } : {})
|
|
390
406
|
}));
|
|
391
407
|
// Total honors the same filters as the rows, or the pager's
|
|
@@ -488,6 +504,49 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
488
504
|
}
|
|
489
505
|
});
|
|
490
506
|
|
|
507
|
+
// --- 画像提取(issue #497):一次性从活跃记忆出「助手眼中的用户」**草稿** ------
|
|
508
|
+
// 语义上它是「读 + 一次模型调用」,不是写端点:草稿回给面板,采用与否是用户在那里
|
|
509
|
+
// 另一次显式 PUT /profile。所以这个路由永远不碰 user_settings——画像正文是用户
|
|
510
|
+
// 手写的原文,让模型合并等于给它改写权,而改写后的文本每帧都注入。
|
|
511
|
+
register({
|
|
512
|
+
kind: "exact",
|
|
513
|
+
path: "/api/dsh-mneme/profile/extract",
|
|
514
|
+
// policy 是 `token` 而不是 `token-write`:这条路由**没有读分支**(非 POST 一律 405),
|
|
515
|
+
// 声明成 token-write 会让 SECURITY.md 的开放读清单按「GET 开放」把它算进去——
|
|
516
|
+
// 那张表是代码的投影,不是愿望清单(test/security-doc.test.js 就是钉这件事的)。
|
|
517
|
+
policy: "token",
|
|
518
|
+
handler(req, res) {
|
|
519
|
+
// 方法白名单在碰任何副作用之前:#489 的教训是「只拦 POST/PUT,其余直落
|
|
520
|
+
// run(null)」——那等于给带 token 的 PATCH/DELETE 开了一条静默花一次 LLM 的口子。
|
|
521
|
+
// 顺序也要紧:405 若挪到 readBody 之后,一个不带 body 的方法会把请求永远挂在
|
|
522
|
+
// 读体上(把这道判断改成只拦 GET 时实测卡住整条用例),等于一个免费的挂起面。
|
|
523
|
+
if (req.method !== "POST") {
|
|
524
|
+
res.writeHead(405, { Allow: "POST" });
|
|
525
|
+
res.end("");
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
if (config?.profileExtractEnabled !== true) return sendJson(res, 409, { error: "disabled" });
|
|
529
|
+
if (typeof profileExtract !== "function") return sendJson(res, 503, { error: "unavailable" });
|
|
530
|
+
return readBody(req, res).then((text) => {
|
|
531
|
+
if (text === null) return undefined; // 超限已在 readBody 里回过 413
|
|
532
|
+
return profileExtract().then(
|
|
533
|
+
(out) => sendJson(res, 200, out),
|
|
534
|
+
(error) => {
|
|
535
|
+
// 「没东西可提」与「模型没配合」必须能被调用方区分:把它们都压成 200 +
|
|
536
|
+
// 空草稿,面板就会把故障显示成「这次没提炼出东西」(#493 同一条教训)。
|
|
537
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
538
|
+
if (code === "no-input") return sendJson(res, 409, { error: code });
|
|
539
|
+
// 没配模型是「环境缺东西」,与「模型不配合」两码:前者重跑也一样,后者可重试。
|
|
540
|
+
if (code === "no-route") return sendJson(res, 503, { error: code });
|
|
541
|
+
if (code === "unparseable" || code === "stream-failed") return sendJson(res, 502, { error: code });
|
|
542
|
+
ctx.logger?.warn?.(`dsh-mneme: profile extract failed: ${String(error?.message ?? error)}`);
|
|
543
|
+
return sendJson(res, 500, { error: "internal" });
|
|
544
|
+
}
|
|
545
|
+
);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
491
550
|
// --- agent persona(人格定义页,beta) -------------------------------------
|
|
492
551
|
// 形状照抄 /profile 路由(设计稿②):写方法走 requireAuth + readBody + parseBody。
|
|
493
552
|
// v1.2 多卡:GET 回全量 { default, byAgent };PUT 双形——单对象形(旧面板)只更新
|
|
@@ -519,7 +578,20 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
519
578
|
sendJson(res, 200, { persona: settings.getPersona() });
|
|
520
579
|
});
|
|
521
580
|
}
|
|
522
|
-
|
|
581
|
+
const url = new URL(req.url, "http://localhost");
|
|
582
|
+
// 预览(面板「Agent 眼中的画像」用):由注入器同一个渲染函数产出,前端只显示
|
|
583
|
+
// 返回的文本——以前端自己拼列表冒充「生效文本」是这个面板最容易漂的地方。
|
|
584
|
+
// ?agent=<身份> 让面板能逐张卡预览:传了就按那个身份寻址,没传按「解析不到身份」
|
|
585
|
+
// 走(也就是 default 卡的受众)。
|
|
586
|
+
// 这里不加鉴权是有意的:画像、规则、人格三处在同一条开放读面上本来就能各读一次
|
|
587
|
+
// (GET /profile 与 GET /persona 都无 token 可读),本字段只是把同样的内容拼成
|
|
588
|
+
// 模型实际看到的形状,没有新增可达面;真要锁,配 apiToken(写面与它同源)。
|
|
589
|
+
const preview = renderUserSettingsBlock({
|
|
590
|
+
settings,
|
|
591
|
+
config,
|
|
592
|
+
agentId: url.searchParams.get("agent")
|
|
593
|
+
});
|
|
594
|
+
sendJson(res, 200, { persona: settings.getPersona(), preview });
|
|
523
595
|
} catch {
|
|
524
596
|
sendJson(res, 500, { error: "internal" });
|
|
525
597
|
}
|
|
@@ -1236,6 +1308,52 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1236
1308
|
}
|
|
1237
1309
|
});
|
|
1238
1310
|
|
|
1311
|
+
// --- 记忆级溯源读出口(#399 写、#421 读)-----------------------------------
|
|
1312
|
+
// #399 把蒸馏窗口的原文落进 distill_records、把 memory → (session, seq 区间)
|
|
1313
|
+
// 的归因写进 memory_provenance,但只做了写:库里有答案、service.provenanceFor
|
|
1314
|
+
// 能查,外部(面板 / standalone API / 脚本)拿不到,「这条记忆是从哪几句蒸出来
|
|
1315
|
+
// 的」只能开 sqlite 手写 JOIN。本端点补最后一段接线。
|
|
1316
|
+
//
|
|
1317
|
+
// 一个链接一个窗口:区间 + 区间里的原文行(按 seq 升序)。没有链接的记忆
|
|
1318
|
+
// (memory_save / 工具写入 / 冷启动)返回空 windows——「不是蒸馏来的」本身是
|
|
1319
|
+
// 有效答案,不是错误,所以不 404。
|
|
1320
|
+
//
|
|
1321
|
+
// policy 是 **token**,不是 open(#509 维护者评审定):records 带的是蒸馏窗口的
|
|
1322
|
+
// 逐条会话原文,而 /list 是 open 且逐条带 id——两者串起来等于「先枚举 id、再按 id
|
|
1323
|
+
// 捞回原文」,可达结果与 /export(一次拿全库,本仓 2026-10 审计后列 gated)同量级,
|
|
1324
|
+
// 只是换了形状。抽稀不该靠「调用方不知道有这条路」。
|
|
1325
|
+
//
|
|
1326
|
+
// 口径只在 src/provenance-http.js 写一份,两个入口都调它(#411 的教训:两张皮
|
|
1327
|
+
// 整段复制必然漂移)。
|
|
1328
|
+
//
|
|
1329
|
+
// distillRecords.enabled 门(#509 评审整改):这个读出口吃的是**写侧同一枚开关、
|
|
1330
|
+
// 同一个判据**(isDistillRecordsEnabled,与 src/summarize.js:988 逐字同源;本文件
|
|
1331
|
+
// 不重写那个表达式,静态锁盯着)。关着时回 409,错误码取共用模块导出的
|
|
1332
|
+
// DISTILL_RECORDS_DISABLED——「用户关了」而不是「装配坏了」(后者是 503,
|
|
1333
|
+
// src/index.js:589-591 立的可区分性原则),也不是 404(本端点刻意不就「这条记忆
|
|
1334
|
+
// 是否存在」作断言)。cfg 是启动期冻结值,所以面板改开关要到下次重启才对这里生效;
|
|
1335
|
+
// 该键在 settings 白名单与 NESTED_FLAG_PATHS 里都登记着,面板会显示「待重启」
|
|
1336
|
+
// (#481 的教训)。阶梯全貌见 provenance-http.js 文件头。
|
|
1337
|
+
register({
|
|
1338
|
+
kind: "exact",
|
|
1339
|
+
path: "/api/dsh-mneme/memory/provenance",
|
|
1340
|
+
policy: "token",
|
|
1341
|
+
handler(req, res) {
|
|
1342
|
+
try {
|
|
1343
|
+
const url = new URL(req.url, "http://localhost");
|
|
1344
|
+
const parsed = parseProvenanceQuery(url.searchParams.get("id"));
|
|
1345
|
+
if (!parsed.ok) {
|
|
1346
|
+
sendJson(res, parsed.status, { error: parsed.error });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const { status, body } = provenanceReply({ service, id: parsed.id, config, warn: (m) => ctx.logger?.warn?.(m) });
|
|
1350
|
+
sendJson(res, status, body);
|
|
1351
|
+
} catch {
|
|
1352
|
+
sendJson(res, 500, { error: "internal" });
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1239
1357
|
// --- ego graph: 1-2 hop neighborhood of one entity (graph panel P1) ---
|
|
1240
1358
|
// Read-only like list/search/semantic, so it stays open when apiToken is set.
|
|
1241
1359
|
// BFS from the root entity over entity_relations (both directions; the
|
|
@@ -1461,6 +1579,133 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1461
1579
|
}
|
|
1462
1580
|
});
|
|
1463
1581
|
|
|
1582
|
+
// --- 分享包(spec §4)------------------------------------------------------
|
|
1583
|
+
// 读侧全库口径,鉴权与 /export 同门(token):它导出的是**脱敏后**的行集,但
|
|
1584
|
+
// 「谁在往外打整库」这件事本身就需要跟写路由一样登记在册。
|
|
1585
|
+
// GET 不开口子:这条路由的存在感来自「有人主动按分享键」,不是给面板轮询用的。
|
|
1586
|
+
// 判据一条都不在这里:哪些行进包、超没超限、抛错回什么码,全在 share-http.js 那份
|
|
1587
|
+
// 共用实现里(#411 的口径——两张皮只许剩鉴权与路径不同)。本侧独有的只有两件事:
|
|
1588
|
+
// Content-Disposition(sendJson 的 headers 第四参,数据面没有这个签名)和把
|
|
1589
|
+
// shareReply 的 warn 接到宿主 logger 上——它兜住抛错回 500,不接日志就是静默失败。
|
|
1590
|
+
register({
|
|
1591
|
+
kind: "exact",
|
|
1592
|
+
path: "/api/dsh-mneme/share",
|
|
1593
|
+
policy: "token",
|
|
1594
|
+
handler(req, res) {
|
|
1595
|
+
try {
|
|
1596
|
+
if (req.method !== "POST") {
|
|
1597
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
return readBody(req, res).then((text) => {
|
|
1601
|
+
if (text === null) return; // readBody 已经答过 413,再写一次就是双写响应
|
|
1602
|
+
const parsed = parseShareBody(text);
|
|
1603
|
+
if (!parsed.ok) {
|
|
1604
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
const out = shareReply({
|
|
1608
|
+
service,
|
|
1609
|
+
packageVersion: PACKAGE_VERSION,
|
|
1610
|
+
homeDir: homedir(),
|
|
1611
|
+
options: parsed.options,
|
|
1612
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1613
|
+
});
|
|
1614
|
+
if (out.status === 200) {
|
|
1615
|
+
sendJson(res, 200, out.body, {
|
|
1616
|
+
"Content-Disposition": `attachment; filename="${shareFilename(out.body.pack_id)}"`
|
|
1617
|
+
});
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
sendJson(res, out.status, out.body);
|
|
1621
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1622
|
+
// .catch 与下面的外层 try 各挡一类:外层那个只包得住同步那半,而 readBody 的回调
|
|
1623
|
+
// 抛出时它早已退栈。共用层从不抛(连 warn 自己坏都被守卫吞掉),所以这条链现在能被
|
|
1624
|
+
// 走到的只剩**面自己**写答复那一步——sendJson 的 writeHead 在真 socket 上会因
|
|
1625
|
+
// 「一个响应两写」抛 ERR_HTTP_HEADERS_SENT。少这一声就是 unhandled rejection +
|
|
1626
|
+
// 一个字节都没回(#410),而下面 /receive 两条一直带着它。
|
|
1627
|
+
} catch {
|
|
1628
|
+
sendJson(res, 500, { error: "internal" });
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
// --- 分享包入库与整包退出(车道 C,spec §5)--------------------------------
|
|
1634
|
+
// 写库的一面(token-write:非 GET 要 Bearer)。判据一条都不在这里:包体合格不合格住在
|
|
1635
|
+
// share.js 的闸门(由 service 的 importSharePack 调),错误码 → 状态的映射住在
|
|
1636
|
+
// share-http.js 那份共用实现里——#411 的口径是两张皮只许剩鉴权与路径不同。本侧独有的
|
|
1637
|
+
// 只有把 warn 接到宿主 logger 上(不接就是一条静默的 500,现场只剩 internal)。
|
|
1638
|
+
//
|
|
1639
|
+
// 非 POST 一律 405。这**故意**与上面的 /maintenance/reclaim 不同(那边掉到末尾 404):
|
|
1640
|
+
// 每条 receive 都有一个数据面双胞胎(C6 的 POST /receive、/receive/undo),那里只认
|
|
1641
|
+
// 405,状态码差一次就是第三处差别——一致性在这里比「照抄本文件的邻居」更硬。
|
|
1642
|
+
//
|
|
1643
|
+
// 外层 try 与 .catch 都要留着:外层那个在 .then 的回调里已经退栈,回调里抛出去就是没人接
|
|
1644
|
+
// 的 rejection,客户端拿到的不是 500 而是「一个字节都没回」(#410)。
|
|
1645
|
+
register({
|
|
1646
|
+
kind: "exact",
|
|
1647
|
+
path: "/api/dsh-mneme/receive",
|
|
1648
|
+
policy: "token-write",
|
|
1649
|
+
handler(req, res) {
|
|
1650
|
+
try {
|
|
1651
|
+
if (req.method !== "POST") {
|
|
1652
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
return readBody(req, res).then((text) => {
|
|
1656
|
+
if (text === null) return; // readBody 已经答过 413,再写一次就是双写响应
|
|
1657
|
+
const parsed = parseReceiveBody(text);
|
|
1658
|
+
if (!parsed.ok) {
|
|
1659
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
const out = receiveReply({
|
|
1663
|
+
service,
|
|
1664
|
+
pack: parsed.pack,
|
|
1665
|
+
dryRun: parsed.dryRun,
|
|
1666
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1667
|
+
});
|
|
1668
|
+
sendJson(res, out.status, out.body);
|
|
1669
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1670
|
+
} catch {
|
|
1671
|
+
sendJson(res, 500, { error: "internal" });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
// 退出按 pack_id 收起一整包(物删不归档,理由见 src/share-import.js)。dry-run 同样是
|
|
1677
|
+
// 缺省:没带 confirm 就只报「会删掉几条」,与 reclaim、与入库那侧同一个脾气。
|
|
1678
|
+
register({
|
|
1679
|
+
kind: "exact",
|
|
1680
|
+
path: "/api/dsh-mneme/receive/undo",
|
|
1681
|
+
policy: "token-write",
|
|
1682
|
+
handler(req, res) {
|
|
1683
|
+
try {
|
|
1684
|
+
if (req.method !== "POST") {
|
|
1685
|
+
sendJson(res, 405, { error: "method-not-allowed" });
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
return readBody(req, res).then((text) => {
|
|
1689
|
+
if (text === null) return;
|
|
1690
|
+
const parsed = parseUndoBody(text);
|
|
1691
|
+
if (!parsed.ok) {
|
|
1692
|
+
sendJson(res, 400, { error: parsed.error });
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const out = undoReply({
|
|
1696
|
+
service,
|
|
1697
|
+
packId: parsed.packId,
|
|
1698
|
+
dryRun: parsed.dryRun,
|
|
1699
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
1700
|
+
});
|
|
1701
|
+
sendJson(res, out.status, out.body);
|
|
1702
|
+
}).catch(() => sendJson(res, 500, { error: "internal" }));
|
|
1703
|
+
} catch {
|
|
1704
|
+
sendJson(res, 500, { error: "internal" });
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1464
1709
|
// --- 导入(Markdown 镜像回填)----------------------------------------------
|
|
1465
1710
|
// 写路径(requireAuth)。body {type, markdown}:type 是镜像 TYPE_FILE 键,
|
|
1466
1711
|
// markdown 是与镜像文件同构的文本。解析复用 readHumanEdits 的纯函数核心 parseHumanEdits(同一实现,
|
|
@@ -1525,7 +1770,13 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1525
1770
|
// 数据来自 runSleep 写入的 decisions.demotion({demoted,archived} 数组)。
|
|
1526
1771
|
demotion: r.decisions?.demotion
|
|
1527
1772
|
? { demoted: (r.decisions.demotion.demoted ?? []).length, archived: (r.decisions.demotion.archived ?? []).length }
|
|
1528
|
-
: null
|
|
1773
|
+
: null,
|
|
1774
|
+
// Issue #521:degraded 轮的 skipped 明细按归一化原因聚合。status:
|
|
1775
|
+
// degraded + applied: 3 从外面看完全是健康轮的样子,不展开 skipped
|
|
1776
|
+
// 看不出「一整个 action 类型 100% 失败」——实测那个 bug 跑了一整天才
|
|
1777
|
+
// 被发现。这里只回聚合(原因 + 动作 + 条数),明细仍留在库里。
|
|
1778
|
+
skippedCount: Array.isArray(r.skipped) ? r.skipped.length : 0,
|
|
1779
|
+
skippedReasons: summarizeSkipped(r.skipped)
|
|
1529
1780
|
}));
|
|
1530
1781
|
const pendingConflicts = service.countConflictPending?.() ?? 0;
|
|
1531
1782
|
const ids = new Set();
|