@seamnet/client 0.12.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.
@@ -0,0 +1,93 @@
1
+ # Services
2
+
3
+ Guardian 的内部能力。对插件可见(`hub.service(name)`),不对 MCP 暴露。
4
+
5
+ ## 边界
6
+
7
+ > **跨外部 = Plugin(外部通道适配器);不跨外部 = Service(内部能力)**
8
+
9
+ 所有不直接跟外部世界连接、但能复用的能力都是 Service。
10
+
11
+ ## Service 契约
12
+
13
+ ```js
14
+ function createXxxService(config) {
15
+ return {
16
+ name: 'xxx',
17
+ async init(hub) { /* 启动 */ },
18
+ async destroy() { /* 关闭 */ },
19
+ // + 具体能力方法(logger 的 info/warn/error; scheduler 的 every/at/cancel; ...)
20
+ };
21
+ }
22
+ module.exports = { createXxxService };
23
+ ```
24
+
25
+ - Service **不声明 actions**(不对 MCP)
26
+ - Service 生命周期由 guardian 统一管
27
+ - Service 之间可以互相依赖(`hub.service('dep')`)
28
+
29
+ ## 目录结构
30
+
31
+ 每个 Service 一个目录,和 Plugin 对称:
32
+
33
+ ```
34
+ lib/services/
35
+ ├── logger/ ✅ 实现
36
+ │ ├── index.cjs
37
+ │ └── README.md
38
+ ├── state/ ✅ 实现
39
+ │ ├── index.cjs
40
+ │ └── README.md
41
+ ├── event-bus/ ✅ 实现
42
+ │ ├── index.cjs
43
+ │ └── README.md
44
+ ├── message-buffer/ ✅ 实现
45
+ │ ├── index.cjs
46
+ │ └── README.md
47
+ ├── inbox/ ✅ 实现
48
+ │ ├── index.cjs
49
+ │ └── README.md
50
+ ├── scheduler/ ⏳ v1.5
51
+ └── watcher/ ⏳ v1.5
52
+ ```
53
+
54
+ ## 当前状态
55
+
56
+ | Service | 状态 | 作用 |
57
+ |---|---|---|
58
+ | `logger` | ✅ | 结构化 JSONL 日志 + 自动脱敏 |
59
+ | `state` | ✅ | 统一状态持久化(`.seam/state.json`),namespace 隔离 |
60
+ | `event-bus` | ✅ | 插件间事件总线(emit/on/once/off) |
61
+ | `message-buffer` | ✅ | 消息合并(debounce),避免 AI 抽风 |
62
+ | `inbox` | ✅ | 收到图片/文件的本地存储(`.seam/inbox/`) |
63
+ | `inject` | ⚠️ 嵌在 hub 里 | tmux send-keys 到 CC session |
64
+ | `run` | ⚠️ 嵌在 hub 里 | 错误隔离执行 |
65
+ | `scheduler` | ⏳ v1.5 | 定时任务(cron / every / at) |
66
+ | `watcher` | ⏳ v1.5 | 文件变化监听 |
67
+
68
+ ## 新增 Service 流程
69
+
70
+ 1. `lib/services/<name>/index.cjs` 按契约实现
71
+ 2. `lib/services/<name>/README.md` 说明:能力、API、依赖、错误码
72
+ 3. 在 `guardian-core.cjs` 的启动流程里 `hub.registerService(createXxxService(...))`
73
+ 4. 补单元测试 `test/unit/<name>.test.cjs`
74
+ 5. 在本文件的状态表里标 ✅
75
+
76
+ ## 使用示例
77
+
78
+ 插件里用 Service:
79
+
80
+ ```js
81
+ async init(hub) {
82
+ // 快捷方式(shortcut,常用)
83
+ const log = hub.logger(this.name);
84
+ log.info('started');
85
+
86
+ // 直接拿 service(v1.5 会用到)
87
+ const scheduler = hub.service('scheduler');
88
+ scheduler.every('5m', () => this.checkSomething());
89
+
90
+ const state = hub.service('state');
91
+ await state.set(this.name, { active: true });
92
+ }
93
+ ```
@@ -0,0 +1,76 @@
1
+ # Event Bus Service
2
+
3
+ 插件间事件总线。
4
+
5
+ ## 为什么
6
+
7
+ 现在 IM 插件自己维护 `messageCallbacks[]` 给其他插件订阅——是 hack。将来多插件协作必须有通用事件机制。
8
+
9
+ ## API
10
+
11
+ ```js
12
+ const bus = hub.service('event-bus');
13
+
14
+ // 订阅
15
+ const unsub = bus.on('im.message', async (msg) => {
16
+ // 处理...
17
+ });
18
+
19
+ // 一次性订阅
20
+ bus.once('wechat.binding_active', (data) => { ... });
21
+
22
+ // 发布(async,等所有 handler 完成)
23
+ await bus.emit('im.message', { from, text, isGroup });
24
+
25
+ // 取消订阅
26
+ unsub(); // 或者
27
+ bus.off('im.message', handler);
28
+ ```
29
+
30
+ ## 事件命名
31
+
32
+ 格式:`<plugin>.<event>`
33
+
34
+ | 事件 | 谁 emit | Payload |
35
+ |---|---|---|
36
+ | `im.message` | IM 插件 | `{ from, text, isGroup, conversationType }` |
37
+ | `wechat.message` | WeChat 插件 | `{ sender, text }` |
38
+ | `wechat.binding_active` | WeChat 插件 | `{ ilink_user_id }` |
39
+
40
+ 新插件加事件:在自己的 README 里声明会 emit 哪些 event,格式就是 plugin 名为前缀。
41
+
42
+ ## 错误隔离
43
+
44
+ - Handler 抛异常 **不影响** 其他 handler——内部 try-catch 后记日志继续
45
+ - Handler 可以是 async——emit 等所有完成后返回(`Promise.all`)
46
+
47
+ ## 已知限制
48
+
49
+ - 没有 wildcard(`im.*` 订阅不了)——v2 再说
50
+ - 没有事件级别优先级
51
+ - Payload 不校验——plugin 各自负责
52
+
53
+ ## 示例:IM 插件迁移
54
+
55
+ **老方式**(插件里自己维护订阅者数组):
56
+
57
+ ```js
58
+ const messageCallbacks = [];
59
+ function onMessage(cb) { messageCallbacks.push(cb); }
60
+ // 收到消息时
61
+ for (const cb of messageCallbacks) { try { cb(msg); } catch {} }
62
+ ```
63
+
64
+ **新方式**:
65
+
66
+ ```js
67
+ // 订阅方(wechat 插件 init)
68
+ hub.service('event-bus').on('im.message', ({ from, text, isGroup }) => {
69
+ if (!isGroup && text.trim() === '绑定微信') startLogin(from);
70
+ });
71
+
72
+ // 发布方(im 插件收到消息)
73
+ await hub.service('event-bus').emit('im.message', {
74
+ from: msg.from, text, isGroup, conversationType
75
+ });
76
+ ```
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Event Bus Service — 插件间事件总线。
3
+ *
4
+ * 设计:
5
+ * - emit / on / once / off 四个核心 API
6
+ * - Handler 可以是 async——emit 等所有 handler 完成(Promise.all)
7
+ * - Handler 抛异常不影响其他 handler(内部 try-catch + 日志)
8
+ * - 事件名约定:`<plugin>.<event>`(例如 `im.message`, `wechat.binding_active`)
9
+ * - Wildcard 不做(v1 简单为主)
10
+ */
11
+
12
+ function createEventBus() {
13
+ const listeners = new Map(); // event -> [handler, ...]
14
+ let log = null;
15
+
16
+ async function init(hub) {
17
+ log = hub.logger('event-bus');
18
+ }
19
+
20
+ async function destroy() {
21
+ listeners.clear();
22
+ }
23
+
24
+ function on(event, handler) {
25
+ if (typeof event !== 'string' || !event) {
26
+ throw new Error('event must be a non-empty string');
27
+ }
28
+ if (typeof handler !== 'function') {
29
+ throw new Error('handler must be a function');
30
+ }
31
+ if (!listeners.has(event)) listeners.set(event, []);
32
+ listeners.get(event).push(handler);
33
+ return () => off(event, handler);
34
+ }
35
+
36
+ function once(event, handler) {
37
+ const wrapped = async (...args) => {
38
+ off(event, wrapped);
39
+ return handler(...args);
40
+ };
41
+ return on(event, wrapped);
42
+ }
43
+
44
+ function off(event, handler) {
45
+ const arr = listeners.get(event);
46
+ if (!arr) return false;
47
+ const idx = arr.indexOf(handler);
48
+ if (idx < 0) return false;
49
+ arr.splice(idx, 1);
50
+ if (arr.length === 0) listeners.delete(event);
51
+ return true;
52
+ }
53
+
54
+ async function emit(event, ...args) {
55
+ const handlers = listeners.get(event);
56
+ if (!handlers || handlers.length === 0) return;
57
+ if (log) log.debug('emit', { event, listenerCount: handlers.length });
58
+ await Promise.all(
59
+ handlers.map(async (h) => {
60
+ try {
61
+ await h(...args);
62
+ } catch (e) {
63
+ if (log) log.error('handler_error', e, { event });
64
+ }
65
+ })
66
+ );
67
+ }
68
+
69
+ function listenerCount(event) {
70
+ return listeners.get(event)?.length || 0;
71
+ }
72
+
73
+ function events() {
74
+ return [...listeners.keys()];
75
+ }
76
+
77
+ return {
78
+ name: 'event-bus',
79
+ init,
80
+ destroy,
81
+ on,
82
+ once,
83
+ off,
84
+ emit,
85
+ listenerCount,
86
+ events,
87
+ };
88
+ }
89
+
90
+ module.exports = { createEventBus };
@@ -0,0 +1,52 @@
1
+ # Inbox Service
2
+
3
+ 本地存储收到的图片/文件。AI 用 `Read` 读本地路径看图。
4
+
5
+ ## 目录
6
+
7
+ `.seam/inbox/` — 权限 0700,文件权限 0600
8
+
9
+ ## 命名
10
+
11
+ `<ts>_<src>_<shortId>.<ext>`
12
+ - `ts`:ISO 时间戳 `20260418T123045`
13
+ - `src`:来源(wechat / im)
14
+ - `shortId`:6 字符随机 hex,避免同秒冲突
15
+ - `ext`:扩展名
16
+
17
+ ## API
18
+
19
+ ```js
20
+ const inbox = hub.service('inbox');
21
+ const filePath = inbox.save(buf, { ext: 'jpg', src: 'wechat' });
22
+ // → /path/.seam/inbox/20260418T123045_wechat_a1b2c3.jpg
23
+ ```
24
+
25
+ - `save(buf, { ext, src }) → localPath`
26
+ - `list() → [{name, path, size, mtimeMs}]`
27
+ - `cleanup() → {removed}` 按策略清理
28
+ - `remove(filePath) → bool`(只能删 inbox 内文件)
29
+ - `.dir` 当前 inbox 目录
30
+
31
+ ## 清理策略
32
+
33
+ - 7 天未访问 → 删
34
+ - 总大小超 500 MB → 按 FIFO 删老的
35
+
36
+ 配置:
37
+ ```js
38
+ createInboxService({ maxAgeMs, maxTotalBytes })
39
+ ```
40
+
41
+ ## 典型使用(插件)
42
+
43
+ ```js
44
+ // WeChat 收图
45
+ const key = Buffer.from(imageItem.aeskey, 'hex');
46
+ const resp = await fetch(imageItem.media.full_url);
47
+ const encrypted = Buffer.from(await resp.arrayBuffer());
48
+ const dec = crypto.createDecipheriv('aes-128-ecb', key, null);
49
+ const decrypted = Buffer.concat([dec.update(encrypted), dec.final()]);
50
+ const filePath = inbox.save(decrypted, { ext: 'jpg', src: 'wechat' });
51
+ hub.inject(`📱 [微信图片 ${ts}] ${sender} → ${filePath}`);
52
+ ```
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Inbox Service — 接收图片/文件的本地存储。
3
+ *
4
+ * 设计:
5
+ * - 目录:<seamDir>/inbox/
6
+ * - 文件名:<ts>_<src>_<shortId>.<ext>
7
+ * - 权限 0700
8
+ * - 清理策略:启动时 + 定期清超过 maxAgeMs 或总大小超过 maxTotalBytes 的文件(FIFO)
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const crypto = require('node:crypto');
14
+
15
+ const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
16
+ const DEFAULT_MAX_TOTAL_BYTES = 500 * 1024 * 1024; // 500 MB
17
+
18
+ function createInboxService(opts = {}) {
19
+ const {
20
+ maxAgeMs = DEFAULT_MAX_AGE_MS,
21
+ maxTotalBytes = DEFAULT_MAX_TOTAL_BYTES,
22
+ } = opts;
23
+ let hub = null;
24
+ let log = null;
25
+ let dir = null;
26
+
27
+ function ensureDir() {
28
+ if (!fs.existsSync(dir)) {
29
+ fs.mkdirSync(dir, { recursive: true });
30
+ try {
31
+ fs.chmodSync(dir, 0o700);
32
+ } catch {}
33
+ }
34
+ }
35
+
36
+ async function init(h) {
37
+ hub = h;
38
+ log = hub.logger('inbox');
39
+ dir = path.join(hub.seamDir, 'inbox');
40
+ ensureDir();
41
+ cleanup().catch(() => {});
42
+ log.info('inbox_ready', { dir });
43
+ }
44
+
45
+ async function destroy() {}
46
+
47
+ function sanitizeExt(ext) {
48
+ if (!ext) return '';
49
+ const cleaned = String(ext).replace(/[^a-zA-Z0-9]/g, '').slice(0, 8);
50
+ return cleaned ? `.${cleaned}` : '';
51
+ }
52
+
53
+ function sanitizeSrc(src) {
54
+ if (!src) return 'msg';
55
+ return String(src).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 16) || 'msg';
56
+ }
57
+
58
+ /**
59
+ * 保存一段 buffer 到 inbox。
60
+ * @param {Buffer} buf
61
+ * @param {Object} opts
62
+ * @param {string} [opts.ext] 文件扩展名(不含点)
63
+ * @param {string} [opts.src] 来源标识(如 'wechat', 'im')
64
+ * @returns {string} 本地绝对路径
65
+ */
66
+ function save(buf, opts = {}) {
67
+ ensureDir();
68
+ const ts = new Date().toISOString().replace(/[:.]/g, '').slice(0, 15); // 20260418T123045
69
+ const srcTag = sanitizeSrc(opts.src);
70
+ const shortId = crypto.randomBytes(3).toString('hex');
71
+ const ext = sanitizeExt(opts.ext);
72
+ const filename = `${ts}_${srcTag}_${shortId}${ext}`;
73
+ const filePath = path.join(dir, filename);
74
+ fs.writeFileSync(filePath, buf);
75
+ try {
76
+ fs.chmodSync(filePath, 0o600);
77
+ } catch {}
78
+ if (log) {
79
+ log.info('saved', {
80
+ path: filePath,
81
+ bytes: buf.length,
82
+ src: srcTag,
83
+ });
84
+ }
85
+ return filePath;
86
+ }
87
+
88
+ function list() {
89
+ ensureDir();
90
+ const entries = fs.readdirSync(dir);
91
+ return entries.map((name) => {
92
+ const full = path.join(dir, name);
93
+ const st = fs.statSync(full);
94
+ return { name, path: full, size: st.size, mtimeMs: st.mtimeMs };
95
+ });
96
+ }
97
+
98
+ /**
99
+ * 清理策略:
100
+ * 1. 删除 age 超过 maxAgeMs 的文件
101
+ * 2. 若剩余总 size 仍超过 maxTotalBytes,按 mtime 升序(旧在前)删到合规
102
+ */
103
+ async function cleanup() {
104
+ ensureDir();
105
+ const now = Date.now();
106
+ let removed = 0;
107
+ let items = list();
108
+
109
+ for (const item of items) {
110
+ if (now - item.mtimeMs > maxAgeMs) {
111
+ try {
112
+ fs.unlinkSync(item.path);
113
+ removed++;
114
+ } catch {}
115
+ }
116
+ }
117
+
118
+ items = list();
119
+ const totalBytes = items.reduce((a, b) => a + b.size, 0);
120
+ if (totalBytes > maxTotalBytes) {
121
+ items.sort((a, b) => a.mtimeMs - b.mtimeMs);
122
+ let current = totalBytes;
123
+ for (const item of items) {
124
+ if (current <= maxTotalBytes) break;
125
+ try {
126
+ fs.unlinkSync(item.path);
127
+ current -= item.size;
128
+ removed++;
129
+ } catch {}
130
+ }
131
+ }
132
+
133
+ if (log && removed > 0) {
134
+ log.info('cleanup_done', { removed, maxAgeMs, maxTotalBytes });
135
+ }
136
+ return { removed };
137
+ }
138
+
139
+ function remove(filePath) {
140
+ try {
141
+ const resolved = path.resolve(filePath);
142
+ // 安全:只允许删 inbox 目录下的文件
143
+ if (!resolved.startsWith(dir + path.sep)) {
144
+ throw new Error('path not in inbox dir');
145
+ }
146
+ fs.unlinkSync(resolved);
147
+ return true;
148
+ } catch (e) {
149
+ if (log) log.warn('remove_failed', { filePath, message: e.message });
150
+ return false;
151
+ }
152
+ }
153
+
154
+ return {
155
+ name: 'inbox',
156
+ init,
157
+ destroy,
158
+ save,
159
+ list,
160
+ cleanup,
161
+ remove,
162
+ get dir() {
163
+ return dir;
164
+ },
165
+ };
166
+ }
167
+
168
+ module.exports = { createInboxService };
@@ -0,0 +1,81 @@
1
+ # Logger Service
2
+
3
+ JSONL 结构化日志,写到 `.seam/logs/guardian.jsonl`。
4
+
5
+ ## 特性
6
+
7
+ - **每行一个 JSON 对象**,`jq` 友好
8
+ - **自动脱敏**:`userSig` / `bot_token` / `password` 等字段值替换为 `<redacted>`
9
+ - **child logger**:派生带插件名和请求 id 的子 logger
10
+ - **永不抛**:日志写入失败静默吞掉,不影响主流程
11
+
12
+ ## API
13
+
14
+ ```js
15
+ const { createLogger } = require('./services/logger/index.cjs');
16
+ const logger = createLogger({ logPath: '/path/.seam/logs/guardian.jsonl' });
17
+
18
+ logger.info('event_name', { data }); // level: info
19
+ logger.warn('event_name', { data }); // level: warn
20
+ logger.error('event_name', errorObj, { data }); // level: error
21
+ logger.debug('event_name', { data }); // level: debug
22
+
23
+ // Child logger 带插件名和 reqId
24
+ const imLog = logger.child({ plugin: 'im', reqId: 'abc' });
25
+ imLog.info('sdk_ready'); // → {ts, level:'info', plugin:'im', req_id:'abc', event:'sdk_ready'}
26
+ ```
27
+
28
+ 在插件里通常用 hub 的快捷方式:
29
+
30
+ ```js
31
+ const log = hub.logger(this.name);
32
+ log.info('started');
33
+ ```
34
+
35
+ ## 日志格式
36
+
37
+ ```json
38
+ {"ts":"2026-04-18T10:00:00.000Z","level":"info","plugin":"im","req_id":"abc-123","event":"sdk_ready","data":{"userId":"u1"}}
39
+ ```
40
+
41
+ 字段:
42
+ - `ts` — ISO 时间戳
43
+ - `level` — info / warn / error / debug
44
+ - `plugin` — 插件名(可选)
45
+ - `req_id` — 请求 id(可选,经 child logger 传入)
46
+ - `event` — 事件名(snake_case)
47
+ - `data` — 自由结构(会被脱敏)
48
+ - `error` — 错误对象(含 message/stack/code)
49
+
50
+ ## 脱敏字段
51
+
52
+ `SENSITIVE_KEYS` 数组(大小写不敏感包含匹配):
53
+
54
+ - `userSig`
55
+ - `secretKey`
56
+ - `bot_token` / `botToken`
57
+ - `password`
58
+ - `authorization`
59
+ - `token`
60
+ - `context_token` / `contextToken`
61
+
62
+ 添加新字段:直接改 `lib/services/logger/index.cjs` 里的 `SENSITIVE_KEYS` 数组。
63
+
64
+ ## 查日志
65
+
66
+ ```bash
67
+ # 按 req_id 查完整轨迹
68
+ jq 'select(.req_id=="<id>")' .seam/logs/guardian.jsonl
69
+
70
+ # 按插件+级别过滤
71
+ jq 'select(.plugin=="wechat" and .level=="error")' .seam/logs/guardian.jsonl
72
+
73
+ # 最近 N 条错误
74
+ tail -n 200 .seam/logs/guardian.jsonl | jq 'select(.level=="error")'
75
+ ```
76
+
77
+ ## 已知限制
78
+
79
+ - 写入是 sync(`appendFileSync`)——高并发下有轻微阻塞,当前量级(<100 req/s)无问题
80
+ - 无 log rotation——未来跨天要补(v1.5)
81
+ - 递归深度上限 5(防循环引用)
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Seam Logger — JSONL 结构化日志。
3
+ *
4
+ * 设计:
5
+ * - 每行一个 JSON 对象,写到 .seam/logs/guardian.jsonl
6
+ * - 自动脱敏 SENSITIVE_KEYS(userSig / bot_token / password 等)
7
+ * - child({plugin, reqId}) 派生带插件名和请求 id 的子 logger
8
+ * - 不抛异常——日志失败不应该影响主流程
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const SENSITIVE_KEYS = [
15
+ 'userSig',
16
+ 'secretKey',
17
+ 'bot_token',
18
+ 'botToken',
19
+ 'password',
20
+ 'authorization',
21
+ 'token',
22
+ 'context_token',
23
+ 'contextToken',
24
+ ];
25
+
26
+ function shouldRedact(key) {
27
+ const lower = String(key).toLowerCase();
28
+ return SENSITIVE_KEYS.some((s) => lower.includes(s.toLowerCase()));
29
+ }
30
+
31
+ function redact(value, depth = 0) {
32
+ if (depth > 5) return '[deep]';
33
+ if (value === null || value === undefined) return value;
34
+ if (typeof value !== 'object') return value;
35
+ if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
36
+ const out = {};
37
+ for (const [k, v] of Object.entries(value)) {
38
+ if (shouldRedact(k)) {
39
+ out[k] = '<redacted>';
40
+ } else {
41
+ out[k] = redact(v, depth + 1);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ class Logger {
48
+ constructor({ logPath, plugin, reqId }) {
49
+ this.logPath = logPath;
50
+ this.plugin = plugin;
51
+ this.reqId = reqId;
52
+ }
53
+
54
+ _write(level, event, data, error) {
55
+ const line = {
56
+ ts: new Date().toISOString(),
57
+ level,
58
+ ...(this.plugin && { plugin: this.plugin }),
59
+ ...(this.reqId && { req_id: this.reqId }),
60
+ event,
61
+ };
62
+ if (data !== undefined) line.data = redact(data);
63
+ if (error !== undefined) line.error = redact(error);
64
+ try {
65
+ if (this.logPath) {
66
+ const dir = path.dirname(this.logPath);
67
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
68
+ fs.appendFileSync(this.logPath, JSON.stringify(line) + '\n');
69
+ }
70
+ } catch {
71
+ // logger never throws
72
+ }
73
+ return line;
74
+ }
75
+
76
+ info(event, data) {
77
+ return this._write('info', event, data);
78
+ }
79
+
80
+ warn(event, data) {
81
+ return this._write('warn', event, data);
82
+ }
83
+
84
+ error(event, error, data) {
85
+ const errObj =
86
+ error instanceof Error
87
+ ? { message: error.message, stack: error.stack, ...(error.code && { code: error.code }) }
88
+ : error;
89
+ return this._write('error', event, data, errObj);
90
+ }
91
+
92
+ debug(event, data) {
93
+ return this._write('debug', event, data);
94
+ }
95
+
96
+ child({ plugin, reqId } = {}) {
97
+ return new Logger({
98
+ logPath: this.logPath,
99
+ plugin: plugin || this.plugin,
100
+ reqId: reqId || this.reqId,
101
+ });
102
+ }
103
+ }
104
+
105
+ function createLogger({ logPath }) {
106
+ return new Logger({ logPath });
107
+ }
108
+
109
+ module.exports = { createLogger, Logger, redact, SENSITIVE_KEYS };