dsh-mobile-plugin 0.5.27 → 0.5.29
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/extensions/catalog.json +10 -1
- package/extensions/magpie/agentcompact.js +104 -0
- package/extensions/magpie/app.css +4902 -0
- package/extensions/magpie/app.js +24474 -0
- package/extensions/magpie/autobackup.js +185 -0
- package/extensions/magpie/autoresetown.js +151 -0
- package/extensions/magpie/balfound.js +38 -0
- package/extensions/magpie/boot.js +23 -0
- package/extensions/magpie/cfgblocked.js +39 -0
- package/extensions/magpie/checkinnow.js +111 -0
- package/extensions/magpie/claudemode.js +92 -0
- package/extensions/magpie/compat.js +41 -0
- package/extensions/magpie/context.css +433 -0
- package/extensions/magpie/context.js +1576 -0
- package/extensions/magpie/ctxtext.js +223 -0
- package/extensions/magpie/fonts.js +42 -0
- package/extensions/magpie/i18n.js +18942 -0
- package/extensions/magpie/icons/ai21.svg +1 -0
- package/extensions/magpie/icons/ai302-color.svg +1 -0
- package/extensions/magpie/icons/ai360-color.svg +1 -0
- package/extensions/magpie/icons/aihubmix-color.svg +1 -0
- package/extensions/magpie/icons/aionui.svg +1 -0
- package/extensions/magpie/icons/air.svg +4 -0
- package/extensions/magpie/icons/alibaba-color.svg +1 -0
- package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
- package/extensions/magpie/icons/alma.png +0 -0
- package/extensions/magpie/icons/anthropic.svg +1 -0
- package/extensions/magpie/icons/antigravity-color.svg +1 -0
- package/extensions/magpie/icons/anyscale-color.svg +1 -0
- package/extensions/magpie/icons/app-alma.png +0 -0
- package/extensions/magpie/icons/app-cc-switch.png +0 -0
- package/extensions/magpie/icons/apple-touch-icon.png +0 -0
- package/extensions/magpie/icons/aside.svg +28 -0
- package/extensions/magpie/icons/astrbot.svg +1 -0
- package/extensions/magpie/icons/atomcode.png +0 -0
- package/extensions/magpie/icons/aws.svg +1 -0
- package/extensions/magpie/icons/azure-color.svg +1 -0
- package/extensions/magpie/icons/azureai-color.svg +1 -0
- package/extensions/magpie/icons/baichuan-color.svg +1 -0
- package/extensions/magpie/icons/baidu-color.svg +1 -0
- package/extensions/magpie/icons/baiducloud-color.svg +1 -0
- package/extensions/magpie/icons/bailian-color.svg +1 -0
- package/extensions/magpie/icons/baseten.svg +1 -0
- package/extensions/magpie/icons/bedrock-color.svg +1 -0
- package/extensions/magpie/icons/burncloud-color.svg +1 -0
- package/extensions/magpie/icons/bytedance-color.svg +1 -0
- package/extensions/magpie/icons/cerebras.svg +1 -0
- package/extensions/magpie/icons/chatglm-color.svg +1 -0
- package/extensions/magpie/icons/cherryin-color.svg +1 -0
- package/extensions/magpie/icons/chutes.svg +1 -0
- package/extensions/magpie/icons/cindy.png +0 -0
- package/extensions/magpie/icons/claude-color.svg +1 -0
- package/extensions/magpie/icons/claudecode-color.svg +1 -0
- package/extensions/magpie/icons/cline.svg +1 -0
- package/extensions/magpie/icons/cloudflare-color.svg +1 -0
- package/extensions/magpie/icons/codebuddy-color.svg +1 -0
- package/extensions/magpie/icons/codex-color.svg +1 -0
- package/extensions/magpie/icons/cohere-color.svg +1 -0
- package/extensions/magpie/icons/cometapi-color.svg +1 -0
- package/extensions/magpie/icons/commandcode.svg +1 -0
- package/extensions/magpie/icons/copilot-color.svg +1 -0
- package/extensions/magpie/icons/coze.svg +1 -0
- package/extensions/magpie/icons/crush.png +0 -0
- package/extensions/magpie/icons/curl.svg +1 -0
- package/extensions/magpie/icons/cursor.svg +1 -0
- package/extensions/magpie/icons/deepinfra-color.svg +1 -0
- package/extensions/magpie/icons/deepmind-color.svg +1 -0
- package/extensions/magpie/icons/deepseek-color.svg +1 -0
- package/extensions/magpie/icons/deveco-color.svg +12 -0
- package/extensions/magpie/icons/devin.svg +1 -0
- package/extensions/magpie/icons/doubao-color.svg +1 -0
- package/extensions/magpie/icons/empryo-color.svg +1 -0
- package/extensions/magpie/icons/factory.svg +1 -0
- package/extensions/magpie/icons/favicon.svg +1 -0
- package/extensions/magpie/icons/featherless-color.svg +1 -0
- package/extensions/magpie/icons/fireworks-color.svg +1 -0
- package/extensions/magpie/icons/friendli.svg +1 -0
- package/extensions/magpie/icons/fx.svg +1 -0
- package/extensions/magpie/icons/gemini-color.svg +1 -0
- package/extensions/magpie/icons/geminicli-color.svg +1 -0
- package/extensions/magpie/icons/gemma-color.svg +1 -0
- package/extensions/magpie/icons/giteeai.svg +1 -0
- package/extensions/magpie/icons/github.svg +1 -0
- package/extensions/magpie/icons/githubcopilot.svg +1 -0
- package/extensions/magpie/icons/glama.svg +1 -0
- package/extensions/magpie/icons/gmicloud.svg +1 -0
- package/extensions/magpie/icons/google-color.svg +1 -0
- package/extensions/magpie/icons/googlecloud-color.svg +1 -0
- package/extensions/magpie/icons/goose.svg +1 -0
- package/extensions/magpie/icons/grok.svg +1 -0
- package/extensions/magpie/icons/groq.svg +1 -0
- package/extensions/magpie/icons/hanako.png +0 -0
- package/extensions/magpie/icons/hermes.svg +1 -0
- package/extensions/magpie/icons/huawei-color.svg +1 -0
- package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
- package/extensions/magpie/icons/huggingface-color.svg +1 -0
- package/extensions/magpie/icons/hunyuan-color.svg +1 -0
- package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
- package/extensions/magpie/icons/icon-512.png +0 -0
- package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
- package/extensions/magpie/icons/inference.svg +1 -0
- package/extensions/magpie/icons/infinigence-color.svg +1 -0
- package/extensions/magpie/icons/internlm-color.svg +1 -0
- package/extensions/magpie/icons/jcode.svg +1 -0
- package/extensions/magpie/icons/jina.svg +1 -0
- package/extensions/magpie/icons/kilo.svg +1 -0
- package/extensions/magpie/icons/kilocode.svg +1 -0
- package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
- package/extensions/magpie/icons/kimi.svg +1 -0
- package/extensions/magpie/icons/kiro-color.svg +1 -0
- package/extensions/magpie/icons/kling-color.svg +1 -0
- package/extensions/magpie/icons/lambda.svg +1 -0
- package/extensions/magpie/icons/leptonai-color.svg +1 -0
- package/extensions/magpie/icons/liquid.svg +1 -0
- package/extensions/magpie/icons/liveagent-color.svg +1 -0
- package/extensions/magpie/icons/llmapi.svg +1 -0
- package/extensions/magpie/icons/lmstudio.svg +1 -0
- package/extensions/magpie/icons/lody.png +0 -0
- package/extensions/magpie/icons/longcat.svg +1 -0
- package/extensions/magpie/icons/magpie.svg +1 -0
- package/extensions/magpie/icons/meta-color.svg +1 -0
- package/extensions/magpie/icons/metaai-color.svg +1 -0
- package/extensions/magpie/icons/microsoft-color.svg +1 -0
- package/extensions/magpie/icons/mimocode.svg +1 -0
- package/extensions/magpie/icons/minimax-color.svg +1 -0
- package/extensions/magpie/icons/mistermorph-color.svg +1 -0
- package/extensions/magpie/icons/mistral-color.svg +1 -0
- package/extensions/magpie/icons/mlx-serve.png +0 -0
- package/extensions/magpie/icons/modelscope-color.svg +1 -0
- package/extensions/magpie/icons/monica-color.svg +1 -0
- package/extensions/magpie/icons/moonshot.svg +1 -0
- package/extensions/magpie/icons/morph-color.svg +1 -0
- package/extensions/magpie/icons/mothx.png +0 -0
- package/extensions/magpie/icons/nebius.svg +1 -0
- package/extensions/magpie/icons/newapi-color.svg +1 -0
- package/extensions/magpie/icons/nousresearch.svg +1 -0
- package/extensions/magpie/icons/nova-color.svg +1 -0
- package/extensions/magpie/icons/novita-color.svg +1 -0
- package/extensions/magpie/icons/nvidia-color.svg +1 -0
- package/extensions/magpie/icons/ollama.svg +1 -0
- package/extensions/magpie/icons/omlx.svg +1 -0
- package/extensions/magpie/icons/omo.svg +1 -0
- package/extensions/magpie/icons/omp.svg +1 -0
- package/extensions/magpie/icons/openai.svg +1 -0
- package/extensions/magpie/icons/openchamber.svg +11 -0
- package/extensions/magpie/icons/openclaw.svg +1 -0
- package/extensions/magpie/icons/opencode.svg +1 -0
- package/extensions/magpie/icons/openmausbot.svg +1 -0
- package/extensions/magpie/icons/openrouter.svg +1 -0
- package/extensions/magpie/icons/openwebui.svg +1 -0
- package/extensions/magpie/icons/parasail.svg +1 -0
- package/extensions/magpie/icons/penguin-color.svg +1 -0
- package/extensions/magpie/icons/perplexity-color.svg +1 -0
- package/extensions/magpie/icons/phala-color.svg +1 -0
- package/extensions/magpie/icons/pi.svg +1 -0
- package/extensions/magpie/icons/pipellm-color.svg +5 -0
- package/extensions/magpie/icons/poe.svg +1 -0
- package/extensions/magpie/icons/ppio-color.svg +1 -0
- package/extensions/magpie/icons/prime-agent.svg +1 -0
- package/extensions/magpie/icons/qiniu-color.svg +1 -0
- package/extensions/magpie/icons/qoder.svg +1 -0
- package/extensions/magpie/icons/qwen-color.svg +1 -0
- package/extensions/magpie/icons/qwenpaw.svg +1 -0
- package/extensions/magpie/icons/raycast.svg +1 -0
- package/extensions/magpie/icons/reasonix-color.svg +48 -0
- package/extensions/magpie/icons/reka.svg +1 -0
- package/extensions/magpie/icons/replicate.svg +1 -0
- package/extensions/magpie/icons/roocode.svg +1 -0
- package/extensions/magpie/icons/rtk.png +0 -0
- package/extensions/magpie/icons/sambanova-color.svg +1 -0
- package/extensions/magpie/icons/sensenova-color.svg +1 -0
- package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
- package/extensions/magpie/icons/snow.png +0 -0
- package/extensions/magpie/icons/snowflake-color.svg +1 -0
- package/extensions/magpie/icons/sophnet-color.svg +1 -0
- package/extensions/magpie/icons/spark-color.svg +1 -0
- package/extensions/magpie/icons/stepfun-color.svg +1 -0
- package/extensions/magpie/icons/straico-color.svg +1 -0
- package/extensions/magpie/icons/submodel-color.svg +1 -0
- package/extensions/magpie/icons/t3code.svg +4 -0
- package/extensions/magpie/icons/targon-color.svg +1 -0
- package/extensions/magpie/icons/tencent-color.svg +1 -0
- package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
- package/extensions/magpie/icons/tiangong-color.svg +1 -0
- package/extensions/magpie/icons/together-color.svg +1 -0
- package/extensions/magpie/icons/trae.svg +1 -0
- package/extensions/magpie/icons/typesafe.png +0 -0
- package/extensions/magpie/icons/upstage-color.svg +1 -0
- package/extensions/magpie/icons/venice-color.svg +1 -0
- package/extensions/magpie/icons/vercel.svg +1 -0
- package/extensions/magpie/icons/vertexai-color.svg +1 -0
- package/extensions/magpie/icons/vllm-color.svg +1 -0
- package/extensions/magpie/icons/volcengine-color.svg +1 -0
- package/extensions/magpie/icons/voyage-color.svg +1 -0
- package/extensions/magpie/icons/vscode.svg +1 -0
- package/extensions/magpie/icons/wenxin-color.svg +1 -0
- package/extensions/magpie/icons/windsurf.svg +1 -0
- package/extensions/magpie/icons/workbuddy-color.svg +1 -0
- package/extensions/magpie/icons/workersai-color.svg +1 -0
- package/extensions/magpie/icons/xai.svg +1 -0
- package/extensions/magpie/icons/xiaomimimo.svg +1 -0
- package/extensions/magpie/icons/xinference-color.svg +1 -0
- package/extensions/magpie/icons/yi.svg +1 -0
- package/extensions/magpie/icons/yuanbao-color.svg +1 -0
- package/extensions/magpie/icons/yylx.svg +5 -0
- package/extensions/magpie/icons/zai.svg +1 -0
- package/extensions/magpie/icons/zcode.png +0 -0
- package/extensions/magpie/icons/zed.svg +1 -0
- package/extensions/magpie/icons/zenmux.svg +1 -0
- package/extensions/magpie/icons/zeroone.svg +1 -0
- package/extensions/magpie/icons/zhipu-color.svg +1 -0
- package/extensions/magpie/icons/zoo-code.png +0 -0
- package/extensions/magpie/index.html +572 -0
- package/extensions/magpie/kernel.js +274 -0
- package/extensions/magpie/keyspend.js +63 -0
- package/extensions/magpie/layout.js +26 -0
- package/extensions/magpie/manifest.json +37 -0
- package/extensions/magpie/navlamp.js +47 -0
- package/extensions/magpie/omarchy.css +72 -0
- package/extensions/magpie/omarchy.js +46 -0
- package/extensions/magpie/passerrors.js +21 -0
- package/extensions/magpie/pluginpages.js +405 -0
- package/extensions/magpie/plugins/gateway/client.js +1194 -0
- package/extensions/magpie/plugins/gateway/keys.js +972 -0
- package/extensions/magpie/plugins/gateway/settings.js +459 -0
- package/extensions/magpie/plugins/library/client.js +6788 -0
- package/extensions/magpie/plugins/library/i18n.js +3228 -0
- package/extensions/magpie/plugins/library/library.css +848 -0
- package/extensions/magpie/plugins/otel/i18n.js +48 -0
- package/extensions/magpie/plugins/otel/page.js +189 -0
- package/extensions/magpie/plugins/redact/client.js +46 -0
- package/extensions/magpie/plugins/redact/demo.js +461 -0
- package/extensions/magpie/plugins/redact/i18n.js +432 -0
- package/extensions/magpie/plugins/redact/page.js +985 -0
- package/extensions/magpie/plugins/redact/redact.css +385 -0
- package/extensions/magpie/plugins/redact/settings.js +248 -0
- package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
- package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
- package/extensions/magpie/plugins/routing/client.js +8593 -0
- package/extensions/magpie/plugins/routing/i18n.js +2696 -0
- package/extensions/magpie/plugins/routing/routing.css +1111 -0
- package/extensions/magpie/plugins/search/settings.js +460 -0
- package/extensions/magpie/plugins/sessions/client.js +1622 -0
- package/extensions/magpie/plugins/sessions/i18n.js +360 -0
- package/extensions/magpie/plugins/sessions/sessions.css +181 -0
- package/extensions/magpie/plugins/sessions/settings.js +100 -0
- package/extensions/magpie/plugins/usage/client.js +150 -0
- package/extensions/magpie/plugins/usage/i18n.js +984 -0
- package/extensions/magpie/plugins/usage/page.js +4307 -0
- package/extensions/magpie/plugins.css +319 -0
- package/extensions/magpie/plugins.js +1926 -0
- package/extensions/magpie/profilerename.js +27 -0
- package/extensions/magpie/purposes.js +2 -0
- package/extensions/magpie/quitask.js +79 -0
- package/extensions/magpie/quotaall.js +142 -0
- package/extensions/magpie/ratewindows.js +123 -0
- package/extensions/magpie/registry.js +87 -0
- package/extensions/magpie/resetask.js +137 -0
- package/extensions/magpie/ruleedit.js +53 -0
- package/extensions/magpie/scrollends.js +91 -0
- package/extensions/magpie/settingshints.js +75 -0
- package/extensions/magpie/signsheets.js +86 -0
- package/extensions/magpie/start.js +26 -0
- package/extensions/magpie/syncfiles.js +216 -0
- package/extensions/magpie/theme.js +29 -0
- package/extensions/magpie/traywindows.js +41 -0
- package/extensions/magpie/webfonts.js +130 -0
- package/extensions/magpie/whatsnew.js +252 -0
- package/extensions/magpie.zip +0 -0
- package/knowledge/apps/baijiahao.md +30 -0
- package/knowledge/apps/bilibili.md +1 -0
- package/knowledge/apps/dedao.md +33 -0
- package/knowledge/apps/feishu.md +35 -0
- package/knowledge/apps/gmail.md +33 -0
- package/knowledge/apps/jike.md +31 -0
- package/knowledge/apps/kuaishou.md +34 -0
- package/knowledge/apps/linkedin.md +33 -0
- package/knowledge/apps/qqmail.md +30 -0
- package/knowledge/apps/telegram.md +27 -0
- package/knowledge/apps/toutiao.md +32 -0
- package/knowledge/apps/uber-eats.md +26 -0
- package/knowledge/apps/uber.md +27 -0
- package/knowledge/apps/whatsapp.md +27 -0
- package/knowledge/apps/x.md +32 -0
- package/knowledge/apps/xiaohongshu.md +1 -0
- package/knowledge/apps/zhihu.md +1 -0
- package/knowledge/sites/bilibili.com/SITE.md +50 -0
- package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
- package/knowledge/sites/github.com/SITE.md +51 -0
- package/knowledge/sites/github.com/references/repo-fields.md +13 -0
- package/knowledge/sites/x.com/SITE.md +47 -0
- package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
- package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
- package/knowledge/sites/youtube.com/SITE.md +49 -0
- package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
- package/knowledge/sites/zhihu.com/SITE.md +47 -0
- package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
- package/lib/mobile/extension-catalog.js +41 -1
- package/lib/mobile/extension-data.js +224 -0
- package/lib/mobile/extension-dev.js +3 -1
- package/lib/mobile/file-ops.js +252 -0
- package/lib/mobile/history-derive.js +80 -4
- package/lib/mobile/host-bridge/endpoints.js +48 -6
- package/lib/mobile/host-bridge/event-synthesis.js +33 -6
- package/lib/mobile/jobs-control.js +118 -0
- package/lib/mobile/memory.js +635 -0
- package/lib/mobile/model-badges.js +77 -0
- package/lib/mobile/pet-tools.js +50 -4
- package/lib/mobile/pets.js +214 -18
- package/lib/mobile/phone/access.js +85 -0
- package/lib/mobile/phone/evidence.js +228 -0
- package/lib/mobile/phone/gate.js +290 -0
- package/lib/mobile/phone/grounding.js +314 -0
- package/lib/mobile/phone/guard.js +465 -0
- package/lib/mobile/phone/irreversible.js +88 -8
- package/lib/mobile/phone/knowledge-tools.js +665 -0
- package/lib/mobile/phone/learn.js +329 -0
- package/lib/mobile/phone/redact.js +237 -0
- package/lib/mobile/phone/risk.js +95 -0
- package/lib/mobile/phone/routes.js +28 -1
- package/lib/mobile/phone/service.js +23 -0
- package/lib/mobile/phone/site-commands.js +271 -0
- package/lib/mobile/phone/step-summary.js +201 -0
- package/lib/mobile/phone/stop-summary.js +235 -0
- package/lib/mobile/phone/tool-groups.js +125 -0
- package/lib/mobile/phone/tools.js +589 -0
- package/lib/mobile/phone/untrusted.js +65 -0
- package/lib/mobile/phone-knowledge.js +20 -4
- package/lib/mobile/schedule-edit.js +500 -0
- package/lib/mobile/schedule.js +5 -0
- package/lib/mobile/send-continuation.js +52 -0
- package/lib/mobile/session-models.js +27 -31
- package/lib/mobile/session-send.js +12 -12
- package/lib/mobile/skill-import.js +338 -0
- package/lib/mobile/skill-store.js +524 -0
- package/lib/routes/mobile-api.js +42 -0
- package/package.json +7 -6
- package/skills/skill-authoring/SKILL.md +46 -0
- package/skills/skill-authoring/references/examples.md +82 -0
- package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
- package/skills/skill-authoring/scripts/check_skill.py +167 -0
- package/skills/skill-authoring/scripts/manifest.json +3 -0
- package/knowledge/sites/bilibili.com.md +0 -27
- package/knowledge/sites/github.com.md +0 -27
- package/knowledge/sites/x.com.md +0 -28
- package/knowledge/sites/xiaohongshu.com.md +0 -27
- package/knowledge/sites/youtube.com.md +0 -28
- package/knowledge/sites/zhihu.com.md +0 -27
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 长期记忆层(数据层 + 纯逻辑):保存用户的事实 / 偏好 / 做事方式,跨会话按工作区或全局分开。
|
|
5
|
+
*
|
|
6
|
+
* ## 这一层做什么、不做什么
|
|
7
|
+
*
|
|
8
|
+
* 记忆的**真值**在宿主会话记录那一侧:抽取器只读「用户这次说的话 + 相关旧记忆」,产出
|
|
9
|
+
* add/update/forget,再由代码校验引文确实出自用户原话;每轮再把记忆拼进 prompt。那一段
|
|
10
|
+
* (抽取任务、引文校验、注入、预算裁剪的落地)要宿主内核配合,本模块不硬做。
|
|
11
|
+
*
|
|
12
|
+
* 本模块把「存储 + 检索 + 管理」抽成一本按 scope 分开的 JSON 账本,负责:
|
|
13
|
+
* · 写入前过敏感信息关卡(密码 / 验证码 / 卡号 / 身份证 / api key 一律不落盘);
|
|
14
|
+
* · 按类型去重(同 kind + 同内容不重复记)、来源与时间(quote / conversationId / atMs / channel);
|
|
15
|
+
* · 置顶、删除、撤销(最近 {@link UNDO_KEEP} 次变更)、做法需用户批准(procedure 默认 pending);
|
|
16
|
+
* · BM25 检索与把一撮记忆渲染成上下文块(纯文本,供将来注入调用)。
|
|
17
|
+
*
|
|
18
|
+
* 记忆是用户私有数据,只落本机(`$DSH_HOME/storages/mobile-plugin/memory/`),不经中转;
|
|
19
|
+
* 这里不碰 shell、不写任意路径,scope 名也被收窄成安全文件名。
|
|
20
|
+
*
|
|
21
|
+
* ## 落盘形状
|
|
22
|
+
*
|
|
23
|
+
* memory/global.json —— 全局记忆:{ version, scope, items: [...] }
|
|
24
|
+
* memory/workspaces/<slug>-<h>.json —— 某个工作区的记忆
|
|
25
|
+
*
|
|
26
|
+
* 读不出(JSON 坏了、权限不对)时**拒绝写入**,宁可这轮不记,也不能用空表把真正的记忆盖掉;
|
|
27
|
+
* 文件不存在是正常空账本。
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const crypto = require('node:crypto');
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const { dshHome } = require('../shared/paths.js');
|
|
35
|
+
|
|
36
|
+
/** 三类记忆。procedure(做法)默认待批准。 */
|
|
37
|
+
const KINDS = Object.freeze(['fact', 'preference', 'procedure']);
|
|
38
|
+
/** 状态:active 在用;pending 等用户点头(只有 procedure 会 pending)。 */
|
|
39
|
+
const STATUSES = Object.freeze(['active', 'pending']);
|
|
40
|
+
/** 全局 scope 的固定名。 */
|
|
41
|
+
const GLOBAL_SCOPE = 'global';
|
|
42
|
+
|
|
43
|
+
/** 单条记忆正文上限(字符):记忆块按行编号,太长会占满上下文。 */
|
|
44
|
+
const MAX_CONTENT = 200;
|
|
45
|
+
/** 单条记忆的关键词上限。 */
|
|
46
|
+
const MAX_KEYWORDS = 8;
|
|
47
|
+
/** 一本账本的记忆条数上限。 */
|
|
48
|
+
const LIMIT = 500;
|
|
49
|
+
/** 撤销令牌在内存里保留的最近变更数。 */
|
|
50
|
+
const UNDO_KEEP = 50;
|
|
51
|
+
|
|
52
|
+
/** 逻辑拒绝:code 供 handler 翻成 400 的 error.code。 */
|
|
53
|
+
class MemoryError extends Error {
|
|
54
|
+
constructor(code) {
|
|
55
|
+
super(code);
|
|
56
|
+
this.name = 'MemoryError';
|
|
57
|
+
this.code = code;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 记忆库根。`DSH_MOBILE_MEMORY` 只给测试用。 */
|
|
62
|
+
function memoryRoot() {
|
|
63
|
+
return process.env.DSH_MOBILE_MEMORY || path.join(dshHome(), 'storages', 'mobile-plugin', 'memory');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 由工作区路径算出一个稳定的 scope 名。空路径回落全局。
|
|
68
|
+
* 只做可读的归一(保留名字好排查),真正的唯一性由落盘文件名的短哈希保证。
|
|
69
|
+
*/
|
|
70
|
+
function scopeForWorkspace(workspacePath) {
|
|
71
|
+
const raw = typeof workspacePath === 'string' ? workspacePath.trim() : '';
|
|
72
|
+
if (!raw) return GLOBAL_SCOPE;
|
|
73
|
+
const slug = raw
|
|
74
|
+
.replace(/[\\/]+/g, '-')
|
|
75
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
76
|
+
.replace(/^-+|-+$/g, '')
|
|
77
|
+
.slice(0, 80);
|
|
78
|
+
return `workspace:${slug || 'workspace'}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── 敏感信息关卡 ─────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const SPACES = /\s+/g;
|
|
84
|
+
|
|
85
|
+
/** 先把全角数字字母、兼容字符统一成 NFKC,再比对。 */
|
|
86
|
+
function normalizeForSensitive(text) {
|
|
87
|
+
return String(text == null ? '' : text).normalize('NFKC');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 支付/凭据类关键词(中英)。pin/otp 太短,单独只在后面跟数字时算。
|
|
91
|
+
const SECRET_KEYWORDS = /支付密码|密码|口令|验证码|校验码|动态码|安全码|安全问题|(?<![A-Za-z])(?:password|passcode|passwd|cvv|cvc|verification code|security code|security question|one-time code)(?![A-Za-z])/i;
|
|
92
|
+
const DIGIT_KEYWORDS = /(?<![A-Za-z])(?:pin|otp)(?![A-Za-z])/i;
|
|
93
|
+
// 关键词与 4 位以上数字/字母串之间只允许很短的连接,免得一句普通话里的数字被误判。
|
|
94
|
+
const KEYWORD_GAP = '[\\s::=,,是为叫]{0,4}(?:(?:is|was|are)\\s+)?';
|
|
95
|
+
const OWNER = '\\s*(?:就是|是|为)(?:我的|我|用户的)?\\s*';
|
|
96
|
+
|
|
97
|
+
const KEYWORD_RULES = [
|
|
98
|
+
new RegExp(`(?:${SECRET_KEYWORDS.source})${KEYWORD_GAP}[A-Za-z0-9]{4,}`, 'i'),
|
|
99
|
+
new RegExp(`(?<![A-Za-z0-9])[A-Za-z0-9]{4,}${OWNER}(?:${SECRET_KEYWORDS.source})`, 'i'),
|
|
100
|
+
new RegExp(`(?:${DIGIT_KEYWORDS.source})${KEYWORD_GAP}[0-9]{4,}`, 'i'),
|
|
101
|
+
new RegExp(`(?<![A-Za-z0-9])[0-9]{4,}${OWNER}(?:${DIGIT_KEYWORDS.source})`, 'i'),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// 18 位身份证(出生日期是真实日期、末位符合 ISO 7064 MOD 11-2)。
|
|
105
|
+
const ID_NUMBER = /(?<![0-9])[1-9][0-9]{16}[0-9Xx](?![0-9A-Za-z])/;
|
|
106
|
+
// 卡号:13–19 位数字,允许单个空格或连字符分组。
|
|
107
|
+
const CARD_LIKE = /(?<![0-9])[0-9](?:[ -]?[0-9]){12,18}(?![0-9])/;
|
|
108
|
+
const ID_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
|
109
|
+
const ID_CHECK = '10X98765432';
|
|
110
|
+
|
|
111
|
+
// 常见凭据形状。
|
|
112
|
+
const API_KEY_PATTERNS = [
|
|
113
|
+
/(?<![A-Za-z0-9])sk-[A-Za-z0-9]{16,}(?![A-Za-z0-9])/,
|
|
114
|
+
/(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{30,}(?![A-Za-z0-9])/,
|
|
115
|
+
/(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{30,}(?![A-Za-z0-9])/,
|
|
116
|
+
/(?<![A-Za-z])Bearer\s+[A-Za-z0-9._-]{20,}/i,
|
|
117
|
+
/(?<![A-Za-z0-9])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9])/,
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
function luhn(digits) {
|
|
121
|
+
if (digits.length < 13 || digits.length > 19) return false;
|
|
122
|
+
let sum = 0;
|
|
123
|
+
const reversed = digits.split('').reverse();
|
|
124
|
+
for (let i = 0; i < reversed.length; i += 1) {
|
|
125
|
+
let d = reversed[i].charCodeAt(0) - 48;
|
|
126
|
+
if (i % 2 === 1) {
|
|
127
|
+
d *= 2;
|
|
128
|
+
if (d > 9) d -= 9;
|
|
129
|
+
}
|
|
130
|
+
sum += d;
|
|
131
|
+
}
|
|
132
|
+
return sum % 10 === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validIdNumber(id) {
|
|
136
|
+
const year = Number(id.slice(6, 10));
|
|
137
|
+
const month = Number(id.slice(10, 12));
|
|
138
|
+
const day = Number(id.slice(12, 14));
|
|
139
|
+
if (!(year >= 1900 && month >= 1 && month <= 12 && day >= 1 && day <= 31)) return false;
|
|
140
|
+
let sum = 0;
|
|
141
|
+
for (let i = 0; i < 17; i += 1) sum += (id.charCodeAt(i) - 48) * ID_WEIGHTS[i];
|
|
142
|
+
return ID_CHECK[sum % 11] === id[17].toUpperCase();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 这段文字里有没有不该长期落盘的秘密。命中返回类别(`api_key` / `id_number` /
|
|
147
|
+
* `card_number` / `password`),否则 null。手机号、地址、生日、家人称呼放行(办事常用的
|
|
148
|
+
* 事实),别把它们当秘密。
|
|
149
|
+
*/
|
|
150
|
+
function sensitiveHit(text) {
|
|
151
|
+
const t = normalizeForSensitive(text);
|
|
152
|
+
if (!t) return null;
|
|
153
|
+
const compact = t.replace(SPACES, '');
|
|
154
|
+
if (API_KEY_PATTERNS.some((re) => re.test(t))) return 'api_key';
|
|
155
|
+
const idCandidates = t.match(new RegExp(ID_NUMBER.source, 'g')) || [];
|
|
156
|
+
if (idCandidates.some(validIdNumber)) return 'id_number';
|
|
157
|
+
const cardCandidates = t.match(new RegExp(CARD_LIKE.source, 'g')) || [];
|
|
158
|
+
if (cardCandidates.some((c) => luhn(c.replace(/[^0-9]/g, '')))) return 'card_number';
|
|
159
|
+
if (KEYWORD_RULES.some((re) => re.test(t) || re.test(compact))) return 'password';
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── 检索(词法切词 + BM25)──────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
const WORD = /[A-Za-z0-9]+/g;
|
|
166
|
+
|
|
167
|
+
/** 是否汉字。 */
|
|
168
|
+
function isHan(ch) {
|
|
169
|
+
const code = ch.codePointAt(0);
|
|
170
|
+
return (
|
|
171
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
172
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
173
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
174
|
+
(code >= 0x20000 && code <= 0x2ebef)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 本机词法切词:汉字取相邻两字(单字成词时保留单字),英文数字按单词小写。
|
|
180
|
+
* withUnigrams 另给每个汉字单字(查询用,让「辣」这类单字检索词能对上)。
|
|
181
|
+
*/
|
|
182
|
+
function tokenize(text, withUnigrams = false) {
|
|
183
|
+
const source = String(text == null ? '' : text);
|
|
184
|
+
const out = [];
|
|
185
|
+
let han = '';
|
|
186
|
+
const flushHan = () => {
|
|
187
|
+
if (!han) return;
|
|
188
|
+
if (han.length === 1) out.push(han);
|
|
189
|
+
else {
|
|
190
|
+
for (let i = 0; i < han.length - 1; i += 1) out.push(han.slice(i, i + 2));
|
|
191
|
+
if (withUnigrams) for (const ch of han) out.push(ch);
|
|
192
|
+
}
|
|
193
|
+
han = '';
|
|
194
|
+
};
|
|
195
|
+
let i = 0;
|
|
196
|
+
while (i < source.length) {
|
|
197
|
+
const ch = source[i];
|
|
198
|
+
if (isHan(ch)) {
|
|
199
|
+
han += ch;
|
|
200
|
+
i += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
flushHan();
|
|
204
|
+
WORD.lastIndex = i;
|
|
205
|
+
const m = WORD.exec(source);
|
|
206
|
+
if (m && m.index === i) {
|
|
207
|
+
out.push(m[0].toLowerCase());
|
|
208
|
+
i += m[0].length;
|
|
209
|
+
} else {
|
|
210
|
+
i += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
flushHan();
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const STOP_TERMS = new Set([
|
|
218
|
+
'帮我', '给我', '一下', '我的', '不要', '这个', '那个', '可以', '一个', '用户',
|
|
219
|
+
'今天', '这次', '现在', '什么', '怎么', '我们', '一些', '看看',
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const K1 = 1.2;
|
|
223
|
+
const B = 0.75;
|
|
224
|
+
const KEYWORD_TF = 2;
|
|
225
|
+
const MIN_CORPUS = 50;
|
|
226
|
+
|
|
227
|
+
function isUnigram(term) {
|
|
228
|
+
return term.length === 1 && isHan(term[0]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 对一个条目集合建倒排、按 BM25 打分,只回相关的。供 {@link createMemoryStore} 的 search 用。
|
|
233
|
+
* 相关判据与 Mika 一致:检索词对上了,或至少两个不同查询词命中,或唯一命中的词只此一条有
|
|
234
|
+
* 且是它的检索词;单字只参与打分、不算进这三条。
|
|
235
|
+
*/
|
|
236
|
+
function rankItems(items, query, limit) {
|
|
237
|
+
const eligible = items;
|
|
238
|
+
if (eligible.length === 0 || limit <= 0) return [];
|
|
239
|
+
const docs = eligible.map((item) => {
|
|
240
|
+
const counts = new Map();
|
|
241
|
+
for (const term of tokenize(item.content)) counts.set(term, (counts.get(term) || 0) + 1);
|
|
242
|
+
for (const kw of item.keywords || []) {
|
|
243
|
+
for (const term of tokenize(kw)) counts.set(term, (counts.get(term) || 0) + KEYWORD_TF);
|
|
244
|
+
}
|
|
245
|
+
return counts;
|
|
246
|
+
});
|
|
247
|
+
const lengths = docs.map((d) => {
|
|
248
|
+
let total = 0;
|
|
249
|
+
for (const v of d.values()) total += v;
|
|
250
|
+
return total;
|
|
251
|
+
});
|
|
252
|
+
const keywordTerms = eligible.map((item) => {
|
|
253
|
+
const set = new Set();
|
|
254
|
+
for (const kw of item.keywords || []) for (const term of tokenize(String(kw).toLowerCase())) set.add(term);
|
|
255
|
+
return set;
|
|
256
|
+
});
|
|
257
|
+
const corpus = Math.max(eligible.length, MIN_CORPUS);
|
|
258
|
+
const avgLen = lengths.reduce((a, b) => a + b, 0) / eligible.length || 1;
|
|
259
|
+
const q = String(query || '').toLowerCase();
|
|
260
|
+
const terms = [...new Set(tokenize(q, true))].filter((t) => !STOP_TERMS.has(t));
|
|
261
|
+
if (terms.length === 0) return [];
|
|
262
|
+
const termSet = new Set(terms);
|
|
263
|
+
const df = new Map(terms.map((t) => [t, docs.filter((d) => d.has(t)).length]));
|
|
264
|
+
const idf = new Map(terms.map((t) => {
|
|
265
|
+
const n = df.get(t);
|
|
266
|
+
return [t, Math.log(1 + (corpus - n + 0.5) / (n + 0.5))];
|
|
267
|
+
}));
|
|
268
|
+
const scored = [];
|
|
269
|
+
eligible.forEach((item, idx) => {
|
|
270
|
+
const doc = docs[idx];
|
|
271
|
+
const matched = terms.filter((t) => doc.has(t));
|
|
272
|
+
if (matched.length === 0) return;
|
|
273
|
+
const multi = matched.filter((t) => !isUnigram(t));
|
|
274
|
+
const keywordHit = (item.keywords || []).some((kw) => {
|
|
275
|
+
const k = String(kw).toLowerCase();
|
|
276
|
+
const kt = tokenize(k);
|
|
277
|
+
return q.includes(k) || (kt.length > 0 && kt.every((t) => termSet.has(t)));
|
|
278
|
+
});
|
|
279
|
+
const relevant = keywordHit || multi.length >= 2 ||
|
|
280
|
+
(multi.length === 1 && df.get(multi[0]) === 1 && keywordTerms[idx].has(multi[0]));
|
|
281
|
+
if (!relevant) return;
|
|
282
|
+
const norm = K1 * (1 - B + (B * lengths[idx]) / Math.max(avgLen, 1));
|
|
283
|
+
let score = 0;
|
|
284
|
+
for (const t of matched) {
|
|
285
|
+
const tf = doc.get(t);
|
|
286
|
+
score += (idf.get(t) * tf * (K1 + 1)) / (tf + norm);
|
|
287
|
+
}
|
|
288
|
+
scored.push({ item, score });
|
|
289
|
+
});
|
|
290
|
+
scored.sort((a, b) => b.score - a.score);
|
|
291
|
+
return scored.slice(0, limit);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── 账本 ────────────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
function cleanContent(value) {
|
|
297
|
+
return String(value == null ? '' : value).replace(SPACES, ' ').trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function cleanKeywords(values) {
|
|
301
|
+
if (!Array.isArray(values)) return [];
|
|
302
|
+
const out = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
for (const raw of values) {
|
|
305
|
+
const kw = cleanContent(raw);
|
|
306
|
+
if (!kw || seen.has(kw)) continue;
|
|
307
|
+
seen.add(kw);
|
|
308
|
+
out.push(kw);
|
|
309
|
+
if (out.length >= MAX_KEYWORDS) break;
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeSource(source, now) {
|
|
315
|
+
const src = source && typeof source === 'object' ? source : {};
|
|
316
|
+
const type = src.type === 'manual' ? 'manual' : 'user_said';
|
|
317
|
+
return {
|
|
318
|
+
type,
|
|
319
|
+
quote: typeof src.quote === 'string' && src.quote.trim() ? cleanContent(src.quote) : null,
|
|
320
|
+
conversationId: typeof src.conversationId === 'string' && src.conversationId.trim() ? src.conversationId.trim() : null,
|
|
321
|
+
atMs: Number.isFinite(src.atMs) ? src.atMs : now,
|
|
322
|
+
channel: typeof src.channel === 'string' && src.channel.trim() ? src.channel.trim() : null,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isItemRecord(value) {
|
|
327
|
+
return value && typeof value === 'object' && typeof value.id === 'string' && typeof value.content === 'string' &&
|
|
328
|
+
KINDS.includes(value.kind);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function cloneItem(item) {
|
|
332
|
+
return {
|
|
333
|
+
...item,
|
|
334
|
+
keywords: Array.isArray(item.keywords) ? item.keywords.slice() : [],
|
|
335
|
+
source: item.source ? { ...item.source } : item.source,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 建一个记忆账本。
|
|
341
|
+
*
|
|
342
|
+
* @param {object} [options]
|
|
343
|
+
* @param {string} [options.rootDir] 账本根目录,缺省 {@link memoryRoot}(测试可指临时目录)。
|
|
344
|
+
* @param {() => number} [options.clock] 时钟,缺省 Date.now。
|
|
345
|
+
* @param {() => string} [options.ids] id/撤销令牌生成器,缺省 randomUUID。
|
|
346
|
+
* @param {number} [options.limit] 每个 scope 的条数上限,缺省 {@link LIMIT}。
|
|
347
|
+
*/
|
|
348
|
+
function createMemoryStore(options = {}) {
|
|
349
|
+
const root = options.rootDir || memoryRoot();
|
|
350
|
+
const clock = typeof options.clock === 'function' ? options.clock : () => Date.now();
|
|
351
|
+
const makeId = typeof options.ids === 'function' ? options.ids : () => crypto.randomUUID();
|
|
352
|
+
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : LIMIT;
|
|
353
|
+
|
|
354
|
+
const cache = new Map(); // scope -> items[]
|
|
355
|
+
const undos = new Map(); // token -> { scope, change }
|
|
356
|
+
|
|
357
|
+
function fileFor(scope) {
|
|
358
|
+
if (scope === GLOBAL_SCOPE) return path.join(root, 'global.json');
|
|
359
|
+
const safe = String(scope).replace(/^workspace:/, '').replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'workspace';
|
|
360
|
+
const hash = crypto.createHash('sha1').update(String(scope)).digest('hex').slice(0, 8);
|
|
361
|
+
return path.join(root, 'workspaces', `${safe}-${hash}.json`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function load(scope) {
|
|
365
|
+
const hit = cache.get(scope);
|
|
366
|
+
if (hit) return hit;
|
|
367
|
+
const file = fileFor(scope);
|
|
368
|
+
let raw;
|
|
369
|
+
try {
|
|
370
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err && err.code === 'ENOENT') {
|
|
373
|
+
const empty = [];
|
|
374
|
+
cache.set(scope, empty);
|
|
375
|
+
return empty;
|
|
376
|
+
}
|
|
377
|
+
throw new MemoryError('unavailable');
|
|
378
|
+
}
|
|
379
|
+
let parsed;
|
|
380
|
+
try {
|
|
381
|
+
parsed = JSON.parse(raw);
|
|
382
|
+
} catch {
|
|
383
|
+
throw new MemoryError('unavailable');
|
|
384
|
+
}
|
|
385
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.items)) {
|
|
386
|
+
throw new MemoryError('unavailable');
|
|
387
|
+
}
|
|
388
|
+
const items = parsed.items.filter(isItemRecord).map(cloneItem);
|
|
389
|
+
cache.set(scope, items);
|
|
390
|
+
return items;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function save(scope, items) {
|
|
394
|
+
const file = fileFor(scope);
|
|
395
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
396
|
+
const payload = `${JSON.stringify({ version: 1, scope, items }, null, 2)}\n`;
|
|
397
|
+
const tmp = `${file}.${process.pid}.${clock()}.tmp`;
|
|
398
|
+
fs.writeFileSync(tmp, payload);
|
|
399
|
+
fs.renameSync(tmp, file);
|
|
400
|
+
cache.set(scope, items);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function remember(scope, change) {
|
|
404
|
+
const token = makeId();
|
|
405
|
+
undos.set(token, { scope, change });
|
|
406
|
+
while (undos.size > UNDO_KEEP) undos.delete(undos.keys().next().value);
|
|
407
|
+
return token;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function check(content, keywords, source) {
|
|
411
|
+
if (content.length === 0 || content.length > MAX_CONTENT) throw new MemoryError('too_long');
|
|
412
|
+
const texts = [content, ...keywords, source && source.quote ? source.quote : ''];
|
|
413
|
+
for (const text of texts) if (sensitiveHit(text)) throw new MemoryError('sensitive');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function add(input = {}) {
|
|
417
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
418
|
+
if (!KINDS.includes(input.kind)) throw new MemoryError('invalid_kind');
|
|
419
|
+
const items = load(scope);
|
|
420
|
+
const content = cleanContent(input.content);
|
|
421
|
+
const keywords = cleanKeywords(input.keywords);
|
|
422
|
+
const source = normalizeSource(input.source, clock());
|
|
423
|
+
check(content, keywords, source);
|
|
424
|
+
|
|
425
|
+
// 同类型同内容视为同一条:不重复记,只把更新时间推到现在。
|
|
426
|
+
const dup = items.find((it) => it.kind === input.kind && cleanContent(it.content) === content);
|
|
427
|
+
if (dup) {
|
|
428
|
+
const bumped = { ...dup, updatedAt: clock() };
|
|
429
|
+
save(scope, items.map((it) => (it.id === dup.id ? bumped : it)));
|
|
430
|
+
return { item: cloneItem(bumped), undoToken: null, deduped: true };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (items.length >= limit) throw new MemoryError('memory_full');
|
|
434
|
+
const id = makeId();
|
|
435
|
+
if (items.some((it) => it.id === id)) throw new MemoryError('duplicate_id');
|
|
436
|
+
const now = clock();
|
|
437
|
+
const item = {
|
|
438
|
+
id,
|
|
439
|
+
kind: input.kind,
|
|
440
|
+
content,
|
|
441
|
+
keywords,
|
|
442
|
+
source,
|
|
443
|
+
status: input.kind === 'procedure' ? 'pending' : 'active',
|
|
444
|
+
pinned: input.pinned === true,
|
|
445
|
+
createdAt: now,
|
|
446
|
+
updatedAt: now,
|
|
447
|
+
lastUsedAt: null,
|
|
448
|
+
useCount: 0,
|
|
449
|
+
replaces: null,
|
|
450
|
+
};
|
|
451
|
+
const undoToken = remember(scope, { type: 'added', item: cloneItem(item) });
|
|
452
|
+
save(scope, [...items, item]);
|
|
453
|
+
return { item: cloneItem(item), undoToken, deduped: false };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function update(input = {}) {
|
|
457
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
458
|
+
const items = load(scope);
|
|
459
|
+
const before = items.find((it) => it.id === input.id);
|
|
460
|
+
if (!before) throw new MemoryError('not_found');
|
|
461
|
+
const content = cleanContent(input.content != null ? input.content : before.content);
|
|
462
|
+
const keywords = input.keywords != null ? cleanKeywords(input.keywords) : before.keywords;
|
|
463
|
+
const source = input.source != null ? normalizeSource(input.source, clock()) : before.source;
|
|
464
|
+
check(content, keywords, source);
|
|
465
|
+
// 做法被非手写来源改动后要重新批准;用户手改或没有来源的改动保持原状态。
|
|
466
|
+
const reapprove = before.kind === 'procedure' && input.source != null && source.type !== 'manual';
|
|
467
|
+
const item = {
|
|
468
|
+
...before,
|
|
469
|
+
content,
|
|
470
|
+
keywords,
|
|
471
|
+
source,
|
|
472
|
+
updatedAt: clock(),
|
|
473
|
+
replaces: content !== before.content ? before.content : before.replaces,
|
|
474
|
+
status: reapprove ? 'pending' : before.status,
|
|
475
|
+
};
|
|
476
|
+
const undoToken = remember(scope, { type: 'updated', item: cloneItem(item), before: cloneItem(before) });
|
|
477
|
+
save(scope, items.map((it) => (it.id === item.id ? item : it)));
|
|
478
|
+
return { item: cloneItem(item), undoToken };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function forget(input = {}) {
|
|
482
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
483
|
+
const items = load(scope);
|
|
484
|
+
const before = items.find((it) => it.id === input.id);
|
|
485
|
+
if (!before) throw new MemoryError('not_found');
|
|
486
|
+
const undoToken = remember(scope, { type: 'forgot', before: cloneItem(before) });
|
|
487
|
+
save(scope, items.filter((it) => it.id !== before.id));
|
|
488
|
+
return { item: cloneItem(before), undoToken };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** 未知/过期/已用过的令牌回 false。更新的撤销只在条目仍是这次改后的样子时才做。 */
|
|
492
|
+
function undo(token) {
|
|
493
|
+
const entry = undos.get(String(token));
|
|
494
|
+
if (!entry) return false;
|
|
495
|
+
const items = load(entry.scope);
|
|
496
|
+
const { change } = entry;
|
|
497
|
+
let next;
|
|
498
|
+
if (change.type === 'added') {
|
|
499
|
+
next = items.filter((it) => it.id !== change.item.id);
|
|
500
|
+
} else if (change.type === 'forgot') {
|
|
501
|
+
next = items.some((it) => it.id === change.before.id) ? items : [...items, cloneItem(change.before)];
|
|
502
|
+
} else if (change.type === 'updated') {
|
|
503
|
+
const cur = items.find((it) => it.id === change.item.id);
|
|
504
|
+
if (!cur || cur.content !== change.item.content || cleanKeywords(cur.keywords).join('\u0000') !== cleanKeywords(change.item.keywords).join('\u0000')) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
next = items.map((it) => {
|
|
508
|
+
if (it.id !== change.item.id) return it;
|
|
509
|
+
return { ...cloneItem(change.before), useCount: cur.useCount, lastUsedAt: cur.lastUsedAt, pinned: cur.pinned };
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
save(entry.scope, next);
|
|
515
|
+
undos.delete(String(token));
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function approve(input = {}) {
|
|
520
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
521
|
+
const items = load(scope);
|
|
522
|
+
const target = items.find((it) => it.id === input.id);
|
|
523
|
+
if (!target || target.status !== 'pending') return false;
|
|
524
|
+
save(scope, items.map((it) => (it.id === input.id ? { ...it, status: 'active', updatedAt: clock() } : it)));
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function setPinned(input = {}) {
|
|
529
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
530
|
+
const items = load(scope);
|
|
531
|
+
if (!items.some((it) => it.id === input.id)) return false;
|
|
532
|
+
save(scope, items.map((it) => (it.id === input.id ? { ...it, pinned: input.pinned === true } : it)));
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function markUsed(input = {}) {
|
|
537
|
+
const scope = input.scope || GLOBAL_SCOPE;
|
|
538
|
+
const ids = Array.isArray(input.ids) ? input.ids.map(String) : [];
|
|
539
|
+
if (ids.length === 0) return;
|
|
540
|
+
const set = new Set(ids);
|
|
541
|
+
const items = load(scope);
|
|
542
|
+
const now = clock();
|
|
543
|
+
save(scope, items.map((it) => (set.has(it.id) ? { ...it, useCount: (it.useCount || 0) + 1, lastUsedAt: now } : it)));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** 用户要整本清除:这就是用户的意思,空表覆盖。 */
|
|
547
|
+
function clear(scope = GLOBAL_SCOPE) {
|
|
548
|
+
undos.clear();
|
|
549
|
+
save(scope, []);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** 列出某个 scope 的全部记忆(副本)。 */
|
|
553
|
+
function list(scope = GLOBAL_SCOPE) {
|
|
554
|
+
return load(scope).map(cloneItem);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** 某个 scope 里待批准的做法。 */
|
|
558
|
+
function pending(scope = GLOBAL_SCOPE) {
|
|
559
|
+
return load(scope).filter((it) => it.status === 'pending').map(cloneItem);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* 按 query 检索。默认只搜 active;includePending 时可带上待批准做法(给模型看前应过滤)。
|
|
564
|
+
* 返回 [{ item, score }],按分数降序。
|
|
565
|
+
*/
|
|
566
|
+
function search(scope = GLOBAL_SCOPE, query = '', opts = {}) {
|
|
567
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : 8;
|
|
568
|
+
const items = load(scope).filter((it) => (opts.includePending ? true : it.status === 'active'));
|
|
569
|
+
return rankItems(items, query, limit);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
root,
|
|
574
|
+
fileFor,
|
|
575
|
+
add,
|
|
576
|
+
update,
|
|
577
|
+
forget,
|
|
578
|
+
undo,
|
|
579
|
+
approve,
|
|
580
|
+
setPinned,
|
|
581
|
+
markUsed,
|
|
582
|
+
clear,
|
|
583
|
+
list,
|
|
584
|
+
pending,
|
|
585
|
+
search,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* 把一撮记忆渲染成一段上下文文本(纯逻辑;将来由宿主注入时直接拼到 prompt 前面)。
|
|
591
|
+
* 置顶优先、其后按更新时间倒序;做法最多 3 条;超过 maxChars 从末尾丢。
|
|
592
|
+
*/
|
|
593
|
+
function renderContext(items, opts = {}) {
|
|
594
|
+
const maxChars = Number.isInteger(opts.maxChars) && opts.maxChars > 0 ? opts.maxChars : 8000;
|
|
595
|
+
const active = (Array.isArray(items) ? items : []).filter((it) => it && it.status === 'active');
|
|
596
|
+
if (active.length === 0) return '';
|
|
597
|
+
const sorted = active.slice().sort((a, b) => (Number(b.pinned) - Number(a.pinned)) || (b.updatedAt - a.updatedAt));
|
|
598
|
+
const facts = sorted.filter((it) => it.kind !== 'procedure');
|
|
599
|
+
const procedures = sorted.filter((it) => it.kind === 'procedure').slice(0, 3);
|
|
600
|
+
const sections = [];
|
|
601
|
+
if (facts.length > 0) {
|
|
602
|
+
sections.push(
|
|
603
|
+
'Memories about the user (things the user told you; may be out of date; when they conflict with what the user says now, follow what they say now):\n' +
|
|
604
|
+
facts.map((it, i) => `${i + 1}. ${it.content}`).join('\n'),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
if (procedures.length > 0) {
|
|
608
|
+
sections.push(
|
|
609
|
+
'Ways of doing things the user approved (when they conflict with what the user says now, follow what they say now):\n' +
|
|
610
|
+
procedures.map((it, i) => `${i + 1}. ${it.content}`).join('\n'),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
let text = sections.join('\n\n');
|
|
614
|
+
if (text.length > maxChars) text = text.slice(0, maxChars);
|
|
615
|
+
return text;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
module.exports = {
|
|
619
|
+
KINDS,
|
|
620
|
+
STATUSES,
|
|
621
|
+
GLOBAL_SCOPE,
|
|
622
|
+
MAX_CONTENT,
|
|
623
|
+
MAX_KEYWORDS,
|
|
624
|
+
LIMIT,
|
|
625
|
+
UNDO_KEEP,
|
|
626
|
+
MemoryError,
|
|
627
|
+
memoryRoot,
|
|
628
|
+
scopeForWorkspace,
|
|
629
|
+
sensitiveHit,
|
|
630
|
+
normalizeForSensitive,
|
|
631
|
+
tokenize,
|
|
632
|
+
rankItems,
|
|
633
|
+
createMemoryStore,
|
|
634
|
+
renderContext,
|
|
635
|
+
};
|