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.
Files changed (52) hide show
  1. package/README.md +94 -1
  2. package/README.zh-CN.md +94 -1
  3. package/dist/assets/{agent-store-DOyMdB_7.js → agent-store-qLZ_Nmmz.js} +2 -2
  4. package/dist/assets/{agent-store-DOyMdB_7.js.map → agent-store-qLZ_Nmmz.js.map} +1 -1
  5. package/dist/assets/background.js +83 -89
  6. package/dist/assets/background.js.map +1 -1
  7. package/dist/assets/content.js +40 -40
  8. package/dist/assets/{i18n-MmYITlVj.js → i18n-sOTxZemV.js} +5 -5
  9. package/dist/assets/i18n-sOTxZemV.js.map +1 -0
  10. package/dist/assets/{model-profiles-CV_sUuPf.js → model-profiles-DcTdflVP.js} +5 -5
  11. package/dist/assets/model-profiles-DcTdflVP.js.map +1 -0
  12. package/dist/assets/options-B6ca-cwx.css +1 -0
  13. package/dist/assets/options.js +218 -197
  14. package/dist/assets/options.js.map +1 -1
  15. package/dist/assets/{page-snapshot-renderer-CE53ibi-.js → page-snapshot-renderer-C-4bo-cW.js} +2 -2
  16. package/dist/assets/{page-snapshot-renderer-CE53ibi-.js.map → page-snapshot-renderer-C-4bo-cW.js.map} +1 -1
  17. package/dist/assets/{session-store-B0ZPGm7d.js → session-store-U8WKrdGX.js} +4 -4
  18. package/dist/assets/session-store-U8WKrdGX.js.map +1 -0
  19. package/dist/assets/side_panel.css +1 -1
  20. package/dist/assets/side_panel.js +327 -293
  21. package/dist/assets/side_panel.js.map +1 -1
  22. package/dist/assets/version-DzUoXKcK.js +2 -0
  23. package/dist/assets/version-DzUoXKcK.js.map +1 -0
  24. package/dist/manifest.json +2 -2
  25. package/dist/offscreen/offscreen.js +4 -4
  26. package/dist/options.html +111 -3
  27. package/dist/side_panel.html +27 -4
  28. package/package.json +1 -1
  29. package/src/config.js +72 -24
  30. package/src/crypto-lite.js +31 -0
  31. package/src/locales/en.json +55 -0
  32. package/src/locales/zh.json +55 -0
  33. package/src/memory/embedding-config.js +95 -0
  34. package/src/memory/fusion.js +60 -0
  35. package/src/memory/injection.js +118 -0
  36. package/src/memory/routes.js +193 -0
  37. package/src/memory/service.js +957 -0
  38. package/src/memory/tags.js +62 -0
  39. package/src/memory/tier.js +37 -0
  40. package/src/memory/tokens.js +158 -0
  41. package/src/memory/vectors.js +128 -0
  42. package/src/rag/embedding/index.js +7 -0
  43. package/src/rag/embedding/local.js +25 -0
  44. package/src/rag/keyword-match.js +112 -0
  45. package/src/rag/manager.js +15 -0
  46. package/src/rag/searcher.js +15 -87
  47. package/src/server.js +6 -0
  48. package/dist/assets/i18n-MmYITlVj.js.map +0 -1
  49. package/dist/assets/model-profiles-CV_sUuPf.js.map +0 -1
  50. package/dist/assets/options-Df_1Dtn8.css +0 -1
  51. package/dist/assets/session-store-B0ZPGm7d.js.map +0 -1
  52. /package/dist/_locales/{zh → zh_CN}/messages.json +0 -0
@@ -0,0 +1,60 @@
1
+ // agent/src/memory/fusion.js - 语义采信门与排名融合(纯函数,无 IO)
2
+ //
3
+ // 【2026-10-14 计划五:记忆语义档转「纯 top-K」,删掉查询级绝对地板】
4
+ // 本模块原有三道采信门,现只剩后两道(都是**相对量**,随模型自动平移,无需标定):
5
+ // ② 条目级相对落差:cos ≥ top1 - margin,砍高分查询的长尾。
6
+ // ③ 条数封顶 maxTake:防止一次语义命中填满 limit。
7
+ // 第①道「查询级绝对地板」(原 SEMANTIC_QUERY_FLOOR=0.48,top1<floor 整通道否决)**已移除**。
8
+ // 移除理由:那个地板是在 BAAI/bge-m3 上标定的绝对值,不可跨模型平移;而记忆向量允许用户任选
9
+ // 远端/本地任意模型,套同一个 0.48 会在别的模型上失配(本地档实测:噪声 top1=0.5599 逃过地板
10
+ // 被误返、真相关 top1=0.4888 被地板绞杀)。绝对值判「本库有无答案」这件事绕不开按模型标定,与
11
+ // 「用户不必懂向量、开箱即用」根本冲突 —— 故放弃「无匹配空返」能力,换取任意模型开箱可用。
12
+ // 代价(已知并被接受):与全库无关的查询也会返回至多 maxTake 条语义头部。护栏随之从「空返」
13
+ // 降级为「至多 maxTake 条、不填满 limit」,由评测集守住。
14
+ //
15
+ // 【两个仍然成立的结论,不要回退】
16
+ // • 条目级绝对值不可用:问「这条记忆算不算命中」用绝对阈值放过的太多(≥0.45 仍放过 20/41),
17
+ // 所以条目级只看相对落差。
18
+ // • 分离度(top1-top2)不可加回:它不可跨库规模外推(库越大无关查询 top1-top2 越可能偶然变大),
19
+ // 且真实库目标查询 Q1 的 gap 0.0214 低于无关查询的 0.0361/0.0397,在 gap 轴上分不开。
20
+ //
21
+ // SEMANTIC_QUERY_FLOOR 常量保留仅供离线标定/参考(换模型时可用 test/eval/recalibrate-semantic-floor.mjs
22
+ // 量一个地板),**但线上 selectSemanticHits 不再施加它**——本文件的采信门里没有绝对值。
23
+
24
+ export const SEMANTIC_QUERY_FLOOR = 0.48; // 【离线参考】仅供标定脚本;线上 selectSemanticHits 不施加
25
+ export const SEMANTIC_MARGIN = 0.05; // 条目级相对落差:只采信距 top1 不超过此值的头部
26
+ export const SEMANTIC_MAX_TAKE = 2; // 每次查询最多采信几条语义命中
27
+ export const RRF_K = 60; // 排名融合的平滑常数(业界默认值,本计划不重调)
28
+
29
+ /**
30
+ * 语义通道采信哪些条目(纯 top-K:只按相对远近取头部,不含任何随模型漂移的绝对值)。
31
+ * 两道门:① 条目级相对落差 cos ≥ top1 - margin(砍高分查询的长尾);② 条数封顶 maxTake。
32
+ * 与全库无关的查询也会返回至多 maxTake 条头部 —— 这是「纯 top-K 换任意模型开箱即用」的已知代价。
33
+ * 注意:**既不看绝对地板、也不看 top1-top2 分离度**(理由见文件头)。
34
+ * @param {Array<[string, number]>} pairs [id, cos],顺序任意
35
+ * @param {{margin:number, maxTake:number}} gate
36
+ * @returns {Array<[string, number]>} 按 cos 降序的采信结果(候选为空时返回空表)
37
+ */
38
+ export function selectSemanticHits(pairs = [], gate = { margin: SEMANTIC_MARGIN, maxTake: SEMANTIC_MAX_TAKE }) {
39
+ const ranked = [...pairs].filter(([, c]) => Number.isFinite(c)).sort((a, b) => b[1] - a[1]);
40
+ if (!ranked.length) return [];
41
+ const floor = ranked[0][1] - gate.margin;
42
+ return ranked.filter(([, c]) => c >= floor).slice(0, gate.maxTake);
43
+ }
44
+
45
+ /**
46
+ * Reciprocal Rank Fusion:只消费名次,不消费分数,因此两路刻度不可比也不再是问题。
47
+ * @param {Array<Array<[string, number]>>} lists 每路的 [id, rank](rank 从 1 起)
48
+ * @param {number} k
49
+ * @returns {Array<[string, number]>} [id, 融合分] 降序
50
+ */
51
+ export function rrfFuse(lists = [], k = RRF_K) {
52
+ const acc = new Map();
53
+ for (const list of lists) {
54
+ for (const [id, rank] of list || []) {
55
+ if (!id || !(rank >= 1)) continue;
56
+ acc.set(id, (acc.get(id) || 0) + 1 / (k + rank));
57
+ }
58
+ }
59
+ return [...acc.entries()].sort((a, b) => b[1] - a[1]);
60
+ }
@@ -0,0 +1,118 @@
1
+ // agent/src/memory/injection.js - 记忆分级注入策略(纯函数、零依赖,便于单测)
2
+ // 三级加载:
3
+ // Tier1 常驻 importance ≥ alwaysInjectImportance 无条件注入(不占预算)
4
+ // Tier2 预算内 其余按 calcMemoryValue 降序在预算内填充(装不下的跳过继续尝试)
5
+ // Tier3 按需 未注入的转由系统提示词索引行引导 agent_memory recall 检索
6
+
7
+ export const MEMORY_INJECTION_CONFIG = {
8
+ alwaysInjectImportance: 8, // Tier1 常驻阈值(importance ≥ 此值不占预算)
9
+ budgetChars: 1500, // Tier2 字符预算(content + 标签 + 渲染开销近似)
10
+ budgetCount: 12, // Tier2 条数预算
11
+ itemOverheadChars: 50, // 每条渲染开销近似(序号、[重要性: N]、(标签: ...))
12
+ maxTopicTags: 8, // 索引提示最多展示的主题标签数
13
+ maxTagLength: 12, // 单个主题标签最大长度(超出截断)
14
+ };
15
+
16
+ /**
17
+ * 计算记忆价值分数(用于淘汰判断与注入排序)
18
+ * 迁移自 tool-memory.js(单一来源),行为不变:
19
+ * value = importance × (1 + ln(访问次数 + 1)) × 时间衰减
20
+ */
21
+ export function calcMemoryValue(memory, now) {
22
+ const importance = memory.importance || 5;
23
+ const accessCount = memory.accessCount || 0;
24
+ const createdAt = new Date(memory.createdAt).getTime();
25
+ const ageDays = (now - createdAt) / (1000 * 60 * 60 * 24);
26
+
27
+ // 时间衰减因子
28
+ let decay;
29
+ if (ageDays <= 7) decay = 1.0;
30
+ else if (ageDays <= 30) decay = 0.8;
31
+ else if (ageDays <= 90) decay = 0.5;
32
+ else decay = 0.2;
33
+
34
+ return importance * (1 + Math.log(accessCount + 1)) * decay;
35
+ }
36
+
37
+ /**
38
+ * 单条记忆注入成本近似(字符)
39
+ */
40
+ function estimateInjectionCost(memory, config) {
41
+ const contentLen = typeof memory.content === 'string' ? memory.content.length : 0;
42
+ const tagsLen = Array.isArray(memory.tags) ? memory.tags.join(', ').length : 0;
43
+ return contentLen + tagsLen + config.itemOverheadChars;
44
+ }
45
+
46
+ /**
47
+ * 收集未注入记忆的主题标签索引:
48
+ * 去重 → 频率降序(同频按首现顺序)→ 截断数量与单标签长度
49
+ */
50
+ function collectRemainingTags(remainingFacts, summaries, config) {
51
+ const counts = new Map();
52
+ const firstSeen = new Map();
53
+ let seq = 0;
54
+ const addTags = (item) => {
55
+ if (!Array.isArray(item.tags)) return;
56
+ for (const raw of item.tags) {
57
+ const tag = String(raw ?? '').trim();
58
+ if (!tag) continue;
59
+ if (!counts.has(tag)) {
60
+ counts.set(tag, 0);
61
+ firstSeen.set(tag, seq++);
62
+ }
63
+ counts.set(tag, counts.get(tag) + 1);
64
+ }
65
+ };
66
+ for (const m of remainingFacts) addTags(m);
67
+ for (const m of summaries) addTags(m);
68
+
69
+ return [...counts.keys()]
70
+ .sort((a, b) => (counts.get(b) - counts.get(a)) || (firstSeen.get(a) - firstSeen.get(b)))
71
+ .slice(0, config.maxTopicTags)
72
+ .map(tag => (tag.length > config.maxTagLength ? tag.slice(0, config.maxTagLength) : tag));
73
+ }
74
+
75
+ /**
76
+ * 三级筛选:选出注入系统提示词的记忆
77
+ * @param {Array} facts - 事实记忆数组(原文件顺序)
78
+ * @param {Array} summaries - 摘要记忆数组(永不注入,仅计入按需召回)
79
+ * @param {object} [config] - 注入配置
80
+ * @param {number} [now] - 当前时间戳(测试注入用)
81
+ * @returns {{injected: Array, remainingCount: number, remainingTags: string[]}}
82
+ */
83
+ export function selectMemoriesForInjection(facts, summaries, config = MEMORY_INJECTION_CONFIG, now = Date.now()) {
84
+ const factList = Array.isArray(facts) ? facts : [];
85
+ const summaryList = Array.isArray(summaries) ? summaries : [];
86
+ const threshold = config.alwaysInjectImportance;
87
+
88
+ // Tier1:常驻(不占预算),内部按价值降序
89
+ const tier1 = factList
90
+ .filter(m => (m.importance || 5) >= threshold)
91
+ .sort((a, b) => calcMemoryValue(b, now) - calcMemoryValue(a, now));
92
+ const tier1Set = new Set(tier1);
93
+
94
+ // Tier2:剩余按价值降序,在预算内贪心填充(装不下的跳过,继续尝试后续小条目)
95
+ const tier2Pool = factList
96
+ .filter(m => !tier1Set.has(m))
97
+ .sort((a, b) => calcMemoryValue(b, now) - calcMemoryValue(a, now));
98
+
99
+ const injected = [...tier1];
100
+ let usedChars = 0;
101
+ let usedCount = 0;
102
+ for (const m of tier2Pool) {
103
+ if (usedCount >= config.budgetCount) break;
104
+ const cost = estimateInjectionCost(m, config);
105
+ if (usedChars + cost > config.budgetChars) continue;
106
+ injected.push(m);
107
+ usedChars += cost;
108
+ usedCount++;
109
+ }
110
+
111
+ // Tier3:未注入的 facts + 全部 summaries 转按需召回
112
+ const injectedSet = new Set(injected);
113
+ const remainingFacts = factList.filter(m => !injectedSet.has(m));
114
+ const remainingCount = remainingFacts.length + summaryList.length;
115
+ const remainingTags = collectRemainingTags(remainingFacts, summaryList, config);
116
+
117
+ return { injected, remainingCount, remainingTags };
118
+ }
@@ -0,0 +1,193 @@
1
+ // agent/src/memory/routes.js - /api/memory/* 业务端点(认证与语言解析由 server.js 完成后转发)
2
+ // 零外部依赖:与 service 同进程,文件锁、session 召回缓存复用进程级单例。
3
+
4
+ import { createMemoryService } from './service.js';
5
+ import {
6
+ resolveMemoryEmbeddingConfig,
7
+ maskMemoryEmbeddingConfig,
8
+ saveMemoryEmbeddingConfig,
9
+ toEmbeddingProviderConfig
10
+ } from './embedding-config.js';
11
+ import { createEmbeddingProvider } from '../rag/embedding/index.js';
12
+
13
+ // 探活超时上限:语义通道绝不能拖垮调用方
14
+ const DEFAULT_EMBEDDING_TIMEOUT_MS = 8000;
15
+
16
+ // 进程级单例(默认路径 MEMORY_DIR/global-memory.json)
17
+ const defaultService = createMemoryService();
18
+
19
+ // 子路径 → service 方法映射
20
+ const ROUTE_METHODS = {
21
+ store: 'store',
22
+ recall: 'recall',
23
+ manage: 'manage',
24
+ inject: 'injectSelect',
25
+ // 重建向量缓存:派生缓存的显式重建入口(模型变更/首次启用语义通道后调一次)
26
+ reindex: 'backfillVectors'
27
+ };
28
+
29
+ /**
30
+ * 判断请求来源是否允许跨域读取响应(与 server.js 的 getAllowedOrigin 行为一致)
31
+ */
32
+ function getAllowedOrigin(req) {
33
+ const origin = req?.headers?.origin;
34
+ if (typeof origin === 'string' && origin.startsWith('chrome-extension://')) {
35
+ return origin;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * JSON 响应辅助(与 server.js 的 jsonResponse 行为一致)
42
+ */
43
+ function jsonResponse(res, status, data) {
44
+ const headers = {
45
+ 'Content-Type': 'application/json; charset=utf-8',
46
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
47
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS'
48
+ };
49
+ const allowedOrigin = getAllowedOrigin(res.req);
50
+ if (allowedOrigin) {
51
+ headers['Access-Control-Allow-Origin'] = allowedOrigin;
52
+ headers['Vary'] = 'Origin';
53
+ }
54
+ res.writeHead(status, headers);
55
+ res.end(JSON.stringify(data));
56
+ }
57
+
58
+ /**
59
+ * embed 调用超时保护。到期后必须 clearTimeout:否则 daemon 里每调一次就多挂一个定时器,
60
+ * 测试进程也会因为残留句柄延迟退出。
61
+ */
62
+ function withTimeout(promise, ms) {
63
+ let timer = null;
64
+ return Promise.race([
65
+ promise,
66
+ new Promise((_, reject) => {
67
+ timer = setTimeout(() => reject(new Error('embedding_timeout')), ms);
68
+ })
69
+ ]).finally(() => clearTimeout(timer));
70
+ }
71
+
72
+ /**
73
+ * 记忆 embedding 的配置/探活端点。配置类不走 ROUTE_METHODS,也不受「非 POST 即 404」约束
74
+ * (GET 是合法读操作);返回 true 表示已响应,false 则继续走业务分发/404。
75
+ * @param {{createProvider?: Function, embeddingTimeoutMs?: number}} [options] 测试接缝:注入假 provider
76
+ */
77
+ async function handleEmbeddingEndpoints(req, res, key, t, requestBody, options = {}) {
78
+ const body = requestBody || {};
79
+
80
+ if (key === 'embedding-config') {
81
+ if (req.method === 'GET') {
82
+ jsonResponse(res, 200, { success: true, config: maskMemoryEmbeddingConfig() });
83
+ return true;
84
+ }
85
+ if (req.method === 'POST') {
86
+ try {
87
+ const config = await saveMemoryEmbeddingConfig(body);
88
+ jsonResponse(res, 200, { success: true, config });
89
+ } catch (err) {
90
+ const error = err.message === 'invalid_embedding_mode'
91
+ ? t('memory.invalidEmbeddingMode')
92
+ : (err.message || 'embedding_config_error');
93
+ jsonResponse(res, 400, { success: false, error });
94
+ }
95
+ return true;
96
+ }
97
+ return false;
98
+ }
99
+
100
+ if (key === 'test-embedding') {
101
+ if (req.method !== 'POST') return false;
102
+ // 允许用请求体覆盖尚未保存的草稿配置,明文 key 缺省时回落已存 key
103
+ const cfg = resolveMemoryEmbeddingConfig(body);
104
+ const createProvider = options.createProvider || createEmbeddingProvider;
105
+ const timeoutMs = Number(options.embeddingTimeoutMs) > 0
106
+ ? Number(options.embeddingTimeoutMs)
107
+ : DEFAULT_EMBEDDING_TIMEOUT_MS;
108
+ try {
109
+ const provider = createProvider(toEmbeddingProviderConfig(cfg));
110
+ const vec = await withTimeout(Promise.resolve(provider.embedQuery('ping')), timeoutMs);
111
+ jsonResponse(res, 200, { success: true, dimensions: Array.isArray(vec) ? vec.length : 0 });
112
+ } catch (err) {
113
+ jsonResponse(res, 400, { success: false, error: err.message || 'embedding_test_failed' });
114
+ }
115
+ return true;
116
+ }
117
+
118
+ // 只读状态:条数 + 指纹陈旧。走 GET(配置类端点同例,不受「非 POST 即 404」约束)。
119
+ // 放在这里而不是 ROUTE_METHODS:后者只收 POST 业务动作,且需要请求体。
120
+ if (key === 'vector-status') {
121
+ if (req.method !== 'GET') return false;
122
+ const service = options.service || defaultService;
123
+ try {
124
+ const data = await service.vectorStatus();
125
+ if (data && data.success === false) {
126
+ jsonResponse(res, 400, data);
127
+ } else {
128
+ jsonResponse(res, 200, { success: true, ...data });
129
+ }
130
+ } catch (err) {
131
+ jsonResponse(res, 400, { success: false, error: err.message || 'vector_status_failed' });
132
+ }
133
+ return true;
134
+ }
135
+
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * /api/memory/* 分发
141
+ * @param {import('http').IncomingMessage} req
142
+ * @param {import('http').ServerResponse} res
143
+ * @param {string} pathname 形如 /api/memory/store
144
+ * @param {URL} url
145
+ * @param {(key: string, params?: object) => string} t 请求级翻译函数
146
+ * @param {object} requestBody 已解析的请求体
147
+ * @param {{service?: object}} [options] 可注入 service 实例(测试用),缺省使用进程级单例
148
+ */
149
+ export async function memoryDispatch(req, res, pathname, url, t, requestBody = {}, options = {}) {
150
+ const service = options.service || defaultService;
151
+ const key = pathname.slice('/api/memory/'.length).replace(/\/+$/, '');
152
+ const method = ROUTE_METHODS[key];
153
+
154
+ // 配置与探活端点先分流(GET 合法,且不进业务 404 判定)
155
+ if (await handleEmbeddingEndpoints(req, res, key, t, requestBody, options)) return;
156
+
157
+ // 404 仅表示路径/方法不匹配(扩展端据此探测旧版本 agent 无记忆端点)
158
+ if (!method || req.method !== 'POST') {
159
+ return jsonResponse(res, 404, { success: false, error: t('error.unknownApiPath') });
160
+ }
161
+
162
+ const body = requestBody || {};
163
+
164
+ try {
165
+ let data;
166
+ if (method === 'recall') {
167
+ data = await service.recall(body, body.sessionId ?? null, t);
168
+ } else if (method === 'injectSelect') {
169
+ data = await service.injectSelect(t);
170
+ } else if (method === 'backfillVectors') {
171
+ // 不接受请求体:只做「按当前配置把全部记忆补进向量缓存」,t 用于渲染跳过原因
172
+ data = await service.backfillVectors(t);
173
+ } else {
174
+ data = await service[method](body, t);
175
+ }
176
+
177
+ // 业务失败(参数缺失、文件读写失败):HTTP 400 + 原样 error,扩展端仍读 body
178
+ if (data && data.success === false) {
179
+ return jsonResponse(res, 400, data);
180
+ }
181
+ return jsonResponse(res, 200, { success: true, ...data });
182
+ } catch (err) {
183
+ console.error('[Memory] route error:', err);
184
+ return jsonResponse(res, 400, { success: false, error: err.message || 'memory_error' });
185
+ }
186
+ }
187
+
188
+ /**
189
+ * server.js 接线入口(签名与 ragRouter 一致)
190
+ */
191
+ export async function memoryRouter(req, res, pathname, url, t, requestBody) {
192
+ return memoryDispatch(req, res, pathname, url, t, requestBody);
193
+ }