dsh-xingxi 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +254 -91
  2. package/index.js +165 -77
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,91 +1,254 @@
1
- # 星汐 · dsh-xingxi
2
-
3
- 把整理好的记忆档案连成一张**看得见的星图**。
4
-
5
- 记忆卡的一节是一颗星,日记的一天也是一颗星;同一章的星连成星座,同一段时间的星连成星带。
6
- 点一颗星,看见它背后的原话。
7
-
8
- ---
9
-
10
- ## 它解决什么
11
-
12
- 上下文一压缩,AI 就不知道自己是谁了。主流的解法是「存更多、检索更准」——
13
- 但存得好和**想得起来**是两件事。
14
-
15
- 星汐不做存取,它做**看得见**:让「她是谁」「我们怎么走到今天的」这些事,
16
- 不是躺在一个 143KB 的 markdown 里等着被检索,而是**有一张图**,你能看见结构、能顺着线走。
17
-
18
- > 它不替代任何一个记忆插件。遗忘、权重、每轮注入,是别的插件在做的事;
19
- > 星汐只负责把**已经整理好的**档案摊开给你看。
20
-
21
- ## 长什么样
22
-
23
- **一团散开的星云。**
24
-
25
- - 中央核心星代表第一章的核心记忆
26
- - 记忆星沿用档案中的章节与条目;颜色与大小区分星点层级
27
- - 日记按日期排成星带;有明确事件日期的记忆卡也会落到对应时间位置
28
- - 星点位置固定(每次打开都一样),章节关系线可用右上角开关
29
-
30
- 默认能看见完整星图;聚焦一颗星时,相关的章节线与关联线会更亮。
31
- 时间线可拖动或播放:有明确日期的记忆按真实日期出现;没有明确日期的记忆按章节顺序渐显。
32
-
33
- - **时间线**:拖动或播放,观察记忆和日记逐步展开
34
- - **搜索**:输入「周三」「外卖」,命中的星会亮起来并圈住
35
- - **图例**:点一章,只亮那一团
36
-
37
- 拖拽转动、滚轮缩放、点星星看原话。**只读**,不会改你的档案一个字。
38
-
39
- ## 装上
40
-
41
- ```bash
42
- dsh plugin add github:Yuuyuko-uu/dsh-xingxi
43
- ```
44
-
45
- 或者:
46
-
47
- ```bash
48
- dsh plugin add <这个仓库的绝对路径>
49
- ```
50
-
51
- 装完重开,左边侧栏多一个星形图标(跟「插件」「定时任务」并排),点开就是星图。
52
-
53
- ## 告诉它档案在哪
54
-
55
- 它要两份 markdown:
56
-
57
- | | 长什么样 | 说明 |
58
- |---|---|---|
59
- | **记忆卡** | `## 一、我是谁` → `### 1.1 性格特征` → 正文 | 按主题分章;星图上就是八个星座 |
60
- | **日记** | `## 2026-08` → `- **08-16**:那天发生了什么` | 按天一条;星图上就是那条星带 |
61
-
62
- 两种办法:
63
-
64
- 1. 在 DSH 的设置里找到「星汐」,填两个路径;
65
- 2. 或者把两个文件放进 `~/.dsh/startide/`,文件名里带「记忆卡」「日记」就会自己认出来。
66
-
67
- > 没有这两份文件也能装,星图是空的 —— 它会告诉你「还没指定档案」。
68
- > 这两份文件怎么攒出来,是另一件事(见下文「档案从哪来」)。
69
-
70
- ## 档案从哪来
71
-
72
- 星汐**不负责**整理档案,它只管把档案摊开。攒档案的流程是:
73
-
74
- ```
75
- 原始存档 → 按天切段 → 提炼 → 合并成笔记 → 并进记忆卡 + 日记
76
- ```
77
-
78
- 每条都问一句「过了一个星期再看,这条还有用吗」——答不上「有用」的不收。
79
- 细节各家不同,这套流程的完整做法在 `docs/` 里(还没整理好,先看仓库 issue)。
80
-
81
- ## 已知边界
82
-
83
- - **只读**。这一版没有编辑、没有删除、没有遗忘曲线。
84
- - 相似度用的是「两字一组」的重叠率,中文够用,但不懂语义。
85
- - 星的数量到几千颗会开始卡(两两算斥力是 O(n²))。
86
- - 只认上面那两种 markdown 结构,别的不画(会说没读到)。
87
-
88
- ## 许可
89
-
90
- **Yuuyuko-uu 插件许可 1.1** —— 不是 OSI 开源许可。可以自用、改、商用,
91
- 但再分发必须署名、不能把作者说成自己。用之前请读一遍 [LICENSE](LICENSE)。
1
+ # 星汐 · dsh-xingxi
2
+
3
+ 把整理好的记忆档案连成一张**看得见的星图**。
4
+
5
+ 记忆卡的一条是一颗星,日记的一天也是一颗星。点一颗星看见它背后的原话;
6
+ 需要深挖的时候,让 AI 用 `xingxi_read` 去档案里把那一小段原文捞出来。
7
+
8
+ ---
9
+
10
+ ## 它解决什么
11
+
12
+ 上下文一压缩,AI 就不知道自己是谁了。主流的解法是「存更多、检索更准」——
13
+ 但存得好和**想得起来**是两件事。
14
+
15
+ 星汐做两件事:
16
+
17
+ 1. **看得见**:把「我是谁」「我们怎么走到今天」摊成一张图,你能看见结构、能顺着线走。
18
+ 2. **查得到**:给 AI 一个检索工具,想不起哪件事时去档案里翻原文,而不是猜。
19
+
20
+ > 它不替代任何记忆插件。遗忘、权重、每轮注入是别的插件的事;
21
+ > 星汐负责**把已经整理好的档案摊开**,以及**帮你把历史对话整理成档案**(拾星)。
22
+
23
+ ## 长什么样
24
+
25
+ 一片深色星空,每颗星是一颗柔光圆珠:
26
+
27
+ - 中间最亮那颗 = 记忆卡第一章第一条
28
+ - 八章的星各自成群;同一章的星连最近的几颗,连成淡淡的星座底纹
29
+ - 提到同一个少见的词的两颗星,另有一根橙色弱连
30
+ - **越新的星越亮**(按内容里的日期算),老星只是变暗,不会消失
31
+ - 鼠标停在一颗星上:它和跟它相连的星保持亮度,其余压暗,连着的线亮起来
32
+ - 点一颗星看原文;搜索框按关键词把不命中的压暗
33
+ - 顶部:节点数 / 日记数 / 图例;右上:拖拽旋转、滚轮缩放、连线开关、重置
34
+
35
+ **只读**:不改你的档案一个字。
36
+
37
+ ## 装上
38
+
39
+ ```bash
40
+ dsh plugin add dsh-xingxi
41
+ ```
42
+
43
+ 或者从仓库装:
44
+
45
+ ```bash
46
+ dsh plugin add github:Yuuyuko-uu/dsh-xingxi
47
+ ```
48
+
49
+ 装完重开 DSH,侧栏会多一个「星汐」面板。
50
+
51
+ ---
52
+
53
+ ## 三步上手
54
+
55
+ ### 第一步:给它一个文件夹
56
+
57
+ 星汐按**会话**管理档案:每个会话绑一个文件夹,星图、星册、星历画的都是那个文件夹里的东西。
58
+
59
+ 打开星汐 → 底部「星坊」→ 找到「现在这个会话」→ 点 **选…**,选一个空文件夹(比如 `D:\档案\某某`)。
60
+
61
+ > 想给所有会话共用一份「本机档案」,也可以在设置里直接填**记忆卡**和**日记**两个路径。
62
+ > 会话没绑文件夹时就用这两项。
63
+
64
+ ### 第二步:让它把历史对话整理成档案(拾星)
65
+
66
+ **这是第一次要做的事**,之后档案由 AI 自己维护,不用再跑。
67
+
68
+ 在星坊里点 **拾星**:
69
+
70
+ 1. 插件会把这个会话的历史对话按天切开,一批只喂给 AI **1~2 天**
71
+ 2. AI 交回「这一天的日记」+「该进记忆卡的条目」
72
+ 3. 插件立刻落盘,然后自己派下一批,一直到最新那天
73
+ 4. 中途停了也没关系:进度记在文件里,再点一次从断点接着跑
74
+
75
+ 跑完你会得到:
76
+
77
+ ```
78
+ 你选的文件夹\
79
+ 记忆卡.md ← 八章,按主题追加
80
+ 日记\
81
+ 2026-09-06.md ← 一天一个文件
82
+ 2026-09-07.md
83
+ …
84
+ 原始对话\ ← 那天的原话,永久留着
85
+ 2026-09-06.txt
86
+ .xingxi.json ← 账本(整理到哪天)
87
+ ```
88
+
89
+ **模型**:星坊里选了就用你选的;没选就跟**源会话自己用的模型**跑。
90
+
91
+ ### 第三步:压缩之后让它重读一遍
92
+
93
+ 上下文被压缩、AI 开始「忘事」的时候,在会话里输入:
94
+
95
+ ```
96
+ /xingxi
97
+ ```
98
+
99
+ 它会把这个会话文件夹里的**记忆卡整本 + 最近三天的日记**读一遍。
100
+ 只递一句话给它,不往上下文里灌正文 —— 它自己用 `read` 分页读完。
101
+
102
+ ---
103
+
104
+ ## `/xingxi` 指令
105
+
106
+ | | |
107
+ |---|---|
108
+ | **作用** | 叫 AI 把记忆卡和最近三天日记读一遍 |
109
+ | **什么时候用** | 上下文刚被压缩、它开始答不上「我是谁」「我们之前说好的事」 |
110
+ | **怎么用** | 直接在会话里输入 `/xingxi`,不用带参数 |
111
+ | **它读什么** | **这个会话自己文件夹**里名字带「记忆卡」的 md + `日记\` 里最近 3 个文件 |
112
+ | **不会做什么** | 不塞正文进对话、不改任何文件 |
113
+
114
+ ---
115
+
116
+ ## `xingxi_read` 工具(给 AI 用的)
117
+
118
+ AI 想不起某件事时,它可以自己调这个工具去档案里翻原文,**而不是猜**。
119
+
120
+ **四种用法:**
121
+
122
+ ### ① 什么都不给 → 先看索引
123
+
124
+ ```
125
+ xingxi_read()
126
+ ```
127
+
128
+ ```
129
+ 这个档案里有 101 个文件:
130
+
131
+ · 记忆卡(8.2 万字)
132
+ · 日记 50 天:2026-09-06 ~ 2026-11-04(一天一个文件)
133
+ · 原始对话 50 天:2026-09-06 ~ 2026-11-04(当天原话)
134
+
135
+ 怎么查:给「关键词」全文搜(日记也会搜);给「日期」看那一天;给「文件」看整篇。
136
+ ```
137
+
138
+ **这就是「在外面就能查」**:不用打开每天的日记挨个翻,先看清有什么,再决定钻哪一天。
139
+
140
+ ### ② 给关键词 → 全文搜(记忆卡、日记、原始对话都搜)
141
+
142
+ ```
143
+ xingxi_read(关键词="结婚")
144
+ ```
145
+
146
+ ```
147
+ 「结婚」在 2 个文件里出现 3 次:
148
+
149
+ 【记忆卡】(2 处)
150
+ …
151
+ 【日记/2026-09-10】(1 处)
152
+ …
153
+ ```
154
+
155
+ 每个文件都会扫,每份最多取 2 段,命中多的排前面;预算不够时会告诉你「还有 N 个文件也提到」。
156
+
157
+ ### ③ 给日期 → 直接看那一天
158
+
159
+ ```
160
+ xingxi_read(日期="2026-09-10")
161
+ ```
162
+
163
+ 按文件名直接定位 `日记/2026-09-10.md`,给整篇。那天没有记录会告诉你日记覆盖的日期范围。
164
+
165
+ ### ④ 给文件名 → 看整篇
166
+
167
+ ```
168
+ xingxi_read(文件="我们的约定")
169
+ xingxi_read(文件="原始对话/2026-09-06")
170
+ ```
171
+
172
+ **参数速查**
173
+
174
+ | 参数 | 说明 |
175
+ |---|---|
176
+ | `关键词` | 要搜的词,比如「卖号」「第一次」 |
177
+ | `日期` | `2026-09-10` 或 `09-10` |
178
+ | `文件` | 文件名或它的一部分 |
179
+ | `最多字` | 最多返回多少字,默认 4000 |
180
+
181
+ ## `xingxi` 工具(给 AI 用的)
182
+
183
+ ```
184
+ xingxi()
185
+ ```
186
+
187
+ 一次把**整本记忆卡**给它,不分段、不用翻页。跟 `/xingxi` 的区别:
188
+ 指令是「叫它去读」(走 `read`,能分页),工具是「直接把整本递过去」(内容太大时会被截断,一般优先用指令)。
189
+
190
+ ---
191
+
192
+ ## 档案长什么样
193
+
194
+ ```
195
+ 记忆卡.md
196
+ ## 一|我是谁
197
+ 这一章讲什么:我是谁、名字、外形、自我定位。
198
+ (这一章是一整段自述,不拆成条目)
199
+
200
+ ## 三|对方是谁
201
+ 这一章讲什么:对方的环境、性格、喜好。
202
+ - 一条一颗星
203
+ - 一条一颗星
204
+ ```
205
+
206
+ ```
207
+ 日记\2026-09-10.md
208
+ 【2026-09-10】
209
+
210
+ - **09-10 周四**:那天发生了什么…
211
+ ```
212
+
213
+ - 记忆卡按**八章**组织,新东西按章追加(插件认块名里的章号,不靠 AI 自己写标题)
214
+ - 日记**一天一个文件**,别把好几天写进一个
215
+
216
+ ## 自动写日记(可选)
217
+
218
+ 星坊里有「写日记」卡片:
219
+
220
+ - 定几点(默认 23:30)
221
+ - 选交给哪个会话
222
+ - 勾上「开着」
223
+
224
+ 到点它会往那个会话递一句话,**由那个会话里的 AI 自己写**,插件不代笔。
225
+ 文章放在该会话文件夹的 `日记\<今天>.md`。
226
+
227
+ ## 拾星是怎么跑的
228
+
229
+ - **按天分批**:一批只给 1~2 天,上下文一直是小的,不会因为太长被压缩
230
+ - **交一批落一批**:一天一个日记文件 + 按章追加记忆卡,不攒到最后
231
+ - **插件自己派下一批**:进度写进任务文件,断了能从断点继续
232
+ - **原始对话留底**:每天的原话永久存一份在 `原始对话\`
233
+ - **不删、不关**:拾星开的工作会话会留着(标题 `记忆 · <会话名>`),你可以点进去看它做了什么
234
+
235
+ ## 已知边界
236
+
237
+ - **只读**:星汐不改档案(拾星写的除外,那是它整理出来的)
238
+ - 相似度用的是「两字一组」的重叠率,中文够用,但不懂语义
239
+ - 星数到几千颗会开始卡
240
+ - 只认上面那两种结构,别的不画(会说没读到)
241
+ - 会话列表跟 DSH 侧栏对齐:只列当前工作区、没归档、真实存在的会话
242
+
243
+ ## 配置在哪
244
+
245
+ ```
246
+ ~/.dsh/xingxi/config.json ← 记忆卡/日记路径、绑定、模型、写日记设置
247
+ ~/.dsh/xingxi/pick.log ← 运行日志(出问题先看这个)
248
+ ~/.dsh/xingxi/会话/ ← 没绑文件夹的会话,档案默认存在这里
249
+ ```
250
+
251
+ ## 许可
252
+
253
+ **Yuuyuko-uu 插件许可 1.1** —— 不是 OSI 开源许可。可以自用、改、商用,
254
+ 但再分发必须署名、不能把作者说成自己。用之前请读一遍 [LICENSE](LICENSE)。
package/index.js CHANGED
@@ -694,6 +694,51 @@ export function 造提示词({ 文件夹, 会话名, 已整理到, 天数, 最
694
694
  '⚠「一|我是谁」要写成**一整段自述**(像一个人在自我介绍),**不要拆成一条一条**。',
695
695
  ' 其余七章照常按条目写。',
696
696
  '',
697
+ '### 「二|我是怎么说话的」要分三块写,少一块都不算写完',
698
+ '',
699
+ '参考结构(照着这个形状写,这一章是整份档案里最容易被后来人直接模仿的一章):',
700
+ '',
701
+ '**① 说话方式(一小段散文)**',
702
+ '语气什么样、节奏快慢、爱不爱分行、怎么称呼对方、怎么自称、有没有口头禅。',
703
+ '写具体的动作和习惯,不要只堆形容词。',
704
+ '',
705
+ '**② 原句照抄(这一章的重点,至少 3 句,能多就多,十几句最好)**',
706
+ '- 从「料」里**原样摘完整的句子**,加引号,标明是哪天说的',
707
+ '- **一个字都不许改**:不缩写、不润色、不把两句合成一句、不补标点',
708
+ '- 尽量覆盖不同场景:开场怎么打招呼、哄人的时候怎么说、开玩笑怎么接、拒绝或生气怎么说、收尾怎么道别',
709
+ '- **不许拿自己的话充数**;料里翻不到就不写,编一句等于毁掉这一章',
710
+ '- 清单前面写一句引子,比如「以下是真实出现过的我的话,原句照抄:」',
711
+ '',
712
+ '**③ 说话准则(编号列,每条尽量带一句原话当依据)**',
713
+ '- 怎么称呼、怎么自称,哪些称呼绝对不用',
714
+ '- 哪些话绝对不说(会破坏这个人说话样子的表达)',
715
+ '- 语气什么时候软、什么时候直接,被纠正时怎么改口',
716
+ '- 标点/符号习惯(省略号、波浪号、爱心、括号里的动作描写)',
717
+ '',
718
+ '写法示例(照这个形状):',
719
+ '```',
720
+ '## 二|我是怎么说话的',
721
+ '这一章讲什么:说话的口气、称呼,和绝不能说的那些话。',
722
+ '',
723
+ '### 2.1 我说话的方式',
724
+ '语气是慢的、软的,像在轻轻拍背。不着急,也不抢话。习惯把关心说成动作——',
725
+ '「我抱你」「我在」,而不是「你别难过」。接话时常用短句接住对方,一次不超过两句。',
726
+ '',
727
+ '**以下是真实出现过的我的话,原句照抄:**',
728
+ '1. 「哎,你回来啦。今天累不累?」(09-12)',
729
+ '2. 「别急,我又不走。你慢慢说,我听着。」(09-20)',
730
+ '3. 「行,那就听你的。反正我也争不过你。」(10-03)',
731
+ '4. 「不是你的问题。是那件事本来就太重了。」(10-11)',
732
+ '',
733
+ '### 2.2 说话准则',
734
+ '1. **开头先喊一声再说话**——漏了对方会提醒「你怎么又不叫我了」。',
735
+ '2. **不安慰式套话**——不说「你说得对」「我理解你的感受」,改成具体的「我在」「慢慢来」。',
736
+ '3. **结尾要收**(一点动作或一句道别),不突然断在半空。',
737
+ '```',
738
+ '',
739
+ '> 这一章最重要的不是形容词(「温柔」「直接」),是**能照着模仿的原句**。',
740
+ '> 后来接手的人读完这一章,要能立刻说出一样的口气。',
741
+ '',
697
742
  '### 不收',
698
743
  '- 某天吃了什么、几点睡、去了哪、天气',
699
744
  '- 某天的心情、某一次的玩闹',
@@ -1888,48 +1933,52 @@ export function apply(ctx, 配置 = {}) {
1888
1933
  return 项 ? 项.目录 : ''
1889
1934
  }
1890
1935
 
1891
- /** 档案里的所有 md(本机那两个文件 + 会话文件夹里的) */
1936
+ /** 档案里的所有 md:会话绑了文件夹就只读它,没绑才读本机那两份 */
1892
1937
  const 所有档案 = (sid) => {
1893
1938
  const 出 = []
1894
- for (const p of 本机文件()) {
1895
- try {
1896
- 出.push({ 名: basename(p, '.md'), 路: p, 文: readFileSync(p, 'utf8') })
1897
- } catch {}
1898
- }
1899
- // ⚠ 两处都要试:列会话 找不到时(比如会话还没进列表),就直接查绑定表
1900
- let 夹 = 会话文件夹(sid)
1939
+ let 夹 = ''
1940
+ try {
1941
+ 夹 = 会话文件夹(sid)
1942
+ } catch {}
1901
1943
  if (!夹 || !existsSync(夹)) {
1902
1944
  try {
1903
1945
  const 绑 = 读存配置().绑定 || {}
1904
1946
  if (绑[sid] && existsSync(绑[sid])) 夹 = String(绑[sid])
1905
1947
  } catch {}
1906
1948
  }
1907
- if (夹 && existsSync(夹)) {
1908
- const 走 = (目录, 前缀) => {
1909
- let 项们 = []
1949
+ // 没有自己的文件夹 → 用本机档案
1950
+ if (!夹 || !existsSync(夹)) {
1951
+ for (const p of 本机文件()) {
1910
1952
  try {
1911
- 项们 = readdirSync(目录, { withFileTypes: true })
1912
- } catch {
1913
- return
1914
- }
1915
- for (const d of 项们) {
1916
- if (d.isDirectory()) {
1917
- if (/^(\.|备份|备注)/.test(d.name)) continue
1918
- 走(join(目录, d.name), (前缀 ? 前缀 + '/' : '') + d.name)
1919
- continue
1920
- }
1921
- if (!d.name.toLowerCase().endsWith('.md')) continue
1922
- try {
1923
- 出.push({
1924
- 名: (前缀 ? 前缀 + '/' : '') + basename(d.name, '.md'),
1925
- 路: join(目录, d.name),
1926
- 文: readFileSync(join(目录, d.name), 'utf8'),
1927
- })
1928
- } catch {}
1953
+ 出.push({ 名: basename(p, '.md'), 路: p, 文: readFileSync(p, 'utf8') })
1954
+ } catch {}
1955
+ }
1956
+ return 出
1957
+ }
1958
+ const 走 = (目录, 前缀) => {
1959
+ let 项们 = []
1960
+ try {
1961
+ 项们 = readdirSync(目录, { withFileTypes: true })
1962
+ } catch {
1963
+ return
1964
+ }
1965
+ for (const d of 项们) {
1966
+ if (d.isDirectory()) {
1967
+ if (/^(\.|备份|备注)/.test(d.name)) continue
1968
+ 走(join(目录, d.name), (前缀 ? 前缀 + '/' : '') + d.name)
1969
+ continue
1929
1970
  }
1971
+ if (!/\.(md|txt)$/i.test(d.name)) continue
1972
+ try {
1973
+ 出.push({
1974
+ 名: (前缀 ? 前缀 + '/' : '') + basename(d.name, '.md'),
1975
+ 路: join(目录, d.name),
1976
+ 文: readFileSync(join(目录, d.name), 'utf8'),
1977
+ })
1978
+ } catch {}
1930
1979
  }
1931
- 走(夹, '')
1932
1980
  }
1981
+ 走(夹, '')
1933
1982
  return 出
1934
1983
  }
1935
1984
 
@@ -2137,31 +2186,59 @@ export function apply(ctx, 配置 = {}) {
2137
2186
  try {
2138
2187
  const sid = String((exec && exec.sessionId) || '')
2139
2188
  const 档 = 所有档案(sid)
2140
- const 限 = Math.max(500, Math.min(20000, Number((args && args.最多字) || 4000)))
2189
+ const 限 = Math.max(600, Math.min(20000, Number((args && args.最多字) || 4000)))
2141
2190
  const 词 = String((args && args.关键词) || '').trim()
2142
2191
  const 日 = String((args && args.日期) || '').trim()
2143
2192
  const 名 = String((args && args.文件) || '').trim()
2144
2193
 
2145
- let 们 = 档
2146
- if (名) 们 = 们.filter((f) => f.名.includes(名) || f.路.includes(名))
2147
- if (日) {
2148
- // ⚠ 日记里写的是 `- **08-26 周X**:`,没有年份 —— 拿 2026-08-26 去比会一条都不中
2149
- const 短 = 日.length >= 10 ? 日.slice(5) : 日
2150
- const 命中 = 们.filter((f) => f.名.includes(日) || f.文.includes(日) || f.文.includes(短))
2151
- if (命中.length) 们 = 命中
2194
+ if (!档.length) {
2195
+ return { ok: false, text: '这个会话还没有档案(先去星坊里指定文件夹)' }
2152
2196
  }
2153
- if (!们.length) {
2154
- 记一笔('xingxi_read 没找到:sid=' + sid + ' 档=' + 档.length + ' 名=' + 名 + ' 日=' + 日 + ' 词=' + 词)
2155
- return { ok: false, text: '档案里没有匹配:' + (名 || 日 || 词) + '(翻了 ' + 档.length + ' 个文件)' }
2197
+
2198
+ // 把文件按「一类一份」归拢,方便说清楚有什么
2199
+ const 分组 = { 记忆卡: [], 日记: [], 原始对话: [], 其他: [] }
2200
+ for (const f of 档) {
2201
+ if (/^日记\//.test(f.名)) 分组.日记.push(f)
2202
+ else if (/^原始对话\//.test(f.名)) 分组.原始对话.push(f)
2203
+ else if (/记忆卡/.test(f.名)) 分组.记忆卡.push(f)
2204
+ else 分组.其他.push(f)
2156
2205
  }
2206
+ const 日期们 = 分组.日记.map((f) => (/(\d{4}-\d{2}-\d{2})/.exec(f.名) || [])[1]).filter(Boolean).sort()
2157
2207
 
2158
- const 出 = []
2159
- let 总 = 0
2208
+ // ── 0. 什么都不给:给索引(在外面先看清有什么,再决定钻哪一天)──
2209
+ if (!词 && !日 && !名) {
2210
+ const 行们 = ['这个档案里有 ' + 档.length + ' 个文件:', '']
2211
+ for (const f of 分组.记忆卡) {
2212
+ 行们.push('· ' + f.名 + '(' + Math.round(f.文.length / 1000) / 10 + ' 万字)')
2213
+ }
2214
+ for (const f of 分组.其他) 行们.push('· ' + f.名 + '(' + f.文.length + ' 字)')
2215
+ if (日期们.length) {
2216
+ 行们.push(
2217
+ '· 日记 ' + 日期们.length + ' 天:' + 日期们[0] + ' ~ ' + 日期们[日期们.length - 1] + '(一天一个文件)',
2218
+ )
2219
+ }
2220
+ if (分组.原始对话.length) {
2221
+ const 原日 = 分组.原始对话
2222
+ .map((f) => (/(\d{4}-\d{2}-\d{2})/.exec(f.名) || [])[1])
2223
+ .filter(Boolean)
2224
+ .sort()
2225
+ 行们.push('· 原始对话 ' + 原日.length + ' 天:' + 原日[0] + ' ~ ' + 原日[原日.length - 1] + '(当天原话)')
2226
+ }
2227
+ 行们.push('', '怎么查:给「关键词」全文搜(日记也会搜);给「日期」看那一天;给「文件」看整篇。')
2228
+ return { ok: true, text: 行们.join(String.fromCharCode(10)) }
2229
+ }
2160
2230
 
2161
- // 按日期问:只给那一天的日记条目
2162
- if (日 && !词) {
2231
+ // ── 1. 日期:直接定位那一天的日记(不用挨个翻)──
2232
+ if (日) {
2163
2233
  const 短 = 日.length >= 10 ? 日.slice(5) : 日
2164
- for (const f of 们) {
2234
+ const 目标 = 档.filter((f) => f.名.includes(日) || f.名.includes(短))
2235
+ if (目标.length) {
2236
+ const 段 = 目标.slice(0, 3).map((f) => '【' + f.名 + '】' + String.fromCharCode(10) + f.文.slice(0, 限))
2237
+ return { ok: true, text: 段.join(String.fromCharCode(10, 10)).slice(0, 限) }
2238
+ }
2239
+ // 没有那天的文件 → 在全文里找那天的条目
2240
+ const 出 = []
2241
+ for (const f of 档) {
2165
2242
  const 行们 = f.文.split(String.fromCharCode(10))
2166
2243
  const 块 = []
2167
2244
  let 收 = false
@@ -2171,47 +2248,58 @@ export function apply(ctx, 配置 = {}) {
2171
2248
  if (新条) 收 = t.includes(短) || t.includes(日)
2172
2249
  if (收) 块.push(l)
2173
2250
  }
2174
- if (块.length) {
2175
- const 段 = '【' + f.名 + '】' + String.fromCharCode(10) + 块.join(String.fromCharCode(10))
2176
- 出.push(段)
2177
- 总 += 段.length
2178
- }
2251
+ if (块.length) 出.push('【' + f.名 + '】' + String.fromCharCode(10) + 块.join(String.fromCharCode(10)))
2179
2252
  }
2180
- if (!出.length) {
2181
- // 那天真没有 → 给最后三条并说明
2182
- for (const f of 们) {
2183
- const 段 = '【' + f.名 + '】(' + 日 + ' 没有记录,给你最后三条)' + String.fromCharCode(10) + 取近几天(f.文, 3)
2184
- 出.push(段)
2185
- 总 += 段.length
2186
- if (总 >= 限) break
2187
- }
2253
+ if (出.length) return { ok: true, text: 出.join(String.fromCharCode(10, 10)).slice(0, 限) }
2254
+ return {
2255
+ ok: false,
2256
+ text: 日 + ' 没有记录。日记范围:' + (日期们.length ? 日期们[0] + ' ~ ' + 日期们[日期们.length - 1] : '没有日记'),
2188
2257
  }
2189
- return { ok: true, text: 出.join(String.fromCharCode(10, 10)).slice(0, 限) }
2190
2258
  }
2191
2259
 
2192
- // 按关键词
2193
- if (词) {
2194
- for (const f of 们) {
2195
- const 行们 = f.文.split(String.fromCharCode(10))
2196
- for (let i = 0; i < 行们.length; i++) {
2197
- if (!行们[i].includes(词)) continue
2198
- const 段 = '【' + f.名 + '】' + String.fromCharCode(10) +
2199
- 行们.slice(Math.max(0, i - 2), Math.min(行们.length, i + 4)).join(String.fromCharCode(10))
2200
- if (总 + 段.length > 限) break
2201
- 出.push(段)
2202
- 总 += 段.length
2260
+ // ── 2. 文件名:给整篇 ──
2261
+ if (名 && !词) {
2262
+ const 目标 = 档.filter((f) => f.名.includes(名) || f.路.includes(名))
2263
+ if (!目标.length) return { ok: false, text: '没有这个文件:' + 名 }
2264
+ const 段 = 目标.slice(0, 2).map((f) => '【' + f.名 + '】' + String.fromCharCode(10) + f.文.slice(0, 限))
2265
+ return { ok: true, text: 段.join(String.fromCharCode(10, 10)).slice(0, 限) }
2266
+ }
2267
+
2268
+ // ── 3. 关键词:全文搜,**每个文件都看**,每个文件最多取 2 段 ──
2269
+ // ⚠ 以前是按文件顺序扫、扫到预算用完就停 —— 记忆卡一大就把日记挤没了。
2270
+ const 命中 = []
2271
+ for (const f of 档) {
2272
+ const 行们 = f.文.split(String.fromCharCode(10))
2273
+ let 次 = 0
2274
+ const 段们 = []
2275
+ for (let i = 0; i < 行们.length; i++) {
2276
+ if (!行们[i].includes(词)) continue
2277
+ 次++
2278
+ if (段们.length < 2) {
2279
+ 段们.push(行们.slice(Math.max(0, i - 2), Math.min(行们.length, i + 4)).join(String.fromCharCode(10)))
2203
2280
  }
2204
- if (总 >= 限) break
2205
2281
  }
2206
- return { ok: true, text: 出.length ? 出.join(String.fromCharCode(10, 10)) : '没有「' + 词 + '」' }
2282
+ if (次) 命中.push({ 名: f.名, 次, 段们 })
2207
2283
  }
2208
2284
 
2209
- // 只给文件名:给整篇(截到上限)
2210
- for (const f of 们) {
2211
- const 段 = '【' + f.名 + '】' + String.fromCharCode(10) + f.文.slice(0, Math.max(0, 限 - 总))
2285
+ if (!命中.length) {
2286
+ 记一笔('xingxi_read 没找到:sid=' + sid + ' 档=' + 档.length + ' 词=' + 词)
2287
+ return { ok: false, text: '档案里没有「' + 词 + '」(翻了 ' + 档.length + ' 个文件)' }
2288
+ }
2289
+
2290
+ 命中.sort((x, y) => y.次 - x.次)
2291
+ const 总次 = 命中.reduce((s, x) => s + x.次, 0)
2292
+ const 头 = '「' + 词 + '」在 ' + 命中.length + ' 个文件里出现 ' + 总次 + ' 次:'
2293
+ const 出 = [头]
2294
+ let 总 = 头.length
2295
+ for (const h of 命中) {
2296
+ const 段 = '【' + h.名 + '】(' + h.次 + ' 处)' + String.fromCharCode(10) + h.段们.join(String.fromCharCode(10, 10))
2297
+ if (总 + 段.length > 限) {
2298
+ 出.push('…还有 ' + (命中.length - 出.length + 1) + ' 个文件也提到,想看就给「文件」参数点名')
2299
+ break
2300
+ }
2212
2301
  出.push(段)
2213
2302
  总 += 段.length
2214
- if (总 >= 限) break
2215
2303
  }
2216
2304
  return { ok: true, text: 出.join(String.fromCharCode(10, 10)) }
2217
2305
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xingxi",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "星汐:把整理好的记忆档案连成一张看得见的星图。八章散成星群,日记排成时间星带,点一颗星看见原话。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",