@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/src/tools.js
CHANGED
|
@@ -677,6 +677,241 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
677
677
|
}
|
|
678
678
|
}),
|
|
679
679
|
|
|
680
|
+
// #231 第二批:agent 主动整理接口的调用入口。引擎在 src/organize.js(PR #267),
|
|
681
|
+
// 当时刻意「不进工具列表、不加独立 opt-in 开关——#249 到位时只差注册工具一步」,
|
|
682
|
+
// 本条就是那一步。工具描述是 agent 侧唯一的协议说明,所以两步规则(apply 必须带
|
|
683
|
+
// 真实 run_id、筛除=discard 不删任何东西、archive 可恢复)都写在这里:本体已经
|
|
684
|
+
// 强制了这些,描述的作用是让第一次调用就成功,而不是靠被拒后试错。
|
|
685
|
+
defineTool({
|
|
686
|
+
name: "memory_organize",
|
|
687
|
+
description:
|
|
688
|
+
"Close out a stretch of work: compare candidate memories against the store first, then land only what you decide to keep. " +
|
|
689
|
+
"Two steps through one tool. mode=dryRun (default) takes `candidates` and answers one verdict per candidate — exact / near / new — with the " +
|
|
690
|
+
"matched row when there is one; it writes NO memories, only an audit receipt. mode=apply takes that report's `run_id` plus one decision per " +
|
|
691
|
+
"candidate (save | discard) and may archive a row the report flagged as superseded (action=archive with that matched id). " +
|
|
692
|
+
"apply is rejected without a real run_id, and a report can be applied only once: candidates that were never compared cannot be written, and " +
|
|
693
|
+
"a row the report never matched cannot be archived. Discard keeps a candidate out and deletes nothing; archive is recoverable and never " +
|
|
694
|
+
"physically removes a row. This tool exists only when organizeEnabled is on (off by default, and off in lightweight mode) — if it is missing " +
|
|
695
|
+
"from your tool list the user has not enabled it. For a single write use memory_save instead.",
|
|
696
|
+
parameters: {
|
|
697
|
+
mode: { type: "string", description: '"dryRun" (default: compare and report, writes nothing) or "apply" (land the decisions of a reported dryRun)' },
|
|
698
|
+
candidates: {
|
|
699
|
+
type: "array",
|
|
700
|
+
// 宿主强制的 JSON Schema 子集要求 object 显式声明 additionalProperties,
|
|
701
|
+
// 所以候选字段在这里逐键写死:scope **来源**两列(*_scope_source)故意不给
|
|
702
|
+
// ——那是可信调用层盖的章,不是模型可自选的内容。
|
|
703
|
+
items: {
|
|
704
|
+
type: "object",
|
|
705
|
+
additionalProperties: false,
|
|
706
|
+
properties: {
|
|
707
|
+
type: { type: "string", required: true, description: "Memory type (project / preference / constraint / ... ; 'document' is rejected here — it is minted by memory_register_document)" },
|
|
708
|
+
title: { type: "string", required: true, description: "Short stable title — part of the dedupe key (type + title + scope)" },
|
|
709
|
+
content: { type: "string", required: true, description: "The body that would be stored" },
|
|
710
|
+
importance: { type: "integer", description: "1-5 (default 3)" },
|
|
711
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
|
|
712
|
+
sensitivity: { type: "string", description: "Optional per-candidate sensitivity, overriding the payload-level default" },
|
|
713
|
+
agent_scope: { type: "string", description: "Optional per-candidate agent scope, overriding the payload-level default" },
|
|
714
|
+
workspace_scope: { type: "string", description: "Optional per-candidate workspace scope, overriding the payload-level default" }
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
description: "dryRun only: candidates to compare (hard cap 50 per call — this is for closing out a work stretch, not bulk import). Candidates missing type/title/content, or of type document, come back in `skipped`."
|
|
718
|
+
},
|
|
719
|
+
run_id: { type: "string", description: "apply only: the run_id returned by the dryRun report you are acting on" },
|
|
720
|
+
decisions: {
|
|
721
|
+
type: "array",
|
|
722
|
+
items: {
|
|
723
|
+
type: "object",
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
properties: {
|
|
726
|
+
action: { type: "string", required: true, enum: ["save", "discard", "archive"], description: 'save writes the candidate at `index` through the normal dedupe path (an existing row with the same key is merged, not duplicated); discard keeps it out and deletes nothing; archive hides the row at `id` (recoverable, and only legal for a row this report matched)' },
|
|
727
|
+
index: { type: "integer", description: "save | discard: the candidate index from the dryRun report" },
|
|
728
|
+
id: { type: "string", description: "archive: id of the matched row to archive" }
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
description: "apply only: one entry per candidate you judged. Entries whose index the dryRun never accepted are reported in `skipped`."
|
|
732
|
+
},
|
|
733
|
+
sensitivity: { type: "string", description: "dryRun only: payload-level default sensitivity label for candidates that do not set their own (also the scope dimension the comparison runs within)" },
|
|
734
|
+
agent_scope: { type: "string", description: "dryRun only: payload-level default agent scope for candidates that do not set their own. 'global' or '*' widens visibility to every agent; any other value narrows it. Defaults to this session's auto-stamped agent label when scope labeling is on (same rule as memory_save)." },
|
|
735
|
+
workspace_scope: { type: "string", description: "dryRun only: payload-level default workspace scope for candidates that do not set their own. 'global' or '*' widens visibility to every workspace; any other value narrows it. Defaults to this session's auto-stamped workspace label when scope labeling is on." }
|
|
736
|
+
},
|
|
737
|
+
output: {
|
|
738
|
+
schema: {
|
|
739
|
+
type: "object",
|
|
740
|
+
additionalProperties: false,
|
|
741
|
+
properties: {
|
|
742
|
+
// mode 是判别键:两种分支的返回键集不同,渲染与调用方都要靠它分流。
|
|
743
|
+
mode: { type: "string", required: true, enum: ["dryRun", "apply"] },
|
|
744
|
+
run_id: { type: "string", required: true },
|
|
745
|
+
status: { type: "string", required: true },
|
|
746
|
+
skipped: {
|
|
747
|
+
type: "array",
|
|
748
|
+
required: true,
|
|
749
|
+
items: {
|
|
750
|
+
type: "object",
|
|
751
|
+
additionalProperties: false,
|
|
752
|
+
properties: {
|
|
753
|
+
index: { type: "integer", required: true },
|
|
754
|
+
action: { type: "string" },
|
|
755
|
+
error: { type: "string", required: true },
|
|
756
|
+
ids: { type: "array", items: { type: "string" } }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
// --- dryRun 分支 ---
|
|
761
|
+
created_at: { type: "string" },
|
|
762
|
+
counts: {
|
|
763
|
+
type: "object",
|
|
764
|
+
additionalProperties: false,
|
|
765
|
+
properties: {
|
|
766
|
+
total: { type: "integer", required: true },
|
|
767
|
+
exact: { type: "integer", required: true },
|
|
768
|
+
near: { type: "integer", required: true },
|
|
769
|
+
new: { type: "integer", required: true },
|
|
770
|
+
skipped: { type: "integer", required: true },
|
|
771
|
+
truncated: { type: "integer", required: true }
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
items: {
|
|
775
|
+
type: "array",
|
|
776
|
+
items: {
|
|
777
|
+
type: "object",
|
|
778
|
+
additionalProperties: false,
|
|
779
|
+
properties: {
|
|
780
|
+
index: { type: "integer", required: true },
|
|
781
|
+
type: { type: "string", required: true },
|
|
782
|
+
title: { type: "string", required: true },
|
|
783
|
+
verdict: { type: "string", required: true, enum: ["exact", "near", "new"] },
|
|
784
|
+
// match.sim 只在向量档命中时出现(exact 档无相似度可报);
|
|
785
|
+
// 归档目标必须是报告里出现过的 id,所以这条链接就是 archive 的凭据。
|
|
786
|
+
match: {
|
|
787
|
+
type: "object",
|
|
788
|
+
additionalProperties: false,
|
|
789
|
+
properties: {
|
|
790
|
+
id: { type: "string", required: true },
|
|
791
|
+
title: { type: "string", required: true },
|
|
792
|
+
sim: { type: "number" }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
// --- apply 分支 ---
|
|
799
|
+
dry_run_id: { type: "string" },
|
|
800
|
+
saved: { type: "integer" },
|
|
801
|
+
merged: { type: "integer" },
|
|
802
|
+
discarded: { type: "integer" },
|
|
803
|
+
archived: { type: "integer" },
|
|
804
|
+
memory_ids: { type: "array", items: { type: "string" } },
|
|
805
|
+
degraded: { type: "boolean" }
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
render: (_args, value) => {
|
|
809
|
+
if (value.mode === "apply") {
|
|
810
|
+
return TEXT_OUTPUT(
|
|
811
|
+
`organize applied (${value.run_id}, report ${value.dry_run_id}): ${value.saved} saved, ${value.merged} merged, ` +
|
|
812
|
+
`${value.archived} archived, ${value.discarded} discarded${value.skipped.length ? ` | ${value.skipped.length} skipped` : ""}.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const c = value.counts;
|
|
816
|
+
return TEXT_OUTPUT(
|
|
817
|
+
`organize dryRun ${value.run_id}: ${c.total} compared — ${c.new} new, ${c.exact} exact, ${c.near} near` +
|
|
818
|
+
`${c.skipped ? `, ${c.skipped} rejected` : ""}${c.truncated ? `, ${c.truncated} over the cap not compared` : ""}. ` +
|
|
819
|
+
`Nothing was written; pass this run_id with your decisions to memory_organize mode=apply.`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
async execute(args, exec) {
|
|
824
|
+
const mode = String(args?.mode ?? "dryRun").trim();
|
|
825
|
+
const scope = resolveSessionScope(exec);
|
|
826
|
+
const autoStamping = config?.scopeEnabled === true;
|
|
827
|
+
// 载荷级缺省与 memory_save 同源:显式参数盖 explicit 章,否则吃会话自动标注
|
|
828
|
+
// 盖 auto 章。少了这一步,整理接口就成了第二条「写进库却不带 scope」的通道
|
|
829
|
+
// ——candidates 会全部落在未标注(全局可见)那一档。
|
|
830
|
+
const agentLabel = args.agent_scope !== undefined
|
|
831
|
+
? { value: normalizeExplicitScope(args.agent_scope), source: "explicit" }
|
|
832
|
+
: autoStamping && scope ? { value: scope.agent_scope, source: "auto" } : null;
|
|
833
|
+
const workspaceLabel = args.workspace_scope !== undefined
|
|
834
|
+
? { value: normalizeExplicitScope(args.workspace_scope), source: "explicit" }
|
|
835
|
+
: autoStamping && scope ? { value: scope.workspace_scope, source: "auto" } : null;
|
|
836
|
+
const hiddenIds = [];
|
|
837
|
+
if (mode === "apply" && config?.strictScope === true && scope && Array.isArray(args.decisions)) {
|
|
838
|
+
// #170 复核项 4 的口径延伸(register_document 的 hiddenEvidenceIds 同形):
|
|
839
|
+
// 他 scope 的 explicit 行按「不存在」处理,由**可信调用层**算好传入,绝不
|
|
840
|
+
// 取 payload 里的字段——那是调用方可自选的内容,不是授权。少了这一段,
|
|
841
|
+
// archive 就成了第二条绕过 scope 治理的改库通道。
|
|
842
|
+
for (const decision of args.decisions) {
|
|
843
|
+
if (String(decision?.action ?? "").trim() !== "archive") continue;
|
|
844
|
+
const id = String(decision?.id ?? "").trim();
|
|
845
|
+
if (!id) continue;
|
|
846
|
+
const row = service.getById(id);
|
|
847
|
+
if (row && !service.isVisibleInScope(row, scope)) hiddenIds.push(id);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const result = await service.organize(
|
|
851
|
+
{
|
|
852
|
+
mode,
|
|
853
|
+
...(args.candidates !== undefined ? { candidates: args.candidates } : {}),
|
|
854
|
+
...(args.run_id !== undefined ? { run_id: args.run_id } : {}),
|
|
855
|
+
...(args.decisions !== undefined ? { decisions: args.decisions } : {}),
|
|
856
|
+
...(args.sensitivity !== undefined ? { sensitivity: args.sensitivity } : {}),
|
|
857
|
+
...(agentLabel ? { agent_scope: agentLabel.value, agent_scope_source: agentLabel.source } : {}),
|
|
858
|
+
...(workspaceLabel ? { workspace_scope: workspaceLabel.value, workspace_scope_source: workspaceLabel.source } : {})
|
|
859
|
+
},
|
|
860
|
+
{ hiddenIds }
|
|
861
|
+
);
|
|
862
|
+
if (mode === "apply") {
|
|
863
|
+
return {
|
|
864
|
+
mode: "apply",
|
|
865
|
+
run_id: result.run_id,
|
|
866
|
+
dry_run_id: result.dry_run_id,
|
|
867
|
+
status: result.status,
|
|
868
|
+
saved: result.saved,
|
|
869
|
+
merged: result.merged,
|
|
870
|
+
discarded: result.discarded,
|
|
871
|
+
archived: result.archived,
|
|
872
|
+
memory_ids: result.memory_ids,
|
|
873
|
+
skipped: result.skipped,
|
|
874
|
+
degraded: result.degraded
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
// dryRun 的落点必须过一遍调用方的可见性。`agent_scope` / `workspace_scope`
|
|
878
|
+
// 是模型自己给的参数,而 `organize.dryRun` 的比对只看三列**值**(不看
|
|
879
|
+
// `*_scope_source`、不调 `isVisibleInScope`)——不过这一关,dryRun 就成了
|
|
880
|
+
// 「那一档有没有这个标题」的探测口:#170 复核项 4 给 memory_get / memory_delete
|
|
881
|
+
// 的口径是「不存在」,这里也按不存在说,落点一个键都不带。
|
|
882
|
+
// counts 跟着重算:留着引擎那份计数,`exact: 1` 配一个不带 match 的条目本身就是
|
|
883
|
+
// 存在性oracle,等于把泄露从 id 挪到计数器。审计行仍存原始落点(那是给操作者看的
|
|
884
|
+
// 证据面),apply 侧另有 hiddenIds 挡归档。
|
|
885
|
+
const visibleItems = result.items.map((item) => {
|
|
886
|
+
const match = item.match && visibleUnderStrictScope(service.getById(item.match.id), exec) ? item.match : null;
|
|
887
|
+
return {
|
|
888
|
+
index: item.index,
|
|
889
|
+
type: item.type,
|
|
890
|
+
title: item.title,
|
|
891
|
+
verdict: match ? item.verdict : "new",
|
|
892
|
+
...(match ? { match: { id: match.id, title: match.title, ...(match.sim != null ? { sim: match.sim } : {}) } } : {})
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
const verdictCount = (verdict) => visibleItems.filter((i) => i.verdict === verdict).length;
|
|
896
|
+
return {
|
|
897
|
+
mode: "dryRun",
|
|
898
|
+
run_id: result.run_id,
|
|
899
|
+
created_at: result.created_at,
|
|
900
|
+
status: result.status,
|
|
901
|
+
counts: {
|
|
902
|
+
total: visibleItems.length,
|
|
903
|
+
exact: verdictCount("exact"),
|
|
904
|
+
near: verdictCount("near"),
|
|
905
|
+
new: verdictCount("new"),
|
|
906
|
+
skipped: result.counts.skipped,
|
|
907
|
+
truncated: result.counts.truncated
|
|
908
|
+
},
|
|
909
|
+
items: visibleItems,
|
|
910
|
+
skipped: result.skipped
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}),
|
|
914
|
+
|
|
680
915
|
// 运行时口子(issue #131 / PR-C)。
|
|
681
916
|
//
|
|
682
917
|
// 为什么要有这个工具:mneme 的取向是「本体保持完善但轻量;重的功能存在、默认不开、不被依赖」。
|
|
@@ -805,10 +1040,14 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
805
1040
|
const hiddenTools = new Set();
|
|
806
1041
|
if (config?.disableMemorySearch === true) hiddenTools.add("memory_search");
|
|
807
1042
|
if (config?.disableMemoryArchive === true) hiddenTools.add("memory_archive");
|
|
1043
|
+
// #231 第二批是**反向**语义:memory_organize 默认不在场,organizeEnabled 打开才
|
|
1044
|
+
// 注册。仍走同一个闸门而不是在数组里条件展开——模型看不到就不会调,这比在描述里
|
|
1045
|
+
// 劝它别用可靠(上面那段 v0.8.5 的定性对两个方向都成立)。
|
|
1046
|
+
if (config?.organizeEnabled !== true) hiddenTools.add("memory_organize");
|
|
808
1047
|
|
|
809
1048
|
for (const tool of tools) {
|
|
810
1049
|
if (hiddenTools.has(tool.name)) {
|
|
811
|
-
ctx.logger?.info?.(`[dsh-mneme] tool "${tool.name}" hidden by config (disableMemorySearch/disableMemoryArchive)`);
|
|
1050
|
+
ctx.logger?.info?.(`[dsh-mneme] tool "${tool.name}" hidden by config (disableMemorySearch/disableMemoryArchive/organizeEnabled)`);
|
|
812
1051
|
continue;
|
|
813
1052
|
}
|
|
814
1053
|
if (registeredTools.has(tool.name)) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// dsh-mneme/src/user-settings.js
|
|
2
|
+
// 「用户设置」注入块(人格 / 画像 / 规则)的唯一渲染处。
|
|
3
|
+
//
|
|
4
|
+
// 为什么要单独成模块:面板「Agent 眼中的画像」那张卡原本自己在前端拼了一份 profile +
|
|
5
|
+
// rules 的列表,注释写着「注入生效文本」——但它没有表头、没有「- 用户画像:」前缀,
|
|
6
|
+
// 也完全没有人格三行。两处渲染代码各写各的,就一定会漂;而漂了没人报错,因为两边都
|
|
7
|
+
// 「看着像」。现在注入器与预览路由调同一个函数,前端只显示后端给的文本。
|
|
8
|
+
//
|
|
9
|
+
// 顺序是设计口径(人格设计稿⑥):特化指令(人格)先出,画像与规则后出、后写覆盖占
|
|
10
|
+
// 上风,rules 恒最硬。改这个顺序等于改提示词权重,别顺手调。
|
|
11
|
+
|
|
12
|
+
import { STR, langOf } from "./lang.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 提示词变量转义:宿主模板插值对 `{{`/`}}` 会按变量名解析并在缺失时抛错,整轮崩掉。
|
|
16
|
+
* 所以把 2 个以上连续花括号逐字符插 `\`,让任何 `{{`/`}}` 子串都不残留;转换幂等,
|
|
17
|
+
* 单花括号原样通过。`config.escapePromptVariables === false` 时退回原文。
|
|
18
|
+
*
|
|
19
|
+
* 住在这里是刻意的:预览必须与注入共用同一个转义,否则「同源」又是假的。
|
|
20
|
+
* `src/inject.js` 的同名闭包函数只是薄薄一层委托,其余调用点不动。
|
|
21
|
+
*/
|
|
22
|
+
export function escapePromptVars(config, text) {
|
|
23
|
+
if (config?.escapePromptVariables === false) return String(text);
|
|
24
|
+
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 按会话身份取人格卡。返回 `{ card, source }`,`source` 是给面板解释「用的哪张卡」的
|
|
29
|
+
* 读数,不参与判定。
|
|
30
|
+
*
|
|
31
|
+
* `strict`(config.personaStrictAddressing,默认关)决定「有身份、没这张身份的卡」
|
|
32
|
+
* 与「这张卡被显式关掉」这两种情况:
|
|
33
|
+
* - 关(现状):落 default 卡。default 于是变成「所有没建卡的 agent 都能拿到它」,
|
|
34
|
+
* 包括宿主里跑的外部 bot——它自带身份与设定,两处「你是谁」撞在同一帧里。
|
|
35
|
+
* - 开:不注入人格(`source: "none"`)。default 退回它的本意:只兜「这一帧解析不到
|
|
36
|
+
* 身份」的会话(无名会话本来就是 default 卡的受众)。
|
|
37
|
+
* 默认关是有意的:这是一次口径变更,不能让老用户的注入文本在升级后静默变化。
|
|
38
|
+
*/
|
|
39
|
+
export function resolvePersonaCard(settings, agentId, { strict = false } = {}) {
|
|
40
|
+
const store = settings.getPersona();
|
|
41
|
+
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|
|
42
|
+
const card = id ? store.byAgent[id] : null;
|
|
43
|
+
if (card && card.enabled === true) return { card, source: "byAgent" };
|
|
44
|
+
if (id && strict) return { card: {}, source: "none" };
|
|
45
|
+
return { card: store.default, source: "default" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default 兜底)。
|
|
50
|
+
* 返回 `{ text, source, agent, configGate }`:`text` 为最终注入文本(全空时 ""),
|
|
51
|
+
* `configGate` 是 personaEnabled 的现值——面板要靠它解释「卡开着为什么没生效」。
|
|
52
|
+
*/
|
|
53
|
+
export function renderUserSettingsBlock({ settings, config, agentId = null }) {
|
|
54
|
+
const language = langOf(config);
|
|
55
|
+
const profile = settings.getProfile().trim();
|
|
56
|
+
const rules = settings.getRules();
|
|
57
|
+
const enabled = config?.personaEnabled === true;
|
|
58
|
+
const resolved = enabled
|
|
59
|
+
? resolvePersonaCard(settings, agentId, { strict: config?.personaStrictAddressing === true })
|
|
60
|
+
: { card: {}, source: "default" };
|
|
61
|
+
const persona = resolved.card;
|
|
62
|
+
const personaLines = persona?.enabled === true
|
|
63
|
+
? [
|
|
64
|
+
...(typeof persona.name === "string" && persona.name.trim() ? [STR.personaNameLine[language](persona.name.trim())] : []),
|
|
65
|
+
...(typeof persona.personality === "string" && persona.personality.trim() ? [STR.personaPersonalityLine[language](persona.personality.trim())] : []),
|
|
66
|
+
...(typeof persona.replyStyle === "string" && persona.replyStyle.trim() ? [STR.personaReplyStyleLine[language](persona.replyStyle.trim())] : [])
|
|
67
|
+
]
|
|
68
|
+
: [];
|
|
69
|
+
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : null;
|
|
70
|
+
if (!profile && !rules.length && !personaLines.length) {
|
|
71
|
+
return { text: "", source: resolved.source, agent: id, configGate: enabled };
|
|
72
|
+
}
|
|
73
|
+
const lines = [STR.userSettingsHeader[language]];
|
|
74
|
+
lines.push(...personaLines);
|
|
75
|
+
if (profile) lines.push(STR.profileLine[language](profile));
|
|
76
|
+
for (const rule of rules) lines.push(STR.ruleLine[language](rule));
|
|
77
|
+
return { text: escapePromptVars(config, lines.join("\n")), source: resolved.source, agent: id, configGate: enabled };
|
|
78
|
+
}
|