ai-helper-agent 2.2.0 → 2.3.1

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 (53) 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-DcAs6YyA.js} +2 -2
  4. package/dist/assets/{agent-store-DOyMdB_7.js.map → agent-store-DcAs6YyA.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 +81 -65
  8. package/dist/assets/{i18n-MmYITlVj.js → i18n-C4jSRoCH.js} +5 -5
  9. package/dist/assets/i18n-C4jSRoCH.js.map +1 -0
  10. package/dist/assets/{model-profiles-CV_sUuPf.js → model-profiles-Dl_MsXWa.js} +5 -5
  11. package/dist/assets/model-profiles-Dl_MsXWa.js.map +1 -0
  12. package/dist/assets/options-8-nu3P5K.css +1 -0
  13. package/dist/assets/options.js +264 -197
  14. package/dist/assets/options.js.map +1 -1
  15. package/dist/assets/{page-snapshot-renderer-CE53ibi-.js → page-snapshot-renderer-CVL6r6Kf.js} +2 -2
  16. package/dist/assets/{page-snapshot-renderer-CE53ibi-.js.map → page-snapshot-renderer-CVL6r6Kf.js.map} +1 -1
  17. package/dist/assets/{session-store-B0ZPGm7d.js → session-store-CzVQCgQ_.js} +4 -4
  18. package/dist/assets/session-store-CzVQCgQ_.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-CNG2KGnJ.js +2 -0
  23. package/dist/assets/version-CNG2KGnJ.js.map +1 -0
  24. package/dist/libs/jspdf.min.js +1 -1
  25. package/dist/manifest.json +2 -2
  26. package/dist/offscreen/offscreen.js +4 -4
  27. package/dist/options.html +140 -3
  28. package/dist/side_panel.html +27 -4
  29. package/package.json +1 -1
  30. package/src/config.js +72 -24
  31. package/src/crypto-lite.js +31 -0
  32. package/src/locales/en.json +55 -0
  33. package/src/locales/zh.json +55 -0
  34. package/src/memory/embedding-config.js +95 -0
  35. package/src/memory/fusion.js +60 -0
  36. package/src/memory/injection.js +118 -0
  37. package/src/memory/routes.js +193 -0
  38. package/src/memory/service.js +957 -0
  39. package/src/memory/tags.js +62 -0
  40. package/src/memory/tier.js +37 -0
  41. package/src/memory/tokens.js +158 -0
  42. package/src/memory/vectors.js +128 -0
  43. package/src/rag/embedding/index.js +7 -0
  44. package/src/rag/embedding/local.js +25 -0
  45. package/src/rag/keyword-match.js +112 -0
  46. package/src/rag/manager.js +15 -0
  47. package/src/rag/searcher.js +15 -87
  48. package/src/server.js +6 -0
  49. package/dist/assets/i18n-MmYITlVj.js.map +0 -1
  50. package/dist/assets/model-profiles-CV_sUuPf.js.map +0 -1
  51. package/dist/assets/options-Df_1Dtn8.css +0 -1
  52. package/dist/assets/session-store-B0ZPGm7d.js.map +0 -1
  53. /package/dist/_locales/{zh → zh_CN}/messages.json +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # AI Helper Agent
4
4
 
5
- AI Helper Agent — a local proxy service that provides the [AI Helper Chrome Extension](https://github.com/xiweicheng/ai-helper) ([Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/kabhmgfbkhpbfhhnokaafhkdbckeipcl)) with local file read/write, system command execution, Skill system, MCP protocol extension, and an optional local knowledge base (RAG) capability.
5
+ AI Helper Agent — a local proxy service that provides the [AI Helper Chrome Extension](https://github.com/xiweicheng/ai-helper) ([Chrome Web Store](https://chromewebstore.google.com/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/fnolhphogjkdcmnecpeckhjoeofldiac) / [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/kabhmgfbkhpbfhhnokaafhkdbckeipcl)) with local file read/write, system command execution, Skill system, MCP protocol extension, and an optional local knowledge base (RAG) capability.
6
6
 
7
7
  ## Installation
8
8
 
@@ -156,6 +156,7 @@ Configuration file path: `~/.ai-helper-agent/config.json`
156
156
  | `pairCodeTTL` | Pairing code validity (seconds) | 30 |
157
157
  | `commandTimeout` | Command execution timeout (ms) | 300000 (5 min) |
158
158
  | `fileMaxSize` | Max file read/write size (bytes) | 52428800 (50 MB) |
159
+ | `memoryEmbedding` | Semantic memory recall channel config — see [Semantic recall channel](#semantic-recall-channel-tiers). Managed through `/api/memory/embedding-config`; `enabled: false` keeps recall keyword-only | disabled |
159
160
 
160
161
  ## File Search
161
162
 
@@ -243,6 +244,15 @@ Results are returned in reverse chronological order (newest first).
243
244
  | POST | `/api/fs/delete` | Delete file/directory |
244
245
  | POST | `/api/fs/search_files` | Search files by name pattern (glob) |
245
246
  | POST | `/api/fs/search_content` | Search file content (rg preferred, Node.js fallback) |
247
+ | POST | `/api/memory/store` | Write long-term memory (action: add / update / delete) |
248
+ | POST | `/api/memory/recall` | Recall long-term memory by keyword scoring (with per-session dedup) |
249
+ | POST | `/api/memory/manage` | Memory review (`action: review`) / compaction (`action: compact`) |
250
+ | POST | `/api/memory/inject` | Three-tier injection selection for the system prompt |
251
+ | POST | `/api/memory/reindex` | Rebuild the memory vector cache for every entry using the current embedding config |
252
+ | GET | `/api/memory/embedding-config` | Read the semantic recall channel config (plain key never echoed; `hasApiKey` included) |
253
+ | POST | `/api/memory/embedding-config` | Save the semantic recall channel config |
254
+ | POST | `/api/memory/test-embedding` | Probe the embedding endpoint (returns vector dimensions on success) |
255
+ | GET | `/api/memory/vector-status` | Read-only vector index status: `indexed` / `total`, stored vs current fingerprint (`storedModelKey` / `currentModelKey`), `stale`, `tier`. Never builds a provider, so querying it cannot trigger a model download |
246
256
  | POST | `/api/exec` | Execute system command |
247
257
  | POST | `/api/exec/stop` | Stop command execution |
248
258
  | GET | `/api/exec/running` | List running processes |
@@ -381,6 +391,89 @@ Configuration file path: `~/.ai-helper-agent/config.json`
381
391
  4. Tool call requests are forwarded to MCP servers via the agent
382
392
  5. Tool results are returned to the extension
383
393
 
394
+ ## Long-term Memory
395
+
396
+ Cross-session long-term memory is owned by the agent: the storage file, the read-modify-write lock, recall scoring, review/compaction and injection selection all run in this process. The extension only forwards tool calls to the endpoints below — no generic file-API round-trips, no dependency on the extension Service Worker staying alive.
397
+
398
+ - **Data location** — `~/.ai-helper-agent/memory/global-memory.json` (facts + summaries + stats)
399
+ - **Concurrency** — an in-process promise-chain lock serializes every read-modify-write; the per-session recall cache (which memories were already surfaced in this conversation) lives in agent memory
400
+ - **i18n** — response text is rendered from the request's `Accept-Language`
401
+ - **Where summaries come from** — besides explicit tool writes, `type: "summary"` entries are sedimented automatically by the extension's session context compaction (tags `compaction` + `session:<id>`, `importance: 6`, `category: "decision"`); re-compacting the same session updates the same `memoryId` instead of piling up
402
+
403
+ ### Record schema
404
+
405
+ ```json
406
+ {
407
+ "id": "fact_a1b2c3d4_xxxx",
408
+ "type": "fact | summary",
409
+ "category": "custom",
410
+ "content": "...",
411
+ "tags": ["..."],
412
+ "importance": 5,
413
+ "accessCount": 0,
414
+ "lastAccessAt": null,
415
+ "createdAt": "2026-01-15T10:30:00.000Z",
416
+ "updatedAt": "2026-01-15T10:30:00.000Z",
417
+ "sourceSessionId": null
418
+ }
419
+ ```
420
+
421
+ ### Recall scoring
422
+
423
+ A memory's keyword score is `Σ (matched units in a field × that field's weight)`, plus whole-query bonuses:
424
+
425
+ | Signal | Score |
426
+ |--------|-------|
427
+ | matched unit in `content` | × 3 per unit |
428
+ | matched unit in `title` | × 2 per unit |
429
+ | matched unit in `tags` | × 2 per unit |
430
+ | full query (≥ 2 chars) found in `content` | +5 |
431
+ | full query (≥ 2 chars) found in `title` | +3 |
432
+
433
+ Units come from the **longest matching fragment**, not from chopping the query into pieces: a CJK fragment of length 2 counts as 1 unit, 3 counts as 2, ≥ 4 counts as 3 (top of the scale), and each whole latin/digit word counts as 1 — capped at 3 units per field. Extraction and matching are zero-dependency and **shared with knowledge-base retrieval** (`rag/keyword-match.js`), so a query means the same thing in both subsystems.
434
+
435
+ Two gates keep irrelevant memories out:
436
+
437
+ - **Document frequency** — a unit found in over half of the candidates (and in more than 2 of them) carries no discriminating power and scores nothing.
438
+ - **Hit threshold** — a memory counts as a hit only when it matched a *whole* unit (an entire latin word or an entire CJK run) or at least 2 units. Sharing only a random 2-character fragment is no longer a hit; the exact-match bonuses alone cannot rescue such an entry.
439
+
440
+ ### Semantic recall channel (tiers)
441
+
442
+ A vector channel can be layered on top of keyword scoring. It runs at one of three tiers and **the default is always Tier A**:
443
+
444
+ | Tier | Condition | Behaviour |
445
+ |------|-----------|-----------|
446
+ | A `none` | Unconfigured or `enabled: false` (default) | Keyword scoring only, exactly as above; no new network calls |
447
+ | B `remote` | `mode: "openai-compat"` with both apiKey and modelName set | Remote embeddings + in-process cosine comparison |
448
+ | C `local` | `mode: "local"` and RAG dependencies present locally | Vectors from a local transformers model |
449
+
450
+ - **Vectors are a derived cache** — `~/.ai-helper-agent/memory/memory-vectors.json` can be deleted at any time; `global-memory.json` is the single source of truth. A missing or corrupt cache is treated as empty and refilled on demand during recall — it never throws.
451
+ - **Changing the model invalidates everything** — the cache key is `mode|modelName|dimensions`; if any part changes the old vectors are dropped rather than compared (mismatched dimensions cannot be compared). No manual cleanup needed.
452
+ - **Failure degrades, it does not break memory** — when the embedding endpoint errors or exceeds its 8s timeout, that recall falls back to Tier A and still answers 200. `tier` and `vectorMatched` in the recall response say which tier actually ran and how many hits were accepted by the semantic route (capped at 2).
453
+ - **Both channels fuse ranks, not scores** — keywords and vectors each produce their own ranking, which is merged as `1/(60 + rank)` (RRF). The two scales are not comparable, so only **rank** is consumed, never magnitude: an item hit by both routes naturally floats to the top, one hit by a single route lands behind it.
454
+ - **The semantic gate has two stages, not one absolute cosine threshold** — (1) **query-level floor**: if the best cosine of this query is below `0.48`, the library is judged to hold no answer for it and the whole semantic route contributes nothing (recall output goes straight into the prompt — "everything is marginally relevant" equals "nothing is relevant"); (2) **item-level relative drop-off**: only items within `0.05` of that best score are accepted, at most 2.
455
+ - **Why there is no absolute similarity threshold** — measured on a real 41-item library: absolute cosine fails at item level (irrelevant queries reach top1 scores of 0.3340~0.4506, while a ≥0.3 threshold admits 40/41 items and ≥0.45 still admits 20/41). It separates only at query-level top1 (irrelevant 0.3340~0.4506 vs relevant 0.5467~0.6353, gap 0.096), which is exactly where the floor above is applied.
456
+ - **Known limits** — the `0.48` floor was calibrated on `BAAI/bge-m3` with a 41-item library and 9 queries; its usable window is `(0.4506, 0.5163]` (roughly 0.03 of headroom on each side). **Recalibration is mandatory when the embedding model or the memory-text concatenation changes**, and the rule needs re-measurement once the library grows to a few hundred items. Separation (`top1 − top2`) was the original main gate of this channel and was falsified by measurement: the larger the library, the more an irrelevant query's top1−top2 can grow by chance, and the real library's target query had a gap of 0.0214 — below two irrelevant queries at 0.0361 / 0.0397, so the two are inseparable on that axis. **Do not add it back.** Practical consequence: when a target item is not the library's best cosine and falls outside the `0.05` drop-off, the semantic route will not return it (on the real library such items still enter via the keyword route, at a lower rank).
457
+ - **The local backend only indexes the first 512 tokens (the tail is dropped silently)** — the tokenizer window of `Xenova/bge-small-zh-v1.5` is 512 tokens and inference cuts the tail **without any error** (measured: 13 of 41 items in a real library exceed it; the largest one gets only 46% of its content in). Blast radius is limited: the semantic channel cannot see the tail, while **the keyword channel still reads the full text**. The knowledge base is unaffected (its default `chunkSize: 400` already fits). Remote models have far larger windows, and a vendor's truncation behaviour is invisible in the vectors it returns — so the remote tier reports nothing. This round changes no vector content; it only makes the overflow visible: `reindex` now returns `overWindow: { count, window, ids }` and the process log names the items.
458
+ - **Vectors drift slightly with batch composition (known noise, not a bug)** — embedding one memory alone vs inside a batch gives cos 0.9940–0.9974 (measured), caused by the `q8` quantized kernels and unrelated to how long the batch partners are; no observable effect on rank fusion and no effect on recall results.
459
+ - **Operations entry point** — after first enabling the channel or switching models, call `POST /api/memory/reindex` once to backfill existing memories (it returns `skipped` when the channel is off, and a clear 400 — not 500 — when the endpoint is unreachable). When only some batches fail the response still reports success but carries `failedBatches` plus `lastError` (the first reason); the full reasons go to the process log. On the local tier it also carries `overWindow` (how many items exceed the window, and which).
460
+ - **Configuration UI** — the extension's options page has a **Memory** tab that reads and writes this channel (`/api/memory/embedding-config`), probes the endpoint (`/api/memory/test-embedding`, remote tier only — the local tier's first call has to load the model and would always blow the 8s budget, so the button is hidden there instead of lying), rebuilds the cache (`/api/memory/reindex` behind an explicit confirmation that quotes the measured cost) and shows `GET /api/memory/vector-status`, whose `stale` flag is what makes "the model changed but nobody rebuilt" visible instead of silently degrading recall. Note the tab is a convenience, not a restart workaround: `loadConfig()` caches by file mtime and the provider cache is keyed by the fingerprint, so editing `config.json` by hand takes effect just as immediately. Its real value is discoverability, verification and mis-fill prevention. One rule the form follows strictly — a field the user cannot see is **absent from the request body**, never empty or `0`: sending `dimensions: 0` would overwrite a stored `512`, change the fingerprint, raise a false `stale` and force a pointless rebuild.
461
+ - **Fingerprint staleness is report-only** — `vector-status` tells you the stored vectors were built by a different `mode|modelName|dimensions`; it does not discard or rewrite them, because `stale` with `indexed > 0` is a state worth looking at (and an empty library is never called stale — first-time enablement is normal).
462
+
463
+ ### Injection tiers (system prompt)
464
+
465
+ | Tier | Rule |
466
+ |------|------|
467
+ | 1 — resident | `importance >= 8` is always injected |
468
+ | 2 — budgeted | ranked by value, capped at 1500 characters and 12 items |
469
+ | 3 — index only | remaining memories surface as tag index lines; the model pulls them with a recall call |
470
+
471
+ > Entries with `type: "summary"` are **never** part of Tier 1/2 injection — they count only toward Tier 3 (tag index + on-demand recall), so compaction summaries never eat system-prompt budget; the model pulls them when needed.
472
+
473
+ Memory value = `importance x (1 + ln(accessCount + 1)) x timeDecay`; entries whose value falls below 10 are evicted by compaction (`/api/memory/manage` with `action: "compact"`).
474
+
475
+ > Long-term memory needs an agent build that ships the `/api/memory/*` endpoints. The extension deliberately does **not** fall back to the generic file API — a missing endpoint is reported as a version-mismatch hint instead.
476
+
384
477
  ## Knowledge Base (RAG)
385
478
 
386
479
  Optional local retrieval-augmented generation. Turn documents into searchable knowledge bases so the extension can answer with cited sources. The RAG stack ships as optional dependencies — the agent detects availability at startup and reports it via `/api/rag/status`; when dependencies are missing, RAG requests return `503` and the extension offers one-click installation (whitelisted packages, progress polled from `/api/rag/install/status`). Installation is also available from the CLI: `ai-helper-agent rag status` / `ai-helper-agent rag install`. Users who have installed the dependencies successfully before get **automatic recovery on startup**: `npm -g` upgrades prune manually-installed packages inside the package directory, so the agent re-installs them automatically (marked `trigger=auto`) — no manual action needed; users who never installed successfully are not prompted.
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # AI Helper Agent
4
4
 
5
- AI Helper 代理服务,为 [AI Helper Chrome 扩展](https://github.com/xiweicheng/ai-helper)([Edge 扩展商店](https://microsoftedge.microsoft.com/addons/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/kabhmgfbkhpbfhhnokaafhkdbckeipcl))提供本地文件读写、系统命令执行、Skill 技能系统、MCP 协议扩展与可选本地知识库(RAG)能力。
5
+ AI Helper 代理服务,为 [AI Helper Chrome 扩展](https://github.com/xiweicheng/ai-helper)([Chrome 应用商店](https://chromewebstore.google.com/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/fnolhphogjkdcmnecpeckhjoeofldiac) / [Edge 扩展商店](https://microsoftedge.microsoft.com/addons/detail/ai-helper-%E7%BD%91%E9%A1%B5%E6%99%BA%E8%83%BD%E5%8A%A9%E6%89%8B/kabhmgfbkhpbfhhnokaafhkdbckeipcl))提供本地文件读写、系统命令执行、Skill 技能系统、MCP 协议扩展与可选本地知识库(RAG)能力。
6
6
 
7
7
  ## 安装
8
8
 
@@ -156,6 +156,7 @@ Agent 通过 PID 文件(`~/.ai-helper-agent/agent.pid`)管理进程生命周
156
156
  | `pairCodeTTL` | 配对码有效期(秒) | 30 |
157
157
  | `commandTimeout` | 命令执行超时(毫秒) | 300000(5分钟) |
158
158
  | `fileMaxSize` | 文件读写最大字节数 | 52428800(50MB) |
159
+ | `memoryEmbedding` | 记忆语义召回通道配置,详见[语义召回通道](#语义召回通道tier);推荐通过 `/api/memory/embedding-config` 读写,`enabled: false` 时 recall 只走关键词 | 关闭 |
159
160
 
160
161
  ## 文件搜索
161
162
 
@@ -243,6 +244,15 @@ GET /api/logs/dates
243
244
  | POST | `/api/fs/delete` | 删除文件/目录 |
244
245
  | POST | `/api/fs/search_files` | 按文件名模式搜索(glob) |
245
246
  | POST | `/api/fs/search_content` | 搜索文件内容(rg 优先,Node.js 回退) |
247
+ | POST | `/api/memory/store` | 写入长期记忆(action:add / update / delete) |
248
+ | POST | `/api/memory/recall` | 关键词计分召回长期记忆(含会话级去重) |
249
+ | POST | `/api/memory/manage` | 记忆审查(`action: review`)/ 压缩淘汰(`action: compact`) |
250
+ | POST | `/api/memory/inject` | 为系统提示词执行三级注入筛选 |
251
+ | POST | `/api/memory/reindex` | 按当前 embedding 配置重建全部记忆向量缓存 |
252
+ | GET | `/api/memory/embedding-config` | 读取语义召回通道配置(明文 key 永不回显,附 `hasApiKey`) |
253
+ | POST | `/api/memory/embedding-config` | 保存语义召回通道配置 |
254
+ | POST | `/api/memory/test-embedding` | 探活 embedding 端点(成功回报向量维度) |
255
+ | GET | `/api/memory/vector-status` | 只读向量索引状态:`indexed` / `total`、已存指纹与当前指纹(`storedModelKey` / `currentModelKey`)、`stale`、`tier`。不构造 provider,因此查状态不会触发模型下载 |
246
256
  | POST | `/api/exec` | 执行系统命令 |
247
257
  | POST | `/api/exec/stop` | 停止命令执行 |
248
258
  | GET | `/api/exec/running` | 运行中的进程列表 |
@@ -381,6 +391,89 @@ enabled: true
381
391
  4. 工具调用请求通过 Agent 转发到 MCP Server
382
392
  5. 工具结果返回给扩展
383
393
 
394
+ ## 长期记忆
395
+
396
+ 跨会话长期记忆由 Agent 全权托管:存储文件、读写互斥锁、召回计分、审查/淘汰、注入筛选全部在本进程内完成。插件只向下表端点转发工具调用——不再走通用文件 API 往返,也不依赖插件 Service Worker 是否存活。
397
+
398
+ - **数据位置** — `~/.ai-helper-agent/memory/global-memory.json`(facts + summaries + stats)
399
+ - **并发** — 进程内 Promise 链互斥锁串行化每一次读-改-写;会话级召回缓存(本对话已呈现过哪些记忆)常驻 Agent 内存
400
+ - **国际化** — 响应文案根据请求 `Accept-Language` 渲染
401
+ - **摘要来源** — 除工具主动写入外,`type: "summary"` 条目可由插件的会话上下文压缩自动沉淀(标签 `compaction` + `session:<会话 ID>`,`importance: 6`,`category: "decision"`);同一会话重复压缩按 `memoryId` 更新同一条,不堆积
402
+
403
+ ### 记录结构
404
+
405
+ ```json
406
+ {
407
+ "id": "fact_a1b2c3d4_xxxx",
408
+ "type": "fact | summary",
409
+ "category": "custom",
410
+ "content": "...",
411
+ "tags": ["..."],
412
+ "importance": 5,
413
+ "accessCount": 0,
414
+ "lastAccessAt": null,
415
+ "createdAt": "2026-01-15T10:30:00.000Z",
416
+ "updatedAt": "2026-01-15T10:30:00.000Z",
417
+ "sourceSessionId": null
418
+ }
419
+ ```
420
+
421
+ ### 召回计分
422
+
423
+ 记忆关键词分 = `Σ(某字段上的命中单元数 × 该字段权重)`,另加整句原样命中分:
424
+
425
+ | 命中信号 | 分值 |
426
+ |----------|------|
427
+ | `content` 的命中单元 | × 3 / 单元 |
428
+ | `title` 的命中单元 | × 2 / 单元 |
429
+ | `tags` 的命中单元 | × 2 / 单元 |
430
+ | 查询原文(≥ 2 字)出现在 `content` 中 | +5 |
431
+ | 查询原文(≥ 2 字)出现在 `title` 中 | +3 |
432
+
433
+ 单元数来自**最长命中片段**,而不是把查询拆碎:中文片段长 2 计 1 个单元、长 3 计 2 个、≥ 4 计 3 个(顶格),英文/数字整词每命中一个计 1 个;单字段封顶 3 个单元。切段与匹配零依赖,且**与知识库检索共用同一份实现**(`rag/keyword-match.js`),同一条查询在两个子系统里含义一致。
434
+
435
+ 两道闸门挡住无关条目:
436
+
437
+ - **文档频率剔除** —— 在过半候选(且超过 2 条)里出现的单元无区分度,不参与计分。
438
+ - **命中门槛** —— 只有命中了**完整单元**(整个英文词或整个中文连续段)或累计 ≥ 2 个单元才算命中。只共享一个随机 2 字碎片不再算命中,整句原样加分也不能单独救回这样的条目。
439
+
440
+ ### 语义召回通道(Tier)
441
+
442
+ 关键词之外可以叠加一路向量召回,分三档,**缺省永远是 A 档**:
443
+
444
+ | 档位 | 条件 | 行为 |
445
+ |------|------|------|
446
+ | A `none` | 未配置或 `enabled: false`(缺省) | 只走上表的关键词计分,不产生任何新的网络调用 |
447
+ | B `remote` | `mode: "openai-compat"` 且 apiKey 与 modelName 都已填 | 远端 embedding + 进程内余弦比对 |
448
+ | C `local` | `mode: "local"` 且本机已具备 RAG 依赖 | 本地 transformers 模型出向量 |
449
+
450
+ - **向量是派生缓存** — `~/.ai-helper-agent/memory/memory-vectors.json` 可随时整删重建;`global-memory.json` 是唯一真源。缓存缺失或损坏一律当空处理,并在召回命中时按需补齐,绝不抛错。
451
+ - **模型一变即整体作废** — 缓存键是 `mode|modelName|dimensions`,任一变化都不再复用旧向量(维度不同无法比对),无需手工清理。
452
+ - **失败即降级,不拖垮记忆** — embedding 端点报错或超时(上限 8s)时,该次召回自动退回 A 档且响应仍是 200。`recall` 响应里的 `tier` 与 `vectorMatched` 说明这次实际走了哪一档、有几条被语义路采信(后者上限 2)。
453
+ - **双通道只融合名次,不融合分数** — 关键词与向量各排各的名次,按 `1/(60 + 名次)` 相加后重排(RRF)。两路分值刻度不可比,因此**只比名次、不比大小**:同时被两路命中的条目自然排前,只被一路命中的靠后。
454
+ - **语义采信门是两道,而不是「cos 超过某个绝对阈值」** — ① **查询级落点地板**:本次查询的最高 cos 低于 `0.48` 时判定「本库对这个查询没有答案」,整个语义通道一条都不采信(记忆会直接注入提示词,「全都勉强相关」等于「没有相关」);② **条目级相对落差**:只采信距该最高分 `0.05` 以内的头部,最多 2 条。
455
+ - **为什么没有绝对相似度阈值** — 在真实 41 条库上:条目级绝对 cos 不可用(无关查询的 top1 落在 0.3340~0.4506,而阈值 ≥0.3 会放过 40/41 条、≥0.45 仍放过 20/41);它只在查询级 top1 上可分(无关 0.3340~0.4506 vs 相关 0.5467~0.6353,间隙 0.096),所以上述地板只作用在查询级。
456
+ - **已知限制** — 地板 `0.48` 在 `BAAI/bge-m3` + 41 条库 + 9 条查询上标定,可用窗口为 `(0.4506, 0.5163]`(上下余量各约 0.03),**换 embedding 模型或改变记忆文本拼接方式后必须重新标定**;记忆条目数增长到几百条后该规则也需重测。分离度(`top1 − top2`)曾是本通道的主门,已被实测否定:库越大、无关查询的 top1−top2 越可能偶然变大,而真实库目标查询的 gap(0.0214)低于两条无关查询的(0.0361 / 0.0397),在 gap 轴上根本分不开——**不要把它加回来**。具体后果:目标条目的 cos 不是全场第 1、且落在头部 `0.05` 落差之外时,语义路不会返回它(真实库上这种条目靠词面路进窗口,名次会被压低)。
457
+ - **本地档只索引前 512 token(超出部分静默不进向量)** — 本地 `Xenova/bge-small-zh-v1.5` 的 tokenizer 窗口为 512 token,推理时**不报错地**砍掉尾部(实测真实库 41 条中 13 条超窗,最大一条只吃进 46% 的内容)。影响面有限:语义路看不到尾部,而**词面路拿的是全文**。知识库侧不受影响(默认 `chunkSize: 400` 已在窗口内)。远端模型窗口普遍大得多,且厂商截断行为从返回向量里看不出来,所以远端档不报该字段。本轮不改向量内容,只把超窗变成可见:`reindex` 响应带 `overWindow: { count, window, ids }`,进程日志点名是哪几条。
458
+ - **向量会随分批组合轻微漂移(已知噪声,非缺陷)** — 同一条记忆逐条嵌 vs 同批嵌的 cos 在 0.9940~0.9974(实测),源于 `q8` 量化核的数值差异,与批内文本长短无关;对名次融合无可观测影响,不影响召回结果。
459
+ - **运维入口** — 首次启用或更换模型后调一次 `POST /api/memory/reindex` 把存量记忆补进缓存(未启用时返回 `skipped`,端点不可达则明确 400 而非 500)。部分批失败时响应仍为成功但带 `failedBatches` 与 `lastError`(首条原因),全量原因进进程日志;本地档还会带 `overWindow`(超窗条数与点名)。
460
+ - **配置界面** — 扩展 options 页新增「记忆」Tab:读写本通道(`/api/memory/embedding-config`)、探活(`/api/memory/test-embedding`,**仅远端档**——本地档首次调用要先加载模型,必定击穿 8s 超时,所以那里直接隐藏按钮而不是给个骗人的失败)、重建缓存(`/api/memory/reindex`,执行前弹确认并写明实测耗时)、展示 `GET /api/memory/vector-status`——其中 `stale` 才是把「改了模型但没人重建」从静默退化变成可见的那个信号。注意这个界面不是为「免重启」而做的:`loadConfig()` 按文件 mtime 缓存、provider 缓存又按指纹取键,手改 `config.json` 同样即时生效。它的真实价值是可发现、可验证、防填错。表单硬守一条规则——用户看不见的字段从请求体里**缺席**,绝不交空值或 `0`:交 `dimensions: 0` 会把已存的 512 覆盖掉、改变指纹、冒报 `stale`,逼用户做一次无谓重建。
461
+ - **指纹陈旧只报告不改数据** — `vector-status` 只告诉你已存向量是由另一个 `mode|modelName|dimensions` 生成的,不删除也不重写:`stale` 且 `indexed > 0` 本身就是要看到的状态(而空库永不判为陈旧——首次启用是正常态)。
462
+
463
+ ### 三级注入策略(系统提示词)
464
+
465
+ | 层级 | 规则 |
466
+ |------|------|
467
+ | 1 — 常驻 | `importance >= 8` 总是注入 |
468
+ | 2 — 预算 | 按价值排序,上限 1500 字符 + 12 条 |
469
+ | 3 — 仅索引 | 其余记忆只以标签索引行呈现,由模型主动 recall 拉取 |
470
+
471
+ > `type: "summary"` 的条目**不参与** Tier 1/2 注入,只计入 Tier 3(标签索引 + 按需召回)——会话压缩摘要因此不会挤占系统提示词预算,需要时由模型自己拉取。
472
+
473
+ 记忆价值 = `importance x (1 + ln(accessCount + 1)) x 时间衰减`;价值降到 10 以下的条目由压缩淘汰(`/api/memory/manage` 的 `action: "compact"`)。
474
+
475
+ > 长期记忆需要带 `/api/memory/*` 端点的 Agent 版本。插件**不会**回退到通用文件 API——端点缺失时直接提示版本不匹配。
476
+
384
477
  ## 知识库(RAG)
385
478
 
386
479
  可选的本地检索增强生成(RAG)能力:将文档索引为可检索的知识库,让插件回答时附带引用来源。RAG 依赖为可选安装 —— Agent 启动时自动检测可用性,并通过 `/api/rag/status` 对外暴露;依赖缺失时 RAG 请求返回 `503`,插件端提供一键安装(白名单依赖,进度可经 `/api/rag/install/status` 轮询)。也可通过 CLI 安装:`ai-helper-agent rag status` / `ai-helper-agent rag install`。曾成功安装过依赖的用户享受**启动自动恢复**:`npm -g` 更新代理会清理包目录内手动安装的依赖,Agent 启动检测到缺失时自动重新安装(标记 `trigger=auto`),用户零操作;从未安装成功过的用户不触发。
@@ -1,2 +1,2 @@
1
- import{r as e}from"./rolldown-runtime-C0FnF6B9.js";import{a as t,c as n}from"./i18n-MmYITlVj.js";import{_ as r,g as i}from"./session-store-B0ZPGm7d.js";var a=e({createAgent:()=>m,deleteAgent:()=>g,getActiveAgentId:()=>_,getAgent:()=>p,getAllAgents:()=>d,getCustomAgents:()=>f,setActiveAgentId:()=>v,updateAgent:()=>h});t(`zh`,{agentStore:{unnamedAgent:`未命名Agent`}}),t(`en`,{agentStore:{unnamedAgent:`Unnamed Agent`}});var o=`customAgents`,s=`activeAssistantId`,c=`activeAgentId`,l=!1;async function u(){if(l)return;l=!0;let e=await chrome.storage.local.get([c,s]),t=e[c],n=e[s];t&&!n&&!t.startsWith(`pa_`)&&(await chrome.storage.local.set({[s]:t}),await chrome.storage.local.remove(c))}async function d(){let e=(await chrome.storage.local.get([o]))[o]||[];return[...i,...e]}async function f(){return(await chrome.storage.local.get([o]))[o]||[]}async function p(e){return e&&(await d()).find(t=>t.id===e)||null}async function m(e){let t=await f(),i={id:r(),name:e.name||n(`agentStore.unnamedAgent`),description:e.description||``,icon:e.icon||`🤖`,systemPrompt:e.systemPrompt||``,toolIds:e.toolIds||null,skillIds:e.skillIds||null,mcpExcludedServerIds:e.mcpExcludedServerIds??null,isBuiltin:!1,allowSubDispatch:e.allowSubDispatch!==void 0&&e.allowSubDispatch,model:e.model||null,temperature:e.temperature===void 0?null:e.temperature,topP:e.topP===void 0?null:e.topP,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};return t.push(i),await chrome.storage.local.set({[o]:t}),i}async function h(e,t){let n=await f(),r=n.findIndex(t=>t.id===e);return r===-1?null:(n[r]={...n[r],...t,id:e,isBuiltin:!1,updatedAt:new Date().toISOString()},await chrome.storage.local.set({[o]:n}),n[r])}async function g(e){let t=await f(),n=t.findIndex(t=>t.id===e);return n!==-1&&(t.splice(n,1),await chrome.storage.local.set({[o]:t}),(await chrome.storage.local.get([s]))[s]===e&&await chrome.storage.local.remove(s),!0)}async function _(){return await u(),(await chrome.storage.local.get([s]))[s]||null}async function v(e){e?await chrome.storage.local.set({[s]:e}):await chrome.storage.local.remove(s)}export{p as a,h as c,_ as i,m as n,d as o,g as r,v as s,a as t};
2
- //# sourceMappingURL=agent-store-DOyMdB_7.js.map
1
+ import{r as e}from"./rolldown-runtime-C0FnF6B9.js";import{a as t,c as n}from"./i18n-C4jSRoCH.js";import{_ as r,v as i}from"./session-store-CzVQCgQ_.js";var a=e({createAgent:()=>m,deleteAgent:()=>g,getActiveAgentId:()=>_,getAgent:()=>p,getAllAgents:()=>d,getCustomAgents:()=>f,setActiveAgentId:()=>v,updateAgent:()=>h});t(`zh`,{agentStore:{unnamedAgent:`未命名Agent`}}),t(`en`,{agentStore:{unnamedAgent:`Unnamed Agent`}});var o=`customAgents`,s=`activeAssistantId`,c=`activeAgentId`,l=!1;async function u(){if(l)return;l=!0;let e=await chrome.storage.local.get([c,s]),t=e[c],n=e[s];t&&!n&&!t.startsWith(`pa_`)&&(await chrome.storage.local.set({[s]:t}),await chrome.storage.local.remove(c))}async function d(){let e=(await chrome.storage.local.get([o]))[o]||[];return[...r,...e]}async function f(){return(await chrome.storage.local.get([o]))[o]||[]}async function p(e){return e&&(await d()).find(t=>t.id===e)||null}async function m(e){let t=await f(),r={id:i(),name:e.name||n(`agentStore.unnamedAgent`),description:e.description||``,icon:e.icon||`🤖`,systemPrompt:e.systemPrompt||``,toolIds:e.toolIds||null,skillIds:e.skillIds||null,mcpExcludedServerIds:e.mcpExcludedServerIds??null,isBuiltin:!1,allowSubDispatch:e.allowSubDispatch!==void 0&&e.allowSubDispatch,model:e.model||null,temperature:e.temperature===void 0?null:e.temperature,topP:e.topP===void 0?null:e.topP,createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};return t.push(r),await chrome.storage.local.set({[o]:t}),r}async function h(e,t){let n=await f(),r=n.findIndex(t=>t.id===e);return r===-1?null:(n[r]={...n[r],...t,id:e,isBuiltin:!1,updatedAt:new Date().toISOString()},await chrome.storage.local.set({[o]:n}),n[r])}async function g(e){let t=await f(),n=t.findIndex(t=>t.id===e);return n!==-1&&(t.splice(n,1),await chrome.storage.local.set({[o]:t}),(await chrome.storage.local.get([s]))[s]===e&&await chrome.storage.local.remove(s),!0)}async function _(){return await u(),(await chrome.storage.local.get([s]))[s]||null}async function v(e){e?await chrome.storage.local.set({[s]:e}):await chrome.storage.local.remove(s)}export{p as a,h as c,_ as i,m as n,d as o,g as r,v as s,a as t};
2
+ //# sourceMappingURL=agent-store-DcAs6YyA.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-store-DOyMdB_7.js","names":[],"sources":["../../src/side_panel/agent-store.js"],"sourcesContent":["// side_panel/agent-store.js - Agent 数据持久化\nimport { BUILTIN_AGENTS, generateAgentId } from '../shared/agent-defaults.js';\nimport { t, registerTranslations } from '../shared/i18n.js';\n\nregisterTranslations('zh', {\n agentStore: {\n unnamedAgent: '未命名Agent',\n },\n});\nregisterTranslations('en', {\n agentStore: {\n unnamedAgent: 'Unnamed Agent',\n },\n});\n\nconst STORAGE_KEY = 'customAgents';\nconst ACTIVE_KEY = 'activeAssistantId';\nconst LEGACY_ACTIVE_KEY = 'activeAgentId';\n\nlet migratedActiveKey = false;\n\nasync function migrateLegacyActiveKey() {\n if (migratedActiveKey) return;\n migratedActiveKey = true;\n\n const result = await chrome.storage.local.get([LEGACY_ACTIVE_KEY, ACTIVE_KEY]);\n const legacyValue = result[LEGACY_ACTIVE_KEY];\n const newValue = result[ACTIVE_KEY];\n\n if (legacyValue && !newValue && !legacyValue.startsWith('pa_')) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: legacyValue });\n await chrome.storage.local.remove(LEGACY_ACTIVE_KEY);\n }\n}\n\n/**\n * 获取所有 Agent(内置 + 用户自定义)\n * @returns {Promise<Array>}\n */\nexport async function getAllAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n const customAgents = result[STORAGE_KEY] || [];\n return [...BUILTIN_AGENTS, ...customAgents];\n}\n\n/**\n * 仅获取用户自定义的 Agent\n * @returns {Promise<Array>}\n */\nexport async function getCustomAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n return result[STORAGE_KEY] || [];\n}\n\n/**\n * 根据 ID 获取单个 Agent\n * @param {string} agentId\n * @returns {Promise<Object|null>}\n */\nexport async function getAgent(agentId) {\n if (!agentId) return null;\n const all = await getAllAgents();\n return all.find(a => a.id === agentId) || null;\n}\n\n/**\n * 创建新的自定义 Agent\n * @param {Object} agentData - 不含 id 的 Agent 数据\n * @returns {Promise<Object>}\n */\nexport async function createAgent(agentData) {\n const agents = await getCustomAgents();\n const newAgent = {\n id: generateAgentId(),\n name: agentData.name || t('agentStore.unnamedAgent'),\n description: agentData.description || '',\n icon: agentData.icon || '🤖',\n systemPrompt: agentData.systemPrompt || '',\n toolIds: agentData.toolIds || null, // null = 继承全局\n skillIds: agentData.skillIds || null, // null = 继承全部启用技能\n mcpExcludedServerIds: agentData.mcpExcludedServerIds ?? null, // MCP 服务排除列表(服务级 deny-list)\n isBuiltin: false,\n allowSubDispatch: agentData.allowSubDispatch !== undefined ? agentData.allowSubDispatch : false,\n model: agentData.model || null,\n temperature: agentData.temperature !== undefined ? agentData.temperature : null,\n topP: agentData.topP !== undefined ? agentData.topP : null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n agents.push(newAgent);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return newAgent;\n}\n\n/**\n * 更新已有的自定义 Agent\n * @param {string} agentId\n * @param {Object} updates\n * @returns {Promise<Object|null>}\n */\nexport async function updateAgent(agentId, updates) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return null;\n \n agents[idx] = {\n ...agents[idx],\n ...updates,\n id: agentId, // id 不可变\n isBuiltin: false, // 不可变\n updatedAt: new Date().toISOString(),\n };\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return agents[idx];\n}\n\n/**\n * 删除自定义 Agent\n * @param {string} agentId\n * @returns {Promise<boolean>}\n */\nexport async function deleteAgent(agentId) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return false;\n \n agents.splice(idx, 1);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n\n // 如果删除的 Agent 是当前选中的,重置为默认\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n if (result[ACTIVE_KEY] === agentId) {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n \n return true;\n}\n\n/**\n * 获取当前选中的 Agent ID\n * @returns {Promise<string|null>}\n */\nexport async function getActiveAgentId() {\n await migrateLegacyActiveKey();\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n return result[ACTIVE_KEY] || null;\n}\n\n/**\n * 设置当前选中的 Agent ID\n * @param {string|null} agentId\n */\nexport async function setActiveAgentId(agentId) {\n if (agentId) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: agentId });\n } else {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n}\n\n/**\n * 从模板创建 Agent\n * @param {Object} template\n * @returns {Promise<Object>}\n */\nexport async function createAgentFromTemplate(template) {\n return createAgent({\n name: template.name,\n icon: template.icon,\n description: template.description,\n systemPrompt: template.systemPrompt,\n toolIds: template.toolIds ? [...template.toolIds] : null,\n allowSubDispatch: template.allowSubDispatch || false,\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agent-store-DcAs6YyA.js","names":[],"sources":["../../src/side_panel/agent-store.js"],"sourcesContent":["// side_panel/agent-store.js - Agent 数据持久化\nimport { BUILTIN_AGENTS, generateAgentId } from '../shared/agent-defaults.js';\nimport { t, registerTranslations } from '../shared/i18n.js';\n\nregisterTranslations('zh', {\n agentStore: {\n unnamedAgent: '未命名Agent',\n },\n});\nregisterTranslations('en', {\n agentStore: {\n unnamedAgent: 'Unnamed Agent',\n },\n});\n\nconst STORAGE_KEY = 'customAgents';\nconst ACTIVE_KEY = 'activeAssistantId';\nconst LEGACY_ACTIVE_KEY = 'activeAgentId';\n\nlet migratedActiveKey = false;\n\nasync function migrateLegacyActiveKey() {\n if (migratedActiveKey) return;\n migratedActiveKey = true;\n\n const result = await chrome.storage.local.get([LEGACY_ACTIVE_KEY, ACTIVE_KEY]);\n const legacyValue = result[LEGACY_ACTIVE_KEY];\n const newValue = result[ACTIVE_KEY];\n\n if (legacyValue && !newValue && !legacyValue.startsWith('pa_')) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: legacyValue });\n await chrome.storage.local.remove(LEGACY_ACTIVE_KEY);\n }\n}\n\n/**\n * 获取所有 Agent(内置 + 用户自定义)\n * @returns {Promise<Array>}\n */\nexport async function getAllAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n const customAgents = result[STORAGE_KEY] || [];\n return [...BUILTIN_AGENTS, ...customAgents];\n}\n\n/**\n * 仅获取用户自定义的 Agent\n * @returns {Promise<Array>}\n */\nexport async function getCustomAgents() {\n const result = await chrome.storage.local.get([STORAGE_KEY]);\n return result[STORAGE_KEY] || [];\n}\n\n/**\n * 根据 ID 获取单个 Agent\n * @param {string} agentId\n * @returns {Promise<Object|null>}\n */\nexport async function getAgent(agentId) {\n if (!agentId) return null;\n const all = await getAllAgents();\n return all.find(a => a.id === agentId) || null;\n}\n\n/**\n * 创建新的自定义 Agent\n * @param {Object} agentData - 不含 id 的 Agent 数据\n * @returns {Promise<Object>}\n */\nexport async function createAgent(agentData) {\n const agents = await getCustomAgents();\n const newAgent = {\n id: generateAgentId(),\n name: agentData.name || t('agentStore.unnamedAgent'),\n description: agentData.description || '',\n icon: agentData.icon || '🤖',\n systemPrompt: agentData.systemPrompt || '',\n toolIds: agentData.toolIds || null, // null = 继承全局\n skillIds: agentData.skillIds || null, // null = 继承全部启用技能\n mcpExcludedServerIds: agentData.mcpExcludedServerIds ?? null, // MCP 服务排除列表(服务级 deny-list)\n isBuiltin: false,\n allowSubDispatch: agentData.allowSubDispatch !== undefined ? agentData.allowSubDispatch : false,\n model: agentData.model || null,\n temperature: agentData.temperature !== undefined ? agentData.temperature : null,\n topP: agentData.topP !== undefined ? agentData.topP : null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n };\n agents.push(newAgent);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return newAgent;\n}\n\n/**\n * 更新已有的自定义 Agent\n * @param {string} agentId\n * @param {Object} updates\n * @returns {Promise<Object|null>}\n */\nexport async function updateAgent(agentId, updates) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return null;\n \n agents[idx] = {\n ...agents[idx],\n ...updates,\n id: agentId, // id 不可变\n isBuiltin: false, // 不可变\n updatedAt: new Date().toISOString(),\n };\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n return agents[idx];\n}\n\n/**\n * 删除自定义 Agent\n * @param {string} agentId\n * @returns {Promise<boolean>}\n */\nexport async function deleteAgent(agentId) {\n const agents = await getCustomAgents();\n const idx = agents.findIndex(a => a.id === agentId);\n if (idx === -1) return false;\n \n agents.splice(idx, 1);\n await chrome.storage.local.set({ [STORAGE_KEY]: agents });\n\n // 如果删除的 Agent 是当前选中的,重置为默认\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n if (result[ACTIVE_KEY] === agentId) {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n \n return true;\n}\n\n/**\n * 获取当前选中的 Agent ID\n * @returns {Promise<string|null>}\n */\nexport async function getActiveAgentId() {\n await migrateLegacyActiveKey();\n const result = await chrome.storage.local.get([ACTIVE_KEY]);\n return result[ACTIVE_KEY] || null;\n}\n\n/**\n * 设置当前选中的 Agent ID\n * @param {string|null} agentId\n */\nexport async function setActiveAgentId(agentId) {\n if (agentId) {\n await chrome.storage.local.set({ [ACTIVE_KEY]: agentId });\n } else {\n await chrome.storage.local.remove(ACTIVE_KEY);\n }\n}\n\n/**\n * 从模板创建 Agent\n * @param {Object} template\n * @returns {Promise<Object>}\n */\nexport async function createAgentFromTemplate(template) {\n return createAgent({\n name: template.name,\n icon: template.icon,\n description: template.description,\n systemPrompt: template.systemPrompt,\n toolIds: template.toolIds ? [...template.toolIds] : null,\n allowSubDispatch: template.allowSubDispatch || false,\n });\n}\n"],"mappings":"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"}