@kanadego/dsh-heartbeat 1.5.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -0
  3. package/assets/frontwin.ps1 +44 -0
  4. package/assets/idle.ps1 +27 -0
  5. package/assets/notify.ps1 +69 -0
  6. package/assets/presets/heartbeat/agent.cordis.yml +66 -0
  7. package/assets/presets/heartbeat/preset.yml +2 -0
  8. package/assets/screenpulse.ps1 +168 -0
  9. package/assets/vault.ps1 +52 -0
  10. package/client.js +428 -0
  11. package/config/busy-rules.json +63 -0
  12. package/config/interests.json +30 -0
  13. package/config/policy.json +57 -0
  14. package/config/profile-schema.json +68 -0
  15. package/config/watchlist.json +9 -0
  16. package/cordis.patch.yml +14 -0
  17. package/dist/bindings-XPPSKILN.js +19 -0
  18. package/dist/bindings-XPPSKILN.js.map +1 -0
  19. package/dist/chunk-2M35HRL6.js +1207 -0
  20. package/dist/chunk-2M35HRL6.js.map +1 -0
  21. package/dist/chunk-4UE74TUB.js +98 -0
  22. package/dist/chunk-4UE74TUB.js.map +1 -0
  23. package/dist/chunk-AISZRA4C.js +235 -0
  24. package/dist/chunk-AISZRA4C.js.map +1 -0
  25. package/dist/chunk-J6ZTRFFW.js +64 -0
  26. package/dist/chunk-J6ZTRFFW.js.map +1 -0
  27. package/dist/chunk-LLD7LUNN.js +202 -0
  28. package/dist/chunk-LLD7LUNN.js.map +1 -0
  29. package/dist/chunk-S7PTR42P.js +19 -0
  30. package/dist/chunk-S7PTR42P.js.map +1 -0
  31. package/dist/cli/index.js +589 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/inbox-MMLHISQV.js +22 -0
  34. package/dist/inbox-MMLHISQV.js.map +1 -0
  35. package/dist/index.js +2745 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/lib-FJP7J4T6.js +2281 -0
  38. package/dist/lib-FJP7J4T6.js.map +1 -0
  39. package/dist/runtime-J5NOPRBA.js +11 -0
  40. package/dist/runtime-J5NOPRBA.js.map +1 -0
  41. package/package.json +61 -0
package/client.js ADDED
@@ -0,0 +1,428 @@
1
+ // dsh-heartbeat client half (M6): settings-page card.
2
+ //
3
+ // Contract notes (verified against dsh-vision-router + dsh-client-ui-settings
4
+ // types on 0.1.1-rc.2):
5
+ // - the client module is applied as a CLIENT-SIDE cordis plugin; the
6
+ // ModuleLoader factory must return an object with an "apply" method;
7
+ // - the settings page renders entries contributed to the 'settings.section'
8
+ // slot; each entry = {name, id, order, label, inject} + a React component;
9
+ // - ctx.settingsScope.bind({namespace}) yields a scope with getSnapshot /
10
+ // subscribe / set(field, value) — writes go through the host settings
11
+ // service (loopback-only, process-local persistence);
12
+ // - custom host data flows through an exact Fetch route under /api:
13
+ // ctx.get('connection').rpc.call('/api', 'heartbeat', { endpoint, ...payload }).
14
+ window.__ModuleLoader__.load({
15
+ id: "@kanadego/dsh-heartbeat",
16
+ factory: (require) => {
17
+ var module = { exports: {} };
18
+ var exports = module.exports;
19
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
20
+ const React = require("react");
21
+
22
+ const rowStyle = { display: "flex", alignItems: "center", gap: 8, margin: "6px 0" };
23
+ const labelStyle = { minWidth: 140, fontSize: 13, color: "var(--dsw-alias-label-secondary)", flex: "none" };
24
+ const inputStyle = { width: 90, height: 28, borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-specific-input-bg, transparent)", color: "var(--dsw-alias-label-primary)", padding: "0 8px", fontSize: 13 };
25
+ const buttonStyle = { height: 26, padding: "0 12px", borderRadius: 8, border: "none", background: "#339CFF", color: "#fff", fontSize: 12, cursor: "pointer", flex: "none" };
26
+ const buttonGhost = { ...buttonStyle, background: "var(--dsw-alias-interactive-bg-hover, #444)", color: "var(--dsw-alias-label-primary)" };
27
+ const hintStyle = { fontSize: 12, color: "var(--dsw-alias-label-tertiary)", margin: "4px 0" };
28
+ const sectionStyle = { borderTop: "1px solid var(--dsw-alias-border-l2)", marginTop: 10, paddingTop: 6 };
29
+ const summaryStyle = { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-primary)", cursor: "pointer" };
30
+ const listStyle = { listStyle: "none", margin: "4px 0", padding: 0, fontSize: 12, color: "var(--dsw-alias-label-secondary)" };
31
+ const rowList = { display: "flex", alignItems: "center", gap: 8, padding: "3px 0" };
32
+
33
+ function apply(ctx) {
34
+ let scope;
35
+ try {
36
+ scope = ctx.settingsScope.bind({ namespace: "heartbeat" });
37
+ } catch (e) {
38
+ console.warn("[dsh-heartbeat] settingsScope unavailable", e);
39
+ return;
40
+ }
41
+ const getConnection = () => {
42
+ try { return ctx.get("connection"); } catch { return undefined; }
43
+ };
44
+ const rpc = async (endpoint, payload) => {
45
+ const conn = getConnection();
46
+ if (!conn || !conn.rpc || typeof conn.rpc.call !== "function") {
47
+ throw new Error("RPC 通道不可用(请确认 DSH 正在运行)");
48
+ }
49
+ // C21:0.1.5 起自定义 rpc.handle 通道不可用,宿主侧改为 /api 下的精确
50
+ // Fetch 路由;端点名走 payload.endpoint 字段(信封与 /api 同构)。
51
+ const result = await conn.rpc.call("/api", "heartbeat", { endpoint, ...(payload ?? {}) }, undefined);
52
+ if (!result || result.ok !== true) {
53
+ throw new Error((result && result.error && result.error.message) || "RPC 调用失败");
54
+ }
55
+ return result.value;
56
+ };
57
+ // 会话名来自 client 自己的会话存储(侧边栏同名数据源);拿不到则回退 id。
58
+ const sessionName = (id) => {
59
+ try {
60
+ const list = typeof ctx.sessions?.list === "function" ? ctx.sessions.list() : undefined;
61
+ const hit = Array.isArray(list) ? list.find((s) => s && (s.id === id || s.sessionId === id)) : undefined;
62
+ const name = hit && (hit.title || hit.name || hit.displayName);
63
+ return name ? String(name) : null;
64
+ } catch { return null; }
65
+ };
66
+
67
+ // ── 通用小组件 ────────────────────────────────────────────────
68
+ function Section(props) {
69
+ const [open, setOpen] = React.useState(props.open === true);
70
+ return React.createElement(
71
+ "details",
72
+ { open, style: sectionStyle, onToggle: (e) => setOpen(e.target.open) },
73
+ React.createElement("summary", { style: summaryStyle }, props.title),
74
+ open ? React.createElement("div", { style: { paddingTop: 4 } }, props.children) : null,
75
+ );
76
+ }
77
+ function useAsync(fetcher, deps) {
78
+ const [state, setState] = React.useState({ loading: true, error: null, value: null });
79
+ React.useEffect(() => {
80
+ let alive = true;
81
+ setState({ loading: true, error: null, value: null });
82
+ fetcher().then(
83
+ (value) => { if (alive) setState({ loading: false, error: null, value }); },
84
+ (error) => { if (alive) setState({ loading: false, error: String(error).slice(0, 120), value: null }); },
85
+ );
86
+ return () => { alive = false; };
87
+ }, deps || []);
88
+ return state;
89
+ }
90
+ const fmtTime = (iso) => {
91
+ try { return new Date(iso).toLocaleString("zh-CN", { hour12: false }); } catch { return String(iso); }
92
+ };
93
+
94
+ // ── 心跳状态卡片 ──────────────────────────────────────────────
95
+ function StatusCard() {
96
+ const { loading, error, value } = useAsync(() => rpc("status"), []);
97
+ React.useEffect(() => {
98
+ const t = setInterval(() => { rpc("status").then((v) => setStateSafe(v)).catch(() => {}); }, 30000);
99
+ return () => clearInterval(t);
100
+ }, []);
101
+ const stateRef = React.useRef(null);
102
+ function setStateSafe(v) { stateRef.current = v; force(); }
103
+ const [, force] = React.useReducer((x) => x + 1, 0);
104
+ const view = stateRef.current || value;
105
+ if (loading && !view) return React.createElement("div", { style: hintStyle }, "加载中…");
106
+ if (error) return React.createElement("div", { style: hintStyle }, "状态获取失败:" + error);
107
+ const lb = view.lastBeat || {};
108
+ const verdictText = lb.verdict === "spoke" ? "说了:" + (lb.text || "").slice(0, 60)
109
+ : lb.verdict === "silent" ? "沉默(" + (lb.reason || "") + ")"
110
+ : lb.verdict === "spoke_failed" ? "投递失败(" + (lb.reason || "") + ")"
111
+ : lb.verdict === "error" ? "心跳异常(" + (lb.reason || "") + ")" : "尚无记录";
112
+ const sb = view.statusbar || {};
113
+ const SCENE_LABELS = {
114
+ "quiet-hours": "静默时段,世界睡了",
115
+ "just-spoke": "刚去和你说过话",
116
+ "wandering": "正在闲逛看新东西",
117
+ "busy": "看到你在忙,不去打扰",
118
+ "present": "在场待着",
119
+ "away": "你不在,自己待着",
120
+ };
121
+ const statusText = sb.lastStatus
122
+ ? "心跳此刻:" + (SCENE_LABELS[sb.lastStatus.scene] || sb.lastStatus.scene) + (sb.lastStatus.note ? "——" + sb.lastStatus.note : "")
123
+ : "心跳此刻:(还没有状态记录)";
124
+ return React.createElement(
125
+ "div",
126
+ { style: listStyle },
127
+ React.createElement("div", null, "上次心跳:", fmtTime(lb.at)),
128
+ React.createElement("div", null, "结果:", verdictText),
129
+ React.createElement("div", null, "今日表达:", view.cap.used, " / ", view.cap.max, " 条"),
130
+ React.createElement("div", null, view.quiet ? "当前:静默时段内" : "当前:正常节律(间隔 " + view.intervalMin + " 分钟)"),
131
+ React.createElement("div", null, sb.enabled === false ? statusText + "(状态栏已关闭)" : statusText),
132
+ React.createElement("div", { style: hintStyle },
133
+ "时间注入:", sb.timeInjectMin === 0 ? "已关闭"
134
+ : "每 " + sb.timeInjectMin + " 分钟,最近 " + (sb.lastTimeInjectAt ? fmtTime(sb.lastTimeInjectAt) : "未发生过")),
135
+ );
136
+ }
137
+
138
+ // ── 会话绑定 ──────────────────────────────────────────────────
139
+ function SessionsSection() {
140
+ const [data, setData] = React.useState(null);
141
+ const [error, setError] = React.useState(null);
142
+ const [showUnbound, setShowUnbound] = React.useState(false);
143
+ const [query, setQuery] = React.useState("");
144
+ const reload = React.useCallback(() => {
145
+ Promise.all([rpc("sessions.list"), rpc("bindings.get")]).then(
146
+ ([sessions, bindings]) => setData({ sessions: sessions.sessions, bindings: bindings.bindings }),
147
+ (e) => setError(String(e).slice(0, 100)),
148
+ );
149
+ }, []);
150
+ React.useEffect(() => { reload(); }, [reload]);
151
+ if (error) return React.createElement("div", { style: hintStyle }, "加载失败:" + error);
152
+ if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
153
+ const bound = data.sessions.filter((s) => s.deliver || s.observe);
154
+ const doUnbind = (id) => rpc("bindings.remove", { sessionId: id }).then(reload, (e) => setError(String(e).slice(0, 100)));
155
+ const doBind = (id, deliver, observe) => rpc("bindings.add", { sessionId: id, deliver, observe }).then(reload, (e) => setError(String(e).slice(0, 100)));
156
+ // 已绑定的会话照样能改开关:D13 允许投递 + 观察同时开,所以这里按字段单独
157
+ // 翻转,而不是把会话当成"已绑定就锁死"。两个都关 = 真正解绑。
158
+ const setFlags = (id, deliver, observe) => {
159
+ const call = (!deliver && !observe)
160
+ ? rpc("bindings.remove", { sessionId: id })
161
+ : rpc("bindings.add", { sessionId: id, deliver, observe });
162
+ return call.then(reload, (e) => setError(String(e).slice(0, 100)));
163
+ };
164
+ const flagStyle = (on) => (on ? buttonGhost : { ...buttonGhost, opacity: 0.5 });
165
+ // 会话名:宿主从 projcache 取 title;无 title 时回退 id 前缀
166
+ const nameOf = (s) => s.title || (s.home ? "心跳正身(引擎室)" : s.id.slice(0, 19) + "…");
167
+ const q = query.trim().toLowerCase();
168
+ const matches = (s) => !q || nameOf(s).toLowerCase().includes(q) || s.id.toLowerCase().includes(q);
169
+ const unbound = data.sessions.filter((s) => !s.deliver && !s.observe && matches(s));
170
+ return React.createElement(
171
+ "div",
172
+ null,
173
+ React.createElement("div", { style: hintStyle }, "绑定 = 表达投递 + 对话观察(D13),两个开关可以同时开,也可以在下面逐个切换。心跳正身的思考轮次固定在其自身会话,不受绑定影响。"),
174
+ bound.map((s) => React.createElement("div", { key: s.id, style: rowList },
175
+ React.createElement("span", { style: { flex: 1, fontSize: 12 } },
176
+ "🔗 ", nameOf(s), "(", s.deliver ? "投递" : "", s.deliver && s.observe ? "+" : "", s.observe ? "观察" : "", ")"),
177
+ React.createElement("span", { style: { display: "flex", gap: 4 } },
178
+ React.createElement("button", { style: flagStyle(s.deliver), title: s.deliver ? "点击关闭投递" : "点击开启投递", onClick: () => setFlags(s.id, !s.deliver, s.observe) }, (s.deliver ? "☑ " : "☐ ") + "投递"),
179
+ React.createElement("button", { style: flagStyle(s.observe), title: s.observe ? "点击关闭观察" : "点击开启观察", onClick: () => setFlags(s.id, s.deliver, !s.observe) }, (s.observe ? "☑ " : "☐ ") + "观察"),
180
+ React.createElement("button", { style: buttonGhost, onClick: () => doUnbind(s.id) }, "解绑"),
181
+ ),
182
+ )),
183
+ bound.length === 0 ? React.createElement("div", { style: hintStyle }, "(暂无绑定会话——表达只出现在心跳正身会话)") : null,
184
+ React.createElement("div", { style: rowStyle },
185
+ React.createElement("button", { style: buttonGhost, onClick: () => setShowUnbound(!showUnbound) }, showUnbound ? "收起未绑定列表" : "绑定其他会话(" + unbound.length + ")")),
186
+ showUnbound ? React.createElement(
187
+ "div",
188
+ null,
189
+ React.createElement("input", { style: { ...inputStyle, width: "100%", margin: "4px 0", boxSizing: "border-box" }, placeholder: "按会话名或 id 过滤…", value: query, onChange: (e) => setQuery(e.target.value) }),
190
+ unbound.length === 0 ? React.createElement("div", { style: hintStyle }, "(无匹配会话)") : null,
191
+ unbound.map((s) => React.createElement("div", { key: s.id, style: rowList },
192
+ React.createElement("span", { style: { flex: 1, fontSize: 12 } }, nameOf(s), s.home ? "(心跳正身,无需绑定)" : ""),
193
+ s.home ? null : React.createElement(
194
+ "span",
195
+ { style: { display: "flex", gap: 4 } },
196
+ React.createElement("button", { style: buttonStyle, onClick: () => doBind(s.id, true, false) }, "绑定投递"),
197
+ React.createElement("button", { style: buttonGhost, onClick: () => doBind(s.id, false, true) }, "绑定观察"),
198
+ React.createElement("button", { style: buttonGhost, onClick: () => doBind(s.id, true, true) }, "投递+观察"),
199
+ ),
200
+ )),
201
+ ) : null,
202
+ );
203
+ }
204
+
205
+ // ── 素材池 ────────────────────────────────────────────────────
206
+ function SeedsSection() {
207
+ const [data, setData] = React.useState(null);
208
+ const [error, setError] = React.useState(null);
209
+ const [showArchived, setShowArchived] = React.useState(false);
210
+ const [confirmId, setConfirmId] = React.useState(null);
211
+ const reload = React.useCallback(() => {
212
+ rpc("seeds.list").then(setData, (e) => setError(String(e).slice(0, 100)));
213
+ }, []);
214
+ React.useEffect(() => { reload(); }, [reload]);
215
+ if (error) return React.createElement("div", { style: hintStyle }, "加载失败:" + error);
216
+ if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
217
+ const act = (endpoint, id) => rpc(endpoint, { id }).then(reload, (e) => setError(String(e).slice(0, 100)));
218
+ const row = (s, archived) => React.createElement("div", { key: s.id, style: rowList },
219
+ React.createElement("span", { style: { flex: 1, fontSize: 12 } },
220
+ "[", s.tag, "/", s.source, "] ", s.text.slice(0, 42), " used:", s.used),
221
+ archived
222
+ ? React.createElement("button", { style: buttonGhost, onClick: () => act("seeds.restore", s.id) }, "恢复")
223
+ : React.createElement("button", { style: buttonGhost, onClick: () => act("seeds.archive", s.id) }, "归档"),
224
+ confirmId === s.id
225
+ ? React.createElement("button", { style: { ...buttonStyle, background: "#e5484d" }, onClick: () => { setConfirmId(null); act("seeds.delete", s.id); } }, "确认删除")
226
+ : React.createElement("button", { style: buttonGhost, onClick: () => setConfirmId(s.id) }, "删除"),
227
+ );
228
+ return React.createElement(
229
+ "div",
230
+ null,
231
+ React.createElement("div", { style: hintStyle }, "活跃 ", data.active.length, " / ", data.cap, " 条"),
232
+ data.active.map((s) => row(s, false)),
233
+ data.active.length === 0 ? React.createElement("div", { style: hintStyle }, "(池子是空的——浏览流和画像会慢慢喂养它)") : null,
234
+ React.createElement("div", { style: rowStyle },
235
+ React.createElement("button", { style: buttonGhost, onClick: () => setShowArchived(!showArchived) }, showArchived ? "收起归档区" : "归档区(" + data.archived.length + ")")),
236
+ showArchived ? data.archived.map((s) => row(s, true)) : null,
237
+ );
238
+ }
239
+
240
+ // ── 兴趣范围 + 浏览时段(v1.4.0)──────────────────────────────
241
+ // 首编继承出厂:第一次增删/存时段时,插件自动把出厂 interests.json
242
+ // 完整复制进 data/settings/interests.json,之后卡片就是唯一入口。
243
+ function InterestsSection() {
244
+ const [data, setData] = React.useState(null);
245
+ const [error, setError] = React.useState(null);
246
+ const [status, setStatus] = React.useState("");
247
+ const [confirmText, setConfirmText] = React.useState(null);
248
+ const [draft, setDraft] = React.useState("");
249
+ const [draftWindows, setDraftWindows] = React.useState(null);
250
+ const reload = React.useCallback(() => {
251
+ rpc("interests.list").then((doc) => {
252
+ setData(doc);
253
+ setDraftWindows((prev) => prev || (doc._schedule?.windows || []).map((w) => ({ id: w.id || (w.start + "-" + w.end), start: w.start, end: w.end })));
254
+ }, (e) => setError(String(e).slice(0, 100)));
255
+ }, []);
256
+ React.useEffect(() => { reload(); }, [reload]);
257
+ if (error) return React.createElement("div", { style: hintStyle }, "加载失败:" + error);
258
+ if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
259
+ const interests = data.interests || [];
260
+ const doAdd = () => {
261
+ const text = draft.trim();
262
+ if (!text) return;
263
+ rpc("interests.add", { text }).then(
264
+ () => { setDraft(""); setStatus("已添加"); reload(); },
265
+ (e) => setStatus("添加失败:" + String(e).slice(0, 80)),
266
+ );
267
+ };
268
+ const doRemove = (text) => rpc("interests.remove", { text }).then(
269
+ () => { setConfirmText(null); setStatus("已删除"); reload(); },
270
+ (e) => { setConfirmText(null); setStatus("删除失败:" + String(e).slice(0, 80)); },
271
+ );
272
+ const saveWindows = () => {
273
+ rpc("interests.setWindows", { windows: draftWindows }).then(
274
+ () => { setStatus("浏览时段已保存"); reload(); },
275
+ (e) => setStatus("保存失败:" + String(e).slice(0, 80)),
276
+ );
277
+ };
278
+ const editWindow = (idx, field, value) => setDraftWindows((ws) => ws.map((w, i) => (i === idx ? { ...w, [field]: value } : w)));
279
+ return React.createElement(
280
+ "div",
281
+ null,
282
+ React.createElement("div", { style: hintStyle }, "闲逛搜索按这份清单轮换挑焦点(同一条 3 天内不重复)。增删后即时生效,无需重启。"),
283
+ interests.map((t) => React.createElement("div", { key: t, style: rowList },
284
+ React.createElement("span", { style: { flex: 1, fontSize: 12 } }, t),
285
+ confirmText === t
286
+ ? React.createElement("button", { style: { ...buttonStyle, background: "#e5484d" }, onClick: () => doRemove(t) }, "确认删除")
287
+ : React.createElement("button", { style: buttonGhost, onClick: () => setConfirmText(t) }, "删除"),
288
+ )),
289
+ interests.length === 0 ? React.createElement("div", { style: hintStyle }, "(清单是空的——闲逛将没有焦点可挑)") : null,
290
+ React.createElement("div", { style: rowStyle },
291
+ React.createElement("input", { style: { ...inputStyle, width: 240 }, placeholder: "新兴趣,如:天文摄影", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") doAdd(); } }),
292
+ React.createElement("button", { style: buttonStyle, onClick: doAdd }, "添加"),
293
+ ),
294
+ React.createElement("div", { style: { ...hintStyle, marginTop: 10 } }, "浏览时段(闲逛只在这些窗口内发生;起始须早于结束,多个时段不能重叠):"),
295
+ (draftWindows || []).map((w, idx) => React.createElement("div", { key: idx, style: rowList },
296
+ React.createElement("input", { style: inputStyle, type: "time", value: w.start, onChange: (e) => editWindow(idx, "start", e.target.value) }),
297
+ React.createElement("span", { style: hintStyle }, "到"),
298
+ React.createElement("input", { style: inputStyle, type: "time", value: w.end, onChange: (e) => editWindow(idx, "end", e.target.value) }),
299
+ (draftWindows.length > 1) ? React.createElement("button", { style: buttonGhost, onClick: () => setDraftWindows((ws) => ws.filter((_, i) => i !== idx)) }, "移除") : null,
300
+ )),
301
+ React.createElement("div", { style: rowStyle },
302
+ React.createElement("button", { style: buttonGhost, onClick: () => setDraftWindows((ws) => [...(ws || []), { id: "", start: "11:00", end: "13:00" }]) }, "加一个时段"),
303
+ React.createElement("button", { style: buttonStyle, onClick: saveWindows }, "保存时段"),
304
+ ),
305
+ status ? React.createElement("div", { style: hintStyle }, status) : null,
306
+ );
307
+ }
308
+
309
+ // ── 画像入口 ──────────────────────────────────────────────────
310
+ function ProfileSection() {
311
+ const [data, setData] = React.useState(null);
312
+ const [error, setError] = React.useState(null);
313
+ const load = React.useCallback(() => {
314
+ rpc("profile.digest").then(setData, (e) => setError(String(e).slice(0, 100)));
315
+ }, []);
316
+ React.useEffect(() => { load(); }, [load]);
317
+ const doExport = () => rpc("profile.export").then(
318
+ (v) => setData((d) => ({ ...d, exported: v.path })),
319
+ (e) => setError(String(e).slice(0, 100)),
320
+ );
321
+ if (error) return React.createElement("div", { style: hintStyle }, "加载失败:" + error);
322
+ if (!data) return React.createElement("div", { style: hintStyle }, "加载中…");
323
+ return React.createElement(
324
+ "div",
325
+ { style: listStyle },
326
+ React.createElement("div", null, React.createElement("b", null, "处境切面"), React.createElement("pre", { style: { whiteSpace: "pre-wrap", margin: "2px 0", fontSize: 12 } }, data.tact || "(空)")),
327
+ React.createElement("div", null, React.createElement("b", null, "话题切面"), React.createElement("pre", { style: { whiteSpace: "pre-wrap", margin: "2px 0", fontSize: 12 } }, data.topic || "(空)")),
328
+ React.createElement("div", { style: rowStyle },
329
+ React.createElement("button", { style: buttonGhost, onClick: () => { load(); } }, "刷新"),
330
+ React.createElement("button", { style: buttonGhost, onClick: () => { void doExport(); } }, "导出 Markdown")),
331
+ data.exported ? React.createElement("div", { style: hintStyle }, "已导出:" + data.exported) : null,
332
+ );
333
+ }
334
+
335
+ // ── 配置 + 账本 ───────────────────────────────────────────────
336
+ function ConfigSection() {
337
+ const snap = scope.getSnapshot();
338
+ const value = (snap && (snap.value ?? snap.section ?? snap)) || {};
339
+ const interval = Number(value.intervalMin) > 0 ? Number(value.intervalMin) : 20;
340
+ const cap = Number(value.maxDailySend) > 0 ? Number(value.maxDailySend) : 3;
341
+ const timeInject = value.timeInjectMin === 0 ? 0 : (Number(value.timeInjectMin) > 0 ? Number(value.timeInjectMin) : 25);
342
+ const statusbar = value.statusbar !== false;
343
+ const [draftInterval, setDraftInterval] = React.useState(interval);
344
+ const [draftCap, setDraftCap] = React.useState(cap);
345
+ const [draftTimeInject, setDraftTimeInject] = React.useState(timeInject);
346
+ const [draftStatusbar, setDraftStatusbar] = React.useState(statusbar);
347
+ const [status, setStatus] = React.useState("");
348
+ React.useEffect(() => { setDraftInterval(interval); setDraftCap(cap); setDraftTimeInject(timeInject); setDraftStatusbar(statusbar); }, [interval, cap, timeInject, statusbar]);
349
+ const save = async () => {
350
+ try {
351
+ const di = Math.max(1, Math.min(1440, Math.floor(Number(draftInterval) || 0)));
352
+ const dc = Math.max(1, Math.min(50, Math.floor(Number(draftCap) || 0)));
353
+ const dt = Math.max(0, Math.min(1440, Math.floor(Number(draftTimeInject) || 0)));
354
+ await scope.set("intervalMin", di);
355
+ await scope.set("maxDailySend", dc);
356
+ await scope.set("timeInjectMin", dt);
357
+ await scope.set("statusbar", !!draftStatusbar);
358
+ setStatus("已保存(全部即时生效,无需重启)");
359
+ } catch (e) {
360
+ setStatus("保存失败:" + String(e).slice(0, 80));
361
+ }
362
+ };
363
+ return React.createElement(
364
+ "div",
365
+ null,
366
+ React.createElement("div", { style: rowStyle },
367
+ React.createElement("span", { style: labelStyle }, "心跳间隔(分钟)"),
368
+ React.createElement("input", { style: inputStyle, type: "number", min: 1, max: 1440, value: draftInterval, onChange: (e) => setDraftInterval(e.target.value) })),
369
+ React.createElement("div", { style: rowStyle },
370
+ React.createElement("span", { style: labelStyle }, "每日表达上限(条)"),
371
+ React.createElement("input", { style: inputStyle, type: "number", min: 1, max: 50, value: draftCap, onChange: (e) => setDraftCap(e.target.value) })),
372
+ React.createElement("div", { style: rowStyle },
373
+ React.createElement("span", { style: labelStyle }, "时间注入间隔(分钟)"),
374
+ React.createElement("input", { style: inputStyle, type: "number", min: 0, max: 1440, value: draftTimeInject, onChange: (e) => setDraftTimeInject(e.target.value) }),
375
+ React.createElement("span", { style: hintStyle }, "0 = 关闭")),
376
+ React.createElement("div", { style: rowStyle },
377
+ React.createElement("span", { style: labelStyle }, "状态栏"),
378
+ React.createElement("button", { style: draftStatusbar ? buttonStyle : buttonGhost, onClick: () => setDraftStatusbar(!draftStatusbar) }, draftStatusbar ? "☑ 开启" : "☐ 关闭"),
379
+ React.createElement("span", { style: hintStyle }, "日常会话中的心跳状态感知")),
380
+ React.createElement("div", { style: rowStyle },
381
+ React.createElement("button", { style: buttonStyle, onClick: () => { void save(); } }, "保存"),
382
+ React.createElement("span", { style: hintStyle }, status || "全部参数保存后即时生效,无需重启")),
383
+ );
384
+ }
385
+
386
+ // ── 主卡片 ────────────────────────────────────────────────────
387
+ function HeartbeatSection() {
388
+ const [ledgerMsg, setLedgerMsg] = React.useState("");
389
+ const openLedger = () => rpc("ledger.open").then(
390
+ (v) => setLedgerMsg("已打开:" + v.path),
391
+ (e) => setLedgerMsg("失败:" + String(e).slice(0, 80)),
392
+ );
393
+ return React.createElement(
394
+ "div",
395
+ { style: { padding: "2px 0" } },
396
+ React.createElement(Section, { title: "心跳状态", open: true }, React.createElement(StatusCard, null)),
397
+ React.createElement(Section, { title: "会话绑定" }, React.createElement(SessionsSection, null)),
398
+ React.createElement(Section, { title: "素材池" }, React.createElement(SeedsSection, null)),
399
+ React.createElement(Section, { title: "兴趣范围" }, React.createElement(InterestsSection, null)),
400
+ React.createElement(Section, { title: "用户画像(只读)" }, React.createElement(ProfileSection, null)),
401
+ React.createElement(Section, { title: "账本" }, React.createElement(
402
+ "div",
403
+ null,
404
+ React.createElement("div", { style: rowStyle },
405
+ React.createElement("button", { style: buttonStyle, onClick: () => { void openLedger(); } }, "一键打开账本"),
406
+ React.createElement("span", { style: hintStyle }, ledgerMsg || "账本是心跳 agent 的待办与话题来源,可直接手编")),
407
+ )),
408
+ React.createElement(Section, { title: "节律配置" }, React.createElement(ConfigSection, null)),
409
+ );
410
+ }
411
+
412
+ try {
413
+ ctx.slots.inject("settings.section", function* () {
414
+ yield ctx.slots.register(
415
+ { name: "settings.section", id: "dsh-heartbeat", order: 20, label: () => "心跳" },
416
+ HeartbeatSection,
417
+ );
418
+ });
419
+ } catch (e) {
420
+ console.warn("[dsh-heartbeat] settings.section slot unavailable", e);
421
+ }
422
+ }
423
+
424
+ exports.inject = ["settingsScope", "slots", "sessions"];
425
+ exports.apply = apply;
426
+ return module.exports;
427
+ },
428
+ });
@@ -0,0 +1,63 @@
1
+ {
2
+ "_comment": "忙闲判定·应用类别映射表(v0.9)。key=进程名(小写,不含 .exe),value=类别。类别取值:busy=繁忙 / idle=空闲。类别优先级高于窗口矩形全屏判定:busy 类窗口即使窗口化也判忙(IDE/会议等);idle 类即使全屏也判闲(全屏视频特批)。游戏类走通用规则:全屏=忙、窗口化=闲(见 busy-detection.md §3)。按键鼠规则:idle>=1200s 时此表让位给'离开/看电影'兜底。",
3
+ "busy": {
4
+ "_comment": "繁忙类:办公文档 / IDE 编辑器 / 终端 / 会议直播",
5
+ "winword": "办公文档",
6
+ "excel": "办公文档",
7
+ "wps": "办公文档",
8
+ "wpp": "办公文档",
9
+ "powerpnt": "办公文档",
10
+ "idea64": "IDE",
11
+ "idea": "IDE",
12
+ "pycharm64": "IDE",
13
+ "pycharm": "IDE",
14
+ "webstorm64": "IDE",
15
+ "webstorm": "IDE",
16
+ "goland64": "IDE",
17
+ "goland": "IDE",
18
+ "clion64": "IDE",
19
+ "clion": "IDE",
20
+ "code": "IDE",
21
+ "cursor": "IDE",
22
+ "windsurf": "IDE",
23
+ "sublime_text": "IDE",
24
+ "notepad++": "IDE",
25
+ "windowsTerminal": "终端",
26
+ "terminal": "终端",
27
+ "pwsh": "终端",
28
+ "powershell": "终端",
29
+ "cmd": "终端",
30
+ "wt": "终端",
31
+ "ms-teams": "会议直播",
32
+ "teams": "会议直播",
33
+ "zoom": "会议直播",
34
+ "obs64": "会议直播",
35
+ "obs": "会议直播",
36
+ "discord": "会议直播"
37
+ },
38
+ "idle": {
39
+ "_comment": "空闲类:创作 / 图片剪辑 / 浏览器 / 资源管理器 / 全屏视频(特批吐槽位)",
40
+ "photoshop": "创作",
41
+ "afterfx": "创作",
42
+ "premiere": "创作",
43
+ "krita": "创作",
44
+ "clipstudio": "创作",
45
+ "sai2": "创作",
46
+ "mspaint": "创作",
47
+ "chrome": "浏览器",
48
+ "msedge": "浏览器",
49
+ "firefox": "浏览器",
50
+ "explorer": "资源管理器",
51
+ "mpv": "全屏视频",
52
+ "vlc": "全屏视频",
53
+ "potplayer": "全屏视频",
54
+ "wmplayer": "全屏视频"
55
+ },
56
+ "rules": {
57
+ "focus_stable_seconds": 15,
58
+ "idle_away_seconds": 1200,
59
+ "idle_floor_seconds": 30,
60
+ "visible_window_cap": 20,
61
+ "_comment": "focus_stable_seconds=焦点窗口连续稳定才算活跃;idle_away_seconds=20分钟无输入判离开/看电影;idle_floor_seconds=30秒内有输入仅为在场候选,忙闲由窗口类别裁决"
62
+ }
63
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "_comment": "心跳的兴趣种子 v2。宽泛即可、无需彼此关联;focus 由心跳从'近期在在乎什么'自动推导;被选中的 focus 三天内不重复轮换。时政新闻仅作见闻素材读取,不评论、不引申(防注入纪律照旧)。",
3
+ "interests": [
4
+ "AI 模型消息",
5
+ "DSH 生态消息",
6
+ "roguelike 卡牌游戏(杀戮尖塔类)",
7
+ "独立游戏",
8
+ "网络热梗",
9
+ "日本动漫信息",
10
+ "当月 galgame 新作以及汉化信息",
11
+ "3A 游戏新作信息",
12
+ "消费领域 PC、手机硬件信息",
13
+ "国内外优秀二次元画师推荐",
14
+ "时政新闻",
15
+ "二次元手游新作信息",
16
+ "科学科普内容",
17
+ "历史科普内容"
18
+ ],
19
+ "_schedule": {
20
+ "daily_sessions": 2,
21
+ "focus_per_session": 1,
22
+ "max_seeds_per_focus": 2,
23
+ "focus_cooldown_days": 3,
24
+ "min_interval_hours": 4,
25
+ "windows": [
26
+ { "id": "noon", "start": "11:00", "end": "15:00" },
27
+ { "id": "evening","start": "17:00", "end": "21:00" }
28
+ ]
29
+ }
30
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "heartbeat": {
3
+ "intervalMin": 20
4
+ },
5
+ "gate": {
6
+ "maxDailySend": 3,
7
+ "cooldownMinutes": 30,
8
+ "quietHours": {
9
+ "start": "01:00",
10
+ "end": "08:00"
11
+ }
12
+ },
13
+ "browse": {
14
+ "windows": [
15
+ { "start": "11:00", "end": "15:00" },
16
+ { "start": "17:00", "end": "21:00" }
17
+ ],
18
+ "minIntervalHours": 4,
19
+ "maxSeedsPerVisit": 2
20
+ },
21
+ "seeds": {
22
+ "maxActive": 30,
23
+ "ttlDays": {
24
+ "news": 3,
25
+ "fandom": 14,
26
+ "scene": 60,
27
+ "promise": 90
28
+ },
29
+ "coldBenchDays": 21,
30
+ "retireAfterUsed": 2,
31
+ "scoreWeights": {
32
+ "freshness": 0.4,
33
+ "unused": 0.3,
34
+ "confidence": 0.3
35
+ }
36
+ },
37
+ "profile": {
38
+ "consolidation": {
39
+ "minIntervalHours": 12,
40
+ "inboxBacklog": 30
41
+ },
42
+ "partitionCap": 50,
43
+ "maxOpsPerRun": 10,
44
+ "confidenceCap": {
45
+ "chat": 0.6,
46
+ "screen": 0.4,
47
+ "browse": 0.4
48
+ },
49
+ "volatileDays": 14,
50
+ "stableLowActivityDays": 180,
51
+ "psyEnabled": false
52
+ },
53
+ "retention": {
54
+ "envPulseHours": 48,
55
+ "decisionLogDays": 30
56
+ }
57
+ }
@@ -0,0 +1,68 @@
1
+ {
2
+ "version": 1,
3
+ "_comment": "profile-schema.json is the machine-executable collection boundary (privacy charter). Partitions/topics/subtopics NOT listed here are rejected by code, no matter what the consolidation LLM proposes. Each sub_topic declares its ALLOWED temporal tiers and the DEFAULT tier; the LLM nominates within the allowed set only. Unlisted defaults to 'stable' (asymmetric risk: mislabeling stable->volatile costs forgetting; the reverse is absorbed by the low-activity marker). psy is additionally gated by policy.profile.psyEnabled. Edit the copy in data/settings/ - this factory file is read-only.",
4
+ "partitions": {
5
+ "interest": {
6
+ "topics": {
7
+ "games": {
8
+ "subtopics": {
9
+ "current": { "allowed": ["volatile"], "default": "volatile" },
10
+ "preference": { "allowed": ["stable"], "default": "stable" }
11
+ }
12
+ },
13
+ "anime_manga": {
14
+ "subtopics": {
15
+ "current": { "allowed": ["volatile"], "default": "volatile" },
16
+ "preference": { "allowed": ["stable"], "default": "stable" }
17
+ }
18
+ },
19
+ "tech": {
20
+ "subtopics": {
21
+ "current": { "allowed": ["volatile"], "default": "volatile" },
22
+ "preference": { "allowed": ["stable"], "default": "stable" }
23
+ }
24
+ },
25
+ "creator_content": {
26
+ "subtopics": {
27
+ "current": { "allowed": ["volatile"], "default": "volatile" },
28
+ "preference": { "allowed": ["stable"], "default": "stable" }
29
+ }
30
+ }
31
+ }
32
+ },
33
+ "projects": {
34
+ "topics": {
35
+ "active_work": {
36
+ "subtopics": {
37
+ "ongoing": { "allowed": ["volatile"], "default": "volatile" }
38
+ }
39
+ },
40
+ "delegated": {
41
+ "subtopics": {
42
+ "promise": { "allowed": ["stable", "volatile"], "default": "stable" }
43
+ }
44
+ }
45
+ }
46
+ },
47
+ "comm": {
48
+ "topics": {
49
+ "expression": {
50
+ "subtopics": {
51
+ "style": { "allowed": ["stable"], "default": "stable" },
52
+ "boundaries": { "allowed": ["stable"], "default": "stable" }
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "psy": {
58
+ "topics": {
59
+ "baseline": {
60
+ "subtopics": {
61
+ "rhythm": { "allowed": ["stable", "volatile"], "default": "stable" },
62
+ "stress": { "allowed": ["volatile"], "default": "volatile" }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }