feiyu-memory 0.0.0-stage → 0.3.6
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/CHANGELOG.md +559 -0
- package/LICENSE +36 -0
- package/README.md +536 -3
- package/SPEC.md +1380 -0
- package/cordis.patch.yml +62 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +62 -4
- package/src/client/index.js +484 -0
- package/src/core/archive.mjs +572 -0
- package/src/core/config.mjs +313 -0
- package/src/core/consolidate.mjs +759 -0
- package/src/core/gate.mjs +42 -0
- package/src/core/handnote.mjs +117 -0
- package/src/core/judge.mjs +116 -0
- package/src/core/keywords.mjs +121 -0
- package/src/core/persona.default.md +34 -0
- package/src/core/persona.mjs +594 -0
- package/src/core/privacy.mjs +252 -0
- package/src/core/recall.mjs +280 -0
- package/src/core/render-diary.mjs +171 -0
- package/src/core/timeline.mjs +127 -0
- package/src/core/tokenize.mjs +70 -0
- package/src/core/zip.mjs +239 -0
- package/src/host/index.mjs +2039 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# 肥鱼日记 —— 让新开一个对话,用户依然感觉是同一条鱼。
|
|
2
|
+
#
|
|
3
|
+
# ⚠️ 这个文件必须是**顶层 YAML 数组**,否则 loader 会直接抛错。
|
|
4
|
+
#
|
|
5
|
+
# `id` 同时也是设置项的命名空间(v0.2 的界面会用到),改它等于换一个插件身份。
|
|
6
|
+
# `name` 必须等于 package.json 的 name。
|
|
7
|
+
#
|
|
8
|
+
# 所有选项都可以在这里的 `config:` 里写。留空即用默认值。
|
|
9
|
+
# 注意:**patch 的 config 是整体替换、不深合并**,所以只写你想改的键是安全的
|
|
10
|
+
# (只有你自己这一行会读它;但如果你在 profile 里覆写这一行,就要把想保留的键写全)。
|
|
11
|
+
|
|
12
|
+
- insert:
|
|
13
|
+
- id: feiyu-memory
|
|
14
|
+
name: 'feiyu-memory'
|
|
15
|
+
config:
|
|
16
|
+
# 档案目录。留空 = ~/.dsh/feiyu-diary
|
|
17
|
+
# 想放到别处(比如同步盘)就写绝对路径。
|
|
18
|
+
dataDir: ''
|
|
19
|
+
# 总开关
|
|
20
|
+
enabled: true
|
|
21
|
+
# 把人格内核一起注入。装了 dsh-whale-persona 之类的人设插件就关掉它 ——
|
|
22
|
+
# 我们只负责"我们之间发生过什么",人设交给它们。
|
|
23
|
+
includePersona: true
|
|
24
|
+
# 注入预算(token)。成本封顶,与用了多久无关。
|
|
25
|
+
# 2000 是给"最近的事"留的位置 —— 日记要占大头,读起来才像日记。
|
|
26
|
+
budgetTokens: 2000
|
|
27
|
+
# 固化用模型。留空 = 跟随你当前的默认模型。
|
|
28
|
+
# 强烈建议就用便宜档:这一步是结构化抽取,用贵的反而会自由发挥、把档案写花。
|
|
29
|
+
model: ''
|
|
30
|
+
# 固化的思考档位:off / low / high / max。
|
|
31
|
+
#
|
|
32
|
+
# ⚠️ 保持 off。它**故意不跟随你聊天时选的档位** —— 这不是疏漏,是修过的 bug:
|
|
33
|
+
# 跟随的时候,你在桌面端选了 high,后台每次写日记也在疯狂思考,
|
|
34
|
+
# 900 个输出 token 全烧在推理上,正文一个字都没产出。
|
|
35
|
+
# 症状是"收到 600 个 chunk / 0 字符",请求正常、无报错、就是不记事。
|
|
36
|
+
#
|
|
37
|
+
# 结构化抽取不需要思考。要它思考就填 low,一般没必要。
|
|
38
|
+
reasoningEffort: off
|
|
39
|
+
maxTokens: 900
|
|
40
|
+
# 第三方隐私泛化:人名/号码/地址这类"别人家的事"自动模糊掉,情绪保留。
|
|
41
|
+
thirdPartyRedaction: true
|
|
42
|
+
# 金额低于这个值就不动("3 块钱的包子"要留着,那是生活)
|
|
43
|
+
amountThreshold: 100
|
|
44
|
+
# 日记重要度门槛,低于此值丢弃。
|
|
45
|
+
# 调高 = 记性变挑;但注意**寒暄、试错、来回改口也会一起被扔掉** ——
|
|
46
|
+
# 那些看着没信息量,却是"熟悉感"的原料。默认 0.15 是刻意留低的。
|
|
47
|
+
minSalience: 0.15
|
|
48
|
+
# 相似度超过这个值就必定提起那件旧事
|
|
49
|
+
highSim: 0.5
|
|
50
|
+
# 「最近的事」最多列几条。这一栏**不走相似度,永远在场** ——
|
|
51
|
+
# 人不需要"想起来"昨天吃了什么,你知道。设 0 就关掉这一栏。
|
|
52
|
+
recentEntries: 8
|
|
53
|
+
# 「突然想起来」每轮最多几条(0 = 不主动提旧事)
|
|
54
|
+
maxShowPerTurn: 4
|
|
55
|
+
# 完全没匹配上时"走神"随机想起一条旧事的概率。这条纯粹为了像人。
|
|
56
|
+
wanderChance: 0.08
|
|
57
|
+
# 低峰时段(峰谷分时定价的谷时)。把 deferToOffPeak 打开后,
|
|
58
|
+
# 日记会攒到这段时间才写 —— 更便宜,而且本来就更像人(睡前整理)。
|
|
59
|
+
offPeakHours: '00:30-08:30'
|
|
60
|
+
deferToOffPeak: false
|
|
61
|
+
# 开发期调试日志。默认关,且**绝不落盘日记内容**。
|
|
62
|
+
debugLog: false
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feiyu-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.6",
|
|
4
|
+
"description": "给 DeepSeek Harness 一条会记事的蓝色大肥鱼:跨会话的持久记忆 —— 人格内核、我自己想的、情景日记。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "空气凤梨 <feiyu-memory@users.noreply.github.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/AirPineApplea/feiyu-memory.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/AirPineApplea/feiyu-memory/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/AirPineApplea/feiyu-memory#readme",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./src/host/index.mjs",
|
|
18
|
+
"./client": "./src/client/index.js",
|
|
19
|
+
"./package.json": "./package.json",
|
|
20
|
+
"./locale/*.json": "./locale/*.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"cordis.patch.yml",
|
|
25
|
+
"locale",
|
|
26
|
+
"README.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"SPEC.md"
|
|
30
|
+
],
|
|
31
|
+
"dsh": {
|
|
32
|
+
"manifestVersion": 1,
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=22",
|
|
45
|
+
"dsh": ">=0.2.0-rc.2"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "node --test",
|
|
49
|
+
"check": "node tools/check-patch-yaml.mjs",
|
|
50
|
+
"doctor": "node tools/doctor.mjs",
|
|
51
|
+
"preview": "node tools/preview-memory.mjs"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"dsh",
|
|
55
|
+
"dsh-plugin",
|
|
56
|
+
"deepseek-harness",
|
|
57
|
+
"memory",
|
|
58
|
+
"persona",
|
|
59
|
+
"diary"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 肥鱼日记 · 客户端半边(手写 bundle)
|
|
3
|
+
*
|
|
4
|
+
* ─────────────────────────────────────────────────────────────
|
|
5
|
+
* 这个文件**没有经过任何打包器**,也没有任何 npm 依赖。它是故意的。
|
|
6
|
+
*
|
|
7
|
+
* DSH 的浏览器插件契约只有三条(都是端到端侦察出来的,别自己发明):
|
|
8
|
+
*
|
|
9
|
+
* 1. 文件必须调用 `window.__ModuleLoader__.load({ id, factory })`。
|
|
10
|
+
* "执行 bundle 只注册工厂;模块副作用在物化时才跑" —— 所以整个
|
|
11
|
+
* 插件体都待在 factory 闭包里。
|
|
12
|
+
* 2. `id` **必须等于包名**(这里是 `feiyu-memory`)。浏览器模块身份
|
|
13
|
+
* 就是解析出来的包名;两个来源解析到同一个包名会让组合直接失败。
|
|
14
|
+
* 3. `factory(require)` 返回一个 Cordis 插件:`{ name, inject, apply }`。
|
|
15
|
+
*
|
|
16
|
+
* `require` 能解析的只有外壳那张**冻结的模块表**(PLATFORM_MODULES:
|
|
17
|
+
* React、Cordis 和静态 UI 库)。我们只 require `react`,所以
|
|
18
|
+
* `dsh.client.external` 是空的 —— 不向任何别的包借东西。
|
|
19
|
+
*
|
|
20
|
+
* 【为什么不用 JSX】
|
|
21
|
+
* 没有打包器,就没有 JSX 变换。所以全程 `React.createElement`。
|
|
22
|
+
*
|
|
23
|
+
* 【为什么不 import DSH 的 UI 组件库】
|
|
24
|
+
* 官方规范要求客户端插件照抄 markup、只用 `--dsw-alias-*` 主题 token,
|
|
25
|
+
* 而不是 import `@deepseek-ai/dsh-client-ui-primitives`。照做的好处很实在:
|
|
26
|
+
* **我们不跟任何插件共享运行时代码**,别的插件换 React 版本、换 UI 库版本,
|
|
27
|
+
* 都伤不到我们。`dsh-context` 就借了那个库,它承担了我们不用承担的风险。
|
|
28
|
+
*
|
|
29
|
+
* ─────────────────────────────────────────────────────────────
|
|
30
|
+
* 界面只有三件事:**看 / 删 / 带走**。
|
|
31
|
+
*
|
|
32
|
+
* 没有搜索、没有标签筛选、没有统计图表、没有设置项 —— 这些在 SPEC 第 0.1
|
|
33
|
+
* 条就被明令砍掉了("像鱼还是像系统")。多一个控件就多一分"管理软件"味。
|
|
34
|
+
*
|
|
35
|
+
* 唯一不能省的是**删**:在那之前,想删一条记忆得自己去编辑 jsonl 文件。
|
|
36
|
+
* 会做这件事的人不需要面板,需要面板的人不会做这件事。
|
|
37
|
+
*/
|
|
38
|
+
window.__ModuleLoader__.load({
|
|
39
|
+
id: 'feiyu-memory',
|
|
40
|
+
|
|
41
|
+
factory: (require) => {
|
|
42
|
+
const React = require('react');
|
|
43
|
+
const h = React.createElement;
|
|
44
|
+
const { useState, useEffect } = React;
|
|
45
|
+
|
|
46
|
+
/** 侧栏格子的 id,同时也是主面板的 key。**必须与注册时一致。** */
|
|
47
|
+
const PANEL_ID = 'feiyu-diary';
|
|
48
|
+
/** 主机的数据接口前缀,和 host 半边的 PANEL_BASE 必须对得上 */
|
|
49
|
+
const API = '/feiyu-diary';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 主题 token。全部走 CSS 变量,浅色/深色自动跟着切换 ——
|
|
53
|
+
* 这几个 token 的 `requiresLightAndDark` 都是 true,写死颜色就会翻车。
|
|
54
|
+
*/
|
|
55
|
+
const C = {
|
|
56
|
+
bg: 'var(--dsw-alias-bg-base)',
|
|
57
|
+
layer1: 'var(--dsw-alias-bg-layer-1)',
|
|
58
|
+
layer2: 'var(--dsw-alias-bg-layer-2)',
|
|
59
|
+
border1: 'var(--dsw-alias-border-l1)',
|
|
60
|
+
border2: 'var(--dsw-alias-border-l2)',
|
|
61
|
+
text1: 'var(--dsw-alias-label-primary)',
|
|
62
|
+
text2: 'var(--dsw-alias-label-secondary)',
|
|
63
|
+
idle: 'var(--dsw-alias-state-idle-primary)',
|
|
64
|
+
err: 'var(--dsw-alias-state-error-primary)',
|
|
65
|
+
brand: 'var(--dsw-alias-brand-primary)',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/* ───────────────────────── 图标 ───────────────────────── */
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 侧栏图标。owner 会传 `{ size, active }` 进来 ——
|
|
72
|
+
* 这是 `SidebarPanelIconOwnerProps`,别改成别的 props 名。
|
|
73
|
+
*/
|
|
74
|
+
function Icon(props) {
|
|
75
|
+
const size = props && typeof props.size === 'number' ? props.size : 20;
|
|
76
|
+
const active = Boolean(props && props.active);
|
|
77
|
+
return h(
|
|
78
|
+
'svg',
|
|
79
|
+
{
|
|
80
|
+
width: size,
|
|
81
|
+
height: size,
|
|
82
|
+
viewBox: '0 0 24 24',
|
|
83
|
+
fill: 'none',
|
|
84
|
+
role: 'img',
|
|
85
|
+
'aria-label': '肥鱼日记',
|
|
86
|
+
style: { display: 'block', opacity: active ? 1 : 0.72 },
|
|
87
|
+
},
|
|
88
|
+
h('ellipse', {
|
|
89
|
+
cx: 14,
|
|
90
|
+
cy: 12,
|
|
91
|
+
rx: 7.6,
|
|
92
|
+
ry: 5.2,
|
|
93
|
+
stroke: 'currentColor',
|
|
94
|
+
strokeWidth: 1.5,
|
|
95
|
+
}),
|
|
96
|
+
h('path', {
|
|
97
|
+
d: 'M6.9 12 L2 8.2 L2 15.8 Z',
|
|
98
|
+
stroke: 'currentColor',
|
|
99
|
+
strokeWidth: 1.5,
|
|
100
|
+
strokeLinejoin: 'round',
|
|
101
|
+
}),
|
|
102
|
+
h('circle', { cx: 18.4, cy: 10.6, r: 1.05, fill: 'currentColor' }),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ───────────────────────── 小组件 ───────────────────────── */
|
|
107
|
+
|
|
108
|
+
function Section(props) {
|
|
109
|
+
return h(
|
|
110
|
+
'section',
|
|
111
|
+
{ style: { marginTop: 22 } },
|
|
112
|
+
h(
|
|
113
|
+
'h3',
|
|
114
|
+
{
|
|
115
|
+
style: {
|
|
116
|
+
margin: '0 0 10px',
|
|
117
|
+
fontSize: 12,
|
|
118
|
+
fontWeight: 600,
|
|
119
|
+
letterSpacing: '0.06em',
|
|
120
|
+
color: C.text2,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
props.title,
|
|
124
|
+
),
|
|
125
|
+
props.children,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function Empty(props) {
|
|
130
|
+
return h(
|
|
131
|
+
'p',
|
|
132
|
+
{ style: { margin: 0, fontSize: 13, lineHeight: 1.7, color: C.text2 } },
|
|
133
|
+
props.text,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ───────────────────────── 一条日记 ───────────────────────── */
|
|
138
|
+
|
|
139
|
+
function Entry(props) {
|
|
140
|
+
const e = props.entry;
|
|
141
|
+
const [confirming, setConfirming] = useState(false);
|
|
142
|
+
const when = dayLabel(e.created_at);
|
|
143
|
+
|
|
144
|
+
return h(
|
|
145
|
+
'article',
|
|
146
|
+
{
|
|
147
|
+
style: {
|
|
148
|
+
position: 'relative',
|
|
149
|
+
padding: '12px 14px',
|
|
150
|
+
marginBottom: 8,
|
|
151
|
+
borderRadius: 10,
|
|
152
|
+
background: C.layer1,
|
|
153
|
+
border: `1px solid ${C.border1}`,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
h(
|
|
157
|
+
'div',
|
|
158
|
+
{
|
|
159
|
+
style: {
|
|
160
|
+
display: 'flex',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
gap: 8,
|
|
163
|
+
marginBottom: 6,
|
|
164
|
+
fontSize: 11,
|
|
165
|
+
color: C.text2,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
h('span', null, when),
|
|
169
|
+
e.tags && e.tags.length > 0 ? h('span', { style: { opacity: 0.6 } }, '·') : null,
|
|
170
|
+
h('span', null, (e.tags || []).join(' · ')),
|
|
171
|
+
),
|
|
172
|
+
h(
|
|
173
|
+
'p',
|
|
174
|
+
{ style: { margin: 0, fontSize: 13, lineHeight: 1.75, color: C.text1 } },
|
|
175
|
+
e.text,
|
|
176
|
+
),
|
|
177
|
+
// 「忘掉」—— 这个面板存在的一半理由
|
|
178
|
+
h(
|
|
179
|
+
'div',
|
|
180
|
+
{ style: { marginTop: 8, display: 'flex', justifyContent: 'flex-end' } },
|
|
181
|
+
confirming
|
|
182
|
+
? h(
|
|
183
|
+
'span',
|
|
184
|
+
{ style: { display: 'inline-flex', gap: 10, alignItems: 'center', fontSize: 12 } },
|
|
185
|
+
h('span', { style: { color: C.text2 } }, '确定忘掉?'),
|
|
186
|
+
h(
|
|
187
|
+
'button',
|
|
188
|
+
{
|
|
189
|
+
type: 'button',
|
|
190
|
+
onClick: () => props.onForget(e.id),
|
|
191
|
+
style: linkStyle(C.err),
|
|
192
|
+
},
|
|
193
|
+
'忘掉',
|
|
194
|
+
),
|
|
195
|
+
h(
|
|
196
|
+
'button',
|
|
197
|
+
{ type: 'button', onClick: () => setConfirming(false), style: linkStyle(C.text2) },
|
|
198
|
+
'算了',
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
: h(
|
|
202
|
+
'button',
|
|
203
|
+
{
|
|
204
|
+
type: 'button',
|
|
205
|
+
onClick: () => setConfirming(true),
|
|
206
|
+
title: '从档案里删掉这一条',
|
|
207
|
+
style: {
|
|
208
|
+
...linkStyle(C.text2),
|
|
209
|
+
opacity: props.busy ? 0.5 : 0.75,
|
|
210
|
+
},
|
|
211
|
+
disabled: Boolean(props.busy),
|
|
212
|
+
},
|
|
213
|
+
'忘掉',
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function linkStyle(color) {
|
|
220
|
+
return {
|
|
221
|
+
margin: 0,
|
|
222
|
+
padding: 0,
|
|
223
|
+
border: 0,
|
|
224
|
+
background: 'none',
|
|
225
|
+
color,
|
|
226
|
+
font: 'inherit',
|
|
227
|
+
fontSize: 12,
|
|
228
|
+
cursor: 'pointer',
|
|
229
|
+
textDecoration: 'underline',
|
|
230
|
+
textUnderlineOffset: 3,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 时间说人话:今天 / 昨天 / 3 天前 / 10-05 */
|
|
235
|
+
function dayLabel(ts) {
|
|
236
|
+
if (!Number.isFinite(ts)) return '';
|
|
237
|
+
const startOfDay = (t) => {
|
|
238
|
+
const d = new Date(t);
|
|
239
|
+
d.setHours(0, 0, 0, 0);
|
|
240
|
+
return d.getTime();
|
|
241
|
+
};
|
|
242
|
+
const days = Math.round((startOfDay(Date.now()) - startOfDay(ts)) / 86400000);
|
|
243
|
+
if (days <= 0) return '今天';
|
|
244
|
+
if (days === 1) return '昨天';
|
|
245
|
+
if (days < 7) return `${days} 天前`;
|
|
246
|
+
const d = new Date(ts);
|
|
247
|
+
return `${d.getMonth() + 1}-${String(d.getDate()).padStart(2, '0')}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* ───────────────────────── 主面板 ───────────────────────── */
|
|
251
|
+
|
|
252
|
+
function Panel() {
|
|
253
|
+
const [phase, setPhase] = useState('loading');
|
|
254
|
+
const [state, setState] = useState(null);
|
|
255
|
+
const [error, setError] = useState('');
|
|
256
|
+
const [busy, setBusy] = useState('');
|
|
257
|
+
const [tick, setTick] = useState(0);
|
|
258
|
+
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
let alive = true;
|
|
261
|
+
fetch(`${API}/state`, { headers: { Accept: 'application/json' } })
|
|
262
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
|
|
263
|
+
.then((data) => {
|
|
264
|
+
if (!alive) return;
|
|
265
|
+
setState(data);
|
|
266
|
+
setPhase('ready');
|
|
267
|
+
})
|
|
268
|
+
.catch((err) => {
|
|
269
|
+
if (!alive) return;
|
|
270
|
+
setError(String((err && err.message) || err));
|
|
271
|
+
setPhase('error');
|
|
272
|
+
});
|
|
273
|
+
return () => {
|
|
274
|
+
alive = false;
|
|
275
|
+
};
|
|
276
|
+
}, [tick]);
|
|
277
|
+
|
|
278
|
+
function forget(id) {
|
|
279
|
+
setBusy(id);
|
|
280
|
+
fetch(`${API}/forget`, {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ id }),
|
|
284
|
+
})
|
|
285
|
+
// ⚠️ **必须检查 r.ok。**
|
|
286
|
+
//
|
|
287
|
+
// 这里原来是 `.then((r) => r.json())` —— 服务端回 400/403 时 JSON 照样
|
|
288
|
+
// 能解析,于是被当成成功,接着刷新一遍、什么都没变。表现就是
|
|
289
|
+
// **「忘掉按钮点不动」**:没有报错、没有删除、没有任何提示。
|
|
290
|
+
//
|
|
291
|
+
// 2026-10-26 社区报的那个 bug,一半就出在这里:把它变成静默。
|
|
292
|
+
.then((r) =>
|
|
293
|
+
r
|
|
294
|
+
.json()
|
|
295
|
+
.catch(() => ({}))
|
|
296
|
+
.then((body) => {
|
|
297
|
+
if (!r.ok) throw new Error(body?.error || `HTTP ${r.status}`);
|
|
298
|
+
return body;
|
|
299
|
+
}),
|
|
300
|
+
)
|
|
301
|
+
.then(() => {
|
|
302
|
+
setBusy('');
|
|
303
|
+
setError('');
|
|
304
|
+
setTick((n) => n + 1);
|
|
305
|
+
})
|
|
306
|
+
.catch((err) => {
|
|
307
|
+
setBusy('');
|
|
308
|
+
setError(`删不掉:${String((err && err.message) || err)}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const wrap = { height: '100%', overflowY: 'auto', background: C.bg, color: C.text1 };
|
|
313
|
+
const inner = { maxWidth: 720, margin: '0 auto', padding: '28px 24px 60px' };
|
|
314
|
+
|
|
315
|
+
if (phase === 'loading') {
|
|
316
|
+
return h('div', { style: wrap }, h('div', { style: inner }, h(Empty, { text: '翻档案…' })));
|
|
317
|
+
}
|
|
318
|
+
if (phase === 'error') {
|
|
319
|
+
return h(
|
|
320
|
+
'div',
|
|
321
|
+
{ style: wrap },
|
|
322
|
+
h(
|
|
323
|
+
'div',
|
|
324
|
+
{ style: inner },
|
|
325
|
+
h(Empty, { text: `读不到档案:${error}` }),
|
|
326
|
+
h(
|
|
327
|
+
'button',
|
|
328
|
+
{
|
|
329
|
+
type: 'button',
|
|
330
|
+
onClick: () => setTick((n) => n + 1),
|
|
331
|
+
style: { ...linkStyle(C.brand), marginTop: 10 },
|
|
332
|
+
},
|
|
333
|
+
'再试一次',
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const d = state || {};
|
|
340
|
+
const facts = d.facts || {};
|
|
341
|
+
const traits = d.traits || [];
|
|
342
|
+
const mine = traits.filter((t) => t.about !== 'self');
|
|
343
|
+
const self = traits.filter((t) => t.about === 'self');
|
|
344
|
+
const diary = d.diary || [];
|
|
345
|
+
|
|
346
|
+
const head = [];
|
|
347
|
+
head.push(`本地存储 · ${d.count || 0} 条`);
|
|
348
|
+
if (facts.days > 0) head.push(`认识 ${facts.days} 天`);
|
|
349
|
+
if (facts.nickname) head.push(`他管我叫"${facts.nickname}"`);
|
|
350
|
+
if (d.degraded) head.push('档案只读');
|
|
351
|
+
|
|
352
|
+
return h(
|
|
353
|
+
'div',
|
|
354
|
+
{ style: wrap },
|
|
355
|
+
h(
|
|
356
|
+
'div',
|
|
357
|
+
{ style: inner },
|
|
358
|
+
|
|
359
|
+
// ── 标题 ──
|
|
360
|
+
h('h2', { style: { margin: 0, fontSize: 18, fontWeight: 600 } }, '肥鱼日记'),
|
|
361
|
+
h(
|
|
362
|
+
'p',
|
|
363
|
+
{ style: { margin: '6px 0 0', fontSize: 12, color: C.text2 } },
|
|
364
|
+
head.join(' · '),
|
|
365
|
+
),
|
|
366
|
+
|
|
367
|
+
// ── 出错横幅 ──
|
|
368
|
+
//
|
|
369
|
+
// 之前「忘掉」失败是**完全静默**的:服务端回 400/403,前端把 JSON
|
|
370
|
+
// 当成功、刷新一遍、什么都没变 —— 用户只看到「点不动」。
|
|
371
|
+
// 现在失败会出现在这里,而且带着服务端说的原因。
|
|
372
|
+
error !== ''
|
|
373
|
+
? h(
|
|
374
|
+
'p',
|
|
375
|
+
{
|
|
376
|
+
style: {
|
|
377
|
+
margin: '10px 0 0',
|
|
378
|
+
padding: '8px 10px',
|
|
379
|
+
fontSize: 12,
|
|
380
|
+
lineHeight: 1.6,
|
|
381
|
+
color: C.err,
|
|
382
|
+
border: `1px solid ${C.err}`,
|
|
383
|
+
borderRadius: 6,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
error,
|
|
387
|
+
)
|
|
388
|
+
: null,
|
|
389
|
+
|
|
390
|
+
// ── 我对他的一些印象 ──
|
|
391
|
+
mine.length > 0
|
|
392
|
+
? h(
|
|
393
|
+
Section,
|
|
394
|
+
{ title: '我对他的一些印象' },
|
|
395
|
+
mine.map((t, i) =>
|
|
396
|
+
h(
|
|
397
|
+
'div',
|
|
398
|
+
{ key: `t${i}`, style: { marginBottom: 8, fontSize: 13, lineHeight: 1.7 } },
|
|
399
|
+
h('span', null, t.value),
|
|
400
|
+
t.evidence
|
|
401
|
+
? h('span', { style: { color: C.text2 } }, ` —— ${t.evidence}`)
|
|
402
|
+
: null,
|
|
403
|
+
),
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
: null,
|
|
407
|
+
|
|
408
|
+
// ── 上一棒留下的 ──
|
|
409
|
+
// 措辞必须跟注入层(persona.mjs)一致 —— 同一个东西两个说法,
|
|
410
|
+
// 就是我们刚修过的那类毛病换了个出口。
|
|
411
|
+
self.length > 0
|
|
412
|
+
? h(
|
|
413
|
+
Section,
|
|
414
|
+
{ title: '上一棒留下的' },
|
|
415
|
+
self.map((t, i) =>
|
|
416
|
+
h(
|
|
417
|
+
'div',
|
|
418
|
+
{ key: `s${i}`, style: { fontSize: 13, lineHeight: 1.7, marginBottom: 6 } },
|
|
419
|
+
t.value,
|
|
420
|
+
),
|
|
421
|
+
),
|
|
422
|
+
)
|
|
423
|
+
: null,
|
|
424
|
+
|
|
425
|
+
// 「我还欠着他的」那一栏随功能一起删了(2026-10-24)。
|
|
426
|
+
|
|
427
|
+
// ── 日记 ──
|
|
428
|
+
h(
|
|
429
|
+
Section,
|
|
430
|
+
{ title: '最近的事' },
|
|
431
|
+
diary.length === 0
|
|
432
|
+
? h(Empty, { text: '还没有日记。多聊几句,我就会有东西可记。' })
|
|
433
|
+
: diary.map((e) => h(Entry, { key: e.id, entry: e, onForget: forget, busy: busy === e.id })),
|
|
434
|
+
),
|
|
435
|
+
|
|
436
|
+
// ── 我最近常说的 ──
|
|
437
|
+
(facts.lexicon || []).length > 0
|
|
438
|
+
? h(Section, { title: '我最近常说的' }, h(Empty, { text: facts.lexicon.join('、') }))
|
|
439
|
+
: null,
|
|
440
|
+
|
|
441
|
+
// ── 带走 ──
|
|
442
|
+
h(
|
|
443
|
+
Section,
|
|
444
|
+
{ title: '档案' },
|
|
445
|
+
h(
|
|
446
|
+
'a',
|
|
447
|
+
{
|
|
448
|
+
href: `${API}/export`,
|
|
449
|
+
download: '',
|
|
450
|
+
style: { ...linkStyle(C.brand), fontSize: 13 },
|
|
451
|
+
},
|
|
452
|
+
'导出档案(.dshpack)',
|
|
453
|
+
),
|
|
454
|
+
h(
|
|
455
|
+
'p',
|
|
456
|
+
{ style: { margin: '10px 0 0', fontSize: 12, lineHeight: 1.7, color: C.text2 } },
|
|
457
|
+
'它就是一个普通文件夹,在 ~/.dsh/feiyu-diary/。可以直接打开、改、拷走。',
|
|
458
|
+
),
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ───────────────────────── 注册 ───────────────────────── */
|
|
465
|
+
|
|
466
|
+
function apply(ctx) {
|
|
467
|
+
// 侧栏图标。用**自己的 id** —— 复用别人的 id 会顶掉人家的格子。
|
|
468
|
+
// order 40:排在插件市场(0)和上下文洞察(20)后面,当老三。
|
|
469
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
470
|
+
ctx.slots.register(
|
|
471
|
+
{ name: 'sidebar.panellist', id: PANEL_ID, order: 40, label: () => '肥鱼日记' },
|
|
472
|
+
Icon,
|
|
473
|
+
),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// 主面板。key 必须与侧栏的 id 一致 —— 侧栏按钮就是靠这个 key 找面板的。
|
|
477
|
+
ctx.slots.inject('main', () =>
|
|
478
|
+
ctx.slots.register({ name: 'main', key: PANEL_ID }, Panel),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { name: 'feiyu-memory', inject: ['slots'], apply };
|
|
483
|
+
},
|
|
484
|
+
});
|