@fgbg/mdocs 1.3.21 → 1.3.23
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 +1 -1
- package/agent-skills/changelog/SKILL.md +20 -0
- package/agent-skills/getting-started-installation/SKILL.md +8 -2
- package/dist/server/agent/Agent/session-manager.js +354 -144
- package/dist/server/agent/Agent/session-manager.js.map +1 -1
- package/dist/server/agent/Agent/session-manager.queue.test.js +65 -0
- package/dist/server/agent/Agent/session-manager.queue.test.js.map +1 -0
- package/dist/server/agent/Agent/session-meta-lock.js +73 -0
- package/dist/server/agent/Agent/session-meta-lock.js.map +1 -0
- package/dist/server/agent/Agent/session-meta-lock.test.js +44 -0
- package/dist/server/agent/Agent/session-meta-lock.test.js.map +1 -0
- package/dist/server/agent/Agent/session-repair.test.js +40 -0
- package/dist/server/agent/Agent/session-repair.test.js.map +1 -0
- package/dist/server/cli/main.js +74 -1
- package/dist/server/cli/main.js.map +1 -1
- package/dist/server/cli/self-update.js +34 -4
- package/dist/server/cli/self-update.js.map +1 -1
- package/dist/server/config/index.js +2 -1
- package/dist/server/config/index.js.map +1 -1
- package/dist/server/documents/document.service.js +1 -0
- package/dist/server/documents/document.service.js.map +1 -1
- package/dist/server/main.js +6 -0
- package/dist/server/main.js.map +1 -1
- package/dist/server/search/document-index-manager.js +17 -4
- package/dist/server/search/document-index-manager.js.map +1 -1
- package/dist/server/search/embedding-admin.js +12 -2
- package/dist/server/search/embedding-admin.js.map +1 -1
- package/dist/server/search/embedding-model.js +55 -13
- package/dist/server/search/embedding-model.js.map +1 -1
- package/dist/server/search/embedding-store.js +13 -4
- package/dist/server/search/embedding-store.js.map +1 -1
- package/dist/web/assets/{AgentChatPanel-BZlAgyrV.js → AgentChatPanel-CXCoVr71.js} +3 -3
- package/dist/web/assets/{AgentSkillFormCard-CzJG8FK4.js → AgentSkillFormCard-BokYfssA.js} +1 -1
- package/dist/web/assets/{AiWriteWorkbench-DiSDDAee.js → AiWriteWorkbench-BLYRPfhm.js} +1 -1
- package/dist/web/assets/{DiagramEditor-NtNY3sZL-DfkpQX3P.js → DiagramEditor-NtNY3sZL-uAUfkpbp.js} +1 -1
- package/dist/web/assets/DocumentEditor-C7eEw_Gh.js +1 -0
- package/dist/web/assets/HtmlEditor-B8hAvriH.js +1 -0
- package/dist/web/assets/{MergeView-CbFSN3Bi.js → MergeView-3RaowcU-.js} +1 -1
- package/dist/web/assets/{PlaygroundPage-DuMH_V-7.js → PlaygroundPage-Bc54r4cU.js} +1 -1
- package/dist/web/assets/{SettingsPage-C_RMO-5M.js → SettingsPage-hnisLfn5.js} +1 -1
- package/dist/web/assets/{Toolbar-D9FcRwg3.js → Toolbar-D0IqMu2I.js} +1 -1
- package/dist/web/assets/{VisitorPickerModal-CM13Y6jQ.js → VisitorPickerModal-C31LXFbB.js} +1 -1
- package/dist/web/assets/{check-Bf8YbzH7.js → check-CmDfmXpb.js} +1 -1
- package/dist/web/assets/{copy-SBghRY2T.js → copy-CeksexKY.js} +1 -1
- package/dist/web/assets/{diagram-CgEd0hrk.js → diagram-BF8g9yg4.js} +3 -3
- package/dist/web/assets/docPlainCharCount-D3z2Ee8P.js +21 -0
- package/dist/web/assets/{editor-CL03se2B.js → editor-DBWWidi3.js} +1 -1
- package/dist/web/assets/{index-Cwy87sZC.js → index-3-Dt0Ns3.js} +5 -5
- package/dist/web/assets/{meta2d-1-g23JOU.js → meta2d-DP4Vt_ZE.js} +1 -1
- package/dist/web/assets/{meta2dEngine-BD2XbzaV-ulfRZEvk.js → meta2dEngine-BD2XbzaV-CFDBvzS1.js} +1 -1
- package/dist/web/assets/{motion-Dyy8pB0Q.js → motion-BAXVQJfN.js} +1 -1
- package/dist/web/assets/{plus-CEapsZwl.js → plus-Vt2TO_HW.js} +1 -1
- package/dist/web/assets/{react-vendor-BuABiQkA.js → react-vendor-BVS1XoFi.js} +1 -1
- package/dist/web/assets/{react-BkX5Koku.js → react-zRau5ppr.js} +171 -168
- package/dist/web/assets/{ui-ZPt_BNBg.js → ui-CEFkTVnG.js} +200 -181
- package/dist/web/index.html +5 -5
- package/package.json +21 -30
- package/dist/web/assets/DocChrome-QTGER_Af.js +0 -21
- package/dist/web/assets/DocumentEditor-DYSRZnJa.js +0 -1
- package/dist/web/assets/HtmlEditor-oDPrhq3o.js +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
面向个人开发者与小团队:产品内 **智能助手 + 帮写**,外部 **CLI / Skills** 接入 Cursor / Claude;SQLite + 本地文件,**无账户**即可协作。
|
|
6
6
|
|
|
7
|
-
[文档站点](https://xuhuafeifei.github.io/mdocs-site/) · [GitHub](https://github.com/xuhuafeifei/mdocs) · [npm `@fgbg/mdocs`](https://www.npmjs.com/package/@fgbg/mdocs) · 当前版本 **1.3.
|
|
7
|
+
[文档站点](https://xuhuafeifei.github.io/mdocs-site/) · [GitHub](https://github.com/xuhuafeifei/mdocs) · [npm `@fgbg/mdocs`](https://www.npmjs.com/package/@fgbg/mdocs) · 当前版本 **1.3.23**
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<img src="image/site主页.png" alt="mdocs 文档站点首页" width="860" />
|
|
@@ -10,6 +10,26 @@ source: changelog.md
|
|
|
10
10
|
|
|
11
11
|
现行文档里,产品概念「域」已改称 **工作空间**。下面各版本仍按当时的叫法记录。
|
|
12
12
|
|
|
13
|
+
## v1.3.23
|
|
14
|
+
|
|
15
|
+
### 智能助手会话
|
|
16
|
+
- 修复历史会话列表偶发「只剩一条」:`session.json` 读改写改为 Manager 方法级串行队列 + 原子写;读失败时从 `*.jsonl` 恢复索引
|
|
17
|
+
- 运维命令:`mdocs agent session repair --dry-run|--confirm`(可加 `--visitor` / `--kind`),把孤儿 transcript 补回列表
|
|
18
|
+
|
|
19
|
+
### 文档信息
|
|
20
|
+
- 新增 **字符数**:去 Markdown 语法 / HTML 标签后的纯文本总字符(异步统计,回答「写了多少字」)
|
|
21
|
+
|
|
22
|
+
### 语义模型与日志
|
|
23
|
+
- Embedding GGUF 默认目录改为 `~/.mdocs/embedding/`(随 `MDOCS_DATA_DIR`);旧 `~/.fgbg/shared/embedding/` 仅作已下载文件回退
|
|
24
|
+
- 文件日志默认只落 **error+**(`MDOCS_LOG_LEVEL` 可改);路径仍为 `~/.mdocs/logs/`;无前台管理页
|
|
25
|
+
|
|
26
|
+
## v1.3.22
|
|
27
|
+
|
|
28
|
+
### 语义索引
|
|
29
|
+
- 构建过程打进度日志(batch / 文档 / chunk 耗时),方便在 console 观察
|
|
30
|
+
- `MDOCS_DISABLE_EMBEDDING=1`:小内存机器可整路关闭语义(仅关键词 FTS)
|
|
31
|
+
- 缺 `node-llama-cpp` 时日志给出重装指引;`mdocs update` 同步新依赖并补装该包
|
|
32
|
+
|
|
13
33
|
## v1.3.21
|
|
14
34
|
|
|
15
35
|
同 v1.3.20(npm 上 1.3.20 卡在 processing,改发此版本)。
|
|
@@ -77,11 +77,15 @@ mdocs visitor migrate --from Alice --to Bob --dry-run
|
|
|
77
77
|
|
|
78
78
|
# 显式启动服务
|
|
79
79
|
mdocs start
|
|
80
|
+
|
|
81
|
+
# 修复智能助手会话索引(历史列表丢条目时;须 --dry-run 或 --confirm)
|
|
82
|
+
mdocs agent session repair --dry-run
|
|
83
|
+
mdocs agent session repair --confirm
|
|
80
84
|
```
|
|
81
85
|
|
|
82
86
|
## 运行时数据
|
|
83
87
|
|
|
84
|
-
首次启动后,数据目录自动创建在 `~/.mdocs
|
|
88
|
+
首次启动后,数据目录自动创建在 `~/.mdocs/`(可用 `MDOCS_DATA_DIR` 改根目录):
|
|
85
89
|
|
|
86
90
|
```
|
|
87
91
|
~/.mdocs/
|
|
@@ -94,7 +98,9 @@ mdocs start
|
|
|
94
98
|
folder/ # 工作空间内子目录(逻辑存在于数据库)
|
|
95
99
|
b.md
|
|
96
100
|
assets/ # 上传的附件
|
|
97
|
-
logs/ #
|
|
101
|
+
logs/ # 运行日志(默认文件级别 error+,见 MDOCS_LOG_LEVEL)
|
|
102
|
+
embedding/ # 本地语义模型 GGUF(可用 MDOCS_EMBEDDING_DIR 覆盖)
|
|
103
|
+
tenant/ # 访客私有运行时(含 agent 会话 jsonl)
|
|
98
104
|
```
|
|
99
105
|
|
|
100
106
|
文档以 Lexical JSON 格式存储在 `files/docs/` 中,只能由 mdocs 加载编辑。
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { getConfig } from "../../config/index.js";
|
|
5
|
+
import { atomicWriteJson, withSessionMetaLock } from "./session-meta-lock.js";
|
|
5
6
|
const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
7
|
/** 空白帮写(无 documentId)的 lastOpened 键 */
|
|
7
8
|
export const CODING_BLANK_DOC_KEY = "__blank__";
|
|
@@ -96,7 +97,164 @@ export class AgentSessionManager {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
async writeMeta(meta) {
|
|
99
|
-
await
|
|
100
|
+
await atomicWriteJson(this.metaPath, meta);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 从目录内 *.jsonl 重建 sessions 列表(不删文件)。
|
|
104
|
+
* 用于 meta 损坏 / 被写成只剩一条时的恢复。
|
|
105
|
+
*/
|
|
106
|
+
async scanTranscriptEntries() {
|
|
107
|
+
await this.ensureSessionDir();
|
|
108
|
+
let names = [];
|
|
109
|
+
try {
|
|
110
|
+
names = await fs.promises.readdir(this.sessionDir);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const name of names) {
|
|
117
|
+
if (!name.endsWith(".jsonl"))
|
|
118
|
+
continue;
|
|
119
|
+
const id = name.slice(0, -".jsonl".length);
|
|
120
|
+
if (!isSessionId(id))
|
|
121
|
+
continue;
|
|
122
|
+
const full = this.jsonlPath(id);
|
|
123
|
+
let updatedAt = nowIso();
|
|
124
|
+
try {
|
|
125
|
+
const st = await fs.promises.stat(full);
|
|
126
|
+
updatedAt = st.mtime.toISOString();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* ignore */
|
|
130
|
+
}
|
|
131
|
+
const title = await this.peekTitleFromTranscript(id);
|
|
132
|
+
out.push({ id, title, updatedAt });
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
async peekTitleFromTranscript(sessionId) {
|
|
137
|
+
try {
|
|
138
|
+
const txt = await fs.promises.readFile(this.jsonlPath(sessionId), "utf8");
|
|
139
|
+
for (const line of txt.split("\n")) {
|
|
140
|
+
const t = line.trim();
|
|
141
|
+
if (!t)
|
|
142
|
+
continue;
|
|
143
|
+
try {
|
|
144
|
+
const row = JSON.parse(t);
|
|
145
|
+
if (row.role === "user" && typeof row.content === "string" && row.content.trim()) {
|
|
146
|
+
return titleFromUserContent(row.content);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* ignore */
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
/** readMeta 失败或 sessions 空时,用磁盘 jsonl 拼一份可用 meta(不写盘)。 */
|
|
160
|
+
async recoverMetaFromDisk(preferLastOpened) {
|
|
161
|
+
const scanned = await this.scanTranscriptEntries();
|
|
162
|
+
if (scanned.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
const prefer = preferLastOpened && isSessionId(preferLastOpened) && scanned.some((s) => s.id === preferLastOpened)
|
|
165
|
+
? preferLastOpened
|
|
166
|
+
: scanned.slice().sort((a, b) => (a.updatedAt < b.updatedAt ? 1 : -1))[0].id;
|
|
167
|
+
return {
|
|
168
|
+
lastOpenedSessionId: prefer,
|
|
169
|
+
sessions: scanned,
|
|
170
|
+
lastOpenedByDocument: {},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Manager 公开方法级串行入口(队列 key = metaPath)。
|
|
175
|
+
* 所有会动磁盘会话状态的路径只应通过这里,禁止队外裸写。
|
|
176
|
+
*/
|
|
177
|
+
runSerialized(fn) {
|
|
178
|
+
return withSessionMetaLock(this.metaPath, fn);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 已在 runSerialized 内:读改写 meta。
|
|
182
|
+
* read 失败时先从 jsonl 恢复,避免再写成「只剩当前一条」。
|
|
183
|
+
*/
|
|
184
|
+
async mutateMetaUnlocked(mutator) {
|
|
185
|
+
let meta = await this.readMeta();
|
|
186
|
+
if (!meta) {
|
|
187
|
+
meta =
|
|
188
|
+
(await this.recoverMetaFromDisk()) ?? {
|
|
189
|
+
lastOpenedSessionId: "",
|
|
190
|
+
sessions: [],
|
|
191
|
+
lastOpenedByDocument: {},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
await mutator(meta);
|
|
195
|
+
if (!meta.lastOpenedSessionId || !isSessionId(meta.lastOpenedSessionId)) {
|
|
196
|
+
const fallback = meta.sessions[0]?.id ?? randomUUID();
|
|
197
|
+
meta.lastOpenedSessionId = fallback;
|
|
198
|
+
this.ensureSessionEntry(meta, fallback, { touchUpdatedAt: false });
|
|
199
|
+
}
|
|
200
|
+
await this.writeMeta(meta);
|
|
201
|
+
return meta;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 把磁盘上有 jsonl、但 meta.sessions 里没有的条目补回去。
|
|
205
|
+
* @returns 补入条数
|
|
206
|
+
*/
|
|
207
|
+
async repairMetaFromDisk(opts) {
|
|
208
|
+
const dryRun = opts?.dryRun === true;
|
|
209
|
+
await this.ensureSessionDir();
|
|
210
|
+
return this.runSerialized(async () => {
|
|
211
|
+
const existing = await this.readMeta();
|
|
212
|
+
const scanned = await this.scanTranscriptEntries();
|
|
213
|
+
const byId = new Map();
|
|
214
|
+
for (const s of existing?.sessions ?? [])
|
|
215
|
+
byId.set(s.id, { ...s });
|
|
216
|
+
let added = 0;
|
|
217
|
+
for (const s of scanned) {
|
|
218
|
+
if (byId.has(s.id)) {
|
|
219
|
+
const cur = byId.get(s.id);
|
|
220
|
+
if (!cur.title?.trim() && s.title)
|
|
221
|
+
cur.title = s.title;
|
|
222
|
+
if (!cur.updatedAt && s.updatedAt)
|
|
223
|
+
cur.updatedAt = s.updatedAt;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
byId.set(s.id, s);
|
|
227
|
+
added += 1;
|
|
228
|
+
}
|
|
229
|
+
const sessions = Array.from(byId.values());
|
|
230
|
+
const lastOpened = existing?.lastOpenedSessionId && byId.has(existing.lastOpenedSessionId)
|
|
231
|
+
? existing.lastOpenedSessionId
|
|
232
|
+
: sessions.slice().sort((a, b) => ((a.updatedAt ?? "") < (b.updatedAt ?? "") ? 1 : -1))[0]
|
|
233
|
+
?.id ?? randomUUID();
|
|
234
|
+
if (sessions.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
before: existing?.sessions.length ?? 0,
|
|
237
|
+
after: 0,
|
|
238
|
+
added: 0,
|
|
239
|
+
sessionDir: this.sessionDir,
|
|
240
|
+
dryRun,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const next = {
|
|
244
|
+
lastOpenedSessionId: lastOpened,
|
|
245
|
+
sessions,
|
|
246
|
+
lastOpenedByDocument: existing?.lastOpenedByDocument,
|
|
247
|
+
};
|
|
248
|
+
if (!dryRun)
|
|
249
|
+
await this.writeMeta(next);
|
|
250
|
+
return {
|
|
251
|
+
before: existing?.sessions.length ?? 0,
|
|
252
|
+
after: sessions.length,
|
|
253
|
+
added,
|
|
254
|
+
sessionDir: this.sessionDir,
|
|
255
|
+
dryRun,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
100
258
|
}
|
|
101
259
|
ensureSessionEntry(meta, sessionId, patch) {
|
|
102
260
|
const idx = meta.sessions.findIndex((s) => s.id === sessionId);
|
|
@@ -175,134 +333,130 @@ export class AgentSessionManager {
|
|
|
175
333
|
return [];
|
|
176
334
|
}
|
|
177
335
|
}
|
|
178
|
-
/**
|
|
179
|
-
|
|
180
|
-
* coding + documentId:按文档维度;normal:全局 lastOpened。
|
|
181
|
-
*/
|
|
182
|
-
async ensureLastOpened(documentId) {
|
|
183
|
-
await this.ensureSessionDir();
|
|
336
|
+
/** 已在 runSerialized 内:确保 lastOpened。 */
|
|
337
|
+
async ensureLastOpenedUnlocked(documentId) {
|
|
184
338
|
if (this.kind !== "coding") {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.cachedSessionId = sessionId;
|
|
195
|
-
return sessionId;
|
|
196
|
-
}
|
|
197
|
-
this.ensureSessionEntry(meta, meta.lastOpenedSessionId, { touchUpdatedAt: false });
|
|
198
|
-
await this.writeMeta(meta);
|
|
339
|
+
const meta = await this.mutateMetaUnlocked((m) => {
|
|
340
|
+
if (!m.lastOpenedSessionId || !isSessionId(m.lastOpenedSessionId)) {
|
|
341
|
+
const sessionId = m.sessions[0]?.id ?? randomUUID();
|
|
342
|
+
m.lastOpenedSessionId = sessionId;
|
|
343
|
+
this.ensureSessionEntry(m, sessionId, { touchUpdatedAt: false });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.ensureSessionEntry(m, m.lastOpenedSessionId, { touchUpdatedAt: false });
|
|
347
|
+
});
|
|
199
348
|
this.cachedSessionId = meta.lastOpenedSessionId;
|
|
200
349
|
return meta.lastOpenedSessionId;
|
|
201
350
|
}
|
|
202
351
|
const key = docScopeKey(documentId);
|
|
203
|
-
const meta =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
scoped
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.cachedSessionId = sessionId;
|
|
226
|
-
return sessionId;
|
|
227
|
-
}
|
|
228
|
-
const sessionId = randomUUID();
|
|
229
|
-
meta.lastOpenedSessionId = sessionId;
|
|
230
|
-
this.ensureSessionEntry(meta, sessionId, {
|
|
231
|
-
touchUpdatedAt: true,
|
|
232
|
-
documentId: documentId?.trim() ? documentId.trim() : null,
|
|
352
|
+
const meta = await this.mutateMetaUnlocked((m) => {
|
|
353
|
+
const scoped = this.filterByDocument(m.sessions, documentId);
|
|
354
|
+
const mapped = m.lastOpenedByDocument?.[key];
|
|
355
|
+
const mappedOk = mapped && isSessionId(mapped) && scoped.some((s) => s.id === mapped);
|
|
356
|
+
if (mappedOk) {
|
|
357
|
+
m.lastOpenedSessionId = mapped;
|
|
358
|
+
this.ensureSessionEntry(m, mapped, { touchUpdatedAt: false });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (scoped[0]) {
|
|
362
|
+
const sessionId = scoped[0].id;
|
|
363
|
+
m.lastOpenedSessionId = sessionId;
|
|
364
|
+
this.setLastOpenedForDoc(m, documentId, sessionId);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const sessionId = randomUUID();
|
|
368
|
+
m.lastOpenedSessionId = sessionId;
|
|
369
|
+
this.ensureSessionEntry(m, sessionId, {
|
|
370
|
+
touchUpdatedAt: true,
|
|
371
|
+
documentId: documentId?.trim() ? documentId.trim() : null,
|
|
372
|
+
});
|
|
373
|
+
this.setLastOpenedForDoc(m, documentId, sessionId);
|
|
233
374
|
});
|
|
234
|
-
this.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
375
|
+
this.cachedSessionId = meta.lastOpenedSessionId;
|
|
376
|
+
return meta.lastOpenedSessionId;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* 确保有可用 lastOpened。
|
|
380
|
+
* coding + documentId:按文档维度;normal:全局 lastOpened。
|
|
381
|
+
*/
|
|
382
|
+
async ensureLastOpened(documentId) {
|
|
383
|
+
await this.ensureSessionDir();
|
|
384
|
+
if (this.kind !== "coding" && this.cachedSessionId)
|
|
385
|
+
return this.cachedSessionId;
|
|
386
|
+
return this.runSerialized(() => this.ensureLastOpenedUnlocked(documentId));
|
|
238
387
|
}
|
|
239
388
|
async listSessions(documentId) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
389
|
+
await this.ensureSessionDir();
|
|
390
|
+
return this.runSerialized(async () => {
|
|
391
|
+
const lastOpenedSessionId = await this.ensureLastOpenedUnlocked(documentId);
|
|
392
|
+
const meta = await this.readMeta();
|
|
393
|
+
const scoped = this.filterByDocument(meta?.sessions ?? [], documentId);
|
|
394
|
+
return {
|
|
395
|
+
lastOpenedSessionId,
|
|
396
|
+
sessions: this.sortSummaries(scoped),
|
|
397
|
+
};
|
|
398
|
+
});
|
|
247
399
|
}
|
|
248
400
|
async loadLastOpenedMessages(documentId) {
|
|
249
|
-
|
|
250
|
-
return
|
|
401
|
+
await this.ensureSessionDir();
|
|
402
|
+
return this.runSerialized(async () => {
|
|
403
|
+
const sessionId = await this.ensureLastOpenedUnlocked(documentId);
|
|
404
|
+
return { sessionId, messages: await this.loadTranscript(sessionId) };
|
|
405
|
+
});
|
|
251
406
|
}
|
|
252
407
|
/** 新建空会话并设为该文档(或空白桶)的 lastOpened */
|
|
253
408
|
async createSession(documentId) {
|
|
254
409
|
await this.ensureSessionDir();
|
|
255
410
|
const sessionId = randomUUID();
|
|
256
|
-
const meta = (await this.readMeta()) ?? {
|
|
257
|
-
lastOpenedSessionId: sessionId,
|
|
258
|
-
sessions: [],
|
|
259
|
-
lastOpenedByDocument: {},
|
|
260
|
-
};
|
|
261
|
-
meta.lastOpenedSessionId = sessionId;
|
|
262
411
|
const doc = this.kind === "coding"
|
|
263
412
|
? documentId?.trim()
|
|
264
413
|
? documentId.trim()
|
|
265
414
|
: null
|
|
266
415
|
: undefined;
|
|
267
|
-
this.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
416
|
+
return this.runSerialized(async () => {
|
|
417
|
+
await this.mutateMetaUnlocked((meta) => {
|
|
418
|
+
meta.lastOpenedSessionId = sessionId;
|
|
419
|
+
this.ensureSessionEntry(meta, sessionId, {
|
|
420
|
+
title: "新会话",
|
|
421
|
+
touchUpdatedAt: true,
|
|
422
|
+
documentId: doc,
|
|
423
|
+
});
|
|
424
|
+
if (this.kind === "coding") {
|
|
425
|
+
this.setLastOpenedForDoc(meta, documentId, sessionId);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
this.cachedSessionId = sessionId;
|
|
429
|
+
return { sessionId, messages: [] };
|
|
271
430
|
});
|
|
272
|
-
if (this.kind === "coding") {
|
|
273
|
-
this.setLastOpenedForDoc(meta, documentId, sessionId);
|
|
274
|
-
}
|
|
275
|
-
await this.writeMeta(meta);
|
|
276
|
-
this.cachedSessionId = sessionId;
|
|
277
|
-
return { sessionId, messages: [] };
|
|
278
431
|
}
|
|
279
432
|
/** 切换 lastOpened;coding 时可校验属于同一 documentId */
|
|
280
433
|
async openSession(sessionId, documentId) {
|
|
281
434
|
if (!isSessionId(sessionId))
|
|
282
435
|
throw new Error("invalid_session_id");
|
|
283
436
|
await this.ensureSessionDir();
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
437
|
+
return this.runSerialized(async () => {
|
|
438
|
+
await this.mutateMetaUnlocked((meta) => {
|
|
439
|
+
const inMeta = meta.sessions.some((s) => s.id === sessionId);
|
|
440
|
+
const hasFile = fs.existsSync(this.jsonlPath(sessionId));
|
|
441
|
+
if (!inMeta && !hasFile)
|
|
442
|
+
throw new Error("session_not_found");
|
|
443
|
+
const entry = meta.sessions.find((s) => s.id === sessionId);
|
|
444
|
+
if (this.kind === "coding" && documentId !== undefined) {
|
|
445
|
+
const want = docScopeKey(documentId);
|
|
446
|
+
const have = entry ? entryDocKey(entry) : CODING_BLANK_DOC_KEY;
|
|
447
|
+
if (want !== have)
|
|
448
|
+
throw new Error("session_document_mismatch");
|
|
449
|
+
}
|
|
450
|
+
meta.lastOpenedSessionId = sessionId;
|
|
451
|
+
this.ensureSessionEntry(meta, sessionId, { touchUpdatedAt: true });
|
|
452
|
+
if (this.kind === "coding") {
|
|
453
|
+
const bindDoc = entry?.documentId ?? (documentId !== undefined ? documentId : null);
|
|
454
|
+
this.setLastOpenedForDoc(meta, bindDoc, sessionId);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
this.cachedSessionId = sessionId;
|
|
458
|
+
return { sessionId, messages: await this.loadTranscript(sessionId) };
|
|
459
|
+
});
|
|
306
460
|
}
|
|
307
461
|
/**
|
|
308
462
|
* 空白帮写写回成文后:把 session 绑到新 documentId,并设为该文 lastOpened。
|
|
@@ -316,49 +470,43 @@ export class AgentSessionManager {
|
|
|
316
470
|
if (!doc)
|
|
317
471
|
throw new Error("document_id_required");
|
|
318
472
|
await this.ensureSessionDir();
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
473
|
+
return this.runSerialized(async () => {
|
|
474
|
+
await this.mutateMetaUnlocked((meta) => {
|
|
475
|
+
const inMeta = meta.sessions.some((s) => s.id === sessionId);
|
|
476
|
+
const hasFile = fs.existsSync(this.jsonlPath(sessionId));
|
|
477
|
+
if (!inMeta && !hasFile)
|
|
478
|
+
throw new Error("session_not_found");
|
|
479
|
+
this.ensureSessionEntry(meta, sessionId, {
|
|
480
|
+
documentId: doc,
|
|
481
|
+
touchUpdatedAt: true,
|
|
482
|
+
});
|
|
483
|
+
meta.lastOpenedSessionId = sessionId;
|
|
484
|
+
this.setLastOpenedForDoc(meta, doc, sessionId);
|
|
485
|
+
if (meta.lastOpenedByDocument?.[CODING_BLANK_DOC_KEY] === sessionId) {
|
|
486
|
+
delete meta.lastOpenedByDocument[CODING_BLANK_DOC_KEY];
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
this.cachedSessionId = sessionId;
|
|
490
|
+
return { sessionId, documentId: doc };
|
|
331
491
|
});
|
|
332
|
-
meta.lastOpenedSessionId = sessionId;
|
|
333
|
-
this.setLastOpenedForDoc(meta, doc, sessionId);
|
|
334
|
-
// 从空白桶 lastOpened 清掉自己(若仍指向本 session)
|
|
335
|
-
if (meta.lastOpenedByDocument?.[CODING_BLANK_DOC_KEY] === sessionId) {
|
|
336
|
-
delete meta.lastOpenedByDocument[CODING_BLANK_DOC_KEY];
|
|
337
|
-
}
|
|
338
|
-
await this.writeMeta(meta);
|
|
339
|
-
this.cachedSessionId = sessionId;
|
|
340
|
-
return { sessionId, documentId: doc };
|
|
341
492
|
}
|
|
342
|
-
async
|
|
343
|
-
|
|
344
|
-
lastOpenedSessionId
|
|
345
|
-
sessions
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
493
|
+
async touchLastOpenedUnlocked(sessionId, patch) {
|
|
494
|
+
await this.mutateMetaUnlocked((meta) => {
|
|
495
|
+
meta.lastOpenedSessionId = sessionId;
|
|
496
|
+
const existing = meta.sessions.find((s) => s.id === sessionId);
|
|
497
|
+
const needTitle = patch?.titleIfEmpty &&
|
|
498
|
+
!(existing?.title && existing.title.trim() && existing.title !== "新会话");
|
|
499
|
+
this.ensureSessionEntry(meta, sessionId, {
|
|
500
|
+
title: needTitle ? patch.titleIfEmpty : undefined,
|
|
501
|
+
touchUpdatedAt: true,
|
|
502
|
+
});
|
|
503
|
+
if (this.kind === "coding") {
|
|
504
|
+
this.setLastOpenedForDoc(meta, existing?.documentId ?? null, sessionId);
|
|
505
|
+
}
|
|
355
506
|
});
|
|
356
|
-
if (this.kind === "coding") {
|
|
357
|
-
this.setLastOpenedForDoc(meta, existing?.documentId ?? null, sessionId);
|
|
358
|
-
}
|
|
359
|
-
await this.writeMeta(meta);
|
|
360
507
|
this.cachedSessionId = sessionId;
|
|
361
508
|
}
|
|
509
|
+
/** jsonl append + touch meta 同一 Manager 队列任务。 */
|
|
362
510
|
async appendUser(sessionId, content, skillNames) {
|
|
363
511
|
if (!isSessionId(sessionId))
|
|
364
512
|
throw new Error("invalid_session_id");
|
|
@@ -370,14 +518,20 @@ export class AgentSessionManager {
|
|
|
370
518
|
const row = { role: "user", content, ts: nowIso() };
|
|
371
519
|
if (names.length > 0)
|
|
372
520
|
row.skillNames = names;
|
|
373
|
-
await
|
|
374
|
-
|
|
521
|
+
await this.runSerialized(async () => {
|
|
522
|
+
await fs.promises.appendFile(this.jsonlPath(sessionId), JSON.stringify(row) + "\n", "utf8");
|
|
523
|
+
await this.touchLastOpenedUnlocked(sessionId, {
|
|
524
|
+
titleIfEmpty: titleFromUserContent(content),
|
|
525
|
+
});
|
|
526
|
+
});
|
|
375
527
|
}
|
|
376
528
|
async appendAssistant(sessionId, content) {
|
|
377
529
|
if (!isSessionId(sessionId))
|
|
378
530
|
throw new Error("invalid_session_id");
|
|
379
|
-
await
|
|
380
|
-
|
|
531
|
+
await this.runSerialized(async () => {
|
|
532
|
+
await fs.promises.appendFile(this.jsonlPath(sessionId), JSON.stringify({ role: "assistant", content, ts: nowIso() }) + "\n", "utf8");
|
|
533
|
+
await this.touchLastOpenedUnlocked(sessionId);
|
|
534
|
+
});
|
|
381
535
|
}
|
|
382
536
|
}
|
|
383
537
|
export async function getAgentSessionForApi(visitorId, kind = "normal", documentId) {
|
|
@@ -395,4 +549,60 @@ export async function openAgentSessionForApi(visitorId, sessionId, kind = "norma
|
|
|
395
549
|
export async function bindCodingSessionDocumentForApi(visitorId, sessionId, documentId) {
|
|
396
550
|
return new AgentSessionManager(visitorId, { kind: "coding" }).bindSessionDocument(sessionId, documentId);
|
|
397
551
|
}
|
|
552
|
+
/** 修复单个访客的 normal / coding session meta(按 kind)。 */
|
|
553
|
+
export async function repairAgentSessionsForVisitor(params) {
|
|
554
|
+
const kinds = params.kind === "coding"
|
|
555
|
+
? ["coding"]
|
|
556
|
+
: params.kind === "normal"
|
|
557
|
+
? ["normal"]
|
|
558
|
+
: ["normal", "coding"];
|
|
559
|
+
const rows = [];
|
|
560
|
+
for (const kind of kinds) {
|
|
561
|
+
const mgr = new AgentSessionManager(params.visitorId, { kind });
|
|
562
|
+
const r = await mgr.repairMetaFromDisk({ dryRun: params.dryRun });
|
|
563
|
+
rows.push({
|
|
564
|
+
visitorId: params.visitorId,
|
|
565
|
+
kind,
|
|
566
|
+
sessionDir: r.sessionDir,
|
|
567
|
+
before: r.before,
|
|
568
|
+
after: r.after,
|
|
569
|
+
added: r.added,
|
|
570
|
+
dryRun: r.dryRun,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return rows;
|
|
574
|
+
}
|
|
575
|
+
/** 扫描 dataDir/tenant/*,修复全部(或指定)访客的 session 索引。 */
|
|
576
|
+
export async function repairAgentSessionsUnderDataDir(params) {
|
|
577
|
+
const { dataDir } = getConfig();
|
|
578
|
+
const tenantRoot = path.join(dataDir, "tenant");
|
|
579
|
+
let visitorIds = [];
|
|
580
|
+
if (params.visitorId) {
|
|
581
|
+
visitorIds = [params.visitorId];
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
try {
|
|
585
|
+
visitorIds = (await fs.promises.readdir(tenantRoot)).filter((name) => {
|
|
586
|
+
try {
|
|
587
|
+
return fs.statSync(path.join(tenantRoot, name)).isDirectory();
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const out = [];
|
|
599
|
+
for (const visitorId of visitorIds) {
|
|
600
|
+
out.push(...(await repairAgentSessionsForVisitor({
|
|
601
|
+
visitorId,
|
|
602
|
+
kind: params.kind ?? "all",
|
|
603
|
+
dryRun: params.dryRun,
|
|
604
|
+
})));
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
398
608
|
//# sourceMappingURL=session-manager.js.map
|