@gehennawu/dsh-service 0.21.0 → 0.21.1

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/README.en.md CHANGED
@@ -45,7 +45,7 @@ The plugin also appears under **Plugins → Plugin configuration**, with five ho
45
45
  ### Quota lookup
46
46
 
47
47
  - A dedicated "Quota lookup" tab shows adapted providers as separate cards, each window rendered as a percentage with its own bar and a reset countdown on its own line, plus a refresh icon beside the updated time that force-refreshes that provider on click (bypassing the poll interval); unadapted providers take no space — a "Manual adapt" row at the bottom enables one by picking its type, and each card's footer can switch type, fall back to auto-detect, or disable lookup at any time
48
- - A quota ring inside the conversation composer follows the provider selected by the current session and shows the tightest budget window as a percentage (green below 80%, amber at or above); clicking opens a panel headed by the provider name, with a bar and used percentage for each window and reset times on their own lines
48
+ - A quota ring inside the conversation composer follows the provider selected by the current session and shows the tightest budget window as a percentage (green below 80%, amber at or above); clicking opens a panel headed by the provider name, with a bar and used percentage for each window and reset times on their own lines; on narrow viewports (phones) the panel switches to a viewport-centered floating layer — fully visible, scrolling internally when too tall — and rotates or resizing wider flips it back to the anchored placement above the ring
49
49
  - A "Show a remote quota entry in the settings left navigation" switch lives in the Quota lookup tab (off by default), mirroring the Restart entry
50
50
  - Built-in adaptations: **OpenCode Go** (`{baseURL}/usage`), **Zhipu GLM Coding Plan / zai-coding-cn** (official monitor `quota/limit` endpoint with three windows — 5-hour rolling tokens, weekly tokens, monthly MCP quota; an idle 5-hour window hides its reset time, matching the official console), **OpenRouter** (credits used %), **Kimi/Moonshot** and **SiliconFlow** (CNY balance text); dialects that natively report remaining percentage flip the panel word to "Remaining" and invert the warn threshold; transient network errors retry automatically and the Zhipu dual-domain candidate chain switches automatically; the provider-to-kind mapping lives in `DSH_HOME/dsh-service-quota.json`, and known services are auto-detected from their baseURL (e.g. opencode.ai, bigmodel.cn) with no manual picking; no upstream request is ever made for unadapted or disabled providers; disabling can be done via "Disable" in a card's footer or by writing `"<provider>": null` in the config file (both are equivalent). Zhipu reset cards have no API-key-queryable endpoint yet — you can add any number of them via each provider card's "Add reset card" button on the tab (name plus expiry time, down to the minute), and remove each one independently — the ring panel shows them too (stored in the same config file); expired cards are flagged automatically
51
51
  - Anti-rate-limit pacing is enforced by the host: successful results are cached for 60 seconds (shared across tabs), failures back off exponentially (30 s doubling, capped at 15 minutes), upstream timeout is 15 s; the panel can set auto query to manual only / 1 / 2 / 5 / 10 minutes (manual only by default), paused automatically while the page is hidden
package/README.md CHANGED
@@ -45,7 +45,7 @@
45
45
  ### 额度查询
46
46
 
47
47
  - 独立的「额度查询」标签:以卡片分区展示已适配的供应商,每个窗口显示百分比、独立进度条,重置时间单独一行;卡片头部更新时间旁有刷新图标,点击即强制重拉该供应商(不受轮询间隔限制);未适配的不占位置,统一收进底部「手动适配」行选择类型启用,卡片脚部可随时切换适配类型、回退自动识别或停用查询
48
- - 对话输入框内一枚额度圆环,跟随当前会话所选模型的供应商,显示最紧预算窗口的已用百分比(<80% 绿色、≥80% 黄色);点击弹出面板——头部标明供应商,各窗口带独立进度条与已用百分比、重置时间单独一行
48
+ - 对话输入框内一枚额度圆环,跟随当前会话所选模型的供应商,显示最紧预算窗口的已用百分比(<80% 绿色、≥80% 黄色);点击弹出面板——头部标明供应商,各窗口带独立进度条与已用百分比、重置时间单独一行;手机等窄屏上面板自动切换为视口居中的浮层(完整可见、超高时内部滚动),旋转或拖宽窗口即时切回圆环上方锚定
49
49
  - 可在「额度查询」标签开启「设置页左列显示入口」开关(默认关闭),开启后在设置页左侧标签列底部显示「额度查询」快捷入口(与「重启」入口同模式)
50
50
  - 内置适配:**OpenCode Go**(`{baseURL}/usage`)、**智谱 GLM Coding Plan / zai-coding-cn**(官方监控端点 `quota/limit`,含 5 小时滚动 Token、每周 Token、MCP 月度配额三个窗口,5 小时窗口空闲时与官网一致地不显示重置时间)、**OpenRouter**(credits 已用%)、**Kimi/Moonshot** 与 **硅基流动**(人民币余额);原生报「剩余百分比」的方言会自动把面板头部切换为「剩余」并把预警阈值反向;上游瞬时网络错误自动重试,智谱双域候选链自动切换;供应商与适配类型的对应关系保存在 `DSH_HOME/dsh-service-quota.json`,已知服务商按 baseURL 自动识别适配(如 opencode.ai、bigmodel.cn),无需手选;未适配或已停用的供应商不会被发起任何上游请求,停用可在卡片脚部选「停用查询」或在配置文件写 `"<provider>": null`(两者等价)。智谱的重置卡暂无 API Key 可查的接口,可在「额度查询」标签内点各供应商卡片的「添加重置卡」填写名称与到期时间(可精确到分钟),可连续添加多条、每条独立移除;圆环面板同步显示(数据存入同一配置文件),过期自动标注
51
51
  - 防风控节律由宿主统一控制:成功结果缓存 60 秒(多标签共享)、失败指数退避(30 秒起 ×2、封顶 15 分钟)、上游超时 15 秒;面板可把自动查询调成仅手动 / 1 / 2 / 5 / 10 分钟(默认仅手动),页面不可见时自动暂停
package/client.js CHANGED
@@ -1570,6 +1570,33 @@ window.__ModuleLoader__.load({
1570
1570
  resetNode)
1571
1571
  }
1572
1572
 
1573
+ // v0.21.1 圆环弹窗窄视口居中:宿主事实(别再翻外壳源码)——composer 工具行容器带
1574
+ // container-type:inline-size(布局包含:fixed 后代的 containing block 是它而非视口),
1575
+ // 外层会话滚动体又是 overflow:hidden auto,挂在 conversation.input.right 的弹层在手机上
1576
+ // 必然被裁一半,CSS 覆盖救不了。唯一可靠解法是 portal 到 document.body 后 fixed 视口居中。
1577
+ // react-dom 在平台 seed 表内(官方附件插件同源用 createPortal),老外壳可能缺席:
1578
+ // 惰性尝试,拿不到就回落原锚定弹层(与 modelDirectories 同款可选依赖哲学)。
1579
+ let quotaCreatePortal = null
1580
+ try {
1581
+ const reactDom = require('react-dom')
1582
+ if (reactDom !== null && reactDom !== undefined && typeof reactDom.createPortal === 'function') {
1583
+ quotaCreatePortal = reactDom.createPortal
1584
+ }
1585
+ } catch (_) {
1586
+ quotaCreatePortal = null
1587
+ }
1588
+
1589
+ /** 窄视口判定:matchMedia 优先(可订阅变化),缺席时 innerWidth 兜底;
1590
+ * 无 window(SSR/测试)一律宽视口,维持圆环上方锚定弹层。断点 480px 与外壳窄容器一致。 */
1591
+ function quotaNarrowViewport() {
1592
+ if (typeof window === 'undefined' || window === null) return false
1593
+ if (typeof window.matchMedia === 'function') {
1594
+ const query = window.matchMedia('(max-width: 480px)')
1595
+ return typeof query.matches === 'boolean' ? query.matches : false
1596
+ }
1597
+ return typeof window.innerWidth === 'number' && window.innerWidth <= 480
1598
+ }
1599
+
1573
1600
  function QuotaRing(props) {
1574
1601
  const translate = useTranslation()
1575
1602
  const [quota, setQuota] = useState(quotaStore.getSnapshot())
@@ -1593,6 +1620,22 @@ window.__ModuleLoader__.load({
1593
1620
  }, [store, directoryState, props && props.loadDirectory])
1594
1621
  const [open, setOpen] = useState(false)
1595
1622
  const rootRef = useRef(null)
1623
+ const panelRef = useRef(null)
1624
+ // 窄视口(手机竖屏):弹层 portal 到 body 视口居中;宽视口保持圆环上方锚定。
1625
+ // matchMedia change 订阅让旋转/拖宽窗口时弹层几何实时迁移,open 态不丢。
1626
+ const [narrow, setNarrow] = useState(quotaNarrowViewport)
1627
+ useEffect(() => {
1628
+ if (typeof window === 'undefined' || window === null || typeof window.matchMedia !== 'function') return undefined
1629
+ const query = window.matchMedia('(max-width: 480px)')
1630
+ const sync = () => setNarrow(query.matches)
1631
+ sync()
1632
+ if (typeof query.addEventListener === 'function') query.addEventListener('change', sync)
1633
+ else if (typeof query.addListener === 'function') query.addListener(sync)
1634
+ return () => {
1635
+ if (typeof query.removeEventListener === 'function') query.removeEventListener('change', sync)
1636
+ else if (typeof query.removeListener === 'function') query.removeListener(sync)
1637
+ }
1638
+ }, [])
1596
1639
  const provider = directoryState?.current?.provider ?? null
1597
1640
  const row = provider !== null ? (quota.providers || []).find((entry) => entry.provider === provider && entry.adapted === true) : null
1598
1641
  const windows = Array.isArray(row?.windows) ? row.windows : []
@@ -1617,6 +1660,8 @@ window.__ModuleLoader__.load({
1617
1660
  if (!open || typeof document === 'undefined') return undefined
1618
1661
  const onPointerDown = (event) => {
1619
1662
  if (rootRef.current && event.target instanceof Node && rootRef.current.contains(event.target)) return
1663
+ // portal 模式下面板挂在 body 下、不在 rootRef 子树内:点面板本体不算外点。
1664
+ if (panelRef.current && event.target instanceof Node && panelRef.current.contains(event.target)) return
1620
1665
  setOpen(false)
1621
1666
  }
1622
1667
  const onKeyDown = (event) => {
@@ -1648,8 +1693,11 @@ window.__ModuleLoader__.load({
1648
1693
  ? React.createElement('div', { style: { marginTop: '8px', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } },
1649
1694
  row.refreshing === true ? translate('quota.refreshing') : translate('quota.updated', { time: formatClockTime(row.fetchedAt) }))
1650
1695
  : null
1651
- return React.createElement('span', { ref: rootRef, style: { position: 'relative', display: 'inline-flex' } },
1652
- React.createElement('button', {
1696
+ // 面板先构造、再决定挂载方式:窄视口 portal 到 document.body 后 fixed 视口居中
1697
+ // (根因见 quotaCreatePortal 处注释);宽视口或 react-dom 缺席时锚定圆环上方(原行为)。
1698
+ const centered = open && narrow && quotaCreatePortal !== null
1699
+ && typeof document !== 'undefined' && document.body !== null && document.body !== undefined
1700
+ const triggerNode = React.createElement('button', {
1653
1701
  type: 'button',
1654
1702
  'data-testid': 'quota-ring-trigger',
1655
1703
  'aria-label': ariaText,
@@ -1670,13 +1718,16 @@ window.__ModuleLoader__.load({
1670
1718
  cx: '7', cy: '7', r: radius, fill: 'none', stroke: color, strokeWidth: '2', strokeLinecap: 'round',
1671
1719
  strokeDasharray: `${(circumference * percent) / 100} ${circumference}`,
1672
1720
  transform: 'rotate(-90 7 7)',
1673
- }))),
1674
- open ? React.createElement('div', {
1675
- role: 'dialog',
1676
- 'aria-label': translate('quota.panel.title'),
1677
- 'data-testid': 'quota-ring-panel',
1678
- style: { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 100, boxSizing: 'border-box', width: '240px', padding: '12px', borderRadius: '12px', background: 'var(--dsw-specific-menu)', border: '1px solid var(--dsw-alias-border-inverted)', boxShadow: 'var(--dsw-shadow-lv3)' },
1679
- },
1721
+ })))
1722
+ const panelNode = open ? React.createElement('div', {
1723
+ ref: panelRef,
1724
+ role: 'dialog',
1725
+ 'aria-label': translate('quota.panel.title'),
1726
+ 'data-testid': 'quota-ring-panel',
1727
+ style: centered
1728
+ ? { position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', zIndex: 1000, boxSizing: 'border-box', width: 'min(280px, calc(100vw - 32px))', maxHeight: 'calc(100dvh - 96px)', overflowY: 'auto', padding: '12px', borderRadius: '12px', background: 'var(--dsw-specific-menu)', border: '1px solid var(--dsw-alias-border-inverted)', boxShadow: 'var(--dsw-shadow-lv3)' }
1729
+ : { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 100, boxSizing: 'border-box', width: '240px', padding: '12px', borderRadius: '12px', background: 'var(--dsw-specific-menu)', border: '1px solid var(--dsw-alias-border-inverted)', boxShadow: 'var(--dsw-shadow-lv3)' },
1730
+ },
1680
1731
  React.createElement('div', { style: { display: 'flex', alignItems: 'baseline', gap: '6px' } },
1681
1732
  React.createElement('span', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, usedWord),
1682
1733
  React.createElement('span', { style: { marginLeft: 'auto', fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, provider)),
@@ -1696,7 +1747,10 @@ window.__ModuleLoader__.load({
1696
1747
  }))]
1697
1748
  : []),
1698
1749
  errorNode,
1699
- updatedNode) : null)
1750
+ updatedNode) : null
1751
+ return React.createElement('span', { ref: rootRef, style: { position: 'relative', display: 'inline-flex' } },
1752
+ triggerNode,
1753
+ centered ? quotaCreatePortal(panelNode, document.body) : panelNode)
1700
1754
  }
1701
1755
 
1702
1756
  function FeatureSettingsCard() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gehennawu/dsh-service",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "DSH Web 自托管运维面板:安全重启、健康监控、备份和 Linux 权限维护。",
5
5
  "type": "module",
6
6
  "scripts": {