🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

伴侣记忆 Root-7-KHX/companion-memory

DeepSeek Harness
Root-7-KHX1 天前有提交已提供中文文档扫描:含敏感能力

为角色扮演对话保存人设、好感度与长期记忆

30.8/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/30 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Root-7-KHX/companion-memory
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · search
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 1 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 172 处,展示前 104 条)
  • 发起网络请求
    client.js:387
    const response = await fetch(`${API_BASE}${withSession(path)}`, {
  • 发起网络请求
    client.js:396
    const response = await fetch(`${API_BASE}${path}`, {
  • 写入剪贴板
    client.js:3027
    await navigator.clipboard.writeText(payload.full)
  • 发起网络请求
    client.js:3149
    const response = await fetch(API_BASE + '/people?limit=200', { headers: { accept: 'application/json' }, credentials: 'same-origin' })
  • 发起网络请求
    client.js:3205
    const get = (path) => fetch(API_BASE + path, { headers: { accept: 'application/json' }, credentials: 'same-origin' }).then(unwrap)
  • 发起网络请求
    client.js:4088
    const response = await fetch(API_BASE + '/integrations', {
  • 发起网络请求
    client.js:4312
    const response = await fetch(url, {
  • 写入本地文件
    lib/manager.js:97
    mkdirSync(target, { recursive: true })
  • 读取本地文件
    lib/manager.js:98
    for (const entry of readdirSync(source, { withFileTypes: true })) {
  • 写入本地文件
    lib/manager.js:102
    else if (!existsSync(to)) copyFileSync(from, to)
  • 读取本地文件
    lib/manager.js:102
    else if (!existsSync(to)) copyFileSync(from, to)
  • 写入本地文件
    lib/manager.js:113
    mkdirSync(this.root, { recursive: true })
  • 读取本地文件
    lib/manager.js:120
    if (existsSync(this.registryFile)) {
  • 读取本地文件
    lib/manager.js:121
    const parsed = JSON.parse(readFileSync(this.registryFile, 'utf8'))
  • 写入本地文件
    lib/manager.js:143
    mkdirSync(this.root, { recursive: true })
  • 写入本地文件
    lib/manager.js:145
    writeFileSync(temp, JSON.stringify(this.registry, null, 2), 'utf8')
  • 写入本地文件
    lib/manager.js:146
    renameSync(temp, this.registryFile)
  • 读取本地文件
    lib/manager.js:207
    if (!existsSync(legacyState) && !existsSync(legacyDocs)) return
  • 读取本地文件
    lib/manager.js:210
    for (const entry of readdirSync(this.root, { withFileTypes: true })) {
  • 读取本地文件
    lib/tracking.js:79
    size = statSync(file).size
  • 写入本地文件
    lib/tracking.js:83
    if (size > MAX_LOG_BYTES) writeFileSync(file, `# companion-memory event log (rotated at ${new Date().toISOString()})\n`, 'utf8')
  • 写入本地文件
    lib/tracking.js:84
    appendFileSync(file, `${new Date().toISOString()} ${label} ${detail ?? ''}\n`, 'utf8')
  • 读取本地文件
    lib/context-compress.js:453
    size = statSync(file).size
  • 写入本地文件
    lib/context-compress.js:457
    if (size > MAX_LOG_BYTES) writeFileSync(file, `# companion-memory event log (rotated at ${new Date().toISOString()})\n`, 'utf8')
  • 写入本地文件
    lib/context-compress.js:459
    appendFileSync(file, `${new Date().toISOString()} ${label} ${text}\n`, 'utf8')
  • 写入本地文件
    lib/hierarchy.js:32
    mkdirSync(backupsDir(store), { recursive: true })
  • 读取本地文件
    lib/hierarchy.js:41
    return readFileSync(join(store.dir, relative), 'utf8')
  • 写入本地文件
    lib/hierarchy.js:49
    writeFileSync(join(store.dir, relative), text)
  • 读取本地文件
    lib/hierarchy.js:55
    if (!existsSync(abs)) return ''
  • 写入本地文件
    lib/hierarchy.js:60
    copyFileSync(abs, join(store.dir, target))
  • 读取本地文件
    lib/hierarchy.js:77
    for (const name of readdirSync(join(store.dir, dir))) {
  • 读取本地文件
    lib/hierarchy.js:81
    try { bytes = readFileSync(join(store.dir, rel)).length } catch { bytes = 0 }
  • 读取本地文件
    lib/hierarchy.js:319
    backups = readdirSync(backupsDir(store)).filter((item) => item.startsWith(`${name}.`)).sort()
  • 写入本地文件
    lib/hierarchy.js:325
    copyFileSync(join(backupsDir(store), latest), join(store.dir, `hierarchy/${name}`))
  • 读取本地文件
    lib/dynamic-context.js:34
    size = statSync(file).size
  • 写入本地文件
    lib/dynamic-context.js:38
    if (size > MAX_LOG_BYTES) writeFileSync(file, `# companion-memory event log (rotated at ${new Date().toISOString()})\n`, 'utf8')
  • 写入本地文件
    lib/dynamic-context.js:39
    appendFileSync(file, `${new Date().toISOString()} dynamic-context ${label} ${detail ?? ''}\n`, 'utf8')
  • 写入本地文件
    lib/lean-tools.js:112
    appendFileSync(file, `${new Date().toISOString()} lean-tools ${label} ${detail ?? ''}\n`, 'utf8')
  • 写入本地文件
    lib/api.js:154
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/api.js:164
    writeFileSync(file, body, 'utf8')
  • 读取本地文件
    lib/api.js:772
    const wav = readFileSync(wavPath)
  • 读取本地文件
    lib/api.js:793
    if (existsSync(file)) {
  • 读取本地文件
    lib/api.js:794
    const tail = readFileSync(file, 'utf8').trim().split('\n').slice(-1)[0]
  • 读取本地文件
    lib/api.js:822
    if (existsSync(file)) {
  • 读取本地文件
    lib/api.js:823
    const tail = readFileSync(file, 'utf8').trim().split('\n').slice(-1)[0]
  • 读取本地文件
    lib/store.js:40
    const pkg = JSON.parse(readFileSync(pkgPath, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/store.js:646
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    lib/store.js:648
    writeFileSync(tmp, content, 'utf8')
  • 写入本地文件
    lib/store.js:649
    renameSync(tmp, file)
  • 写入本地文件
    lib/store.js:944
    mkdirSync(join(this.dir, 'archive'), { recursive: true })
  • 写入本地文件
    lib/store.js:945
    mkdirSync(join(this.dir, 'monthly'), { recursive: true })
  • 写入本地文件
    lib/store.js:946
    mkdirSync(join(this.dir, 'packs'), { recursive: true })
  • 读取本地文件
    lib/store.js:953
    if (existsSync(file)) {
  • 读取本地文件
    lib/store.js:955
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 读取本地文件
    lib/store.js:1041
    if (!existsSync(file)) {
  • 读取本地文件
    lib/store.js:1053
    const content = readFileSync(this.docPath(key), 'utf8')
  • 读取本地文件
    lib/store.js:1199
    if (!existsSync(file)) return []
  • 写入本地文件
    lib/people.js:54
    writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8')
  • 写入本地文件
    lib/people.js:55
    renameSync(tmp, file)
  • 写入本地文件
    lib/people.js:71
    mkdirSync(this.dir, { recursive: true })
  • 读取本地文件
    lib/people.js:79
    if (!existsSync(file)) return null
  • 读取本地文件
    lib/people.js:80
    const parsed = JSON.parse(readFileSync(file, 'utf8').replace(/^ FEFF/, ''))
  • 读取本地文件
    lib/people.js:191
    names = readdirSync(this.dir).filter((name) => name.endsWith('.json'))
  • 读取本地文件
    lib/people.js:212
    files = readdirSync(this.dir).filter((name) => name.endsWith('.json')).length
  • 读取本地文件
    lib/qq.js:86
    if (!existsSync(this.configFile)) return { appId: '', appSecret: '', enabled: false, autoSticker: { enabled: false, minGapMinutes: 10, maxPerHour: 6 }
  • 读取本地文件
    lib/qq.js:87
    const parsed = JSON.parse(readFileSync(this.configFile, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/qq.js:156
    mkdirSync(this.root, { recursive: true })
  • 写入本地文件
    lib/qq.js:157
    writeFileSync(this.configFile, JSON.stringify(next, null, 2) + '\n', 'utf8')
  • 发起网络请求
    lib/qq.js:217
    const response = await fetch(TOKEN_URL, {
  • 发起网络请求
    lib/qq.js:234
    const response = await fetch(API_BASE + path, {
  • 写入本地文件
    lib/qq.js:424
    mkdirSync(this.root, { recursive: true })
  • 写入本地文件
    lib/qq.js:425
    appendFileSync(this.messagesFile, JSON.stringify(record) + '\n', 'utf8')
  • 写入本地文件
    lib/qq.js:432
    appendFileSync(join(this.root, 'qq-inbound-debug.jsonl'), JSON.stringify({ at: new Date().toISOString(), t: type, id: record.id, raw: data }).slice(0,
  • 读取本地文件
    lib/qq.js:548
    if (!existsSync(this.stickerIndexFile)) return []
  • 读取本地文件
    lib/qq.js:549
    const parsed = JSON.parse(readFileSync(this.stickerIndexFile, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/qq.js:558
    mkdirSync(this.stickerDir, { recursive: true })
  • 发起网络请求
    lib/qq.js:575
    const response = await fetch(url)
  • 读取本地文件
    lib/qq.js:604
    if (existsSync(file)) writeFileSync(file, '')
  • 读取本地文件
    lib/qq.js:747
    try { const file = join(this.stickerDir, sticker.file); if (existsSync(file)) { writeFileSync(file, ''); } } catch { }
  • 发起网络请求
    lib/qq.js:876
    const response = await fetch(url)
  • 执行外部命令来自配置/变量
    lib/qq.js:973
    const stdout = execFileSync(argv[0], argv.slice(1), { encoding: 'utf8', timeout: 60000, windowsHide: true })
  • 写入本地文件
    lib/qq-bridge.js:70
    appendFileSync(this.debugFile, new Date().toISOString() + ' ' + parts.map((item) => typeof item === 'string' ? item : JSON.stringify(item)).join(' ')
  • 读取本地文件
    lib/qq-bridge.js:76
    if (!existsSync(this.file)) return { enabled: false, sessionId: '', approvalEnabled: false, approvalTimeoutMs: 120000, allowedPeers: [], peerSessions:
  • 读取本地文件
    lib/qq-bridge.js:77
    const parsed = JSON.parse(readFileSync(this.file, 'utf8').replace(/^ FEFF/, ''))
  • 读取本地文件
    lib/qq-bridge.js:109
    const disk = JSON.parse(readFileSync(this.file, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/qq-bridge.js:115
    writeFileSync(this.file, JSON.stringify(next, null, 2) + '\n', 'utf8')
  • 读取本地文件
    lib/qq-bridge.js:144
    const disk = JSON.parse(readFileSync(this.file, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/qq-bridge.js:178
    writeFileSync(this.file, JSON.stringify(this.config, null, 2) + '\n', 'utf8')
  • 读取本地文件
    lib/qq-bridge.js:343
    const bytes = readFileSync(path)
  • 写入本地文件
    lib/qq-bridge.js:509
    appendFileSync(this.outboxFile, JSON.stringify(entry) + '\n', 'utf8')
  • 读取本地文件
    lib/qq-bridge.js:517
    if (!existsSync(this.outboxFile)) return
  • 写入本地文件
    lib/qq-bridge.js:535
    writeFileSync(this.outboxFile, keep.length ? keep.join('\n') + '\n' : '', 'utf8')
  • 读取本地文件
    lib/recent.js:122
    const parsed = JSON.parse(readFileSync(this.file, 'utf8'))
  • 写入本地文件
    lib/recent.js:167
    mkdirSync(dirname(this.file), { recursive: true })
  • 写入本地文件
    lib/recent.js:169
    writeFileSync(temporary, JSON.stringify({ version: 1, savedAt: new Date().toISOString(), entries: this.entries }), 'utf8')
  • 写入本地文件
    lib/recent.js:170
    renameSync(temporary, this.file)
  • 读取本地文件
    lib/voiceprint.js:97
    if (!existsSync(this.file)) return {}
  • 读取本地文件
    lib/voiceprint.js:98
    const parsed = JSON.parse(readFileSync(this.file, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/voiceprint.js:122
    mkdirSync(this.root, { recursive: true })
  • 写入本地文件
    lib/voiceprint.js:130
    writeFileSync(this.file, JSON.stringify({ profiles }, null, 2) + '\n', 'utf8')
  • 读取本地文件
    lib/voiceprint.js:155
    modelReady: existsSync(this.config.modelPath),
  • 读取本地文件
    lib/voiceprint.js:156
    sherpaReady: existsSync(this.config.sherpaModulePath),
  • 读取本地文件
    lib/voiceprint.js:170
    if (!existsSync(this.config.sherpaModulePath)) throw new Error('没有本地声纹引擎:' + this.config.sherpaModulePath)
  • 读取本地文件
    lib/voiceprint.js:178
    if (!existsSync(this.config.modelPath)) throw new Error('没有声纹模型:' + this.config.modelPath)
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DeepSeek Harness 的角色扮演伴侣记忆插件:按会话保存人设文档、追踪好感度、检索最近 48 小时对话记录,还有按日期归档的本地知识库。

README

由 DeepSeek 最新模型翻译生成
伴侣记忆 · companion-memory 给 DeepSeek Harness 的角色扮演 / 伴侣型对话加一套常驻记忆与人格配置系统。 每个对话独立一套数据,互不干扰;新对话默认关闭,想用时一键开启。 这个插件解决什么 角色扮演聊久了,模型会忘设定、忘剧情、忘你说过的话;而普通聊天窗口既放不下长篇人设,也不该让 AI 每轮都翻旧账。这个插件在本地给每个对话配一套「人格文档 + 好感度 + 记忆 + 知识库」: - 人设随时改、立即生效:侧边栏里填写的三份文档,每次对话前现读,改完下一轮立刻按新设定说话。 - 重点原文一字不改:判定为重信息的原文只做「保存 / 调取」,系统不会改写或脑补。 - AI 自我演化:角色在长期相处中会自己慢慢长出新细节 —— 性格侧面、喜欢 / 讨厌的事物、行为习惯。由 AI 在剧情触发时自行增删(每条必须写理由),越攒越多,也会同步进系统提示词;只做加法,永远不推翻用户写死的三份核心文档。 - 聊天周期自动管理:按真实对话累计交互日,满 30 天自动归档;48 小时小归纳、每 30 个交互日月度打包、可自定义轮数的自动打包,全部写进可检索的本地知识库。 - 两段式检索:最近两个聊天日的事直接检索原始对话(热缓存),更早的事从知识库归档 / 归纳里查。所有计时都按「聊天时间」——只有真正聊过天的日子才推进,没聊天的日子不占用额度。
展开完整文档(README 剩余 1 节)
伴侣记忆 · companion-memory

给 DeepSeek Harness 的角色扮演 / 伴侣型对话加一套常驻记忆与人格配置系统。
每个对话独立一套数据,互不干扰;新对话默认关闭,想用时一键开启。

这个插件解决什么

角色扮演聊久了,模型会忘设定、忘剧情、忘你说过的话;而普通聊天窗口既放不下长篇人设,也不该让 AI 每轮都翻旧账。这个插件在本地给每个对话配一套「人格文档 + 好感度 + 记忆 + 知识库」:

- 人设随时改、立即生效:侧边栏里填写的三份文档,每次对话前现读,改完下一轮立刻按新设定说话。
- 重点原文一字不改:判定为重信息的原文只做「保存 / 调取」,系统不会改写或脑补。
- AI 自我演化:角色在长期相处中会自己慢慢长出新细节 —— 性格侧面、喜欢 / 讨厌的事物、行为习惯。由 AI 在剧情触发时自行增删(每条必须写理由),越攒越多,也会同步进系统提示词;只做加法,永远不推翻用户写死的三份核心文档。
- 聊天周期自动管理:按真实对话累计交互日,满 30 天自动归档;48 小时小归纳、每 30 个交互日月度打包、可自定义轮数的自动打包,全部写进可检索的本地知识库。
- 两段式检索:最近两个聊天日的事直接检索原始对话(热缓存),更早的事从知识库归档 / 归纳里查。所有计时都按「聊天时间」——只有真正聊过天的日子才推进,没聊天的日子不占用额度。
- 多人 / 群聊预留:一个对话里出现多个人时,AI 自己判断现在是谁在说话,对每个人的好感度分开记录;记忆(剧情)大家共享。单人对聊不受影响,行为与以前完全一样。
- 说话简短、像真人:默认注入「像真人在聊天软件里发消息」的说话方式——1~3 个短句、口语化、不用 Markdown 标题/列表/客服腔、不每轮长篇复述;可按需改成 normal / detailed / off。
- 长对话不超限:上下文接近模型窗口时,较早的对话会自动压缩成「伴侣记忆式」记忆块(重要原文 + 次要摘要 + 好感度变化 + 未完成事项),写进本地知识库并替换旧对话;原文仍留在本地日志里,需要时可检索,长聊可以一直继续。
- 按对话隔离:每个对话各自一套人设、好感度、记忆与知识库;关闭只停止注入,数据保留不删。

最近更新(1.0.0)

- 好感度满了还能继续涨「羁绊值」(无上限):超过 ±100 的部分自动转羁绊;满值后继续记录也会 +1,面板可手动 +1/−1;
- 导入聊天原文加了保护:最多 500 条、单条 6000 字,预览显示体量,回执报告跳过条数;
- 修复旧版清理逻辑导致的会话加载失败(system/message does not match an open turn and step);
- 新增 ROADMAP.md 与《多级记忆压缩设计》(年度摘要 + 滚动跨年沉积,关键记忆原样保留)。

完整记录见 CHANGELOG.md。

界面预览

| 配置:固定说明 + 分字段填空 | 按对话开启 / 关闭 |
| 配置 | 关闭引导 |

| 记忆:好感度与活跃记忆 | 知识库:48h 热上下文 / 自动打包 |
| 记忆 | 知识库 |

| AI 自我演化:四类细节可增删,AI 与用户都能维护 |
| 自我演化 |

总览页(只读):核对 AI 到底看到了什么

总览

功能

六个页面

1. 配置:三份角色文档 —— 角色性格.md、允许&禁止行为.md、预设发言例句.md。说明文字固定只读,按固定字段逐项填空(基础性格 / 三观 / 说话语调 / 思考方式 / 喜好 / 雷区;允许 / 禁止 / 行为边界;日常问候 / 表达关心 / 表达不满 / 口头禅),每份末尾另有「其他补充」自由填写框。保存后下一轮对话立刻生效,优先级高于一切记忆。默认还允许 AI 在剧情需要或你明确要求时小步修改「角色性格」和「预设发言例句」(每次必须写原因,改动自动归档到知识库,你随时可以改回来;「允许&禁止行为」默认只有你能改)。
2. 记忆:全局活跃记忆(重要原文不改、上限 30 个聊天交互日)、好感度(角色 / AI 对当前说话人的好感度,-100 ~ 100,多人对话时按人分开,变更必须写理由并归档;满 ±100 后继续加深的部分记入羁绊值,无上限、可正可负)、AI 自我演化(性格细节 / 喜欢的事物 / 讨厌回避的事物 / 行为习惯四类,每类上限 12 条;AI 在剧情触发时自行增删并写明理由,用户也可以在面板里手动补充或删除,所有条目都会同步进系统提示词)。
3. 知识库:按日期归档的本地文档 + 每 30 个交互日自动生成的月度打包文档 + 可自定义间隔的自动打包文档,支持关键词检索与原文查看;页面顶部显示「最近两个聊天日上下文」状态与「自动打包」设置(每 N 轮一次,0 = 关闭)。
4. 状态:交互日计数、48 小时(聊天时间)归纳到期状态、对话月周期进度、在场人物与各自好感度、说话方式、数据目录与统计。
5. 总览:只读。把注入给 AI 的每一块内容分别列成独立框,末尾给出「整体」完整系统提示词,可展开 / 复制全文,方便核对 AI 到底看到了什么。
6. 备份:把当前对话的整套记忆(人格文档、好感度与变动历史、活跃记忆、知识库归档/月度/自动打包、交互日与设置)导出为一个可读的 JSON 文档(如 伴侣记忆_对话_2026-10-29.json);换对话或重装后导入即可完整接上。导入前会显示摘要预览,可选「合并到当前记忆」或完全覆盖。

记忆页每条活跃记忆都带操作按钮:置顶 / 取消置顶、上移、下移、归档、删除;好感度卡片支持 撤销最近一次变动(把数值恢复到上一次记录之前)。

Host 侧自动行为

- 只对已开启的对话注入系统提示词(三份人设文档、AI 自我演化条目、全局活跃记忆、好感度、最近上下文状态与工具协议);未开启的对话完全不受影响;
- 自动缓存该对话最近两个聊天日的原始对话(用户与角色原话),供模型 search_recent / read_recent 检索;超出后自动清理,只保留知识库归档与归纳;
- 按「当天是否产生有效对话」累计该对话的聊天交互日(不聊天的日子不计数);
- 活跃记忆满 30 个交互日自动移入知识库(原文不变);
- 每满 30 个聊天交互日自动生成 对话月度归档_第N周期_起始YYYY-MM-DD.md;
- 按面板设置的轮数(默认 5,可设 0 关闭):每累计 N 轮真实对话,自动把区间内的重点原文(原样)、次要摘要、48 小时归纳、好感度变更整理成 packs/对话自动打包_第N次_起始…_截止….md,并写入知识库供检索;不修改任何原有归档;
- 通过 companion_memory 工具让模型记录每轮对话、保存重点原文、更新好感度、检索上下文与知识库、提交 48 小时归纳,以及用 evolve 动作让角色慢慢演化自己的性格细节 / 喜好 / 厌恶 / 习惯(每次最多 2~3 条、必须写理由,只做加法不推翻核心人设);未开启的对话调用会被拒绝;
- 每轮强制记忆提醒:系统提示词最末尾单独一段提醒「回复前先调用 turn_end」,并说明历史里「未开启伴侣记忆」的旧报错不代表现在不可用(避免模型因为一次旧报错而放弃调用);
- 被动记忆记录(Auto turn_end):如果模型某轮漏掉 turn_end,Host 侧会自动补记——
· passiveTurnEnd(默认开):用一次轻量旁路模型调用提取本轮的「重点原文 + 次要摘要 + 好感度变化」,像工具调用一样写入记忆、好感度与知识库;
· 旁路调用失败时退化为一条流水摘要兜底(autoTurnRecord),保证交互日、轮次统计、自动打包计数不断档;
· 模型自己调用过时不会重复记录;可用 passiveTurnEnd: false / autoTurnRecord: false 关闭;
- AI 可小步修改配置文档(edit_doc):默认允许改「角色性格」「预设发言例句」,不允许改「允许&禁止行为」;每次必须写理由,修改会归档进知识库;只做补充和渐变,不推翻用户写死的核心设定,用户手动修改永远优先;可用 aiEditDocs: false 整组关闭;
- 对话上下文压缩:每轮请求前估算当前上下文压力,接近模型窗口上限时,把最旧且「工具调用配对完整」的一段对话交给模型压成「重要原文 + 次要摘要 + 好感度变化 + 未完成事项」的记忆块,写进 packs/上下文压缩_第N次_YYYY-MM-DD.md,再用这条记忆块替换那段旧对话 —— 旧消息从此不再进入上下文,长聊不会撞上窗口上限。遇到上下文溢出错误(CONTEXT_WINDOW_EXCEEDED)时会强制压缩一次并重试,作为兜底;压缩请求失败或在压缩期间 surface 变化时会安全放弃,不影响当前对话;
- 记住最近活跃的对话,面板打开时自动跟随;「切换对话」浮层按工作区(项目)分组,分组可点击折叠 / 展开,只列出真实对话(未聊过的空会话不显示),点选即可切换。

两段式检索

| 时间范围 | 数据位置 | 工具动作 | 内容 |
| 最近 2 个聊天日(聊天时间) | recent.json(自动缓存) | search_recent / read_recent | 对话原话,可直接引用 |
| 超出最近 2 个聊天日 | archive/、monthly/、packs/ | search_kb / read_archive | 归档记录、48 小时归纳、月度打包、自动打包、上下文压缩记忆 |

「聊天时间」口径:时间只按真正聊过天的自然日累计,每天算 24 小时;某天没聊天不会消耗额度。例如今天和昨天聊过,之后间隔一周没聊,回来时那两天的原话仍然可以检索。

上下文压缩(长对话不超限)

长对话里最早的历史最占 token,却往往只需要记住重点。开启伴侣记忆的对话会在每次请求前检查上下文压力:一旦超过可用窗口的阈值(默认 50%),就自动压缩最旧的一段:
1. 选出最旧、工具调用配对完整的一段区间,保留最近原文(默认 16000 tokens)和最后一条用户消息;
2. 用当前模型把这段区间压成伴侣记忆格式:重要原文(原样保留)|次要摘要|好感度变化|未完成的事;
3. 写进知识库 packs/上下文压缩_第N次_YYYY-MM-DD.md(带归档日期,可用 search_kb / read_archive 检索);
4. 在会话 surface 上用这条记忆块替换旧区间 —— 旧消息不再送给模型,摘要之后的对话照常继续。

压缩请求失败、摘要不比原文小、或压缩期间会话发生变化时,会安全放弃本次压缩(下一轮再试);上下文溢出报错(CONTEXT_WINDOW_EXCEEDED)后会强制压缩一次并重试该请求。面板「状态」页显示已压缩次数与累计节省估算。

可在 profile 补丁里调整(全部可选):

- id: companion-memory
config:
contextCompression:
enabled: true # 关闭 = 只做记忆,不做上下文压缩 thresholdRatio: 0.5 # 占「窗口 - 输出预留」的比例,到阈值就压缩 retainTokens: 16000 # 最近至少保留多少 token 原文 minCompressTokens: 16000 # 单次至少压缩多少 token 才值得做 summaryMaxTokens: 4096 # 摘要输出上限 summaryReasoningEffort: low # 摘要推理强度:low / high / max,留空不指定 cooldownMs: 180000 # 两次自动压缩的最小间隔(毫秒) overflowRecovery: true # 上下文溢出后强制压缩并重试

压缩只作用于「已开启伴侣记忆」的对话;未开启的对话完全不受影响。原始对话仍保存在本地会话日志中(只是不再进入模型上下文),压缩记忆随时可在知识库页查看。

多人 / 群聊(按说话人分开好感度)

一个对话里可以出现多个人(几个人轮流用同一个对话、角色扮演里有多个角色、或者以后接入真正的群聊插件)。插件把每个人当独立的「说话人」处理:

- 好感度按人分开:角色对小明和对小红各有一套 -100 ~ 100 的好感度,互不影响;语气和态度按「当前说话人」的好感度调整。
- AI 自己判断是谁在说话:提示词里会给出「在场人物」清单和识别规则——单人对聊时默认就是对方,不要乱切换;消息明显来自另一个人(自报姓名、落款、@ 某人、群聊口吻、身份/关系不同)时,先调用 companion_memory({action:"speaker", name:"名字", reason:"判断理由"}) 登记并切换,再继续回复;拿不准时用角色口吻问一句。
- 消息前缀自动识别:消息以 [名字]、【名字】 或「已知名字:」开头时,Host 侧会自动切到那个说话人(只对已登记的名字生效,避免误判)。
- 记忆是共享的:角色记得大家说过的话(剧情/约定/关键原文),但每条记忆会记下「来自谁」,多人对话里注入时会显示来源。
- 面板可以看和改:状态页「在场人物」列出每个人的好感度;记忆页好感度卡片会显示当前说话人,并给出人物切换按钮;AI/用户都能用 speaker 动作登记或切换(name / aliases / note / switchTo)。
- 旧数据自动迁移:以前单人对话的好感度会自动成为默认说话人「用户」的好感度,不用手动处理;单人对话不会出现多余的人物。

群聊插件本身还没做(需要真正的多人接入和发言人标记);目前是「同一个对话里的多人轮流说话」形态,等以后有群聊插件可以直接对接这套说话人机制。

AI 也可以改配置文档(edit_doc)

默认情况下,配置页的三份文档 AI 只能读、不能改。但角色在长期剧情里可能会自然变化(例如经历重大事件、和用户相处很久之后变得更主动),这时候只加「AI 自我演化」条目有时不够——所以插件支持让 AI 小步修改配置文档本身:

- 工具动作:companion_memory({action:"edit_doc", key:"personality"|"examples"|"rules", fields:{...} 或 content:"...", reason:"为什么改"});
- 默认允许:personality(角色性格)、examples(预设发言例句);
- 默认禁止:rules(允许&禁止行为)——安全底线只由用户本人改;
- 每次修改必须写 reason,改动会以「AI 修改配置文档」类型写进知识库归档,用户在配置页随时可以改回来;
- 提示词里会明确告诉 AI:只做补充和渐变、不推翻用户写的核心设定、用户手动修改永远优先、不要每轮都改;
- 全部可以关掉:在 profile 补丁里设置 aiEditDocs: false(或只关某一类):

- id: companion-memory
config:
aiEditDocs:
personality: true # 角色性格:AI 可小步修改 examples: true # 预设发言例句:AI 可小步修改 rules: false # 允许&禁止行为:仅用户可改(建议保持 false)

如果你完全不想让 AI 动配置文档:aiEditDocs: false。用户手写的内容仍是最高优先级,AI 的修改只是「在用户设定之上做补充」。

说话方式(简短、像真人)

默认给 AI 注入一段「说话方式」约束,让它像真人在聊天软件里发消息,而不是写文档:

- 默认 1~3 个短句就够,能一句回完就一句;对方发得短你也回得短;
- 不用 Markdown 标题 / 列表 / 编号 / 表格 / 加粗 / 引用块,也不用「总结/建议」这种结构;
- 不用客服腔和书面语("好的""首先""综上所述"),用口语和语气词;
- 不每轮复述剧情、不写一段内心独白、不一口气发一大段;结尾最多自然问一句;
- 只有用户明确要求「详细说明/列一下/写出来」时才展开成长回答。

可在 profile 补丁里调整:

- id: companion-memory
config:
replyStyle:
mode: brief # brief 简短像真人(默认)| normal 自然长度| detailed 按需详细| off 不注入 maxSentences: 3 # brief/normal 的短句上限参考

面板「状态」页会显示当前说话方式;如果角色开始长篇大论,先检查这里是不是被改成了 normal/detailed/off。

安装

方式一:从 GitHub 安装(推荐分享)

在 DSH 插件市场选择「从链接安装」,填入 Release 资产直链:

https://github.com/Root-7-KHX/companion-memory/releases/download/v0.6.2/companion-memory-0.6.2.tgz

或命令行:

dsh plugin add https://github.com/Root-7-KHX/companion-memory/releases/download/v0.6.2/companion-memory-0.6.2.tgz

方式二:本地目录安装

1. 下载本仓库(Code → Download ZIP 并解压,或 git clone);
2. 在 DSH 中让插件管理器安装该目录(plugin_manager → install_bundle → 指向解压出来的 companion-memory/ 目录);
3. 刷新 Web GUI 页面,侧边栏出现「伴侣记忆」图标。

方式三:解压即用

把 companion-memory-0.6.2.zip 解压到固定目录,指向该目录安装即可(无需构建、无第三方依赖、纯 ESM)。

使用

1. 打开左侧「伴侣记忆」面板 —— 顶部「当前对话」栏会显示当前对话与开启状态;
2. 点击「开启本对话」:这个对话从此拥有独立的一套数据;
3. 到「配置」页填写角色性格、允许与禁止的行为、常用例句(自动保存,下一轮生效);
4. 正常聊天即可:AI 每轮结束会记录重点原文、次要摘要并评估好感度;长期相处中还会自己慢慢演化人设细节(性格侧面 / 喜欢 / 讨厌 / 习惯),每轮最多几条并写明理由;
5. 想核实时去「总览」页看 AI 收到的完整内容;想回忆旧事时直接问,AI 会先查 48 小时热缓存,再查知识库;
6. 长对话变长后不用管:上下文接近窗口上限时会自动压缩更早的对话(见上文「上下文压缩」),「状态」页能看到压缩次数与累计节省估算。
7. 多人对话(几个人轮流用这个对话):直接聊就行,她会在有人自报姓名/明显换人时登记新人物并分开记好感度;也可以叫她「记住小明是我朋友」。状态页和记忆页能看到每个人的好感度。
8. 想让她自己调整角色设定:直接说「把性格里的‘有点社恐’改成‘慢热但熟了话多’」「口头禅换成 Z」——她会用 edit_doc 小步修改并写原因(默认只允许改「角色性格」和「预设发言例句」,「允许&禁止行为」仍只有你能改);改动会归档到知识库,你随时能在配置页改回来。

新对话默认关闭 —— 每个对话单独点一下开启即可;关掉只停止注入,已有数据不会删除。

数据位置

默认根目录 $DSH_HOME/companion-memory(回退 ~/.dsh/companion-memory):

companion-memory/
├── sessions.json # 哪些对话已开启、最近活跃对话 ├── sessions/<对话 id>/ # 每个对话独立一套 │ ├── 角色性格.md │ ├── 允许&禁止行为.md │ ├── 预设发言例句.md │ ├── state.json # 好感度(按说话人分开)、AI 自我演化、活跃记忆、交互日、周期与打包设置 │ ├── recent.json # 最近 48 小时原始对话 │ ├── archive/YYYY-MM-DD.md # 按日期归档 │ ├── monthly/对话月度归档_第N周期_起始YYYY-MM-DD.md │ └── packs/ │ ├── 对话自动打包_第N次_起始YYYY-MM-DD_截止YYYY-MM-DD.md │ └── 上下文压缩_第N次_YYYY-MM-DD.md # 长对话的压缩记忆(早期对话的替身) └── (旧版单套数据:首次启用某个对话时自动迁入该对话目录)

所有归档条目以 归档日期:YYYY-MM-DD HH:MM 开头,按日期分组;AI 修改配置文档也会以「类型:AI 修改配置文档」记进归档。数据全部保存在本地,不上传任何服务器。

常见问题

下面是安装、升级与日常使用中最常见的问题。

Q:装了插件,侧边栏却没有「伴侣记忆」,或者面板突然消失? 答:先检查 package.json 是否带 UTF-8 BOM。DSH 解析带 BOM 的插件清单会失败,常见报错是 Unexpected token ''(引号内是 BOM 字符);用编辑器另存为 UTF-8 无 BOM,再到插件管理里把本插件的 bundle 关掉再打开即可。

Q:打开旧对话提示会话历史加载失败(session corrupt)? 答:这是旧版 0.7.0 的 dynamic-context 在 turn/step 之外写 surface replace 导致的,典型报错为 system/message does not match an open turn and step。1.0.0 已停用这段逻辑并修复会话日志;升级后仍复现,可把会话日志目录下的 .bak- 备份与 raw log 路径附到 issue 里。

Q:长对话越聊越贵、token 暴涨怎么办? 答:1.0.0 已把系统提示词拆成静态 / 动态两部分,伴侣会话工具从 74 个精简到 25 个,旧原文默认不加载、只在需要时检索。如果还是觉得贵,先在面板里关掉不用的会话的伴侣记忆;关闭只停止注入,不会删除数据。

Q:好感度到 100 后怎么还能涨? 答:±100 是好感度上限,不是关系上限;满值后继续加深的部分会记入「羁绊值」,无上限、可正可负。也可以在面板里手动 +1 / −1 微调。

Q:卸载或更新插件,数据会丢吗? 答:不会。数据独立保存在 ~/.dsh/companion-memory(或 $DSH_HOME/companion-memory),卸载、更新插件都不会删除。每个会话一套目录,真要删某个会话的记忆,删除 companion-memory\sessions\<对应目录> 即可;删前建议先导出备份。

Q:在 QQ 里也能用伴侣记忆吗? 答:可以,目前走桥接方案:把 QQ 发言人映射成伴侣记忆的「说话人」。同一角色在 QQ 和 DSH 里共享人格文档与同一套好感度体系,但对话内容和数据分别存储。

Q:导入后记忆没有变多,或者出现了重复? 答:导入会做机械去重,完全相同的内容不会重复入库;它不做语义合并,所以相近但不相同的条目可能同时保留,看起来就像「没变多」。语义整理已列入 1.1.0 计划。

迁移说明

- 0.6.x → 0.7.x → 1.0.0:数据格式由 store normalize 自动迁移,无需手动改文件,升级后直接使用即可。
- 旧对话显示「未开启伴侣记忆」:数据还在,只是该对话的开关没打开;到侧边栏「伴侣记忆」面板点「开启本对话」即可恢复注入。
- 换电脑 / 换工作区:在面板「备份」页导出 JSON 包,到新环境再导入;包内含人格文档、好感度与变动历史、活跃记忆、知识库与 state。聊天原文可以选择是否一起导出,导入时可选「合并导入」或「覆盖导入」。
- 升级 DSH 本体:不影响插件数据;数据始终在 ~/.dsh/companion-memory(或 $DSH_HOME/companion-memory),不在 DSH 安装目录里。
- 插件更新被覆盖 / 改了代码不生效:本机通过 profile 的 pnpm link 安装(@local/companion-memory → link:D:/AI/插件/companion-memory),更新仓库代码后,在插件管理里把 bundle 关掉再打开一次即可,无需重新安装。

开发与结构

companion-memory/
├── index.js # Host 入口:注册提示词段、工具、事件追踪、HTTP API ├── client.js # Client 半:侧边栏图标 + 主面板(六个页面,轮询状态) ├── lib/ │ ├── manager.js # 按会话管理数据目录、开启注册表、会话列表、旧数据迁移 │ ├── store.js # 单会话数据层:文档、记忆、好感度(按说话人)、自我演化、归档、周期、打包 │ ├── recent.js # 48 小时热上下文缓存与检索 │ ├── prompt.js # 系统提示词分段渲染(按对话隔离注入;含在场人物/说话人识别、AI 可改文档说明) │ ├── tool.js # companion_memory 工具(转发到对应会话的 store;含 speaker / edit_doc 动作) │ ├── tracking.js # 会话事件 → 交互日计数 / 热缓存 / 自动打包 / 说话人前缀识别 │ ├── context-compress.js # 对话上下文压缩:压力检测 → 摘要 → 知识库 → 替换旧对话 │ └── api.js # 同源 HTTP API(/companion/api/) ├── locale/ # 中英双语文案 ├── cordis.patch.yml # 插件加载补丁 └── docs/screenshots/ # README 图片

插件通过 Host HTTP 路由与 Client 面板通信,数据与状态全部落在本地文件;Host 侧在 agent/created 时按对话注册系统提示词段,关闭的对话不注入任何内容。

在 GitHub Codespaces 里改代码(免装 Git)

仓库自带 .devcontainer 配置,所以可以直接在浏览器里改代码:

1. 仓库页点绿色的 Code 按钮 → Codespaces 标签 → Create codespace on main;
2. 等几十秒,浏览器里打开一个 VS Code(已装好 Node 22);
3. 改完文件,在左侧 Source Control(Ctrl+Shift+G)填写提交信息 → Commit & Push;
4. 也可以在终端跑校验:node --check index.js && node --check client.js。

限制:Codespaces 只是云端编辑器/终端,跑不了 DSH 本体。插件的实际界面与数据验证仍需在本地 DSH 里做;它适合改代码、改文档、打包和发版。

本地校验

语法检查(Node ≥ 22;纯 ESM,无需安装依赖)
node --check index.js
node --check client.js
node --check lib/manager.js
node --check lib/context-compress.js

参与贡献

这个项目是公开的,欢迎任何人一起完善(改代码、提建议、修文档都可以):

- 📋 提 Bug / 建议:Issues(有对应模板)
- 🔧 帮忙改代码:Fork 本仓库 → 新建分支 → 提交 PR(流程与自检清单见 CONTRIBUTING.md)
- 🌐 参与方式不限身份:公开仓库下,任何 GitHub 用户都可以 fork、提 Issue、提 PR;如果你希望某个朋友直接改仓库,可以把他加成 Collaborator(仓库 Settings → Collaborators → Add people)
- 🧩 本地开发:node --check 自检 + 安装到 DSH 后按 CONTRIBUTING.md 的冒烟清单走一遍

不接收任何用户对话数据;Issue 里请勿粘贴真实聊天隐私。

License

MIT © 2026 Root-7-KHX
预览分享卡片长什么样 ↓Root-7-KHX/companion-memory
分享 · DeepSeek Harness Hub
Root-7-KHX/companion-memory

为角色扮演对话保存人设、好感度与长期记忆

回到顶部