@jingyi0605/codingns4dsh 0.2.1-rc.2 → 0.2.1-rc.4

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.
Files changed (210) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -6
  3. package/data/build/dist/bootstrap/dsh-connection-adapter.d.ts +1 -1
  4. package/data/build/dist/bootstrap/index.d.ts +1 -1
  5. package/data/build/dist/client/bundle.js +5224 -290
  6. package/data/build/dist/client/bundle.js.map +1 -1
  7. package/data/build/dist/client/client.magpie-model-tag.js +155 -5
  8. package/data/build/dist/client/client.magpie-model-tag.js.map +1 -1
  9. package/data/build/dist/client/client.native-workspace-store.js +30 -4
  10. package/data/build/dist/client/client.native-workspace-store.js.map +1 -1
  11. package/data/build/dist/client/client.transport.js +747 -48
  12. package/data/build/dist/client/client.transport.js.map +1 -1
  13. package/data/build/dist/client/desktop-assistant.js +953 -185
  14. package/data/build/dist/client/feature-requirement.d.ts +43 -0
  15. package/data/build/dist/client/feature-requirement.d.ts.map +1 -0
  16. package/data/build/dist/client/feature-requirement.js +105 -0
  17. package/data/build/dist/client/feature-requirement.js.map +1 -0
  18. package/data/build/dist/client/features/index.d.ts +1 -0
  19. package/data/build/dist/client/features/index.d.ts.map +1 -1
  20. package/data/build/dist/client/features/index.js +3 -0
  21. package/data/build/dist/client/features/index.js.map +1 -1
  22. package/data/build/dist/client/features/magpie-management-controls.d.ts +46 -0
  23. package/data/build/dist/client/features/magpie-management-controls.d.ts.map +1 -0
  24. package/data/build/dist/client/features/magpie-management-controls.js +794 -0
  25. package/data/build/dist/client/features/magpie-management-controls.js.map +1 -0
  26. package/data/build/dist/client/features/magpie-management-page.d.ts +129 -0
  27. package/data/build/dist/client/features/magpie-management-page.d.ts.map +1 -0
  28. package/data/build/dist/client/features/magpie-management-page.js +1279 -0
  29. package/data/build/dist/client/features/magpie-management-page.js.map +1 -0
  30. package/data/build/dist/client/features/magpie-management.d.ts +7 -0
  31. package/data/build/dist/client/features/magpie-management.d.ts.map +1 -0
  32. package/data/build/dist/client/features/magpie-management.js +154 -0
  33. package/data/build/dist/client/features/magpie-management.js.map +1 -0
  34. package/data/build/dist/client/features/magpie-mark.d.ts +10 -0
  35. package/data/build/dist/client/features/magpie-mark.d.ts.map +1 -0
  36. package/data/build/dist/client/features/magpie-mark.js +21 -0
  37. package/data/build/dist/client/features/magpie-mark.js.map +1 -0
  38. package/data/build/dist/client/features/magpie-provider-logo.d.ts +11 -0
  39. package/data/build/dist/client/features/magpie-provider-logo.d.ts.map +1 -0
  40. package/data/build/dist/client/features/magpie-provider-logo.js +35 -0
  41. package/data/build/dist/client/features/magpie-provider-logo.js.map +1 -0
  42. package/data/build/dist/client/features/subscription-quota-groups.d.ts +193 -0
  43. package/data/build/dist/client/features/subscription-quota-groups.d.ts.map +1 -0
  44. package/data/build/dist/client/features/subscription-quota-groups.js +548 -0
  45. package/data/build/dist/client/features/subscription-quota-groups.js.map +1 -0
  46. package/data/build/dist/client/git-management.d.ts.map +1 -1
  47. package/data/build/dist/client/git-management.js +11 -9
  48. package/data/build/dist/client/git-management.js.map +1 -1
  49. package/data/build/dist/client/git-panel-styles.js +1 -0
  50. package/data/build/dist/client/git-panel-styles.js.map +1 -1
  51. package/data/build/dist/client/index.d.ts +1 -1
  52. package/data/build/dist/client/index.d.ts.map +1 -1
  53. package/data/build/dist/client/index.js +1 -1
  54. package/data/build/dist/client/index.js.map +1 -1
  55. package/data/build/dist/client/locale.d.ts.map +1 -1
  56. package/data/build/dist/client/locale.js +7 -0
  57. package/data/build/dist/client/locale.js.map +1 -1
  58. package/data/build/dist/client/locales/magpie.d.ts +4 -0
  59. package/data/build/dist/client/locales/magpie.d.ts.map +1 -0
  60. package/data/build/dist/client/locales/magpie.js +644 -0
  61. package/data/build/dist/client/locales/magpie.js.map +1 -0
  62. package/data/build/dist/client/locales/usage.d.ts.map +1 -1
  63. package/data/build/dist/client/locales/usage.js +52 -0
  64. package/data/build/dist/client/locales/usage.js.map +1 -1
  65. package/data/build/dist/client/magpie-model-tag.d.ts +9 -1
  66. package/data/build/dist/client/magpie-model-tag.d.ts.map +1 -1
  67. package/data/build/dist/client/magpie-model-tag.js +15 -3
  68. package/data/build/dist/client/magpie-model-tag.js.map +1 -1
  69. package/data/build/dist/client/provider-icons.d.ts +8 -0
  70. package/data/build/dist/client/provider-icons.d.ts.map +1 -1
  71. package/data/build/dist/client/provider-icons.js +122 -2
  72. package/data/build/dist/client/provider-icons.js.map +1 -1
  73. package/data/build/dist/client/settings-bridge.d.ts +1 -0
  74. package/data/build/dist/client/settings-bridge.d.ts.map +1 -1
  75. package/data/build/dist/client/settings-bridge.js +29 -10
  76. package/data/build/dist/client/settings-bridge.js.map +1 -1
  77. package/data/build/dist/client/settings-section.d.ts.map +1 -1
  78. package/data/build/dist/client/settings-section.js +21 -3
  79. package/data/build/dist/client/settings-section.js.map +1 -1
  80. package/data/build/dist/client/style.css +224 -221
  81. package/data/build/dist/client/subscription-slot.d.ts +22 -1
  82. package/data/build/dist/client/subscription-slot.d.ts.map +1 -1
  83. package/data/build/dist/client/subscription-slot.js +374 -25
  84. package/data/build/dist/client/subscription-slot.js.map +1 -1
  85. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts +27 -0
  86. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.d.ts.map +1 -0
  87. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js +109 -0
  88. package/data/build/dist/dsh-capabilities/client/global-panel-adapter.js.map +1 -0
  89. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts +2 -1
  90. package/data/build/dist/dsh-capabilities/host/session-title-adapter.d.ts.map +1 -1
  91. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js +32 -2
  92. package/data/build/dist/dsh-capabilities/host/session-title-adapter.js.map +1 -1
  93. package/data/build/dist/dsh-capabilities/matrix.d.ts.map +1 -1
  94. package/data/build/dist/dsh-capabilities/matrix.js +12 -11
  95. package/data/build/dist/dsh-capabilities/matrix.js.map +1 -1
  96. package/data/build/dist/dsh-capabilities/routes.d.ts.map +1 -1
  97. package/data/build/dist/dsh-capabilities/routes.js +12 -9
  98. package/data/build/dist/dsh-capabilities/routes.js.map +1 -1
  99. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  100. package/data/build/dist/dsh-capabilities/types.d.ts.map +1 -1
  101. package/data/build/dist/dsh-capabilities/types.js.map +1 -1
  102. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts +27 -0
  103. package/data/build/dist/host/cli-adapters/acp-cli-driver.d.ts.map +1 -1
  104. package/data/build/dist/host/cli-adapters/acp-cli-driver.js +88 -13
  105. package/data/build/dist/host/cli-adapters/acp-cli-driver.js.map +1 -1
  106. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +10 -0
  107. package/data/build/dist/host/cli-adapters/codex-driver.d.ts.map +1 -1
  108. package/data/build/dist/host/cli-adapters/codex-driver.js +32 -1
  109. package/data/build/dist/host/cli-adapters/codex-driver.js.map +1 -1
  110. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts +6 -5
  111. package/data/build/dist/host/cli-adapters/command-code-acp-loader.d.ts.map +1 -1
  112. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js +72 -12
  113. package/data/build/dist/host/cli-adapters/command-code-acp-loader.js.map +1 -1
  114. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts.map +1 -1
  115. package/data/build/dist/host/cli-adapters/command-code-driver.js +26 -1
  116. package/data/build/dist/host/cli-adapters/command-code-driver.js.map +1 -1
  117. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts.map +1 -1
  118. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +15 -2
  119. package/data/build/dist/host/cli-adapters/dsh-message-projector.js.map +1 -1
  120. package/data/build/dist/host/cli-adapters/feature.d.ts +13 -0
  121. package/data/build/dist/host/cli-adapters/feature.d.ts.map +1 -1
  122. package/data/build/dist/host/cli-adapters/feature.js +438 -6
  123. package/data/build/dist/host/cli-adapters/feature.js.map +1 -1
  124. package/data/build/dist/host/cli-adapters/grok-driver.js +5 -0
  125. package/data/build/dist/host/cli-adapters/grok-driver.js.map +1 -1
  126. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts +72 -0
  127. package/data/build/dist/host/cli-adapters/magpie-detection.d.ts.map +1 -0
  128. package/data/build/dist/host/cli-adapters/magpie-detection.js +312 -0
  129. package/data/build/dist/host/cli-adapters/magpie-detection.js.map +1 -0
  130. package/data/build/dist/host/cli-adapters/magpie-management.d.ts +50 -0
  131. package/data/build/dist/host/cli-adapters/magpie-management.d.ts.map +1 -0
  132. package/data/build/dist/host/cli-adapters/magpie-management.js +1343 -0
  133. package/data/build/dist/host/cli-adapters/magpie-management.js.map +1 -0
  134. package/data/build/dist/host/cli-adapters/magpie-update.d.ts +59 -0
  135. package/data/build/dist/host/cli-adapters/magpie-update.d.ts.map +1 -0
  136. package/data/build/dist/host/cli-adapters/magpie-update.js +249 -0
  137. package/data/build/dist/host/cli-adapters/magpie-update.js.map +1 -0
  138. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts +70 -8
  139. package/data/build/dist/host/cli-adapters/magpie-usage.d.ts.map +1 -1
  140. package/data/build/dist/host/cli-adapters/magpie-usage.js +1075 -125
  141. package/data/build/dist/host/cli-adapters/magpie-usage.js.map +1 -1
  142. package/data/build/dist/host/cli-adapters/magpie-web.d.ts +69 -0
  143. package/data/build/dist/host/cli-adapters/magpie-web.d.ts.map +1 -0
  144. package/data/build/dist/host/cli-adapters/magpie-web.js +485 -0
  145. package/data/build/dist/host/cli-adapters/magpie-web.js.map +1 -0
  146. package/data/build/dist/host/cli-adapters/process-utils.d.ts +2 -0
  147. package/data/build/dist/host/cli-adapters/process-utils.d.ts.map +1 -1
  148. package/data/build/dist/host/cli-adapters/process-utils.js +8 -0
  149. package/data/build/dist/host/cli-adapters/process-utils.js.map +1 -1
  150. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts +6 -0
  151. package/data/build/dist/host/cli-adapters/session-performance-projection.d.ts.map +1 -1
  152. package/data/build/dist/host/cli-adapters/session-performance-projection.js +25 -0
  153. package/data/build/dist/host/cli-adapters/session-performance-projection.js.map +1 -1
  154. package/data/build/dist/host/dsh-gateway-feature.d.ts.map +1 -1
  155. package/data/build/dist/host/dsh-gateway-feature.js +26 -3
  156. package/data/build/dist/host/dsh-gateway-feature.js.map +1 -1
  157. package/data/build/dist/host/features/host-status.d.ts +25 -0
  158. package/data/build/dist/host/features/host-status.d.ts.map +1 -1
  159. package/data/build/dist/host/features/host-status.js +60 -6
  160. package/data/build/dist/host/features/host-status.js.map +1 -1
  161. package/data/build/dist/host/features/lan-access-dsh.d.ts.map +1 -1
  162. package/data/build/dist/host/features/lan-access-dsh.js +61 -10
  163. package/data/build/dist/host/features/lan-access-dsh.js.map +1 -1
  164. package/data/build/dist/host/features/session-translation-protection.d.ts.map +1 -1
  165. package/data/build/dist/host/features/session-translation-protection.js +2 -1
  166. package/data/build/dist/host/features/session-translation-protection.js.map +1 -1
  167. package/data/build/dist/host/lan-access-dsh.d.ts.map +1 -1
  168. package/data/build/dist/host/lan-access-dsh.js +6 -2
  169. package/data/build/dist/host/lan-access-dsh.js.map +1 -1
  170. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js +2 -1
  171. package/data/build/dist/host/modules/pwa/pwa-session-notifications.js.map +1 -1
  172. package/data/build/dist/host/rpc.d.ts +15 -2
  173. package/data/build/dist/host/rpc.d.ts.map +1 -1
  174. package/data/build/dist/host/rpc.js +47 -6
  175. package/data/build/dist/host/rpc.js.map +1 -1
  176. package/data/build/dist/host/settings.d.ts.map +1 -1
  177. package/data/build/dist/host/settings.js +5 -0
  178. package/data/build/dist/host/settings.js.map +1 -1
  179. package/data/build/dist/shared/contracts/config.d.ts +18 -0
  180. package/data/build/dist/shared/contracts/config.d.ts.map +1 -1
  181. package/data/build/dist/shared/contracts/config.js +8 -0
  182. package/data/build/dist/shared/contracts/config.js.map +1 -1
  183. package/data/build/dist/shared/contracts/errors.d.ts +2 -2
  184. package/data/build/dist/shared/contracts/feature.d.ts +7 -0
  185. package/data/build/dist/shared/contracts/feature.d.ts.map +1 -1
  186. package/data/build/dist/shared/contracts/magpie-management.d.ts +414 -0
  187. package/data/build/dist/shared/contracts/magpie-management.d.ts.map +1 -0
  188. package/data/build/dist/shared/contracts/magpie-management.js +2 -0
  189. package/data/build/dist/shared/contracts/magpie-management.js.map +1 -0
  190. package/data/build/dist/shared/contracts/subscription.d.ts +77 -1
  191. package/data/build/dist/shared/contracts/subscription.d.ts.map +1 -1
  192. package/data/build/dist/shared/contracts/subscription.js.map +1 -1
  193. package/data/build/dist/shared/contracts/version.d.ts +4 -4
  194. package/data/build/dist/shared/contracts/version.js +3 -3
  195. package/data/build/dist/shared/index.d.ts +3 -1
  196. package/data/build/dist/shared/index.d.ts.map +1 -1
  197. package/data/build/dist/shared/index.js +2 -1
  198. package/data/build/dist/shared/index.js.map +1 -1
  199. package/data/build/dist/shared/magpie-provider-id.d.ts +12 -0
  200. package/data/build/dist/shared/magpie-provider-id.d.ts.map +1 -0
  201. package/data/build/dist/shared/magpie-provider-id.js +16 -0
  202. package/data/build/dist/shared/magpie-provider-id.js.map +1 -0
  203. package/data/build/dist/shared/session-title.d.ts.map +1 -1
  204. package/data/build/dist/shared/session-title.js +10 -4
  205. package/data/build/dist/shared/session-title.js.map +1 -1
  206. package/package.json +29 -29
  207. package/profile/README.md +2 -2
  208. package/profile/package.json +3 -3
  209. package/profile/version.json +3 -3
  210. package/version.json +3 -3
@@ -1,14 +1,19 @@
1
1
  import { startSerialPolling } from './serial-polling.js';
2
- import { createElement, useEffect, useLayoutEffect, useRef, useState } from 'react';
2
+ import { createElement, Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
  import { isSubscriptionUsageFresh } from '../shared/contracts/subscription.js';
4
5
  import { DEFAULT_SUBSCRIPTION_USAGE_SETTINGS } from '../shared/contracts/config.js';
5
6
  import { callCliRpc } from './cli-catalog.js';
6
7
  import { resolveCodingNsTranslator, useCodingNsTranslator } from './locale.js';
7
8
  import { providerIconUrl } from './provider-icons.js';
9
+ import { MagpieProviderLogo } from './features/magpie-provider-logo.js';
10
+ import { MAGPIE_REQUESTS_PAGE_SIZE, RequestsPage, installMagpiePageStyles } from './features/magpie-management-page.js';
11
+ import { createQuotaAccountBlock, createQuotaGroupHeader, formatQuotaCredits, formatQuotaNumber, groupQuotasByProvider, readWindowCreditBalance, quotaWindowRemaining, resolveQuotaBalanceLabel, resolveQuotaCreditBalance, } from './features/subscription-quota-groups.js';
8
12
  import { subscribeSessionAdapters } from './session-adapter-cache.js';
9
13
  import { loadCliSessionConfig } from './cli-session-config-cache.js';
10
14
  import { dshPopupSurfaceStyle, dshSettingsButtonStyle, dshSettingsPrimaryButtonStyle, dshThemeColor } from './theme.js';
11
- import { useDismissOnOutsidePointer } from './popup-dismiss.js';
15
+ import { backdropPointerDownHandler, useDismissOnOutsidePointer } from './popup-dismiss.js';
16
+ import { callCodingNsRpcResult } from './rpc-call.js';
12
17
  import { uiFontSize } from './font-scale.js';
13
18
  const SUBSCRIPTION_STYLE_ID = 'codingns4dsh-subscription-responsive-style';
14
19
  /**
@@ -57,6 +62,7 @@ function loadUsage(rpc, key, payload) {
57
62
  /** 图标重置按钮的悬停/按下/聚焦/禁用只能由注入样式表表达,内联样式无法命中伪类。 */
58
63
  const RESET_BUTTON_CSS = '.codingns4dsh-subscription-reset{display:inline-flex;align-items:center;justify-content:center;flex:none;width:26px;height:26px;padding:0;border:1px solid var(--dsw-alias-border-l2,#d9d9d9);border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary,GrayText);cursor:pointer;transition:background .15s ease,color .15s ease}.codingns4dsh-subscription-reset:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,CanvasText)}.codingns4dsh-subscription-reset:active:not(:disabled){background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.14))}.codingns4dsh-subscription-reset:focus-visible{outline:none;box-shadow:0 0 0 var(--dsw-focus-ring-width,2px) var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#1677ff))}.codingns4dsh-subscription-reset:disabled{opacity:.4;cursor:not-allowed}@media (prefers-reduced-motion: reduce){.codingns4dsh-subscription-reset{transition:none}}';
59
64
  const MAGPIE_ROUTE_CSS = '@keyframes codingns4dsh-magpie-route-pulse{0%{transform:translateY(-120%);opacity:0}20%{opacity:1}80%{opacity:1}100%{transform:translateY(320%);opacity:0}}@keyframes codingns4dsh-magpie-route-breathe{0%,100%{box-shadow:0 0 0 0 rgba(22,119,255,0)}50%{box-shadow:0 0 0 4px rgba(22,119,255,.16)}}@keyframes codingns4dsh-magpie-route-step{from{opacity:0;transform:translateY(4px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}.codingns4dsh-magpie-route-pulse{animation:codingns4dsh-magpie-route-pulse 1.8s ease-in-out infinite}.codingns4dsh-magpie-route-node-active{animation:codingns4dsh-magpie-route-breathe 2s ease-in-out infinite}.codingns4dsh-magpie-route-step{animation:codingns4dsh-magpie-route-step .38s cubic-bezier(.2,.75,.25,1) both;transition:background .16s ease}.codingns4dsh-magpie-route-step:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.07))}.codingns4dsh-magpie-route-step:hover .codingns4dsh-magpie-route-node{box-shadow:0 0 0 1px var(--dsw-alias-state-business-primary,#1677ff),0 0 0 4px rgba(22,119,255,.1)}.codingns4dsh-magpie-route-node{transition:box-shadow .16s ease}.codingns4dsh-magpie-tab:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#1677ff)) inset}@media (prefers-reduced-motion:reduce){.codingns4dsh-magpie-route-pulse,.codingns4dsh-magpie-route-node-active,.codingns4dsh-magpie-route-step,.codingns4dsh-magpie-route-node{animation:none;transition:none}}';
65
+ const MAGPIE_REQUESTS_CSS = '.codingns4dsh-magpie-route-requests:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,CanvasText)}.codingns4dsh-magpie-route-requests:focus-visible,.codingns4dsh-magpie-requests-close:focus-visible,.codingns4dsh-magpie-requests-retry:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,#1677ff))}.codingns4dsh-magpie-requests-close:hover,.codingns4dsh-magpie-requests-retry:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,CanvasText)}.codingns4dsh-magpie-requests-dialog:focus,.codingns4dsh-magpie-requests-dialog:focus-visible{outline:none}.codingns4dsh-magpie-requests-body{flex:1 1 auto;min-height:0;overflow:auto;overscroll-behavior:contain}.codingns4dsh-magpie-requests-view{display:block!important;width:100%;height:auto!important;min-height:0;padding-left:0!important;background:transparent}.codingns4dsh-magpie-requests-view .codingns-magpie-view{max-width:none;padding:0}@media (max-width:768px){.codingns4dsh-magpie-requests-overlay{align-items:flex-end!important;padding:12px!important}.codingns4dsh-magpie-requests-dialog{width:100%!important;max-height:78vh!important;max-height:78dvh!important}}@media (prefers-reduced-motion:reduce){.codingns4dsh-magpie-route-requests{transition:none}}';
60
66
  export function calculateMagpiePopoverPlacement(rootLeft, viewportWidth, sidebarLeft) {
61
67
  const margin = 12;
62
68
  const boundary = Math.min(viewportWidth - margin, sidebarLeft === null ? viewportWidth - margin : sidebarLeft - margin);
@@ -64,14 +70,19 @@ export function calculateMagpiePopoverPlacement(rootLeft, viewportWidth, sidebar
64
70
  const left = Math.max(margin, Math.min(rootLeft, boundary - width));
65
71
  return { left: left - rootLeft, width };
66
72
  }
67
- /** 移动端订阅入口只保留图标,完整数据仍可在点击后的弹层中查看。 */
73
+ /**
74
+ * 移动端订阅入口只保留图标,完整数据仍可在点击后的弹层中查看。
75
+ * 弹层高度上限固定为浏览器可视高度的 75%:先写 `vh` 兜底,再由 `dvh` 覆盖,
76
+ * 使移动端工具栏收展后仍按真实可视区计算;超出部分在弹层内部纵向滚动。
77
+ */
78
+ export const SUBSCRIPTION_RESPONSIVE_CSS = '@media (max-width: 768px){.codingns4dsh-subscription-trigger{gap:0!important;padding-left:4px!important;padding-right:4px!important}.codingns4dsh-subscription-label,.codingns4dsh-subscription-value{display:none!important}.codingns4dsh-subscription-popover{position:fixed!important;left:12px!important;right:12px!important;bottom:48px!important;width:auto!important;min-width:0!important;max-width:none!important;max-height:75vh!important;max-height:75dvh!important;overflow-x:hidden!important;overflow-y:auto!important;overscroll-behavior:contain!important;box-sizing:border-box!important}}';
68
79
  function installSubscriptionStyles() {
69
80
  if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${SUBSCRIPTION_STYLE_ID}"]`) !== null)
70
81
  return;
71
82
  const style = document.createElement('style');
72
83
  style.dataset.plugin = 'codingns4dsh';
73
84
  style.dataset.pluginCss = SUBSCRIPTION_STYLE_ID;
74
- style.textContent = '@media (max-width: 768px){.codingns4dsh-subscription-trigger{gap:0!important;padding-left:4px!important;padding-right:4px!important}.codingns4dsh-subscription-label,.codingns4dsh-subscription-value{display:none!important}.codingns4dsh-subscription-popover{position:fixed!important;left:12px!important;right:12px!important;bottom:48px!important;width:auto!important;min-width:0!important;max-width:none!important;max-height:calc(100vh - 72px)!important;overflow:auto!important}}' + RESET_BUTTON_CSS + MAGPIE_ROUTE_CSS;
85
+ style.textContent = SUBSCRIPTION_RESPONSIVE_CSS + RESET_BUTTON_CSS + MAGPIE_ROUTE_CSS + MAGPIE_REQUESTS_CSS;
75
86
  document.head.appendChild(style);
76
87
  }
77
88
  /** 在 DSH 原生步骤统计左侧显示当前 Agent 的订阅余量。 */
@@ -101,10 +112,19 @@ function CommandCodeSubscriptionSlot(props) {
101
112
  const [open, setOpen] = useState(false);
102
113
  const [loading, setLoading] = useState(false);
103
114
  const [magpieTab, setMagpieTab] = useState('usage');
115
+ // 多账号厂商默认只展开会话实际使用的账号;展开状态挂在根组件上,
116
+ // 弹层内容本身不持有 Hook,便于无浏览器的组件测试直接展开内容树。
117
+ const [expandedQuotaGroups, setExpandedQuotaGroups] = useState([]);
118
+ const toggleQuotaGroup = (provider) => {
119
+ setExpandedQuotaGroups((current) => current.includes(provider)
120
+ ? current.filter((item) => item !== provider)
121
+ : [...current, provider]);
122
+ };
104
123
  const [magpiePlacement, setMagpiePlacement] = useState({ left: 0, width: 400 });
105
124
  const [clock, setClock] = useState(() => Date.now());
106
125
  const rootRef = useRef(null);
107
126
  const magpiePopoverRef = useRef(null);
127
+ const magpieRequestsDialogRef = useRef(null);
108
128
  const resetDialogRef = useRef(null);
109
129
  // 重置成功后的刷新不能重跑加载 effect,否则 usage 会被清空导致底部入口闪断。
110
130
  const refreshRef = useRef(null);
@@ -184,6 +204,9 @@ function CommandCodeSubscriptionSlot(props) {
184
204
  return;
185
205
  }
186
206
  let active = true;
207
+ // 未解析的 Magpie 配额必须快速补读:官方余额一旦抢先显示,用户要等到下一次
208
+ // 轮询(默认 5 分钟)或整页刷新才会切到 Magpie。
209
+ let upstreamUnresolved = false;
187
210
  setEligible(false);
188
211
  setSnapshot(null);
189
212
  setAdapterId(null);
@@ -249,6 +272,7 @@ function CommandCodeSubscriptionSlot(props) {
249
272
  : await loadUsage(props.rpc, cacheKey, payload);
250
273
  if (next !== null)
251
274
  subscriptionUsageCache.set(cacheKey, { usage: next, capturedAt: Date.now() });
275
+ upstreamUnresolved = shouldResolveMagpieQuota(next?.magpie);
252
276
  if (active)
253
277
  setSnapshot(next);
254
278
  }
@@ -267,7 +291,11 @@ function CommandCodeSubscriptionSlot(props) {
267
291
  }
268
292
  };
269
293
  const intervalMins = props.getRefreshIntervalMins?.() ?? DEFAULT_SUBSCRIPTION_USAGE_SETTINGS.refreshIntervalMins;
270
- const poll = startSerialPolling(refresh, Math.max(0, intervalMins) * 60_000, { timeoutMs: 30_000 });
294
+ const pollIntervalMs = Math.max(0, intervalMins) * 60_000;
295
+ const poll = startSerialPolling(refresh, pollIntervalMs, {
296
+ timeoutMs: 30_000,
297
+ getIntervalMs: () => upstreamUnresolved ? Math.min(pollIntervalMs, MAGPIE_QUOTA_RETRY_MS) : pollIntervalMs,
298
+ });
271
299
  refreshRef.current = poll.refresh;
272
300
  return () => {
273
301
  active = false;
@@ -335,13 +363,13 @@ function CommandCodeSubscriptionSlot(props) {
335
363
  }
336
364
  })();
337
365
  };
338
- useDismissOnOutsidePointer(rootRef, open, () => setOpen(false));
366
+ useDismissOnOutsidePointer(rootRef, open, () => setOpen(false), { portal: magpieRequestsDialogRef });
339
367
  useDismissOnOutsidePointer(resetDialogRef, resetOpen && !resetPending, closeResetDialog);
340
368
  if (!eligible || snapshot === null)
341
369
  return null;
342
370
  if (snapshot.magpie !== null) {
343
371
  const magpie = snapshot.magpie;
344
- const magpieRemaining = resolveMagpieQuotaRemaining(magpie);
372
+ const readout = resolveMagpieTriggerReadout(magpie);
345
373
  const label = t('usage.magpieUsageLabel');
346
374
  return createElement('div', { ref: rootRef, style: subscriptionRootStyle }, createElement('button', {
347
375
  type: 'button',
@@ -356,9 +384,18 @@ function CommandCodeSubscriptionSlot(props) {
356
384
  width: 20,
357
385
  height: 20,
358
386
  style: magpieTriggerImageStyle,
359
- })), magpieRemaining === null
360
- ? createElement('span', { className: 'codingns4dsh-subscription-label', style: subscriptionLabelStyle }, t('usage.magpieShortLabel'))
361
- : createRemainingRing(magpieRemaining)), open && createElement(MagpieUsagePopover, { usage: magpie, t, tab: magpieTab, onTabChange: setMagpieTab, placement: magpiePlacement, popoverRef: magpiePopoverRef }));
387
+ })), createMagpieTriggerReadout(readout, t)), open && createElement(MagpieUsagePopover, {
388
+ rpc: props.rpc,
389
+ usage: magpie,
390
+ t,
391
+ tab: magpieTab,
392
+ onTabChange: setMagpieTab,
393
+ expandedGroups: expandedQuotaGroups,
394
+ onToggleGroup: toggleQuotaGroup,
395
+ placement: magpiePlacement,
396
+ popoverRef: magpiePopoverRef,
397
+ requestsDialogRef: magpieRequestsDialogRef,
398
+ }));
362
399
  }
363
400
  const usage = snapshot.providerUsage;
364
401
  // 未拿到真实订阅数据时不占用底部栏空间;加载状态不能伪装成订阅存在。
@@ -439,7 +476,7 @@ function CommandCodeSubscriptionSlot(props) {
439
476
  dialogRef: resetDialogRef,
440
477
  }));
441
478
  }
442
- function MagpieUsagePopover({ usage, t, tab = 'usage', onTabChange = () => { }, placement = { left: 0, width: 400 }, popoverRef }) {
479
+ function MagpieUsagePopover({ rpc, usage, t, tab = 'usage', onTabChange = () => { }, expandedGroups = [], onToggleGroup = () => { }, placement = { left: 0, width: 400 }, popoverRef, requestsDialogRef }) {
443
480
  const aggregateRows = usage.aggregate === null ? [] : [
444
481
  [t('usage.magpieRequests'), formatMagpieNumber(usage.aggregate.calls)],
445
482
  [t('usage.magpieErrors'), formatMagpieNumber(usage.aggregate.errors)],
@@ -489,12 +526,14 @@ function MagpieUsagePopover({ usage, t, tab = 'usage', onTabChange = () => { },
489
526
  'aria-labelledby': `codingns4dsh-magpie-tab-${tab}`,
490
527
  style: magpieTabPanelStyle,
491
528
  }, tab === 'usage' ? MagpieUsagePanel({ usage, aggregateRows, sessionRows, t })
492
- : MagpieRoutePanel({ usage, quotas, t })));
529
+ : MagpieRoutePanel({ rpc, usage, quotas, t, expandedGroups, onToggleGroup,
530
+ ...(requestsDialogRef === undefined ? {} : { requestsDialogRef }),
531
+ })));
493
532
  }
494
533
  function MagpieUsagePanel({ usage, aggregateRows, sessionRows, t }) {
495
534
  return createElement('div', undefined, createElement('section', { style: magpieUsageSectionStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieAggregateTitle')), usage.usageStatus === 'available'
496
535
  ? createMagpieRows(aggregateRows)
497
- : createElement('div', { style: magpieMutedTextStyle }, t('usage.magpieUsageUnavailable'))), createElement('section', { style: magpieUsageSectionStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieSessionTitle')), usage.sessionId !== null && createElement('div', { style: magpieSessionIdentityStyle }, createElement('span', undefined, t('usage.magpieAgent', { agent: usage.agent })), createElement('code', { style: magpieSessionIdStyle }, usage.sessionId)), usage.session === null
536
+ : createElement('div', { style: magpieMutedTextStyle }, t('usage.magpieUsageUnavailable'))), createElement('section', { style: magpieUsageSectionStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieSessionTitle')), usage.sessionId !== null && createElement('div', { style: magpieSessionIdentityStyle }, createElement('span', undefined, t('usage.magpieAgent', { agent: usage.agent })), usage.sessionName !== undefined && createElement('strong', { style: magpieSessionNameStyle, title: usage.sessionName }, usage.sessionName), createElement('code', { style: magpieSessionIdStyle }, usage.sessionId)), usage.session === null
498
537
  ? createElement('div', { style: magpieMutedTextStyle }, usage.sessionId === null ? t('usage.magpieSessionIdUnavailable') : t('usage.magpieSessionNoUsage'))
499
538
  : createMagpieRows(sessionRows)));
500
539
  }
@@ -512,16 +551,34 @@ function createMagpieProviderBadges(usage, style) {
512
551
  return createElement('span', { style }, ...providers.map((provider) => createElement('span', { key: provider, style: magpieProviderBadgeStyle }, provider)));
513
552
  }
514
553
  function createMagpieQuotaCard(quota, t, index) {
515
- return createElement('div', { key: `${quota.provider}-${quota.user ?? 'account'}-${index}`, style: magpieQuotaCardStyle }, createElement('div', { style: magpieQuotaCardHeadingStyle }, createElement('span', { style: magpieQuotaNameStyle }, quota.alias ?? quota.name), createElement('span', { style: magpieQuotaHeadingMetaStyle }, quota.plan ?? quota.provider)), (quota.user !== null || quota.balance !== null) && createElement('div', { style: magpieQuotaIdentityStyle }, quota.user !== null && createElement('span', { title: quota.user }, t('usage.magpieQuotaAccount', { account: quota.user })), quota.balance !== null && createElement('span', undefined, t('usage.magpieQuotaBalance', { balance: quota.balance }))), quota.windows.length === 0
554
+ return createElement('div', { key: `${quota.provider}-${quota.user ?? 'account'}-${index}`, style: magpieQuotaCardStyle }, createElement('div', { style: magpieQuotaCardHeadingStyle }, createElement('span', { style: magpieQuotaNameWithLogoStyle }, createElement(MagpieProviderLogo, { identities: [quota.provider, quota.name], logoDataUrl: quota.logoDataUrl, size: 20, className: 'codingns4dsh-magpie-quota-logo', fallback: 'none' }), createElement('span', { style: magpieQuotaNameStyle }, quota.alias ?? quota.name)), createElement('span', { style: magpieQuotaHeadingMetaStyle }, quota.plan ?? quota.provider)), (quota.user !== null || quota.balance !== null) && createElement('div', { style: magpieQuotaIdentityStyle }, quota.user !== null && createElement('span', { title: quota.user }, t('usage.magpieQuotaAccount', { account: quota.user })), quota.balance !== null && createElement('span', undefined, t('usage.magpieQuotaBalance', { balance: quota.balance }))), quota.windows.length === 0
516
555
  ? createElement('div', { style: magpieQuotaMetaStyle }, t('usage.magpieQuotaNoWindows'))
517
556
  : quota.windows.map((window, windowIndex) => {
518
- const remaining = window.remainingPercent;
557
+ const remaining = quotaWindowRemaining(window);
558
+ // 积分窗口(WorkBuddy 的 `0 / 350`)按数量读更准确:百分比在这里没有意义。
559
+ const credits = formatQuotaWindowCredits(window);
519
560
  const amount = window.display ?? (window.amount !== null && window.limit !== null ? `${window.amount} / ${window.limit}${window.unit === null ? '' : ` ${window.unit}`}` : null);
520
- return createElement('div', { key: `${window.name}-${windowIndex}`, style: magpieQuotaWindowStyle }, createElement('div', { style: windowHeadingStyle }, createElement('span', undefined, window.name), remaining === null ? amount : createElement('strong', undefined, `${formatPercent(remaining)}%`)), remaining !== null && createElement('div', { role: 'progressbar', 'aria-label': t('usage.magpieQuotaRemaining', { name: window.name, percent: formatPercent(remaining) }), 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': remaining, style: barStyle }, createElement('span', { style: { ...barFillStyle, width: `${remaining}%` } })), (amount !== null || window.resetsAt !== null) && createElement('div', { style: magpieQuotaFooterStyle }, amount !== null && createElement('span', undefined, amount), window.resetsAt !== null && createElement('span', undefined, t('usage.magpieQuotaResets', { time: formatMagpieTimestamp(window.resetsAt) }))));
561
+ return createElement('div', { key: `${window.name}-${windowIndex}`, style: magpieQuotaWindowStyle }, createElement('div', { style: windowHeadingStyle }, createElement('span', undefined, window.name), credits !== null
562
+ ? createElement('strong', undefined, credits)
563
+ : remaining === null ? amount : createElement('strong', undefined, `${formatPercent(remaining)}%`)),
564
+ // 积分窗口用满格数量条展示已用比例,避免"0% 还能用"的误读。
565
+ remaining !== null && createElement('div', { role: 'progressbar', 'aria-label': t('usage.magpieQuotaRemaining', { name: window.name, percent: formatPercent(remaining) }), 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': remaining, style: barStyle }, createElement('span', { style: { ...barFillStyle, width: `${remaining}%` } })), (amount !== null || window.resetsAt !== null) && createElement('div', { style: magpieQuotaFooterStyle }, amount !== null && createElement('span', undefined, amount), window.resetsAt !== null && createElement('span', undefined, t('usage.magpieQuotaResets', { time: formatMagpieTimestamp(window.resetsAt) }))));
521
566
  }));
522
567
  }
523
- function MagpieRoutePanel({ usage, quotas, t }) {
568
+ /**
569
+ * 单个窗口若是按积分计量,返回"剩余 / 上限"文本;普通百分比窗口返回 null。
570
+ *
571
+ * 卡片会为每个窗口单独调用一次,所以这里按窗口自治判断,不依赖账号整体状态。
572
+ */
573
+ function formatQuotaWindowCredits(window) {
574
+ const credits = readWindowCreditBalance(window);
575
+ return credits === null ? null : formatQuotaCredits(credits);
576
+ }
577
+ function MagpieRoutePanel({ rpc, usage, quotas, t, expandedGroups, onToggleGroup, requestsDialogRef }) {
524
578
  const route = usage.route;
579
+ const groups = groupQuotasByProvider(quotas);
580
+ const sessionAccount = route?.providerAccount ?? null;
581
+ const requestSessionId = usage.requestSessionId ?? usage.session?.id ?? usage.sessionId;
525
582
  const quotaMessage = magpieQuotaUnavailableMessage(usage, quotas, t);
526
583
  const routeStatus = usage.routeStatus === 'no-route'
527
584
  ? t('usage.magpieRouteNoRequests')
@@ -543,7 +600,33 @@ function MagpieRoutePanel({ usage, quotas, t }) {
543
600
  ];
544
601
  return createElement('div', undefined, createElement('section', { style: magpieSectionStyle }, createElement('div', { style: magpieQuotaHeadingStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieQuotaTitle')), createMagpieProviderBadges(usage, magpieProviderBadgesStyle)), quotaMessage !== null
545
602
  ? createElement('div', { style: magpieEmptyStateStyle }, quotaMessage)
546
- : quotas.map((quota, index) => createMagpieQuotaCard(quota, t, index))), createElement('section', { style: magpieSectionStyle }, createElement('div', { style: magpieRouteHeadingStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieRouteTitle')), route !== null && createElement('span', { className: active ? 'codingns4dsh-magpie-route-node-active' : undefined, style: magpieRouteStatusStyle }, createElement('span', { style: magpieRouteStatusDotStyle }), createElement('strong', undefined, route.done === true ? t('usage.magpieRouteComplete') : route.done === false ? t('usage.magpieRouteRunning').replace(/\s*·\s*$/u, '') : route.status ?? '--'))), route === null
603
+ : groups.map((group) => {
604
+ const expanded = expandedGroups.includes(group.provider);
605
+ const multiple = group.accounts.length > 1;
606
+ return createElement('div', { key: group.provider, style: magpieQuotaCardStyle }, createQuotaGroupHeader({
607
+ group,
608
+ t,
609
+ expanded,
610
+ onToggle: multiple ? () => onToggleGroup(group.provider) : null,
611
+ }), group.accounts.map((quota, index) => createElement('div', { key: `${group.provider}-${quota.user ?? 'account'}-${index}` }, createQuotaAccountBlock(quota, {
612
+ t,
613
+ nowMs: Date.now(),
614
+ // 单账号厂商由组头承担名称,账号块不再重复。
615
+ showAccountName: multiple,
616
+ // 组未展开时只留会话账号的明细,其余账号折叠成余量行。
617
+ compact: multiple && !expanded && quota.active !== true && (sessionAccount === null || quota.user !== sessionAccount),
618
+ fallbackName: group.name === '' ? group.provider : group.name,
619
+ }))));
620
+ })), createElement('section', { style: magpieSectionStyle }, createElement('div', { style: magpieRouteHeadingStyle }, createElement('strong', { style: magpieSectionTitleStyle }, t('usage.magpieRouteTitle')), route !== null && createElement('div', { style: magpieRouteHeadingActionsStyle },
621
+ // “查看请求”入口与状态徽标同高、同排:入口在左,状态在右。
622
+ createElement(MagpieRouteRequests, {
623
+ rpc,
624
+ sessionId: requestSessionId,
625
+ sessionName: usage.sessionName ?? null,
626
+ agent: usage.agent,
627
+ t,
628
+ ...(requestsDialogRef === undefined ? {} : { requestsDialogRef }),
629
+ }), createElement('span', { className: active ? 'codingns4dsh-magpie-route-node-active' : undefined, style: magpieRouteStatusStyle }, createElement('span', { style: magpieRouteStatusDotStyle }), createElement('strong', undefined, route.done === true ? t('usage.magpieRouteComplete') : route.done === false ? t('usage.magpieRouteRunning').replace(/\s*·\s*$/u, '') : route.status ?? '--')))), route === null
547
630
  ? createElement('div', { style: magpieEmptyStateStyle }, routeStatus)
548
631
  : createElement('div', { style: magpieRouteHeroStyle }, createElement('div', { style: magpieRouteTrackStyle, 'aria-label': t('usage.magpieRouteFlowTitle'), role: 'list' }, ...routeStages.map(([label, model], index) => createElement('div', {
549
632
  key: `step-${index}`,
@@ -552,6 +635,203 @@ function MagpieRoutePanel({ usage, quotas, t }) {
552
635
  style: { ...magpieRouteTrackItemStyle, animationDelay: `${index * 90}ms` },
553
636
  }, createElement('div', { style: magpieRouteMarkerColumnStyle, 'aria-hidden': true }, createElement('div', { className: 'codingns4dsh-magpie-route-node', style: magpieRouteNodeStyle }, createElement('span', { style: magpieRouteNodeDotStyle })), index < routeStages.length - 1 && createElement('div', { style: magpieRouteConnectorStyle }, active && createElement('span', { className: 'codingns4dsh-magpie-route-pulse', style: magpieRoutePulseStyle }))), createElement('div', { style: magpieRouteStageCopyStyle }, createElement('span', { style: magpieRouteStageLabelStyle }, label), createElement('strong', { style: magpieRouteStageValueStyle, title: model }, model))))), routeRows.length > 0 && createElement('div', { style: magpieRouteMetaStyle }, ...routeRows.filter((row) => row[1] !== null).map(([label, value]) => createElement('div', { key: label, style: magpieRouteMetaItemStyle }, createElement('span', { style: magpieRouteMetaLabelStyle }, label), createElement('strong', { style: magpieRouteMetaValueStyle, title: value }, value)))), route.tries.length > 0 && createElement('div', { style: magpieAttemptsStyle }, ...route.tries.map((attempt, index) => createElement('div', { key: `${attempt.model ?? 'model'}-${index}`, style: magpieAttemptRowStyle }, createElement('span', { style: magpieAttemptModelStyle }, attempt.model ?? t('usage.magpieUnknownModel')), attempt.failed && createElement('span', { style: magpieFailedStyle }, t('usage.magpieFallback'))))))));
554
637
  }
638
+ /**
639
+ * 路由标题栏的“查看请求”入口:按钮、模态框与读取状态放在同一个子组件里。
640
+ *
641
+ * 弹层内容本身不持有 Hook(无浏览器测试会直接展开内容树),因此这里只由
642
+ * `MagpieRoutePanel` 渲染元素,真正的状态留在本组件内部。模态框内的请求明细
643
+ * 直接复用管理页的 `RequestsPage`,只把会话筛选固定为当前会话。
644
+ */
645
+ export function MagpieRouteRequests({ rpc, sessionId, sessionName, agent, t, requestsDialogRef }) {
646
+ const enabled = canReadMagpieSessionRequests(sessionId, agent);
647
+ const [open, setOpen] = useState(false);
648
+ // 弹层固定筛选一个会话:这里持有从头累计的所有已加载请求,而不是只留当前一页。
649
+ const [page, setPage] = useState(null);
650
+ const [offset, setOffset] = useState(0);
651
+ const [loading, setLoading] = useState(false);
652
+ const [error, setError] = useState(false);
653
+ const [reloadKey, setReloadKey] = useState(0);
654
+ const controllerRef = useRef(null);
655
+ const sequenceRef = useRef(0);
656
+ useEffect(() => {
657
+ if (!open || !enabled)
658
+ return;
659
+ const controller = new AbortController();
660
+ controllerRef.current = controller;
661
+ // 只有一次请求在飞:更晚的请求序号作废更早的回包。
662
+ const sequence = sequenceRef.current + 1;
663
+ sequenceRef.current = sequence;
664
+ setLoading(true);
665
+ setError(false);
666
+ void readMagpieSessionRequestPage(rpc, sessionId, agent, offset, controller.signal).then((next) => {
667
+ if (controller.signal.aborted || sequence !== sequenceRef.current)
668
+ return;
669
+ if (next === null) {
670
+ setError(true);
671
+ return;
672
+ }
673
+ setPage((current) => current === null || offset === 0
674
+ ? next
675
+ : { ...next, items: [...current.items, ...next.items] });
676
+ }).catch(() => {
677
+ if (!controller.signal.aborted && sequence === sequenceRef.current)
678
+ setError(true);
679
+ }).finally(() => {
680
+ if (!controller.signal.aborted && sequence === sequenceRef.current)
681
+ setLoading(false);
682
+ });
683
+ return () => {
684
+ controller.abort();
685
+ if (controllerRef.current === controller)
686
+ controllerRef.current = null;
687
+ };
688
+ }, [open, enabled, offset, reloadKey, rpc, sessionId, agent]);
689
+ // 滚到列表底部就自动追加下一页:149 条请求也能一口气看完,不必数着点“加载更多”。
690
+ // 用捕获阶段监听滚动,真实滚动容器(弹层主体、表格横向容器)都在子树里。
691
+ useEffect(() => {
692
+ if (!open || typeof document === 'undefined' || !enabled)
693
+ return;
694
+ const onScroll = (event) => {
695
+ const target = event.target;
696
+ if (!(target instanceof Element) || !target.closest('.codingns4dsh-magpie-requests-body'))
697
+ return;
698
+ if (target.scrollHeight - target.scrollTop - target.clientHeight > 160)
699
+ return;
700
+ if (loading || error || page === null || !page.hasMore)
701
+ return;
702
+ setOffset(page.items.length);
703
+ };
704
+ document.addEventListener('scroll', onScroll, true);
705
+ return () => document.removeEventListener('scroll', onScroll, true);
706
+ }, [open, enabled, loading, error, page]);
707
+ // 模态框覆盖整个视口,Esc 是移动端之外唯一的键盘关闭方式。
708
+ useEffect(() => {
709
+ if (!open || typeof document === 'undefined')
710
+ return;
711
+ // 捕获阶段拦截 Esc:只关闭模态框,不再让底部弹层的关闭监听把整个弹层一起收起。
712
+ const onKeyDown = (event) => {
713
+ if (event.key !== 'Escape')
714
+ return;
715
+ event.stopPropagation();
716
+ setOpen(false);
717
+ };
718
+ document.addEventListener('keydown', onKeyDown, true);
719
+ return () => document.removeEventListener('keydown', onKeyDown, true);
720
+ }, [open]);
721
+ const close = () => {
722
+ controllerRef.current?.abort();
723
+ controllerRef.current = null;
724
+ setOpen(false);
725
+ };
726
+ const openRequests = () => {
727
+ controllerRef.current?.abort();
728
+ setPage(null);
729
+ setOffset(0);
730
+ setError(false);
731
+ setLoading(true);
732
+ // 请求明细复用管理页组件,需要先装上管理页自己的样式表。
733
+ installMagpiePageStyles();
734
+ setOpen(true);
735
+ };
736
+ return createElement(Fragment, undefined, enabled && createElement('button', {
737
+ type: 'button',
738
+ className: 'codingns4dsh-magpie-route-requests',
739
+ onClick: openRequests,
740
+ 'aria-haspopup': 'dialog',
741
+ title: t('usage.magpieViewRequests'),
742
+ style: magpieRouteRequestsButtonStyle,
743
+ }, t('usage.magpieViewRequests')), enabled && open && createElement(MagpieSessionRequestsDialog, {
744
+ sessionId,
745
+ sessionName,
746
+ agent,
747
+ page,
748
+ offset,
749
+ loading,
750
+ error,
751
+ t,
752
+ onClose: close,
753
+ ...(requestsDialogRef === undefined ? {} : { dialogRef: requestsDialogRef }),
754
+ onOffsetChange: (value) => {
755
+ // 追加下一页;重试则从头重读,避免把同一页拼两遍。
756
+ if (value === 0)
757
+ setPage(null);
758
+ setOffset(value);
759
+ },
760
+ onRetry: () => { setLoading(true); setError(false); setReloadKey((value) => value + 1); },
761
+ }));
762
+ }
763
+ /** Host 只接受小写 Agent ID;会话与 Agent 必须成对出现,缺一不可。 */
764
+ function canReadMagpieSessionRequests(sessionId, agent) {
765
+ return sessionId !== null && sessionId.trim() !== '' && sessionId.length <= 512 && /^[a-z0-9][a-z0-9-]*$/u.test(agent);
766
+ }
767
+ /** 只读取今天的会话请求,与路由卡片和“当前 Agent 会话”用量保持同一时间口径。 */
768
+ async function readMagpieSessionRequestPage(rpc, sessionId, agent, offset, signal) {
769
+ const result = await callCodingNsRpcResult(rpc, 'magpie/usage/requests', {
770
+ period: 'today',
771
+ offset,
772
+ limit: MAGPIE_REQUESTS_PAGE_SIZE,
773
+ sessionId,
774
+ sessionAgent: agent,
775
+ }, signal);
776
+ if (!result.ok)
777
+ throw new Error(result.error.message);
778
+ const value = result.value;
779
+ return isRecord(value) && Array.isArray(value.items)
780
+ ? value
781
+ : null;
782
+ }
783
+ /**
784
+ * 请求模态框:外壳(遮罩、标题、关闭)由弹层提供,明细直接复用管理页的请求用量视图。
785
+ *
786
+ * 管理页视图自带统计条、桌面表格 / 移动卡片、分页和空状态,这里只负责把会话固定住,
787
+ * 不再维护一套重复的请求卡片渲染。
788
+ */
789
+ function MagpieSessionRequestsDialog({ sessionId, sessionName, agent, page, offset, loading, error, t, onClose, dialogRef, onOffsetChange, onRetry }) {
790
+ const dialog = createElement('div', {
791
+ className: 'codingns4dsh-magpie-requests-overlay',
792
+ ref: dialogRef,
793
+ role: 'presentation',
794
+ style: magpieRequestsOverlayStyle,
795
+ // 只有按在遮罩自身时才关闭;从内容内部按下再拖到遮罩上不会误关。
796
+ onPointerDown: backdropPointerDownHandler(onClose),
797
+ }, createElement('div', {
798
+ role: 'dialog',
799
+ 'aria-modal': true,
800
+ 'aria-label': t('usage.magpieRequestsTitle'),
801
+ className: 'codingns4dsh-magpie-requests-dialog',
802
+ tabIndex: -1,
803
+ autoFocus: true,
804
+ style: magpieRequestsDialogStyle,
805
+ }, createElement('div', { style: magpieRequestsHeadingStyle }, createElement('strong', { style: magpieRequestsTitleStyle }, t('usage.magpieRequestsTitle')), createElement('div', { style: magpieRequestsHeadingActionsStyle }, error && createElement('button', {
806
+ type: 'button',
807
+ className: 'codingns4dsh-magpie-requests-retry',
808
+ onClick: onRetry,
809
+ style: magpieRequestsRetryStyle,
810
+ }, t('magpie.refresh')), createElement('button', {
811
+ type: 'button',
812
+ className: 'codingns4dsh-magpie-requests-close',
813
+ onClick: onClose,
814
+ 'aria-label': t('usage.magpieRequestsClose'),
815
+ style: magpieRequestsCloseStyle,
816
+ }, '×'))), createElement('div', { className: 'codingns4dsh-magpie-requests-body', style: magpieRequestsBodyStyle },
817
+ // `.codingns-magpie-page` 提供管理页的 `--mp-*` 主题变量;外层样式再把它压进弹层尺寸。
818
+ createElement('div', { className: 'codingns-magpie-page codingns4dsh-magpie-requests-view' }, createElement(RequestsPage, {
819
+ page,
820
+ loading,
821
+ error,
822
+ selectedSession: { id: sessionId, agent, ...(sessionName === null ? {} : { dshSessionName: sessionName }) },
823
+ offset,
824
+ onOffsetChange,
825
+ onClearSession: null,
826
+ t,
827
+ // 弹层筛选的是单一会话,列表从头累计,直到整条会话的请求都加载完。
828
+ append: true,
829
+ })))));
830
+ // The usage popover has its own lower stacking context. Portaling the modal
831
+ // to body lets it actually cover the host sidebar instead of being clipped
832
+ // by the popover's parent context.
833
+ return typeof document === 'undefined' ? dialog : createPortal(dialog, document.body);
834
+ }
555
835
  function createMagpieRows(rows) {
556
836
  if (rows.length === 0)
557
837
  return createElement('div', { style: magpieMutedTextStyle }, '--');
@@ -954,20 +1234,74 @@ function createModelRow(model, unit) {
954
1234
  function resolveDisplayWindow(usage) {
955
1235
  return usage.primary ?? usage.secondary ?? usage.monthly;
956
1236
  }
1237
+ /** 配额解析完成前的补读间隔;与弹层打开时的路由读取共用串行轮询。 */
1238
+ const MAGPIE_QUOTA_RETRY_MS = 3_000;
1239
+ /**
1240
+ * Magpie 用量入口是否还需要带路由补读。
1241
+ *
1242
+ * Host 会为「已确认属于 Magpie、但会话身份还没写进账本」的模型下发带会话数据的
1243
+ * `provider-unavailable` 占位快照;还在启动中的网关则是 `unavailable`。
1244
+ * 两种状态都表示上游未知,必须按 `MAGPIE_QUOTA_RETRY_MS` 快速重新解析,否则首屏
1245
+ * 会一直停在官方余额或 Magpie 标签上,直到整页刷新。
1246
+ *
1247
+ * 上游已解析(`provider-unavailable` 但 `upstreamProvider` 有值,例如该厂商本来
1248
+ * 就不上报任何配额窗口)不再补读——再读也只是重复确认同一个结论。没有上游信息的
1249
+ * 占位快照才有资格要求补读,Host 也只在这种情况下才下发它。
1250
+ */
957
1251
  function shouldResolveMagpieQuota(usage) {
958
- return usage?.quotaStatus === 'provider-unavailable' && usage.sessionId !== null && usage.session !== null;
1252
+ if (usage === null || usage === undefined)
1253
+ return false;
1254
+ if (usage.quotaStatus !== 'available')
1255
+ return usage.upstreamProvider === null;
1256
+ return usage.sessionId !== null && usage.session !== null && (usage.quotas?.length ?? 0) === 0;
959
1257
  }
960
1258
  function resolveMagpieQuotaRemaining(usage) {
961
- if (usage.quotaStatus !== 'available')
962
- return null;
963
- for (const quota of usage.quotas) {
1259
+ for (const quota of usage.quotas ?? []) {
964
1260
  for (const window of quota.windows) {
965
- if (window.remainingPercent !== null)
966
- return window.remainingPercent;
1261
+ // A few Magpie responses report only `used`; the quota card derives the same
1262
+ // remaining share, so the compact trigger must use that normalized value too.
1263
+ const remaining = quotaWindowRemaining(window);
1264
+ if (remaining !== null)
1265
+ return remaining;
967
1266
  }
968
1267
  }
969
1268
  return null;
970
1269
  }
1270
+ function resolveMagpieTriggerReadout(usage) {
1271
+ const remaining = resolveMagpieQuotaRemaining(usage);
1272
+ if (remaining !== null)
1273
+ return { kind: 'ring', percent: remaining };
1274
+ const quotas = usage.quotas ?? [];
1275
+ const balance = resolveQuotaBalanceLabel(quotas);
1276
+ if (balance !== null)
1277
+ return { kind: 'money', text: balance };
1278
+ const credits = resolveQuotaCreditsLabel(quotas);
1279
+ if (credits !== null)
1280
+ return { kind: 'credits', text: credits };
1281
+ return { kind: 'label' };
1282
+ }
1283
+ /** 积分余额的展示文本:只给数量,单位放在 title 里,不占用底部栏宽度。 */
1284
+ function resolveQuotaCreditsLabel(quotas) {
1285
+ for (const quota of quotas) {
1286
+ const credits = resolveQuotaCreditBalance(quota);
1287
+ if (credits === null || !(credits.remaining > 0))
1288
+ continue;
1289
+ return formatQuotaNumber(credits.remaining);
1290
+ }
1291
+ return null;
1292
+ }
1293
+ function createMagpieTriggerReadout(readout, t) {
1294
+ switch (readout.kind) {
1295
+ case 'ring':
1296
+ return createRemainingRing(readout.percent);
1297
+ case 'money':
1298
+ return createElement('span', { className: 'codingns4dsh-subscription-value', style: magpieBalanceStyle, title: t('usage.accountBalance') }, readout.text);
1299
+ case 'credits':
1300
+ return createElement('span', { className: 'codingns4dsh-subscription-value', style: magpieBalanceStyle, title: t('usage.creditBalanceLabel') }, readout.text);
1301
+ default:
1302
+ return createElement('span', { className: 'codingns4dsh-subscription-label', style: subscriptionLabelStyle }, t('usage.magpieShortLabel'));
1303
+ }
1304
+ }
971
1305
  function selectDeepseekBalance(usage) {
972
1306
  return usage.balances.find((balance) => balance.currency.toUpperCase() === 'USD') ?? usage.balances[0] ?? null;
973
1307
  }
@@ -1103,6 +1437,8 @@ function formatCreditBalance(value) {
1103
1437
  const subscriptionRootStyle = { position: 'relative', minWidth: 0, display: 'inline-flex', alignItems: 'center' };
1104
1438
  const subscriptionTriggerStyle = { display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, border: 0, borderRadius: 14, padding: '0 8px 0 4px', color: dshThemeColor.labelSecondary, background: 'transparent', cursor: 'pointer', fontSize: uiFontSize(13), lineHeight: '20px' };
1105
1439
  const subscriptionLabelStyle = { whiteSpace: 'nowrap' };
1440
+ // 余额金额与积分和百分比环并排/替换显示:数字用等宽字形,避免刷新时抖动。
1441
+ const magpieBalanceStyle = { display: 'inline-flex', alignItems: 'center', whiteSpace: 'nowrap', color: dshThemeColor.labelSecondary, fontSize: uiFontSize(12), fontWeight: 600, fontVariantNumeric: 'tabular-nums' };
1106
1442
  const magpieTriggerIconStyle = { display: 'inline-flex', width: 20, height: 20, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', borderRadius: 6, background: dshThemeColor.surfaceSubtle };
1107
1443
  const magpieTriggerImageStyle = { display: 'block', width: '100%', height: '100%', objectFit: 'cover' };
1108
1444
  const magpieMutedTextStyle = { color: dshThemeColor.labelTertiary, fontSize: uiFontSize(12), lineHeight: '17px', overflowWrap: 'anywhere' };
@@ -1122,6 +1458,7 @@ const magpieStatStyle = { display: 'grid', alignContent: 'space-between', gap: 1
1122
1458
  const magpieStatLabelStyle = { minWidth: 0, color: dshThemeColor.labelTertiary, fontSize: uiFontSize(10), lineHeight: '13px', overflowWrap: 'anywhere' };
1123
1459
  const magpieRowValueStyle = { color: dshThemeColor.labelPrimary, fontSize: uiFontSize(14), fontWeight: 650, lineHeight: '18px', textAlign: 'right', overflowWrap: 'anywhere' };
1124
1460
  const magpieSessionIdentityStyle = { display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', gap: '2px 10px', minWidth: 0, color: dshThemeColor.labelSecondary, fontSize: uiFontSize(12), lineHeight: '16px' };
1461
+ const magpieSessionNameStyle = { maxWidth: '100%', overflow: 'hidden', color: dshThemeColor.labelPrimary, fontSize: uiFontSize(12), fontWeight: 650, textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
1125
1462
  const magpieSessionIdStyle = { flex: '1 1 120px', minWidth: 0, color: dshThemeColor.labelTertiary, fontSize: uiFontSize(10), lineHeight: '14px', whiteSpace: 'normal', overflowWrap: 'anywhere' };
1126
1463
  const magpieAttemptsStyle = { display: 'grid', gap: 5, paddingTop: 6 };
1127
1464
  const magpieAttemptRowStyle = { display: 'flex', justifyContent: 'space-between', gap: 14, fontSize: uiFontSize(12) };
@@ -1133,6 +1470,7 @@ const magpieProviderBadgeStyle = { flex: 'none', maxWidth: 220, overflow: 'hidde
1133
1470
  const magpieEmptyStateStyle = { display: 'grid', placeItems: 'center', minHeight: 36, padding: '8px 10px', color: dshThemeColor.labelTertiary, fontSize: uiFontSize(12), lineHeight: '17px', textAlign: 'center' };
1134
1471
  const magpieQuotaCardStyle = { display: 'grid', gap: 5, marginTop: 7, padding: '9px 10px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 9, background: dshThemeColor.surfaceSubtle };
1135
1472
  const magpieQuotaCardHeadingStyle = { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 10 };
1473
+ const magpieQuotaNameWithLogoStyle = { display: 'inline-flex', alignItems: 'center', gap: 6, minWidth: 0, overflow: 'hidden' };
1136
1474
  const magpieQuotaNameStyle = { minWidth: 0, overflow: 'hidden', color: dshThemeColor.labelPrimary, fontSize: uiFontSize(13), fontWeight: 650, textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
1137
1475
  const magpieQuotaHeadingMetaStyle = { flex: 'none', maxWidth: 160, overflow: 'hidden', color: dshThemeColor.labelTertiary, fontSize: uiFontSize(11), textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
1138
1476
  const magpieQuotaIdentityStyle = { display: 'flex', flexWrap: 'wrap', gap: '3px 12px', color: dshThemeColor.labelTertiary, fontSize: uiFontSize(11), lineHeight: '15px', overflowWrap: 'anywhere' };
@@ -1141,7 +1479,7 @@ const magpieQuotaWindowStyle = { display: 'grid', gap: 4, paddingTop: 3 };
1141
1479
  const magpieQuotaFooterStyle = { display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '3px 10px', color: dshThemeColor.labelTertiary, fontSize: uiFontSize(11), lineHeight: '15px' };
1142
1480
  const magpieRouteHeadingStyle = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10 };
1143
1481
  const magpieRouteHeroStyle = { display: 'grid', gap: 11, padding: '12px 13px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 12, background: `linear-gradient(145deg, ${dshThemeColor.surfaceSubtle}, transparent)` };
1144
- const magpieRouteStatusStyle = { display: 'inline-flex', alignItems: 'center', gap: 6, width: 'fit-content', padding: '3px 8px', borderRadius: 999, background: dshThemeColor.surfaceSubtle, color: dshThemeColor.labelSecondary, fontSize: uiFontSize(11), whiteSpace: 'nowrap' };
1482
+ const magpieRouteStatusStyle = { display: 'inline-flex', alignItems: 'center', gap: 6, width: 'fit-content', boxSizing: 'border-box', padding: '3px 8px', borderRadius: 999, background: dshThemeColor.surfaceSubtle, color: dshThemeColor.labelSecondary, fontSize: uiFontSize(11), lineHeight: '15px', whiteSpace: 'nowrap' };
1145
1483
  const magpieRouteStatusDotStyle = { width: 8, height: 8, borderRadius: '50%', background: dshThemeColor.accent };
1146
1484
  const magpieRouteTrackStyle = { display: 'grid', gap: 0, minWidth: 0, padding: '3px 0' };
1147
1485
  const magpieRouteTrackItemStyle = { display: 'grid', gridTemplateColumns: '18px minmax(0, 1fr)', alignItems: 'start', gap: 12, minWidth: 0, padding: '5px 8px', margin: '0 -8px', borderRadius: 8 };
@@ -1157,6 +1495,17 @@ const magpieRouteMetaStyle = { display: 'grid', gridTemplateColumns: 'repeat(2,
1157
1495
  const magpieRouteMetaItemStyle = { display: 'grid', alignContent: 'start', gap: 3, minWidth: 0, padding: '6px 8px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 8, background: dshThemeColor.menuBackground };
1158
1496
  const magpieRouteMetaLabelStyle = { color: dshThemeColor.labelTertiary, fontSize: uiFontSize(10), lineHeight: '13px' };
1159
1497
  const magpieRouteMetaValueStyle = { color: dshThemeColor.labelSecondary, fontSize: uiFontSize(11), fontWeight: 550, lineHeight: '15px', overflowWrap: 'anywhere' };
1498
+ // 请求入口与请求模态框:入口与状态徽标同高并排在标题栏,模态框覆盖视口并在内部滚动。
1499
+ const magpieRouteHeadingActionsStyle = { display: 'inline-flex', alignItems: 'center', gap: 8, minWidth: 0 };
1500
+ const magpieRouteRequestsButtonStyle = { flex: 'none', display: 'inline-flex', alignItems: 'center', gap: 4, boxSizing: 'border-box', padding: '2px 8px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 999, background: dshThemeColor.menuBackground, color: dshThemeColor.labelSecondary, fontFamily: 'inherit', fontSize: uiFontSize(11), lineHeight: '15px', cursor: 'pointer', whiteSpace: 'nowrap', transition: 'background .15s ease,color .15s ease' };
1501
+ const magpieRequestsOverlayStyle = { position: 'fixed', inset: 0, zIndex: 2147483000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, boxSizing: 'border-box', background: dshThemeColor.overlay };
1502
+ const magpieRequestsDialogStyle = { ...dshPopupSurfaceStyle, display: 'flex', flexDirection: 'column', gap: 10, width: 'min(96vw, 1320px)', maxHeight: 'min(88vh, 820px)', boxSizing: 'border-box', padding: 14, borderRadius: 14 };
1503
+ const magpieRequestsHeadingStyle = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, paddingBottom: 10, borderBottom: `1px solid ${dshThemeColor.border}` };
1504
+ const magpieRequestsHeadingActionsStyle = { display: 'inline-flex', alignItems: 'center', gap: 8, flex: 'none' };
1505
+ const magpieRequestsTitleStyle = { fontSize: uiFontSize(15), lineHeight: '20px' };
1506
+ const magpieRequestsCloseStyle = { flex: 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 28, height: 28, padding: 0, border: 0, borderRadius: '50%', background: 'transparent', color: dshThemeColor.labelSecondary, fontSize: uiFontSize(18), lineHeight: 1, cursor: 'pointer' };
1507
+ const magpieRequestsBodyStyle = { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', overscrollBehavior: 'contain' };
1508
+ const magpieRequestsRetryStyle = { padding: '5px 12px', border: `1px solid ${dshThemeColor.border}`, borderRadius: 999, background: dshThemeColor.menuBackground, color: dshThemeColor.labelSecondary, fontFamily: 'inherit', fontSize: uiFontSize(11), lineHeight: '15px', cursor: 'pointer' };
1160
1509
  const sub2apiIdentityStyle = { display: 'inline-flex', alignItems: 'center', gap: 5, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' };
1161
1510
  const sub2apiLogoStyle = { display: 'block', borderRadius: 4, objectFit: 'contain' };
1162
1511
  const deepseekBalanceIdentityStyle = { display: 'inline-flex', alignItems: 'center', gap: 5, whiteSpace: 'nowrap', fontVariantNumeric: 'tabular-nums' };