ai-helper-agent 2.2.0 → 2.3.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 +94 -1
- package/README.zh-CN.md +94 -1
- package/dist/assets/{agent-store-DOyMdB_7.js → agent-store-qLZ_Nmmz.js} +2 -2
- package/dist/assets/{agent-store-DOyMdB_7.js.map → agent-store-qLZ_Nmmz.js.map} +1 -1
- package/dist/assets/background.js +83 -89
- package/dist/assets/background.js.map +1 -1
- package/dist/assets/content.js +40 -40
- package/dist/assets/{i18n-MmYITlVj.js → i18n-sOTxZemV.js} +5 -5
- package/dist/assets/i18n-sOTxZemV.js.map +1 -0
- package/dist/assets/{model-profiles-CV_sUuPf.js → model-profiles-DcTdflVP.js} +5 -5
- package/dist/assets/model-profiles-DcTdflVP.js.map +1 -0
- package/dist/assets/options-B6ca-cwx.css +1 -0
- package/dist/assets/options.js +218 -197
- package/dist/assets/options.js.map +1 -1
- package/dist/assets/{page-snapshot-renderer-CE53ibi-.js → page-snapshot-renderer-C-4bo-cW.js} +2 -2
- package/dist/assets/{page-snapshot-renderer-CE53ibi-.js.map → page-snapshot-renderer-C-4bo-cW.js.map} +1 -1
- package/dist/assets/{session-store-B0ZPGm7d.js → session-store-U8WKrdGX.js} +4 -4
- package/dist/assets/session-store-U8WKrdGX.js.map +1 -0
- package/dist/assets/side_panel.css +1 -1
- package/dist/assets/side_panel.js +327 -293
- package/dist/assets/side_panel.js.map +1 -1
- package/dist/assets/version-DzUoXKcK.js +2 -0
- package/dist/assets/version-DzUoXKcK.js.map +1 -0
- package/dist/manifest.json +2 -2
- package/dist/offscreen/offscreen.js +4 -4
- package/dist/options.html +111 -3
- package/dist/side_panel.html +27 -4
- package/package.json +1 -1
- package/src/config.js +72 -24
- package/src/crypto-lite.js +31 -0
- package/src/locales/en.json +55 -0
- package/src/locales/zh.json +55 -0
- package/src/memory/embedding-config.js +95 -0
- package/src/memory/fusion.js +60 -0
- package/src/memory/injection.js +118 -0
- package/src/memory/routes.js +193 -0
- package/src/memory/service.js +957 -0
- package/src/memory/tags.js +62 -0
- package/src/memory/tier.js +37 -0
- package/src/memory/tokens.js +158 -0
- package/src/memory/vectors.js +128 -0
- package/src/rag/embedding/index.js +7 -0
- package/src/rag/embedding/local.js +25 -0
- package/src/rag/keyword-match.js +112 -0
- package/src/rag/manager.js +15 -0
- package/src/rag/searcher.js +15 -87
- package/src/server.js +6 -0
- package/dist/assets/i18n-MmYITlVj.js.map +0 -1
- package/dist/assets/model-profiles-CV_sUuPf.js.map +0 -1
- package/dist/assets/options-Df_1Dtn8.css +0 -1
- package/dist/assets/session-store-B0ZPGm7d.js.map +0 -1
- /package/dist/_locales/{zh → zh_CN}/messages.json +0 -0
package/dist/options.html
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title data-i18n="settings.docTitle">AI Helper - 配置</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/i18n-
|
|
8
|
-
<script type="module" crossorigin src="/assets/model-profiles-
|
|
7
|
+
<script type="module" crossorigin src="/assets/i18n-sOTxZemV.js"></script>
|
|
8
|
+
<script type="module" crossorigin src="/assets/model-profiles-DcTdflVP.js"></script>
|
|
9
|
+
<script type="module" crossorigin src="/assets/version-DzUoXKcK.js"></script>
|
|
9
10
|
<script type="module" crossorigin src="/assets/options.js"></script>
|
|
10
11
|
<script type="module" crossorigin src="/assets/modulepreload-polyfill-P2Xu9kJm.js"></script>
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/options-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/options-B6ca-cwx.css">
|
|
12
13
|
</head>
|
|
13
14
|
<body>
|
|
14
15
|
<div class="container">
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
<button class="tab-nav-btn" data-tab="agent" data-i18n="settings.tabAgent">代理</button>
|
|
44
45
|
<button class="tab-nav-btn" data-tab="toolbox" data-i18n="settings.tabToolbox">扩展</button>
|
|
45
46
|
<button class="tab-nav-btn" data-tab="knowledge" data-i18n="knowledge.tabTitle">知识库</button>
|
|
47
|
+
<button class="tab-nav-btn" data-tab="memory" data-i18n="memoryPanel.tabTitle">记忆</button>
|
|
46
48
|
</div>
|
|
47
49
|
|
|
48
50
|
<div class="tab-panel active" data-tab="basic">
|
|
@@ -580,6 +582,29 @@
|
|
|
580
582
|
</div>
|
|
581
583
|
</div>
|
|
582
584
|
|
|
585
|
+
<!-- 代理端安装指引(与当前插件版本匹配,一键复制) -->
|
|
586
|
+
<div class="agent-install-guide" id="agentInstallGuide">
|
|
587
|
+
<div class="agent-install-guide-header">
|
|
588
|
+
<h3 id="agentInstallTitle" data-i18n="agentConfig.installGuideTitle">安装代理端</h3>
|
|
589
|
+
<button class="btn btn-primary" id="copyInstallAuto" data-i18n="agentConfig.copyCmdAuto">一键复制(按当前系统)</button>
|
|
590
|
+
</div>
|
|
591
|
+
<p class="agent-install-guide-hint" data-i18n="agentConfig.installGuideHint">复制下方命令到终端执行,将安装与当前插件版本一致的代理端,避免版本不匹配导致无法连接。</p>
|
|
592
|
+
<div class="agent-install-cmd-row">
|
|
593
|
+
<div class="agent-install-cmd-main">
|
|
594
|
+
<span class="agent-install-os-label" data-i18n="agentConfig.installGuideMacLinux">macOS / Linux</span>
|
|
595
|
+
<code class="agent-install-cmd" id="installCmdUnix"></code>
|
|
596
|
+
</div>
|
|
597
|
+
<button class="btn btn-secondary agent-install-copy-btn" id="copyInstallUnix" data-i18n="agentConfig.copyCmd">复制</button>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="agent-install-cmd-row">
|
|
600
|
+
<div class="agent-install-cmd-main">
|
|
601
|
+
<span class="agent-install-os-label">Windows (PowerShell)</span>
|
|
602
|
+
<code class="agent-install-cmd" id="installCmdWin"></code>
|
|
603
|
+
</div>
|
|
604
|
+
<button class="btn btn-secondary agent-install-copy-btn" id="copyInstallWin" data-i18n="agentConfig.copyCmd">复制</button>
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
|
|
583
608
|
<div class="info-box collapsible collapsed" style="margin-top: 15px;">
|
|
584
609
|
<h3 class="collapsible-header"><span data-i18n="agentConfig.useInstructionTitle">💡 使用说明及安全提示</span> <span class="collapsible-arrow">▶</span></h3>
|
|
585
610
|
<div class="collapsible-body" style="display:none;">
|
|
@@ -737,6 +762,89 @@
|
|
|
737
762
|
</div>
|
|
738
763
|
</div><!-- /tab-knowledge -->
|
|
739
764
|
|
|
765
|
+
<!-- 记忆:语义召回向量配置(数据在 agent 端 config.json 的 memoryEmbedding) -->
|
|
766
|
+
<div class="tab-panel" data-tab="memory">
|
|
767
|
+
<div class="knowledge-header">
|
|
768
|
+
<div class="knowledge-title-row">
|
|
769
|
+
<h2 class="knowledge-title" data-i18n="memoryPanel.title">记忆向量</h2>
|
|
770
|
+
</div>
|
|
771
|
+
<div style="display:flex; align-items:center; gap:16px; flex-shrink:0;">
|
|
772
|
+
<label class="global-toggle">
|
|
773
|
+
<input type="checkbox" id="memoryEnabled">
|
|
774
|
+
<span class="global-toggle-slider"></span>
|
|
775
|
+
<span class="global-toggle-label" id="memoryEnabledLabel"></span>
|
|
776
|
+
</label>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
<div id="memoryGate" style="display:none;"></div>
|
|
781
|
+
|
|
782
|
+
<!-- 本 Tab 的配置在点底部「保存」时写入代理 config.json(代理未连接则跳过);底部保存同时存扩展自身设置 -->
|
|
783
|
+
<div class="memory-scope-hint" data-i18n="memoryPanel.scopeHint"></div>
|
|
784
|
+
|
|
785
|
+
<div id="memoryMain">
|
|
786
|
+
<!-- 状态卡:条数 + 指纹陈旧(stale 是本次设计的核心收益) -->
|
|
787
|
+
<div class="form-group">
|
|
788
|
+
<label data-i18n="memoryPanel.statusTitle">向量索引状态</label>
|
|
789
|
+
<div id="memoryStatus" class="memory-status"></div>
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
<div class="form-group">
|
|
793
|
+
<label data-i18n="memoryPanel.modeLabel">嵌入来源</label>
|
|
794
|
+
<label style="display:inline-flex; align-items:center; gap:6px; margin-right:16px;">
|
|
795
|
+
<input type="radio" name="memoryEmbeddingMode" value="openai-compat" style="width:auto;">
|
|
796
|
+
<span data-i18n="memoryPanel.modeRemote">远端服务(OpenAI 兼容接口)</span>
|
|
797
|
+
</label>
|
|
798
|
+
<label style="display:inline-flex; align-items:center; gap:6px;">
|
|
799
|
+
<input type="radio" name="memoryEmbeddingMode" value="local" style="width:auto;">
|
|
800
|
+
<span data-i18n="memoryPanel.modeLocal">本地模型</span>
|
|
801
|
+
</label>
|
|
802
|
+
</div>
|
|
803
|
+
|
|
804
|
+
<div class="form-group" id="memoryRemoteGroup">
|
|
805
|
+
<label data-i18n="memoryPanel.baseUrlLabel">服务地址</label>
|
|
806
|
+
<input type="text" id="memoryBaseUrlInput" data-i18n-placeholder="memoryPanel.baseUrlPlaceholder" placeholder="https://api.openai.com/v1">
|
|
807
|
+
</div>
|
|
808
|
+
|
|
809
|
+
<div class="form-group" id="memoryModelGroup">
|
|
810
|
+
<label data-i18n="memoryPanel.modelLabel">模型名称</label>
|
|
811
|
+
<input type="text" id="memoryModelInput" data-i18n-placeholder="memoryPanel.modelPlaceholder" placeholder="BAAI/bge-m3">
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
<div class="form-group" id="memoryApiKeyGroup">
|
|
815
|
+
<label data-i18n="memoryPanel.apiKeyLabel">API Key</label>
|
|
816
|
+
<input type="password" id="memoryApiKeyInput" data-i18n-placeholder="memoryPanel.apiKeyPlaceholder" placeholder="">
|
|
817
|
+
<!-- L1:明文 key 永不回填 + 留空=不修改 ⇒ 用户没有清除路径,这是唯一显式清除入口(hasApiKey 时显示) -->
|
|
818
|
+
<button class="kb-action-btn" type="button" id="memoryApiKeyClearBtn" data-i18n="memoryPanel.apiKeyClearBtn" style="display:none; margin-top:8px;">清除已存 Key</button>
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
<div class="form-group" id="memoryDimsGroup">
|
|
822
|
+
<label data-i18n="memoryPanel.dimsLabel">向量维度(可留空,测试时自动探测)</label>
|
|
823
|
+
<div class="kb-vec-test-row" id="memoryTestRow">
|
|
824
|
+
<input type="number" id="memoryDimsInput" min="1" max="8192" placeholder="">
|
|
825
|
+
<button class="kb-action-btn" type="button" id="memoryTestBtn" data-i18n="memoryPanel.testBtn">测试连通性</button>
|
|
826
|
+
<span class="kb-vec-test-result" id="memoryTestResult"></span>
|
|
827
|
+
</div>
|
|
828
|
+
</div>
|
|
829
|
+
|
|
830
|
+
<div class="form-group" id="memoryPrefixGroup">
|
|
831
|
+
<label data-i18n="memoryPanel.prefixLabel">查询前缀(queryPrefix,可留空)</label>
|
|
832
|
+
<input type="text" id="memoryPrefixInput" placeholder="">
|
|
833
|
+
</div>
|
|
834
|
+
|
|
835
|
+
<div class="form-group" id="memoryLocalNotice" style="display:none;">
|
|
836
|
+
<span data-i18n="memoryPanel.localNotice">本地档无需测试连通性:保存后直接重建向量索引(首次使用需下载模型,可能数十分钟)。嵌入在 512 token 处静默截断,超长条目只有前部进入向量。</span>
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<!-- 「保存到代理」按钮已并入底部全局「保存配置」:点保存时若代理已连接会自动推到这里 -->
|
|
840
|
+
<div class="form-group" style="display:flex; gap:12px; align-items:center;">
|
|
841
|
+
<button class="toolbox-add-btn" type="button" id="memoryReindexBtn" style="width:auto; margin:0; padding:8px 18px; border-style:solid;" data-i18n="memoryPanel.reindexBtn">重建向量索引</button>
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
<div id="memoryResult" class="memory-status" style="display:none;"></div>
|
|
845
|
+
</div>
|
|
846
|
+
</div><!-- /tab-memory -->
|
|
847
|
+
|
|
740
848
|
<!-- 全局保存按钮(所有 tab 可见;内容超出一屏时吸附在视口底部,保证按钮始终可见) -->
|
|
741
849
|
<div class="save-bar" id="saveBar">
|
|
742
850
|
<button class="btn btn-save" id="saveBtn">
|
package/dist/side_panel.html
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title data-i18n="header.docTitle">AI Helper - 模型问答</title>
|
|
7
7
|
<script type="module" crossorigin src="/assets/rolldown-runtime-C0FnF6B9.js"></script>
|
|
8
|
-
<script type="module" crossorigin src="/assets/i18n-
|
|
9
|
-
<script type="module" crossorigin src="/assets/model-profiles-
|
|
10
|
-
<script type="module" crossorigin src="/assets/session-store-
|
|
11
|
-
<script type="module" crossorigin src="/assets/agent-store-
|
|
8
|
+
<script type="module" crossorigin src="/assets/i18n-sOTxZemV.js"></script>
|
|
9
|
+
<script type="module" crossorigin src="/assets/model-profiles-DcTdflVP.js"></script>
|
|
10
|
+
<script type="module" crossorigin src="/assets/session-store-U8WKrdGX.js"></script>
|
|
11
|
+
<script type="module" crossorigin src="/assets/agent-store-qLZ_Nmmz.js"></script>
|
|
12
|
+
<script type="module" crossorigin src="/assets/version-DzUoXKcK.js"></script>
|
|
12
13
|
<script type="module" crossorigin src="/assets/side_panel.js"></script>
|
|
13
14
|
<script type="module" crossorigin src="/assets/modulepreload-polyfill-P2Xu9kJm.js"></script>
|
|
14
15
|
<link rel="stylesheet" crossorigin href="/assets/side_panel.css">
|
|
@@ -547,6 +548,10 @@
|
|
|
547
548
|
<div class="memory-limit-dropdown-title">
|
|
548
549
|
<span data-i18n="memory.historyLimit">记忆历史限制</span>
|
|
549
550
|
<span class="memory-limit-info-icon" data-i18n-title="memory.hint" title="记忆越多,上下文越连贯,但 token 消耗也越大;记忆太少,则可能无法结合前文准确回答。">ⓘ</span>
|
|
551
|
+
<!-- 跳配置页记忆 Tab 的快捷入口:与输入框 ✚ 面板的 MCP/Skill 管理入口同一形状同一通道 -->
|
|
552
|
+
<button class="memory-limit-config-entry" id="memoryLimitConfigEntry" type="button" data-i18n-title="memory.configEntry" title="打开记忆配置">
|
|
553
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14"/><path d="M5 12h14"/></svg>
|
|
554
|
+
</button>
|
|
550
555
|
</div>
|
|
551
556
|
<div class="memory-limit-options" id="memoryLimitOptions">
|
|
552
557
|
<div class="memory-limit-option" data-value="0"><span class="memory-limit-check"></span><span data-i18n="memory.unlimited">不限制</span></div>
|
|
@@ -563,6 +568,24 @@
|
|
|
563
568
|
<input type="number" id="memoryLimitInput" min="1" max="400" placeholder="1-400" class="memory-limit-input">
|
|
564
569
|
<span class="memory-limit-custom-unit" data-i18n="memory.unit">条</span>
|
|
565
570
|
</div>
|
|
571
|
+
<div class="memory-extra-rows">
|
|
572
|
+
<div class="memory-switch-row">
|
|
573
|
+
<label class="memory-switch" for="autoCompactBtn">
|
|
574
|
+
<input type="checkbox" id="autoCompactBtn" style="display: none;">
|
|
575
|
+
<span class="toggle-switch"></span>
|
|
576
|
+
<span class="memory-switch-label" data-i18n="memory.autoCompactLabel">自动压缩</span>
|
|
577
|
+
</label>
|
|
578
|
+
<span class="memory-limit-info-icon" data-i18n-title="memory.autoCompactHint" title="上下文占用达到 85% 时,发送前自动压缩一次为摘要(会额外产生一次摘要调用)。">ⓘ</span>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="memory-switch-row">
|
|
581
|
+
<label class="memory-switch" for="persistSummaryBtn">
|
|
582
|
+
<input type="checkbox" id="persistSummaryBtn" style="display: none;">
|
|
583
|
+
<span class="toggle-switch"></span>
|
|
584
|
+
<span class="memory-switch-label" data-i18n="memory.persistSummaryLabel">摘要存入记忆</span>
|
|
585
|
+
</label>
|
|
586
|
+
<span class="memory-limit-info-icon" data-i18n-title="memory.persistSummaryHint" title="压缩时把摘要沉淀为跨会话长期记忆;同一会话重复压缩只更新同一条。">ⓘ</span>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
566
589
|
</div>
|
|
567
590
|
</div>
|
|
568
591
|
<div class="tool-toggle-wrapper">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-helper-agent",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI Helper Agent - Local file access and system command execution proxy for Chrome extension / AI Helper 代理服务,为 Chrome 扩展提供本地文件读写和系统命令执行",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/server.js",
|
package/src/config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// agent/src/config.js - 配置管理(带内存缓存 + 写入互斥锁)
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from 'fs';
|
|
5
5
|
import { t as translate } from './i18n.js';
|
|
6
|
+
import { encodeSecret, decodeSecret } from './crypto-lite.js';
|
|
6
7
|
|
|
7
8
|
const AGENT_DIR = join(homedir(), '.ai-helper-agent');
|
|
8
9
|
const CONFIG_FILE = join(AGENT_DIR, 'config.json');
|
|
@@ -21,7 +22,19 @@ const DEFAULTS = {
|
|
|
21
22
|
fileMaxSize: 50 * 1024 * 1024,
|
|
22
23
|
// RAG URL 导入内网访问策略:allowedPrivateHosts 非空时仅放行白名单命中(总开关不生效);
|
|
23
24
|
// 为空时由 allowPrivateNetwork 决定;缺省均关闭(默认拒绝内网),公网访问不受影响
|
|
24
|
-
ragUrlIngest: { allowPrivateNetwork: false, allowedPrivateHosts: [] }
|
|
25
|
+
ragUrlIngest: { allowPrivateNetwork: false, allowedPrivateHosts: [] },
|
|
26
|
+
// 长期记忆语义召回(hybrid recall)的 embedding 配置:缺省关闭 → recall 只走关键词通道(Tier A),
|
|
27
|
+
// 不引入任何新的默认网络调用。mode: 'openai-compat'(远端,零依赖)| 'local'(本地模型,需 RAG 依赖)。
|
|
28
|
+
// apiKey 只落本地 config.json,任何对外响应均经 mask 脱敏(hasApiKey 供前端提示“已配置”)。
|
|
29
|
+
memoryEmbedding: {
|
|
30
|
+
enabled: false,
|
|
31
|
+
mode: 'openai-compat',
|
|
32
|
+
baseUrl: '',
|
|
33
|
+
modelName: '',
|
|
34
|
+
apiKey: '',
|
|
35
|
+
dimensions: 0,
|
|
36
|
+
queryPrefix: ''
|
|
37
|
+
}
|
|
25
38
|
};
|
|
26
39
|
|
|
27
40
|
const VALID_CONFIG_KEYS = Object.keys(DEFAULTS);
|
|
@@ -51,14 +64,18 @@ function withWriteLock(fn) {
|
|
|
51
64
|
const prev = writeLock;
|
|
52
65
|
let release;
|
|
53
66
|
writeLock = new Promise(resolve => { release = resolve; });
|
|
54
|
-
|
|
67
|
+
// fn 允许返回非 Promise(同步写入体):统一用 Promise.resolve 兜住,
|
|
68
|
+
// 否则 fn().finally 直接 TypeError,且锁永远不会释放。
|
|
69
|
+
return prev
|
|
70
|
+
.then(() => Promise.resolve(fn()))
|
|
71
|
+
.finally(release);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
function ensureAgentDir() {
|
|
58
75
|
try {
|
|
59
|
-
// 确保 Agent
|
|
76
|
+
// 确保 Agent 根目录存在(0700:仅当前用户可进入)
|
|
60
77
|
if (!existsSync(AGENT_DIR)) {
|
|
61
|
-
mkdirSync(AGENT_DIR, { recursive: true });
|
|
78
|
+
mkdirSync(AGENT_DIR, { recursive: true, mode: 0o700 });
|
|
62
79
|
}
|
|
63
80
|
// 确保 skills、workspace 和 memory 子目录存在
|
|
64
81
|
for (const dir of [SKILLS_DIR, WORKSPACE_DIR, MEMORY_DIR]) {
|
|
@@ -105,6 +122,10 @@ export function loadConfig() {
|
|
|
105
122
|
try {
|
|
106
123
|
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
107
124
|
const userConfig = JSON.parse(raw);
|
|
125
|
+
// 磁盘密文 → 内存明文(兼容旧版无 enc: 前缀的明文)
|
|
126
|
+
if (userConfig.memoryEmbedding && userConfig.memoryEmbedding.apiKey) {
|
|
127
|
+
userConfig.memoryEmbedding = { ...userConfig.memoryEmbedding, apiKey: decodeSecret(userConfig.memoryEmbedding.apiKey) };
|
|
128
|
+
}
|
|
108
129
|
configCache = { ...DEFAULTS, ...userConfig };
|
|
109
130
|
configCacheMtime = mtime;
|
|
110
131
|
} catch (err) {
|
|
@@ -135,26 +156,41 @@ function saveConfigSync(config) {
|
|
|
135
156
|
for (const key of VALID_CONFIG_KEYS) {
|
|
136
157
|
if (config[key] !== undefined) safe[key] = config[key];
|
|
137
158
|
}
|
|
138
|
-
|
|
159
|
+
// 落盘前混淆 apiKey(内存缓存保持明文)
|
|
160
|
+
if (safe.memoryEmbedding && safe.memoryEmbedding.apiKey) {
|
|
161
|
+
safe.memoryEmbedding = { ...safe.memoryEmbedding, apiKey: encodeSecret(safe.memoryEmbedding.apiKey) };
|
|
162
|
+
}
|
|
163
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(safe, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
164
|
+
try { chmodSync(CONFIG_FILE, 0o600); } catch {} // 历史宽松文件收紧
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 取锁后的实际写入体:落盘 + 刷新缓存(saveConfig 与 updateConfigField 共用)。
|
|
169
|
+
* 必须不加锁:两者都已在自己的 withWriteLock 内,再嵌一次会 自我等自我 而永久挂起。
|
|
170
|
+
*/
|
|
171
|
+
function writeConfigBody(config) {
|
|
172
|
+
try {
|
|
173
|
+
saveConfigSync(config);
|
|
174
|
+
// 从磁盘重新加载确保缓存与磁盘一致(解码后缓存保持明文)
|
|
175
|
+
const mtime = getMtime(CONFIG_FILE);
|
|
176
|
+
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
if (parsed.memoryEmbedding && parsed.memoryEmbedding.apiKey) {
|
|
179
|
+
parsed.memoryEmbedding = { ...parsed.memoryEmbedding, apiKey: decodeSecret(parsed.memoryEmbedding.apiKey) };
|
|
180
|
+
}
|
|
181
|
+
configCache = { ...DEFAULTS, ...parsed };
|
|
182
|
+
configCacheMtime = mtime;
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error('[Config] Save config failed:', err.message);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
/**
|
|
142
190
|
* 保存配置(写入磁盘并更新缓存,加写锁防并发覆盖)
|
|
143
191
|
*/
|
|
144
192
|
export async function saveConfig(config) {
|
|
145
|
-
return withWriteLock(
|
|
146
|
-
try {
|
|
147
|
-
saveConfigSync(config);
|
|
148
|
-
// 从磁盘重新加载确保缓存与磁盘一致
|
|
149
|
-
const mtime = getMtime(CONFIG_FILE);
|
|
150
|
-
const raw = readFileSync(CONFIG_FILE, 'utf-8');
|
|
151
|
-
configCache = { ...DEFAULTS, ...JSON.parse(raw) };
|
|
152
|
-
configCacheMtime = mtime;
|
|
153
|
-
} catch (err) {
|
|
154
|
-
console.error('[Config] Save config failed:', err.message);
|
|
155
|
-
throw err;
|
|
156
|
-
}
|
|
157
|
-
});
|
|
193
|
+
return withWriteLock(() => writeConfigBody(config));
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
/**
|
|
@@ -165,9 +201,10 @@ export async function updateConfigField(key, value) {
|
|
|
165
201
|
throw new Error(translate(currentLang, 'error.invalidConfigKey', { key }));
|
|
166
202
|
}
|
|
167
203
|
return withWriteLock(async () => {
|
|
168
|
-
|
|
169
|
-
config[key]
|
|
170
|
-
|
|
204
|
+
// 浅拷贝后再改:loadConfig() 返回的是缓存本体,直接赋值会就地修改调用方已持有的对象
|
|
205
|
+
const config = { ...loadConfig(), [key]: value };
|
|
206
|
+
writeConfigBody(config);
|
|
207
|
+
return config;
|
|
171
208
|
});
|
|
172
209
|
}
|
|
173
210
|
|
|
@@ -189,7 +226,12 @@ export function loadPairings() {
|
|
|
189
226
|
}
|
|
190
227
|
|
|
191
228
|
try {
|
|
192
|
-
|
|
229
|
+
const parsed = JSON.parse(readFileSync(PAIRINGS_FILE, 'utf-8'));
|
|
230
|
+
// 磁盘密文 → 内存明文(兼容旧版)
|
|
231
|
+
for (const entry of Object.values(parsed)) {
|
|
232
|
+
if (entry.token) entry.token = decodeSecret(entry.token);
|
|
233
|
+
}
|
|
234
|
+
pairingsCache = parsed;
|
|
193
235
|
pairingsCacheMtime = mtime;
|
|
194
236
|
return pairingsCache;
|
|
195
237
|
} catch (err) {
|
|
@@ -209,7 +251,13 @@ export async function savePairing(extensionId, token) {
|
|
|
209
251
|
pairings[extensionId] = { token, pairedAt: Date.now() };
|
|
210
252
|
ensureAgentDir();
|
|
211
253
|
try {
|
|
212
|
-
|
|
254
|
+
// 落盘前混淆 token(内存缓存保持明文)
|
|
255
|
+
const diskPairings = {};
|
|
256
|
+
for (const [id, entry] of Object.entries(pairings)) {
|
|
257
|
+
diskPairings[id] = { ...entry, token: encodeSecret(entry.token) };
|
|
258
|
+
}
|
|
259
|
+
writeFileSync(PAIRINGS_FILE, JSON.stringify(diskPairings, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
260
|
+
try { chmodSync(PAIRINGS_FILE, 0o600); } catch {} // 历史宽松文件收紧
|
|
213
261
|
pairingsCache = pairings;
|
|
214
262
|
pairingsCacheMtime = getMtime(PAIRINGS_FILE);
|
|
215
263
|
} catch (err) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// agent/src/crypto-lite.js - 密钥/Token 落盘混淆(Base64 + enc: 前缀标记)
|
|
2
|
+
// 非密码学安全,目的:避免配置文件被 cat/截图时一眼识别出密钥明文。
|
|
3
|
+
// 兼容规则:读取时若无 enc: 前缀视为旧明文原样返回;下次写入自动升级。
|
|
4
|
+
|
|
5
|
+
const PREFIX = 'enc:';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 编码:明文 → "enc:" + base64(明文)
|
|
9
|
+
* 空值原样返回(空字符串不编码)
|
|
10
|
+
* @param {string} plain
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function encodeSecret(plain) {
|
|
14
|
+
if (!plain) return plain;
|
|
15
|
+
return PREFIX + Buffer.from(plain, 'utf-8').toString('base64');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 解码:"enc:..." → 明文;无 enc: 前缀 → 原样返回(兼容旧明文)
|
|
20
|
+
* @param {string} stored
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function decodeSecret(stored) {
|
|
24
|
+
if (!stored) return stored;
|
|
25
|
+
if (!stored.startsWith(PREFIX)) return stored;
|
|
26
|
+
try {
|
|
27
|
+
return Buffer.from(stored.slice(PREFIX.length), 'base64').toString('utf-8');
|
|
28
|
+
} catch {
|
|
29
|
+
return stored; // 解码失败(损坏数据)→ 原样返回,不崩溃
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/locales/en.json
CHANGED
|
@@ -294,5 +294,60 @@
|
|
|
294
294
|
"gracefulShutdownTimeout": "Graceful shutdown timeout, force exit",
|
|
295
295
|
"uncaughtException": "Uncaught exception",
|
|
296
296
|
"unhandledRejection": "Unhandled Promise rejection"
|
|
297
|
+
},
|
|
298
|
+
"memory": {
|
|
299
|
+
"missingAction": "Missing action parameter",
|
|
300
|
+
"missingType": "Missing type parameter",
|
|
301
|
+
"invalidImportance": "importance must be a number between 1-10, received \"{value}\"",
|
|
302
|
+
"invalidMemoryType": "Invalid memoryType \"{type}\", only all / fact / summary are supported",
|
|
303
|
+
"invalidType": "Invalid memory type \"{type}\", only fact or summary are supported",
|
|
304
|
+
"missingContent": "Missing content parameter",
|
|
305
|
+
"readFailed": "Failed to read memory file: {error}",
|
|
306
|
+
"writeFailed": "Failed to write memory file: {error}",
|
|
307
|
+
"parseFailed": "Memory file parse failed: {error}",
|
|
308
|
+
"updatedDuplicate": "Updated existing memory: {id} (content identical, merged)",
|
|
309
|
+
"added": "Added memory: {id} ({type}){warning}",
|
|
310
|
+
"nearLimitWarning": "\n⚠️ Memory count approaching limit (facts: {facts}/{maxFacts}, summaries: {summaries}/{maxSummaries}). Consider calling agent_memory action=manage to review and clean up.",
|
|
311
|
+
"updateNeedsId": "update action requires memoryId parameter",
|
|
312
|
+
"deleteNeedsId": "delete action requires memoryId parameter",
|
|
313
|
+
"typeMismatch": "Memory {id} type mismatch (actual type: {type})",
|
|
314
|
+
"notFound": "Memory not found: {id}",
|
|
315
|
+
"updated": "Updated memory: {id}",
|
|
316
|
+
"deleted": "Deleted memory: {id}",
|
|
317
|
+
"unsupportedAction": "Unsupported action: {action}",
|
|
318
|
+
"storeMissingSubAction": "store action missing subAction (add/update/delete)",
|
|
319
|
+
"manageMissingSubAction": "manage action missing subAction (review/compact)",
|
|
320
|
+
"unsupportedMemoryAction": "Unsupported action: {action}",
|
|
321
|
+
"memoryEmpty": "Memory file is empty, no stored memories.",
|
|
322
|
+
"noMatch": "No matching memories found.",
|
|
323
|
+
"foundResults": "Found {count} relevant memories:{note}",
|
|
324
|
+
"recallNoteAllOld": "(Note: The above memories have already been retrieved in this conversation, returned again)",
|
|
325
|
+
"recallNoteMixed": "(Note: {oldCount} memories already retrieved in this conversation, only returning {newCount} new ones)",
|
|
326
|
+
"compactDone": "Memory compaction complete. Removed {facts} fact memories and {summaries} summary memories (value below {threshold}). Current: facts {totalFacts}, summaries {totalSummaries}",
|
|
327
|
+
"reviewTitle": "## Memory Review Report",
|
|
328
|
+
"reviewOverview": "**Overview**: Facts {facts}/{maxFacts}, Summaries {summaries}/{maxSummaries}",
|
|
329
|
+
"reviewLastReview": "**Last review**: {date}",
|
|
330
|
+
"reviewNeverReviewed": "Never reviewed",
|
|
331
|
+
"reviewCandidates": "**Low-value memory candidates** (value < {threshold}):",
|
|
332
|
+
"reviewNoCandidates": "No candidates for removal, all memories have good value.",
|
|
333
|
+
"reviewSuggestion": "**Suggested actions**:",
|
|
334
|
+
"reviewSuggestionDelete": "- For outdated/no longer applicable memories, use agent_memory action=store subAction=delete to remove",
|
|
335
|
+
"reviewSuggestionMerge": "- For similar memories, use agent_memory action=store subAction=update to merge",
|
|
336
|
+
"reviewSuggestionKeep": "- For still useful but low-value memories, keep them as-is",
|
|
337
|
+
"factLabel": "Fact",
|
|
338
|
+
"summaryLabel": "Summary",
|
|
339
|
+
"neverAccessed": "Never",
|
|
340
|
+
"memoryItemHeader": "**{index}. [{type}] {id}**",
|
|
341
|
+
"memoryItemContent": " Content: {content}",
|
|
342
|
+
"memoryItemTitle": " Title: {title}",
|
|
343
|
+
"memoryItemTags": " Tags: {tags}",
|
|
344
|
+
"memoryItemStats": " Importance: {importance}/10 | Access: {accessCount} times | Created: {createdAt}",
|
|
345
|
+
"reviewItemCreated": " Created: {createdAt} | Last access: {lastAccess} | Access: {accessCount} times",
|
|
346
|
+
"invalidEmbeddingMode": "Unsupported embedding mode, only openai-compat or local is allowed",
|
|
347
|
+
"embeddingDisabled": "Semantic recall is off, keyword channel only",
|
|
348
|
+
"embeddingTimeout": "Embedding service timed out, fell back to keyword recall",
|
|
349
|
+
"tierLabelNone": "keyword",
|
|
350
|
+
"tierLabelRemote": "remote vector + keyword",
|
|
351
|
+
"tierLabelLocal": "local vector + keyword"
|
|
297
352
|
}
|
|
298
353
|
}
|
package/src/locales/zh.json
CHANGED
|
@@ -294,5 +294,60 @@
|
|
|
294
294
|
"gracefulShutdownTimeout": "优雅关闭超时,强制退出",
|
|
295
295
|
"uncaughtException": "未捕获异常",
|
|
296
296
|
"unhandledRejection": "未处理的 Promise 拒绝"
|
|
297
|
+
},
|
|
298
|
+
"memory": {
|
|
299
|
+
"missingAction": "缺少 action 参数",
|
|
300
|
+
"missingType": "缺少 type 参数",
|
|
301
|
+
"invalidImportance": "importance 必须是 1-10 之间的数字,收到 \"{value}\"",
|
|
302
|
+
"invalidMemoryType": "memoryType \"{type}\" 无效,只支持 all / fact / summary",
|
|
303
|
+
"invalidType": "记忆类型 \"{type}\" 无效,只支持 fact 或 summary",
|
|
304
|
+
"missingContent": "缺少 content 参数",
|
|
305
|
+
"readFailed": "读取记忆文件失败: {error}",
|
|
306
|
+
"writeFailed": "写入记忆文件失败: {error}",
|
|
307
|
+
"parseFailed": "记忆文件解析失败: {error}",
|
|
308
|
+
"updatedDuplicate": "已更新已有记忆: {id}(内容相同,已合并)",
|
|
309
|
+
"added": "已添加记忆: {id} ({type}){warning}",
|
|
310
|
+
"nearLimitWarning": "\n⚠️ 记忆数量接近上限(事实: {facts}/{maxFacts}, 摘要: {summaries}/{maxSummaries}),建议调用 agent_memory action=manage 进行审查整理。",
|
|
311
|
+
"updateNeedsId": "update 操作需要 memoryId 参数",
|
|
312
|
+
"deleteNeedsId": "delete 操作需要 memoryId 参数",
|
|
313
|
+
"typeMismatch": "记忆 {id} 类型不匹配(实际类型: {type})",
|
|
314
|
+
"notFound": "未找到记忆: {id}",
|
|
315
|
+
"updated": "已更新记忆: {id}",
|
|
316
|
+
"deleted": "已删除记忆: {id}",
|
|
317
|
+
"unsupportedAction": "不支持的操作: {action}",
|
|
318
|
+
"storeMissingSubAction": "store 操作缺少 subAction (add/update/delete)",
|
|
319
|
+
"manageMissingSubAction": "manage 操作缺少 subAction (review/compact)",
|
|
320
|
+
"unsupportedMemoryAction": "不支持的 action: {action}",
|
|
321
|
+
"memoryEmpty": "记忆文件为空,暂无存储的记忆。",
|
|
322
|
+
"noMatch": "未找到匹配的记忆。",
|
|
323
|
+
"foundResults": "找到 {count} 条相关记忆:{note}",
|
|
324
|
+
"recallNoteAllOld": "(注:以上记忆在本对话中已检索过,此处为重复返回)",
|
|
325
|
+
"recallNoteMixed": "(注:{oldCount} 条记忆在本对话中已检索过,仅返回新的 {newCount} 条)",
|
|
326
|
+
"compactDone": "记忆压缩完成。移除了 {facts} 条事实记忆和 {summaries} 条摘要记忆(价值低于 {threshold})。当前: 事实 {totalFacts}, 摘要 {totalSummaries}",
|
|
327
|
+
"reviewTitle": "## 记忆审查报告",
|
|
328
|
+
"reviewOverview": "**概况**: 事实 {facts}/{maxFacts}, 摘要 {summaries}/{maxSummaries}",
|
|
329
|
+
"reviewLastReview": "**上次审查**: {date}",
|
|
330
|
+
"reviewNeverReviewed": "从未审查",
|
|
331
|
+
"reviewCandidates": "**低价值记忆候选** (价值 < {threshold}):",
|
|
332
|
+
"reviewNoCandidates": "无候选淘汰项,所有记忆价值良好。",
|
|
333
|
+
"reviewSuggestion": "**建议操作**:",
|
|
334
|
+
"reviewSuggestionDelete": "- 对于确实过时/不再适用的记忆,使用 agent_memory action=store subAction=delete 删除",
|
|
335
|
+
"reviewSuggestionMerge": "- 对于内容相似的记忆,使用 agent_memory action=store subAction=update 合并",
|
|
336
|
+
"reviewSuggestionKeep": "- 对于仍有用但价值低的记忆,可保留不做处理",
|
|
337
|
+
"factLabel": "事实",
|
|
338
|
+
"summaryLabel": "摘要",
|
|
339
|
+
"neverAccessed": "从未",
|
|
340
|
+
"memoryItemHeader": "**{index}. [{type}] {id}**",
|
|
341
|
+
"memoryItemContent": " 内容: {content}",
|
|
342
|
+
"memoryItemTitle": " 标题: {title}",
|
|
343
|
+
"memoryItemTags": " 标签: {tags}",
|
|
344
|
+
"memoryItemStats": " 重要性: {importance}/10 | 访问: {accessCount}次 | 创建: {createdAt}",
|
|
345
|
+
"reviewItemCreated": " 创建: {createdAt} | 最后访问: {lastAccess} | 访问: {accessCount}次",
|
|
346
|
+
"invalidEmbeddingMode": "不支持的 embedding 模式,仅支持 openai-compat 或 local",
|
|
347
|
+
"embeddingDisabled": "未启用语义召回,当前仅关键词通道",
|
|
348
|
+
"embeddingTimeout": "向量服务响应超时,本次已回落关键词召回",
|
|
349
|
+
"tierLabelNone": "关键词",
|
|
350
|
+
"tierLabelRemote": "远端向量+关键词",
|
|
351
|
+
"tierLabelLocal": "本地向量+关键词"
|
|
297
352
|
}
|
|
298
353
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// agent/src/memory/embedding-config.js - 记忆语义召回的 embedding 配置读写与脱敏
|
|
2
|
+
// 单一配置出口:只读写 agent config.json 的 memoryEmbedding 字段,请求体永不回显明文 key。
|
|
3
|
+
// 缺省 enabled=false:recall 只走关键词通道(Tier A),不新增任何默认网络调用。
|
|
4
|
+
|
|
5
|
+
import { loadConfig, updateConfigField } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MEMORY_EMBEDDING_CONFIG = {
|
|
8
|
+
enabled: false,
|
|
9
|
+
mode: 'openai-compat',
|
|
10
|
+
baseUrl: '',
|
|
11
|
+
modelName: '',
|
|
12
|
+
apiKey: '',
|
|
13
|
+
dimensions: 0,
|
|
14
|
+
queryPrefix: ''
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// mode 白名单:远端 OpenAI 兼容端点(零依赖)与本地 transformers 模型(需 RAG 依赖)
|
|
18
|
+
export const MEMORY_EMBEDDING_MODES = ['openai-compat', 'local'];
|
|
19
|
+
|
|
20
|
+
/** 原始配置(含 apiKey 明文,仅进程内使用,绝不出响应体) */
|
|
21
|
+
export function getMemoryEmbeddingConfig() {
|
|
22
|
+
const cfg = loadConfig().memoryEmbedding || {};
|
|
23
|
+
return { ...DEFAULT_MEMORY_EMBEDDING_CONFIG, ...cfg };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 对外响应形态:隐藏明文 key,附 hasApiKey 供前端「已配置」提示(沿用 rag/routes.js 约定) */
|
|
27
|
+
export function maskMemoryEmbeddingConfig(cfg = getMemoryEmbeddingConfig()) {
|
|
28
|
+
const { apiKey, ...rest } = cfg;
|
|
29
|
+
return { ...rest, apiKey: '', hasApiKey: Boolean(apiKey) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 保存配置:mode 白名单校验;空白 apiKey 表示不修改(前端表单不回填明文时不能抹掉已存 key);
|
|
34
|
+
* clearApiKey===true 是唯一显式清除通道,且优先于同传的 apiKey;
|
|
35
|
+
* dimensions 规整为非负整数;enabled 只认真 true(字符串/数字一律视为关闭)。
|
|
36
|
+
* @param {object} input
|
|
37
|
+
* @returns {Promise<object>} 脱敏后的最新配置
|
|
38
|
+
* @throws {Error} message = 'invalid_embedding_mode'
|
|
39
|
+
*/
|
|
40
|
+
export async function saveMemoryEmbeddingConfig(input = {}) {
|
|
41
|
+
const current = getMemoryEmbeddingConfig();
|
|
42
|
+
const next = { ...current };
|
|
43
|
+
|
|
44
|
+
if (input.mode !== undefined) {
|
|
45
|
+
if (!MEMORY_EMBEDDING_MODES.includes(input.mode)) {
|
|
46
|
+
throw new Error('invalid_embedding_mode');
|
|
47
|
+
}
|
|
48
|
+
next.mode = input.mode;
|
|
49
|
+
}
|
|
50
|
+
if (input.enabled !== undefined) next.enabled = input.enabled === true;
|
|
51
|
+
if (typeof input.baseUrl === 'string') next.baseUrl = input.baseUrl.trim();
|
|
52
|
+
if (typeof input.modelName === 'string') next.modelName = input.modelName.trim();
|
|
53
|
+
if (typeof input.queryPrefix === 'string') next.queryPrefix = input.queryPrefix;
|
|
54
|
+
if (typeof input.apiKey === 'string' && input.apiKey.trim()) next.apiKey = input.apiKey.trim();
|
|
55
|
+
// L1(评审):明文不回显 + 空白=不修改 ⇒ 用户没有清除路径。clearApiKey 放在赋值之后:
|
|
56
|
+
// 清除是显式动作,赢过同传的新 key(界面不会同传,但 API 语义不能含糊)。
|
|
57
|
+
if (input.clearApiKey === true) next.apiKey = '';
|
|
58
|
+
if (input.dimensions !== undefined) {
|
|
59
|
+
const n = Number(input.dimensions);
|
|
60
|
+
next.dimensions = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await updateConfigField('memoryEmbedding', next);
|
|
64
|
+
return maskMemoryEmbeddingConfig(next);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 探活/向量派生用的 provider 配置:请求体可覆盖尚未保存的草稿值,
|
|
69
|
+
* 但明文 key 缺省时回落到已保存的 key(前端不会回传明文)。
|
|
70
|
+
* @param {object} [override]
|
|
71
|
+
*/
|
|
72
|
+
export function resolveMemoryEmbeddingConfig(override = {}) {
|
|
73
|
+
const saved = getMemoryEmbeddingConfig();
|
|
74
|
+
const merged = { ...saved, ...(override || {}) };
|
|
75
|
+
if (!merged.apiKey) merged.apiKey = saved.apiKey;
|
|
76
|
+
return merged;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 转成 rag/embedding provider 需要的形态:配置面统一叫 baseUrl(与扩展端表单一致),
|
|
81
|
+
* 而 OpenAI 兼容后端读的是 endpoint。缺这一映射会让远端档永远 fetch('/embeddings')。
|
|
82
|
+
* 单一出口:所有把记忆 embedding 配置交给 provider 的地方都必须过这里。
|
|
83
|
+
* @param {object} [cfg]
|
|
84
|
+
*/
|
|
85
|
+
export function toEmbeddingProviderConfig(cfg = {}) {
|
|
86
|
+
const baseUrl = cfg.baseUrl || '';
|
|
87
|
+
const endpoint = cfg.endpoint || '';
|
|
88
|
+
const out = { ...cfg, endpoint: endpoint || baseUrl, baseUrl: baseUrl || endpoint };
|
|
89
|
+
// 空串在本配置面表示「未设置」(DEFAULT_MEMORY_EMBEDDING_CONFIG.queryPrefix 就是 ''),
|
|
90
|
+
// 而 provider 用 `??` 判回落——空串不是 nullish,原样交出会让本地 BGE 的查询前缀永远加不上
|
|
91
|
+
// (实测:mode=local 时 provider 日志 queryPrefix: off)。非对称检索漏前缀对中文召回是实质质量损失,
|
|
92
|
+
// 所以这里交出「不带这个键」,把回落权还给 provider。显式非空前缀照常透传。
|
|
93
|
+
if (!String(out.queryPrefix || '').trim()) delete out.queryPrefix;
|
|
94
|
+
return out;
|
|
95
|
+
}
|