@modusensus/dsh-mneme 0.8.15 → 0.9.0
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 +21 -3
- package/assets/icon.svg +2 -2
- package/bin/cli.mjs +4 -2
- package/docs/CONFIGURATION.md +16 -3
- package/docs/CONTINUITY.md +18 -3
- package/docs/DISTILL.md +162 -0
- package/docs/STORAGE.md +11 -2
- package/docs/WORKFLOWS.md +299 -0
- package/lib/api-standalone.js +22 -39
- package/lib/api.js +294 -10
- package/lib/candidates.js +454 -0
- package/lib/capabilities.js +819 -0
- package/lib/client.js +2767 -337
- package/lib/config.js +99 -6
- package/lib/continuity.js +35 -6
- package/lib/dream/decisions.js +37 -1
- package/lib/dream/sleep.js +10 -2
- package/lib/dream.js +7 -1
- package/lib/entities/extractor.js +4 -0
- package/lib/hot-memory.js +62 -0
- package/lib/index.js +72 -18
- package/lib/inject.js +33 -64
- package/lib/maintenance.js +25 -5
- package/lib/pipeline.js +534 -0
- package/lib/recall-stats.js +22 -7
- package/lib/reclaim-http.js +150 -0
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +16 -4
- package/lib/serve.js +1 -1
- package/lib/service.js +83 -17
- package/lib/session-usage.js +350 -0
- package/lib/settings.js +19 -2
- package/lib/store.js +251 -10
- package/lib/summarize.js +239 -60
- package/lib/tools.js +11 -2
- package/package.json +1 -1
- package/scripts/publish-gate.mjs +197 -0
- package/src/api-standalone.js +22 -39
- package/src/api.js +294 -10
- package/src/candidates.js +454 -0
- package/src/capabilities.js +819 -0
- package/src/config.js +99 -6
- package/src/continuity.js +35 -6
- package/src/dream/decisions.js +37 -1
- package/src/dream/sleep.js +10 -2
- package/src/dream.js +7 -1
- package/src/entities/extractor.js +4 -0
- package/src/hot-memory.js +62 -0
- package/src/index.js +72 -18
- package/src/inject.js +33 -64
- package/src/maintenance.js +25 -5
- package/src/pipeline.js +534 -0
- package/src/recall-stats.js +22 -7
- package/src/reclaim-http.js +150 -0
- package/src/safe-slice.js +18 -0
- package/src/scope.js +16 -4
- package/src/serve.js +1 -1
- package/src/service.js +83 -17
- package/src/session-usage.js +350 -0
- package/src/settings.js +19 -2
- package/src/store.js +251 -10
- package/src/summarize.js +239 -60
- package/src/tools.js +11 -2
- package/test/api-routes-gaps.test.js +681 -0
- package/test/api.test.js +171 -8
- package/test/candidates.test.js +345 -0
- package/test/capabilities.test.js +577 -0
- package/test/client.test.js +715 -4
- package/test/continuity.test.js +84 -0
- package/test/contrast.test.js +314 -0
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/embed-ready-poll.test.js +94 -0
- package/test/entities.test.js +13 -2
- package/test/failure-query.test.js +147 -0
- package/test/failure-retention.test.js +105 -0
- package/test/graph-api.test.js +36 -2
- package/test/hot-memory.test.js +67 -1
- package/test/inject-recall.test.js +68 -1
- package/test/inject.test.js +23 -0
- package/test/llm-audit.test.js +190 -1
- package/test/maintenance.test.js +112 -2
- package/test/mint-source.test.js +202 -0
- package/test/pipeline.test.js +505 -0
- package/test/placement.test.js +1 -1
- package/test/pre-inject-gate.test.js +10 -0
- package/test/publish-gate.test.js +272 -0
- package/test/recall-retention.test.js +128 -0
- package/test/recall-stats.test.js +89 -4
- package/test/reclaim-http.test.js +209 -0
- package/test/release-workflow.test.js +77 -0
- package/test/scope-distill-stamp.test.js +135 -0
- package/test/serve.test.js +1 -1
- package/test/service.test.js +7 -2
- package/test/session-usage.test.js +308 -0
- package/test/settings.test.js +8 -0
- package/test/store-write-path-columns.test.js +191 -0
- package/test/store.test.js +108 -0
- package/test/summarize.test.js +350 -0
- package/test/supersede-state-tracking.test.js +4 -4
- package/test/tools.test.js +16 -1
package/src/api-standalone.js
CHANGED
|
@@ -18,6 +18,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
|
18
18
|
import { TYPES } from "./store.js";
|
|
19
19
|
import { bootstrapFromDirectory } from "./bootstrap.js";
|
|
20
20
|
import { PACKAGE_VERSION } from "./version-check.js";
|
|
21
|
+
import { parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
|
|
21
22
|
|
|
22
23
|
const DEFAULT_PORT = 8790;
|
|
23
24
|
const DEFAULT_HOST = "127.0.0.1";
|
|
@@ -482,54 +483,36 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
482
483
|
// 默认 dry-run:只有显式 confirm:true 才真改数据。这两步都是不可逆的内容丢弃
|
|
483
484
|
// (历史 run 的输入快照置空、归档行向量置空),所以不给「带 body 就执行」这种
|
|
484
485
|
// 省事路径——确认必须建立在看得见的数字上。
|
|
486
|
+
//
|
|
487
|
+
// 口径(错误码字符串、默认值、报告形状)不在这里写:与宿主内的面板路由
|
|
488
|
+
// /api/dsh-mneme/maintenance/reclaim 共用 src/reclaim-http.js,两张皮只有鉴权
|
|
489
|
+
// 与路径不同。这条曾经是整段复制的,于是面板侧修好之后这边还停在旧形态(#411)。
|
|
485
490
|
if (req.method === "POST" && pathname === "/maintenance/reclaim") {
|
|
486
491
|
void readBody(req).then((text) => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
} catch {
|
|
491
|
-
sendJson(res, 400, { error: "invalid-json" });
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
495
|
-
sendJson(res, 400, { error: "invalid-body" });
|
|
492
|
+
const parsed = parseReclaimJson(text);
|
|
493
|
+
if (!parsed.ok) {
|
|
494
|
+
sendJson(res, 400, { error: parsed.error });
|
|
496
495
|
return;
|
|
497
496
|
}
|
|
497
|
+
// 顺序敏感:端点整个没接线时报「窗口不合法」是误导,503 必须赢过字段校验。
|
|
498
498
|
if (!maintenance) {
|
|
499
|
-
sendJson(res, 503, { error:
|
|
499
|
+
sendJson(res, 503, { error: RECLAIM_UNAVAILABLE });
|
|
500
500
|
return;
|
|
501
501
|
}
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const olderThanDays = body.olderThanDays === undefined || body.olderThanDays === null
|
|
506
|
-
? undefined
|
|
507
|
-
: body.olderThanDays;
|
|
508
|
-
if (olderThanDays !== undefined && (!Number.isInteger(olderThanDays) || olderThanDays < 0)) {
|
|
509
|
-
sendJson(res, 400, { error: "invalid-older-than-days" });
|
|
502
|
+
const fields = readReclaimFields(parsed.body);
|
|
503
|
+
if (!fields.ok) {
|
|
504
|
+
sendJson(res, 400, { error: fields.error });
|
|
510
505
|
return;
|
|
511
506
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
error: "confirm-required",
|
|
522
|
-
hint: "先看 dry-run 报告,确认后带 {\"dryRun\":false,\"confirm\":true} 再发",
|
|
523
|
-
report: maintenance.reclaim({ olderThanDays, vacuum: body.vacuum === true, dryRun: true })
|
|
524
|
-
});
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
try {
|
|
528
|
-
sendJson(res, 200, maintenance.reclaim({ olderThanDays, vacuum: body.vacuum === true, dryRun }));
|
|
529
|
-
} catch (error) {
|
|
530
|
-
logger?.warn?.(`[dsh-mneme] storage reclaim failed: ${String(error)}`);
|
|
531
|
-
sendJson(res, 500, { error: "internal" });
|
|
532
|
-
}
|
|
507
|
+
const { status, body } = reclaimReply({
|
|
508
|
+
maintenance,
|
|
509
|
+
dryRun: fields.dryRun,
|
|
510
|
+
olderThanDays: fields.olderThanDays,
|
|
511
|
+
vacuum: fields.vacuum,
|
|
512
|
+
confirm: fields.confirm,
|
|
513
|
+
warn: (msg) => logger?.warn?.(msg)
|
|
514
|
+
});
|
|
515
|
+
sendJson(res, status, body);
|
|
533
516
|
});
|
|
534
517
|
return;
|
|
535
518
|
}
|
package/src/api.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { URL } from "node:url";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
2
3
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
3
4
|
import { FEATURE_FLAG_SPEC } from "./settings.js";
|
|
5
|
+
import { TYPES } from "./store.js";
|
|
4
6
|
import { TYPE_FILE, renderMirrorText, renderFileHeader, parseHumanEdits, MIRROR_READONLY_TYPES } from "./mirror.js";
|
|
5
7
|
import { langOf } from "./lang.js";
|
|
6
8
|
import { computeHeat } from "./heat.js";
|
|
7
|
-
import { describeStreamFailure, resolveRoute } from "./dream.js";
|
|
9
|
+
import { EFFORT_REJECT_RE, describeStreamFailure, resolveRoute, withEffortFallback } from "./dream.js";
|
|
8
10
|
import { getInjectionSnapshot } from "./inject.js";
|
|
9
11
|
import { describeLocalRuntime, publicRuntimeStatus } from "./runtime/loader.js";
|
|
10
12
|
import { hostModulesDir, provisionRuntime } from "./runtime/provision.js";
|
|
11
13
|
import { classify, fetchLatestVersion, PACKAGE_VERSION } from "./version-check.js";
|
|
14
|
+
import { parseReclaimQuery, parseReclaimJson, readReclaimFields, reclaimReply, RECLAIM_UNAVAILABLE } from "./reclaim-http.js";
|
|
15
|
+
import { readPipelineDoc } from "./pipeline.js";
|
|
16
|
+
import { readCandidateDoc } from "./candidates.js";
|
|
12
17
|
|
|
13
18
|
// headers:少数端点(/export 附件下载)需要追加 Content-Disposition 等响应头。
|
|
14
19
|
function sendJson(res, status, payload, headers = {}) {
|
|
@@ -92,7 +97,10 @@ function parseBody(text) {
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
// maintenance 是第 9 个位置参数,默认 null:既有调用点(测试里的 14 处)全是位置传参、
|
|
101
|
+
// 不传它,必须继续工作——不传时 /maintenance/reclaim 回 503 maintenance-unavailable,
|
|
102
|
+
// 而不是把「没接线」伪装成崩溃(#386 第 4 条)。
|
|
103
|
+
export function createApi(ctx, service, settings, commands, embedder, semantic = null, apiToken = "", config = null, maintenance = null) {
|
|
96
104
|
const disposers = [];
|
|
97
105
|
|
|
98
106
|
// #182 注入状态探测:面板状态页的「极简模式下注入按宿主设计关闭」提示需要
|
|
@@ -146,7 +154,11 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
146
154
|
// Issue #380:注入前判定。漏登记的后果是 effective 静默缺键、面板盲——
|
|
147
155
|
// 上面的 round-trip 测试专门锁这张表与白名单成对。
|
|
148
156
|
"preInjectGate.enabled": ["preInjectGate", "enabled"],
|
|
149
|
-
"preInjectGate.enforce": ["preInjectGate", "enforce"]
|
|
157
|
+
"preInjectGate.enforce": ["preInjectGate", "enforce"],
|
|
158
|
+
// Issue #399:蒸馏原文留存 + 溯源。两个键都是嵌套子字段(enabled 开关 +
|
|
159
|
+
// retentionDays 整数),漏登记的后果同上——effective 静默缺键。
|
|
160
|
+
"distillRecords.enabled": ["distillRecords", "enabled"],
|
|
161
|
+
"distillRecords.retentionDays": ["distillRecords", "retentionDays"]
|
|
150
162
|
// sensitiveScanEnabled(#164 A2)是顶层扁平键,不需要进这张表——走 configFlagValue
|
|
151
163
|
// 的默认分支即可。
|
|
152
164
|
};
|
|
@@ -367,13 +379,30 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
367
379
|
// the same error wording the audit rows carry. Optional reasoningEffort
|
|
368
380
|
// mirrors what the consolidation would send — the panel can verify a model
|
|
369
381
|
// actually accepts the configured effort without tripping a full run.
|
|
382
|
+
//
|
|
383
|
+
// 档位被拒时探针必须和真实 run 一样降级:这里是全仓唯一「接受并转发调用方档位、
|
|
384
|
+
// 却此前没有兜底」的调用面——巩固/睡眠/蒸馏/总览/实体抽取都包在 withEffortFallback
|
|
385
|
+
// 里,档位被拒就丢掉字段重试一次(issue #9/#109/#135)。直调 llm.stream 的其余各面
|
|
386
|
+
// 要么本来就在兜底之内(dream.js/summarize.js/sleep.js 的共用辅助),要么是
|
|
387
|
+
// src/index.js 的手写版兜底,要么像 pre-inject-gate 的判定池那样根本不转发档位。
|
|
388
|
+
// 探针原先没有这层,于是同一份配置「后台真的跑得通、点一下测试就报红」,把用户
|
|
389
|
+
// 引向「换哪家 Provider 都失败」的错误诊断(issue #215 的后续)。降级跑通后响应带
|
|
390
|
+
// effortDropped:true,面板据此说明「档位不被接受、已按无档位连通」,而不是与「真的
|
|
391
|
+
// 连不上」同色。
|
|
370
392
|
async function testLlmConnectivity(llm, provider, model, reasoningEffort) {
|
|
371
393
|
const startedAt = Date.now();
|
|
372
394
|
const elapsed = () => Date.now() - startedAt;
|
|
373
395
|
const modelId = `${provider}:${model}`;
|
|
374
|
-
|
|
396
|
+
// 流级失败原因:withEffortFallback 只用它做拒收甄别,回给面板的仍是同一条
|
|
397
|
+
// describeStreamFailure 文案(与审计行同口径,不制造第二套措辞)。
|
|
398
|
+
let streamError = "";
|
|
399
|
+
// 降级标记刻意放在 attempt 之外:fallback 里置位、attempt 里读出,结果对象
|
|
400
|
+
// 据此带上 effortDropped——降级是「换个姿势跑通了」,不是失败。
|
|
401
|
+
let droppedEffort = false;
|
|
402
|
+
|
|
403
|
+
const attempt = async (effort) => {
|
|
404
|
+
streamError = "";
|
|
375
405
|
let reply = "";
|
|
376
|
-
let error = "";
|
|
377
406
|
for await (const chunk of llm.stream({
|
|
378
407
|
provider,
|
|
379
408
|
model,
|
|
@@ -382,7 +411,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
382
411
|
// reply 恒为空,「真的答了 ok」对思考模型不成立。按实际用量计费,手动
|
|
383
412
|
// 按钮无放大风险。
|
|
384
413
|
maxTokens: 1024,
|
|
385
|
-
...(
|
|
414
|
+
...(effort && effort !== "none" ? { reasoningEffort: effort } : {}),
|
|
386
415
|
// role 用 user 而非 system(#189 后续):0.1.6-alpha.1 起适配器把 system
|
|
387
416
|
// 抽成顶层参数,system-only 对话会被官方 API 以
|
|
388
417
|
// 「messages: at least one message is required」整单拒绝——探测按钮 502。
|
|
@@ -397,12 +426,39 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
397
426
|
})) {
|
|
398
427
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") reply += chunk.text;
|
|
399
428
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
400
|
-
|
|
429
|
+
streamError = describeStreamFailure(chunk.reason);
|
|
401
430
|
break;
|
|
402
431
|
}
|
|
403
432
|
}
|
|
404
|
-
|
|
405
|
-
|
|
433
|
+
const dropped = droppedEffort ? { effortDropped: true } : {};
|
|
434
|
+
if (streamError) {
|
|
435
|
+
// 只在「确实带了档位」时把流级拒收交回给 withEffortFallback 的 undefined
|
|
436
|
+
// 分支——rc.1 的适配器不抛错,把 provider 的拒收折叠成终止 finish chunk,
|
|
437
|
+
// 不看这里那次重试就是死代码。不带档位时原样上报:探针不该因为一句文案
|
|
438
|
+
// 没匹配上正则就退化成 500。
|
|
439
|
+
if (effort && effort !== "none" && EFFORT_REJECT_RE.test(streamError)) return undefined;
|
|
440
|
+
// 失败行不带 effortDropped:该字段只有一个含义——「连通了,但不是用你配的
|
|
441
|
+
// 档位连通的」(见下方成功分支的注释),失败时它只是噪音。
|
|
442
|
+
return { ok: false, durationMs: elapsed(), error: streamError, modelId };
|
|
443
|
+
}
|
|
444
|
+
return { ok: true, durationMs: elapsed(), modelId, reply: reply.trim().slice(0, 100), ...dropped };
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const result = await withEffortFallback(
|
|
449
|
+
ctx,
|
|
450
|
+
reasoningEffort,
|
|
451
|
+
() => attempt(reasoningEffort),
|
|
452
|
+
() => { droppedEffort = true; return attempt(""); },
|
|
453
|
+
() => streamError
|
|
454
|
+
);
|
|
455
|
+
// result === undefined 只可能来自上面那次「判据一致」的主动交还(两边用同
|
|
456
|
+
// 一份 EFFORT_REJECT_RE,走到这里等于假设被推翻):退回已捕获的流失败原
|
|
457
|
+
// 因,绝不让探针 500。
|
|
458
|
+
if (result === undefined) {
|
|
459
|
+
return { ok: false, durationMs: elapsed(), error: streamError || "no stream result", modelId };
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
406
462
|
} catch (error) {
|
|
407
463
|
return { ok: false, durationMs: elapsed(), error: String(error?.message ?? error), modelId };
|
|
408
464
|
}
|
|
@@ -602,6 +658,165 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
602
658
|
}
|
|
603
659
|
});
|
|
604
660
|
|
|
661
|
+
// --- 新建一条记忆(面板写路径,与 /update、/delete 对称命名)---------------
|
|
662
|
+
// #386 第 5 条:CLI `add` 与 standalone `POST /memories` 都能建行,面板此前只
|
|
663
|
+
// 能看/改/删/导入。字段与 memory_save 工具对齐(type/title/content/importance/
|
|
664
|
+
// tags),走同一条 saveWithDedupe 路径——去重键仍是 (type, title, scope,
|
|
665
|
+
// sensitivity),所以「面板建」与「工具建」同标题会并入同一行,不会长出两份。
|
|
666
|
+
//
|
|
667
|
+
// 三处刻意的口径差异(都写在代码里,免得下一个人以为是抄漏):
|
|
668
|
+
// 1. content 要求 trim 后非空,与 /update 一致而非 standalone 的宽松版:
|
|
669
|
+
// 面板是人工输入,空正文只可能是误操作;standalone 面对的是程序化调用,
|
|
670
|
+
// 放宽是为了兼容「先建壳再补内容」的既有客户端。
|
|
671
|
+
// 2. document 直接 400:行铸造口唯一(registerDocument,#230),saveWithDedupe
|
|
672
|
+
// 会 throw,这里提前给干净错误码而不是让它落成 500。
|
|
673
|
+
// 3. 不接受 scope/sensitivity 字段——面板的 scope 编辑在详情抽屉里(带放宽
|
|
674
|
+
// 确认的安全阀,见 lib/client.js 的 MemoryDrawer),新建时就盖章 explicit
|
|
675
|
+
// 会绕过那道确认。传了也不报错,只是不生效(与 /update 的「字段出现即校验」
|
|
676
|
+
// 不同,因为这里根本没有该字段的语义)。
|
|
677
|
+
register({
|
|
678
|
+
kind: "exact",
|
|
679
|
+
path: "/api/dsh-mneme/create",
|
|
680
|
+
handler(req, res) {
|
|
681
|
+
try {
|
|
682
|
+
if (req.method !== "POST") {
|
|
683
|
+
sendJson(res, 404, { error: "not-found" });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
687
|
+
return readBody(req).then((text) => {
|
|
688
|
+
const body = parseBody(text);
|
|
689
|
+
if (!TYPES.has(body.type)) {
|
|
690
|
+
sendJson(res, 400, { error: "invalid-type" });
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (body.type === "document") {
|
|
694
|
+
sendJson(res, 400, { error: "document-requires-register" });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (typeof body.title !== "string" || !body.title.trim()) {
|
|
698
|
+
sendJson(res, 400, { error: "invalid-title" });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (typeof body.content !== "string" || !body.content.trim()) {
|
|
702
|
+
sendJson(res, 400, { error: "invalid-content" });
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (body.importance !== undefined
|
|
706
|
+
&& (!Number.isInteger(body.importance) || body.importance < 1 || body.importance > 5)) {
|
|
707
|
+
sendJson(res, 400, { error: "invalid-importance" });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (body.tags !== undefined && (!Array.isArray(body.tags) || !body.tags.every((tag) => typeof tag === "string"))) {
|
|
711
|
+
sendJson(res, 400, { error: "invalid-tags" });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const result = service.saveWithDedupe({
|
|
716
|
+
type: body.type,
|
|
717
|
+
title: body.title.trim(),
|
|
718
|
+
content: body.content.trim(),
|
|
719
|
+
importance: body.importance,
|
|
720
|
+
tags: body.tags ?? [],
|
|
721
|
+
// 面板不是工具也不是外部程序,来源要能在详情抽屉里一眼认出是谁写的。
|
|
722
|
+
source: "panel"
|
|
723
|
+
});
|
|
724
|
+
// #254 写入准入在无会话身份时放行(write-admission.js 首行早退),面板
|
|
725
|
+
// 路由恰好不带 _sessionKey,所以今天到不了这个分支。仍然显式写出来:
|
|
726
|
+
// 将来若把会话身份接进面板,落到 toApiList([null]) 上会变成 500,把
|
|
727
|
+
// 「被拒」伪装成「服务器出错」——两者的处置完全不同。
|
|
728
|
+
if (result.action === "denied") {
|
|
729
|
+
sendJson(res, 422, { error: "write-rejected", reason: result.reason });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
// action 随行透出(created/merged):面板据此提示「已并入同标题的既有条目」
|
|
733
|
+
// 还是「新建成功」——两者对用户是不同的事实,不能都报「已保存」。
|
|
734
|
+
sendJson(res, result.action === "created" ? 201 : 200, {
|
|
735
|
+
...service.toApiList([result.memory])[0],
|
|
736
|
+
action: result.action
|
|
737
|
+
});
|
|
738
|
+
} catch (error) {
|
|
739
|
+
ctx.logger?.warn?.(`[dsh-mneme] panel create failed: ${String(error)}`);
|
|
740
|
+
sendJson(res, 500, { error: "internal" });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
} catch {
|
|
744
|
+
sendJson(res, 500, { error: "internal" });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// --- maintenance/reclaim(#386 第 4 条:面板有了回收入口)------------------
|
|
750
|
+
// 与 standalone 数据面的 POST /maintenance/reclaim 是同一能力的两张皮:口径(错误码
|
|
751
|
+
// 字符串、默认值、报告形状)只写在 src/reclaim-http.js 里,这里与数据面都调它。
|
|
752
|
+
// 差别只有鉴权与路径——两张皮的其余部分必须没有第二份实现(#411 的教训:以前是整段
|
|
753
|
+
// 复制,于是 #410 修好这边之后那边还停在旧形态)。
|
|
754
|
+
//
|
|
755
|
+
// 为什么 GET 也能跑:dry-run 是只读的(src/maintenance.js 里 dry-run 与执行同一条
|
|
756
|
+
// 代码路径,只是不落 UPDATE/VACUUM),面板 mount 时拿它渲染「现在能回收多少」。
|
|
757
|
+
//
|
|
758
|
+
// 为什么两法都要 requireAuth:这是不可逆内容丢弃的入口,不能有例外。
|
|
759
|
+
register({
|
|
760
|
+
kind: "exact",
|
|
761
|
+
path: "/api/dsh-mneme/maintenance/reclaim",
|
|
762
|
+
handler(req, res) {
|
|
763
|
+
try {
|
|
764
|
+
if (req.method === "GET") {
|
|
765
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
766
|
+
if (!maintenance) {
|
|
767
|
+
sendJson(res, 503, { error: RECLAIM_UNAVAILABLE });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const parsed = parseReclaimQuery(new URL(req.url, "http://localhost").searchParams.get("olderThanDays"));
|
|
771
|
+
if (!parsed.ok) {
|
|
772
|
+
sendJson(res, 400, { error: parsed.error });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const { status, body } = reclaimReply({
|
|
776
|
+
maintenance,
|
|
777
|
+
dryRun: true,
|
|
778
|
+
olderThanDays: parsed.olderThanDays,
|
|
779
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
780
|
+
});
|
|
781
|
+
sendJson(res, status, body);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (req.method !== "POST") {
|
|
785
|
+
sendJson(res, 404, { error: "not-found" });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (!requireAuth(req, res, apiToken)) return;
|
|
789
|
+
return readBody(req).then((text) => {
|
|
790
|
+
const parsed = parseReclaimJson(text);
|
|
791
|
+
if (!parsed.ok) {
|
|
792
|
+
sendJson(res, 400, { error: parsed.error });
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!maintenance) {
|
|
796
|
+
sendJson(res, 503, { error: RECLAIM_UNAVAILABLE });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const fields = readReclaimFields(parsed.body);
|
|
800
|
+
if (!fields.ok) {
|
|
801
|
+
sendJson(res, 400, { error: fields.error });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const { status, body } = reclaimReply({
|
|
805
|
+
maintenance,
|
|
806
|
+
dryRun: fields.dryRun,
|
|
807
|
+
olderThanDays: fields.olderThanDays,
|
|
808
|
+
vacuum: fields.vacuum,
|
|
809
|
+
confirm: fields.confirm,
|
|
810
|
+
warn: (msg) => ctx.logger?.warn?.(msg)
|
|
811
|
+
});
|
|
812
|
+
sendJson(res, status, body);
|
|
813
|
+
});
|
|
814
|
+
} catch {
|
|
815
|
+
sendJson(res, 500, { error: "internal" });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
605
820
|
// --- rules ---
|
|
606
821
|
register({
|
|
607
822
|
kind: "exact",
|
|
@@ -1428,8 +1643,77 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1428
1643
|
}
|
|
1429
1644
|
});
|
|
1430
1645
|
|
|
1646
|
+
// --- M.O.(Modus Operandi · 手法)工作流 ---
|
|
1647
|
+
// 只读端点,读的是**项目里**的 `<项目>/.mneme/pipelines/*.json`(文件头在
|
|
1648
|
+
// src/pipeline.js),项目层优先、插件包里那份降级成全局层;投影成
|
|
1649
|
+
// mneme.pipeline/v1 交给面板与 agent——两边读同一份,画面只是它的投影。
|
|
1650
|
+
// 刻意不做旗标闸门:旗标管的是**客户端座位**(关=一个座位都不注册),不是数据面;
|
|
1651
|
+
// 多一个只读端点不会让任何人看见它,而给它加闸门只会让「为什么面板打不开」多一种成因。
|
|
1652
|
+
register({
|
|
1653
|
+
kind: "exact",
|
|
1654
|
+
path: "/api/dsh-mneme/pipeline",
|
|
1655
|
+
handler(req, res) {
|
|
1656
|
+
try {
|
|
1657
|
+
const url = new URL(req.url, "http://localhost");
|
|
1658
|
+
const id = url.searchParams.get("id") || null;
|
|
1659
|
+
const project = url.searchParams.get("project");
|
|
1660
|
+
// project 是**会话工作目录**,由面板侧解析后带过来——工作目录只有客户端拿得到
|
|
1661
|
+
// (宿主会话头里的 header.cwd),服务端没有「当前项目」这个概念。
|
|
1662
|
+
// 不是绝对路径就**拒掉**,不能忽略:忽略了会静默退成「全局」,而全局看起来
|
|
1663
|
+
// 一切正常——那正是这版要修掉的那类错。
|
|
1664
|
+
if (project !== null && !isAbsolute(project)) {
|
|
1665
|
+
sendJson(res, 400, { error: "project-not-absolute", detail: project });
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
sendJson(res, 200, readPipelineDoc({ projectRoot: project, id }));
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
// 工作流是人手写的 JSON,报错本身就是给人看的:说清哪一条、坏在哪,
|
|
1671
|
+
// 而不是回一句 internal 让人去翻日志。
|
|
1672
|
+
sendJson(res, 500, {
|
|
1673
|
+
error: "pipeline-unreadable",
|
|
1674
|
+
detail: String((error && error.message) || error)
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
// --- M.O. 候选层(绕行走出来、还没进仓库的那些路) ---
|
|
1681
|
+
// 和 /pipeline 读的是两处不同的东西,别混:
|
|
1682
|
+
// /pipeline 定稿,住**仓库**(<项目>/.mneme/pipelines/),可提交、可 review
|
|
1683
|
+
// /candidates 候选,住**本地**(<memoryDir>/candidates/<项目键>/),随时长、随时扔
|
|
1684
|
+
// 分开是刻意的:候选是草稿,一天可能长十条死九条,进仓库就是垃圾;
|
|
1685
|
+
// 而定稿只放人亲手批过的东西——候选是定稿的唯一入口。
|
|
1686
|
+
//
|
|
1687
|
+
// 这一刀只给**读**:提议与批准的动作还没有落点(那要等执行侧)。
|
|
1688
|
+
// 契约里显式写着 boundaries.autoPromote/autoApprove/injectIntoMemory 三个 false,
|
|
1689
|
+
// 免得以后有人以为忘了做。
|
|
1690
|
+
register({
|
|
1691
|
+
kind: "exact",
|
|
1692
|
+
path: "/api/dsh-mneme/candidates",
|
|
1693
|
+
handler(req, res) {
|
|
1694
|
+
try {
|
|
1695
|
+
const url = new URL(req.url, "http://localhost");
|
|
1696
|
+
const project = url.searchParams.get("project");
|
|
1697
|
+
if (project !== null && !isAbsolute(project)) {
|
|
1698
|
+
sendJson(res, 400, { error: "project-not-absolute", detail: project });
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
// memoryDir 从 config 拿(与 store 同源);取不到就退回默认,与 config.js 的默认值一致。
|
|
1702
|
+
const memoryDir = (config && typeof config.memoryDir === "string" && config.memoryDir.trim())
|
|
1703
|
+
? config.memoryDir
|
|
1704
|
+
: "~/.dsh/memory";
|
|
1705
|
+
sendJson(res, 200, readCandidateDoc({ memoryDir, projectRoot: project }));
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
sendJson(res, 500, {
|
|
1708
|
+
error: "candidates-unreadable",
|
|
1709
|
+
detail: String((error && error.message) || error)
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1431
1715
|
return {
|
|
1432
|
-
routes:
|
|
1716
|
+
routes: 36,
|
|
1433
1717
|
dispose: () => {
|
|
1434
1718
|
for (const dispose of disposers) dispose();
|
|
1435
1719
|
}
|