@nettee/beacon 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -6
- package/dist/config/channels.js +65 -0
- package/dist/config/global.js +1 -0
- package/dist/config/profile.js +49 -19
- package/dist/config/registry.js +5 -2
- package/dist/dashboard/server.js +7 -1
- package/dist/doctor.js +3 -1
- package/dist/manual-trigger.js +3 -1
- package/dist/outcome/doctor.js +7 -1
- package/dist/schedule-trigger.js +3 -1
- package/dist/service.js +3 -1
- package/examples/channels.yaml.example +7 -0
- package/examples/profiles/example/profile.yaml +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,11 +109,47 @@ created by Beacon 0.1.2 and earlier; when omitted it defaults to `sessions/`
|
|
|
109
109
|
beside `config.yaml`. When configured, it must be absolute. Beacon creates
|
|
110
110
|
per-Run directories with mode `0700` when Pi starts.
|
|
111
111
|
|
|
112
|
-
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, escaped Profile markdown, permissive secret or runtime-environment permissions,
|
|
112
|
+
Configuration is strict: unknown YAML/JSON fields, YAML aliases or warnings, missing paths, duplicate Schedule IDs, invalid timezones/cron expressions, a missing complete `persona.md` + `task.md` pair, escaped Profile markdown, permissive secret or runtime-environment permissions, missing Profile credentials, and unknown Feishu channel names all fail startup. Beacon validates all Profiles before opening a Feishu connection.
|
|
113
|
+
|
|
114
|
+
Group notify destinations are registered globally in `~/.beacon/channels.yaml`
|
|
115
|
+
(beside `config.yaml`), then referenced by semantic `name` from each Profile's
|
|
116
|
+
`notify`. Admin DMs are **not** channel entries: each Profile keeps a raw
|
|
117
|
+
`admin.chat_id` (one bot×person private chat; putting those in the registry
|
|
118
|
+
would explode as bots and people grow).
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# ~/.beacon/channels.yaml — group chats only
|
|
122
|
+
version: 1
|
|
123
|
+
channels:
|
|
124
|
+
- name: weekday-brief-group
|
|
125
|
+
description: Example weekday brief notify group
|
|
126
|
+
chat_id: REPLACE_WITH_GROUP_CHAT_ID
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
# profile.yaml destinations
|
|
131
|
+
admin:
|
|
132
|
+
chat_id: REPLACE_WITH_ADMIN_DIRECT_CHAT_ID
|
|
133
|
+
schedules:
|
|
134
|
+
- id: weekday-brief
|
|
135
|
+
cron: "0 9 * * 1-5"
|
|
136
|
+
timezone: Asia/Shanghai
|
|
137
|
+
input: Prepare the weekday brief.
|
|
138
|
+
notify:
|
|
139
|
+
name: weekday-brief-group
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Each channel has `name` (stable slug), `description` (human-readable Chinese or
|
|
143
|
+
English label), and `chat_id` (the real Feishu group chat id). Profile `notify`
|
|
144
|
+
binds `name` and may optionally override `description` for local readability;
|
|
145
|
+
Beacon resolves `name` → `chat_id` at load time. Unknown names fail startup.
|
|
146
|
+
`admin` must use bare `chat_id` (not a channel `name`). Legacy bare `chat_id`
|
|
147
|
+
under `notify` still loads so hosts can migrate one Profile at a time; new
|
|
148
|
+
`notify` entries should use `name`.
|
|
113
149
|
|
|
114
150
|
Each Schedule uses a five-field cron expression and an IANA timezone. Profile
|
|
115
|
-
`admin
|
|
116
|
-
`reply_card` / `no_reply` and for failure notices. Optional `notify
|
|
151
|
+
`admin` is the private chat used for schedule `reply_text` /
|
|
152
|
+
`reply_card` / `no_reply` and for failure notices. Optional `notify` on a
|
|
117
153
|
Schedule is the group that receives `notify_card`. Beacon deliberately does not
|
|
118
154
|
infer or fall back to another destination. A newly discovered Schedule starts at
|
|
119
155
|
the current time.
|
|
@@ -188,12 +224,12 @@ listener:
|
|
|
188
224
|
types:
|
|
189
225
|
- com.example.deployment.completed.v1
|
|
190
226
|
notify:
|
|
191
|
-
|
|
227
|
+
name: weekday-brief-group
|
|
192
228
|
```
|
|
193
229
|
|
|
194
230
|
Both lists are required and nonempty. Values match exactly: **OR within each
|
|
195
231
|
list, AND between `sources` and `types`**. There are no wildcards, regular
|
|
196
|
-
expressions, or filters inside `data`. A listener requires `admin
|
|
232
|
+
expressions, or filters inside `data`. A listener requires `admin`;
|
|
197
233
|
`listener.notify` is optional. Matching Profiles each receive one Trigger with
|
|
198
234
|
the complete event as input. Their `persona.md` and `task.md` define what to do.
|
|
199
235
|
`reply_text` and `reply_card` go to the configured admin; `no_reply` is allowed;
|
|
@@ -311,7 +347,7 @@ beacon schedule trigger --profile example --schedule daily-report
|
|
|
311
347
|
|
|
312
348
|
This creates a distinct durable Run on every invocation and prints its
|
|
313
349
|
`run_id`. It uses the Schedule's configured `input`, replies to
|
|
314
|
-
`admin
|
|
350
|
+
the Profile `admin` channel, and may notify the Schedule `notify` channel, but does not read, initialize,
|
|
315
351
|
or advance the Schedule's cron cursor. Unknown
|
|
316
352
|
Profiles or Schedules and failed Runs or Deliveries exit non-zero; failure
|
|
317
353
|
messages include the `run_id` whenever a Run record was created.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { parseStrictYaml } from "./yaml.js";
|
|
6
|
+
export const channelNamePattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
|
|
7
|
+
const channelEntrySchema = z
|
|
8
|
+
.object({
|
|
9
|
+
name: z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(channelNamePattern, "Channel name must be a lowercase slug (a-z, 0-9, _, -)"),
|
|
12
|
+
description: z.string().trim().min(1),
|
|
13
|
+
chat_id: z.string().trim().min(1),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
const channelsDocumentSchema = z
|
|
17
|
+
.object({
|
|
18
|
+
version: z.literal(1),
|
|
19
|
+
channels: z.array(channelEntrySchema).default([]),
|
|
20
|
+
})
|
|
21
|
+
.strict();
|
|
22
|
+
export function defaultChannelsPath(homeDirectory = join(homedir(), ".beacon")) {
|
|
23
|
+
return join(homeDirectory, "channels.yaml");
|
|
24
|
+
}
|
|
25
|
+
export function emptyChannelRegistry() {
|
|
26
|
+
return new Map();
|
|
27
|
+
}
|
|
28
|
+
export async function loadChannelRegistry(path) {
|
|
29
|
+
const metadata = await stat(path).catch((error) => {
|
|
30
|
+
if (error &&
|
|
31
|
+
typeof error === "object" &&
|
|
32
|
+
"code" in error &&
|
|
33
|
+
error.code === "ENOENT") {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Cannot inspect Beacon channels file at ${path}`, {
|
|
37
|
+
cause: error,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
if (!metadata)
|
|
41
|
+
return emptyChannelRegistry();
|
|
42
|
+
if (!metadata.isFile()) {
|
|
43
|
+
throw new Error(`Beacon channels path is not a file: ${path}`);
|
|
44
|
+
}
|
|
45
|
+
const document = channelsDocumentSchema.parse(await parseStrictYaml(path));
|
|
46
|
+
const registry = new Map();
|
|
47
|
+
for (const entry of document.channels) {
|
|
48
|
+
if (registry.has(entry.name)) {
|
|
49
|
+
throw new Error(`Duplicate channel name in ${path}: ${entry.name}`);
|
|
50
|
+
}
|
|
51
|
+
registry.set(entry.name, {
|
|
52
|
+
name: entry.name,
|
|
53
|
+
description: entry.description,
|
|
54
|
+
chatId: entry.chat_id,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return registry;
|
|
58
|
+
}
|
|
59
|
+
export function resolveChannelName(name, channels, channelsPath) {
|
|
60
|
+
const channel = channels.get(name);
|
|
61
|
+
if (!channel) {
|
|
62
|
+
throw new Error(`Unknown Feishu channel name ${JSON.stringify(name)}; define it in ${channelsPath}`);
|
|
63
|
+
}
|
|
64
|
+
return channel;
|
|
65
|
+
}
|
package/dist/config/global.js
CHANGED
|
@@ -100,6 +100,7 @@ export async function loadGlobalConfig(path) {
|
|
|
100
100
|
homeDirectory,
|
|
101
101
|
profilesDirectory,
|
|
102
102
|
secretsPath: resolve(homeDirectory, "secrets.json"),
|
|
103
|
+
channelsPath: resolve(homeDirectory, "channels.yaml"),
|
|
103
104
|
runtimeEnvironmentPath: resolve(homeDirectory, "runtime.env"),
|
|
104
105
|
pi: {
|
|
105
106
|
executable,
|
package/dist/config/profile.js
CHANGED
|
@@ -4,11 +4,32 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { eventSourceSchema } from "../events/cloudevent.js";
|
|
6
6
|
import { nextOccurrence } from "../schedule/cron.js";
|
|
7
|
+
import { channelNamePattern, defaultChannelsPath, emptyChannelRegistry, resolveChannelName, } from "./channels.js";
|
|
7
8
|
import { parseStrictYaml } from "./yaml.js";
|
|
8
9
|
const profileIdPattern = /^[a-z0-9](?:[a-z0-9_-]{0,62})$/;
|
|
9
10
|
export const profilePersonaFile = "persona.md";
|
|
10
11
|
export const profileTaskFile = "task.md";
|
|
11
12
|
export const workspaceProfileDirectoryName = ".beacon-profile";
|
|
13
|
+
/** Admin DM: raw Feishu chat_id only (bot×person DMs are not channel registry entries). */
|
|
14
|
+
const adminDestinationSchema = z
|
|
15
|
+
.object({ chat_id: z.string().trim().min(1) })
|
|
16
|
+
.strict();
|
|
17
|
+
/** Group notify: prefer semantic name from ~/.beacon/channels.yaml; bare chat_id still loads. */
|
|
18
|
+
const namedDestinationSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
name: z
|
|
21
|
+
.string()
|
|
22
|
+
.regex(channelNamePattern, "Channel name must be a lowercase slug (a-z, 0-9, _, -)"),
|
|
23
|
+
description: z.string().trim().min(1).optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
const legacyChatDestinationSchema = z
|
|
27
|
+
.object({ chat_id: z.string().trim().min(1) })
|
|
28
|
+
.strict();
|
|
29
|
+
const notifyDestinationSchema = z.union([
|
|
30
|
+
namedDestinationSchema,
|
|
31
|
+
legacyChatDestinationSchema,
|
|
32
|
+
]);
|
|
12
33
|
const scheduleSchema = z
|
|
13
34
|
.object({
|
|
14
35
|
id: z.string().regex(profileIdPattern),
|
|
@@ -31,10 +52,7 @@ const scheduleSchema = z
|
|
|
31
52
|
}
|
|
32
53
|
}, "Schedule timezone must be a valid IANA timezone"),
|
|
33
54
|
input: z.string().trim().min(1),
|
|
34
|
-
notify:
|
|
35
|
-
.object({ chat_id: z.string().trim().min(1) })
|
|
36
|
-
.strict()
|
|
37
|
-
.optional(),
|
|
55
|
+
notify: notifyDestinationSchema.optional(),
|
|
38
56
|
})
|
|
39
57
|
.strict();
|
|
40
58
|
const profileDocumentSchema = z
|
|
@@ -47,19 +65,13 @@ const profileDocumentSchema = z
|
|
|
47
65
|
id: z.string().min(1),
|
|
48
66
|
})
|
|
49
67
|
.strict(),
|
|
50
|
-
admin:
|
|
51
|
-
.object({ chat_id: z.string().trim().min(1) })
|
|
52
|
-
.strict()
|
|
53
|
-
.optional(),
|
|
68
|
+
admin: adminDestinationSchema.optional(),
|
|
54
69
|
schedules: z.array(scheduleSchema).default([]),
|
|
55
70
|
listener: z
|
|
56
71
|
.object({
|
|
57
72
|
sources: z.array(eventSourceSchema).min(1),
|
|
58
73
|
types: z.array(z.string().min(1)).min(1),
|
|
59
|
-
notify:
|
|
60
|
-
.object({ chat_id: z.string().trim().min(1) })
|
|
61
|
-
.strict()
|
|
62
|
-
.optional(),
|
|
74
|
+
notify: notifyDestinationSchema.optional(),
|
|
63
75
|
})
|
|
64
76
|
.strict()
|
|
65
77
|
.optional(),
|
|
@@ -156,10 +168,23 @@ async function loadPersonaAndTask(profileId, workspace, profileDirectory) {
|
|
|
156
168
|
];
|
|
157
169
|
throw new Error(`Profile ${profileId} has no complete ${profilePersonaFile} + ${profileTaskFile} pair. Missing: ${missing.join(", ")}`);
|
|
158
170
|
}
|
|
159
|
-
|
|
171
|
+
function resolveNotifyDestination(destination, channels, channelsPath) {
|
|
172
|
+
if ("chat_id" in destination) {
|
|
173
|
+
return { chatId: destination.chat_id };
|
|
174
|
+
}
|
|
175
|
+
const channel = resolveChannelName(destination.name, channels, channelsPath);
|
|
176
|
+
return {
|
|
177
|
+
name: channel.name,
|
|
178
|
+
description: destination.description ?? channel.description,
|
|
179
|
+
chatId: channel.chatId,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export async function loadProfile(profileId, profilesDirectory = join(homedir(), ".beacon", "profiles"), options = {}) {
|
|
160
183
|
assertProfileId(profileId);
|
|
161
184
|
const profileDirectory = join(profilesDirectory, profileId);
|
|
162
185
|
const configPath = join(profileDirectory, "profile.yaml");
|
|
186
|
+
const channels = options.channels ?? emptyChannelRegistry();
|
|
187
|
+
const channelsPath = options.channelsPath ?? defaultChannelsPath(dirname(profilesDirectory));
|
|
163
188
|
const config = profileDocumentSchema.parse(await parseStrictYaml(configPath));
|
|
164
189
|
const scheduleIds = new Set();
|
|
165
190
|
for (const schedule of config.schedules) {
|
|
@@ -177,11 +202,12 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
177
202
|
}
|
|
178
203
|
}
|
|
179
204
|
if (config.schedules.length > 0 && !config.admin) {
|
|
180
|
-
throw new Error(`Profile ${profileId} with schedules must declare admin
|
|
205
|
+
throw new Error(`Profile ${profileId} with schedules must declare admin`);
|
|
181
206
|
}
|
|
182
207
|
if (config.listener && !config.admin) {
|
|
183
|
-
throw new Error(`Profile ${profileId} with listener must declare admin
|
|
208
|
+
throw new Error(`Profile ${profileId} with listener must declare admin`);
|
|
184
209
|
}
|
|
210
|
+
const admin = config.admin ? { chatId: config.admin.chat_id } : undefined;
|
|
185
211
|
const canonicalProfileDirectory = await realpath(profileDirectory);
|
|
186
212
|
const workspace = isAbsolute(config.workspace)
|
|
187
213
|
? config.workspace
|
|
@@ -207,14 +233,16 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
207
233
|
workspace,
|
|
208
234
|
runtime: config.runtime,
|
|
209
235
|
model: config.model,
|
|
210
|
-
...(
|
|
236
|
+
...(admin ? { admin } : {}),
|
|
211
237
|
...(config.listener
|
|
212
238
|
? {
|
|
213
239
|
listener: {
|
|
214
240
|
sources: config.listener.sources,
|
|
215
241
|
types: config.listener.types,
|
|
216
242
|
...(config.listener.notify
|
|
217
|
-
? {
|
|
243
|
+
? {
|
|
244
|
+
notify: resolveNotifyDestination(config.listener.notify, channels, channelsPath),
|
|
245
|
+
}
|
|
218
246
|
: {}),
|
|
219
247
|
},
|
|
220
248
|
}
|
|
@@ -225,14 +253,16 @@ export async function loadProfile(profileId, profilesDirectory = join(homedir(),
|
|
|
225
253
|
timezone: schedule.timezone,
|
|
226
254
|
input: schedule.input,
|
|
227
255
|
...(schedule.notify
|
|
228
|
-
? {
|
|
256
|
+
? {
|
|
257
|
+
notify: resolveNotifyDestination(schedule.notify, channels, channelsPath),
|
|
258
|
+
}
|
|
229
259
|
: {}),
|
|
230
260
|
})),
|
|
231
261
|
};
|
|
232
262
|
}
|
|
233
263
|
export function profileAdminChatId(profile) {
|
|
234
264
|
if (!profile.admin?.chatId) {
|
|
235
|
-
throw new Error(`Profile ${profile.id} must declare admin
|
|
265
|
+
throw new Error(`Profile ${profile.id} must declare admin`);
|
|
236
266
|
}
|
|
237
267
|
return profile.admin.chatId;
|
|
238
268
|
}
|
package/dist/config/registry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { loadProfile } from "./profile.js";
|
|
3
|
-
export async function loadProfileRegistry(profilesDirectory) {
|
|
3
|
+
export async function loadProfileRegistry(profilesDirectory, channels, channelsPath) {
|
|
4
4
|
const entries = await readdir(profilesDirectory, { withFileTypes: true });
|
|
5
5
|
const ids = entries
|
|
6
6
|
.filter((entry) => entry.isDirectory())
|
|
@@ -8,5 +8,8 @@ export async function loadProfileRegistry(profilesDirectory) {
|
|
|
8
8
|
.sort((left, right) => left.localeCompare(right));
|
|
9
9
|
if (ids.length === 0)
|
|
10
10
|
throw new Error(`No Profiles found in ${profilesDirectory}`);
|
|
11
|
-
return Promise.all(ids.map((id) => loadProfile(id, profilesDirectory
|
|
11
|
+
return Promise.all(ids.map((id) => loadProfile(id, profilesDirectory, {
|
|
12
|
+
...(channels ? { channels } : {}),
|
|
13
|
+
...(channelsPath ? { channelsPath } : {}),
|
|
14
|
+
})));
|
|
12
15
|
}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { defaultChannelsPath, loadChannelRegistry, } from "../config/channels.js";
|
|
4
5
|
import { loadProfile } from "../config/profile.js";
|
|
5
6
|
import { buildAgentSystemPrompt, } from "../runtime/system-prompt.js";
|
|
6
7
|
import { exportSessionHtml } from "./export.js";
|
|
@@ -40,7 +41,12 @@ async function beaconOwnedSystemPrompt(summary, profilesDirectory) {
|
|
|
40
41
|
if (summary.systemPrompt)
|
|
41
42
|
return summary.systemPrompt;
|
|
42
43
|
try {
|
|
43
|
-
|
|
44
|
+
const channelsPath = defaultChannelsPath(dirname(profilesDirectory));
|
|
45
|
+
const channels = await loadChannelRegistry(channelsPath);
|
|
46
|
+
return buildAgentSystemPrompt(await loadProfile(summary.profileId, profilesDirectory, {
|
|
47
|
+
channels,
|
|
48
|
+
channelsPath,
|
|
49
|
+
}), triggerFromRunSummary(summary));
|
|
44
50
|
}
|
|
45
51
|
catch {
|
|
46
52
|
// Old runs without a stored prompt: reconstruction needs a readable Profile.
|
package/dist/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loadChannelRegistry } from "./config/channels.js";
|
|
1
2
|
import { loadEventCredentials } from "./config/event-credentials.js";
|
|
2
3
|
import { loadGlobalConfig } from "./config/global.js";
|
|
3
4
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
@@ -8,7 +9,8 @@ import { runPiAgent } from "./runtime/pi-rpc.js";
|
|
|
8
9
|
export async function runDoctor(configPath) {
|
|
9
10
|
const global = await loadGlobalConfig(configPath);
|
|
10
11
|
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
11
|
-
const
|
|
12
|
+
const channels = await loadChannelRegistry(global.channelsPath);
|
|
13
|
+
const profiles = await loadProfileRegistry(global.profilesDirectory, channels, global.channelsPath);
|
|
12
14
|
if (global.events)
|
|
13
15
|
await loadEventCredentials(global.events.credentialsPath);
|
|
14
16
|
for (const profile of profiles) {
|
package/dist/manual-trigger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loadChannelRegistry } from "./config/channels.js";
|
|
2
3
|
import { loadGlobalConfig } from "./config/global.js";
|
|
3
4
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
4
5
|
import { loadRuntimeEnvironment } from "./config/runtime-environment.js";
|
|
@@ -17,7 +18,8 @@ export async function runManualTrigger(configPath, profileId, input, output = wr
|
|
|
17
18
|
throw new Error("Manual Trigger input must not be empty");
|
|
18
19
|
const global = await loadGlobalConfig(configPath);
|
|
19
20
|
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
20
|
-
const
|
|
21
|
+
const channels = await loadChannelRegistry(global.channelsPath);
|
|
22
|
+
const profiles = await loadProfileRegistry(global.profilesDirectory, channels, global.channelsPath);
|
|
21
23
|
const profile = profiles.find((candidate) => candidate.id === profileId);
|
|
22
24
|
if (!profile)
|
|
23
25
|
throw new Error(`Unknown Profile: ${profileId}`);
|
package/dist/outcome/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { defaultChannelsPath, loadChannelRegistry, } from "../config/channels.js";
|
|
2
3
|
import { loadProfile } from "../config/profile.js";
|
|
3
4
|
import { createProfileRunner } from "../run/profile-runner.js";
|
|
4
5
|
import { startOutcomeServer } from "./server.js";
|
|
@@ -13,7 +14,12 @@ const doctorTrigger = {
|
|
|
13
14
|
},
|
|
14
15
|
};
|
|
15
16
|
export async function runOutcomeDoctor(profileId) {
|
|
16
|
-
const
|
|
17
|
+
const channelsPath = defaultChannelsPath();
|
|
18
|
+
const channels = await loadChannelRegistry(channelsPath);
|
|
19
|
+
const profile = await loadProfile(profileId, undefined, {
|
|
20
|
+
channels,
|
|
21
|
+
channelsPath,
|
|
22
|
+
});
|
|
17
23
|
const outcomes = await startOutcomeServer();
|
|
18
24
|
const beaconCliPath = fileURLToPath(new URL("../../dist/cli.js", import.meta.url));
|
|
19
25
|
try {
|
package/dist/schedule-trigger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { loadChannelRegistry } from "./config/channels.js";
|
|
2
3
|
import { loadGlobalConfig } from "./config/global.js";
|
|
3
4
|
import { profileAdminChatId } from "./config/profile.js";
|
|
4
5
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
@@ -57,7 +58,8 @@ export async function triggerScheduleOnce(options) {
|
|
|
57
58
|
export async function runScheduleTrigger(configPath, profileId, scheduleId) {
|
|
58
59
|
const global = await loadGlobalConfig(configPath);
|
|
59
60
|
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
60
|
-
const
|
|
61
|
+
const channels = await loadChannelRegistry(global.channelsPath);
|
|
62
|
+
const profiles = await loadProfileRegistry(global.profilesDirectory, channels, global.channelsPath);
|
|
61
63
|
const profile = profiles.find((candidate) => candidate.id === profileId);
|
|
62
64
|
if (!profile)
|
|
63
65
|
throw new Error(`Unknown Profile: ${profileId}`);
|
package/dist/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { loadChannelRegistry } from "./config/channels.js";
|
|
2
3
|
import { loadEventCredentials } from "./config/event-credentials.js";
|
|
3
4
|
import { loadGlobalConfig } from "./config/global.js";
|
|
4
5
|
import { loadProfileRegistry } from "./config/registry.js";
|
|
@@ -37,7 +38,8 @@ function shutdownController() {
|
|
|
37
38
|
export async function runBeacon(configPath) {
|
|
38
39
|
const global = await loadGlobalConfig(configPath);
|
|
39
40
|
const runtimeEnvironment = await loadRuntimeEnvironment(global.runtimeEnvironmentPath);
|
|
40
|
-
const
|
|
41
|
+
const channels = await loadChannelRegistry(global.channelsPath);
|
|
42
|
+
const profiles = await loadProfileRegistry(global.profilesDirectory, channels, global.channelsPath);
|
|
41
43
|
const credentials = await Promise.all(profiles.map((profile) => loadFeishuCredentials(profile.id, global.secretsPath)));
|
|
42
44
|
const eventProducers = global.events
|
|
43
45
|
? await loadEventCredentials(global.events.credentialsPath)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
channels:
|
|
3
|
+
# Group chats only (notify_card). Admin DMs stay on profile admin.chat_id —
|
|
4
|
+
# one bot×person pair each, so they do not belong in this registry.
|
|
5
|
+
- name: weekday-brief-group
|
|
6
|
+
description: Example weekday brief notify group
|
|
7
|
+
chat_id: REPLACE_WITH_GROUP_CHAT_ID
|
|
@@ -11,7 +11,7 @@ schedules:
|
|
|
11
11
|
timezone: Asia/Shanghai
|
|
12
12
|
input: Prepare the weekday brief.
|
|
13
13
|
notify:
|
|
14
|
-
|
|
14
|
+
name: weekday-brief-group
|
|
15
15
|
# Optional external event subscription. Both lists use exact matches.
|
|
16
16
|
# listener:
|
|
17
17
|
# sources:
|
|
@@ -19,4 +19,4 @@ schedules:
|
|
|
19
19
|
# types:
|
|
20
20
|
# - com.example.deployment.completed.v1
|
|
21
21
|
# notify:
|
|
22
|
-
#
|
|
22
|
+
# name: weekday-brief-group
|