dsh-log 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INTEGRATION.md ADDED
@@ -0,0 +1,216 @@
1
+ # 日志包集成教程(从安装到首条落盘)
2
+
3
+ 对应版本:日志包 `0.1.0`(包版本见 `package.json` 的 `version` 字段;升级后以新版教程为准,旧版教程只对应当时的包)。
4
+
5
+ 本文档有两类读者,按自己的形态选一条路走,**不要两条都走**:
6
+
7
+ **路线 A|DSH 插件作者**:你的代码跑在 DSH 宿主里(插件宿主侧与客户端侧两个半区)。走下面的步骤 0、1、2、3、4 一路到底。
8
+
9
+ **路线 B|独立 Node 程序作者**:你的程序不是 DSH 插件、自己独立跑(例如一个技能自己的命令行)。走下面的步骤 0、1、**2B**、跳过 2 与 3,再看步骤 4 里的落点说明。你没有宿主文件服务、没有电话、也没有客户端要转发,用不上那两节。
10
+
11
+ 全程用词:日志系统指日志功能本身,日志包指装着日志系统的这个 npm 包。电话指宿主对外提供的方法;落盘指宿主统一写本地文件的动作。
12
+
13
+ ## 步骤 0. 确认环境
14
+
15
+ Node 22 或更高:
16
+
17
+ ```sh
18
+ node --version
19
+ ```
20
+
21
+ ## 步骤 1. 安装
22
+
23
+ ```sh
24
+ npm install dsh-log
25
+ ```
26
+
27
+ 包还没公开发布时,这条命令装不上:先用本地路径或工作区引用代替(例如 `npm install ../dsh-log`),等包发布后才用上面的命令。发布前用 `npm view dsh-log` 查一次重名:返回 404 表示名字还没被占(本次查重 404,证据贴在 #562 票评论)。
28
+
29
+ 上面这条本地路径命令要在你自己插件的目录里执行,不是在日志包目录里。装完确认法:宿主侧能引用到已构建产物(`dist/host.js` 与 `dist/client.js`),并且读到 `hostLog.phoneNames` 里的 5 个电话名(`logBatch`、`logExport`、`logClear`、`logGetSwitch`、`logSetSwitch` 各一个),即算装好。独立 Node 程序只需要 `dist/node.js` 能引用到,见步骤 2B,读不到电话名是正常的。
30
+
31
+ ## 步骤 2B. 独立 Node 程序的接线(只有这一节,一次调用就够)
32
+
33
+ 独立程序不需要传文件服务、不需要注册电话、也没有客户端要转发,所以只有一步:
34
+
35
+ ```js
36
+ import { createNodeHostLog } from 'dsh-log/node'
37
+
38
+ const log = await createNodeHostLog(
39
+ { cacheDir: '/你自己决定的/日志目录' },
40
+ { pluginId: 'my-standalone-program' }
41
+ )
42
+
43
+ log.store.log('warn', 'my.step.fail', { step: 'fetch', reason: 'timeout' })
44
+ await log.ready // 等这一下:程序退出前日志才真的落盘
45
+ ```
46
+
47
+ 三件事要对上:
48
+
49
+ 1. **`cacheDir` 由你决定,入口不猜**。它不读任何环境变量、也没有默认路径;你要把日志放在自己的数据目录旁边,就把那个目录传进来。目录不存在时入口自己建。
50
+ 2. **`pluginId` 必填**,规则与插件侧一样:小写英文字母、数字、中横线,1 到 32 个字符。它决定落点:日志写在 `<cacheDir>/logs-<标识>/` 下按天一个文件,调试开关写在 `<cacheDir>/log-switch-<标识>.json`。
51
+ 3. **`await log.ready` 不能省**。落盘是防抖加异步写的,短命程序不等它,日志会随进程一起消失。这一下会等到「前面写的每一行都已经在文件里」才返回。
52
+
53
+ 与插件侧的差别,一眼看清:
54
+
55
+ | 事情 | 插件(路线 A) | 独立程序(路线 B) |
56
+ |---|---|---|
57
+ | 文件服务 | 传宿主的文件服务 | 入口内置 Node 标准库实现 |
58
+ | 计时器、平台、默认目录 | 传宿主的 | 入口内置 |
59
+ | 电话名与注册 | 要(客户端经电话转发) | 不用,`phoneNames` 只是派生结果,看看即可 |
60
+ | 5 个电话(导出、清空、开关) | 界面调 | 直接用 `log.store` 上的同名方法 |
61
+ | 开关 | 宿主持有、客户端对账 | 入口建库时自动读回上次的值 |
62
+
63
+ ## 步骤 2. 宿主侧接线(三步接入)
64
+
65
+ 在宿主启动处写:
66
+
67
+ ```js
68
+ import { createHostLog, registerHostLogPhones } from 'dsh-log/host'
69
+
70
+ const hostLog = createHostLog(
71
+ { fs, timer, getCacheDir, getPlatform, DEFAULT_CWD },
72
+ { pluginId: 'my-plugin' }
73
+ )
74
+ registerHostLogPhones(new Map(), hostLog)
75
+ ```
76
+
77
+ 三件事要对上:
78
+
79
+ 1. `pluginId` 必填,换成自己插件的标识(小写英文字母、数字、中横线,1 到 32 个字符,大写直接报错)。
80
+ 2. 五个运行依赖由你传入现成的对象:文件服务 `fs`、计时器 `timer`、取缓存目录函数 `getCacheDir`、取平台函数 `getPlatform`、默认工作目录 `DEFAULT_CWD`。
81
+ 3. 注册表传你自己的电话注册表(例子用 `new Map()` 示意);注册成功的 5 个电话名可从 `hostLog.phoneNames` 读到,形如 `my-plugin.logBatch` 等。
82
+
83
+ 五个运行依赖的最小形状(类型上五个键都可省略,省略后走退化路;要正常落盘,文件服务的读写方法与取缓存目录函数必须给):
84
+
85
+ | 依赖 | 最小形状 | 可省略吗 |
86
+ |---|---|---|
87
+ | 文件服务 `fs` | `resolve(路径字符串)` 回目标对象;`readText(目标)` 回文件原文;`writeText(目标, 原文)` 写文件;`mkdir(目录)` 建目录;`unlink(目标)` 删文件;`listDir(目录目标)` 回条目数组(字符串数组,或含 `name` 的对象数组) | `resolve`、`mkdir`、`unlink`、`listDir` 可省略(建不上目录也不报错;删文件缺了改写空串回退;列目录缺了按空目录处理)。`readText`、`writeText` 落盘必填,缺了报文件服务不可读或不可写。注意列目录必须返回真实条目:永远返回空数组会导致清空电话删掉 0 个文件,这是传入的仿真不对,不是包的问题 |
88
+ | 计时器 `timer` | `timeout(回调, 毫秒数)` 安排一次回调 | 可省略,无则回退运行环境自带的全局定时函数 |
89
+ | 取缓存目录函数 `getCacheDir` | 无入参,回目录字符串(同步返回或异步返回都可) | 落盘必填。返回空表示取不到目录:写盘记丢弃,导出走默认工作目录回退 |
90
+ | 取平台函数 `getPlatform` | 无入参,回平台结构 `{ os, path, fs }`:`os` 是系统名字符串,`path.join` 拼路径,`fs` 是与文件服务同形的备用文件服务 | 可省略,无则路径拼接用包内默认的斜杠拼接,文件走传入的 `fs` |
91
+ | 默认工作目录 `DEFAULT_CWD` | 字符串,例如 `'/work'` | 可省略,无则取空串,只在取不到缓存目录时影响导出回退路径的长短 |
92
+
93
+ 只传 `pluginId` 就用上全部默认:电话名前缀回退到插件标识,目录名派生为 `logs-my-plugin`,开关文件名派生为 `log-switch-my-plugin.json`,文件名按天。第二个插件不得与第一个共用同一目录,走默认派生自然分开。
94
+
95
+ 如果注册时报“电话名已被注册,不覆盖旧的”,说明这个前缀已被别的插件用掉:换一个没被用过的前缀(`prefix` 显式传入),不要复用默认前缀 `wf`(`wf` 只给当前插件用)。
96
+
97
+ ## 步骤 3. 客户端侧接线(两种消费二选一)
98
+
99
+ 推荐直接 import:
100
+
101
+ ```js
102
+ import { createClientLog } from 'dsh-log/client'
103
+
104
+ const clientLog = createClientLog(
105
+ { host, timer, storage, broadcastLogSwitch },
106
+ { pluginId: 'my-plugin' }
107
+ )
108
+ ```
109
+
110
+ 四个依赖全可选,有现成的就传,缺了走退化路、不抛错:宿主调用器 `host`、计时器 `timer`、存储 `storage`(只传 `storage`,旧名 `localStorage` 是迁移期兼容,两个都传以 `storage` 为准)、开关广播 `broadcastLogSwitch`(没有就不传,不报错)。前后缀保持与宿主侧同一个 `pluginId`,两端拼出的电话名自然对上。
111
+
112
+ 宿主调用器这样桥接到步骤 2 注册的那 5 个电话(用同一张注册表):
113
+
114
+ ```js
115
+ const registry = new Map()
116
+ registerHostLogPhones(registry, hostLog)
117
+ const host = { call: (name, args) => registry.get(name)(args) }
118
+ ```
119
+
120
+ 上面三行里前两行与步骤 2 是同一张表,第三行的 `call` 把调用直接转给表里的处理函数。真机上把 `call` 换成自己宿主的实际调用通道即可,电话名与入参形状不变。
121
+
122
+ 只有把客户端拼进插件主文件闭包一起运行的插件,才用文本拼接:构建时取客户端入口编译后的声明体,去行首 `export` 后拼进闭包,调用时把闭包里现成的四个名字原样传给工厂。文本拼接消费方式只走客户端入口的声明体文本。
123
+
124
+ ## 步骤 4. 写第一条日志并看它落盘
125
+
126
+ ```js
127
+ // 开关默认关闭,错误与告警始终记,信息与调试只在开关打开时记。
128
+ // 调试阶段先打开开关(面向用户的开关界面见步骤 6)。
129
+ await clientLog.setLogSwitch(true, 1)
130
+ clientLog.log('info', 'my-plugin.hello', { step: 'started' })
131
+ clientLog.flush()
132
+ ```
133
+
134
+ 第二个参数是采样率,取 0 到 1 之间的小数,1 表示全量(默认就是 1);传非数字时保持旧值不变。
135
+
136
+ 随后在缓存目录下的 `logs-my-plugin` 目录里看到当天的 `年月日.log` 文件,里面有这一条。宿主是唯一的落盘者:客户端只进队列就返回,转发走电话,落盘走宿主刷盘链路。
137
+
138
+ 等多久:调了 `flush` 只剩宿主侧约 1000 毫秒防抖刷盘,约 1 秒后去看文件;不调 `flush` 则客户端约 1000 毫秒转发加上宿主约 1000 毫秒刷盘,约 2 秒后再看。错误与告警两级直通,不用等满防抖。脚本里用轮询等文件出现(例如每 200 毫秒看一次、最多等 6 秒),不要写死睡固定秒数,心急看不到文件会误以为没接通。
139
+
140
+ 记日志前先判断开关(高频调用处必须写,日志器体内的判断只是兜底):
141
+
142
+ ```js
143
+ if (clientLog.isEnabled('info')) {
144
+ clientLog.log('info', 'my-plugin.step', { step: 'done', latencyMs: 12 })
145
+ }
146
+ ```
147
+
148
+ 累计丢弃数随时可读:`clientLog.getDroppedCount()`。失败只计数不抛错,队列满与转发失败都只加到这个数里。
149
+
150
+ ## 步骤 5. 接入对象清单(可选,但推荐)
151
+
152
+ 1. 把包内的 `event-list.template.json` 复制一份,`pluginId` 换成自己插件的标识,按实际事件填 `events` 与三类自报计数 `counts`。只含一条事件的最小例子(#563 验证用同形走通,三个检查函数一次过):
153
+
154
+ ```json
155
+ {
156
+ "version": 1,
157
+ "pluginId": "my-plugin",
158
+ "counts": { "resident": 1, "ondemand": 0, "selfmon": 0 },
159
+ "events": {
160
+ "my-plugin.hello": {
161
+ "level": "info",
162
+ "kind": "resident",
163
+ "fields": ["step"]
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ 上面这条是常驻信息事件:事件名 `my-plugin.hello`,级别 `info`,归类 `resident`,允许字段只有 `step`,自报计数 `resident` 为 1 其余为 0。下面第 3 步检查器示例用的正是这条,上下两环对上了。
170
+
171
+ 2. 自己读成对象再传给 `eventList`(日志包不读盘):
172
+
173
+ ```js
174
+ import { readFileSync } from 'node:fs'
175
+
176
+ const myEventList = JSON.parse(readFileSync('./event-list.my-plugin.json', 'utf8'))
177
+ const hostLog = createHostLog(deps, { pluginId: 'my-plugin', eventList: myEventList })
178
+ ```
179
+
180
+ 3. 发布前用检查器自查(字段白名单与计数检查):
181
+
182
+ ```js
183
+ import { parseEventListManifest, checkEventFields, checkEventCounts } from 'dsh-log/host'
184
+
185
+ const manifest = parseEventListManifest(myEventList)
186
+ checkEventFields(manifest, 'my-plugin.hello', ['step'])
187
+ checkEventCounts(manifest)
188
+ ```
189
+
190
+ 每条事件四样东西:事件名、级别、允许字段(之外的键一律不记)、脱敏引用(只记规则名不记原文)。增删事件必须同步改清单的 `counts`,否则计数检查变红。细节见 `README.md` 第 7 节。
191
+
192
+ ## 步骤 6. 界面建议(导出、清空、开关)
193
+
194
+ - 导出:在状态栏菜单与设置页各放一个导出入口,都调 `logExport`(入参可选日期,不传导出当天)。成功把回参的 `text` 存成文件给用户;失败分支记一行 `log.export.fail`(走 `logExportFail`,成功路径不调用);失败原因只给机器码,面向用户的文案由界面经多语言系统转换,日志包内不写面向用户的中文字符串。
195
+ - 清空:在设置页放清空入口,调 `logClear`(`date` 传某天或 `all`),回参的 `removed` 告诉用户删掉几个文件。
196
+ - 开关:在设置页放调试开关(是否开启加采样率),调 `setLogSwitch` / 启动时调 `reconcileLogSwitch` 向宿主对账(以宿主为准)。开关写失败保持旧值(失败原因只给机器码:`host-unavailable`、`host-rejected`、`switch-timeout`、`stale` 等),由调用处提示用户,不回退为开启。
197
+
198
+ ## 常见坑
199
+
200
+ 1. 多进程同时写同一个目录会写坏:一个插件只建一个日志库实例,目录不要两个实例共用;第二个插件走默认派生自然分到不同目录。
201
+ 2. Windows 文件名里不能有冒号:四段式文件名的启动时间已把冒号与斜杠转写为中横线,目录派生与标识字符约束也排除了文件不安全字符,不要自己拼带冒号的文件名。
202
+ 3. 目录漂移(缓存目录变化导致找不到旧日志):取缓存目录走传入的 `getCacheDir`,不要在包外自己缓存一份目录路径;目录里只记散列不记原文路径。
203
+ 4. 电话名撞名:已注册的电话名再注册会报错、不覆盖,换前缀解决,不要复用 `wf`。
204
+ 5. 标识大小写:收到大写直接报错,不做静默转小写,全小写重传即可。
205
+ 6. 导出 `bytes` 是日志原文的字符串长度,不是真实字节数,按现状使用即可。
206
+ 7. 开关写成功后,采样率以客户端请求时的值为准更新本地,不等宿主回(宿主回参本来就没有采样率)。
207
+
208
+ ## 自查清单(按本文档能否走通)
209
+
210
+ - [ ] `npm install dsh-log` 成功,Node 22 或更高。
211
+ - [ ] 宿主侧三步接入跑通,`hostLog.phoneNames` 里 5 个电话名都带自己的前缀。
212
+ - [ ] 客户端二选一接通,高频调用处写了外层 `isEnabled` 判断。
213
+ - [ ] 首条日志落盘:在派生目录里找到当天文件并看到该条。
214
+ - [ ] `getDroppedCount` 可读;失败场景(断开宿主)只计数不抛错。
215
+ - [ ] 清单接入(如做):模板已复制改名,检查器三函数全过,计数逐项一致。
216
+ - [ ] 发布前跑过第 9 节门禁(包内单测与 14 个日志门禁全绿,55 事件不变),dry-run 文件数与 `README.md` 第 9 节对上。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 王辰浩
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # dsh-log(日志包)
2
+
3
+ 可复用的日志系统装成的 npm 包:DSH 插件与独立跑的 Node 程序都能照着这份文档集成使用。
4
+ 地图 #556 的一部分,本包包含宿主引擎(#559)、客户端引擎(#560)、事件清单格式与通用检查器(#561),集成步骤另见包内的 `INTEGRATION.md`(从安装到首条落盘的按序教程)。
5
+
6
+ 全程用词:日志系统指日志功能本身(引擎、对外接口、检查器、文档);日志包指装着日志系统的这个 npm 包。电话指宿主对外提供的方法;落盘指宿主统一写本地文件的动作。
7
+
8
+ ## 1. 安装
9
+
10
+ ```sh
11
+ npm install dsh-log
12
+ ```
13
+
14
+ 要求 Node 22 或更高。当前版本 `0.2.0`。
15
+
16
+ ## 2. 三个入口,按运行位置选用
17
+
18
+ - `dsh-log/host`:DSH 插件宿主侧用。建日志库、拼电话名、注册电话名、事件清单检查器。
19
+ - `dsh-log/client`:DSH 插件客户端侧用。建日志器、电话名拼法、批量转发口径数字。
20
+ - `dsh-log/node`:**不是插件的独立 Node 程序用**(例如一个技能自己的命令行)。一句话建好落盘日志,不必自己拼文件服务与计时器。
21
+
22
+ 三个入口在同一个包里,装一次全都有。
23
+
24
+ `dsh-log/node` 与前两个的区别:它不碰电话层、也没有客户端转发,只有「把日志落到本地文件」以及它背后的级别规则、目录派生与失败计数。它**不决定日志写到哪**——目录由调用方传入,入口自己不读任何环境变量、也不带默认路径。
25
+
26
+ ## 2.1 独立 Node 程序怎么接(`dsh-log/node`)
27
+
28
+ ```js
29
+ import { createNodeHostLog } from 'dsh-log/node'
30
+
31
+ const log = await createNodeHostLog(
32
+ { cacheDir: '/你自己决定的/日志目录' },
33
+ { pluginId: 'your-program' }
34
+ )
35
+
36
+ log.store.log('warn', 'your.step.fail', { step: 'fetch', reason: 'timeout' })
37
+ await log.ready // 等这一下:程序退出前日志才真的落盘
38
+ ```
39
+
40
+ 落点由 `cacheDir` 与标识一起决定:日志写在 `<cacheDir>/logs-your-program/2026-09-11.log`,调试开关写在 `<cacheDir>/log-switch-your-program.json`。目录不存在时入口自己建。
41
+
42
+ 三件要知道的事:
43
+
44
+ 1. **`await log.ready` 不能省**。落盘是防抖加异步写的,短命程序不等它,日志会随进程一起消失。这一下会等到「前面写的每一行都已经在文件里」才返回。
45
+ 2. **开关每次新建时会被读回来**。独立程序每次调用都是新进程,所以入口建库时会把上次留下的开关读回来,打开的详细日志在下次调用仍然生效;写入用 `log.store.handleLogSetSwitch({ enabled: true, sampleRate: 1 })`。
46
+ 3. **级别规则与插件侧完全一致**:错误与告警恒落盘,常驻信息落盘,其余信息与调试只在开关打开时落盘。
47
+
48
+ ## 3. 三步接入(宿主侧)
49
+
50
+ ```js
51
+ import { createHostLog, registerHostLogPhones } from 'dsh-log/host'
52
+
53
+ // 五个运行依赖全部由调用方传入,包内不自己抓:
54
+ // fs 文件服务、timer 计时器、getCacheDir 取缓存目录函数、
55
+ // getPlatform 取平台函数、DEFAULT_CWD 默认工作目录。
56
+ // 每个依赖的最小形状与可省略项见 INTEGRATION.md 步骤 2 的形状表。
57
+ const hostLog = createHostLog(
58
+ { fs, timer, getCacheDir, getPlatform, DEFAULT_CWD },
59
+ { pluginId: 'wf' }
60
+ )
61
+ registerHostLogPhones(new Map(), hostLog)
62
+ ```
63
+
64
+ 1. 装包。
65
+ 2. 调用建日志库工厂并传入插件标识(`pluginId` 必填,其余全有默认)。
66
+ 3. 用默认配置即跑:默认前缀 `wf` 下 5 个电话名、目录名、开关文件名、按天文件名与本仓现状一字不差。
67
+
68
+ 第二个插件把 `pluginId` 换成自己的标识即可:电话名自动加前缀隔离,目录与开关文件名默认派生为不同名字,不得共用同一目录。
69
+
70
+ 一次建库调用得到一个独立实例:内存队列、累计丢弃数、开关状态各自独立,互不串。
71
+
72
+ ## 4. 配置默认值派生(#558 冻结)
73
+
74
+ 必填只有插件标识 `pluginId`:只能用小写英文字母、数字、中横线,长度 1 到 32。收到大写直接报错,不做静默转小写;收到含点或文件不安全字符(斜杠、反斜杠、冒号、星号、问号、引号、尖括号、竖线、空格)的值同样报错,因为点留作前缀与动作名之间的分隔符,拼进目录名与文件名后必须仍是合法名字。
75
+
76
+ | 配置键 | 默认 | 说明 |
77
+ |---|---|---|
78
+ | `prefix` | 回退到 `pluginId`(当前插件两者都是 `wf`) | 电话名前缀;新电话名 = 前缀 + 点 + 动作名 |
79
+ | `logDirName` | 派生 | 标识为 `wf` 时为 `logs`,其他标识时为 `logs-标识`;允许显式覆盖 |
80
+ | `switchFileName` | 派生 | 标识为 `wf` 时为 `log-switch.json`,其他标识时为 `log-switch-标识.json`;允许显式覆盖 |
81
+ | `fileNamePolicy` | `daily` | 按天(年月日点 log,如 `2026-09-06.log`);四段式只作可选项 |
82
+ | `maxQueue` | `1000` | 宿主内存队列上限,满时按级别丢弃、只计数不抛错 |
83
+ | `eventList` | `null` | 事件清单注入点位(对象形式,见第 7 节;不传为 `null`,主路径零变化) |
84
+
85
+ 四段式文件名的四段定死为日期点插件标识点进程号点启动时间:插件名取配置里的插件标识,进程号取不到时回退 0,启动时间取宿主建日志库那一刻(以启动头写入的 `pid` 与 `startedAt` 为准,里面的冒号与斜杠转写为中横线后才拼入)。清空与导出里按文件名匹配的正则随策略分支:`daily` 走按天正则,四段式走对应的四段正则。
86
+
87
+ ## 5. 电话名、前缀隔离与客户端接入
88
+
89
+ ### 5.1 五个电话
90
+
91
+ 新电话名 = 前缀 + 点 + 动作名。默认前缀 `wf` 下 5 个字面与现状一字不差:
92
+
93
+ | 电话名(默认前缀下) | 方向 | 入参 | 回参 |
94
+ |---|---|---|---|
95
+ | `wf.logBatch` | 客户端调宿主,批量上报 | 日志条目数组(`entries`)加客户端累计丢弃数(`droppedCount`) | 是否成功(`ok`)加接收条数(`accepted`)加宿主侧累计丢弃数(`dropped`) |
96
+ | `wf.logExport` | 客户端调宿主,导出 | 可选日期(`date`),多余字段忽略 | 是否成功加文件名加长度(`bytes`,日志原文的字符串长度,不是真实字节数)加回退标记(`fallback`)加当天日志原文(`text`)加系统信息摘要(`summary`)加目录与路径(`dir`/`path`) |
97
+ | `wf.logClear` | 客户端调宿主,清空 | 日期或全部(`date` 为某天或 `all`) | 是否成功加删掉几个文件(`removed`) |
98
+ | `wf.logGetSwitch` | 客户端调宿主,读开关 | 空对象 `{}` | 是否成功加是否开启加采样率 |
99
+ | `wf.logSetSwitch` | 客户端调宿主,写开关 | 是否开启加采样率 | 是否成功加实际生效的是否开启(`ok` 与 `enabled`,不回采样率;客户端写成功后用请求时的采样率更新本地,不等宿主回) |
100
+
101
+ 前缀只隔离电话名,不隔离磁盘上的文件,磁盘隔离靠上面的目录名与开关文件名默认派生。宿主注册电话名时,若该名字已被注册,则报错、不覆盖旧的。默认前缀 `wf` 只给当前插件用,第二个插件必须显式配自己的前缀后才能注册。
102
+
103
+ 导出成功回 8 个键(含原文 `text`、摘要 `summary`、目录与路径 `dir`/`path`);导出失败只回 4 个键(`ok`、`fileName`、`bytes`、`fallback`),调用处走失败分支处理。
104
+
105
+ ### 5.2 客户端两种消费方式(二选一)
106
+
107
+ 两种方式行为一致,选一种即可,推荐直接 import。
108
+
109
+ 方式一,直接 import(推荐):调用方写 `import { createClientLog } from 'dsh-log/client'`,把四个依赖与插件配置传进来,当场得到日志器,打包工具正常解析 import。
110
+
111
+ ```js
112
+ import { createClientLog } from 'dsh-log/client'
113
+
114
+ const clientLog = createClientLog(
115
+ { host, timer, storage, broadcastLogSwitch },
116
+ { pluginId: 'wf' }
117
+ )
118
+ clientLog.log('info', 'my.event', { step: 'started' })
119
+ ```
120
+
121
+ 四个依赖全部由调用方传入(全可选,缺了走退化路,不抛错):宿主调用器 `host`(只用 `call` 一个方法,桥接写法见 INTEGRATION.md 步骤 3 的三行示例)、计时器 `timer`(只用 `timeout` 一个方法,没有就回退全局函数)、存储 `storage`(只用读写两个方法,没有就每次用默认)、开关广播 `broadcastLogSwitch`(一个无参函数,没有就不广播,不报错)。
122
+
123
+ 方式二,文本拼接(只给把客户端拼进插件主文件闭包一起运行的插件用):构建时取客户端入口编译后的声明体,去行首 `export` 后拼进插件主文件闭包,调用时把闭包里现成的四个名字原样传给工厂。文本拼接消费方式只走客户端入口的声明体文本。本仓当前插件本次不切拼接源(默认 `wf` 下行为零变化),拼接形态留给第二个插件验证。
124
+
125
+ 客户端批量转发口径(#558 冻结,复用 `CLIENT_BATCH` 常量,不另写一遍):每批最多 50 条、每 1000 毫秒发一次、单包约 128KB 或队列 100 条先到先截,裁掉的记入丢弃数。开关看门狗超时 5000 毫秒,只记一行告警,不改返回值。本地开关存在本地存储里,键名是 `dsws.debug`,形状是是否开启加采样率加版本号,默认关闭。建日志器时同步读本地做界面秒显,随后启动对账再向宿主看齐(以宿主为准)。
126
+
127
+ 日志器动作与宿主同名同参同语义:是否允许记(`isEnabled`)、记一行(`log`)、立刻转发或刷盘(`flush`,客户端侧只管转发,不管落盘;调转发后约 1 秒可见、不调约 2 秒、错误与告警直通,见 INTEGRATION.md 步骤 4)、读累计丢弃数(`getDroppedCount`)。
128
+
129
+ ## 6. 失败语义(#558 冻结)
130
+
131
+ 失败只计数不抛错:写盘失败、队列满、转发失败都只加到累计丢弃数里;所有电话失败都回是否成功为假的结构,不抛异常。两处例外原样保留,不扩大:
132
+
133
+ - 唯独记录电话(`logBatch`)的最外层例外回成功加接收 0 条(`ok` 为真、`accepted` 为 0)。
134
+ - 导出电话多余字段忽略(多传的字段不改变行为)。
135
+
136
+ 开关读写失败保持旧值,不回退为开启。客户端写开关失败原因只给机器码(`host-unavailable`、`host-rejected`、`switch-timeout`、`stale` 等),面向用户的文案由界面经多语言系统转换。
137
+
138
+ ## 7. 对象清单、模板与检查器(#561)
139
+
140
+ 每条事件四样东西:事件名、级别(`error`、`warn`、`info`、`debug`)、允许字段(之外的键一律不记)、脱敏引用(`codes` 是截断或散列代号,`rules` 是具名正则名,都是引用名,命中只记规则名不记原文)。`kind` 只为计数检查服务:`resident` 常驻(始终落盘的轻量轨迹)、`ondemand` 按需(只在调试开关打开时记)、`selfmon` 自监控(日志管道自己的故障行),三类实际条数须与清单自报的 `counts` 逐项核对。`guard` 可选,一句话写清采样或节流,无特殊守卫不写。
141
+
142
+ 空模板见包内的 `event-list.template.json`(模板里的 `pluginId` 换成自己插件的标识;只含一条事件的最小填好例子见 INTEGRATION.md 步骤 5)。调用方把清单拼成对象传给 `eventList`:
143
+
144
+ ```js
145
+ import { readFileSync } from 'node:fs'
146
+
147
+ const myEventList = JSON.parse(readFileSync('./event-list.my-plugin.json', 'utf8'))
148
+
149
+ const hostLog = createHostLog(
150
+ { fs, timer, getCacheDir, getPlatform, DEFAULT_CWD },
151
+ { pluginId: 'my-plugin', eventList: myEventList }
152
+ )
153
+ ```
154
+
155
+ 路径形式请调用方自己读成对象再传入,日志包不读盘(字符串直接传给 `eventList` 只存不解析,检查器收到字符串会报错并提示先读成对象)。对象形式的清单当场验形状,错了直接报错(含中文说明),不静默修补;数组形式同样被拦下(只收对象,数组多半是把事件表直接当成了清单)。
156
+
157
+ 检查器经宿主入口导出,都是纯函数,不新增日志事件:
158
+
159
+ ```js
160
+ import { parseEventListManifest, checkEventFields, checkEventCounts } from 'dsh-log/host'
161
+
162
+ const manifest = parseEventListManifest(myEventList)
163
+ checkEventFields(manifest, 'gh.exec', ['argv0', 'cwdHash'])
164
+ checkEventCounts(manifest)
165
+ ```
166
+
167
+ `parseEventListManifest` 验形状,`checkEventFields` 做字段白名单检查(未知事件名、未知字段键都算不通过,并把名单带回给调用方),`checkEventCounts` 做计数检查(增删事件必须同步改清单的 `counts`,否则这里变红)。本仓现有 55 事件对照仍以 `research/489-appendix.md` 与 `tests/verify-log-*.js` 为准,本包只给格式与检查器,不复刻那张表,免得两处对照要双写同步。
168
+
169
+ ## 8. 已知事项(#560 带走的两条 P1,本包首版行为)
170
+
171
+ 1. 导出失败行读全局散列,是四个注入依赖之外的第 5 个隐式依赖。`logExportFail` 在包内没有共享闭包可用时,只认挂在全局对象上的同名函数(`dswsLogHash` 与 `dswsLogTrunc`,测试桩走这条):全局上有就用它们做截断与散列,没有就回退包内自带的散列函数,行为一致。收敛计划:如果调用方闭包里有自己的散列与截断函数且希望输出与旧模块一字相同,把它们挂到全局对象同名位置即可;不需要一字相同时什么都不用做。
172
+ 2. 存储双键过渡期以 `storage` 为准。建日志器收 `storage` 与旧名 `localStorage` 两个键(迁移期兼容):只传一个就用它,两个都传以 `storage` 为准。新接入的插件只传 `storage`。收敛时间表待定,在此之前双键行为保持本节所述不变。
173
+
174
+ ## 9. 发布前 build 与门禁跑法
175
+
176
+ 发布前按顺序两步(演练只跑 dry-run,不真发):
177
+
178
+ ```sh
179
+ node packages/dsh-log/build.mjs
180
+ cd packages/dsh-log && npm publish --dry-run
181
+ ```
182
+
183
+ 编译产物在 `dist` 下(6 个 JS:`client.js`、`config.js`、`host.js`、`node.js`、`phones.js`、`store.js`),本地生成、不入库。发布白名单(`files`)共 11 个文件:`dist` 下 6 个 JS、`event-list.template.json`、`INTEGRATION.md`、`README.md`、`LICENSE`、`package.json`。加新文件进包时同步改 `files` 并重跑 dry-run 确认文件数。
184
+
185
+ 门禁跑法(改包后全跑,退出码全 0 才算过):
186
+
187
+ ```sh
188
+ node --test packages/dsh-log/tests/host.test.mjs packages/dsh-log/tests/eventList.test.mjs packages/dsh-log/tests/client.test.mjs packages/dsh-log/tests/node.test.mjs
189
+ node tests/verify-log-artifacts.js
190
+ node tests/verify-log-channel.js
191
+ node tests/verify-log-client.js
192
+ node tests/verify-log-flush.js
193
+ node tests/verify-log-guards.js
194
+ node tests/verify-log-scrub.js
195
+ node tests/verify-log-selfmon.js
196
+ node tests/verify-log-truncate.js
197
+ node tests/verify-log-switch-526.js
198
+ node tests/verify-log-store.js
199
+ node tests/verify-log-statusbar.js
200
+ node tests/verify-log-coverage.js
201
+ node tests/verify-log-count.js
202
+ node tests/verify-log-fields.js
203
+ ```
204
+
205
+ 包内四套单测共 45 项(宿主引擎、客户端引擎、事件清单、Node 程序入口),14 个日志门禁全绿,且 55 事件(常驻 30、按需 20、自监控 5)不变。有 TypeScript 环境时另跑包内类型检查(`tsc -p packages/dsh-log/tsconfig.json`)。未新增日志事件时,附录第 1 章对照表不用动。
206
+
207
+ 包还没发到官方源(`npm view dsh-log` 返回 404,名字未被占用)。发布前除了上面两步,还要按第 10 节的版本策略定版本号,并把发布演练记录(包内 `notes-publish-drill.md`)重跑到与本次发布物一致的文件数。
208
+
209
+ ## 10. 版本策略
210
+
211
+ 语义化版本。破坏性变更有 5 类:电话改名、增删改入参回参形状、改配置写法、改事件清单字段形状、改文件名策略的默认形状。自首个公开发布起算,日志系统的公共接口连续 3 个版本无破坏性变更之前不议拆。
212
+
213
+ ## 11. 安全声明与许可证
214
+
215
+ 用包的人发现安全漏洞,请走本仓 GitHub Issues 报告(标题注明安全字样),紧急情况可直接联系维护人。MIT 许可证,版权人王辰浩,与仓库根 LICENSE 一致。