dsh-trivium 0.4.1 → 0.4.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.
package/README.md CHANGED
@@ -55,8 +55,8 @@ dsh plugin --profile web add dsh-trivium
55
55
  - **跨会话** — 会话 A 记下「鉴权走 header X」,会话 B 调用 `ctx_find("鉴权")` 命中,并带上它连着谁(`about` / `decided` / `broke` / `fixed`)。
56
56
  - **默认安静** — 新会话只注入一张短地图(≤400 token)。模型要用再调工具,不会每一步灌满。
57
57
  - **会话图** — compaction 切一段就收成一个方框;点方框用 DSH 原生 fork 开分支;勾选的记忆芯片从当前这段的下一轮注入。
58
- - **人能改错** — 设置 → Trivium 记忆:搜、看邻居、改名 / 正文、合并、归档或删除。
59
- - **抽取偏严** — 闲聊、一次性改文件、密钥不入库。
58
+ - **人能改错** — 会话图芯片条可新增、归档、删除;设置页管搜、改名 / 正文、合并、全局开关。
59
+ - **抽取偏严** — 闲聊、一次性改文件、密钥**不会从对话自动入库**。芯片「新增」按你写的存(点一次整段一条)。
60
60
  - **可选 embedding** — 默认关。官方 DeepSeek 对话接口没有 embeddings;可填 OpenAI 兼容地址。
61
61
 
62
62
  ### 四个工具
@@ -74,7 +74,7 @@ dsh plugin --profile web add dsh-trivium
74
74
 
75
75
  ## 会话图
76
76
 
77
- 入口:当前会话标题栏 **对话 / 轨迹 / 会话图**。这是情节图,不是记忆编辑器;改错的节点仍走设置页。
77
+ 入口:当前会话标题栏 **对话 / 轨迹 / 会话图**。这是情节图,不是记忆编辑器;改名、合并、全局开关仍走设置页。芯片条上可以直接新增、归档、删除。
78
78
 
79
79
  ### 方框
80
80
 
@@ -101,6 +101,9 @@ dsh plugin --profile web add dsh-trivium
101
101
  |---|---|
102
102
  | 勾上某条 | 当前这段里你再发的**下一条**带上它(L0,≤300 token) |
103
103
  | 取消勾选 | 再下一轮不再带 |
104
+ | 新增 | 点一次写入一条;整段粘贴也是一条 |
105
+ | 选择 → 归档 | 软删除:find / 短地图 / 芯片不再出现,节点还在 `.tdb` |
106
+ | 选择 → 删除 | 从 `.tdb` 去掉,不可恢复 |
104
107
  | 已经发出去的轮次 | 不回写 |
105
108
 
106
109
  芯片按会话隔离。新开会话、fork 出的子会话默认都不勾;需要的话在分叉对话框旁打开「分叉时继承钉选」,或到子会话里重新勾。短地图仍在 session-start 自动带,不占芯片位。建议标记只是提示,不会自动勾上。
@@ -140,7 +143,7 @@ dsh plugin --profile web add dsh-trivium
140
143
  ## 限制
141
144
 
142
145
  - 模型不调 `ctx_find`、也不勾芯片时,窗口里主要是开场短地图。
143
- - 抽取会漏;脏数据要在设置里改或归档。
146
+ - 抽取会漏;脏数据可在芯片条归档 / 删除,或到设置里改、合并。
144
147
  - 同一个 `.tdb` 不要两个 Node 进程同时打开(正常只开一个 `dsh web` 即可)。
145
148
  - Markdown 导出是给人看的,不能再解析回去。WorkBuddy 导入是一次性,不是双向同步。
146
149
  - 会话图只投影 compaction 与 fork,不会按消息条数自己切方框。
@@ -151,13 +154,7 @@ dsh plugin --profile web add dsh-trivium
151
154
 
152
155
  ## 更新说明
153
156
 
154
- **0.4.1** — 宿主钉到 `@deepseek-ai/dsh@0.1.0-rc.8`。功能与 0.4.0 相同。已在 rc.8 上确认:设置「Trivium 记忆」、新会话短地图、跨会话 `ctx_find`、会话图标签。
155
-
156
- **0.4.0** — 会话图、记忆芯片、从检查点 fork。
157
-
158
- ---
159
-
160
- ## 发布信息
157
+ **0.4.2** — 会话图芯片条可新增(整段粘贴一条)、批量归档 / 删除。自动抽取仍偏严;芯片新增按你写的入库。
161
158
 
162
159
  **0.4.1** — 宿主钉到 `@deepseek-ai/dsh@0.1.0-rc.8`。功能与 0.4.0 相同。已在 rc.8 上确认:设置「Trivium 记忆」、新会话短地图、跨会话 `ctx_find`、会话图标签。
163
160
 
@@ -168,6 +165,6 @@ dsh plugin --profile web add dsh-trivium
168
165
  ## 发布信息
169
166
 
170
167
  - GitHub: https://github.com/QWQcool/dsh-trivium
171
- - npm: [`dsh-trivium@0.4.1`](https://www.npmjs.com/package/dsh-trivium)
168
+ - npm: [`dsh-trivium@0.4.2`](https://www.npmjs.com/package/dsh-trivium)
172
169
  - 测试宿主:`@deepseek-ai/dsh@0.1.0-rc.8`
173
170
  - License: MIT(依赖 [TriviumDB](https://github.com/TriviumDB/triviumdb) 为 Apache-2.0)
package/lib/client.js CHANGED
@@ -1238,6 +1238,8 @@ window.__ModuleLoader__.load({
1238
1238
  position: "relative",
1239
1239
  },
1240
1240
  chipsHead: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" },
1241
+ chipsToolbar: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
1242
+ chipsActions: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
1241
1243
  chipsTitle: { fontSize: 13, fontWeight: 650 },
1242
1244
  hint: { fontSize: 12, opacity: 0.62 },
1243
1245
  chips: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" },
@@ -1255,16 +1257,50 @@ window.__ModuleLoader__.load({
1255
1257
  chipSuggest: { borderColor: "currentColor", background: "rgba(127,127,127,0.08)" },
1256
1258
  suggest: { fontSize: 10, fontWeight: 650, opacity: 0.78, flexShrink: 0 },
1257
1259
  search: {
1258
- width: 180,
1260
+ width: 200,
1259
1261
  boxSizing: "border-box",
1260
- fontSize: 12,
1261
- padding: "4px 8px",
1262
+ fontSize: 13,
1263
+ padding: "6px 10px",
1262
1264
  borderRadius: 8,
1263
1265
  border: "1px solid rgba(127,127,127,0.28)",
1264
1266
  background: "transparent",
1265
1267
  color: "inherit",
1266
1268
  },
1267
1269
  inherit: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, cursor: "pointer", margin: 0 },
1270
+ chipPicked: { borderColor: "currentColor", background: "rgba(127,127,127,0.18)" },
1271
+ draftBox: {
1272
+ width: "100%",
1273
+ minHeight: 96,
1274
+ boxSizing: "border-box",
1275
+ fontSize: 13,
1276
+ padding: "8px 10px",
1277
+ borderRadius: 8,
1278
+ border: "1px solid rgba(127,127,127,0.28)",
1279
+ background: "transparent",
1280
+ color: "inherit",
1281
+ resize: "vertical",
1282
+ fontFamily: "inherit",
1283
+ },
1284
+ actionBtn: {
1285
+ fontSize: 13,
1286
+ padding: "6px 14px",
1287
+ borderRadius: 8,
1288
+ border: "1px solid rgba(127,127,127,0.35)",
1289
+ background: "rgba(127,127,127,0.10)",
1290
+ color: "inherit",
1291
+ cursor: "pointer",
1292
+ flexShrink: 0,
1293
+ },
1294
+ dangerBtn: {
1295
+ fontSize: 13,
1296
+ padding: "6px 14px",
1297
+ borderRadius: 8,
1298
+ border: "1px solid rgba(180,60,60,0.45)",
1299
+ background: "transparent",
1300
+ color: "inherit",
1301
+ cursor: "pointer",
1302
+ flexShrink: 0,
1303
+ },
1268
1304
  titleRow: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 },
1269
1305
  title: {
1270
1306
  fontSize: 14,
@@ -1660,6 +1696,10 @@ window.__ModuleLoader__.load({
1660
1696
  const [chipQuery, setChipQuery] = react.useState("");
1661
1697
  const [inheritPins, setInheritPins] = react.useState(false);
1662
1698
  const [chipsOpen, setChipsOpen] = react.useState(false);
1699
+ const [chipSelect, setChipSelect] = react.useState(false);
1700
+ const [pickedIds, setPickedIds] = react.useState([]);
1701
+ const [draftOpen, setDraftOpen] = react.useState(false);
1702
+ const [draftText, setDraftText] = react.useState("");
1663
1703
  const [forkDraft, setForkDraft] = react.useState(null);
1664
1704
  const canvasRef = react.useRef(null);
1665
1705
  const dragRef = react.useRef(null);
@@ -1924,6 +1964,67 @@ window.__ModuleLoader__.load({
1924
1964
  }
1925
1965
  };
1926
1966
 
1967
+ const qsPins = cwd ? "?cwd=" + encodeURIComponent(cwd) : "";
1968
+
1969
+ const applyChipPayload = (payload) => {
1970
+ if (Array.isArray(payload.chips)) setChips(payload.chips);
1971
+ if (Array.isArray(payload.pinIds)) setPinIds(payload.pinIds);
1972
+ if (payload.clipped != null) setClipped(!!payload.clipped);
1973
+ };
1974
+
1975
+ const togglePicked = (id) => {
1976
+ setPickedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : prev.concat(id)));
1977
+ };
1978
+
1979
+ const runChipBatch = async (action) => {
1980
+ if (!pickedIds.length || busy) return;
1981
+ if (action === "delete") {
1982
+ const ok = window.confirm(
1983
+ "删除会从 .tdb 里拿掉这 " +
1984
+ pickedIds.length +
1985
+ " 条,不可恢复。归档只是藏起来,find / 短地图 / 芯片都不再出现。确定删除?",
1986
+ );
1987
+ if (!ok) return;
1988
+ }
1989
+ setBusy(true);
1990
+ setError("");
1991
+ try {
1992
+ const resp = await api("/api/dsh-trivium/chips/batch" + qsPins, {
1993
+ method: "POST",
1994
+ headers: { "content-type": "application/json" },
1995
+ body: JSON.stringify({ sessionId, action, ids: pickedIds }),
1996
+ });
1997
+ applyChipPayload(resp);
1998
+ setPickedIds([]);
1999
+ setChipSelect(false);
2000
+ } catch (err) {
2001
+ setError(String(err.message || err));
2002
+ } finally {
2003
+ setBusy(false);
2004
+ }
2005
+ };
2006
+
2007
+ const submitChipDraft = async () => {
2008
+ const text = draftText.trim();
2009
+ if (!text || busy) return;
2010
+ setBusy(true);
2011
+ setError("");
2012
+ try {
2013
+ const resp = await api("/api/dsh-trivium/chips" + qsPins, {
2014
+ method: "POST",
2015
+ headers: { "content-type": "application/json" },
2016
+ body: JSON.stringify({ sessionId, text, pin: true }),
2017
+ });
2018
+ applyChipPayload(resp);
2019
+ setDraftText("");
2020
+ setDraftOpen(false);
2021
+ } catch (err) {
2022
+ setError(String(err.message || err));
2023
+ } finally {
2024
+ setBusy(false);
2025
+ }
2026
+ };
2027
+
1927
2028
  const pinSet = new Set(pinIds);
1928
2029
  const visibleChips = react.useMemo(() => {
1929
2030
  const needle = chipQuery.trim().toLowerCase();
@@ -1962,7 +2063,7 @@ window.__ModuleLoader__.load({
1962
2063
  }),
1963
2064
  jsx("button", {
1964
2065
  type: "button",
1965
- style: mapUi.foldBtn,
2066
+ style: mapUi.actionBtn,
1966
2067
  onClick: () => setChipsOpen((v) => !v),
1967
2068
  children: chipsOpen ? "收起" : "展开勾选",
1968
2069
  }),
@@ -1973,7 +2074,7 @@ window.__ModuleLoader__.load({
1973
2074
  style: { display: "flex", flexDirection: "column", gap: 8 },
1974
2075
  children: [
1975
2076
  jsxs("div", {
1976
- style: mapUi.chipsHead,
2077
+ style: mapUi.chipsToolbar,
1977
2078
  children: [
1978
2079
  jsx("input", {
1979
2080
  type: "search",
@@ -1993,25 +2094,113 @@ window.__ModuleLoader__.load({
1993
2094
  "分叉时继承钉选",
1994
2095
  ],
1995
2096
  }),
1996
- clipped
1997
- ? jsx("div", { style: mapUi.hint, children: "已截到预算(≤300 token)" })
1998
- : jsx("div", { style: mapUi.hint, children: "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。" }),
2097
+ jsxs("div", {
2098
+ style: mapUi.chipsActions,
2099
+ children: [
2100
+ jsx("button", {
2101
+ type: "button",
2102
+ style: mapUi.actionBtn,
2103
+ onClick: () => {
2104
+ setDraftOpen((v) => !v);
2105
+ setChipSelect(false);
2106
+ setPickedIds([]);
2107
+ },
2108
+ children: draftOpen ? "取消新增" : "新增",
2109
+ }),
2110
+ jsx("button", {
2111
+ type: "button",
2112
+ style: mapUi.actionBtn,
2113
+ onClick: () => {
2114
+ setChipSelect((v) => !v);
2115
+ setPickedIds([]);
2116
+ setDraftOpen(false);
2117
+ },
2118
+ children: chipSelect ? "取消选择" : "选择",
2119
+ }),
2120
+ chipSelect
2121
+ ? jsx("button", {
2122
+ type: "button",
2123
+ style: mapUi.actionBtn,
2124
+ disabled: !pickedIds.length || busy,
2125
+ onClick: () => runChipBatch("archive"),
2126
+ children: "归档" + (pickedIds.length ? " " + pickedIds.length : ""),
2127
+ })
2128
+ : null,
2129
+ chipSelect
2130
+ ? jsx("button", {
2131
+ type: "button",
2132
+ style: mapUi.dangerBtn,
2133
+ disabled: !pickedIds.length || busy,
2134
+ onClick: () => runChipBatch("delete"),
2135
+ children: "删除" + (pickedIds.length ? " " + pickedIds.length : ""),
2136
+ })
2137
+ : null,
2138
+ ],
2139
+ }),
1999
2140
  ],
2000
2141
  }),
2142
+ jsx("div", {
2143
+ style: mapUi.hint,
2144
+ children: chipSelect
2145
+ ? "点芯片多选。归档:软删除,find / 短地图 / 芯片不再出现,节点还在库里。删除:从 .tdb 去掉,不可恢复。"
2146
+ : clipped
2147
+ ? "已截到预算(≤300 token)"
2148
+ : "勾选后从当前这段的下一轮注入。空白处拖动画布,滚轮缩放。",
2149
+ }),
2150
+ draftOpen
2151
+ ? jsxs("div", {
2152
+ style: { display: "flex", flexDirection: "column", gap: 6 },
2153
+ children: [
2154
+ jsx("textarea", {
2155
+ value: draftText,
2156
+ onChange: (e) => setDraftText(e.target.value),
2157
+ placeholder: "粘贴或输入要记住的内容。点一次写入,整段就是一条芯片。",
2158
+ style: mapUi.draftBox,
2159
+ }),
2160
+ jsx("button", {
2161
+ type: "button",
2162
+ style: { ...mapUi.actionBtn, alignSelf: "flex-start" },
2163
+ disabled: busy || !draftText.trim(),
2164
+ onClick: submitChipDraft,
2165
+ children: "写入并钉选",
2166
+ }),
2167
+ ],
2168
+ })
2169
+ : null,
2001
2170
  visibleChips.length
2002
2171
  ? jsx("div", {
2003
2172
  style: mapUi.chips,
2004
- children: visibleChips.map((c) =>
2005
- jsxs(
2006
- "label",
2173
+ children: visibleChips.map((c) => {
2174
+ const picked = pickedIds.includes(c.id);
2175
+ return jsxs(
2176
+ chipSelect ? "button" : "label",
2007
2177
  {
2008
- style: { ...mapUi.chip, ...(c.suggested ? mapUi.chipSuggest : {}) },
2178
+ type: chipSelect ? "button" : undefined,
2179
+ style: {
2180
+ ...mapUi.chip,
2181
+ ...(c.suggested ? mapUi.chipSuggest : {}),
2182
+ ...(chipSelect && picked ? mapUi.chipPicked : {}),
2183
+ },
2184
+ onClick: chipSelect
2185
+ ? (e) => {
2186
+ e.preventDefault();
2187
+ togglePicked(c.id);
2188
+ }
2189
+ : undefined,
2009
2190
  children: [
2010
- jsx("input", {
2011
- type: "checkbox",
2012
- checked: pinSet.has(c.id),
2013
- onChange: (e) => togglePin(c.id, e.target.checked),
2014
- }),
2191
+ chipSelect
2192
+ ? jsx("input", {
2193
+ type: "checkbox",
2194
+ checked: picked,
2195
+ readOnly: true,
2196
+ tabIndex: -1,
2197
+ style: { pointerEvents: "none" },
2198
+ })
2199
+ : jsx("input", {
2200
+ type: "checkbox",
2201
+ checked: pinSet.has(c.id),
2202
+ onChange: (e) => togglePin(c.id, e.target.checked),
2203
+ }),
2015
2204
  c.suggested ? jsx("span", { style: mapUi.suggest, children: "建议" }) : null,
2016
2205
  jsx("span", {
2017
2206
  style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
@@ -2020,14 +2209,14 @@ window.__ModuleLoader__.load({
2020
2209
  ],
2021
2210
  },
2022
2211
  String(c.id),
2023
- ),
2024
- ),
2212
+ );
2213
+ }),
2025
2214
  })
2026
2215
  : jsx("div", {
2027
2216
  style: mapUi.hint,
2028
2217
  children: chips.length
2029
2218
  ? "没有匹配的芯片。"
2030
- : "还没有可钉选的 preference / decision / entity。",
2219
+ : "还没有可钉选的 preference / decision / entity。用「新增」写一条。",
2031
2220
  }),
2032
2221
  ],
2033
2222
  })
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, { importFile: via === "workbuddy-import" }),
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
@@ -18,7 +18,7 @@ import { embeddingPublicStatus } from "./embed.js";
18
18
  import { recallModeOf, writeUiSettings } from "./settings.js";
19
19
  import { discoverWorkbuddyFiles, exportMarkdown, importWorkbuddy } from "./markdown.js";
20
20
  import { renameEpisode, sessionMapSnapshot } from "./episode.js";
21
- import { applyPinsUpdate, listChips, readSessionPins } from "./pins.js";
21
+ import { applyPinsUpdate, addChipsFromText, batchChipAction, listChips, readSessionPins } from "./pins.js";
22
22
 
23
23
  export { readUiSettings, writeUiSettings } from "./settings.js";
24
24
 
@@ -284,6 +284,52 @@ async function handle(req, res, { liveOptions, status }) {
284
284
  return;
285
285
  }
286
286
 
287
+ if (path === "/api/dsh-trivium/chips" && method === "POST") {
288
+ const { cwd, db } = await dbForRequest(url);
289
+ if (!db) {
290
+ sendJson(res, 400, { ok: false, message: "no workspace" });
291
+ return;
292
+ }
293
+ const body = JSON.parse((await readBody(req)) || "{}");
294
+ const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
295
+ const result = addChipsFromText(db, body.text, { sessionId, pin: body.pin !== false });
296
+ const pin = readSessionPins(db, sessionId);
297
+ sendJson(res, result.ok ? 200 : 400, {
298
+ ...result,
299
+ cwd,
300
+ sessionId,
301
+ chips: listChips(db, { sessionId }),
302
+ pinIds: pin.ids,
303
+ clipped: pin.clipped,
304
+ });
305
+ return;
306
+ }
307
+
308
+ if (path === "/api/dsh-trivium/chips/batch" && method === "POST") {
309
+ const { cwd, db } = await dbForRequest(url);
310
+ if (!db) {
311
+ sendJson(res, 400, { ok: false, message: "no workspace" });
312
+ return;
313
+ }
314
+ const body = JSON.parse((await readBody(req)) || "{}");
315
+ const sessionId = body.sessionId || url.searchParams.get("sessionId") || "";
316
+ const result = batchChipAction(db, {
317
+ action: body.action,
318
+ ids: body.ids,
319
+ sessionId,
320
+ });
321
+ const pin = readSessionPins(db, sessionId);
322
+ sendJson(res, result.ok ? 200 : 400, {
323
+ ...result,
324
+ cwd,
325
+ sessionId,
326
+ chips: listChips(db, { sessionId }),
327
+ pinIds: pin.ids,
328
+ clipped: pin.clipped,
329
+ });
330
+ return;
331
+ }
332
+
287
333
  if (path === "/api/dsh-trivium/pins" && method === "GET") {
288
334
  const { cwd, db } = await dbForRequest(url);
289
335
  const sessionId = url.searchParams.get("sessionId") || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-trivium",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "In-process graph memory and session map for DeepSeek Harness, backed by TriviumDB.",
5
5
  "license": "MIT",
6
6
  "type": "module",