dsh-bridge-gateway 0.2.2 → 0.3.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/CHANGELOG.md +74 -0
- package/README.md +17 -3
- package/README.zh.md +18 -2
- package/assets/apple-touch-icon-180.png +0 -0
- package/assets/icon-192.png +0 -0
- package/assets/icon-512.png +0 -0
- package/assets/icon-maskable-192.png +0 -0
- package/assets/icon-maskable-512.png +0 -0
- package/client/client.js +302 -41
- package/client/index.js +438 -42
- package/docs/banner.jpg +0 -0
- package/docs/custom-tunnel.md +5 -5
- package/docs/platform-abstraction-design.md +1 -1
- package/docs/qq-usage.md +6 -6
- package/docs/screenshots/remote-auth-login-light.jpg +0 -0
- package/docs/screenshots/remote-auth-login.jpg +0 -0
- package/docs/wechat-usage.md +1 -1
- package/lib/auth/brand-assets.js +131 -0
- package/lib/auth/login-template.js +503 -361
- package/lib/auth/manager.js +47 -12
- package/lib/auth/theme-tokens.js +142 -0
- package/lib/bridge-rpc.js +2 -2
- package/lib/cloudflared-manager.mjs +1 -1
- package/lib/feishu/gateway.js +17 -17
- package/lib/feishu/index.js +3 -3
- package/lib/feishu/lark-bundled.mjs +288 -469
- package/lib/feishu/node.js +7 -7
- package/lib/gateway/cert.js +1 -1
- package/lib/index.js +210 -80
- package/lib/paths.js +43 -14
- package/lib/platform/base.js +2 -2
- package/lib/platform/conversation-bridge.js +17 -17
- package/lib/platform/index.js +1 -1
- package/lib/platform/manager.js +4 -4
- package/lib/qq/gateway.js +10 -10
- package/lib/qq/index.js +11 -11
- package/lib/qq/node.js +17 -17
- package/lib/telegram/gateway.js +10 -10
- package/lib/telegram/index.js +3 -3
- package/lib/telegram/node.js +5 -5
- package/lib/wechat/gateway.js +6 -6
- package/lib/wechat/index.js +4 -4
- package/lib/wechat/media.js +1 -1
- package/lib/wechat/node.js +13 -13
- package/package.json +6 -3
package/lib/telegram/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge Telegram platform adapter
|
|
1
|
+
// dsh-bridge-gateway Telegram platform adapter
|
|
2
2
|
// 编排 TelegramGateway(长轮询 + 代理支持)+ TelegramConversationNode(Telegram⇄DSH 会话桥)。
|
|
3
3
|
// 作为 Platform 子类,注册进 PlatformManager 统一管理。
|
|
4
4
|
|
|
@@ -45,7 +45,7 @@ export class TelegramService extends Platform {
|
|
|
45
45
|
|
|
46
46
|
if (this.gateway.configured) {
|
|
47
47
|
void this.start().catch((err) => {
|
|
48
|
-
this.logger.error?.('[dsh-bridge telegram] start failed:', err?.message ?? err)
|
|
48
|
+
this.logger.error?.('[dsh-bridge-gateway telegram] start failed:', err?.message ?? err)
|
|
49
49
|
})
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -203,7 +203,7 @@ export class TelegramService extends Platform {
|
|
|
203
203
|
|
|
204
204
|
if (this.gateway.configured && (botToken !== undefined || proxy !== undefined)) {
|
|
205
205
|
await this.start().catch((err) => {
|
|
206
|
-
this.logger?.warn?.('[dsh-bridge telegram] auto-start failed: %s', err?.message ?? err)
|
|
206
|
+
this.logger?.warn?.('[dsh-bridge-gateway telegram] auto-start failed: %s', err?.message ?? err)
|
|
207
207
|
})
|
|
208
208
|
}
|
|
209
209
|
return { success: true }
|
package/lib/telegram/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge Telegram conversation node
|
|
1
|
+
// dsh-bridge-gateway Telegram conversation node
|
|
2
2
|
// 把 Telegram OpenAPI / 长轮询入站事件解析后交给平台无关的
|
|
3
3
|
// ConversationBridge 处理,出站通过 TelegramGateway 发送文本 / Inline Keyboard 按钮 / 媒体。
|
|
4
4
|
|
|
@@ -107,7 +107,7 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (!text && mediaFiles.length === 0) {
|
|
110
|
-
this.logger.debug?.(`[dsh-bridge telegram] ignore empty message from ${chatId}`)
|
|
110
|
+
this.logger.debug?.(`[dsh-bridge-gateway telegram] ignore empty message from ${chatId}`)
|
|
111
111
|
return
|
|
112
112
|
}
|
|
113
113
|
|
|
@@ -117,7 +117,7 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
117
117
|
fullText = fullText ? `${fullText}\n\n${mediaDesc}` : mediaDesc
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
this.logger.debug?.(`[dsh-bridge telegram] handling inbound message from ${chatId} (group=${isGroup}): ${fullText}`)
|
|
120
|
+
this.logger.debug?.(`[dsh-bridge-gateway telegram] handling inbound message from ${chatId} (group=${isGroup}): ${fullText}`)
|
|
121
121
|
|
|
122
122
|
// 如果还没有活动会话,且是首条消息,自动尝试选择最新已有会话
|
|
123
123
|
if (!this.activeSessionId) {
|
|
@@ -160,7 +160,7 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
160
160
|
if (res) downloaded.push(res)
|
|
161
161
|
}
|
|
162
162
|
} catch (err) {
|
|
163
|
-
this.logger.warn?.(`[dsh-bridge telegram] process media error: ${err.message}`)
|
|
163
|
+
this.logger.warn?.(`[dsh-bridge-gateway telegram] process media error: ${err.message}`)
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
return downloaded
|
|
@@ -256,7 +256,7 @@ export class TelegramConversationNode extends ConversationBridge {
|
|
|
256
256
|
this._streamMsgId = res?.message_id || null
|
|
257
257
|
this._streamContent = firstSlice
|
|
258
258
|
} catch (err) {
|
|
259
|
-
this.logger.warn?.('[dsh-bridge telegram] sendText stream initial failed:', err?.message ?? err)
|
|
259
|
+
this.logger.warn?.('[dsh-bridge-gateway telegram] sendText stream initial failed:', err?.message ?? err)
|
|
260
260
|
await this.gateway.sendText(peerId, content).catch(() => {})
|
|
261
261
|
return
|
|
262
262
|
}
|
package/lib/wechat/gateway.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge WeChat iLink gateway
|
|
1
|
+
// dsh-bridge-gateway WeChat iLink gateway
|
|
2
2
|
//
|
|
3
3
|
// 微信 ClawBot(iLink Bot API)网关:扫码登录 + 长轮询收消息 + 发送 + typing。
|
|
4
4
|
// 由 Jesse-njx/dsh-chatnode-wechat(MIT)移植精简而来,协议细节与 hermes-agent
|
|
@@ -448,7 +448,7 @@ export class WechatGateway extends Service {
|
|
|
448
448
|
if (!text.trim()) return { success: false, error: 'empty message' }
|
|
449
449
|
if (!this.configured) return { success: false, error: 'not configured' }
|
|
450
450
|
let contextToken = this.contextTokens.get(to)
|
|
451
|
-
const id = clientId ?? `dsh-bridge-wechat-${randomId()}`
|
|
451
|
+
const id = clientId ?? `dsh-bridge-gateway-wechat-${randomId()}`
|
|
452
452
|
let lastError
|
|
453
453
|
let retriedWithoutToken = false
|
|
454
454
|
|
|
@@ -575,7 +575,7 @@ export class WechatGateway extends Service {
|
|
|
575
575
|
}) {
|
|
576
576
|
if (!this.configured) return { success: false, error: 'not configured' }
|
|
577
577
|
const contextToken = this.contextTokens.get(to)
|
|
578
|
-
const id = clientId ?? `dsh-bridge-wechat-${randomId()}`
|
|
578
|
+
const id = clientId ?? `dsh-bridge-gateway-wechat-${randomId()}`
|
|
579
579
|
const hexKey = aesKeyHex || (aesKeyBase64 ? Buffer.from(aesKeyBase64, 'base64').toString('hex') : '')
|
|
580
580
|
|
|
581
581
|
// 构建媒体项(全字段兼容各端微信客户端解析)
|
|
@@ -724,7 +724,7 @@ export class WechatGateway extends Service {
|
|
|
724
724
|
rawFileMd5,
|
|
725
725
|
})
|
|
726
726
|
} catch (err) {
|
|
727
|
-
this.logger?.warn?.('[dsh-bridge wechat] sendMediaFile failed: %s', err?.message ?? err)
|
|
727
|
+
this.logger?.warn?.('[dsh-bridge-gateway wechat] sendMediaFile failed: %s', err?.message ?? err)
|
|
728
728
|
return { success: false, error: err?.message }
|
|
729
729
|
}
|
|
730
730
|
}
|
|
@@ -844,7 +844,7 @@ export class WechatGateway extends Service {
|
|
|
844
844
|
consecutiveFailures = 0
|
|
845
845
|
if (batch.syncBuf) this.syncBuf = batch.syncBuf
|
|
846
846
|
if (this.statusValue !== 'connected') {
|
|
847
|
-
this.logger?.info?.('[dsh-bridge wechat] connected to iLink platform')
|
|
847
|
+
this.logger?.info?.('[dsh-bridge-gateway wechat] connected to iLink platform')
|
|
848
848
|
}
|
|
849
849
|
if (this.stopPollingLocal) break
|
|
850
850
|
this.setStatus('connected')
|
|
@@ -859,7 +859,7 @@ export class WechatGateway extends Service {
|
|
|
859
859
|
// iLink 独占锁:同 token 已有别的 poller。响亮报错并停止。
|
|
860
860
|
this.setStatus('error')
|
|
861
861
|
this.ctx.emit('wechat/fatal', new Error(
|
|
862
|
-
'iLink returned HTTP 403: another poller (hermes-agent, OpenClaw, or a duplicate dsh-bridge WeChat bot) is already polling this account. ' +
|
|
862
|
+
'iLink returned HTTP 403: another poller (hermes-agent, OpenClaw, or a duplicate dsh-bridge-gateway WeChat bot) is already polling this account. ' +
|
|
863
863
|
'iLink allows exactly one authenticated poller per token. Stop the other gateway or use a dedicated WeChat account.',
|
|
864
864
|
))
|
|
865
865
|
fatal = true
|
package/lib/wechat/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge WeChat platform adapter
|
|
1
|
+
// dsh-bridge-gateway WeChat platform adapter
|
|
2
2
|
//
|
|
3
3
|
// 编排 WechatGateway(iLink 网关,注册为 ctx.wechat 服务)+ WechatConversationNode
|
|
4
4
|
// (微信⇄DSH 会话桥)。作为 Platform 子类,可注册进 PlatformManager 统一管理。
|
|
@@ -52,7 +52,7 @@ export class WechatService extends Platform {
|
|
|
52
52
|
|
|
53
53
|
if (this.gateway.configured) {
|
|
54
54
|
void this.start().catch((err) => {
|
|
55
|
-
this.logger.error('[dsh-bridge wechat] start failed: %s', err?.message ?? err)
|
|
55
|
+
this.logger.error('[dsh-bridge-gateway wechat] start failed: %s', err?.message ?? err)
|
|
56
56
|
})
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -203,7 +203,7 @@ export class WechatService extends Platform {
|
|
|
203
203
|
baseUrl: creds.baseUrl,
|
|
204
204
|
})
|
|
205
205
|
void this.gateway.start().catch((err) => {
|
|
206
|
-
this.logger.error('[dsh-bridge wechat] login start failed: %s', err?.message ?? err)
|
|
206
|
+
this.logger.error('[dsh-bridge-gateway wechat] login start failed: %s', err?.message ?? err)
|
|
207
207
|
})
|
|
208
208
|
this.loginState = { phase: 'done', qrPayload: null, qrKind: null, error: null }
|
|
209
209
|
}
|
|
@@ -213,7 +213,7 @@ export class WechatService extends Platform {
|
|
|
213
213
|
await this.gateway.stop()
|
|
214
214
|
this.gateway.setCredentials({ token: '', accountId: '', baseUrl: undefined })
|
|
215
215
|
await this.persist({ token: '', accountId: '', baseUrl: '' })
|
|
216
|
-
this.logger.info('[dsh-bridge wechat] unbound: credentials cleared')
|
|
216
|
+
this.logger.info('[dsh-bridge-gateway wechat] unbound: credentials cleared')
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/** 更新白名单并发起持久化。 */
|
package/lib/wechat/media.js
CHANGED
package/lib/wechat/node.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-bridge WeChat conversation node
|
|
1
|
+
// dsh-bridge-gateway WeChat conversation node
|
|
2
2
|
//
|
|
3
3
|
// 微信 ⇄ DSH 会话桥:把 iLink 入站消息解析后交给平台无关的 ConversationBridge 处理,
|
|
4
4
|
// 出站通过 ctx.wechat 发送。负责微信协议相关的部分:
|
|
@@ -74,17 +74,17 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
74
74
|
// 调试:记录收到的消息类型
|
|
75
75
|
const items = message.item_list ?? []
|
|
76
76
|
const itemTypes = items.map((it) => it.type).join(',')
|
|
77
|
-
this.logger?.info?.(`[dsh-bridge wechat] received message from ${sender}, item_types=[${itemTypes}]`)
|
|
77
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] received message from ${sender}, item_types=[${itemTypes}]`)
|
|
78
78
|
|
|
79
79
|
// 快速预检查:白名单非空时,未授权发件人直接忽略(不下载媒体,防资源滥用)。
|
|
80
80
|
// 白名单为空的"首条自动授权"场景交给 handleInbound 处理。
|
|
81
81
|
if (!this.isAllowed(sender) && this.config.allowFrom.length > 0) {
|
|
82
|
-
this.logger?.info?.(`[dsh-bridge wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
|
|
82
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] ignore message from non-allowlisted sender ${sender} (never fed to model)`)
|
|
83
83
|
return
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// v0.2: 处理媒体项(图片/文件/语音/视频)
|
|
87
|
-
this.logger?.info?.(`[dsh-bridge wechat] processing media items for ${sender}...`)
|
|
87
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] processing media items for ${sender}...`)
|
|
88
88
|
let mediaFiles = []
|
|
89
89
|
let hadMediaItems = false
|
|
90
90
|
let mediaError = null
|
|
@@ -94,11 +94,11 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
94
94
|
const result = await this._processMediaItems(message, sender)
|
|
95
95
|
mediaFiles = result.files
|
|
96
96
|
debugInfo = result.debug
|
|
97
|
-
this.logger?.info?.(`[dsh-bridge wechat] media processing done, got ${mediaFiles.length} files`)
|
|
97
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] media processing done, got ${mediaFiles.length} files`)
|
|
98
98
|
} catch (error) {
|
|
99
99
|
// 尽可能捕获完整错误信息
|
|
100
100
|
mediaError = error?.message || error?.toString?.() || JSON.stringify(error) || '未知错误'
|
|
101
|
-
this.logger?.error?.(`[dsh-bridge wechat] media processing failed: ${mediaError}`, error)
|
|
101
|
+
this.logger?.error?.(`[dsh-bridge-gateway wechat] media processing failed: ${mediaError}`, error)
|
|
102
102
|
// 媒体处理失败不阻断文本消息处理
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -113,7 +113,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
113
113
|
await this.sendText(`❌ **${errMsg}**`)
|
|
114
114
|
return
|
|
115
115
|
}
|
|
116
|
-
this.logger?.info?.(`[dsh-bridge wechat] ignore empty message from ${sender}`)
|
|
116
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] ignore empty message from ${sender}`)
|
|
117
117
|
return
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -144,7 +144,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
144
144
|
const img = item.image_item
|
|
145
145
|
if (!img) {
|
|
146
146
|
const msg = `图片项缺少 image_item`
|
|
147
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
147
|
+
this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
|
|
148
148
|
debugInfo.push(msg)
|
|
149
149
|
continue
|
|
150
150
|
}
|
|
@@ -153,7 +153,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
153
153
|
if (!aesKey) {
|
|
154
154
|
const fields = Object.keys(img).join(', ')
|
|
155
155
|
const msg = `图片缺 aes_key,字段: ${fields}`
|
|
156
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
156
|
+
this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
|
|
157
157
|
debugInfo.push(msg)
|
|
158
158
|
continue
|
|
159
159
|
}
|
|
@@ -261,7 +261,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
261
261
|
}
|
|
262
262
|
} catch (error) {
|
|
263
263
|
const msg = `处理媒体项 type=${item.type} 失败: ${error?.message ?? error}`
|
|
264
|
-
this.logger?.warn?.(`[dsh-bridge wechat] ${msg}`)
|
|
264
|
+
this.logger?.warn?.(`[dsh-bridge-gateway wechat] ${msg}`)
|
|
265
265
|
debugInfo.push(msg)
|
|
266
266
|
}
|
|
267
267
|
}
|
|
@@ -276,7 +276,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
276
276
|
async _downloadMediaItem({ encryptedQueryParam, fullUrl, aesKeyBase64, sender, mediaType, filename }) {
|
|
277
277
|
if (!encryptedQueryParam && !fullUrl) return null
|
|
278
278
|
if (!aesKeyBase64) {
|
|
279
|
-
this.logger?.warn?.(`[dsh-bridge wechat] media item missing aes_key, cannot decrypt`)
|
|
279
|
+
this.logger?.warn?.(`[dsh-bridge-gateway wechat] media item missing aes_key, cannot decrypt`)
|
|
280
280
|
return null
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -284,7 +284,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
284
284
|
// 图片的 image_item.aeskey 可能是裸 hex(32 字符),需归一化
|
|
285
285
|
const normalizedKey = normalizeAesKey(aesKeyBase64)
|
|
286
286
|
if (!normalizedKey) {
|
|
287
|
-
this.logger?.warn?.(`[dsh-bridge wechat] media item aes_key 无法解析: ${aesKeyBase64.slice(0, 16)}...`)
|
|
287
|
+
this.logger?.warn?.(`[dsh-bridge-gateway wechat] media item aes_key 无法解析: ${aesKeyBase64.slice(0, 16)}...`)
|
|
288
288
|
return null
|
|
289
289
|
}
|
|
290
290
|
const plaintext = await downloadMedia({
|
|
@@ -305,7 +305,7 @@ export class WechatConversationNode extends ConversationBridge {
|
|
|
305
305
|
const filePath = join(mediaDir, `${Date.now()}_${safeName}`)
|
|
306
306
|
await writeFile(filePath, plaintext)
|
|
307
307
|
|
|
308
|
-
this.logger?.info?.(`[dsh-bridge wechat] downloaded ${mediaType} from ${sender}: ${filePath} (${plaintext.length} bytes)`)
|
|
308
|
+
this.logger?.info?.(`[dsh-bridge-gateway wechat] downloaded ${mediaType} from ${sender}: ${filePath} (${plaintext.length} bytes)`)
|
|
309
309
|
return { type: mediaType, path: filePath, size: plaintext.length }
|
|
310
310
|
}
|
|
311
311
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-bridge-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "以 dsh-bridge 为根基的 DSH 远程访问网关:局域网 / Cloudflare 隧道 / Tailscale 隧道 / 自建隧道 / 公网直连网关 / IM Bot,全部在 Web 面板中配置。",
|
|
5
|
-
"releaseNotes": "【dsh-bridge-gateway 0.
|
|
5
|
+
"releaseNotes": "【dsh-bridge-gateway 0.3.0】\n• 📱 修复「滚不到最新会话」:插件给主框架注入的 height:100dvh + overflow:hidden 组合使 DSH 的布局测量算错,会话只渲染约 4 条消息(scrollHeight 996 vs 正常 11295)。去掉这两个属性组后恢复\n• 📱 移动端共修 8 项:主屏图标空白、渠道中心面板标题被顶栏盖住、会话名恒为「新会话」、轨迹视图内容被裁、顶部两层头过高、视图下拉被裁\n• 🖼️ PWA 主屏图标改为真 PNG(192/512 的 any 与 maskable + iOS 180)并用 DSH 业务蓝 #4176e6 —— 华为 HarmonyOS 与多数 Android 启动器不支持 SVG 图标,此前「添加到桌面」得到的是空白图标\n• 🎨 远程登录页改版为门厅式设计,与 DSH 本体同源同一枚鲸鱼\n• 🔍 新增真机诊断能力:?dsh-diag=1 只读指标叠加层 + ?dsh-mobile=off/no-new/no-<分组> 二分开关\n• 🔧 品牌名统一为 dsh-bridge-gateway,旧数据目录与 cloudflared 缓存目录两级链自动迁移\n• ⚠️ 已知问题:输入框仍会压住少量内容(两次修复尝试都破坏 DSH 滚动锚定,已回退),须由 DSH 侧解决\n• ✅ 269 项测试通过,新增 12 条护栏(逐条用注入 bug 验证过会变红)",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"packageManager": "pnpm@11.24.0",
|
|
7
8
|
"main": "lib/index.js",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
@@ -20,12 +21,14 @@
|
|
|
20
21
|
"README.zh.md",
|
|
21
22
|
"CHANGELOG.md",
|
|
22
23
|
"LICENSE",
|
|
23
|
-
"docs"
|
|
24
|
+
"docs",
|
|
25
|
+
"assets"
|
|
24
26
|
],
|
|
25
27
|
"scripts": {
|
|
26
28
|
"build:client": "node client/build.mjs",
|
|
27
29
|
"build:lark": "node scripts/bundle-lark.mjs",
|
|
28
30
|
"build:banner": "node scripts/generate-banner.mjs",
|
|
31
|
+
"verify:mobile-header": "node scripts/verify-mobile-header.mjs",
|
|
29
32
|
"prepack": "npm run build:lark && npm run build:client",
|
|
30
33
|
"release:github": "node scripts/create-github-release.mjs",
|
|
31
34
|
"test": "node --import ./test/isolate-home.mjs --test test/*.test.mjs"
|