
Security News
OpenCode User Hit with Hostile Backlash After Reporting Free AI Loophole
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.
把本仓日志系统装成的 npm 包:DSH 插件与独立跑的 Node 程序都能照着它集成使用。本包由三块合成——宿主落盘引擎(#559)、客户端转发引擎(#560)、事件清单格式与通用检查器(#561),是地图 #556 的产物。
全程用词:日志系统指日志功能本身(引擎、对外接口、检查器、文档);日志包指装着日志系统的这个 npm 包。电话指宿主对外提供的方法;落盘指宿主统一写本地文件的动作。
| 你的情况 | 从哪儿开始 |
|---|---|
| 写 DSH 插件,要给插件接日志 | 包内 INTEGRATION.md 路线 A:步骤 0 到 4 一路到底 |
| 写独立 Node 程序(不是插件),要往本地写日志 | 包内 INTEGRATION.md 路线 B:步骤 0、1、2B,再看步骤 4 的落点说明 |
| 已经装好,要查接口形状(电话、配置、回参) | 本文第 4 到 7 节 |
| 改了本包要发版 | 本文第 9 节;发布记录与向导在仓库的 packages/dsh-log/notes-publish-drill.md(不随包发布) |
npm install dsh-log
要求 Node 22 或更高。当前版本 0.2.1,已发布到 npm 官方源(标签 latest;可用 npm view dsh-log version 自查)。
dsh-log/host:DSH 插件宿主侧用。建日志库、拼电话名、注册电话名、事件清单检查器。dsh-log/client:DSH 插件客户端侧用。建日志器、电话名拼法、批量转发口径数字。dsh-log/node:不是插件的独立 Node 程序用(例如一个技能自己的命令行)。一句话建好落盘日志,不必自己拼文件服务与计时器。三个入口在同一个包里,装一次全都有。
dsh-log/node 与前两个的区别:它不碰电话层、也没有客户端转发,只有「把日志落到本地文件」以及它背后的级别规则、目录派生与失败计数。它不决定日志写到哪——目录由调用方传入,入口自己不读任何环境变量、也不带默认路径。
本包不做什么(需要这三样得在调用方自己那层做):不自动上报日志,只做本地落盘加手动导出;不自动轮转,也不自动清理旧文件。
dsh-log/node)import { createNodeHostLog } from 'dsh-log/node'
const log = await createNodeHostLog(
{ cacheDir: '/你自己决定的/日志目录' },
{ pluginId: 'your-program' }
)
log.store.log('warn', 'your.step.fail', { step: 'fetch', reason: 'timeout' })
await log.ready // 等这一下:程序退出前日志才真的落盘
落点由 cacheDir 与标识一起决定:日志写在 <cacheDir>/logs-your-program/2026-09-11.log,调试开关写在 <cacheDir>/log-switch-your-program.json。目录不存在时入口自己建。
三条要知道的事:
await log.ready 不能省。落盘是先入内存队列、再由约 1000 毫秒的防抖窗口异步写下去的:程序顺着事件循环自然跑完,Node 会等这个定时器,日志自己会落;但结尾调了 process.exit()、抛错退出或被强杀的程序,队列里还没写下去的那批就随进程消失(warn 与 info 一样会丢)。命令行工具通常在结尾显式退出,所以这条按必须处理。log.ready 返回时表示「前面写的每一行都已经在文件里」。log.store.handleLogSetSwitch({ enabled: true, sampleRate: 1 })。import { createHostLog, registerHostLogPhones } from 'dsh-log/host'
// 五个运行依赖全部由调用方传入,包内不自己抓:
// fs 文件服务、timer 计时器、getCacheDir 取缓存目录函数、
// getPlatform 取平台函数、DEFAULT_CWD 默认工作目录。
// 每个依赖的最小形状与可省略项见 INTEGRATION.md 步骤 2 的形状表。
const hostLog = createHostLog(
{ fs, timer, getCacheDir, getPlatform, DEFAULT_CWD },
{ pluginId: 'wf' }
)
registerHostLogPhones(new Map(), hostLog)
pluginId 必填,其余全有默认)。wf 下 5 个电话名、目录名、开关文件名、按天文件名与本仓现状一字不差。第二个插件把 pluginId 换成自己的标识即可:电话名自动加前缀隔离,目录与开关文件名默认派生为不同名字,不得共用同一目录。
一次建库调用得到一个独立实例:内存队列、累计丢弃数、开关状态各自独立,互不串。
必填只有插件标识 pluginId:只能用小写英文字母、数字、中横线,长度 1 到 32。收到大写直接报错,不做静默转小写;收到含点或文件不安全字符(斜杠、反斜杠、冒号、星号、问号、引号、尖括号、竖线、空格)的值同样报错,因为点留作前缀与动作名之间的分隔符,拼进目录名与文件名后必须仍是合法名字。
| 配置键 | 默认 | 说明 |
|---|---|---|
prefix | 回退到 pluginId(当前插件两者都是 wf) | 电话名前缀;新电话名 = 前缀 + 点 + 动作名 |
logDirName | 派生 | 标识为 wf 时为 logs,其他标识时为 logs-标识;允许显式覆盖 |
switchFileName | 派生 | 标识为 wf 时为 log-switch.json,其他标识时为 log-switch-标识.json;允许显式覆盖 |
fileNamePolicy | daily | 按天(年月日点 log,如 2026-09-06.log);四段式只作可选项 |
maxQueue | 1000 | 宿主内存队列上限,满时按级别丢弃、只计数不抛错 |
eventList | null | 事件清单注入点位(对象形式,见第 7 节;不传为 null,主路径零变化) |
四段式文件名的四段定死为日期点插件标识点进程号点启动时间:插件名取配置里的插件标识,进程号取不到时回退 0,启动时间取宿主建日志库那一刻(以启动头写入的 pid 与 startedAt 为准,里面的冒号与斜杠转写为中横线后才拼入)。清空与导出里按文件名匹配的正则随策略分支:daily 走按天正则,四段式走对应的四段正则。
新电话名 = 前缀 + 点 + 动作名。默认前缀 wf 下 5 个字面与现状一字不差:
| 电话名(默认前缀下) | 方向 | 入参 | 回参 |
|---|---|---|---|
wf.logBatch | 客户端调宿主,批量上报 | 日志条目数组(entries)加客户端累计丢弃数(droppedCount) | 是否成功(ok)加接收条数(accepted)加宿主侧累计丢弃数(dropped) |
wf.logExport | 客户端调宿主,导出 | 可选日期(date),多余字段忽略 | 是否成功加文件名加长度(bytes,日志原文的字符串长度,不是真实字节数)加回退标记(fallback)加当天日志原文(text)加系统信息摘要(summary)加目录与路径(dir/path) |
wf.logClear | 客户端调宿主,清空 | 日期或全部(date 为某天或 all) | 是否成功加删掉几个文件(removed) |
wf.logGetSwitch | 客户端调宿主,读开关 | 空对象 {} | 是否成功加是否开启加采样率 |
wf.logSetSwitch | 客户端调宿主,写开关 | 是否开启加采样率 | 是否成功加实际生效的是否开启(ok 与 enabled,不回采样率;客户端写成功后用请求时的采样率更新本地,不等宿主回) |
前缀只隔离电话名,不隔离磁盘上的文件,磁盘隔离靠上面的目录名与开关文件名默认派生。宿主注册电话名时,若该名字已被注册,则报错、不覆盖旧的。默认前缀 wf 只给当前插件用,第二个插件必须显式配自己的前缀后才能注册。
导出成功回 8 个键(含原文 text、摘要 summary、目录与路径 dir/path);导出失败只回 4 个键(ok、fileName、bytes、fallback),调用处走失败分支处理。
两种方式行为一致,选一种即可,推荐直接 import。
方式一,直接 import(推荐):调用方写 import { createClientLog } from 'dsh-log/client',把四个依赖与插件配置传进来,当场得到日志器,打包工具正常解析 import。
import { createClientLog } from 'dsh-log/client'
const clientLog = createClientLog(
{ host, timer, storage, broadcastLogSwitch },
{ pluginId: 'wf' }
)
clientLog.log('info', 'my.event', { step: 'started' })
四个依赖全部由调用方传入(全可选,缺了走退化路,不抛错):宿主调用器 host(只用 call 一个方法,桥接写法见 INTEGRATION.md 步骤 3 的三行示例)、计时器 timer(只用 timeout 一个方法,没有就回退全局函数)、存储 storage(只用读写两个方法,没有就每次用默认)、开关广播 broadcastLogSwitch(一个无参函数,没有就不广播,不报错)。
方式二,文本拼接(只给把客户端拼进插件主文件闭包一起运行的插件用):构建时取客户端入口编译后的声明体,去行首 export 后拼进插件主文件闭包,调用时把闭包里现成的四个名字原样传给工厂。文本拼接消费方式只走客户端入口的声明体文本。本仓当前插件本次不切拼接源(默认 wf 下行为零变化),拼接形态留给第二个插件验证。
客户端批量转发口径(#558 冻结,复用 CLIENT_BATCH 常量,不另写一遍):每批最多 50 条、每 1000 毫秒发一次、单包约 128KB 或队列 100 条先到先截,裁掉的记入丢弃数。开关看门狗超时 5000 毫秒,只记一行告警,不改返回值。本地开关存在本地存储里,键名是 dsws.debug,形状是是否开启加采样率加版本号,默认关闭。建日志器时同步读本地做界面秒显,随后启动对账再向宿主看齐(以宿主为准)。
日志器的调用面(createClientLog 的返回对象)里,接入要用的是下面两类;对象上还有队列、开关状态等内部字段,调用处可以直接读,改开关请走第二类里的两个方法:
isEnabled)、记一行(log)、立刻转发或刷盘(flush,客户端侧只管转发,不管落盘;调转发后约 1 秒可见、不调约 2 秒、错误与告警直通,见 INTEGRATION.md 步骤 4)、读累计丢弃数(getDroppedCount)。setLogSwitch(是否开启, 采样率) 写开关(写成功后用请求时的采样率更新本地,不等宿主回;失败原因只给机器码),reconcileLogSwitch() 启动时向宿主对账(以宿主为准)。用法见 INTEGRATION.md 步骤 4 与步骤 6。失败只计数不抛错:写盘失败、队列满、转发失败都只加到累计丢弃数里;所有电话失败都回是否成功为假的结构,不抛异常。两处例外原样保留,不扩大:
logBatch)的最外层例外回成功加接收 0 条(ok 为真、accepted 为 0)。开关读写失败保持旧值,不回退为开启。客户端写开关失败原因只给机器码(host-unavailable、host-rejected、switch-timeout、stale 等),面向用户的文案由界面经多语言系统转换。
每条事件四样东西:事件名、级别(error、warn、info、debug)、允许字段(之外的键一律不记)、脱敏引用(codes 是截断或散列代号,rules 是具名正则名,都是引用名,命中只记规则名不记原文)。kind 只为计数检查服务:resident 常驻(始终落盘的轻量轨迹)、ondemand 按需(只在调试开关打开时记)、selfmon 自监控(日志管道自己的故障行),三类实际条数须与清单自报的 counts 逐项核对。guard 可选,一句话写清采样或节流,无特殊守卫不写。
空模板见包内的 event-list.template.json(模板里的 pluginId 换成自己插件的标识;只含一条事件的最小填好例子见 INTEGRATION.md 步骤 5)。调用方把清单拼成对象传给 eventList:
import { readFileSync } from 'node:fs'
const myEventList = JSON.parse(readFileSync('./event-list.my-plugin.json', 'utf8'))
const hostLog = createHostLog(
{ fs, timer, getCacheDir, getPlatform, DEFAULT_CWD },
{ pluginId: 'my-plugin', eventList: myEventList }
)
路径形式请调用方自己读成对象再传入,日志包不读盘(字符串直接传给 eventList 只存不解析,检查器收到字符串会报错并提示先读成对象)。对象形式的清单当场验形状,错了直接报错(含中文说明),不静默修补;数组形式同样被拦下(只收对象,数组多半是把事件表直接当成了清单)。
检查器经宿主入口导出,都是纯函数,不新增日志事件:
import { parseEventListManifest, checkEventFields, checkEventCounts } from 'dsh-log/host'
const manifest = parseEventListManifest(myEventList)
checkEventFields(manifest, 'gh.exec', ['argv0', 'cwdHash'])
checkEventCounts(manifest)
parseEventListManifest 验形状,checkEventFields 做字段白名单检查(未知事件名、未知字段键都算不通过,并把名单带回给调用方),checkEventCounts 做计数检查(增删事件必须同步改清单的 counts,否则这里变红)。本仓现行的事件对照表以 research/489-appendix.md 第 1 章与 tests/verify-log-*.js 为准,本包只给格式与检查器,不复刻那张表,免得两处对照要双写同步;条数会随票增删,查之前先看该附录,别在文档里抄一个数字。
logExportFail 在包内没有共享闭包可用时,只认挂在全局对象上的同名函数(dswsLogHash 与 dswsLogTrunc,测试桩走这条):全局上有就用它们做截断与散列,没有就回退包内自带的散列函数,行为一致。收敛计划:如果调用方闭包里有自己的散列与截断函数且希望输出与旧模块一字相同,把它们挂到全局对象同名位置即可;不需要一字相同时什么都不用做。storage 为准。建日志器收 storage 与旧名 localStorage 两个键(迁移期兼容):只传一个就用它,两个都传以 storage 为准。新接入的插件只传 storage。收敛时间表待定,在此之前双键行为保持本节所述不变。发布前按顺序两步(演练只跑 dry-run,不真发):
node packages/dsh-log/build.mjs
cd packages/dsh-log && npm publish --dry-run
编译产物在 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 确认文件数。
门禁跑法(改包后全跑,退出码全 0 才算过):
npm --prefix packages/dsh-log test
node tests/verify-log-artifacts.js
node tests/verify-log-channel.js
node tests/verify-log-client.js
node tests/verify-log-flush.js
node tests/verify-log-guards.js
node tests/verify-log-scrub.js
node tests/verify-log-selfmon.js
node tests/verify-log-truncate.js
node tests/verify-log-switch-526.js
node tests/verify-log-store.js
node tests/verify-log-statusbar.js
node tests/verify-log-coverage.js
node tests/verify-log-count.js
node tests/verify-log-fields.js
第一条由 package.json 的 test 脚本给出用例清单(宿主引擎、客户端引擎、事件清单、Node 程序入口四套),后 14 条是日志门禁;事件条数由 tests/verify-log-count.js 与 research/489-appendix.md 第 1 章对账,改了事件就得两处同步。有 TypeScript 环境时另跑 npm --prefix packages/dsh-log run typecheck。
包已发布到官方源(最新版本与标签用 npm view dsh-log version 与 npm view dsh-log dist-tags 自查)。再发新版本时,除了上面两步,还要按第 10 节的版本策略定版本号、把实际发布输出记进发布记录(仓库的 packages/dsh-log/notes-publish-drill.md),或用包目录下的发布向导脚本 publish-wizard.sh 走一遍完整流程(体检、登录、升版本、干跑、发布、发布后验证六段)。发布后有一件事向导不代做、但必须自己做:核对包内 README.md 与 INTEGRATION.md 跟仓库一致——发出去的是发布那一刻的文档,仓库里改的不会自动跟上。
语义化版本。破坏性变更有 5 类:电话改名、增删改入参回参形状、改配置写法、改事件清单字段形状、改文件名策略的默认形状。自首个公开发布起算,日志系统的公共接口连续 3 个版本无破坏性变更之前不议拆。
用包的人发现安全漏洞,请走本仓 GitHub Issues 报告(标题注明安全字样),紧急情况可直接联系维护人。MIT 许可证,版权人王辰浩,与仓库根 LICENSE 一致。
FAQs
可复用的日志系统:宿主落盘引擎 + 读侧工具(五键行、存时硬隔离查时授权聚合、error 永不丢;0.3.1 起带 formatRow 与人话命令行 dsh-log tail,0.3.3 起带宿主链上下文 store.trace()、目录自述与 dsh-log export,0.3.4 起目录摘要如实标出读不出来的文件与缺的格子;0.4.1 起宿主可传 switchSource 把开关接到自己的配置文件里)
The npm package dsh-log receives a total of 0 weekly downloads. As such, dsh-log popularity was classified as not popular.
We found that dsh-log demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.