dsh-trivium 0.4.1 → 0.4.3
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 +13 -13
- package/lib/client.js +331 -29
- package/lib/episode.js +42 -0
- package/lib/extract.js +10 -6
- package/lib/pins.js +103 -1
- package/lib/server.js +65 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ dsh plugin --profile web add dsh-trivium
|
|
|
54
54
|
|
|
55
55
|
- **跨会话** — 会话 A 记下「鉴权走 header X」,会话 B 调用 `ctx_find("鉴权")` 命中,并带上它连着谁(`about` / `decided` / `broke` / `fixed`)。
|
|
56
56
|
- **默认安静** — 新会话只注入一张短地图(≤400 token)。模型要用再调工具,不会每一步灌满。
|
|
57
|
-
- **会话图** — compaction
|
|
58
|
-
- **人能改错** —
|
|
59
|
-
- **抽取偏严** —
|
|
57
|
+
- **会话图** — compaction 切一段就收成一个方框;旧会话可点「更新检查点」补上已经发生过的压缩 / 分叉;勾选的记忆芯片从当前这段的下一轮注入。
|
|
58
|
+
- **人能改错** — 会话图芯片条可新增、归档、删除;设置页管搜、改名 / 正文、合并、全局开关。
|
|
59
|
+
- **抽取偏严** — 闲聊、一次性改文件、密钥**不会从对话自动入库**。芯片「新增」按你写的存(点一次整段一条)。
|
|
60
60
|
- **可选 embedding** — 默认关。官方 DeepSeek 对话接口没有 embeddings;可填 OpenAI 兼容地址。
|
|
61
61
|
|
|
62
62
|
### 四个工具
|
|
@@ -74,12 +74,13 @@ dsh plugin --profile web add dsh-trivium
|
|
|
74
74
|
|
|
75
75
|
## 会话图
|
|
76
76
|
|
|
77
|
-
入口:当前会话标题栏 **对话 / 轨迹 /
|
|
77
|
+
入口:当前会话标题栏 **对话 / 轨迹 / 会话图**。这是情节图,不是记忆编辑器;改名、合并、全局开关仍走设置页。芯片条上可以直接新增、归档、删除。
|
|
78
78
|
|
|
79
79
|
### 方框
|
|
80
80
|
|
|
81
81
|
- 没压过上下文:只有一个「后续」方框。
|
|
82
82
|
- 一次成功的 compaction 之后:左边历史检查点(摘要来自 DSH),右边仍是「后续」。
|
|
83
|
+
- 装插件之前已经压过 / 分过叉的旧会话:点「更新检查点」,从当前对话里的压缩标记和侧边栏分叉补方框(可重复点,不会双写)。没压过就仍只有「后续」——会话长、工具多不等于 DSH 已经压缩(默认约窗口 80% 才自动压)。
|
|
83
84
|
- 方框自己不压缩。质量来自 DSH 已经做的摘要替换;fork 时不会再总结一遍。
|
|
84
85
|
- 「改名」只改方框标题,不动 DSH 摘要。点方框可跳回对话里对应的检查点,或打开轨迹。
|
|
85
86
|
|
|
@@ -101,6 +102,9 @@ dsh plugin --profile web add dsh-trivium
|
|
|
101
102
|
|---|---|
|
|
102
103
|
| 勾上某条 | 当前这段里你再发的**下一条**带上它(L0,≤300 token) |
|
|
103
104
|
| 取消勾选 | 再下一轮不再带 |
|
|
105
|
+
| 新增 | 点一次写入一条;整段粘贴也是一条 |
|
|
106
|
+
| 选择 → 归档 | 软删除:find / 短地图 / 芯片不再出现,节点还在 `.tdb` |
|
|
107
|
+
| 选择 → 删除 | 从 `.tdb` 去掉,不可恢复 |
|
|
104
108
|
| 已经发出去的轮次 | 不回写 |
|
|
105
109
|
|
|
106
110
|
芯片按会话隔离。新开会话、fork 出的子会话默认都不勾;需要的话在分叉对话框旁打开「分叉时继承钉选」,或到子会话里重新勾。短地图仍在 session-start 自动带,不占芯片位。建议标记只是提示,不会自动勾上。
|
|
@@ -140,10 +144,10 @@ dsh plugin --profile web add dsh-trivium
|
|
|
140
144
|
## 限制
|
|
141
145
|
|
|
142
146
|
- 模型不调 `ctx_find`、也不勾芯片时,窗口里主要是开场短地图。
|
|
143
|
-
-
|
|
147
|
+
- 抽取会漏;脏数据可在芯片条归档 / 删除,或到设置里改、合并。
|
|
144
148
|
- 同一个 `.tdb` 不要两个 Node 进程同时打开(正常只开一个 `dsh web` 即可)。
|
|
145
149
|
- Markdown 导出是给人看的,不能再解析回去。WorkBuddy 导入是一次性,不是双向同步。
|
|
146
|
-
- 会话图只投影 compaction 与 fork
|
|
150
|
+
- 会话图只投影 compaction 与 fork,不会按消息条数自己切方框。窗口没到 DSH 压缩线时,「更新检查点」也补不出历史方框。
|
|
147
151
|
- 改插件设置后需要重启 `dsh web`。
|
|
148
152
|
- 宿主升到 rc.8 后,DSH 自己的旧会话库可能打不开(官方 SQLite 格式不兼容)。工作区里的 `.tdb` 图记忆还在;对不上号的旧会话图方框可以当情节残留,不影响 `ctx_find`。
|
|
149
153
|
|
|
@@ -151,13 +155,9 @@ dsh plugin --profile web add dsh-trivium
|
|
|
151
155
|
|
|
152
156
|
## 更新说明
|
|
153
157
|
|
|
154
|
-
**0.4.
|
|
155
|
-
|
|
156
|
-
**0.4.0** — 会话图、记忆芯片、从检查点 fork。
|
|
158
|
+
**0.4.3** — 会话图「更新检查点」:把当前对话里 DSH 已有的压缩标记和侧边栏 fork 补进图,给装插件之前的旧会话用。没压过的会话仍然只有「后续」。
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
## 发布信息
|
|
160
|
+
**0.4.2** — 会话图芯片条可新增(整段粘贴一条)、批量归档 / 删除。自动抽取仍偏严;芯片新增按你写的入库。
|
|
161
161
|
|
|
162
162
|
**0.4.1** — 宿主钉到 `@deepseek-ai/dsh@0.1.0-rc.8`。功能与 0.4.0 相同。已在 rc.8 上确认:设置「Trivium 记忆」、新会话短地图、跨会话 `ctx_find`、会话图标签。
|
|
163
163
|
|
|
@@ -168,6 +168,6 @@ dsh plugin --profile web add dsh-trivium
|
|
|
168
168
|
## 发布信息
|
|
169
169
|
|
|
170
170
|
- GitHub: https://github.com/QWQcool/dsh-trivium
|
|
171
|
-
- npm: [`dsh-trivium@0.4.
|
|
171
|
+
- npm: [`dsh-trivium@0.4.3`](https://www.npmjs.com/package/dsh-trivium)
|
|
172
172
|
- 测试宿主:`@deepseek-ai/dsh@0.1.0-rc.8`
|
|
173
173
|
- License: MIT(依赖 [TriviumDB](https://github.com/TriviumDB/triviumdb) 为 Apache-2.0)
|
package/lib/client.js
CHANGED
|
@@ -980,6 +980,67 @@ window.__ModuleLoader__.load({
|
|
|
980
980
|
return best;
|
|
981
981
|
}
|
|
982
982
|
|
|
983
|
+
function collectChatCompactions(chat) {
|
|
984
|
+
const nodes = chat?.nodes;
|
|
985
|
+
if (!nodes || typeof nodes.values !== "function") return [];
|
|
986
|
+
const rows = [];
|
|
987
|
+
const seen = new Set();
|
|
988
|
+
for (const n of nodes.values()) {
|
|
989
|
+
const kind = n.kind;
|
|
990
|
+
const data = n.data || {};
|
|
991
|
+
let seq;
|
|
992
|
+
let summary = "";
|
|
993
|
+
let compactionId = "";
|
|
994
|
+
if (kind === "compaction") {
|
|
995
|
+
seq = Number(n.anchorSeq ?? data.seq);
|
|
996
|
+
summary = String(data.summary || "");
|
|
997
|
+
compactionId = String(data.compactionId || data.id || "");
|
|
998
|
+
} else if (kind === "manual-compaction") {
|
|
999
|
+
const landed = data.compaction;
|
|
1000
|
+
if (!landed) continue;
|
|
1001
|
+
seq = Number(landed.seq ?? n.anchorSeq ?? data.command?.seq);
|
|
1002
|
+
summary = String(landed.summary || "");
|
|
1003
|
+
compactionId = String(landed.compactionId || data.command?.commandId || "");
|
|
1004
|
+
} else continue;
|
|
1005
|
+
if (!Number.isFinite(seq) || seen.has(seq)) continue;
|
|
1006
|
+
seen.add(seq);
|
|
1007
|
+
rows.push({ atSeq: seq, compactionId, summary });
|
|
1008
|
+
}
|
|
1009
|
+
return rows.sort((a, b) => a.atSeq - b.atSeq);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function parentIdOf(row) {
|
|
1013
|
+
return String(row?.parentSessionId || row?.parentId || "");
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function collectSessionForks(sessionId, byId, seedLengthOf) {
|
|
1017
|
+
const forks = [];
|
|
1018
|
+
const seen = new Set();
|
|
1019
|
+
const add = (childId, parentId) => {
|
|
1020
|
+
const child = String(childId || "");
|
|
1021
|
+
const parent = String(parentId || "");
|
|
1022
|
+
if (!child || !parent || seen.has(child + ":" + parent)) return;
|
|
1023
|
+
const row = byId[child];
|
|
1024
|
+
if (row?.origin === "subagent") return;
|
|
1025
|
+
seen.add(child + ":" + parent);
|
|
1026
|
+
const seed = typeof seedLengthOf === "function" ? seedLengthOf(child) : null;
|
|
1027
|
+
const n = Number(seed);
|
|
1028
|
+
forks.push({
|
|
1029
|
+
childSessionId: child,
|
|
1030
|
+
parentSessionId: parent,
|
|
1031
|
+
atSeq: Number.isFinite(n) && n > 0 ? n - 1 : undefined,
|
|
1032
|
+
});
|
|
1033
|
+
};
|
|
1034
|
+
const mine = String(sessionId || "");
|
|
1035
|
+
const rows = byId || {};
|
|
1036
|
+
add(mine, parentIdOf(rows[mine]));
|
|
1037
|
+
for (const id of Object.keys(rows)) {
|
|
1038
|
+
const parent = parentIdOf(rows[id]);
|
|
1039
|
+
if (parent && (parent === mine || id === mine)) add(id, parent);
|
|
1040
|
+
}
|
|
1041
|
+
return forks;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
983
1044
|
function scrollConversation({ key, bottom }) {
|
|
984
1045
|
let n = 0;
|
|
985
1046
|
const tick = () => {
|
|
@@ -1238,6 +1299,8 @@ window.__ModuleLoader__.load({
|
|
|
1238
1299
|
position: "relative",
|
|
1239
1300
|
},
|
|
1240
1301
|
chipsHead: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" },
|
|
1302
|
+
chipsToolbar: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
|
|
1303
|
+
chipsActions: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
|
|
1241
1304
|
chipsTitle: { fontSize: 13, fontWeight: 650 },
|
|
1242
1305
|
hint: { fontSize: 12, opacity: 0.62 },
|
|
1243
1306
|
chips: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" },
|
|
@@ -1255,16 +1318,50 @@ window.__ModuleLoader__.load({
|
|
|
1255
1318
|
chipSuggest: { borderColor: "currentColor", background: "rgba(127,127,127,0.08)" },
|
|
1256
1319
|
suggest: { fontSize: 10, fontWeight: 650, opacity: 0.78, flexShrink: 0 },
|
|
1257
1320
|
search: {
|
|
1258
|
-
width:
|
|
1321
|
+
width: 200,
|
|
1259
1322
|
boxSizing: "border-box",
|
|
1260
|
-
fontSize:
|
|
1261
|
-
padding: "
|
|
1323
|
+
fontSize: 13,
|
|
1324
|
+
padding: "6px 10px",
|
|
1262
1325
|
borderRadius: 8,
|
|
1263
1326
|
border: "1px solid rgba(127,127,127,0.28)",
|
|
1264
1327
|
background: "transparent",
|
|
1265
1328
|
color: "inherit",
|
|
1266
1329
|
},
|
|
1267
1330
|
inherit: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, cursor: "pointer", margin: 0 },
|
|
1331
|
+
chipPicked: { borderColor: "currentColor", background: "rgba(127,127,127,0.18)" },
|
|
1332
|
+
draftBox: {
|
|
1333
|
+
width: "100%",
|
|
1334
|
+
minHeight: 96,
|
|
1335
|
+
boxSizing: "border-box",
|
|
1336
|
+
fontSize: 13,
|
|
1337
|
+
padding: "8px 10px",
|
|
1338
|
+
borderRadius: 8,
|
|
1339
|
+
border: "1px solid rgba(127,127,127,0.28)",
|
|
1340
|
+
background: "transparent",
|
|
1341
|
+
color: "inherit",
|
|
1342
|
+
resize: "vertical",
|
|
1343
|
+
fontFamily: "inherit",
|
|
1344
|
+
},
|
|
1345
|
+
actionBtn: {
|
|
1346
|
+
fontSize: 13,
|
|
1347
|
+
padding: "6px 14px",
|
|
1348
|
+
borderRadius: 8,
|
|
1349
|
+
border: "1px solid rgba(127,127,127,0.35)",
|
|
1350
|
+
background: "rgba(127,127,127,0.10)",
|
|
1351
|
+
color: "inherit",
|
|
1352
|
+
cursor: "pointer",
|
|
1353
|
+
flexShrink: 0,
|
|
1354
|
+
},
|
|
1355
|
+
dangerBtn: {
|
|
1356
|
+
fontSize: 13,
|
|
1357
|
+
padding: "6px 14px",
|
|
1358
|
+
borderRadius: 8,
|
|
1359
|
+
border: "1px solid rgba(180,60,60,0.45)",
|
|
1360
|
+
background: "transparent",
|
|
1361
|
+
color: "inherit",
|
|
1362
|
+
cursor: "pointer",
|
|
1363
|
+
flexShrink: 0,
|
|
1364
|
+
},
|
|
1268
1365
|
titleRow: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 },
|
|
1269
1366
|
title: {
|
|
1270
1367
|
fontSize: 14,
|
|
@@ -1633,7 +1730,7 @@ window.__ModuleLoader__.load({
|
|
|
1633
1730
|
|
|
1634
1731
|
let pendingSessionNav = null;
|
|
1635
1732
|
|
|
1636
|
-
function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions }) {
|
|
1733
|
+
function SessionMapView({ sessionId, useSessions, useSession, fork, open, renameSession, actions, seedLengthOf }) {
|
|
1637
1734
|
const canSetView = typeof actions?.setView === "function";
|
|
1638
1735
|
const chat = useSession((s) => s.chat);
|
|
1639
1736
|
const cwd = useSessions((s) => (sessionId == null ? "" : s.byId[sessionId]?.cwd || ""));
|
|
@@ -1660,7 +1757,12 @@ window.__ModuleLoader__.load({
|
|
|
1660
1757
|
const [chipQuery, setChipQuery] = react.useState("");
|
|
1661
1758
|
const [inheritPins, setInheritPins] = react.useState(false);
|
|
1662
1759
|
const [chipsOpen, setChipsOpen] = react.useState(false);
|
|
1760
|
+
const [chipSelect, setChipSelect] = react.useState(false);
|
|
1761
|
+
const [pickedIds, setPickedIds] = react.useState([]);
|
|
1762
|
+
const [draftOpen, setDraftOpen] = react.useState(false);
|
|
1763
|
+
const [draftText, setDraftText] = react.useState("");
|
|
1663
1764
|
const [forkDraft, setForkDraft] = react.useState(null);
|
|
1765
|
+
const [backfillNote, setBackfillNote] = react.useState("");
|
|
1664
1766
|
const canvasRef = react.useRef(null);
|
|
1665
1767
|
const dragRef = react.useRef(null);
|
|
1666
1768
|
const userMovedRef = react.useRef(false);
|
|
@@ -1719,6 +1821,34 @@ window.__ModuleLoader__.load({
|
|
|
1719
1821
|
}
|
|
1720
1822
|
}, [qs, sessionId]);
|
|
1721
1823
|
|
|
1824
|
+
const syncCheckpoints = async () => {
|
|
1825
|
+
if (!sessionId || busy) return;
|
|
1826
|
+
setBusy(true);
|
|
1827
|
+
setError("");
|
|
1828
|
+
try {
|
|
1829
|
+
const compactations = collectChatCompactions(chat);
|
|
1830
|
+
const forks = collectSessionForks(sessionId, sessionById, seedLengthOf);
|
|
1831
|
+
const resp = await api("/api/dsh-trivium/map/backfill" + qs, {
|
|
1832
|
+
method: "POST",
|
|
1833
|
+
headers: { "content-type": "application/json" },
|
|
1834
|
+
body: JSON.stringify({ sessionId, compactations, forks }),
|
|
1835
|
+
});
|
|
1836
|
+
setNodes(resp.nodes || []);
|
|
1837
|
+
setEdges(resp.edges || []);
|
|
1838
|
+
const checks = Number(resp.checkpoints) || 0;
|
|
1839
|
+
const forkN = Number(resp.forks) || 0;
|
|
1840
|
+
if (!checks && !forkN) {
|
|
1841
|
+
setBackfillNote("没有发现压缩标记或分叉。没压过的会话本来就只有「后续」。");
|
|
1842
|
+
} else {
|
|
1843
|
+
setBackfillNote("已同步 " + checks + " 个检查点、" + forkN + " 条分叉。");
|
|
1844
|
+
}
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
setError(String(err.message || err));
|
|
1847
|
+
} finally {
|
|
1848
|
+
setBusy(false);
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1722
1852
|
react.useEffect(() => {
|
|
1723
1853
|
load();
|
|
1724
1854
|
}, [load, lineageKey]);
|
|
@@ -1924,6 +2054,67 @@ window.__ModuleLoader__.load({
|
|
|
1924
2054
|
}
|
|
1925
2055
|
};
|
|
1926
2056
|
|
|
2057
|
+
const qsPins = cwd ? "?cwd=" + encodeURIComponent(cwd) : "";
|
|
2058
|
+
|
|
2059
|
+
const applyChipPayload = (payload) => {
|
|
2060
|
+
if (Array.isArray(payload.chips)) setChips(payload.chips);
|
|
2061
|
+
if (Array.isArray(payload.pinIds)) setPinIds(payload.pinIds);
|
|
2062
|
+
if (payload.clipped != null) setClipped(!!payload.clipped);
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
const togglePicked = (id) => {
|
|
2066
|
+
setPickedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : prev.concat(id)));
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
const runChipBatch = async (action) => {
|
|
2070
|
+
if (!pickedIds.length || busy) return;
|
|
2071
|
+
if (action === "delete") {
|
|
2072
|
+
const ok = window.confirm(
|
|
2073
|
+
"删除会从 .tdb 里拿掉这 " +
|
|
2074
|
+
pickedIds.length +
|
|
2075
|
+
" 条,不可恢复。归档只是藏起来,find / 短地图 / 芯片都不再出现。确定删除?",
|
|
2076
|
+
);
|
|
2077
|
+
if (!ok) return;
|
|
2078
|
+
}
|
|
2079
|
+
setBusy(true);
|
|
2080
|
+
setError("");
|
|
2081
|
+
try {
|
|
2082
|
+
const resp = await api("/api/dsh-trivium/chips/batch" + qsPins, {
|
|
2083
|
+
method: "POST",
|
|
2084
|
+
headers: { "content-type": "application/json" },
|
|
2085
|
+
body: JSON.stringify({ sessionId, action, ids: pickedIds }),
|
|
2086
|
+
});
|
|
2087
|
+
applyChipPayload(resp);
|
|
2088
|
+
setPickedIds([]);
|
|
2089
|
+
setChipSelect(false);
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
setError(String(err.message || err));
|
|
2092
|
+
} finally {
|
|
2093
|
+
setBusy(false);
|
|
2094
|
+
}
|
|
2095
|
+
};
|
|
2096
|
+
|
|
2097
|
+
const submitChipDraft = async () => {
|
|
2098
|
+
const text = draftText.trim();
|
|
2099
|
+
if (!text || busy) return;
|
|
2100
|
+
setBusy(true);
|
|
2101
|
+
setError("");
|
|
2102
|
+
try {
|
|
2103
|
+
const resp = await api("/api/dsh-trivium/chips" + qsPins, {
|
|
2104
|
+
method: "POST",
|
|
2105
|
+
headers: { "content-type": "application/json" },
|
|
2106
|
+
body: JSON.stringify({ sessionId, text, pin: true }),
|
|
2107
|
+
});
|
|
2108
|
+
applyChipPayload(resp);
|
|
2109
|
+
setDraftText("");
|
|
2110
|
+
setDraftOpen(false);
|
|
2111
|
+
} catch (err) {
|
|
2112
|
+
setError(String(err.message || err));
|
|
2113
|
+
} finally {
|
|
2114
|
+
setBusy(false);
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
|
|
1927
2118
|
const pinSet = new Set(pinIds);
|
|
1928
2119
|
const visibleChips = react.useMemo(() => {
|
|
1929
2120
|
const needle = chipQuery.trim().toLowerCase();
|
|
@@ -1954,17 +2145,31 @@ window.__ModuleLoader__.load({
|
|
|
1954
2145
|
jsx("div", {
|
|
1955
2146
|
style: mapUi.hint,
|
|
1956
2147
|
children:
|
|
2148
|
+
backfillNote ||
|
|
1957
2149
|
"共 " +
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2150
|
+
chips.length +
|
|
2151
|
+
" 条,已钉 " +
|
|
2152
|
+
pinIds.length +
|
|
2153
|
+
"。默认不勾选:不钉就不会按芯片白名单注入,但仍可能被开场短地图点名带上,模型也能 find 到。",
|
|
1962
2154
|
}),
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2155
|
+
jsxs("div", {
|
|
2156
|
+
style: mapUi.chipsActions,
|
|
2157
|
+
children: [
|
|
2158
|
+
jsx("button", {
|
|
2159
|
+
type: "button",
|
|
2160
|
+
style: mapUi.actionBtn,
|
|
2161
|
+
disabled: busy,
|
|
2162
|
+
title: "读取当前对话里 DSH 已有的压缩标记和侧边栏分叉,生成检查点方框。给插件装上之前的旧会话用。",
|
|
2163
|
+
onClick: syncCheckpoints,
|
|
2164
|
+
children: busy ? "更新中…" : "更新检查点",
|
|
2165
|
+
}),
|
|
2166
|
+
jsx("button", {
|
|
2167
|
+
type: "button",
|
|
2168
|
+
style: mapUi.actionBtn,
|
|
2169
|
+
onClick: () => setChipsOpen((v) => !v),
|
|
2170
|
+
children: chipsOpen ? "收起" : "展开勾选",
|
|
2171
|
+
}),
|
|
2172
|
+
],
|
|
1968
2173
|
}),
|
|
1969
2174
|
],
|
|
1970
2175
|
}),
|
|
@@ -1973,7 +2178,7 @@ window.__ModuleLoader__.load({
|
|
|
1973
2178
|
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
1974
2179
|
children: [
|
|
1975
2180
|
jsxs("div", {
|
|
1976
|
-
style: mapUi.
|
|
2181
|
+
style: mapUi.chipsToolbar,
|
|
1977
2182
|
children: [
|
|
1978
2183
|
jsx("input", {
|
|
1979
2184
|
type: "search",
|
|
@@ -1993,25 +2198,113 @@ window.__ModuleLoader__.load({
|
|
|
1993
2198
|
"分叉时继承钉选",
|
|
1994
2199
|
],
|
|
1995
2200
|
}),
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2201
|
+
jsxs("div", {
|
|
2202
|
+
style: mapUi.chipsActions,
|
|
2203
|
+
children: [
|
|
2204
|
+
jsx("button", {
|
|
2205
|
+
type: "button",
|
|
2206
|
+
style: mapUi.actionBtn,
|
|
2207
|
+
onClick: () => {
|
|
2208
|
+
setDraftOpen((v) => !v);
|
|
2209
|
+
setChipSelect(false);
|
|
2210
|
+
setPickedIds([]);
|
|
2211
|
+
},
|
|
2212
|
+
children: draftOpen ? "取消新增" : "新增",
|
|
2213
|
+
}),
|
|
2214
|
+
jsx("button", {
|
|
2215
|
+
type: "button",
|
|
2216
|
+
style: mapUi.actionBtn,
|
|
2217
|
+
onClick: () => {
|
|
2218
|
+
setChipSelect((v) => !v);
|
|
2219
|
+
setPickedIds([]);
|
|
2220
|
+
setDraftOpen(false);
|
|
2221
|
+
},
|
|
2222
|
+
children: chipSelect ? "取消选择" : "选择",
|
|
2223
|
+
}),
|
|
2224
|
+
chipSelect
|
|
2225
|
+
? jsx("button", {
|
|
2226
|
+
type: "button",
|
|
2227
|
+
style: mapUi.actionBtn,
|
|
2228
|
+
disabled: !pickedIds.length || busy,
|
|
2229
|
+
onClick: () => runChipBatch("archive"),
|
|
2230
|
+
children: "归档" + (pickedIds.length ? " " + pickedIds.length : ""),
|
|
2231
|
+
})
|
|
2232
|
+
: null,
|
|
2233
|
+
chipSelect
|
|
2234
|
+
? jsx("button", {
|
|
2235
|
+
type: "button",
|
|
2236
|
+
style: mapUi.dangerBtn,
|
|
2237
|
+
disabled: !pickedIds.length || busy,
|
|
2238
|
+
onClick: () => runChipBatch("delete"),
|
|
2239
|
+
children: "删除" + (pickedIds.length ? " " + pickedIds.length : ""),
|
|
2240
|
+
})
|
|
2241
|
+
: null,
|
|
2242
|
+
],
|
|
2243
|
+
}),
|
|
1999
2244
|
],
|
|
2000
2245
|
}),
|
|
2246
|
+
jsx("div", {
|
|
2247
|
+
style: mapUi.hint,
|
|
2248
|
+
children: chipSelect
|
|
2249
|
+
? "点芯片多选。归档:软删除,find / 短地图 / 芯片不再出现,节点还在库里。删除:从 .tdb 去掉,不可恢复。"
|
|
2250
|
+
: clipped
|
|
2251
|
+
? "已截到预算(≤300 token)"
|
|
2252
|
+
: "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。",
|
|
2253
|
+
}),
|
|
2254
|
+
draftOpen
|
|
2255
|
+
? jsxs("div", {
|
|
2256
|
+
style: { display: "flex", flexDirection: "column", gap: 6 },
|
|
2257
|
+
children: [
|
|
2258
|
+
jsx("textarea", {
|
|
2259
|
+
value: draftText,
|
|
2260
|
+
onChange: (e) => setDraftText(e.target.value),
|
|
2261
|
+
placeholder: "粘贴或输入要记住的内容。点一次写入,整段就是一条芯片。",
|
|
2262
|
+
style: mapUi.draftBox,
|
|
2263
|
+
}),
|
|
2264
|
+
jsx("button", {
|
|
2265
|
+
type: "button",
|
|
2266
|
+
style: { ...mapUi.actionBtn, alignSelf: "flex-start" },
|
|
2267
|
+
disabled: busy || !draftText.trim(),
|
|
2268
|
+
onClick: submitChipDraft,
|
|
2269
|
+
children: "写入并钉选",
|
|
2270
|
+
}),
|
|
2271
|
+
],
|
|
2272
|
+
})
|
|
2273
|
+
: null,
|
|
2001
2274
|
visibleChips.length
|
|
2002
2275
|
? jsx("div", {
|
|
2003
2276
|
style: mapUi.chips,
|
|
2004
|
-
children: visibleChips.map((c) =>
|
|
2005
|
-
|
|
2006
|
-
|
|
2277
|
+
children: visibleChips.map((c) => {
|
|
2278
|
+
const picked = pickedIds.includes(c.id);
|
|
2279
|
+
return jsxs(
|
|
2280
|
+
chipSelect ? "button" : "label",
|
|
2007
2281
|
{
|
|
2008
|
-
|
|
2282
|
+
type: chipSelect ? "button" : undefined,
|
|
2283
|
+
style: {
|
|
2284
|
+
...mapUi.chip,
|
|
2285
|
+
...(c.suggested ? mapUi.chipSuggest : {}),
|
|
2286
|
+
...(chipSelect && picked ? mapUi.chipPicked : {}),
|
|
2287
|
+
},
|
|
2288
|
+
onClick: chipSelect
|
|
2289
|
+
? (e) => {
|
|
2290
|
+
e.preventDefault();
|
|
2291
|
+
togglePicked(c.id);
|
|
2292
|
+
}
|
|
2293
|
+
: undefined,
|
|
2009
2294
|
children: [
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2295
|
+
chipSelect
|
|
2296
|
+
? jsx("input", {
|
|
2297
|
+
type: "checkbox",
|
|
2298
|
+
checked: picked,
|
|
2299
|
+
readOnly: true,
|
|
2300
|
+
tabIndex: -1,
|
|
2301
|
+
style: { pointerEvents: "none" },
|
|
2302
|
+
})
|
|
2303
|
+
: jsx("input", {
|
|
2304
|
+
type: "checkbox",
|
|
2305
|
+
checked: pinSet.has(c.id),
|
|
2306
|
+
onChange: (e) => togglePin(c.id, e.target.checked),
|
|
2307
|
+
}),
|
|
2015
2308
|
c.suggested ? jsx("span", { style: mapUi.suggest, children: "建议" }) : null,
|
|
2016
2309
|
jsx("span", {
|
|
2017
2310
|
style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
@@ -2020,14 +2313,14 @@ window.__ModuleLoader__.load({
|
|
|
2020
2313
|
],
|
|
2021
2314
|
},
|
|
2022
2315
|
String(c.id),
|
|
2023
|
-
)
|
|
2024
|
-
),
|
|
2316
|
+
);
|
|
2317
|
+
}),
|
|
2025
2318
|
})
|
|
2026
2319
|
: jsx("div", {
|
|
2027
2320
|
style: mapUi.hint,
|
|
2028
2321
|
children: chips.length
|
|
2029
2322
|
? "没有匹配的芯片。"
|
|
2030
|
-
: "还没有可钉选的 preference / decision / entity
|
|
2323
|
+
: "还没有可钉选的 preference / decision / entity。用「新增」写一条。",
|
|
2031
2324
|
}),
|
|
2032
2325
|
],
|
|
2033
2326
|
})
|
|
@@ -2217,6 +2510,15 @@ window.__ModuleLoader__.load({
|
|
|
2217
2510
|
inject: () => ({
|
|
2218
2511
|
fork: (opts) => sessions.fork(opts),
|
|
2219
2512
|
open: (id) => sessions.open(id),
|
|
2513
|
+
seedLengthOf: (id) => {
|
|
2514
|
+
try {
|
|
2515
|
+
const seed = sessions.binding?.(id)?.session?.header?.seedLength;
|
|
2516
|
+
const n = Number(seed);
|
|
2517
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
2518
|
+
} catch {
|
|
2519
|
+
return null;
|
|
2520
|
+
}
|
|
2521
|
+
},
|
|
2220
2522
|
renameSession: async (id, title) => {
|
|
2221
2523
|
const face = sessions.binding?.(id)?.session;
|
|
2222
2524
|
if (!face || typeof face.rename !== "function") {
|
package/lib/episode.js
CHANGED
|
@@ -203,6 +203,48 @@ export function syncForkLineage(db, { childSessionId, parentSessionId, atSeq } =
|
|
|
203
203
|
return { cutId, childTail };
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Project DSH's already-landed compactations and fork list onto episode boxes.
|
|
208
|
+
* Idempotent: same atSeq / child reuses the existing node.
|
|
209
|
+
*/
|
|
210
|
+
export function backfillSessionMap(db, { sessionId, compactations = [], forks = [] } = {}) {
|
|
211
|
+
const sid = String(sessionId || "");
|
|
212
|
+
let checkpoints = 0;
|
|
213
|
+
const seenSeq = new Set();
|
|
214
|
+
for (const raw of compactations || []) {
|
|
215
|
+
const atSeq = Number(raw.atSeq);
|
|
216
|
+
if (!sid || !Number.isFinite(atSeq) || seenSeq.has(atSeq)) continue;
|
|
217
|
+
seenSeq.add(atSeq);
|
|
218
|
+
const id = recordCheckpoint(db, {
|
|
219
|
+
sessionId: sid,
|
|
220
|
+
atSeq,
|
|
221
|
+
compactionId: raw.compactionId,
|
|
222
|
+
summary: raw.summary,
|
|
223
|
+
});
|
|
224
|
+
if (id != null) checkpoints += 1;
|
|
225
|
+
}
|
|
226
|
+
if (sid) ensureTail(db, sid);
|
|
227
|
+
let forkLinks = 0;
|
|
228
|
+
for (const raw of forks || []) {
|
|
229
|
+
const child = String(raw.childSessionId || "");
|
|
230
|
+
const parent = String(raw.parentSessionId || "");
|
|
231
|
+
if (!child || !parent) continue;
|
|
232
|
+
const atSeq = Number.isFinite(Number(raw.atSeq)) ? Number(raw.atSeq) : undefined;
|
|
233
|
+
const result = syncForkLineage(db, {
|
|
234
|
+
childSessionId: child,
|
|
235
|
+
parentSessionId: parent,
|
|
236
|
+
atSeq,
|
|
237
|
+
});
|
|
238
|
+
if (result?.childTail != null) forkLinks += 1;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
checkpoints,
|
|
243
|
+
forks: forkLinks,
|
|
244
|
+
...sessionMapSnapshot(db, sid),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
206
248
|
export function sessionMapSnapshot(db, sessionId) {
|
|
207
249
|
const sid = String(sessionId || "");
|
|
208
250
|
const all = listEpisodeRecords(db);
|
package/lib/extract.js
CHANGED
|
@@ -21,6 +21,7 @@ export const CHITCHAT_CUES =
|
|
|
21
21
|
/** WorkBuddy-style per-batch write cap. Not a file-size limit. */
|
|
22
22
|
export const MAX_WRITE_CHARS = 3000;
|
|
23
23
|
export const MAX_LINE_CHARS = 240;
|
|
24
|
+
export const EXPLICIT_LINE_CHARS = 2000;
|
|
24
25
|
export const MAX_WRITE_ITEMS = 24;
|
|
25
26
|
|
|
26
27
|
const NAME_RE =
|
|
@@ -307,10 +308,10 @@ export function ruleCandidates(turns) {
|
|
|
307
308
|
return out;
|
|
308
309
|
}
|
|
309
310
|
|
|
310
|
-
export function filterCandidates(candidates, { importFile = false } = {}) {
|
|
311
|
+
export function filterCandidates(candidates, { importFile = false, explicit = false } = {}) {
|
|
311
312
|
const seen = new Set();
|
|
312
313
|
const out = [];
|
|
313
|
-
const caps = importFile
|
|
314
|
+
const caps = importFile || explicit
|
|
314
315
|
? { entity: 24, preference: 24, decision: 12, experience: 8 }
|
|
315
316
|
: { entity: 8, preference: 8, decision: 6, experience: 4 };
|
|
316
317
|
const used = { entity: 0, preference: 0, decision: 0, experience: 0 };
|
|
@@ -319,10 +320,10 @@ export function filterCandidates(candidates, { importFile = false } = {}) {
|
|
|
319
320
|
const text = String(raw.text || raw.name || "").trim();
|
|
320
321
|
if (!text) continue;
|
|
321
322
|
if (raw.type === "entity" && isNoiseName(raw.name || text)) continue;
|
|
322
|
-
if (looksSecret(text) || looksSecret(raw.fail) || looksSecret(raw.fix) || looksSecret(raw.quote)) {
|
|
323
|
+
if (!explicit && (looksSecret(text) || looksSecret(raw.fail) || looksSecret(raw.fix) || looksSecret(raw.quote))) {
|
|
323
324
|
continue;
|
|
324
325
|
}
|
|
325
|
-
if (raw.type === "preference") {
|
|
326
|
+
if (raw.type === "preference" && !explicit) {
|
|
326
327
|
const blob = `${raw.text || ""} ${raw.quote || ""}`;
|
|
327
328
|
if (CHITCHAT_CUES.test(String(raw.text || "").trim())) continue;
|
|
328
329
|
if (ONESHOT_CUES.test(blob) && !PREFERENCE_CUES.test(blob)) continue;
|
|
@@ -336,7 +337,7 @@ export function filterCandidates(candidates, { importFile = false } = {}) {
|
|
|
336
337
|
out.push({
|
|
337
338
|
type: raw.type,
|
|
338
339
|
name: raw.name ? String(raw.name).slice(0, 80) : undefined,
|
|
339
|
-
text: clip(text, MAX_LINE_CHARS),
|
|
340
|
+
text: clip(text, explicit ? EXPLICIT_LINE_CHARS : MAX_LINE_CHARS),
|
|
340
341
|
linkName: raw.linkName ? String(raw.linkName).slice(0, 80) : undefined,
|
|
341
342
|
linkLabel: raw.linkLabel || undefined,
|
|
342
343
|
fail: raw.fail ? clip(raw.fail, 200) : undefined,
|
|
@@ -409,7 +410,10 @@ export function capWriteBatch(candidates, { maxChars = MAX_WRITE_CHARS, maxItems
|
|
|
409
410
|
|
|
410
411
|
export function applyCandidates(db, candidates, { sessionId, via } = {}) {
|
|
411
412
|
const { kept: list } = capWriteBatch(
|
|
412
|
-
filterCandidates(candidates, {
|
|
413
|
+
filterCandidates(candidates, {
|
|
414
|
+
importFile: via === "workbuddy-import",
|
|
415
|
+
explicit: via === "chip-add",
|
|
416
|
+
}),
|
|
413
417
|
);
|
|
414
418
|
const ordered = [
|
|
415
419
|
...list.filter((c) => c.type === "entity"),
|
package/lib/pins.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { listEpisodeRecords } from "./episode.js";
|
|
4
4
|
import { clipToTokenBudget, estimateTokens } from "./tokens.js";
|
|
5
5
|
import { pinsOf, writePins } from "./settings.js";
|
|
6
|
-
import { suggestedNeighborIds } from "./store.js";
|
|
6
|
+
import { archiveNode, deleteNode, suggestedNeighborIds } from "./store.js";
|
|
7
|
+
import { applyCandidates, DECISION_CUES, EXPLICIT_LINE_CHARS, firstEntityName } from "./extract.js";
|
|
8
|
+
import { clip } from "./schema.js";
|
|
9
|
+
import { parseUntilFromText } from "./until.js";
|
|
7
10
|
|
|
8
11
|
export const PIN_TOKEN_BUDGET = 300;
|
|
9
12
|
export const CHIP_TYPES = Object.freeze(["preference", "decision", "entity"]);
|
|
@@ -153,3 +156,102 @@ export function applyPinsUpdate(db, body = {}) {
|
|
|
153
156
|
}
|
|
154
157
|
return setSessionPins(db, sessionId, Array.isArray(body.ids) ? body.ids : []);
|
|
155
158
|
}
|
|
159
|
+
|
|
160
|
+
/** Markdown is only markup. Strip fences, emphasis, links, headings, list markers. */
|
|
161
|
+
export function stripMarkdownMarkup(text) {
|
|
162
|
+
let s = String(text || "");
|
|
163
|
+
s = s.replace(/```[\s\S]*?```/g, " ");
|
|
164
|
+
s = s.replace(/`([^`]+)`/g, "$1");
|
|
165
|
+
s = s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
166
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "$1");
|
|
167
|
+
s = s.replace(/__([^_]+)__/g, "$1");
|
|
168
|
+
s = s.replace(/\*([^*]+)\*/g, "$1");
|
|
169
|
+
s = s.replace(/_([^_]+)_/g, "$1");
|
|
170
|
+
s = s.replace(/^#{1,6}\s+/gm, "");
|
|
171
|
+
s = s.replace(/^\s*(?:[-*+]|\d+\.)\s+(?:\[[ xX]\]\s+)?/gm, "");
|
|
172
|
+
return s.replace(/\s+/g, " ").trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* One submit → one chip. Long paste stays one node; markdown is stripped, not split.
|
|
177
|
+
*/
|
|
178
|
+
export function splitChipDraft(text) {
|
|
179
|
+
const body = stripMarkdownMarkup(String(text || "").replace(/\r\n/g, "\n"));
|
|
180
|
+
return body ? [body] : [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function candidateFromChipLine(body) {
|
|
184
|
+
const text = stripMarkdownMarkup(body);
|
|
185
|
+
if (!text) return null;
|
|
186
|
+
const name = firstEntityName(text);
|
|
187
|
+
if (DECISION_CUES.test(text)) {
|
|
188
|
+
return {
|
|
189
|
+
skip: null,
|
|
190
|
+
candidates: [
|
|
191
|
+
{
|
|
192
|
+
type: "decision",
|
|
193
|
+
name,
|
|
194
|
+
text: clip(text, EXPLICIT_LINE_CHARS),
|
|
195
|
+
until: parseUntilFromText(text),
|
|
196
|
+
linkName: name,
|
|
197
|
+
linkLabel: "decided",
|
|
198
|
+
quote: clip(text, 120),
|
|
199
|
+
via: "chip-add",
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
skip: null,
|
|
206
|
+
candidates: [
|
|
207
|
+
{
|
|
208
|
+
type: "preference",
|
|
209
|
+
name,
|
|
210
|
+
text: clip(text, EXPLICIT_LINE_CHARS),
|
|
211
|
+
linkName: name,
|
|
212
|
+
linkLabel: "about",
|
|
213
|
+
quote: clip(text, 120),
|
|
214
|
+
via: "chip-add",
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function addChipsFromText(db, text, { sessionId = "", pin = true } = {}) {
|
|
221
|
+
const lines = splitChipDraft(text);
|
|
222
|
+
if (!lines.length) return { ok: false, message: "空内容", ids: [], skipped: 0 };
|
|
223
|
+
const candidates = [];
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const parsed = candidateFromChipLine(line);
|
|
226
|
+
if (!parsed) continue;
|
|
227
|
+
candidates.push(...parsed.candidates);
|
|
228
|
+
}
|
|
229
|
+
if (!candidates.length) {
|
|
230
|
+
return { ok: false, message: "没有可写入的条目", ids: [], skipped: 0 };
|
|
231
|
+
}
|
|
232
|
+
const applied = applyCandidates(db, candidates, { sessionId, via: "chip-add" });
|
|
233
|
+
const ids = [...new Set(applied.map((a) => Number(a.id)).filter((id) => Number.isFinite(id)))];
|
|
234
|
+
if (pin && sessionId && ids.length) {
|
|
235
|
+
const merged = [...new Set([...livePinIds(db, sessionId), ...ids])];
|
|
236
|
+
setSessionPins(db, sessionId, merged);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
ok: ids.length > 0,
|
|
240
|
+
message: ids.length ? `已写入 ${ids.length} 条` : "没有新节点",
|
|
241
|
+
ids,
|
|
242
|
+
applied: applied.length,
|
|
243
|
+
skipped: Math.max(0, lines.length - ids.length),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function batchChipAction(db, { action, ids, sessionId = "" } = {}) {
|
|
248
|
+
const verb = action === "delete" ? "delete" : "archive";
|
|
249
|
+
const list = [...new Set((ids || []).map(Number).filter((id) => Number.isFinite(id)))];
|
|
250
|
+
let count = 0;
|
|
251
|
+
for (const id of list) {
|
|
252
|
+
const ok = verb === "delete" ? deleteNode(db, id) : archiveNode(db, id);
|
|
253
|
+
if (ok) count += 1;
|
|
254
|
+
}
|
|
255
|
+
if (sessionId) prunePins(db, sessionId);
|
|
256
|
+
return { ok: count > 0 || list.length === 0, action: verb, count, ids: list };
|
|
257
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
import { embeddingPublicStatus } from "./embed.js";
|
|
18
18
|
import { recallModeOf, writeUiSettings } from "./settings.js";
|
|
19
19
|
import { discoverWorkbuddyFiles, exportMarkdown, importWorkbuddy } from "./markdown.js";
|
|
20
|
-
import { renameEpisode, sessionMapSnapshot } from "./episode.js";
|
|
21
|
-
import { applyPinsUpdate, listChips, readSessionPins } from "./pins.js";
|
|
20
|
+
import { backfillSessionMap, renameEpisode, sessionMapSnapshot } from "./episode.js";
|
|
21
|
+
import { applyPinsUpdate, addChipsFromText, batchChipAction, listChips, readSessionPins } from "./pins.js";
|
|
22
22
|
|
|
23
23
|
export { readUiSettings, writeUiSettings } from "./settings.js";
|
|
24
24
|
|
|
@@ -272,6 +272,23 @@ async function handle(req, res, { liveOptions, status }) {
|
|
|
272
272
|
return;
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
if (path === "/api/dsh-trivium/map/backfill" && method === "POST") {
|
|
276
|
+
const { cwd, db } = await dbForRequest(url);
|
|
277
|
+
if (!db) {
|
|
278
|
+
sendJson(res, 400, { ok: false, message: "no workspace" });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
282
|
+
const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
|
|
283
|
+
const result = backfillSessionMap(db, {
|
|
284
|
+
sessionId,
|
|
285
|
+
compactations: body.compactations,
|
|
286
|
+
forks: body.forks,
|
|
287
|
+
});
|
|
288
|
+
sendJson(res, 200, { ok: true, cwd, sessionId, ...result });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
275
292
|
if (path === "/api/dsh-trivium/chips" && method === "GET") {
|
|
276
293
|
const { cwd, db } = await dbForRequest(url);
|
|
277
294
|
const q = url.searchParams.get("q") || "";
|
|
@@ -284,6 +301,52 @@ async function handle(req, res, { liveOptions, status }) {
|
|
|
284
301
|
return;
|
|
285
302
|
}
|
|
286
303
|
|
|
304
|
+
if (path === "/api/dsh-trivium/chips" && method === "POST") {
|
|
305
|
+
const { cwd, db } = await dbForRequest(url);
|
|
306
|
+
if (!db) {
|
|
307
|
+
sendJson(res, 400, { ok: false, message: "no workspace" });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
311
|
+
const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
|
|
312
|
+
const result = addChipsFromText(db, body.text, { sessionId, pin: body.pin !== false });
|
|
313
|
+
const pin = readSessionPins(db, sessionId);
|
|
314
|
+
sendJson(res, result.ok ? 200 : 400, {
|
|
315
|
+
...result,
|
|
316
|
+
cwd,
|
|
317
|
+
sessionId,
|
|
318
|
+
chips: listChips(db, { sessionId }),
|
|
319
|
+
pinIds: pin.ids,
|
|
320
|
+
clipped: pin.clipped,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (path === "/api/dsh-trivium/chips/batch" && method === "POST") {
|
|
326
|
+
const { cwd, db } = await dbForRequest(url);
|
|
327
|
+
if (!db) {
|
|
328
|
+
sendJson(res, 400, { ok: false, message: "no workspace" });
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
332
|
+
const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
|
|
333
|
+
const result = batchChipAction(db, {
|
|
334
|
+
action: body.action,
|
|
335
|
+
ids: body.ids,
|
|
336
|
+
sessionId,
|
|
337
|
+
});
|
|
338
|
+
const pin = readSessionPins(db, sessionId);
|
|
339
|
+
sendJson(res, result.ok ? 200 : 400, {
|
|
340
|
+
...result,
|
|
341
|
+
cwd,
|
|
342
|
+
sessionId,
|
|
343
|
+
chips: listChips(db, { sessionId }),
|
|
344
|
+
pinIds: pin.ids,
|
|
345
|
+
clipped: pin.clipped,
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
287
350
|
if (path === "/api/dsh-trivium/pins" && method === "GET") {
|
|
288
351
|
const { cwd, db } = await dbForRequest(url);
|
|
289
352
|
const sessionId = url.searchParams.get("sessionId") || "";
|