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

心绪人格 chisato-lycoreco/dsh-serene

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

让角色记住你,情绪随时间与事件自然演变

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

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

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

状态速览

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

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

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

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

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 78 处,展示前 73 条)
  • 读取本地文件
    src/persona.js:47
    stat = statSync(path);
  • 读取本地文件
    src/persona.js:56
    raw = JSON.parse(readFileSync(file, "utf8"));
  • 读取本地文件
    src/persona.js:119
    return statSync(path).isDirectory();
  • 读取本地文件
    src/persona.js:127
    return statSync(path).isFile();
  • 读取本地文件
    src/persona.js:135
    return readFileSync(path, "utf8").trim();
  • 读取本地文件
    src/tools.js:657
    `- 情感状态:${existsSync(stateFile) ? `${stateFile}(已生成)` : "尚未生成,第一次记事件时创建"}`,
  • 读取本地文件
    src/opening.js:34
    raw = JSON.parse(readFileSync(this.#file, "utf8"));
  • 读取本地文件
    src/opening.js:77
    if (!existsSync(this.#dir)) mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/opening.js:77
    if (!existsSync(this.#dir)) mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/opening.js:79
    writeFileSync(temp, `${JSON.stringify(this.#state, null, 2)}\n`, "utf8");
  • 写入本地文件
    src/opening.js:80
    renameSync(temp, this.#file);
  • 读取本地文件
    src/stores/emotion-store.js:77
    raw = JSON.parse(readFileSync(join(this.#dir, this.#file), "utf8"));
  • 写入本地文件
    src/stores/emotion-store.js:222
    mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/stores/emotion-store.js:225
    writeFileSync(temp, `${JSON.stringify(this.#state, null, 2)}\n`, "utf8");
  • 写入本地文件
    src/stores/emotion-store.js:226
    renameSync(temp, target);
  • 读取本地文件
    src/stores/emotion-store.js:236
    const raw = JSON.parse(readFileSync(join(this.#dir, this.#woundsFile), "utf8"));
  • 读取本地文件
    src/stores/crystal-store.js:68
    lessons: existsSync(join(this.dir, LESSONS_FILE)),
  • 读取本地文件
    src/stores/crystal-store.js:69
    dispositions: existsSync(join(this.dir, DISPOSITIONS_FILE)),
  • 读取本地文件
    src/stores/crystal-store.js:98
    if (!existsSync(path)) return [];
  • 读取本地文件
    src/stores/crystal-store.js:101
    text = readFileSync(path, "utf8");
  • 读取本地文件
    src/stores/crystal-store.js:122
    if (!existsSync(path)) return [];
  • 读取本地文件
    src/stores/crystal-store.js:125
    book = JSON.parse(readFileSync(path, "utf8"));
  • 读取本地文件
    src/stores/memory-store.js:74
    if (!existsSync(path)) {
  • 读取本地文件
    src/stores/memory-store.js:78
    const raw = readFileSync(path, "utf8");
  • 写入本地文件
    src/stores/memory-store.js:130
    mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/stores/memory-store.js:131
    appendFileSync(this.file, `${JSON.stringify(memory)}\n`, "utf8");
  • 写入本地文件
    src/stores/memory-store.js:161
    appendFileSync(this.file, `${JSON.stringify({ type: "reinforce", ...patch })}\n`, "utf8");
  • 写入本地文件
    src/stores/memory-store.js:229
    appendFileSync(this.file, `${JSON.stringify(record)}\n`, "utf8");
  • 读取本地文件
    src/stores/motivation-store.js:44
    raw = JSON.parse(readFileSync(this.file, "utf8"));
  • 写入本地文件
    src/stores/motivation-store.js:116
    mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/stores/motivation-store.js:118
    writeFileSync(temp, `${JSON.stringify(this.#state, null, 2)}\n`, "utf8");
  • 写入本地文件
    src/stores/motivation-store.js:119
    renameSync(temp, this.file);
  • 读取本地文件
    src/stores/doc-store.js:25
    raw = existsSync(this.path) ? JSON.parse(readFileSync(this.path, "utf8")) : null;
  • 读取本地文件
    src/stores/doc-store.js:25
    raw = existsSync(this.path) ? JSON.parse(readFileSync(this.path, "utf8")) : null;
  • 写入本地文件
    src/stores/doc-store.js:49
    mkdirSync(dirname(this.path), { recursive: true });
  • 写入本地文件
    src/stores/doc-store.js:51
    writeFileSync(temp, `${JSON.stringify(this.state, null, 2)}\n`, "utf8");
  • 写入本地文件
    src/stores/doc-store.js:52
    renameSync(temp, this.path);
  • 读取本地文件
    src/stores/leisure-store.js:59
    raw = existsSync(this.file) ? readFileSync(this.file, "utf8") : "";
  • 读取本地文件
    src/stores/leisure-store.js:59
    raw = existsSync(this.file) ? readFileSync(this.file, "utf8") : "";
  • 写入本地文件
    src/stores/leisure-store.js:111
    mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/stores/leisure-store.js:112
    appendFileSync(this.file, `${JSON.stringify(entry)}\n`, "utf8");
  • 写入本地文件
    src/stores/leisure-store.js:158
    mkdirSync(this.#dir, { recursive: true });
  • 写入本地文件
    src/stores/leisure-store.js:161
    writeFileSync(temp, text ? `${text}\n` : "", "utf8");
  • 写入本地文件
    src/stores/leisure-store.js:162
    renameSync(temp, this.file);
  • 读取本地文件
    web/lib/config.mjs:79
    raw = JSON.parse(readFileSync(configFile(base), "utf8"));
  • 写入本地文件
    web/lib/config.mjs:89
    mkdirSync(dirname(file), { recursive: true });
  • 写入本地文件
    web/lib/config.mjs:91
    writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
  • 写入本地文件
    web/lib/config.mjs:92
    renameSync(temp, file);
  • 读取本地文件
    web/lib/config.mjs:144
    if (!existsSync(file)) return null;
  • 读取本地文件
    web/lib/config.mjs:145
    const match = readFileSync(file, "utf8").match(pattern);
  • 写入本地文件
    web/lib/runtime.mjs:116
    mkdirSync(this.dataDir, { recursive: true });
  • 写入本地文件
    web/lib/runtime.mjs:683
    appendFileSync(join(this.dataDir, "chat.jsonl"), `${JSON.stringify(entry)}\n`, "utf8");
  • 读取本地文件
    web/lib/runtime.mjs:692
    if (!existsSync(file)) return [];
  • 读取本地文件
    web/lib/runtime.mjs:694
    for (const line of readFileSync(file, "utf8").split("\n")) {
  • 写入本地文件
    web/lib/runtime.mjs:758
    appendFileSync(join(this.dataDir, "traces.jsonl"), `${JSON.stringify(trace)}\n`, "utf8");
  • 读取本地文件
    web/lib/runtime.mjs:767
    if (!existsSync(file)) return;
  • 读取本地文件
    web/lib/runtime.mjs:769
    const lines = readFileSync(file, "utf8").trim().split("\n").slice(-60);
  • 写入本地文件
    web/lib/runtime.mjs:785
    mkdirSync(dir, { recursive: true });
  • 读取本地文件
    web/lib/runtime.mjs:806
    if (!existsSync(from)) continue;
  • 写入本地文件
    web/lib/runtime.mjs:809
    renameSync(from, to);
  • 发起网络请求
    web/public/app.js:946
    const response = await fetch(path, {
  • 发起网络请求
    web/public/watch.js:91
    const response = await fetch(path, { credentials: "same-origin", ...options });
  • 删除本地文件递归
    web/server.mjs:55
    rmSync(baseDir, { recursive: true, force: true });
  • 读取本地文件
    web/server.mjs:59
    if (demo && config.persona === "zhiyao" && !existsSync(join(packageRoot, "personas", "zhiyao", "persona.json"))) {
  • 读取本地文件
    web/server.mjs:80
    if (demo && !existsSync(join(runtime.dataDir, "chat.jsonl")) && runtime.memory.size === 0) {
  • 发起网络请求
    web/server.mjs:305
    const response = await fetch(`${String(baseUrl).replace(/ +$/, "")}/models`, {
  • 读取本地文件
    web/server.mjs:413
    for (const name of readdirSync(root)) {
  • 读取本地文件
    web/server.mjs:416
    if (statSync(dir).isDirectory() && existsSync(join(dir, "persona.json"))) found.push(readPersonaCard(dir, name));
  • 读取本地文件
    web/server.mjs:416
    if (statSync(dir).isDirectory() && existsSync(join(dir, "persona.json"))) found.push(readPersonaCard(dir, name));
  • 读取本地文件
    web/server.mjs:423
    const raw = JSON.parse(readFileSync(file, "utf8"));
  • 执行外部命令固定命令
    web/server.mjs:578
    spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
  • 执行外部命令固定命令
    web/server.mjs:580
    spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
  • 执行外部命令固定命令
    web/server.mjs:582
    spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/dsh-tools
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

人格、情绪与记忆,给 DeepSeek Harness 用。人格是数据,情绪是状态,记忆是文件——把「一个会记得你的角色」接进 DSH。

README

由 DeepSeek 最新模型翻译生成
dsh-serene 把「一个会记得你的角色」接进 DeepSeek Harness。 人格是数据,情绪是状态,记忆是文件——三者都可读、可改、可复现。 大多数「角色卡」是一段写在 system prompt 里的描述:模型每次都从零开始扮演,聊得越久越不像。 这个插件换一种做法:把角色拆成可计算的状态,让「她此刻怎么样」由时间、事件和历史决定,而不是每次重新即兴。 - 情绪:PAD+ 六维 × 三层(底色 / 此刻 / 关系),会衰减、有昼夜节律、会留疤; - 记忆:jsonl 落盘,按相关度 + 时间衰减 + 重要度召回,公式可复算; - 人格:一份 JSON,换人格不用改代码。 它是 FRAMEWORK.md 里那套七层结构的最小参考实现——只有织法,没有布:不包含任何私人角色、私人记忆、屏幕采集或图像生成。 十天,一个人的情绪怎么走:底色、此刻、关系三条线,以及第 3 天留下的那道疤 上面这张图不是手画的:node demo/timeline.mjs 用 core/emotion.js 的纯函数跑十天, node demo/render.mjs 把结果画成帧。改一行事件,整条曲线和那道疤都会跟着变——这就是「可复算」的意思。
展开完整文档(README 剩余 1 节)
dsh-serene

把「一个会记得你的角色」接进 DeepSeek Harness。
人格是数据,情绪是状态,记忆是文件——三者都可读、可改、可复现。

大多数「角色卡」是一段写在 system prompt 里的描述:模型每次都从零开始扮演,聊得越久越不像。
这个插件换一种做法:把角色拆成可计算的状态,让「她此刻怎么样」由时间、事件和历史决定,而不是每次重新即兴。

- 情绪:PAD+ 六维 × 三层(底色 / 此刻 / 关系),会衰减、有昼夜节律、会留疤;
- 记忆:jsonl 落盘,按相关度 + 时间衰减 + 重要度召回,公式可复算;
- 人格:一份 JSON,换人格不用改代码。

它是 FRAMEWORK.md 里那套七层结构的最小参考实现——只有织法,没有布:不包含任何私人角色、私人记忆、屏幕采集或图像生成。

十天,一个人的情绪怎么走:底色、此刻、关系三条线,以及第 3 天留下的那道疤

上面这张图不是手画的:node demo/timeline.mjs 用 core/emotion.js 的纯函数跑十天, node demo/render.mjs 把结果画成帧。改一行事件,整条曲线和那道疤都会跟着变——这就是「可复算」的意思。

它做什么 / 不做什么

| 做 | 不做 |
| 记录事件、推进情绪、把状态讲成人话 | 不替模型写回复,不改写对话内容 |
| 长期记忆的写入与召回 | 不自己调模型、不自己总结 |
| 人格档案的读取与拼装 | 不读 .env、凭证、聊天记录原文 |
| 状态落盘(原子写)、可手改可进 git | 不联网、不上报、不采集屏幕 |

安装

要求:DSH >= 0.2.1-rc.2,Node >= 22.19。

从 npm 装(推荐)
dsh plugin --profile web add dsh-serene

从 GitHub 装(跟着 tag,不经过 npm)
dsh plugin --profile web add "github:chisato-lycoreco/dsh-serene#v0.1.1"

从本地目录装(开发时)
dsh plugin --profile web add "/absolute/path/to/dsh-serene"

从 tgz 装(去 Release 页面下 dsh-serene-0.1.1.tgz)
dsh plugin --profile web add ./dsh-serene-0.1.1.tgz

安装前先完全退出 DSH(安装会改 profile 的 package.json 和 cordis.patch.yml)。
装完用这条确认插件进了配置树:

dsh --profile web --dump-config | grep -A3 "dsh-serene"

不想装、只想试:用 patch 覆盖方式直接挂本地插件。

dsh --profile web --patch "/absolute/path/to/dsh-serene/cordis.patch.yml"

cordis.patch.yml 里的 config 是整段替换,不是深合并。你在自己 profile 里覆盖配置时,要把想保留的键一起写上。

第一次运行:凭据与自检

模型凭据只有两个来源:GUI 的 Models 页面写进 credentials service,或启动环境里的 DEEPSEEK_API_KEY。新 profile 不会自动继承——另建了一个 profile 时,第一句话就会报 MISSING_CREDENTIAL(这是凭据问题,不是插件问题):

export DEEPSEEK_API_KEY=sk-... # 或者先在 GUI 的 Models 页填一次

装完别只看配置文件。两步确认,第二步才是「真的能用」:

| 想确认 | 怎么做 | 正常时 |
| 配置进了配置树 | dsh --profile web --dump-config \| grep -A3 dsh-serene | 有 - id: serene |
| 插件真的挂上 | 在装着插件的那个 profile 里说一句「用 serene_status 看看」 | 返回人格、数据目录、记忆条数 |

这一步也是「插件正常」和「装好了但没生效」的唯一区分点:人格文件读不到时插件会降级(不拖垮宿主),但会留下 serene_status 和一行 stderr 说明原因。

配置

| 键 | 默认 | 说明 |
| enabled | true | 总开关 |
| persona | demo | personas/ 下的文件名,或绝对路径 |
| personaRoot | 插件自带 personas/ | 人格档案目录 |
| dataDir | $DSH_HOME/plugins/dsh-serene | 状态与记忆落盘位置 |
| recallLimit | 5 | 每次召回默认条数 |
| halfLifeDays | 45 | 记忆时间衰减半衰期 |

覆盖示例(~/.dsh/profiles/web/cordis.patch.yml):

- insert:
- id: serene
name: dsh-serene
config:
enabled: true
persona: xiaoman
personaRoot: /Users/me/serene-personas
dataDir: /Users/me/serene-data

工具

| 工具 | 作用 |
| serene_context | 人格卡 + 此刻状态。新会话开头调一次 |
| serene_mood | 只读此刻状态(自然语言,不含数值) |
| serene_event | 记一件事进情感层(30 种事件,见下) |
| serene_remember | 写一条长期记忆 |
| serene_recall | 按话题召回记忆,带时间和分数 |
| serene_state | 调试用:三层数值、最近事件、疤 |
| serene_status | 诊断:插件是否真的挂上(人格 / 数据目录 / 记忆条数);加载失败时返回原因和修法 |

事件类型:chat / affection / address / intimacy / kiss / tease / care / humor / share / question / goodnight / rejection / neglect / conflict / repair / scold / coax / jealousy / ignored / selfie / reflection / heavy_reflection / sleep / wake / leisure / reply / cold_read / warm_read / moved / disclose / suppress。

模型什么时候调用这些工具,由你在 AGENTS.md 或人格卡里约定;插件不劫持对话。

数据长什么样

$DSH_HOME/plugins/dsh-serene/emotion.json:

{
"version": 1,
"updatedAt": 1761288000000,
"lastInteractionAt": 1761287900000,
"layers": {
"background": { "pleasure": 63.1, "arousal": 43.8, "dominance": 59.9, "curiosity": 57.4, "confidence": 61.6, "connection": 60.4 },
"primary": { "pleasure": 68.2, "arousal": 47.1, "dominance": 60.3, "curiosity": 58.0, "confidence": 62.1, "connection": 61.9 },
"social": { "pleasure": 61.4, "arousal": 43.0, "dominance": 59.5, "curiosity": 57.8, "confidence": 61.2, "connection": 61.1 }
},
"wounds": [],
"history": [{ "kind": "affection", "at": 1761287900000, "intensity": 1 }]
}

memories.jsonl:一行一条,直接 grep 就能找。

两个文件都是你的:可以备份、可以手改、可以删。删掉就是从没发生过。

换一个人格

复制 personas/demo.json,改几个字段,文件名当 persona 传进去:

{
"name": "小满", "role": "住在终端里的同伴", "summary": "你不急着给建议。",
"traits": ["安静", "好奇"],
"speech": ["句子短", "不确定就问"],
"boundaries": ["不假装记得没记过的事"],
"lexicon": { "address": ["小满"] },
"baseline": { "background": { "pleasure": 62 } }
}

baseline 省略就用默认基线;lexicon.address 决定哪些称呼会被识别成「叫你」。

三条设计约定

1. 状态自己走。 时间过去,情绪就衰减、就随昼夜波动——不需要谁说话才动。
2. 情绪只影响语气,不改写事实。 状态进 prompt 的是一段人话(「这会儿有点低,别硬撑」),不是「你现在很生气」,更不会篡改检索到的记忆。
3. 一切可复现。 同样的初始状态 + 同样的事件 + 同样的时间戳,永远得到同样的结果;没有随机数、没有模型调用。测试里跑的就是这一点。

更完整的结构说明见 FRAMEWORK.md。

开发

npm install # 只装 devDependencies(schemastery / dsh-tools)
npm test # node --test,28 个用例

core/emotion.js 纯函数:衰减、节律、事件、疤、描述 core/recall.js 纯函数:分词、衰减、召回打分 src/stores/emotion-store.js 唯一碰磁盘的地方(原子写) src/stores/memory-store.js jsonl 追加与召回 src/persona.js 人格档案 → prompt 段 src/tools.js 7 个工具(含 serene_status 诊断) src/index.js name / inject / Config / apply

core/ 里没有 fs、没有 Date.now()、没有网络。想验证:

grep -n "node:fs\|process.env\|fetch(" core/*.js # 应该没有输出

与其它插件的关系

- 灵枢(@furongjun1999/dsh-memory):那是「工作记忆」,记的是任务和事实。
这个插件记的是「关系」,两者不要合并——工作记忆要准,关系记忆本来就会褪色。
- dsh-dafeiyu:桌宠形态的前端。这个插件是纯后端,没有 UI;想接桌宠,读 serene_state 的返回值自己画即可。

License

代码 MIT。人格档案(personas/)随你改成什么样都行,改完是你的人格,不是我的。
预览分享卡片长什么样 ↓chisato-lycoreco/dsh-serene
分享 · DeepSeek Harness Hub
chisato-lycoreco/dsh-serene

让角色记住你,情绪随时间与事件自然演变

回到顶部