pi-proactive 0.2.0-beta.2 → 0.2.0-beta.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/CHANGELOG.md +29 -0
- package/README.md +32 -28
- package/build/cli.mjs +121 -0
- package/build/extensions/proactive.js +47 -6
- package/build/src/commands.js +78 -2
- package/build/src/config.js +511 -4
- package/build/src/daemon-config.js +221 -0
- package/build/src/daemon-contact.js +103 -0
- package/build/src/daemon-host.js +84 -0
- package/build/src/daemon.js +193 -3
- package/build/src/health.js +84 -0
- package/build/src/lineage.js +41 -2
- package/build/src/notify.js +183 -0
- package/build/src/progress-eta.js +134 -0
- package/build/src/trigger.js +43 -0
- package/build/src/trust-loop.js +321 -0
- package/build/src/trust-metrics.js +236 -0
- package/build/src/trust-notify.js +73 -0
- package/build/src/trust-policy.js +58 -0
- package/build/src/trust-registry.js +323 -0
- package/daemon/launchd.plist +48 -0
- package/daemon/pi-proactive-daemon.service +29 -0
- package/daemon/run-daemon.sh +84 -0
- package/node_modules/typebox/build/compile/code.mjs +2 -1
- package/node_modules/typebox/build/compile/validator.mjs +1 -5
- package/node_modules/typebox/build/format/date_time.d.mts +1 -2
- package/node_modules/typebox/build/format/date_time.mjs +3 -12
- package/node_modules/typebox/build/format/duration.d.mts +2 -2
- package/node_modules/typebox/build/format/duration.mjs +2 -2
- package/node_modules/typebox/build/format/idn_email.d.mts +0 -1
- package/node_modules/typebox/build/format/idn_email.mjs +0 -1
- package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -1
- package/node_modules/typebox/build/format/idn_hostname.mjs +1 -1
- package/node_modules/typebox/build/format/ipv4.d.mts +2 -2
- package/node_modules/typebox/build/format/ipv4.mjs +2 -2
- package/node_modules/typebox/build/format/ipv6.d.mts +1 -1
- package/node_modules/typebox/build/format/ipv6.mjs +1 -1
- package/node_modules/typebox/build/format/iri.mjs +9 -16
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/guard/emit.mjs +2 -2
- package/node_modules/typebox/build/guard/guard.d.mts +5 -7
- package/node_modules/typebox/build/guard/guard.mjs +9 -14
- package/node_modules/typebox/build/guard/index.mjs +0 -6
- package/node_modules/typebox/build/guard/{unicode/unicode_segment.mjs → string.mjs} +19 -16
- package/node_modules/typebox/build/schema/build.mjs +5 -5
- package/node_modules/typebox/build/schema/check.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_context.d.mts +1 -5
- package/node_modules/typebox/build/schema/engine/_context.mjs +3 -16
- package/node_modules/typebox/build/schema/engine/_functions.d.mts +2 -2
- package/node_modules/typebox/build/schema/engine/_functions.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/_reducer.d.mts +3 -3
- package/node_modules/typebox/build/schema/engine/_refine.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/_refine.mjs +9 -14
- package/node_modules/typebox/build/schema/engine/_stack.d.mts +20 -34
- package/node_modules/typebox/build/schema/engine/_stack.mjs +202 -143
- package/node_modules/typebox/build/schema/engine/_unique.d.mts +2 -4
- package/node_modules/typebox/build/schema/engine/_unique.mjs +3 -7
- package/node_modules/typebox/build/schema/engine/additionalItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/additionalItems.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +8 -5
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +32 -27
- package/node_modules/typebox/build/schema/engine/allOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/allOf.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/anyOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/anyOf.mjs +9 -4
- package/node_modules/typebox/build/schema/engine/boolean.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/boolean.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/const.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/const.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/contains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/contains.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/dependencies.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependencies.mjs +7 -5
- package/node_modules/typebox/build/schema/engine/dependentRequired.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentRequired.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/dependentSchemas.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +1 -2
- package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +8 -12
- package/node_modules/typebox/build/schema/engine/enum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/enum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMaximum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/exclusiveMinimum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/format.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/format.mjs +8 -11
- package/node_modules/typebox/build/schema/engine/if.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/if.mjs +13 -5
- package/node_modules/typebox/build/schema/engine/index.d.mts +0 -1
- package/node_modules/typebox/build/schema/engine/index.mjs +0 -1
- package/node_modules/typebox/build/schema/engine/items.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/items.mjs +5 -8
- package/node_modules/typebox/build/schema/engine/maxContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +8 -4
- package/node_modules/typebox/build/schema/engine/maxItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxItems.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maxLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxLength.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maxProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maxProperties.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/maximum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/maximum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minContains.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minContains.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/minItems.d.mts +5 -5
- package/node_modules/typebox/build/schema/engine/minItems.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minLength.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minLength.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minProperties.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/minimum.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/minimum.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/multipleOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/multipleOf.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/not.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/not.mjs +8 -4
- package/node_modules/typebox/build/schema/engine/oneOf.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +11 -6
- package/node_modules/typebox/build/schema/engine/pattern.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/pattern.mjs +7 -3
- package/node_modules/typebox/build/schema/engine/patternProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +5 -6
- package/node_modules/typebox/build/schema/engine/prefixItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/prefixItems.mjs +1 -1
- package/node_modules/typebox/build/schema/engine/properties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/properties.mjs +3 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +10 -7
- package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +8 -12
- package/node_modules/typebox/build/schema/engine/ref.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/ref.mjs +14 -18
- package/node_modules/typebox/build/schema/engine/required.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/required.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/schema.d.mts +7 -7
- package/node_modules/typebox/build/schema/engine/schema.mjs +138 -136
- package/node_modules/typebox/build/schema/engine/type.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/type.mjs +6 -2
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +9 -5
- package/node_modules/typebox/build/schema/engine/uniqueItems.d.mts +4 -4
- package/node_modules/typebox/build/schema/engine/uniqueItems.mjs +6 -2
- package/node_modules/typebox/build/schema/errors.mjs +1 -1
- package/node_modules/typebox/build/schema/intern/intern.d.mts +9 -6
- package/node_modules/typebox/build/schema/intern/intern.mjs +56 -90
- package/node_modules/typebox/build/schema/parse.mjs +0 -3
- package/node_modules/typebox/build/schema/pointer/pointer.d.mts +6 -1
- package/node_modules/typebox/build/schema/pointer/pointer.mjs +64 -47
- package/node_modules/typebox/build/schema/resolve/resolve.d.mts +32 -6
- package/node_modules/typebox/build/schema/resolve/resolve.mjs +227 -231
- package/node_modules/typebox/build/schema/schema.d.mts +0 -1
- package/node_modules/typebox/build/schema/schema.mjs +0 -1
- package/node_modules/typebox/build/system/hashing/hash.mjs +34 -63
- package/node_modules/typebox/build/system/locale/en_US.mjs +1 -2
- package/node_modules/typebox/build/system/memory/assign.d.mts +8 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +6 -13
- package/node_modules/typebox/build/system/memory/clone.d.mts +5 -4
- package/node_modules/typebox/build/system/memory/clone.mjs +96 -8
- package/node_modules/typebox/build/system/memory/create.d.mts +5 -1
- package/node_modules/typebox/build/system/memory/create.mjs +13 -18
- package/node_modules/typebox/build/system/memory/discard.d.mts +1 -1
- package/node_modules/typebox/build/system/memory/discard.mjs +8 -9
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.d.mts +4 -1
- package/node_modules/typebox/build/system/memory/update.mjs +16 -43
- package/node_modules/typebox/build/system/settings/settings.d.mts +0 -11
- package/node_modules/typebox/build/system/settings/settings.mjs +0 -14
- package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +24 -2
- package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +29 -2
- package/node_modules/typebox/build/type/engine/evaluate/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/index.mjs +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +1 -5
- package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +1 -6
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/engine/object/from_intersect.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_intersect.mjs +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.mjs +2 -2
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.d.mts +3 -1
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.mjs +4 -1
- package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
- package/node_modules/typebox/build/type/extends/inference.mjs +2 -15
- package/node_modules/typebox/build/type/extends/tuple.d.mts +1 -2
- package/node_modules/typebox/build/type/extends/tuple.mjs +1 -21
- package/node_modules/typebox/build/value/clean/from_object.mjs +1 -1
- package/node_modules/typebox/build/value/clone/clone.d.mts +3 -4
- package/node_modules/typebox/build/value/clone/clone.mjs +7 -5
- package/node_modules/typebox/build/value/parse/parse.mjs +4 -10
- package/node_modules/typebox/build/value/shared/union_priority_sort.mjs +14 -25
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +84 -52
- package/package.json +11 -3
- package/skills/pi-proactive/SKILL.md +27 -24
- package/tools/cli.mjs +121 -0
- package/tools/daemon-install.sh +397 -0
- package/tools/daemon-uninstall.sh +226 -0
- package/node_modules/typebox/build/guard/unicode/unicode.d.mts +0 -6
- package/node_modules/typebox/build/guard/unicode/unicode.mjs +0 -35
- package/node_modules/typebox/build/schema/engine/_pathing.d.mts +0 -1
- package/node_modules/typebox/build/schema/engine/_pathing.mjs +0 -11
- package/node_modules/typebox/build/schema/meta/draft_2019_09.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_2019_09.mjs +0 -288
- package/node_modules/typebox/build/schema/meta/draft_2020_12.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_2020_12.mjs +0 -300
- package/node_modules/typebox/build/schema/meta/draft_3.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_3.mjs +0 -143
- package/node_modules/typebox/build/schema/meta/draft_4.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_4.mjs +0 -149
- package/node_modules/typebox/build/schema/meta/draft_6.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_6.mjs +0 -155
- package/node_modules/typebox/build/schema/meta/draft_7.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/draft_7.mjs +0 -172
- package/node_modules/typebox/build/schema/meta/index.d.mts +0 -2
- package/node_modules/typebox/build/schema/meta/index.mjs +0 -2
- package/node_modules/typebox/build/schema/meta/meta.d.mts +0 -9
- package/node_modules/typebox/build/schema/meta/meta.mjs +0 -15
- package/node_modules/typebox/build/schema/meta/schema.d.mts +0 -6
- package/node_modules/typebox/build/schema/meta/schema.mjs +0 -1
- package/node_modules/typebox/build/system/memory/deep.d.mts +0 -2
- package/node_modules/typebox/build/system/memory/deep.mjs +0 -108
- package/node_modules/typebox/build/system/memory/finalize.d.mts +0 -4
- package/node_modules/typebox/build/system/memory/finalize.mjs +0 -30
- package/node_modules/typebox/build/system/memory/keys.d.mts +0 -2
- package/node_modules/typebox/build/system/memory/keys.mjs +0 -8
- package/node_modules/typebox/build/system/settings/internal.d.mts +0 -2
- package/node_modules/typebox/build/system/settings/internal.mjs +0 -20
- package/node_modules/typebox/build/type/engine/call/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/call/index.mjs +0 -1
- package/node_modules/typebox/build/type/engine/evaluate/property.d.mts +0 -30
- package/node_modules/typebox/build/type/engine/evaluate/property.mjs +0 -29
- package/node_modules/typebox/build/type/engine/this/index.d.mts +0 -1
- package/node_modules/typebox/build/type/engine/this/index.mjs +0 -1
- package/node_modules/typebox/build/value/repair/from_cyclic.d.mts +0 -2
- package/node_modules/typebox/build/value/repair/from_cyclic.mjs +0 -6
- /package/node_modules/typebox/build/guard/{unicode/unicode_segment.d.mts → string.d.mts} +0 -0
package/build/src/daemon.js
CHANGED
|
@@ -25,12 +25,26 @@ import fs from "node:fs";
|
|
|
25
25
|
import net from "node:net";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import { fileURLToPath } from "node:url";
|
|
28
|
-
|
|
28
|
+
// G12 四件套(**主动联系**能力 —— 代码侧声明):
|
|
29
|
+
// @capability: daemon-contact
|
|
30
|
+
// @observable: pi-proactive/src/daemon.ts:writeHeartbeat
|
|
31
|
+
// @attribution: pi-proactive/src/daemon-contact.ts:decideContact
|
|
32
|
+
// @outcome: pi-proactive/src/daemon-config.ts:appendContactEntry
|
|
33
|
+
// @checked: test:pi-proactive/tests/daemon-contact.test.ts#目标判定
|
|
34
|
+
import { proactiveDir, resolveAgentDir, ensureDir, writeAtomicFile } from "./config.js";
|
|
29
35
|
import { Scheduler } from "./scheduler.js";
|
|
30
36
|
import { TaskStore } from "./task-store.js";
|
|
31
37
|
import { Lineage } from "./lineage.js";
|
|
32
|
-
import { buildHandoverSummary, classifyAction, dayKey, estimateTokenUnits, loadDaemonConfig, loadDaemonState, MAX_ACTION_LOG, saveDaemonState, writeHandoverSummary, } from "./daemon-config.js";
|
|
38
|
+
import { appendContactAudit, appendContactEntry, buildHandoverSummary, classifyAction, dayKey, estimateTokenUnits, heartbeatPath, loadDaemonConfig, loadDaemonState, MAX_ACTION_LOG, formatContactList, readContactEntries, saveDaemonState, writeHandoverSummary, } from "./daemon-config.js";
|
|
39
|
+
import { decideContact } from "./daemon-contact.js";
|
|
40
|
+
import { detectHost } from "./daemon-host.js";
|
|
41
|
+
import { buildContactPayload, selectNotifyChannels } from "./notify.js";
|
|
42
|
+
import { Registry } from "./registry.js";
|
|
43
|
+
import { createEnvelope } from "./envelope.js";
|
|
44
|
+
import { deliverCrossProcess } from "./socket.js";
|
|
33
45
|
const HOUR_MS = 3_600_000;
|
|
46
|
+
/** 心跳间隔(ms):外部验证「它还活着」的刷新周期。 */
|
|
47
|
+
const HEARTBEAT_INTERVAL_MS = 5_000;
|
|
34
48
|
/** 动作标题的最大长度(截断后的标题进流水与交班摘要)。 */
|
|
35
49
|
const MAX_TITLE_LEN = 60;
|
|
36
50
|
/** 创建干跑执行器。 */
|
|
@@ -71,9 +85,15 @@ export class Daemon {
|
|
|
71
85
|
scheduler;
|
|
72
86
|
state;
|
|
73
87
|
timer;
|
|
88
|
+
/** 心跳定时器(独立于 tick 周期 → 「它还活着」的证据始终新鲜)。 */
|
|
89
|
+
heartbeatTimer;
|
|
74
90
|
lastSweepAt = 0;
|
|
75
91
|
lastHandoverDay = "";
|
|
76
92
|
stopped = false;
|
|
93
|
+
/** 常驻宿主(herdr / launchd / systemd)—— 写进心跳,外部可查。 */
|
|
94
|
+
hostKind;
|
|
95
|
+
/** 设备通知通道(注入点;缺省 → 自动挑)。 */
|
|
96
|
+
notifyChannels;
|
|
77
97
|
constructor(opts) {
|
|
78
98
|
this.agentId = opts.config.agentId;
|
|
79
99
|
this.agentDir = opts.agentDir;
|
|
@@ -85,6 +105,8 @@ export class Daemon {
|
|
|
85
105
|
this.lineage =
|
|
86
106
|
opts.lineage ?? new Lineage({ agentDir: this.agentDir, agentId: this.agentId, now: this.now });
|
|
87
107
|
this.state = loadDaemonState(this.agentDir, this.now());
|
|
108
|
+
this.hostKind = opts.hostKind ?? detectHost().hostKind;
|
|
109
|
+
this.notifyChannels = opts.notifyChannels;
|
|
88
110
|
this.scheduler = new Scheduler({
|
|
89
111
|
store: this.store,
|
|
90
112
|
trigger: { deliverToModel: (msg, intent, o) => this.handleDelivery(msg, intent, o) },
|
|
@@ -112,6 +134,14 @@ export class Daemon {
|
|
|
112
134
|
this.timer = setInterval(() => {
|
|
113
135
|
void this.tick();
|
|
114
136
|
}, this.config.tickIntervalMs);
|
|
137
|
+
// 心跳独立于 tick(tick 可能 30 秒一次 → 外部验证「还活着」不该等那么久)。
|
|
138
|
+
this.heartbeatTimer = setInterval(() => {
|
|
139
|
+
this.writeHeartbeat();
|
|
140
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
141
|
+
this.heartbeatTimer.unref?.();
|
|
142
|
+
// 立刻写一次心跳:**监督器靠心跳判活**(等 5s 才有心跳 → 接管会慢 5s;更糟的是
|
|
143
|
+
// 两个监督器可能同时判定「没有心跳」→ 双开值班进程)。
|
|
144
|
+
this.writeHeartbeat();
|
|
115
145
|
this.log(`值班循环已启动(agent=${this.agentId} · tick=${this.config.tickIntervalMs}ms · enabled=${this.config.enabled} · dryRun=${this.config.dryRun})`);
|
|
116
146
|
}
|
|
117
147
|
/** 停值班循环(幂等):停循环 → 写交班摘要 → 落盘。 */
|
|
@@ -120,6 +150,10 @@ export class Daemon {
|
|
|
120
150
|
clearInterval(this.timer);
|
|
121
151
|
this.timer = undefined;
|
|
122
152
|
}
|
|
153
|
+
if (this.heartbeatTimer !== undefined) {
|
|
154
|
+
clearInterval(this.heartbeatTimer);
|
|
155
|
+
this.heartbeatTimer = undefined;
|
|
156
|
+
}
|
|
123
157
|
this.scheduler.stop();
|
|
124
158
|
if (this.stopped)
|
|
125
159
|
return;
|
|
@@ -145,6 +179,148 @@ export class Daemon {
|
|
|
145
179
|
catch (err) {
|
|
146
180
|
this.log(`维护失败(已隔离):${errText(err)}`);
|
|
147
181
|
}
|
|
182
|
+
// 心跳(**外部可验证「它还活着」**:文件里的 ts 随 tick 前进)。
|
|
183
|
+
this.writeHeartbeat(now);
|
|
184
|
+
// 主动联系(7×24 助理的硬需求:它**主动找我**)。
|
|
185
|
+
try {
|
|
186
|
+
await this.maybeContact(now);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
this.log(`主动联系失败(已隔离):${errText(err)}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 写心跳文件(`<agentDir>/proactive/daemon/heartbeat.json`)。
|
|
194
|
+
*
|
|
195
|
+
* 为什么单独一个文件:安装脚本要**外部可验证**「它真的起来了」——
|
|
196
|
+
* 进程在 + 心跳在更新 + 单元已加载,三条证据缺一不可。
|
|
197
|
+
*/
|
|
198
|
+
writeHeartbeat(now = this.now()) {
|
|
199
|
+
const file = heartbeatPath(this.agentDir);
|
|
200
|
+
const body = {
|
|
201
|
+
ts: now,
|
|
202
|
+
pid: process.pid,
|
|
203
|
+
agentId: this.agentId,
|
|
204
|
+
host: this.hostKind,
|
|
205
|
+
runMode: this.config.dryRun ? "dry-run" : "live",
|
|
206
|
+
enabled: this.config.enabled,
|
|
207
|
+
contactEnabled: this.config.contact.enabled,
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
ensureDir(path.dirname(file));
|
|
211
|
+
writeAtomicFile(file, `${JSON.stringify(body, null, 2)}\n`, { durable: true });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// 心跳写失败不影响值班(它只是可观测证据,不是业务数据)
|
|
215
|
+
}
|
|
216
|
+
return file;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* 主动联系(**它主动找我**)。
|
|
220
|
+
*
|
|
221
|
+
* 顺序固定:总开关 → 预算硬上限 → 决策(目标 / 频次 / 每日上限)→ 会话投递 →
|
|
222
|
+
* 设备级通知(去重策略)→ 记账 → 落清单。
|
|
223
|
+
*
|
|
224
|
+
* 不变量:**只联系已登记会话**;找不到目标 → **不联系**(只留可查记录)。
|
|
225
|
+
*/
|
|
226
|
+
async maybeContact(now = this.now()) {
|
|
227
|
+
if (!this.config.enabled)
|
|
228
|
+
return;
|
|
229
|
+
const cfg = this.config.contact;
|
|
230
|
+
if (!cfg.enabled)
|
|
231
|
+
return;
|
|
232
|
+
const budget = this.budgetStatus(now);
|
|
233
|
+
if (budget.exhausted) {
|
|
234
|
+
this.log(`主动联系被预算挡住:${budget.reason}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const registry = new Registry({ agentDir: this.agentDir, now: this.now });
|
|
238
|
+
const decision = decideContact({
|
|
239
|
+
config: cfg,
|
|
240
|
+
entries: registry.listAll(),
|
|
241
|
+
recent: readContactEntries(this.agentDir),
|
|
242
|
+
now,
|
|
243
|
+
selfAgentId: this.agentId,
|
|
244
|
+
});
|
|
245
|
+
if (decision.action !== "contact" || decision.target === undefined) {
|
|
246
|
+
// **可查记录**:不联系也要留下「为什么没联系」(用户能查)。
|
|
247
|
+
appendContactAudit(this.agentDir, { ts: now, reason: decision.reason });
|
|
248
|
+
this.log(`主动联系跳过:${decision.reason}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const target = decision.target;
|
|
252
|
+
const eventId = decision.eventId ?? `c-${now}-${target.agentId}`;
|
|
253
|
+
const payload = buildContactPayload({ ...cfg.payload, why: this.contactWhy() });
|
|
254
|
+
const channels = [];
|
|
255
|
+
// ① 会话投递(**只投给已登记会话**)。
|
|
256
|
+
let sessionStatus = "failed";
|
|
257
|
+
try {
|
|
258
|
+
const envelope = createEnvelope({
|
|
259
|
+
from: this.agentId,
|
|
260
|
+
to: target.agentId,
|
|
261
|
+
kind: "notify",
|
|
262
|
+
intent: "notify",
|
|
263
|
+
body: payload.body ?? "",
|
|
264
|
+
subject: payload.title,
|
|
265
|
+
});
|
|
266
|
+
const ack = await deliverCrossProcess({
|
|
267
|
+
targetAgentId: target.agentId,
|
|
268
|
+
envelope,
|
|
269
|
+
agentDir: this.agentDir,
|
|
270
|
+
});
|
|
271
|
+
sessionStatus = ack.status;
|
|
272
|
+
if (ack.status === "accepted" || ack.status === "duplicate")
|
|
273
|
+
channels.push("session");
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
sessionStatus = `error: ${errText(err)}`;
|
|
277
|
+
}
|
|
278
|
+
// ② 设备级通知(`fallback` = 会话没送达才发 → **双通道去重**,用户不被打扰两次)。
|
|
279
|
+
if (cfg.deviceNotify === "always" || !channels.includes("session")) {
|
|
280
|
+
const deviceChannels = this.notifyChannels ?? selectNotifyChannels({ channels: ["herdr", "osx", "linux"] });
|
|
281
|
+
for (const channel of deviceChannels) {
|
|
282
|
+
const res = await channel.send(payload);
|
|
283
|
+
if (res.ok) {
|
|
284
|
+
channels.push("device");
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// ③ 记账(**联系也算一次动作** → 预算硬上限不被绕过)。
|
|
290
|
+
this.countAction("limited", payload.body ?? "", channels.includes("session") ? "delivered" : "notified");
|
|
291
|
+
this.pushAction({
|
|
292
|
+
ts: now,
|
|
293
|
+
title: `主动联系 ${target.agentId}`,
|
|
294
|
+
tier: "limited",
|
|
295
|
+
outcome: channels.length > 0 ? "executed" : "failed",
|
|
296
|
+
reason: `${decision.reason} · 通道=${channels.join("+") || "无"} · 会话投递=${sessionStatus}`,
|
|
297
|
+
tokenUnits: 0,
|
|
298
|
+
});
|
|
299
|
+
// ④ 落清单(**同一事件只一条**:两条通道用同一个 eventId)。
|
|
300
|
+
appendContactEntry(this.agentDir, {
|
|
301
|
+
eventId,
|
|
302
|
+
ts: now,
|
|
303
|
+
outcome: channels.includes("session")
|
|
304
|
+
? channels.includes("device")
|
|
305
|
+
? "both"
|
|
306
|
+
: "session"
|
|
307
|
+
: channels.includes("device")
|
|
308
|
+
? "device"
|
|
309
|
+
: "failed",
|
|
310
|
+
channels,
|
|
311
|
+
targetAgentId: target.agentId,
|
|
312
|
+
payload,
|
|
313
|
+
reason: `会话投递=${sessionStatus}`,
|
|
314
|
+
});
|
|
315
|
+
this.saveState();
|
|
316
|
+
this.log(`已主动联系 ${target.agentId}(通道:${channels.join("+") || "无"})`);
|
|
317
|
+
}
|
|
318
|
+
/** 「为什么找你」的现实理由(有排队动作就说实话;否则用配置里的模板)。 */
|
|
319
|
+
contactWhy() {
|
|
320
|
+
const queued = this.state.queue.length;
|
|
321
|
+
if (queued > 0)
|
|
322
|
+
return `有 ${queued} 个动作在等你确认`;
|
|
323
|
+
return this.config.contact.payload.why;
|
|
148
324
|
}
|
|
149
325
|
/**
|
|
150
326
|
* 崩溃恢复(**幂等**):死 socket、过期任务认领、半成品执行、临时文件。
|
|
@@ -657,6 +833,8 @@ if (isMainModule()) {
|
|
|
657
833
|
let once = false;
|
|
658
834
|
let handoverOnly = false;
|
|
659
835
|
let enable = false;
|
|
836
|
+
let contactOnce = false;
|
|
837
|
+
let listContacts = false;
|
|
660
838
|
for (let i = 0; i < argv.length; i++) {
|
|
661
839
|
const arg = argv[i];
|
|
662
840
|
if (arg === "--agent-dir") {
|
|
@@ -669,6 +847,10 @@ if (isMainModule()) {
|
|
|
669
847
|
handoverOnly = true;
|
|
670
848
|
else if (arg === "--enable")
|
|
671
849
|
enable = true;
|
|
850
|
+
else if (arg === "--contact-once")
|
|
851
|
+
contactOnce = true;
|
|
852
|
+
else if (arg === "--list-contacts")
|
|
853
|
+
listContacts = true;
|
|
672
854
|
}
|
|
673
855
|
if (!agentDir)
|
|
674
856
|
agentDir = resolveAgentDir();
|
|
@@ -684,7 +866,15 @@ if (isMainModule()) {
|
|
|
684
866
|
config,
|
|
685
867
|
executor: createDryRunExecutor(),
|
|
686
868
|
});
|
|
687
|
-
if (
|
|
869
|
+
if (listContacts) {
|
|
870
|
+
console.log(formatContactList(readContactEntries(agentDir)));
|
|
871
|
+
}
|
|
872
|
+
else if (contactOnce) {
|
|
873
|
+
// 主动联系一次(**手动触发**;决策与投递路径与值班循环完全一致)。
|
|
874
|
+
await daemon.maybeContact();
|
|
875
|
+
console.log(`联系记录:\n${formatContactList(readContactEntries(agentDir))}`);
|
|
876
|
+
}
|
|
877
|
+
else if (handoverOnly) {
|
|
688
878
|
console.log(daemon.writeHandover());
|
|
689
879
|
}
|
|
690
880
|
else if (once) {
|
package/build/src/health.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { defaultConfig, proactiveDir } from "./config.js";
|
|
7
|
+
import { ContentStore } from "./content.js";
|
|
7
8
|
import { OFFLINE_ENTRY_MIN_RETENTION_MS, OFFLINE_HEARTBEAT_FACTOR, } from "./registry.js";
|
|
8
9
|
import { socketPathFor } from "./socket.js";
|
|
9
10
|
import { readLatestHandover } from "./daemon-config.js";
|
|
@@ -338,6 +339,68 @@ function readConfig(configFile) {
|
|
|
338
339
|
}
|
|
339
340
|
return sample;
|
|
340
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* 外置内容引用采样(只读)—— CFG1:`promptRef` / `agentRef` 的引用数与悬空数。
|
|
344
|
+
*
|
|
345
|
+
* 口径与**运行期同一把尺子**:复用 `content.ts` 的 `ContentStore.resolveEntry()`
|
|
346
|
+
* → 悬空 / 空文件 / 格式错 / 越界路径的判定与 `loadConfig()` 完全一致(不在这里重写一套规则)。
|
|
347
|
+
*
|
|
348
|
+
* 「内联优先」(规范 §4):同层仍有内联 `prompt` 时,`promptRef` **不生效** →
|
|
349
|
+
* 运行期不会因它失败 → 计入 `shadowed`(不计入悬空),避免体检报一个假故障。
|
|
350
|
+
*/
|
|
351
|
+
function readExternalContent(root) {
|
|
352
|
+
const out = { refs: 0, dangling: 0, shadowed: 0, issues: [] };
|
|
353
|
+
const raw = readJsonSafe(path.join(root, "config.json"));
|
|
354
|
+
if (!isPlainObject(raw))
|
|
355
|
+
return out;
|
|
356
|
+
const refs = [];
|
|
357
|
+
const idleTrigger = isPlainObject(raw.idleTrigger) ? raw.idleTrigger : {};
|
|
358
|
+
if (typeof idleTrigger.promptRef === "string" && idleTrigger.promptRef.trim() !== "") {
|
|
359
|
+
const inline = typeof idleTrigger.prompt === "string" ? idleTrigger.prompt : "";
|
|
360
|
+
refs.push({
|
|
361
|
+
category: "prompts",
|
|
362
|
+
ref: idleTrigger.promptRef.trim(),
|
|
363
|
+
where: "idleTrigger.promptRef",
|
|
364
|
+
shadowed: inline !== "",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const contentRefs = isPlainObject(raw.contentRefs) ? raw.contentRefs : {};
|
|
368
|
+
for (const [name, value] of Object.entries(contentRefs)) {
|
|
369
|
+
if (!isPlainObject(value))
|
|
370
|
+
continue;
|
|
371
|
+
if (typeof value.promptRef === "string" && value.promptRef.trim() !== "") {
|
|
372
|
+
refs.push({
|
|
373
|
+
category: "prompts",
|
|
374
|
+
ref: value.promptRef.trim(),
|
|
375
|
+
where: `contentRefs.${name}.promptRef`,
|
|
376
|
+
shadowed: false,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (typeof value.agentRef === "string" && value.agentRef.trim() !== "") {
|
|
380
|
+
refs.push({
|
|
381
|
+
category: "agents",
|
|
382
|
+
ref: value.agentRef.trim(),
|
|
383
|
+
where: `contentRefs.${name}.agentRef`,
|
|
384
|
+
shadowed: false,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const item of refs) {
|
|
389
|
+
out.refs += 1;
|
|
390
|
+
if (item.shadowed) {
|
|
391
|
+
out.shadowed += 1;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
new ContentStore(root, item.category).resolveEntry(item.ref, item.category);
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
out.dangling += 1;
|
|
399
|
+
out.issues.push(`${item.where}=${item.ref} → ${e instanceof Error ? e.message : String(e)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
341
404
|
// ---------------------------------------------------------------- 主入口
|
|
342
405
|
/**
|
|
343
406
|
* 采集插件面健康快照(**纯读**)。
|
|
@@ -372,6 +435,7 @@ export function collectHealth(agentDir, opts) {
|
|
|
372
435
|
const events = installed ? readEvents(path.join(root, "lineage"), now) : { exists: false, lastEventAgeMs: null, streams: 0, deliveredThisHour: 0, lastDeliveredAt: null };
|
|
373
436
|
const tasks = installed ? readTasks(path.join(root, "tasks.json")) : { count: 0, totalFireCount: 0 };
|
|
374
437
|
const config = readConfig(path.join(root, "config.json"));
|
|
438
|
+
const external = readExternalContent(root);
|
|
375
439
|
const handoverFile = readLatestHandover(agentDir);
|
|
376
440
|
const handover = {
|
|
377
441
|
exists: handoverFile !== undefined,
|
|
@@ -423,6 +487,7 @@ export function collectHealth(agentDir, opts) {
|
|
|
423
487
|
maxPerHour: config.maxPerHour,
|
|
424
488
|
issues: config.issues,
|
|
425
489
|
},
|
|
490
|
+
external,
|
|
426
491
|
handover,
|
|
427
492
|
findings: [],
|
|
428
493
|
overall: "ok",
|
|
@@ -607,6 +672,25 @@ function buildFindings(report, ctx) {
|
|
|
607
672
|
: "无需处置。",
|
|
608
673
|
alertKind: cfg.issues.length > 0 ? "CONFIG_INVALID" : undefined,
|
|
609
674
|
});
|
|
675
|
+
// ---- 外置内容引用(CFG1)
|
|
676
|
+
const ext = report.external;
|
|
677
|
+
out.push({
|
|
678
|
+
id: "external.content",
|
|
679
|
+
label: "外置内容引用",
|
|
680
|
+
status: ext.dangling > 0 ? "fail" : "ok",
|
|
681
|
+
value: `引用 ${ext.refs} 个` +
|
|
682
|
+
(ext.shadowed > 0 ? `(其中 ${ext.shadowed} 个被内联值覆盖、未生效)` : "") +
|
|
683
|
+
` · 悬空 ${ext.dangling} 个` +
|
|
684
|
+
(ext.issues.length > 0 ? `:${ext.issues.join(";")}` : ""),
|
|
685
|
+
criterion: "配置里的每个 promptRef / agentRef 都必须能解析到 <配置目录>/prompts|agents/<ref>.md" +
|
|
686
|
+
"(悬空 / 空文件 / 格式错 / 越界路径 均判红)",
|
|
687
|
+
thresholdSource: "实现口径:src/content.ts 的 ContentStore.resolveEntry()(CFG1 规范 §9 冻结的契约;失败语义 = 抛错并指名 ref 与路径)",
|
|
688
|
+
advice: ext.dangling > 0
|
|
689
|
+
? "改 <agent-dir>/proactive/config.json 里的引用,或补上对应的 prompts/<ref>.md / agents/<ref>.md;" +
|
|
690
|
+
"不得靠静默回退默认值掩盖(规范 §8)。"
|
|
691
|
+
: "无需处置。",
|
|
692
|
+
alertKind: ext.dangling > 0 ? "EXTERNAL_CONTENT_BROKEN" : undefined,
|
|
693
|
+
});
|
|
610
694
|
return out;
|
|
611
695
|
}
|
|
612
696
|
/** 毫秒 → 人可读时长。 */
|
package/build/src/lineage.js
CHANGED
|
@@ -42,10 +42,10 @@ function isLineageEvent(value) {
|
|
|
42
42
|
return false;
|
|
43
43
|
if (typeof e.type !== "string")
|
|
44
44
|
return false;
|
|
45
|
-
if (e.type !== "triggered" && e.type !== "delivered" && e.type !== "answered" && e.type !== "outcome" && e.type !== "dropped" && e.type !== "decision") {
|
|
45
|
+
if (e.type !== "triggered" && e.type !== "delivered" && e.type !== "answered" && e.type !== "outcome" && e.type !== "dropped" && e.type !== "decision" && e.type !== "trust") {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
|
-
if (e.cause !== "idle" && e.cause !== "schedule" && e.cause !== "peer-request" && e.cause !== "manual")
|
|
48
|
+
if (e.cause !== "idle" && e.cause !== "schedule" && e.cause !== "peer-request" && e.cause !== "manual" && e.cause !== "trust")
|
|
49
49
|
return false;
|
|
50
50
|
return true;
|
|
51
51
|
}
|
|
@@ -212,6 +212,29 @@ export class Lineage {
|
|
|
212
212
|
decision: info.decision,
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* 记一次**信任层事件**(托付四段 / 目标归因)。
|
|
217
|
+
*
|
|
218
|
+
* 落什么:阶段 + 托付/目标 id + 归因步数 + 通道 + 守候时长 + 原因。
|
|
219
|
+
* 为什么落在事件流里(而不是另一个文件):托付闭环率、回应率、守候时长
|
|
220
|
+
* **必须能靠账本复算**(`src/trust-metrics.ts` 只读事件流,不读登记册)→
|
|
221
|
+
* 单一事件流 = 单一真源。
|
|
222
|
+
*/
|
|
223
|
+
noteTrust(info) {
|
|
224
|
+
return this.append({
|
|
225
|
+
type: "trust",
|
|
226
|
+
cause: "trust",
|
|
227
|
+
trust: {
|
|
228
|
+
stage: info.stage,
|
|
229
|
+
...(info.entrustmentId !== undefined ? { entrustmentId: info.entrustmentId } : {}),
|
|
230
|
+
...(info.goalId !== undefined ? { goalId: info.goalId } : {}),
|
|
231
|
+
...(info.progressDelta !== undefined ? { progressDelta: info.progressDelta } : {}),
|
|
232
|
+
...(info.channel !== undefined ? { channel: info.channel } : {}),
|
|
233
|
+
...(info.waitedMs !== undefined ? { waitedMs: info.waitedMs } : {}),
|
|
234
|
+
...(info.reason !== undefined ? { reason: info.reason } : {}),
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
215
238
|
/**
|
|
216
239
|
* 记一次「回应」(窗口内的事实)。
|
|
217
240
|
*
|
|
@@ -420,6 +443,22 @@ export function formatLineageHistory(events) {
|
|
|
420
443
|
if (e.durationMs !== undefined)
|
|
421
444
|
detail.push(`耗时=${formatShortDuration(e.durationMs)}`);
|
|
422
445
|
}
|
|
446
|
+
if (e.type === "trust" && e.trust !== undefined) {
|
|
447
|
+
const t = e.trust;
|
|
448
|
+
detail.push(`阶段=${t.stage}`);
|
|
449
|
+
if (t.entrustmentId !== undefined)
|
|
450
|
+
detail.push(`托付=${t.entrustmentId.slice(0, 8)}`);
|
|
451
|
+
if (t.goalId !== undefined)
|
|
452
|
+
detail.push(`目标=${t.goalId.slice(0, 8)}`);
|
|
453
|
+
if (t.progressDelta !== undefined)
|
|
454
|
+
detail.push(`推进=${t.progressDelta}`);
|
|
455
|
+
if (t.channel !== undefined)
|
|
456
|
+
detail.push(`通道=${t.channel}`);
|
|
457
|
+
if (t.waitedMs !== undefined)
|
|
458
|
+
detail.push(`守候=${formatShortDuration(t.waitedMs)}`);
|
|
459
|
+
if (t.reason !== undefined)
|
|
460
|
+
detail.push(`原因=${t.reason}`);
|
|
461
|
+
}
|
|
423
462
|
lines.push(`${formatEventTime(e.ts)} ${e.type.padEnd(9)} ${e.cause.padEnd(12)} ${ids.join(" ")} ${detail.join(" ")}`.trimEnd());
|
|
424
463
|
}
|
|
425
464
|
return lines.join("\n");
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备级通知(形态 A · N1)与「会话内联系」**共用同一份载荷**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要单独一个文件(而不是塞进 daemon.ts):
|
|
5
|
+
* - 载荷只构造一次 → 会话投递与设备通知**措辞必然一致**(两份各写一遍必然漂移);
|
|
6
|
+
* - 通道**可插拔**(接口 + 三个实现)→ 加新通道(Slack/邮件/托盘)不改调用方;
|
|
7
|
+
* - 命令执行器**可注入** → 测试零真实通知(不在开发者机器上弹窗)。
|
|
8
|
+
*
|
|
9
|
+
* 载荷四段(用户只面对三件事里的「它会做什么 / 怎么回应」):
|
|
10
|
+
* ① 为什么找你 ② 需要你做什么 ③ 不做的后果 ④ 怎么关掉我
|
|
11
|
+
*
|
|
12
|
+
* 纪律:**不造新的动作语义** —— 这里只搬运既有语义(通知 + 设备级提示),
|
|
13
|
+
* 不新增「主动行为」(主动能力登记见 `src/daemon.ts` 的四件套声明)。
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { delimiter } from "node:path";
|
|
18
|
+
/** 通知标题的缺省文案。 */
|
|
19
|
+
export const DEFAULT_CONTACT_TITLE = "助理找你有事";
|
|
20
|
+
/**
|
|
21
|
+
* 构造载荷(**唯一**入口)。
|
|
22
|
+
*
|
|
23
|
+
* 正文格式固定为四行(顺序固定)——人读得懂,机器也能按行取。
|
|
24
|
+
*/
|
|
25
|
+
export function buildContactPayload(input) {
|
|
26
|
+
const title = input.title ?? DEFAULT_CONTACT_TITLE;
|
|
27
|
+
return {
|
|
28
|
+
why: input.why,
|
|
29
|
+
ask: input.ask,
|
|
30
|
+
consequence: input.consequence,
|
|
31
|
+
howToDisable: input.howToDisable,
|
|
32
|
+
title,
|
|
33
|
+
body: contactBody(input),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** 载荷正文(四行;**顺序固定**)—— 会话投递与设备通道都用它 → 措辞不可能漂移。 */
|
|
37
|
+
export function contactBody(input) {
|
|
38
|
+
return [
|
|
39
|
+
`为什么找你:${input.why}`,
|
|
40
|
+
`需要你做什么:${input.ask}`,
|
|
41
|
+
`不做的后果:${input.consequence}`,
|
|
42
|
+
`怎么关掉我:${input.howToDisable}`,
|
|
43
|
+
].join("\n");
|
|
44
|
+
}
|
|
45
|
+
/** 默认执行器:同步执行、5 秒超时、继承环境但**不继承 stdin**。 */
|
|
46
|
+
export const defaultExec = (cmd, args) => {
|
|
47
|
+
const res = spawnSync(cmd, args, { timeout: 5000, stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
const stderr = res.stderr ? res.stderr.toString("utf8").slice(0, 300) : "";
|
|
49
|
+
return {
|
|
50
|
+
ok: res.status === 0 && !res.error,
|
|
51
|
+
code: res.status ?? 1,
|
|
52
|
+
...(stderr !== "" ? { stderr } : {}),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/** macOS 通知中心(`osascript display notification`)。 */
|
|
56
|
+
export function createMacChannel(opts = {}) {
|
|
57
|
+
const exec = opts.exec ?? defaultExec;
|
|
58
|
+
const available = opts.available ?? (() => process.platform === "darwin" && commandExists("osascript"));
|
|
59
|
+
return {
|
|
60
|
+
id: "osx",
|
|
61
|
+
isAvailable: available,
|
|
62
|
+
async send(payload) {
|
|
63
|
+
if (!available())
|
|
64
|
+
return { ok: false, detail: "osascript 不可用" };
|
|
65
|
+
// 参数化传递(正文含换行/引号 → 绝不能拼进 AppleScript 源码)。
|
|
66
|
+
const res = exec("osascript", [
|
|
67
|
+
"-e",
|
|
68
|
+
"on run argv",
|
|
69
|
+
"-e",
|
|
70
|
+
"display notification (item 1 of argv) with title (item 2 of argv)",
|
|
71
|
+
"-e",
|
|
72
|
+
"end run",
|
|
73
|
+
payload.body ?? contactBody(payload),
|
|
74
|
+
payload.title ?? DEFAULT_CONTACT_TITLE,
|
|
75
|
+
]);
|
|
76
|
+
return res.ok ? { ok: true } : { ok: false, detail: res.stderr ?? `osascript 退出码 ${res.code}` };
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Linux 桌面通知(`notify-send`)。 */
|
|
81
|
+
export function createLinuxChannel(opts = {}) {
|
|
82
|
+
const exec = opts.exec ?? defaultExec;
|
|
83
|
+
const available = opts.available ?? (() => process.platform === "linux" && commandExists("notify-send"));
|
|
84
|
+
return {
|
|
85
|
+
id: "linux",
|
|
86
|
+
isAvailable: available,
|
|
87
|
+
async send(payload) {
|
|
88
|
+
if (!available())
|
|
89
|
+
return { ok: false, detail: "notify-send 不可用" };
|
|
90
|
+
const res = exec("notify-send", [payload.title ?? DEFAULT_CONTACT_TITLE, payload.body ?? contactBody(payload)]);
|
|
91
|
+
return res.ok ? { ok: true } : { ok: false, detail: res.stderr ?? `notify-send 退出码 ${res.code}` };
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** herdr 通知(`herdr notification show`)—— herdr 宿主下的现成通道。 */
|
|
96
|
+
export function createHerdrChannel(opts = {}) {
|
|
97
|
+
const exec = opts.exec ?? defaultExec;
|
|
98
|
+
const available = opts.available ?? (() => commandExists("herdr"));
|
|
99
|
+
return {
|
|
100
|
+
id: "herdr",
|
|
101
|
+
isAvailable: available,
|
|
102
|
+
async send(payload) {
|
|
103
|
+
if (!available())
|
|
104
|
+
return { ok: false, detail: "herdr 不可用" };
|
|
105
|
+
const res = exec("herdr", [
|
|
106
|
+
"notification",
|
|
107
|
+
"show",
|
|
108
|
+
payload.title ?? DEFAULT_CONTACT_TITLE,
|
|
109
|
+
"--body",
|
|
110
|
+
payload.body ?? contactBody(payload),
|
|
111
|
+
]);
|
|
112
|
+
return res.ok ? { ok: true } : { ok: false, detail: res.stderr ?? `herdr 退出码 ${res.code}` };
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 空通道(永远不可用 → 选通道时必被跳过;用于「本机没有设备通知」的情形)。 */
|
|
117
|
+
export function createNullChannel() {
|
|
118
|
+
return {
|
|
119
|
+
id: "none",
|
|
120
|
+
isAvailable: () => false,
|
|
121
|
+
async send() {
|
|
122
|
+
return { ok: false, detail: "无设备通知通道" };
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** 内置工厂表。 */
|
|
127
|
+
export function defaultChannelFactories() {
|
|
128
|
+
return {
|
|
129
|
+
herdr: () => createHerdrChannel(),
|
|
130
|
+
osx: () => createMacChannel(),
|
|
131
|
+
linux: () => createLinuxChannel(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 按可用性挑选通道(**不可用的自动跳过** → 回落是自动的)。
|
|
136
|
+
*
|
|
137
|
+
* 返回顺序 = 入参顺序(调用方按顺序发 → 第一个成功的即可停)。
|
|
138
|
+
*/
|
|
139
|
+
export function selectNotifyChannels(opts) {
|
|
140
|
+
const factories = opts.factories ?? defaultChannelFactories();
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const id of opts.channels) {
|
|
143
|
+
const make = factories[id];
|
|
144
|
+
if (make === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
let ch;
|
|
147
|
+
try {
|
|
148
|
+
ch = make();
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (!ch.isAvailable())
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
out.push(ch);
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
/** 命令是否存在(**只看 PATH**,不执行)。 */
|
|
165
|
+
function commandExists(cmd) {
|
|
166
|
+
const pathEnv = process.env["PATH"] ?? "";
|
|
167
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
168
|
+
if (dir === "")
|
|
169
|
+
continue;
|
|
170
|
+
try {
|
|
171
|
+
if (existsSync(joinPath(dir, cmd)))
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// 忽略(权限等异常 → 当作不可用)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/** 拼路径(避免为一个函数引入 path 模块 → 保持本文件零副作用)。 */
|
|
181
|
+
function joinPath(dir, cmd) {
|
|
182
|
+
return dir.endsWith("/") ? `${dir}${cmd}` : `${dir}/${cmd}`;
|
|
183
|
+
}
|