Skip to content

feat: v0.2.0 — TS 重构 + P0/P1 全面升级 - #1

Merged
AlfredChaos merged 12 commits into
mainfrom
v0.2.0
Aug 18, 2026
Merged

feat: v0.2.0 — TS 重构 + P0/P1 全面升级#1
AlfredChaos merged 12 commits into
mainfrom
v0.2.0

Conversation

@AlfredChaos

Copy link
Copy Markdown
Owner

变更摘要

dsh-usage-panel v0.2.0 迭代:JS → TypeScript 全面重构 + P0/P1 全面升级(依据 iteration-strategy.md,竞品分析驱动的补短板/吸长处,main 保持冻结,本分支为 worktree 产物)。

  • TS 重构src/shared | src/host | src/client 三层,strict + noUncheckedIndexedAccess;esbuild 构建 + ModuleLoader 包装;运行时产物契约与 v0.1.0 完全一致(模拟 loader 验证 {apply, inject: ['slots','connection','locale']});exports.types 已提供。
  • 记账正确性(修复 v0.1.0 真实缺陷):落盘投影(ctx.sessionProjections 注册,stateVersion 校验)——重试同一步消息不再重复累计(authoritative 替换 provisional)、compaction 独立归因、llm/retry 独立计数、reasoning 不重复计、四桶互斥记账;fork session/end-seed 种子去重保留sessionQuery 全量重扫降级为回退路径,两路共用同一纯函数 reducer。
  • P0:① UTC 日期口径 + 显式声明;② 缓存命中率 KPI(第 4 张卡);③ 覆盖度诊断(替换静默 catch{continue});④ 会话用量排行(Top 10,标题折叠);⑤ zh/en i18n(DSH locale 服务,host 只返回机器可读错误码)。
  • P1:⑥ 落盘投影(见上);⑦ provider 维度;⑧ 导出 JSON/每日 CSV/模型 CSV(防公式注入 + RFC 4180 + BOM);⑨ localStorage SWR 缓存 + 版本化结构校验 + ErrorBoundary + 一键清缓存 + freshness 五态(强刷失败保留最后成功时间戳)。
  • 测试/CI:42 个单测(usage/format/projection/aggregate/export,Node 内置 test runner);ci.yml(typecheck→build→test→check-pack);publish.yml(OIDC Trusted Publishing + provenance + tag↔版本校验 + 打包门禁)。

P0/P1 完成清单(测试证据)

证据
UTC 日桶 tests/usage.test.ts:dayKeyUTC 跨时区用例
命中率公式 tests/usage.test.ts:hitRate 用例(四桶互斥契约)
覆盖度诊断 tests/aggregate.test.ts:finalizeOverview coverage 断言
会话下钻 tests/aggregate.test.ts:topSessions 排序 + 标题断言
i18n src/client/locales.ts 全量词典 + label thunk + locale/change 订阅
落盘投影 tests/projection.test.ts:14 用例(种子去重/归因/重试替换/压缩/step 提交)
provider tests/projection.test.ts byProvider + aggregate.test.ts providers 断言
导出 tests/export.test.ts:注入防护/RFC4180/BOM
缓存+边界 src/client/api.ts isUsable + boundary.tsx + freshness 五态
契约不变 host {name,inject,apply} 与 client {apply,inject} 导出核对

验证输出:npm run typecheck ✓ · npm run build ✓(干净重建)· npm test 42/42 ✓ · npm run check-pack ✓。

npm 打包门禁

tarball 实测清单(16 文件,无任何静态资产):lib/(js + d.ts)、cordis.patch.ymlREADME.mdREADME.zh-CN.mdLICENSEpackage.jsonscripts/check-pack.mjsprepublishOnly + CI,任何图片进包即失败。

决策文档

  • P2 四项(费用/预算/悬浮窗/余额 API):全部 no-go,前提条件与推荐路线见 docs/P2-decisions.md
  • 开发期决策 D1–D12 见 DECISIONS.md

⚠️ 遗留人工步骤(PR 合并后)

  1. npmjs.com → dsh-usage-panel → Access:为仓库配置 Trusted Publishing(OIDC publisher)。
  2. npm version 0.2.0 → 合并回 main → push tag v0.2.0(publish.yml 会校验 tag 与 package.json 版本一致并执行 test/build/check-pack 后发布,无需本地 token)。
  3. 本 PR 未执行:合并、npm publishnpm version、打 tag。

- drop assets/ from the files whitelist (v0.1.0 shipped ~766KB of images)
- point README images at GitHub raw URLs so the npm page still renders
- add scripts/check-pack.mjs guard + prepublishOnly hook: fail publish
  when the tarball contains assets/ or any image file
- package.json: zod@4 runtime dep, @deepseek-ai/* rc.6 type devDeps, cordis 4.0.1, build/typecheck/test scripts, exports.types
- esbuild build pipeline + ModuleLoader wrapper + node --test runner
- src/shared: wire contract (extended, v0.1.0 shapes preserved), UTC day keys, four disjoint buckets, hit-rate, formatting (zh 亿/万, en K/M/B)
- tests: 19 cases locking aggregation/formatting semantics
Host (src/host):
- projection.ts: per-session persisted projection reducer — four disjoint
  buckets, last-marker seed boundary (fork dedup preserved), dual-source
  model/provider attribution, per-step provisional→authoritative replacement
  (retry dedup fixes v0.1.0 double counting), compaction attribution,
  llm/retry counting, UTC day keys, same-reference contract
- projection-unit.ts: registered unit (zod schema, stateVersion 1)
- aggregate.ts: cross-session merge → Overview (recent-30d window, session
  ranking, coverage, providers)
- scan.ts: v0.1.0 full-rescan fallback through the SAME reducer with
  seedLength-derived boundary + coverage counters (no silent catch{continue})
- index.ts: fail-soft mode detection (projection → scan → none), SWR cache,
  keep-warm, loopback RPC, warmup

Client (src/client, TSX components, single files < 300 lines):
- i18n zh/en via DSH locale service with machine-readable error codes
- KPI 4 cards incl. cache hit rate (P0-②), coverage card (P0-③), top
  sessions (P0-④), providers card (P1-⑦), export menu JSON/CSV with
  injection guard + BOM (P1-⑧), localStorage SWR cache + ErrorBoundary +
  freshness five states (P1-⑨), UTC heatmap/bars/donut
- ModuleLoader wrapper + esbuild pipeline; output contract identical to
  v0.1.0 (verified by simulated loader: {apply, inject: [slots,connection,locale]})

Tests: 42 (usage/format/projection/aggregate/export)
… update

- docs/P2-decisions.md: all four P2 items no-go with prerequisites and
  recommended versions (v0.3.0 candidates)
- .github/workflows: ci.yml (typecheck/build/test/check-pack) + publish.yml
  (OIDC Trusted Publishing, provenance, tag↔version guard, pack gate; no
  NODE_AUTH_TOKEN — OIDC only)
- README.md / README.zh-CN.md: new features (hit rate, coverage, sessions,
  providers, export), UTC timezone declaration, TS implementation table
- AGENTS.md: real commands, shared-reducer data path, v0.2.0 pitfalls (§6.4)
- DECISIONS.md: D1-D12 decision log with acceptance evidence
- host: local cordis interval augmentation (no runtime devDep import)
- lib/: rebuilt artifacts
Field issue: the page showed raw keys (nav.label, kpi.total ...) because the
dictionaries were registered under 'zh-CN'/'en-US' while the DSH locale
runtime ships LOCALE_IDS = ['zh','en'] — translate() then failed loud and
returned the key itself, which bypassed the old truthy-key fallback.

- register(NS, { zh: zhCN, en: enUS }) — mirrors the framework's own form
- treat translate() returning the key itself as a miss, then fall back to
  the local dictionaries; interpolation stays our single source
- i18n.locale is now a getter (field snapshot stayed stale after switches)
- ride locale.subscribe(update) (locale switches + late dict registrations
  both bump revision); ctx.on('locale/change') kept as backup wiring
- tests/locales.test.ts: fake runtime reproducing real semantics — 7 cases
  locking zh/en resolution, switch propagation and no-raw-key guarantee
zh 'providers.title' was half-English ('Provider 用量'). The DSH GUI's own
models page translates Provider as '提供方' (dsh-client-ui-settings-models),
but that term was flagged as awkward; use '服务商用量' instead.
- modelRows now carries each row's four disjoint buckets; the merged 'other'
  row sums the remaining models' buckets (tests/model-rows.test.ts, 4 cases)
- ModelDonut list gains a header row (model/tokens/share/hit rate) and a
  per-model hit-rate column + tooltip line, same formula as the KPI card
- settings-nav entry icon is shell-hardcoded (navIcon: only models /
  agent-presets / plugins have custom icons, everything else falls back to
  the gear — identical in rc.6 and upstream master), so a bar-chart icon is
  added to the page header instead (the plugin-side-controllable spot)
- zh/en dicts: donut.model/donut.tokens/donut.hitRate
… summary

The panel ran in scan fallback (no usagePanel rows in the projection cache);
these logs pinpoint which projection service the host cannot see.
…agent)

Field evidence: of the 32 sessions with usage in the user's corpus, 29 are
subagent sessions (delegationDepth >= 1) and only 3 are main sessions —
which explains why the sidebar (which does not surface subagent sessions)
shows far fewer sessions than the KPI card.

- CoverageStats: usageSessionsMain / usageSessionsSubagent (sessions with
  counted usage, split by header.delegationDepth; each subagent session
  counts as one, tokens stay merged into the shared buckets)
- SessionSummary.depth: the ranking card tags each row 主会话/子代理
- OVERVIEW_VERSION 3 (invalidates older cached payloads)
- coverage card line: 有用量会话:主 X · 子代理 Y
- tests: depth plumbing through mergeSessionValue/finalizeOverview
…e wipe

- Remove the standalone 统计覆盖度 card (CoverageCard.tsx, its CSS and the
  12 coverage.* i18n keys). The host still computes and ships
  payload.coverage, so the data path is unchanged.
- Fold the coverage counts into the 会话数量 KPI card as a secondary line:
  "总会话 {total} · 有用量会话:主 {main} · 子代理 {subagent}" (en mirror).
  The headline stays the sessions-with-usage count (allTime.sessionCount).
- Heatmap now wipes in left-to-right, mirroring the promo GIF: each week
  column fades in at w*0.018s delay over 0.45s (~0.9s total for 26 weeks).
  Blanks opt out via .dsw-ust-heat-blank; the effect is disabled under
  prefers-reduced-motion.
- Sync README.md / README.zh-CN.md and the wrap-client bundle header;
  rebuild lib/client.js. typecheck + 53 tests + check-pack all green.
tsconfig.host.json declares "types": ["node"], which requires the @types/node
package. It was never declared as a dependency, so the build only passed when
@types/node happened to be hoisted in an ancestor node_modules (as on my
machine). On CI's clean `npm ci` there is no ancestor node_modules, so tsc
failed with:

  error TS2688: Cannot find type definition file for 'node'.

This broke every CI run since the TS migration. Add @types/node (^22.10.0,
matching the Node 22 CI runner and `engines: node >=20`) to devDependencies.

Verified in an isolated clean checkout with no ancestor node_modules: npm ci
+ typecheck + build + test (53) + check-pack all pass.
@AlfredChaos
AlfredChaos merged commit bb45eb2 into main Aug 18, 2026
2 checks passed
@AlfredChaos
AlfredChaos deleted the v0.2.0 branch August 18, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant