dsh-mini-remote 0.1.7 → 0.1.9

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/lib/page.js CHANGED
@@ -19,7 +19,9 @@ let template = null
19
19
  export function renderPage({ defaultMode = 'minimal', needsToken = false, build = '', maxUpload = 0 } = {}) {
20
20
  if (template === null) template = readFileSync(PAGE_PATH, 'utf8')
21
21
  return template
22
- .replace('__DEFAULT_MODE__', defaultMode === 'chat' ? 'chat' : 'minimal')
22
+ // 三种显示模式都认(单帧/聊天/完整);不认识的值一律回落单帧——回落要卡在这一处,
23
+ // 各处自己挑认哪几个,迟早出现「存得进去、刷新就变回去」的对不上。
24
+ .replace('__DEFAULT_MODE__', ['chat', 'full'].includes(defaultMode) ? defaultMode : 'minimal')
23
25
  .replace('__NEEDS_TOKEN__', needsToken ? 'true' : 'false')
24
26
  // 立绘地址带上构建指纹(页面自己拼 ?v=__BUILD__),换了图浏览器就不会一直
25
27
  // 拿缓存里那张。只留字母数字,别让它有机会往 HTML 里塞东西。
@@ -67,14 +69,26 @@ const IMAGE_MAX = 20 * 1024 * 1024
67
69
  *
68
70
  * 2026-09-28 之前这里只认"把图拷进 lib/art 并写裸名字"一种写法,正文里写全路径就成了
69
71
  * 破图标——用户第三次为此来问,这是那时的放宽。
72
+ *
73
+ * `extraRoots`(2026-09-28 追加)是「额外候选根」,由调用方给——服务那边传的是**当前已
74
+ * 注册的工作区目录**。理由:正文里的相对路径相对的是**那段对话的工作目录**,而它可能
75
+ * 是别的项目(实测出图那段对话在 I:\DSH\dsh-jev-ultrafast,正文写 `scratch/x.png`),
76
+ * 插件只按自己的两个根找就会 404,手机上正是那张破图。这里只放宽"在哪找":
77
+ * 能发什么、发多大、要不要令牌,全都没动。相对写法里带 `..` 一律拒绝——要跨目录,
78
+ * 正文里写绝对路径,那本来就是支持的写法。
70
79
  */
71
- export function readImage(raw) {
80
+ export function readImage(raw, extraRoots = []) {
72
81
  if (typeof raw !== 'string') return null
73
82
  const p = raw.trim()
74
83
  if (!p || p.length > 4096 || p.includes('\0')) return null
75
84
  const type = IMAGE_TYPES[extname(p).toLowerCase()]
76
85
  if (!type) return null
77
- const candidates = isAbsolute(p) ? [p] : [p, join(PROJECT_ROOT, p), join(ART_DIR, p)]
86
+ const abs = isAbsolute(p)
87
+ const roots = Array.isArray(extraRoots) ? extraRoots : []
88
+ if (!abs && p.split(/[\\/]/).includes('..')) return null
89
+ const candidates = abs
90
+ ? [p]
91
+ : [p, join(PROJECT_ROOT, p), join(ART_DIR, p), ...roots.filter((r) => typeof r === 'string' && r).map((r) => join(r, p))]
78
92
  for (const file of candidates) {
79
93
  try {
80
94
  if (statSync(file).size > IMAGE_MAX) continue
package/lib/pairing.js CHANGED
@@ -140,8 +140,9 @@ async function qr(text) {
140
140
  * @param {string[]} [opts.bound] 真正绑定成功的地址;不传则用探测结果
141
141
  * @param {{enabled?: boolean, url?: string|null, starting?: boolean, error?: string|null}} [opts.tunnel] 公网隧道状态
142
142
  * @param {{installed?: boolean, url?: string|null, urlOfOtherPort?: string|null, error?: string|null}} [opts.serve] Tailscale HTTPS 状态
143
+ * @param {{enabled?: boolean, appId?: string, openIds?: string, chatIds?: string, hasSecret?: boolean, starting?: boolean, running?: boolean, connected?: boolean, rejected?: {reason: string, at: number}|null, error?: string|null}} [opts.feishu] 飞书那一块的现状(**不含 appSecret**)
143
144
  */
144
- export async function buildPairing({ port, token, bound, tunnel, serve }) {
145
+ export async function buildPairing({ port, token, bound, tunnel, serve, feishu }) {
145
146
  const detected = reachableAddresses()
146
147
  const usable = bound ? detected.filter((a) => bound.includes(a.address)) : detected
147
148
 
@@ -200,6 +201,32 @@ export async function buildPairing({ port, token, bound, tunnel, serve }) {
200
201
  error: tunnel?.error ?? null,
201
202
  }
202
203
 
204
+ /**
205
+ * 飞书那一块的现状,**四种返回路径都要带上**(和 `serve` 同一个道理,见下面)。
206
+ *
207
+ * 这里**只回答「有没有凭据」**,绝不把 appSecret 本身送出去——这个字段会进浏览器。
208
+ * `openIds` / `chatIds` 是名单,可以回显:用户得看得见自己填的是什么。
209
+ */
210
+ const feishuStatus = {
211
+ enabled: Boolean(feishu?.enabled),
212
+ appId: feishu?.appId ?? '',
213
+ openIds: feishu?.openIds ?? '',
214
+ chatIds: feishu?.chatIds ?? '',
215
+ hasSecret: Boolean(feishu?.hasSecret),
216
+ starting: Boolean(feishu?.starting),
217
+ running: Boolean(feishu?.running),
218
+ connected: Boolean(feishu?.connected),
219
+ /**
220
+ * 上一次被白名单挡住的来源,原样交给界面。
221
+ *
222
+ * 这是**新用户唯一走得通的那条路**:名单默认空着(谁都不认),他不可能凭空知道
223
+ * 自己的 open_id 长什么样。第一次发消息被挡下时,把「来源是谁」当场显示出来,
224
+ * 抄进名单再发一次就通了——少了这一步,用户会卡死在「开了但没反应」。
225
+ */
226
+ rejected: feishu?.rejected ?? null,
227
+ error: feishu?.error ?? null,
228
+ }
229
+
203
230
  /**
204
231
  * Tailscale HTTPS 的现状,**四种返回路径都要带上**。
205
232
  *
@@ -243,7 +270,7 @@ export async function buildPairing({ port, token, bound, tunnel, serve }) {
243
270
  // 一条能用的都没有。三种成因要分开说,否则用户不知道该去改配置、去查网络,
244
271
  // 还是去查隧道——这三件事的下一步动作完全不同。
245
272
  if (status.enabled && status.error) {
246
- return { ok: false, error: `公网访问没起来:${status.error}`, tunnel: status, tailscale, serve: httpsStatus }
273
+ return { ok: false, error: `公网访问没起来:${status.error}`, tunnel: status, tailscale, serve: httpsStatus, feishu: feishuStatus }
247
274
  }
248
275
  if (detected.length) {
249
276
  return {
@@ -253,6 +280,7 @@ export async function buildPairing({ port, token, bound, tunnel, serve }) {
253
280
  tunnel: status,
254
281
  tailscale,
255
282
  serve: httpsStatus,
283
+ feishu: feishuStatus,
256
284
  }
257
285
  }
258
286
  return {
@@ -261,8 +289,9 @@ export async function buildPairing({ port, token, bound, tunnel, serve }) {
261
289
  tunnel: status,
262
290
  tailscale,
263
291
  serve: httpsStatus,
292
+ feishu: feishuStatus,
264
293
  }
265
294
  }
266
295
 
267
- return { ok: true, port, token, entries, tunnel: status, tailscale, serve: httpsStatus }
296
+ return { ok: true, port, token, entries, tunnel: status, tailscale, serve: httpsStatus, feishu: feishuStatus }
268
297
  }
@@ -57,6 +57,7 @@ export const PRESET_LABELS = {
57
57
  'read-only': '仅可查看',
58
58
  'workspace-write': '工作区内修改',
59
59
  'danger-full-access': '完全权限',
60
+ 'auto': '自动审查',
60
61
  }
61
62
 
62
63
  /**