@yfwu2020/dsh-selection-explain 0.4.0 → 0.7.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/src/index.ts CHANGED
@@ -1381,6 +1381,94 @@ export function apply(ctx: Context, rawConfig: Config): void {
1381
1381
  const historyFile = (): string =>
1382
1382
  join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain', 'history.json')
1383
1383
 
1384
+ /**
1385
+ * 悬浮胶囊"拖到别处"后的位置(用户要求记住,存 host 上 —— 不用 localStorage)。
1386
+ * 只记自由摆放的**右边缘 + 垂直中心**:收球/展开都以右边缘为锚(client 半的 applyFreePill 用 CSS right 钉住)。
1387
+ * 吸附回右下角(anchor)时会清掉 —— 那是默认位置,不需要记。
1388
+ */
1389
+ const pillPosFile = (): string =>
1390
+ join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain', 'pill.json')
1391
+
1392
+ let pillPos: { right: number; y: number } | null = null
1393
+ let pillPosLoaded = false
1394
+ let pillPosTimer: ReturnType<typeof setTimeout> | null = null
1395
+
1396
+ /** 位置校验:只接受有限数值,并夹到一个"任何屏幕都放得下"的量级。 */
1397
+ function normalizePillPos(input: unknown): { right: number; y: number } | null {
1398
+ const value = input as { right?: unknown; y?: unknown } | null | undefined
1399
+ const right = typeof value?.right === 'number' && Number.isFinite(value.right) ? value.right : NaN
1400
+ const y = typeof value?.y === 'number' && Number.isFinite(value.y) ? value.y : NaN
1401
+ if (!Number.isFinite(right) || !Number.isFinite(y)) return null
1402
+ const clamp = (n: number): number => Math.round(Math.min(40000, Math.max(-40000, n)))
1403
+ return { right: clamp(right), y: clamp(y) }
1404
+ }
1405
+
1406
+ async function loadPillPos(): Promise<void> {
1407
+ if (pillPosLoaded) return
1408
+ pillPosLoaded = true
1409
+ try {
1410
+ const parsed = JSON.parse(await readFile(pillPosFile(), 'utf8')) as { position?: unknown }
1411
+ pillPos = normalizePillPos(parsed?.position)
1412
+ } catch {
1413
+ pillPos = null // 没存过 / 坏了都当作"没记过"
1414
+ }
1415
+ }
1416
+
1417
+ function schedulePillPosWrite(): void {
1418
+ if (pillPosTimer) clearTimeout(pillPosTimer)
1419
+ pillPosTimer = setTimeout(() => {
1420
+ pillPosTimer = null
1421
+ void (async () => {
1422
+ try {
1423
+ await mkdir(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain'), { recursive: true })
1424
+ await writeFile(pillPosFile(), JSON.stringify({ version: 1, position: pillPos }, null, 0), 'utf8')
1425
+ } catch (error) {
1426
+ ctx.logger?.warn?.(`[${name}] 写入胶囊位置失败:${String((error as Error)?.message ?? error)}`)
1427
+ }
1428
+ })()
1429
+ }, 300)
1430
+ pillPosTimer.unref?.()
1431
+ }
1432
+
1433
+ /**
1434
+ * 胶囊位置:GET 读回来(挂载时恢复)、POST 存({right,y})或清({clear:true},吸附回右下角时调)。
1435
+ */
1436
+ const handlePillPos = (req: IncomingMessage, res: ServerResponse): void => {
1437
+ void (async () => {
1438
+ await loadPillPos()
1439
+ if (req.method === 'GET') {
1440
+ sendJson(res, 200, { ok: true, position: pillPos })
1441
+ return
1442
+ }
1443
+ if (req.method !== 'POST') {
1444
+ sendJson(res, 405, { ok: false, error: '只支持 GET / POST' })
1445
+ return
1446
+ }
1447
+ const raw = await readBody(req, 4_000)
1448
+ let body: { position?: unknown; clear?: unknown } = {}
1449
+ try {
1450
+ body = raw ? (JSON.parse(raw) as { position?: unknown; clear?: unknown }) : {}
1451
+ } catch {
1452
+ sendJson(res, 400, { ok: false, error: '请求体不是合法 JSON' })
1453
+ return
1454
+ }
1455
+ if (body.clear === true) {
1456
+ pillPos = null
1457
+ schedulePillPosWrite()
1458
+ sendJson(res, 200, { ok: true, position: null })
1459
+ return
1460
+ }
1461
+ const next = normalizePillPos(body.position ?? body)
1462
+ if (!next) {
1463
+ sendJson(res, 400, { ok: false, error: 'position 需要 { right, y } 两个有限数值' })
1464
+ return
1465
+ }
1466
+ pillPos = next
1467
+ schedulePillPosWrite()
1468
+ sendJson(res, 200, { ok: true, position: pillPos })
1469
+ })()
1470
+ }
1471
+
1384
1472
  const clampText = (value: unknown, max: number): string => (typeof value === 'string' ? value.slice(0, max) : '')
1385
1473
 
1386
1474
  async function loadHistory(): Promise<void> {
@@ -3004,6 +3092,10 @@ export function apply(ctx: Context, rawConfig: Config): void {
3004
3092
  () => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/history`, handler: handleHistory }),
3005
3093
  `${name}: history route`,
3006
3094
  )
3095
+ ctx.effect(
3096
+ () => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/pill`, handler: handlePillPos }),
3097
+ `${name}: pill position route`,
3098
+ )
3007
3099
  ctx.effect(
3008
3100
  () => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/models`, handler: handleModels }),
3009
3101
  `${name}: models route`,