@istarry/dsh-assistant-profile 0.5.1 → 0.6.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.
- package/README.md +25 -2
- package/lib/client.js +376 -31
- package/lib/client.js.map +1 -1
- package/lib/index.js +285 -49
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 助理档案 · dsh-assistant-profile
|
|
2
2
|
|
|
3
|
-
给 DeepSeek Harness
|
|
3
|
+
给 DeepSeek Harness 一份**全局人格**和**一份长期记忆**:名称、语言、人格提示词三段,装一次,之后每个新会话的系统提示最前面都从它开始;记忆紧跟在人格后面,聊天里点一下就能把选中的对话提炼进去。侧栏「新会话」上方会显示这份档案的助理名。
|
|
4
4
|
|
|
5
5
|
一个档案,四个模式通用(标准 / PTC / 极简 / 创造)。不需要复制预设,也不需要改模式选择器。
|
|
6
6
|
|
|
@@ -24,6 +24,28 @@ Reply in 简体中文 throughout.
|
|
|
24
24
|
- 四段全空时回落到预设原文,等于原生行为。
|
|
25
25
|
- **子智能体不注入**:委派出去的孩子保留它自己的人格。
|
|
26
26
|
|
|
27
|
+
## 助理记忆
|
|
28
|
+
|
|
29
|
+
单档案所以单记忆:一份 markdown(一行一条 `- [YYYY-MM-DD HH:mm] 内容`),随档案存在 profile 的同一份配置里,所有会话共享。
|
|
30
|
+
|
|
31
|
+
**注入位置**:`memory:assistant` 段,order 500 —— 排在人格(0)之后、工具目录(1000)之前。记忆描述的是「用户和我是什么关系」,工具说明会引用这些前提。
|
|
32
|
+
|
|
33
|
+
**三个入口**:
|
|
34
|
+
|
|
35
|
+
- **聊天里的「记住」按钮**:每条助手消息的动作行上(官方的点赞/点踩旁边)多一个 ✨。点开是一个勾选弹窗,列出这个会话里可提炼的用户/助手消息;勾选后提交,宿主用**该会话自己的模型**跑一次独立请求,把选中的对话压成几条稳定事实,逐条追加(同义条目只刷新时间戳,不重复堆积)。
|
|
36
|
+
- **设置 → Agent记忆**:两个开关(启用记忆 / 自动记忆)+ 一个编辑框。手工写、改、删都在这儿,保存即落盘。
|
|
37
|
+
- **`memory_add` 工具**:只在「启用记忆 + 自动记忆」都打开时注册,模型可以在对话中自己决定记什么。
|
|
38
|
+
|
|
39
|
+
**提炼是 fail-closed 的**:会话不在线、取不到这个会话的模型配置、模型没吐出一条可用条目,都不会写任何东西,只把原因回显在弹窗里。
|
|
40
|
+
|
|
41
|
+
### 三个实现上的坑(改这个功能前先看)
|
|
42
|
+
|
|
43
|
+
1. **宿主不能直接写自己的配置**。`Volatile` 只暴露 `get()`,写入必须经设置服务(`settings.update(ns, patch)`),它才会落进 profile 的 patch 并由加载器提交回运行时引用。
|
|
44
|
+
2. **写回要挪出 HMR 事务**。设置写入本身跑在 `hmr.runExclusive` 里,而写回结果又要经设置服务再开一个事务——直接调用会抛 `HMR transactions cannot be nested`。所以所有触发点都过一层 `setTimeout(0)`。
|
|
45
|
+
3. **设置写入不会触发 `loader/volatile-update`**(实测;值会提交进引用,但没有那次事件)。信箱因此靠 700ms 轮询:每次只读一个字符串字段,没有新请求时就是一次 JSON 判定。
|
|
46
|
+
|
|
47
|
+
因为插件目录里解析不到 `@deepseek-ai/dsh-tools` / `dsh-llm` / `dsh-session`(只有 `cordis` 由宿主提供),工具定义与提炼用的消息都按官方线上形状**手搭**,不 import 这几个包。
|
|
48
|
+
|
|
27
49
|
## 改人格不会重算历史
|
|
28
50
|
|
|
29
51
|
DeepSeek 的上下文缓存是前缀缓存:系统提示在最前面,直接改写开头那条会让后面全部失效。
|
|
@@ -96,7 +118,8 @@ node build.mjs --check # 构建 + 依赖契约校验,不写 dist/
|
|
|
96
118
|
|
|
97
119
|
## 已知边界
|
|
98
120
|
|
|
99
|
-
-
|
|
121
|
+
- 只注入**顶层会话**,子代理不注入(记忆段是全局的,子代理读得到)。
|
|
122
|
+
- **没有远程调用**:记忆、信箱、归档全都走设置通道与配置字段,所以不需要 typert 生成物。
|
|
100
123
|
- 读不到预设定义时(注册表异常)降级为普通注入,不镜像 `complete`,也不报错。
|
|
101
124
|
- 三栏全部清空再保存 = 什么都不注入(回到原生身份句);装好时默认是默认三的内容。
|
|
102
125
|
- 侧栏名plate 是插在「新会话」按钮前的 DOM 节点(外壳没给这个位置留 slot),React 重渲染把它冲掉时会自动补回。
|
package/lib/client.js
CHANGED
|
@@ -103,13 +103,13 @@ window.__ModuleLoader__.load({
|
|
|
103
103
|
];
|
|
104
104
|
//#endregion
|
|
105
105
|
//#region \0assistant-css:E:\Plugin\发布版\dsh-assistant-profile\src\client\ProfilePage.module.css.mjs
|
|
106
|
-
const css$
|
|
107
|
-
const tagId$
|
|
108
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
106
|
+
const css$2 = "._2lYP0a_page{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);min-height:100%;padding:28px 32px 48px;overflow-y:auto}._2lYP0a_section{max-width:720px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;display:flex}._2lYP0a_head{flex-direction:column;gap:4px;display:flex}._2lYP0a_title{margin:0;font-size:18px;font-weight:600;line-height:26px}._2lYP0a_intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:20px}._2lYP0a_form{border:.5px solid var(--dsw-alias-settings-card-stroke);border-radius:var(--dsw-radius-xl);background:var(--dsw-alias-settings-card-fill);flex-direction:column;gap:18px;padding:20px;display:flex}._2lYP0a_row{grid-template-columns:minmax(0,1fr) minmax(0,240px);gap:16px;display:grid}@media (width<=640px){._2lYP0a_row{grid-template-columns:minmax(0,1fr)}}._2lYP0a_field{flex-direction:column;gap:6px;min-width:0;display:flex}._2lYP0a_label{font-size:13px;font-weight:500;line-height:18px}._2lYP0a_hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}._2lYP0a_hintWarn{color:var(--dsw-alias-state-warn-label)}._2lYP0a_control{width:100%}._2lYP0a_textareaWrap{position:relative}._2lYP0a_textarea{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);width:100%;min-height:168px;color:var(--dsw-alias-label-primary);resize:vertical;outline:none;padding:9px 12px 26px;font-family:inherit;font-size:13px;line-height:21px;display:block}._2lYP0a_textarea::placeholder{color:var(--dsw-alias-label-dimmed)}._2lYP0a_textarea:focus-visible{border-color:var(--dsw-alias-state-business-primary)}._2lYP0a_counter{color:var(--dsw-alias-label-tertiary);pointer-events:none;font-size:11px;line-height:14px;position:absolute;bottom:8px;right:10px}._2lYP0a_counter[data-long=true]{color:var(--dsw-alias-state-warn-label)}._2lYP0a_status{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:18px}._2lYP0a_statusError{color:var(--dsw-alias-state-error-primary)}._2lYP0a_footer{align-items:center;gap:10px;padding-top:2px;display:flex}._2lYP0a_presets{scrollbar-width:thin;flex:1;align-items:center;gap:6px;min-width:0;padding:2px 0;display:flex;overflow-x:auto}._2lYP0a_chip{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border-radius:999px;flex:none;padding:3px 10px;font-size:12px;line-height:18px;transition:background .12s,border-color .12s,color .12s}._2lYP0a_chip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}._2lYP0a_chip:disabled{cursor:default;opacity:.55}._2lYP0a_chipActive{border-color:var(--dsw-static-neutral-bluish-400);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}._2lYP0a_custom{border:.5px solid var(--dsw-alias-border-l3);background:0 0;border-radius:999px;flex:none;align-items:center;transition:background .12s,border-color .12s;display:inline-flex}._2lYP0a_custom:hover{background:var(--dsw-alias-interactive-bg-hover)}._2lYP0a_custom ._2lYP0a_chip{border:none;border-radius:999px 0 0 999px;padding-right:4px}._2lYP0a_customDelete{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:0 999px 999px 0;padding:3px 8px 3px 2px;font-size:13px;line-height:18px}._2lYP0a_customDelete:hover:not(:disabled){color:var(--dsw-alias-state-error-primary)}._2lYP0a_customDelete:disabled{cursor:default;opacity:.55}._2lYP0a_nameplate{box-sizing:border-box;color:var(--dsw-alias-label-primary);flex:none;justify-content:center;margin:12px 2px 0;padding:0 10px 8px;display:flex}._2lYP0a_nameplate[data-collapsed=true],._2lYP0a_nameplate[hidden]{display:none}._2lYP0a_nameplateText{text-align:center;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;font-weight:500;line-height:22px;overflow:hidden}";
|
|
107
|
+
const tagId$2 = "@istarry/dsh-assistant-profile/ProfilePage.module.css";
|
|
108
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
109
109
|
const tag = document.createElement("style");
|
|
110
110
|
tag.dataset.plugin = "@istarry/dsh-assistant-profile";
|
|
111
|
-
tag.dataset.pluginCss = tagId$
|
|
112
|
-
tag.textContent = css$
|
|
111
|
+
tag.dataset.pluginCss = tagId$2;
|
|
112
|
+
tag.textContent = css$2;
|
|
113
113
|
document.head.appendChild(tag);
|
|
114
114
|
}
|
|
115
115
|
var ProfilePage_module_css_default = {
|
|
@@ -680,13 +680,13 @@ window.__ModuleLoader__.load({
|
|
|
680
680
|
}
|
|
681
681
|
//#endregion
|
|
682
682
|
//#region \0assistant-css:E:\Plugin\发布版\dsh-assistant-profile\src\client\MemorySection.module.css.mjs
|
|
683
|
-
const css = "._26v_lW_page{flex-direction:column;gap:18px;padding:4px 0 24px;display:flex}._26v_lW_title{margin:0;font-size:16px;font-weight:600}._26v_lW_intro{opacity:.72;margin:0;font-size:13px;line-height:1.6}._26v_lW_group{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;gap:10px;padding:14px;display:flex}._26v_lW_groupHead{margin:0;font-size:13px;font-weight:600}._26v_lW_row{justify-content:space-between;align-items:center;gap:16px;padding:6px 0;display:flex}._26v_lW_rowText{flex-direction:column;gap:3px;display:flex}._26v_lW_rowTitle{font-size:13px}._26v_lW_rowDesc{opacity:.62;margin:0;font-size:12px;line-height:1.5}._26v_lW_switch{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-interactive-bg-default);cursor:pointer;border-radius:11px;flex:none;width:38px;height:22px;padding:0;transition:background .15s;position:relative}._26v_lW_switch:disabled{opacity:.45;cursor:not-allowed}._26v_lW_on{background:var(--dsw-static-deepseek-500,#4d6bfe);border-color:#0000}._26v_lW_knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:2px;left:2px}._26v_lW_on ._26v_lW_knob{transform:translate(16px)}._26v_lW_editorHead{justify-content:space-between;align-items:baseline;display:flex}._26v_lW_textarea{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:100%;color:inherit;resize:vertical;border-radius:8px;padding:10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}._26v_lW_actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._26v_lW_primary,._26v_lW_secondary{cursor:pointer;border-radius:7px;padding:5px 14px;font-size:12px}._26v_lW_primary{background:var(--dsw-static-deepseek-500,#4d6bfe);color:#fff;border:1px solid #0000}._26v_lW_secondary{border:1px solid var(--dsw-alias-border-l1);color:inherit;background:0 0}._26v_lW_primary:disabled,._26v_lW_secondary:disabled{opacity:.45;cursor:not-allowed}._26v_lW_link{color:var(--dsw-static-deepseek-500,#4d6bfe);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}._26v_lW_ok{color:var(--dsw-static-deepseek-500,#4d6bfe);font-size:12px}._26v_lW_error{color:#d33;margin:0;font-size:12px}._26v_lW_overlay{z-index:60;background:#00000059;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._26v_lW_dialog{background:var(--dsw-alias-bg-overlay,#fff);border-radius:12px;flex-direction:column;gap:12px;width:min(560px,92vw);max-height:84vh;padding:18px;display:flex;overflow:auto;box-shadow:0 12px 40px #00000038}._26v_lW_dialogTitle{margin:0;font-size:14px;font-weight:600}";
|
|
684
|
-
const tagId = "@istarry/dsh-assistant-profile/MemorySection.module.css";
|
|
685
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
683
|
+
const css$1 = "._26v_lW_page{flex-direction:column;gap:18px;padding:4px 0 24px;display:flex}._26v_lW_title{margin:0;font-size:16px;font-weight:600}._26v_lW_intro{opacity:.72;margin:0;font-size:13px;line-height:1.6}._26v_lW_group{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;flex-direction:column;gap:10px;padding:14px;display:flex}._26v_lW_groupHead{margin:0;font-size:13px;font-weight:600}._26v_lW_row{justify-content:space-between;align-items:center;gap:16px;padding:6px 0;display:flex}._26v_lW_rowText{flex-direction:column;gap:3px;display:flex}._26v_lW_rowTitle{font-size:13px}._26v_lW_rowDesc{opacity:.62;margin:0;font-size:12px;line-height:1.5}._26v_lW_switch{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-interactive-bg-default);cursor:pointer;border-radius:11px;flex:none;width:38px;height:22px;padding:0;transition:background .15s;position:relative}._26v_lW_switch:disabled{opacity:.45;cursor:not-allowed}._26v_lW_on{background:var(--dsw-static-deepseek-500,#4d6bfe);border-color:#0000}._26v_lW_knob{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .15s;position:absolute;top:2px;left:2px}._26v_lW_on ._26v_lW_knob{transform:translate(16px)}._26v_lW_editorHead{justify-content:space-between;align-items:baseline;display:flex}._26v_lW_textarea{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:100%;color:inherit;resize:vertical;border-radius:8px;padding:10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.6}._26v_lW_actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}._26v_lW_primary,._26v_lW_secondary{cursor:pointer;border-radius:7px;padding:5px 14px;font-size:12px}._26v_lW_primary{background:var(--dsw-static-deepseek-500,#4d6bfe);color:#fff;border:1px solid #0000}._26v_lW_secondary{border:1px solid var(--dsw-alias-border-l1);color:inherit;background:0 0}._26v_lW_primary:disabled,._26v_lW_secondary:disabled{opacity:.45;cursor:not-allowed}._26v_lW_link{color:var(--dsw-static-deepseek-500,#4d6bfe);cursor:pointer;background:0 0;border:none;padding:0;font-size:12px}._26v_lW_ok{color:var(--dsw-static-deepseek-500,#4d6bfe);font-size:12px}._26v_lW_error{color:#d33;margin:0;font-size:12px}._26v_lW_overlay{z-index:60;background:#00000059;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._26v_lW_dialog{background:var(--dsw-alias-bg-overlay,#fff);border-radius:12px;flex-direction:column;gap:12px;width:min(560px,92vw);max-height:84vh;padding:18px;display:flex;overflow:auto;box-shadow:0 12px 40px #00000038}._26v_lW_dialogTitle{margin:0;font-size:14px;font-weight:600}";
|
|
684
|
+
const tagId$1 = "@istarry/dsh-assistant-profile/MemorySection.module.css";
|
|
685
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
686
686
|
const tag = document.createElement("style");
|
|
687
687
|
tag.dataset.plugin = "@istarry/dsh-assistant-profile";
|
|
688
|
-
tag.dataset.pluginCss = tagId;
|
|
689
|
-
tag.textContent = css;
|
|
688
|
+
tag.dataset.pluginCss = tagId$1;
|
|
689
|
+
tag.textContent = css$1;
|
|
690
690
|
document.head.appendChild(tag);
|
|
691
691
|
}
|
|
692
692
|
var MemorySection_module_css_default = {
|
|
@@ -861,6 +861,330 @@ window.__ModuleLoader__.load({
|
|
|
861
861
|
});
|
|
862
862
|
}
|
|
863
863
|
//#endregion
|
|
864
|
+
//#region \0assistant-css:E:\Plugin\发布版\dsh-assistant-profile\src\client\MemoryActions.module.css.mjs
|
|
865
|
+
const css = ".BvVp9q_action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));border-radius:var(--dsw-radius-sm);color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.BvVp9q_action svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}.BvVp9q_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}.BvVp9q_note{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:12px;line-height:20px}.BvVp9q_list{flex-direction:column;gap:2px;max-height:min(52vh,460px);margin:-4px -4px 0;padding:4px;display:flex;overflow-y:auto}.BvVp9q_row{border-radius:var(--dsw-radius-sm);cursor:pointer;align-items:flex-start;gap:8px;padding:7px 8px;display:flex}.BvVp9q_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.BvVp9q_rowChecked{background:var(--dsw-alias-bg-module-platform)}.BvVp9q_check{accent-color:var(--dsw-alias-state-business-primary);flex:none;margin:3px 0 0}.BvVp9q_side{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-tertiary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px;line-height:17px}.BvVp9q_preview{min-width:0;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere;font-size:13px;line-height:20px}.BvVp9q_empty{color:var(--dsw-alias-label-tertiary);text-align:center;padding:18px 8px;font-size:13px;line-height:20px}.BvVp9q_error{color:var(--dsw-alias-state-error-primary);margin-right:auto;font-size:12px;line-height:18px}";
|
|
866
|
+
const tagId = "@istarry/dsh-assistant-profile/MemoryActions.module.css";
|
|
867
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
868
|
+
const tag = document.createElement("style");
|
|
869
|
+
tag.dataset.plugin = "@istarry/dsh-assistant-profile";
|
|
870
|
+
tag.dataset.pluginCss = tagId;
|
|
871
|
+
tag.textContent = css;
|
|
872
|
+
document.head.appendChild(tag);
|
|
873
|
+
}
|
|
874
|
+
var MemoryActions_module_css_default = {
|
|
875
|
+
"action": "BvVp9q_action",
|
|
876
|
+
"check": "BvVp9q_check",
|
|
877
|
+
"empty": "BvVp9q_empty",
|
|
878
|
+
"error": "BvVp9q_error",
|
|
879
|
+
"list": "BvVp9q_list",
|
|
880
|
+
"note": "BvVp9q_note",
|
|
881
|
+
"preview": "BvVp9q_preview",
|
|
882
|
+
"row": "BvVp9q_row",
|
|
883
|
+
"rowChecked": "BvVp9q_rowChecked",
|
|
884
|
+
"side": "BvVp9q_side"
|
|
885
|
+
};
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region ../src/client/MemoryActions.tsx
|
|
888
|
+
/**
|
|
889
|
+
* 聊天里的「记住」:助理消息动作行上的一个按钮 + 一个勾选弹窗。
|
|
890
|
+
*
|
|
891
|
+
* 勾中的消息会按时间顺序拼成「用户:…/助手:…」的文本,交给宿主提炼成长期记忆。
|
|
892
|
+
* 按钮本身不写任何东西:只有人勾选并提交,才会产生一次提炼。
|
|
893
|
+
*
|
|
894
|
+
* @module @istarry/dsh-assistant-profile/client/MemoryActions
|
|
895
|
+
*/
|
|
896
|
+
/**
|
|
897
|
+
* 从助手消息的块里取正文。
|
|
898
|
+
* @param blocks - 消息块。
|
|
899
|
+
* @returns 拼起来的正文。
|
|
900
|
+
*/
|
|
901
|
+
function assistantText(blocks) {
|
|
902
|
+
return blocks.flatMap((block) => (block.kind === "text" || block.type === "text") && typeof block.text === "string" ? [block.text] : []).join("");
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* 从用户消息的内容里取正文。
|
|
906
|
+
* @param content - 消息内容块。
|
|
907
|
+
* @returns 拼起来的正文。
|
|
908
|
+
*/
|
|
909
|
+
function contentText(content) {
|
|
910
|
+
return content.flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []).join("");
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* 压成一行摘要。
|
|
914
|
+
* @param text - 原文。
|
|
915
|
+
* @param limit - 字数上限。
|
|
916
|
+
* @returns 摘要。
|
|
917
|
+
*/
|
|
918
|
+
function preview(text, limit = 120) {
|
|
919
|
+
const trimmed = text.replace(/\s+/g, " ").trim();
|
|
920
|
+
return trimmed.length <= limit ? trimmed : `${trimmed.slice(0, limit)}…`;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* 从会话快照里挑出可提炼的消息:用户消息与已完成的助手消息,按时间线排序。
|
|
924
|
+
* 运行中的回复、工具行、命令与上下文快照都没有可复用的正文,跳过。
|
|
925
|
+
* @param snapshot - 当前会话的聊天快照。
|
|
926
|
+
* @returns 候选列表。
|
|
927
|
+
*/
|
|
928
|
+
function candidatesOf(snapshot) {
|
|
929
|
+
const store = snapshot?.nodes;
|
|
930
|
+
const nodes = store === void 0 || store === null ? [] : typeof store.values === "function" ? store.values() : store instanceof Map ? [...store.values()] : Object.values(store);
|
|
931
|
+
const list = [];
|
|
932
|
+
for (const raw of nodes) {
|
|
933
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
934
|
+
const node = raw;
|
|
935
|
+
const key = node.key ?? node.id;
|
|
936
|
+
if (key === void 0) continue;
|
|
937
|
+
if (node.visibility === "hidden") continue;
|
|
938
|
+
const anchorSeq = typeof node.anchorSeq === "number" ? node.anchorSeq : 0;
|
|
939
|
+
if (node.kind === "user" || node.kind === "steering") {
|
|
940
|
+
const content = node.data?.content ?? [];
|
|
941
|
+
const text = Array.isArray(content) ? contentText(content) : "";
|
|
942
|
+
if (text.trim() === "") continue;
|
|
943
|
+
list.push({
|
|
944
|
+
key,
|
|
945
|
+
anchorSeq,
|
|
946
|
+
side: "user",
|
|
947
|
+
preview: preview(text),
|
|
948
|
+
text
|
|
949
|
+
});
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (node.kind === "assistant-step") {
|
|
953
|
+
if (node.data?.status === "running") continue;
|
|
954
|
+
const blocks = node.data?.blocks ?? [];
|
|
955
|
+
const text = Array.isArray(blocks) ? assistantText(blocks) : "";
|
|
956
|
+
if (text.trim() === "") continue;
|
|
957
|
+
list.push({
|
|
958
|
+
key,
|
|
959
|
+
anchorSeq,
|
|
960
|
+
side: "assistant",
|
|
961
|
+
preview: preview(text),
|
|
962
|
+
text
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return list.sort((left, right) => left.anchorSeq - right.anchorSeq);
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* 动作行上的「记住」入口,以及提交成功后的短暂提示。
|
|
970
|
+
* @param props - 注入的提炼动作与会话运行时。
|
|
971
|
+
* @returns 按钮与可能展开的弹窗。
|
|
972
|
+
*/
|
|
973
|
+
function MemoryActions({ useChat, remember, t }) {
|
|
974
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
975
|
+
const [note, setNote] = (0, react.useState)(null);
|
|
976
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
977
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
978
|
+
label: t("remember.action"),
|
|
979
|
+
side: "bottom",
|
|
980
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
981
|
+
type: "button",
|
|
982
|
+
className: MemoryActions_module_css_default.action,
|
|
983
|
+
"aria-label": t("remember.action"),
|
|
984
|
+
onClick: () => {
|
|
985
|
+
setNote(null);
|
|
986
|
+
setOpen(true);
|
|
987
|
+
},
|
|
988
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSparkleRegular, {})
|
|
989
|
+
})
|
|
990
|
+
}),
|
|
991
|
+
note !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
992
|
+
className: MemoryActions_module_css_default.note,
|
|
993
|
+
role: "status",
|
|
994
|
+
children: note
|
|
995
|
+
}),
|
|
996
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoryDialog, {
|
|
997
|
+
useChat,
|
|
998
|
+
remember,
|
|
999
|
+
t,
|
|
1000
|
+
onClose: (saved) => {
|
|
1001
|
+
setOpen(false);
|
|
1002
|
+
if (saved) setNote(t("remember.saved"));
|
|
1003
|
+
}
|
|
1004
|
+
})
|
|
1005
|
+
] });
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* 消息勾选弹窗。
|
|
1009
|
+
* @param props - 会话选择器、提炼动作与文案。
|
|
1010
|
+
* @returns 弹窗元素。
|
|
1011
|
+
*/
|
|
1012
|
+
function MemoryDialog({ useChat, remember, onClose, t }) {
|
|
1013
|
+
const snapshot = useChat((state) => state);
|
|
1014
|
+
const candidates = (0, react.useMemo)(() => candidatesOf(snapshot), [snapshot]);
|
|
1015
|
+
const [checked, setChecked] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1016
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1017
|
+
const [error, setError] = (0, react.useState)(null);
|
|
1018
|
+
const toggle = (key) => {
|
|
1019
|
+
const next = new Set(checked);
|
|
1020
|
+
if (next.has(key)) next.delete(key);
|
|
1021
|
+
else next.add(key);
|
|
1022
|
+
setChecked(next);
|
|
1023
|
+
};
|
|
1024
|
+
/** 把宿主的结果原因翻成界面文案。 */
|
|
1025
|
+
const outcomeText = (outcome) => {
|
|
1026
|
+
if (outcome === "session not open") return t("remember.notOpen");
|
|
1027
|
+
if (outcome === "no provider/model available") return t("remember.noModel");
|
|
1028
|
+
if (outcome === "empty selection" || outcome === "nothing worth remembering") return t("remember.nothing");
|
|
1029
|
+
if (outcome === "timed out") return t("remember.timeout");
|
|
1030
|
+
if (outcome === "settings write refused") return t("remember.refused");
|
|
1031
|
+
return outcome === "" ? t("remember.failed") : outcome;
|
|
1032
|
+
};
|
|
1033
|
+
const submit = async () => {
|
|
1034
|
+
if (busy) return;
|
|
1035
|
+
const text = candidates.filter((item) => checked.has(item.key)).map((item) => `${item.side === "user" ? t("remember.side.user") : t("remember.side.assistant")}:${item.text}`).join("\n\n");
|
|
1036
|
+
if (text.trim() === "") return;
|
|
1037
|
+
setBusy(true);
|
|
1038
|
+
setError(null);
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await remember(text);
|
|
1041
|
+
if (result.saved) {
|
|
1042
|
+
onClose(true);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
setError(outcomeText(result.outcome));
|
|
1046
|
+
} catch (caught) {
|
|
1047
|
+
setError(caught instanceof Error ? caught.message : t("remember.failed"));
|
|
1048
|
+
} finally {
|
|
1049
|
+
setBusy(false);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
1053
|
+
open: true,
|
|
1054
|
+
onClose: () => {
|
|
1055
|
+
if (!busy) onClose(false);
|
|
1056
|
+
},
|
|
1057
|
+
closeLabel: t("remember.cancel"),
|
|
1058
|
+
title: t("remember.title"),
|
|
1059
|
+
description: t("remember.subtitle"),
|
|
1060
|
+
footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1061
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1062
|
+
className: MemoryActions_module_css_default.error,
|
|
1063
|
+
role: "alert",
|
|
1064
|
+
children: error
|
|
1065
|
+
}),
|
|
1066
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1067
|
+
variant: "outline",
|
|
1068
|
+
disabled: busy,
|
|
1069
|
+
onClick: () => {
|
|
1070
|
+
onClose(false);
|
|
1071
|
+
},
|
|
1072
|
+
children: t("remember.cancel")
|
|
1073
|
+
}),
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
1075
|
+
variant: "primary",
|
|
1076
|
+
disabled: busy || checked.size === 0,
|
|
1077
|
+
onClick: () => {
|
|
1078
|
+
submit();
|
|
1079
|
+
},
|
|
1080
|
+
children: busy ? t("remember.submitting") : t("remember.submit")
|
|
1081
|
+
})
|
|
1082
|
+
] }),
|
|
1083
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1084
|
+
className: MemoryActions_module_css_default.list,
|
|
1085
|
+
children: [candidates.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1086
|
+
className: MemoryActions_module_css_default.empty,
|
|
1087
|
+
children: t("remember.empty")
|
|
1088
|
+
}), candidates.map((item) => {
|
|
1089
|
+
const active = checked.has(item.key);
|
|
1090
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1091
|
+
className: active ? `${MemoryActions_module_css_default.row} ${MemoryActions_module_css_default.rowChecked}` : MemoryActions_module_css_default.row,
|
|
1092
|
+
children: [
|
|
1093
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1094
|
+
type: "checkbox",
|
|
1095
|
+
className: MemoryActions_module_css_default.check,
|
|
1096
|
+
checked: active,
|
|
1097
|
+
onChange: () => {
|
|
1098
|
+
toggle(item.key);
|
|
1099
|
+
}
|
|
1100
|
+
}),
|
|
1101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1102
|
+
className: MemoryActions_module_css_default.side,
|
|
1103
|
+
children: item.side === "user" ? t("remember.side.user") : t("remember.side.assistant")
|
|
1104
|
+
}),
|
|
1105
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1106
|
+
className: MemoryActions_module_css_default.preview,
|
|
1107
|
+
children: item.preview
|
|
1108
|
+
})
|
|
1109
|
+
]
|
|
1110
|
+
}, item.key);
|
|
1111
|
+
})]
|
|
1112
|
+
})
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
//#endregion
|
|
1116
|
+
//#region ../src/client/remember.ts
|
|
1117
|
+
/** 信箱字段名。 */
|
|
1118
|
+
const SIGNAL_FIELD = "rememberSignal";
|
|
1119
|
+
/** 等结果的上限:提炼是一次完整模型调用,给足时间。 */
|
|
1120
|
+
const TIMEOUT_MS = 12e4;
|
|
1121
|
+
/** 一次请求最多带多少字符,与宿主的截断保持一致。 */
|
|
1122
|
+
const TEXT_LIMIT = 2e4;
|
|
1123
|
+
/**
|
|
1124
|
+
* 解析信箱字段。
|
|
1125
|
+
* @param value - 字段原文。
|
|
1126
|
+
* @returns 解析结果;坏数据当空。
|
|
1127
|
+
*/
|
|
1128
|
+
function parseSignal(value) {
|
|
1129
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
1130
|
+
try {
|
|
1131
|
+
const parsed = JSON.parse(value);
|
|
1132
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
1133
|
+
return parsed;
|
|
1134
|
+
} catch {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* 投递一次提炼请求并等待宿主回话。
|
|
1140
|
+
* @param form - 本插件的设置表(`rememberSignal` 所在的命名空间)。
|
|
1141
|
+
* @param sessionId - 用哪个会话的模型跑这次提炼。
|
|
1142
|
+
* @param text - 选中的对话文本(已带「用户:/助手:」前缀)。
|
|
1143
|
+
* @returns 提炼结果;超时或被拒绝时给失败结果。
|
|
1144
|
+
*/
|
|
1145
|
+
async function requestRemember(form, sessionId, text) {
|
|
1146
|
+
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1147
|
+
const request = JSON.stringify({
|
|
1148
|
+
id,
|
|
1149
|
+
phase: "request",
|
|
1150
|
+
sessionId,
|
|
1151
|
+
text: text.slice(0, TEXT_LIMIT)
|
|
1152
|
+
});
|
|
1153
|
+
if (!await form.set(SIGNAL_FIELD, request)) return {
|
|
1154
|
+
saved: false,
|
|
1155
|
+
outcome: "settings write refused"
|
|
1156
|
+
};
|
|
1157
|
+
return await new Promise((resolve) => {
|
|
1158
|
+
let settled = false;
|
|
1159
|
+
let timer;
|
|
1160
|
+
let unsubscribe = () => {};
|
|
1161
|
+
const finish = (value) => {
|
|
1162
|
+
if (settled) return;
|
|
1163
|
+
settled = true;
|
|
1164
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1165
|
+
unsubscribe();
|
|
1166
|
+
resolve(value);
|
|
1167
|
+
};
|
|
1168
|
+
const check = () => {
|
|
1169
|
+
const signal = parseSignal(form.getSnapshot().value?.[SIGNAL_FIELD]);
|
|
1170
|
+
if (signal?.phase !== "done" || signal.id !== id) return;
|
|
1171
|
+
finish({
|
|
1172
|
+
saved: signal.saved === true,
|
|
1173
|
+
...signal.entries === void 0 ? {} : { entries: signal.entries },
|
|
1174
|
+
outcome: signal.outcome ?? ""
|
|
1175
|
+
});
|
|
1176
|
+
};
|
|
1177
|
+
unsubscribe = form.subscribe(check);
|
|
1178
|
+
timer = setTimeout(() => {
|
|
1179
|
+
finish({
|
|
1180
|
+
saved: false,
|
|
1181
|
+
outcome: "timed out"
|
|
1182
|
+
});
|
|
1183
|
+
}, TIMEOUT_MS);
|
|
1184
|
+
check();
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
864
1188
|
//#region ../src/client/memoryStore.ts
|
|
865
1189
|
/** 把持久化配置解码成三个字段(形状不对时给安全默认值)。 */
|
|
866
1190
|
function decodeConfig(section) {
|
|
@@ -996,20 +1320,26 @@ window.__ModuleLoader__.load({
|
|
|
996
1320
|
"memory.editor": "记忆内容",
|
|
997
1321
|
"memory.editor.hint": "每行一条,格式「- [时间] 内容」;也可以写普通段落,模型照样会读到。",
|
|
998
1322
|
"memory.editor.placeholder": "- [2026-10-24 12:00] 用户偏好简洁的中文回答",
|
|
999
|
-
"memory.reload": "重新载入",
|
|
1000
1323
|
"memory.save": "保存记忆",
|
|
1001
1324
|
"memory.saving": "保存中…",
|
|
1002
1325
|
"memory.saved": "已保存",
|
|
1003
1326
|
"memory.saveFailed": "保存失败",
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1008
|
-
"
|
|
1009
|
-
"
|
|
1010
|
-
"
|
|
1011
|
-
"
|
|
1012
|
-
"
|
|
1327
|
+
"remember.action": "记住",
|
|
1328
|
+
"remember.title": "记进长期记忆",
|
|
1329
|
+
"remember.subtitle": "勾选要提炼的消息,模型会把它们压成几条稳定的事实,追加到助理记忆里。",
|
|
1330
|
+
"remember.submit": "提炼并记住",
|
|
1331
|
+
"remember.submitting": "提炼中…",
|
|
1332
|
+
"remember.cancel": "取消",
|
|
1333
|
+
"remember.empty": "这个会话还没有可提炼的消息。",
|
|
1334
|
+
"remember.saved": "已记住",
|
|
1335
|
+
"remember.failed": "没能记住",
|
|
1336
|
+
"remember.side.user": "用户",
|
|
1337
|
+
"remember.side.assistant": "助手",
|
|
1338
|
+
"remember.notOpen": "会话不在线,先回到这个会话再试。",
|
|
1339
|
+
"remember.noModel": "取不到这个会话的模型配置。",
|
|
1340
|
+
"remember.nothing": "这段没有值得记住的内容。",
|
|
1341
|
+
"remember.timeout": "等太久了,没收到结果。",
|
|
1342
|
+
"remember.refused": "设置通道拒绝了这次请求。"
|
|
1013
1343
|
};
|
|
1014
1344
|
/** 英文词典:与中文键一一对应。 */
|
|
1015
1345
|
const en = {
|
|
@@ -1052,20 +1382,26 @@ window.__ModuleLoader__.load({
|
|
|
1052
1382
|
"memory.editor": "Memory content",
|
|
1053
1383
|
"memory.editor.hint": "One entry per line, in the shape \"- [time] content\"; plain prose works too, the model reads all of it.",
|
|
1054
1384
|
"memory.editor.placeholder": "- [2026-10-24 12:00] Prefers concise Chinese answers",
|
|
1055
|
-
"memory.reload": "Reload",
|
|
1056
1385
|
"memory.save": "Save memory",
|
|
1057
1386
|
"memory.saving": "Saving…",
|
|
1058
1387
|
"memory.saved": "Saved",
|
|
1059
1388
|
"memory.saveFailed": "Save failed",
|
|
1060
|
-
"
|
|
1061
|
-
"
|
|
1062
|
-
"
|
|
1063
|
-
"
|
|
1064
|
-
"
|
|
1065
|
-
"
|
|
1066
|
-
"
|
|
1067
|
-
"
|
|
1068
|
-
"
|
|
1389
|
+
"remember.action": "Remember",
|
|
1390
|
+
"remember.title": "Remember into long-term memory",
|
|
1391
|
+
"remember.subtitle": "Tick the messages to distil; the model condenses them into a few stable facts and appends them to the assistant memory.",
|
|
1392
|
+
"remember.submit": "Distil and remember",
|
|
1393
|
+
"remember.submitting": "Distilling…",
|
|
1394
|
+
"remember.cancel": "Cancel",
|
|
1395
|
+
"remember.empty": "This session has no messages to distil yet.",
|
|
1396
|
+
"remember.saved": "Remembered",
|
|
1397
|
+
"remember.failed": "Could not remember",
|
|
1398
|
+
"remember.side.user": "User",
|
|
1399
|
+
"remember.side.assistant": "Assistant",
|
|
1400
|
+
"remember.notOpen": "This session is not open any more.",
|
|
1401
|
+
"remember.noModel": "No model configuration for this session.",
|
|
1402
|
+
"remember.nothing": "Nothing worth remembering here.",
|
|
1403
|
+
"remember.timeout": "Timed out waiting for the result.",
|
|
1404
|
+
"remember.refused": "The settings channel refused the request."
|
|
1069
1405
|
};
|
|
1070
1406
|
//#endregion
|
|
1071
1407
|
//#region ../src/client/index.ts
|
|
@@ -1106,10 +1442,19 @@ window.__ModuleLoader__.load({
|
|
|
1106
1442
|
locale: NS,
|
|
1107
1443
|
inject: () => ({ profile })
|
|
1108
1444
|
}, ProfilePage));
|
|
1109
|
-
const
|
|
1445
|
+
const memoryForm = ctx.configForms.get(PANEL_ID);
|
|
1446
|
+
const signalForm = ctx.configForms.get(PANEL_ID);
|
|
1447
|
+
const controller = new MemorySettingsController(memoryForm);
|
|
1110
1448
|
ctx.effect(() => () => {
|
|
1111
1449
|
controller.dispose();
|
|
1112
1450
|
}, "assistant-profile: memory form");
|
|
1451
|
+
ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
|
|
1452
|
+
name: "conversation.chat.assistant-actions",
|
|
1453
|
+
id: "assistant-profile-remember",
|
|
1454
|
+
order: 20,
|
|
1455
|
+
locale: NS,
|
|
1456
|
+
inject: (sessionId) => ({ remember: (text) => requestRemember(signalForm, String(sessionId ?? ""), text) })
|
|
1457
|
+
}, MemoryActions));
|
|
1113
1458
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
1114
1459
|
name: "settings.section",
|
|
1115
1460
|
id: "assistant-memory",
|