dsh-engram 0.5.0 → 0.6.0

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/lib/client.js CHANGED
@@ -153,6 +153,28 @@ var EngramApi = class {
153
153
  })
154
154
  );
155
155
  }
156
+ /**
157
+ * Data contract (issue #3) — dump the selected workspace, or every
158
+ * workspace when omitted, as a `dsh-engram/export` v1 payload. The same
159
+ * format `POST /import` restores, so the dump is a portable backup.
160
+ */
161
+ async exportData(workspace) {
162
+ return readJson(await fetch(`${API_PREFIX}/export${query({ workspace })}`));
163
+ }
164
+ /**
165
+ * Data contract (issue #3) — restore a dump. `merge` adds missing rows
166
+ * (idempotent), `replace` wipes the target workspace first (the host then
167
+ * requires `confirm: "restore"`). `dryRun` plans without writing.
168
+ */
169
+ async importData(body) {
170
+ return readJson(
171
+ await fetch(`${API_PREFIX}/import`, {
172
+ method: "POST",
173
+ headers: { "content-type": "application/json" },
174
+ body: JSON.stringify(body)
175
+ })
176
+ );
177
+ }
156
178
  /** Settings card transport — read the `dsh-engram` namespace through our
157
179
  * own HTTP API instead of the host connection/remote RPC. */
158
180
  async getSettings() {
@@ -2001,6 +2023,11 @@ function pct(v) {
2001
2023
  if (v === null || v === void 0) return "\u2013";
2002
2024
  return `${Math.round(v * 100)}%`;
2003
2025
  }
2026
+ function tokenCount(n) {
2027
+ const sign = n > 0 ? "+" : n < 0 ? "\u2212" : "";
2028
+ const abs = Math.abs(n);
2029
+ return abs >= 1e3 ? `${sign}${(abs / 1e3).toFixed(1)}k` : `${sign}${abs}`;
2030
+ }
2004
2031
  function EngramTelemetry({ api, workspace }) {
2005
2032
  const { dark, vars } = useEngramTheme();
2006
2033
  const [stats, setStats] = (0, import_react5.useState)(null);
@@ -2043,6 +2070,10 @@ function EngramTelemetry({ api, workspace }) {
2043
2070
  }, [stats]);
2044
2071
  const dose = stats && stats.ratios ? stats.ratios : null;
2045
2072
  const sampleText = stats && stats.ratios && stats.ratios.calls < 10 ? `\u6837\u672C\u4E0D\u8DB3\uFF08${stats.ratios.calls} \u6B21\uFF09\uFF0C\u6BD4\u4F8B\u4EC5\u4F9B\u53C2\u8003` : null;
2073
+ const ledger = stats?.ledger ?? null;
2074
+ const ledgerBucket = ledger === null ? null : workspace === "" ? ledger.totals : ledger.byWorkspace?.[workspace] ?? null;
2075
+ const ledgerDays = ledger === null || workspace !== "" ? [] : [...ledger.byDay ?? []].slice(0, 14).reverse();
2076
+ const ledgerMax = Math.max(1, ...ledgerDays.map((d) => Math.abs(d.netTokens)));
2046
2077
  const hb2 = {
2047
2078
  root: { display: "flex", flexDirection: "column", gap: 12, ...vars },
2048
2079
  head: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
@@ -2156,6 +2187,68 @@ function EngramTelemetry({ api, workspace }) {
2156
2187
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u5931\u8D25\u6B21\u6570" })
2157
2188
  ] })
2158
2189
  ] }),
2190
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.panel, children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.panelTitle, display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" }, children: [
2192
+ "Context Ledger \xB7 \u4E0A\u4E0B\u6587\u51C0\u6536\u76CA",
2193
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: hb2.sub, children: workspace === "" ? "\u5168\u90E8\u5DE5\u4F5C\u533A" : workspace })
2194
+ ] }),
2195
+ !ledger ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8D26\u672C\u672A\u542F\u7528\uFF08`ledgerEnabled:false`\uFF09\u6216\u5BBF\u4E3B API \u672A\u5C31\u7EEA\u3002" }) : !ledgerBucket ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8BE5\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u8D26\u672C\u8BB0\u5F55\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2196
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: 10, flexWrap: "wrap" }, children: [
2197
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: { ...hb2.statNum, fontSize: 24, color: ledgerBucket.netTokens >= 0 ? "#059669" : "#dc2626" }, children: tokenCount(ledgerBucket.netTokens) }),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: hb2.statLabel, children: "\u51C0 token\uFF08\u8282\u7701 \u2212 \u6CE8\u5165 \u2212 \u91CD\u5199\u7F5A\u91D1\uFF09" }),
2199
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: hb2.gaugeSub, children: [
2200
+ "\u8BF7\u6C42 ",
2201
+ ledgerBucket.requests,
2202
+ " \xB7 \u4F1A\u8BDD ",
2203
+ ledgerBucket.sessions,
2204
+ " \xB7 open ",
2205
+ ledger.openSessions,
2206
+ stats.jit ? ` \xB7 JIT \u63A8\u9001 ${stats.jit.pushes}` : ""
2207
+ ] })
2208
+ ] }),
2209
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.grid, marginTop: 8 }, children: [
2210
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2211
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#059669" }, children: tokenCount(ledgerBucket.avoidedTokens) }),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u8282\u7701 \xB7 \u526A\u679D/\u538B\u7F29" })
2213
+ ] }),
2214
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2215
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#d97706" }, children: tokenCount(-ledgerBucket.injectedTokens) }),
2216
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.statLabel, children: "\u6CE8\u5165\u6210\u672C \xB7 \u7D22\u5F15/\u53EC\u56DE/ESR" })
2217
+ ] }),
2218
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statCard, children: [
2219
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.statNum, color: "#dc2626" }, children: tokenCount(-Math.round(ledgerBucket.rewritesCostChars / ledger.charsPerToken)) }),
2220
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.statLabel, children: [
2221
+ "\u91CD\u5199\u7F5A\u91D1 \xB7 ",
2222
+ ledgerBucket.rewriteCount,
2223
+ " \u6B21"
2224
+ ] })
2225
+ ] })
2226
+ ] }),
2227
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { ...hb2.sub, marginTop: 6 }, children: [
2228
+ "\u53E3\u5F84\uFF1Achars/",
2229
+ ledger.charsPerToken,
2230
+ " \u4F30\u7B97 \xB7 \u7F13\u5B58\u6298\u4EF7 ",
2231
+ Math.round(ledger.cacheDiscount * 100),
2232
+ "% \uFF08\u91CD\u5199\u6309 ",
2233
+ Math.round((1 - ledger.cacheDiscount) * 100),
2234
+ "% \u8BA1\uFF09\xB7 \u672A\u8BA1\u6A21\u578B\u8D28\u91CF\u5DEE"
2235
+ ] }),
2236
+ ledgerDays.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { width: 460, height: 96, viewBox: "0 0 460 96", style: { display: "block", maxWidth: "100%", marginTop: 8 }, children: [
2237
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: 0, y1: 48, x2: 460, y2: 48, stroke: dark ? "rgba(148,163,184,.35)" : "rgba(107,114,128,.35)", strokeDasharray: "3 3" }),
2238
+ ledgerDays.map((d, i) => {
2239
+ const w = Math.max(6, Math.min(22, 420 / ledgerDays.length - 4));
2240
+ const x = 8 + i * (w + 4);
2241
+ const h = Math.abs(d.netTokens) / ledgerMax * 38;
2242
+ const positive = d.netTokens >= 0;
2243
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("g", { children: [
2244
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("rect", { x, y: positive ? 48 - h : 48, width: w, height: Math.max(1, h), rx: 2, fill: positive ? "#059669" : "#dc2626", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("title", { children: `${d.day} \xB7 net ${d.netTokens} tok \xB7 \u8BF7\u6C42 ${d.requests}` }) }),
2245
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("text", { x: x + w / 2, y: 92, fontSize: 8, fill: dark ? "#8b93a7" : "#6b7280", textAnchor: "middle", children: (d.day ?? "").slice(5).replace("-", "/") })
2246
+ ] }, d.day);
2247
+ })
2248
+ ] }),
2249
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { ...hb2.sub, marginTop: 4 }, children: "\u7EFF = \u5F53\u65E5\u51C0\u7701 \xB7 \u7EA2 = \u5F53\u65E5\u51C0\u4E8F\uFF08\u6309\u5929\u6EDA\u52A8\uFF0C\u6700\u8FD1 14 \u5929\uFF09" })
2250
+ ] })
2251
+ ] }),
2159
2252
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: hb2.panel, children: [
2160
2253
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.panelTitle, children: "\u8FD1 14 \u5929\u6BCF\u65E5\u6D3B\u52A8 \xB7 mem vs esr" }),
2161
2254
  daily.rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: hb2.sub, children: "\u8FD8\u6CA1\u6709\u6309\u5929\u6570\u636E\uFF08\u5DE5\u5177\u8C03\u7528\u4F1A\u5B9E\u65F6\u7D2F\u8BA1\uFF09\u3002" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: BW, height: BH, viewBox: `0 0 ${BW} ${BH}`, style: { display: "block", maxWidth: "100%" }, children: daily.rows.map((d, i) => {
@@ -2254,7 +2347,7 @@ function gapsOf(t) {
2254
2347
  if (!t.memoryRefs || t.memoryRefs.length === 0) gaps.push("memory_ref");
2255
2348
  return gaps;
2256
2349
  }
2257
- function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMemory, onChanged }) {
2350
+ function EngramDetail({ target, api, tasks, nodes, links, memories = [], onClose, onNavigateMemory, onChanged }) {
2258
2351
  const { dark, vars } = useEngramTheme();
2259
2352
  const [gArt, setGArt] = (0, import_react6.useState)("");
2260
2353
  const [gEval, setGEval] = (0, import_react6.useState)("");
@@ -2274,6 +2367,15 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
2274
2367
  if (target.kind !== "node" || !node) return [];
2275
2368
  return links.filter((l) => l.source === node.id || l.target === node.id).map((l) => ({ l, outgoing: l.source === node.id }));
2276
2369
  }, [links, node, target.kind]);
2370
+ const memoryLinks = (0, import_react6.useMemo)(() => {
2371
+ if (target.kind !== "memory") return null;
2372
+ const m = target.memory;
2373
+ return {
2374
+ m,
2375
+ replacedBy: memories.filter((x) => x.status === "active" && x.supersedes === m.id),
2376
+ contradictedBy: memories.filter((x) => x.status === "active" && x.contradicts === m.id)
2377
+ };
2378
+ }, [target, memories]);
2277
2379
  const hb2 = {
2278
2380
  root: {
2279
2381
  ...vars,
@@ -2304,6 +2406,8 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
2304
2406
  badge: { fontSize: 10.5, fontWeight: 700, padding: "2px 8px", borderRadius: 999, color: "#fff" },
2305
2407
  tag: { fontSize: 11, padding: "1px 8px", borderRadius: 999, background: "var(--dsh-color-hover-bg, #f3f4f6)", color: "var(--dsh-color-muted, #6b7280)", border: "1px solid var(--dsh-color-border, #e5e7eb)" },
2306
2408
  chip: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(37,99,235,.1)", color: "#2563eb", border: "1px solid rgba(37,99,235,.25)", cursor: "pointer" },
2409
+ chipSup: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(124,58,237,.1)", color: "#7c3aed", border: "1px solid rgba(124,58,237,.25)", cursor: "pointer" },
2410
+ chipDanger: { fontSize: 11, padding: "2px 9px", borderRadius: 999, background: "rgba(220,38,38,.1)", color: "#dc2626", border: "1px solid rgba(220,38,38,.25)", cursor: "pointer" },
2307
2411
  input: { fontSize: 12, padding: "5px 8px", border: "1px solid var(--dsh-color-border, #d1d5db)", borderRadius: 8, background: "var(--dsh-color-surface, #ffffff)", color: "var(--dsh-color-muted-strong, #374151)", width: "100%", boxSizing: "border-box" },
2308
2412
  btn: { fontSize: 11.5, fontWeight: 600, padding: "5px 10px", borderRadius: 8, cursor: "pointer", border: "1px solid var(--dsh-color-border, #d1d5db)", background: "transparent", color: "var(--dsh-color-muted-strong, #374151)" },
2309
2413
  btnPrimary: { fontSize: 11.5, fontWeight: 700, padding: "5px 12px", borderRadius: 8, cursor: "pointer", border: "none", background: "#2563eb", color: "#fff" },
@@ -2442,6 +2546,29 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
2442
2546
  t
2443
2547
  ] }, t)) })
2444
2548
  ] }),
2549
+ memoryLinks && (memoryLinks.m.supersedes || memoryLinks.m.contradicts || memoryLinks.replacedBy.length > 0 || memoryLinks.contradictedBy.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: hb2.section, children: [
2550
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: hb2.k, children: "\u66FF\u6362 / \u51B2\u7A81" }),
2551
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", gap: 5, flexWrap: "wrap", marginTop: 3 }, children: [
2552
+ memoryLinks.m.supersedes && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipSup, title: `\u672C\u6761\u66FF\u4EE3 ${memoryLinks.m.supersedes}\uFF08\u65E7\u884C\u4FDD\u7559\u3001\u53EC\u56DE\u964D\u7EA7\uFF09`, onClick: () => onNavigateMemory(memoryLinks.m.supersedes), children: [
2553
+ "\u66FF\u4EE3 #",
2554
+ memoryLinks.m.supersedes.slice(0, 8)
2555
+ ] }),
2556
+ memoryLinks.m.contradicts && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipDanger, title: `\u672C\u6761\u4E0E ${memoryLinks.m.contradicts} \u51B2\u7A81\uFF08\u4E24\u6761\u90FD\u4FDD\u7559\uFF09`, onClick: () => onNavigateMemory(memoryLinks.m.contradicts), children: [
2557
+ "\u51B2\u7A81 #",
2558
+ memoryLinks.m.contradicts.slice(0, 8)
2559
+ ] }),
2560
+ memoryLinks.replacedBy.map((x) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipSup, title: `\u5DF2\u88AB ${x.text.slice(0, 40)}\u2026 \u66FF\u4EE3`, onClick: () => onNavigateMemory(x.id), children: [
2561
+ "\u2190 \u88AB #",
2562
+ x.id.slice(0, 8),
2563
+ " \u66FF\u4EE3"
2564
+ ] }, x.id)),
2565
+ memoryLinks.contradictedBy.map((x) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: hb2.chipDanger, title: `${x.text.slice(0, 40)}\u2026 \u4E0E\u672C\u6761\u51B2\u7A81`, onClick: () => onNavigateMemory(x.id), children: [
2566
+ "\u21C4 #",
2567
+ x.id.slice(0, 8),
2568
+ " \u51B2\u7A81"
2569
+ ] }, x.id))
2570
+ ] })
2571
+ ] }),
2445
2572
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: hb2.section, children: [
2446
2573
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: hb2.k, children: "\u5143\u6570\u636E" }),
2447
2574
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 2, marginTop: 3, fontSize: 11.5, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", color: "var(--dsh-color-muted, #6b7280)" }, children: [
@@ -2469,6 +2596,18 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
2469
2596
  " \xB7 updated ",
2470
2597
  fmtDate(target.memory.updatedAt)
2471
2598
  ] }),
2599
+ target.memory.source && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2600
+ "source ",
2601
+ target.memory.source
2602
+ ] }),
2603
+ target.memory.conditions && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2604
+ "conditions ",
2605
+ target.memory.conditions
2606
+ ] }),
2607
+ target.memory.filePath && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2608
+ "file ",
2609
+ target.memory.filePath
2610
+ ] }),
2472
2611
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { children: [
2473
2612
  "TTL ",
2474
2613
  daysLeft(target.memory.expiresAt)
@@ -2568,6 +2707,11 @@ function EngramDetail({ target, api, tasks, nodes, links, onClose, onNavigateMem
2568
2707
 
2569
2708
  // client/src/EngramSection.tsx
2570
2709
  var import_jsx_runtime7 = require("react/jsx-runtime");
2710
+ function signedTokens(n) {
2711
+ const sign = n > 0 ? "+" : n < 0 ? "\u2212" : "";
2712
+ const abs = Math.abs(n);
2713
+ return abs >= 1e3 ? `${sign}${(abs / 1e3).toFixed(1)}k` : `${sign}${abs}`;
2714
+ }
2571
2715
  var KIND_COLORS2 = {
2572
2716
  preference: "#b45309",
2573
2717
  decision: "#2563eb",
@@ -2791,6 +2935,14 @@ function EngramSection({ api, t }) {
2791
2935
  const [closeEval, setCloseEval] = (0, import_react7.useState)("");
2792
2936
  const [closeRefs, setCloseRefs] = (0, import_react7.useState)("");
2793
2937
  const [closeBusy, setCloseBusy] = (0, import_react7.useState)(false);
2938
+ const [importDump, setImportDump] = (0, import_react7.useState)(null);
2939
+ const [importName, setImportName] = (0, import_react7.useState)("");
2940
+ const [importMode, setImportMode] = (0, import_react7.useState)("merge");
2941
+ const [importDryRun, setImportDryRun] = (0, import_react7.useState)(true);
2942
+ const [importReplaceOk, setImportReplaceOk] = (0, import_react7.useState)(false);
2943
+ const [importBusy, setImportBusy] = (0, import_react7.useState)(false);
2944
+ const [importReport, setImportReport] = (0, import_react7.useState)(null);
2945
+ const [dataError, setDataError] = (0, import_react7.useState)(null);
2794
2946
  const refresh = (0, import_react7.useCallback)(async () => {
2795
2947
  setBusy(true);
2796
2948
  setError(null);
@@ -2847,6 +2999,24 @@ function EngramSection({ api, t }) {
2847
2999
  for (const t2 of tasks) map.set(t2.id, t2.name);
2848
3000
  return (id) => map.get(id) ?? id;
2849
3001
  }, [nodes, tasks]);
3002
+ const supersedeIndex = (0, import_react7.useMemo)(() => {
3003
+ const replacedBy = /* @__PURE__ */ new Map();
3004
+ const contradictedBy = /* @__PURE__ */ new Map();
3005
+ for (const m of memories) {
3006
+ if (m.status !== "active") continue;
3007
+ if (m.supersedes) {
3008
+ const list = replacedBy.get(m.supersedes) ?? [];
3009
+ list.push(m);
3010
+ replacedBy.set(m.supersedes, list);
3011
+ }
3012
+ if (m.contradicts) {
3013
+ const list = contradictedBy.get(m.contradicts) ?? [];
3014
+ list.push(m);
3015
+ contradictedBy.set(m.contradicts, list);
3016
+ }
3017
+ }
3018
+ return { replacedBy, contradictedBy };
3019
+ }, [memories]);
2850
3020
  (0, import_react7.useEffect)(() => {
2851
3021
  if (!newTaskWs && workspaces.length > 0 && !workspace) setNewTaskWs(workspaces[0]);
2852
3022
  }, [newTaskWs, workspaces, workspace]);
@@ -2880,6 +3050,60 @@ function EngramSection({ api, t }) {
2880
3050
  setGcRunning(false);
2881
3051
  }
2882
3052
  };
3053
+ const downloadExport = async () => {
3054
+ setDataError(null);
3055
+ try {
3056
+ const dump = await api.exportData(workspace || void 0);
3057
+ const blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" });
3058
+ const url = URL.createObjectURL(blob);
3059
+ const a = document.createElement("a");
3060
+ const safe = (workspace || "all").replace(/[^a-zA-Z0-9._-]+/g, "_");
3061
+ a.href = url;
3062
+ a.download = `dsh-engram-export-${safe}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`;
3063
+ a.click();
3064
+ URL.revokeObjectURL(url);
3065
+ } catch (e) {
3066
+ setDataError(e instanceof Error ? e.message : String(e));
3067
+ }
3068
+ };
3069
+ const pickImportFile = async (file) => {
3070
+ setDataError(null);
3071
+ setImportReport(null);
3072
+ setImportDump(null);
3073
+ setImportName("");
3074
+ if (!file) return;
3075
+ try {
3076
+ const text = await file.text();
3077
+ setImportDump(JSON.parse(text));
3078
+ setImportName(file.name);
3079
+ } catch (e) {
3080
+ setDataError(`\u65E0\u6CD5\u89E3\u6790 ${file.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
3081
+ }
3082
+ };
3083
+ const runImport = async () => {
3084
+ if (importDump === null) return;
3085
+ if (importMode === "replace" && !importReplaceOk) {
3086
+ setDataError("\u66FF\u6362\u6A21\u5F0F\u4F1A\u5148\u6E05\u7A7A\u76EE\u6807\u5DE5\u4F5C\u533A \u2014 \u8BF7\u5148\u52FE\u9009\u786E\u8BA4");
3087
+ return;
3088
+ }
3089
+ setImportBusy(true);
3090
+ setDataError(null);
3091
+ try {
3092
+ const report = await api.importData({
3093
+ payload: importDump,
3094
+ mode: importMode,
3095
+ dryRun: importDryRun,
3096
+ workspace: workspace || void 0,
3097
+ confirm: importMode === "replace" ? "restore" : void 0
3098
+ });
3099
+ setImportReport(report);
3100
+ if (!report.dryRun) await refresh();
3101
+ } catch (e) {
3102
+ setDataError(e instanceof Error ? e.message : String(e));
3103
+ } finally {
3104
+ setImportBusy(false);
3105
+ }
3106
+ };
2883
3107
  const groupedRows = workspace === "" ? groupByWorkspace(memories) : [[workspace, memories]];
2884
3108
  const taskGroups = workspace === "" ? groupByWorkspace(tasks) : [[workspace, tasks]];
2885
3109
  const linkGroups = workspace === "" ? groupByWorkspace(links) : [[workspace, links]];
@@ -2992,6 +3216,7 @@ function EngramSection({ api, t }) {
2992
3216
  const memNum = wsCounts ? wsCounts.memories : overview.totals.memories;
2993
3217
  const taskNum = wsCounts ? wsCounts.tasks : overview.totals.tasks;
2994
3218
  const linkNum = wsCounts ? wsCounts.links : overview.totals.links;
3219
+ const ledgerBucket = overview.ledger ? workspace && overview.ledger.byWorkspace[workspace] || overview.ledger.totals : null;
2995
3220
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.stats, children: [
2996
3221
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(memNum), label: wsCounts ? "\u8BB0\u5FC6 (active)" : "\u8BB0\u5FC6 (active, \u5168\u5C40)" }),
2997
3222
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatCard, { num: String(taskNum), label: wsCounts ? "\u4EFB\u52A1 (active)" : "\u4EFB\u52A1 (active, \u5168\u5C40)" }),
@@ -3013,6 +3238,13 @@ function EngramSection({ api, t }) {
3013
3238
  num: String(gc.archivedMemories + gc.archivedTasks),
3014
3239
  label: `GC \u5DF2\u5F52\u6863 \xB7 \u94FE\u63A5-${gc.removedLinks}`
3015
3240
  }
3241
+ ),
3242
+ ledgerBucket && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3243
+ StatCard,
3244
+ {
3245
+ num: signedTokens(ledgerBucket.netTokens),
3246
+ label: `\u4E0A\u4E0B\u6587\u51C0 token \xB7 \u8BF7\u6C42 ${ledgerBucket.requests}`
3247
+ }
3016
3248
  )
3017
3249
  ] });
3018
3250
  })(),
@@ -3069,6 +3301,90 @@ function EngramSection({ api, t }) {
3069
3301
  ] }, l.id))
3070
3302
  ] })
3071
3303
  ] }),
3304
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.subPanel, children: [
3305
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
3306
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { fontWeight: 700, fontSize: 13 }, children: "\u6570\u636E\u5907\u4EFD\uFF08export / import\uFF09" }),
3307
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.btn, title: "\u4E0B\u8F7D\u5F53\u524D\u5DE5\u4F5C\u533A\uFF08\u6216\u5168\u90E8\uFF09\u7684 dsh-engram/export v1 \u8F6C\u50A8", onClick: () => void downloadExport(), children: "\u5BFC\u51FA JSON" }),
3308
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { ...s2.btn, display: "inline-flex", alignItems: "center", gap: 6, cursor: "pointer" }, children: [
3309
+ "\u9009\u62E9\u5BFC\u5165\u6587\u4EF6\u2026",
3310
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3311
+ "input",
3312
+ {
3313
+ type: "file",
3314
+ accept: "application/json,.json",
3315
+ style: { display: "none" },
3316
+ onChange: (e) => {
3317
+ void pickImportFile(e.target.files?.[0] ?? null);
3318
+ e.currentTarget.value = "";
3319
+ }
3320
+ }
3321
+ )
3322
+ ] }),
3323
+ importName && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: s2.mono, children: importName })
3324
+ ] }),
3325
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 11.5, color: "var(--dsh-color-muted, #6b7280)", marginTop: 4 }, children: [
3326
+ "\u5BFC\u51FA\u5373\u53EF\u79FB\u690D\u5907\u4EFD\uFF08\u8BB0\u5FC6/\u4EFB\u52A1/\u5173\u7CFB/\u8282\u70B9\uFF0C\u542B\u5F52\u6863\uFF09\uFF1B",
3327
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: "merge" }),
3328
+ " \u53EA\u8865\u7F3A\u5931\uFF08\u5E42\u7B49\uFF09\uFF0C",
3329
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: "replace" }),
3330
+ " \u5148\u6E05\u7A7A\u76EE\u6807\u5DE5\u4F5C\u533A\u518D\u6062\u590D\u3002\u8BB0\u5FC6\u6B63\u6587\u4E0E\u6240\u6709\u5199\u5165\u8DEF\u5F84\u4E00\u6837\u8FC7\u8131\u654F\u3002"
3331
+ ] }),
3332
+ importDump !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { marginTop: 8, display: "flex", flexDirection: "column", gap: 6 }, children: [
3333
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap", fontSize: 12.5 }, children: [
3334
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { display: "inline-flex", gap: 4, alignItems: "center" }, children: [
3335
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: importDryRun, onChange: (e) => setImportDryRun(e.target.checked) }),
3336
+ "\u4EC5\u9884\u89C8\uFF08dry run\uFF09"
3337
+ ] }),
3338
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3339
+ "select",
3340
+ {
3341
+ style: s2.input,
3342
+ value: importMode,
3343
+ onChange: (e) => {
3344
+ const mode = e.target.value;
3345
+ setImportMode(mode);
3346
+ if (mode !== "replace") setImportReplaceOk(false);
3347
+ },
3348
+ children: [
3349
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "merge", children: "\u5408\u5E76\uFF08merge\uFF0C\u5E42\u7B49\uFF09" }),
3350
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "replace", children: "\u66FF\u6362\uFF08replace\uFF0C\u5148\u6E05\u7A7A\u5DE5\u4F5C\u533A\uFF09" })
3351
+ ]
3352
+ }
3353
+ ),
3354
+ importMode === "replace" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { style: { display: "inline-flex", gap: 4, alignItems: "center", color: "#b45309" }, children: [
3355
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: importReplaceOk, onChange: (e) => setImportReplaceOk(e.target.checked) }),
3356
+ "\u6211\u786E\u8BA4\u66FF\u6362",
3357
+ workspace ? `\uFF08${workspace}\uFF09` : "\uFF08\u9700\u5148\u9009\u5DE5\u4F5C\u533A\uFF09"
3358
+ ] }),
3359
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3360
+ "button",
3361
+ {
3362
+ style: s2.btnPrimary,
3363
+ disabled: importBusy || importMode === "replace" && (!importReplaceOk || !workspace),
3364
+ onClick: () => void runImport(),
3365
+ children: importBusy ? "\u2026" : importDryRun ? "\u9884\u89C8\u5BFC\u5165" : "\u6267\u884C\u5BFC\u5165"
3366
+ }
3367
+ )
3368
+ ] }),
3369
+ importReport && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { fontSize: 12.5 }, children: [
3370
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
3371
+ importReport.dryRun ? "dry-run \u9884\u89C8\uFF1A" : "\u5DF2\u5BFC\u5165\uFF1A",
3372
+ " ",
3373
+ Object.entries(importReport.written).map(([k, v]) => `${k} ${v}`).join(" \xB7 ") || "\u2014",
3374
+ importReport.skipped.length > 0 ? ` \xB7 \u8DF3\u8FC7 ${importReport.skipped.length}` : ""
3375
+ ] }),
3376
+ importReport.skipped.slice(0, 5).map((x, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.mono, children: [
3377
+ "- ",
3378
+ x.table,
3379
+ " ",
3380
+ x.id ? x.id.slice(0, 8) : "?",
3381
+ ": ",
3382
+ x.reason
3383
+ ] }, `${x.table}-${x.id ?? i}`))
3384
+ ] }),
3385
+ dataError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 12, color: "#dc2626", fontWeight: 600 }, children: dataError })
3386
+ ] })
3387
+ ] }),
3072
3388
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: s2.row, children: [
3073
3389
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("select", { style: s2.input, value: kind, onChange: (e) => setKind(e.target.value), children: [
3074
3390
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "", children: "\u5168\u90E8\u7C7B\u578B" }),
@@ -3142,7 +3458,8 @@ function EngramSection({ api, t }) {
3142
3458
  ] }) }, `h-${r.ws}`) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("tr", { children: [
3143
3459
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("td", { style: { ...s2.td, whiteSpace: "nowrap" }, children: [
3144
3460
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.badge, background: KIND_COLORS2[r.m.kind] ?? "#6b7280" }, children: KIND_LABEL2[r.m.kind] ?? r.m.kind }),
3145
- r.m.status === "archived" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, children: "archived" })
3461
+ r.m.status === "archived" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, children: "archived" }),
3462
+ supersedeIndex.replacedBy.has(r.m.id) && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...s2.tag, color: "#b45309", background: "#fef3c7" }, title: `\u5DF2\u88AB ${(supersedeIndex.replacedBy.get(r.m.id) ?? []).map((x) => `#${x.id.slice(0, 8)}`).join(", ")} \u66FF\u4EE3\uFF08\u65E7\u884C\u4FDD\u7559\u3001\u53EC\u56DE\u964D\u7EA7\uFF09`, children: "\u5DF2\u66FF\u6362" })
3146
3463
  ] }),
3147
3464
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("td", { style: { ...s2.td, minWidth: 0 }, children: [
3148
3465
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { title: r.m.text, style: expandedRows.has(r.m.id) ? s2.expanded : s2.clamp3, children: r.m.text }),
@@ -3159,7 +3476,11 @@ function EngramSection({ api, t }) {
3159
3476
  " \xB7 ",
3160
3477
  daysLeft2(r.m.expiresAt)
3161
3478
  ] }),
3162
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.linkBtn, onClick: () => toggleExpand(r.m.id), children: expandedRows.has(r.m.id) ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u6587" })
3479
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { style: s2.linkBtn, onClick: () => toggleExpand(r.m.id), children: expandedRows.has(r.m.id) ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u6587" }),
3480
+ r.m.supersedes && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("button", { style: s2.linkBtn, title: `\u6253\u5F00\u88AB\u66FF\u4EE3\u7684\u65E7\u8BB0\u5FC6 ${r.m.supersedes}`, onClick: () => openMemory(r.m.supersedes), children: [
3481
+ "\u66FF\u4EE3 #",
3482
+ r.m.supersedes.slice(0, 8)
3483
+ ] })
3163
3484
  ] }),
3164
3485
  r.m.tags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginTop: 3, display: "flex", flexWrap: "wrap", gap: 4 }, children: r.m.tags.map((tag) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: s2.tag, children: tag }, tag)) })
3165
3486
  ] }),
@@ -3359,6 +3680,7 @@ ${r.m.text.slice(0, 60)}`)) void act(() => api.remove(r.m.id, r.m.workspace));
3359
3680
  tasks,
3360
3681
  nodes,
3361
3682
  links,
3683
+ memories,
3362
3684
  onClose: () => setDetail(null),
3363
3685
  onNavigateMemory: openMemory,
3364
3686
  onChanged: () => void refresh()
@@ -3375,10 +3697,12 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
3375
3697
  var COMMON_FIELDS = [
3376
3698
  { key: "autoCapture", label: "\u81EA\u52A8\u6355\u83B7", hint: "\u96F6 LLM \u4ECE\u5DE5\u5177\u7ED3\u679C\u63D0\u53D6\u8BB0\u5FC6\uFF08git/\u5173\u952E\u6587\u4EF6/\u9519\u8BEF\uFF09", kind: "bool" },
3377
3699
  { key: "autoCapturePreferences", label: "\u504F\u597D\u6355\u83B7", hint: "\u7528\u6237\u7EA0\u6B63/\u504F\u597D\u5B66\u4E60\uFF1A\u68C0\u6D4B\u6D88\u606F\u91CC\u7684\u300C\u4E0D\u8981\u7528X/\u6539\u7528Y/prefer/never use\u300D\u53E5\u5F0F\uFF0C\u81EA\u52A8\u6C89\u6DC0\u4E3A preference \u8BB0\u5FC6\u5E76\u7F6E\u9876 [ENGRAM]\uFF08correx/PAHF \u5F0F\uFF0C\u96F6 LLM\uFF09", kind: "bool" },
3378
- { key: "boundaryPrune", label: "\u8FB9\u754C\u526A\u679D", hint: "ResultPack L1\uFF1Atodo \u5B8C\u6210/goal \u7EC8\u6001\u8FB9\u754C\u4E3B\u52A8\u526A\u6389\u8D85\u9608\u503C\u65E7\u5DE5\u5177\u7ED3\u679C\uFF08DSH \u786E\u5B9A\u6027 surface-replace\u3001\u507F\u8FD8\u95E8\u3001\u96F6 LLM\uFF1B\u9ED8\u8BA4\u5173\uFF09", kind: "bool" },
3700
+ { key: "boundaryPrune", label: "\u8FB9\u754C\u526A\u679D", hint: "ResultPack L1\uFF1Atodo \u5B8C\u6210/goal \u7EC8\u6001\u8FB9\u754C\u4E3B\u52A8\u526A\u6389\u8D85\u9608\u503C\u65E7\u5DE5\u5177\u7ED3\u679C\uFF08DSH \u786E\u5B9A\u6027 surface-replace\u3001\u507F\u8FD8\u95E8\u3001\u96F6 LLM\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u5173=opt-out\uFF09", kind: "bool" },
3701
+ { key: "boundaryPruneAdaptive", label: "\u526A\u679D\u81EA\u9002\u5E94", hint: "\u8D26\u672C\u5426\u51B3\uFF1A\u8BE5 workspace \u771F\u5B9E\u526A\u679D\u540E\u51C0\u6536\u76CA\u4E3A\u8D1F\uFF08\u22653 \u6B21\uFF09\u65F6\u81EA\u52A8\u505C\u6B62\u7EE7\u7EED\u526A\uFF1B\u8BC1\u636E\u53EA\u80FD\u5173\u3001\u4E0D\u4F1A\u81EA\u884C\u5F00", kind: "bool" },
3379
3702
  { key: "boundaryPruneThresholdChars", label: "\u526A\u679D\u9608\u503C(\u5B57\u7B26)", hint: "\u8986\u76D6\u5BBF\u4E3B 8192\uFF1A\u8BBE 2048 \u53EF\u66F4\u65E9\u5F00\u526A\uFF0C\u5E76\u914D L2 \u5F52\u6863\u627E\u56DE\u88AB\u526A\u4E2D\u6BB5\uFF1B\u7559\u7A7A=\u5BBF\u4E3B\u9ED8\u8BA4\u3002\u5EFA\u8BAE\u540C\u65F6\u628A\u4FDD\u7559\u8FB9\u7F18\u8C03\u5C0F\uFF08\u5982 1024/256\uFF09\u4EE5\u83B7\u5F97\u6700\u5927\u8282\u7701", kind: "num", min: 256, max: 16384, step: 256 },
3380
- { key: "resultPack", label: "\u7ED3\u679C\u5F52\u6863\u53EC\u56DE", hint: "ResultPack L2\uFF1A\u526A\u6389\u7684\u4E2D\u6BB5\u8131\u654F\u5F52\u6863\uFF0C\u8282\u70B9\u5C3E\u90E8\u9644 handle\uFF0C\u6A21\u578B\u53EF\u7528 result_recall(callId) \u6309\u9875\u53EC\u56DE\uFF08\u5B8C\u6574 ObservationPack\uFF1B\u9700\u4E0E\u8FB9\u754C\u526A\u679D\u4E00\u8D77\u5F00\uFF09", kind: "bool" },
3703
+ { key: "resultPack", label: "\u7ED3\u679C\u5F52\u6863\u53EC\u56DE", hint: "ResultPack L2\uFF1A\u526A\u6389\u7684\u4E2D\u6BB5\u8131\u654F\u5F52\u6863\uFF0C\u8282\u70B9\u5C3E\u90E8\u9644 handle\uFF0C\u6A21\u578B\u53EF\u7528 result_recall(callId) \u6309\u9875\u53EC\u56DE\uFF08\u5B8C\u6574 ObservationPack\uFF1B\u9ED8\u8BA4\u5F00\uFF0C\u4FDD\u8BC1\u9ED8\u8BA4\u526A\u679D\u65E0\u635F\uFF1B\u9700\u4E0E\u8FB9\u754C\u526A\u679D\u4E00\u8D77\u5F00\uFF09", kind: "bool" },
3381
3704
  { key: "autoRecallOnStart", label: "\u4F1A\u8BDD\u542F\u52A8\u81EA\u52A8\u53EC\u56DE", hint: "\u65B0\u4F1A\u8BDD\u6309\u9996\u6761\u6D88\u606F\u81EA\u52A8\u628A\u6700\u76F8\u5173\u8BB0\u5FC6\u6CE8\u5165 [RECALL] \u5757\uFF08\u65E0\u9700\u6A21\u578B\u60F3\u8D77\u8C03 engram_recall\uFF09\uFF1B\u5173\u6389=\u56DE\u5230\u7EAF\u62C9\u53D6", kind: "bool" },
3705
+ { key: "jitEnabled", label: "JIT \u63A8\u9001", hint: "\u9519\u8BEF\u590D\u53D1 / \u6587\u4EF6\u7126\u70B9 / goal \u5B8C\u6210\u65F6\uFF0C\u628A\u5DF2\u6709\u8BB0\u5FC6\u538B\u6210\u4E00\u884C\u63A8\u8FDB\u4E0A\u4E0B\u6587\uFF08append-only\u3001\u6BCF\u4F1A\u8BDD\u22643 \u884C\u3001\u65E0\u547D\u4E2D\u6C89\u9ED8\uFF1B\u6BCF\u6B21\u63A8\u9001\u6309\u524D\u7F00\u91CD\u5199\u8BA1\u5165\u8D26\u672C\uFF09", kind: "bool" },
3382
3706
  { key: "maxRecallPerSession", label: "\u6BCF\u4F1A\u8BDD\u53BB\u91CD\u4E0A\u9650", hint: "recall \u6BCF\u4F1A\u8BDD\u6700\u591A\u4FDD\u7559 N \u6761\uFF08\u8DE8\u4F1A\u8BDD\u53BB\u91CD\uFF09", kind: "num", min: 1, max: 10 },
3383
3707
  { key: "expireDays", label: "TTL\uFF08\u5929\uFF09", hint: "0 = \u4E0D\u8FC7\u671F", kind: "num", min: 0, max: 3650 },
3384
3708
  { key: "indexMaxChars", label: "\u7D22\u5F15\u5B57\u7B26\u4E0A\u9650", hint: "[ENGRAM] \u5757 token \u9884\u7B97", kind: "num", min: 0, max: 4e3 }