@starwish-ai/xingyuan-dsh 0.5.4 → 0.5.6
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/CHANGELOG.md +200 -0
- package/README.en-US.md +2 -1
- package/README.md +2 -1
- package/lib/client/api.js +23 -3
- package/lib/client/cards.js +16 -12
- package/lib/client/chart-labels.js +107 -0
- package/lib/client/hooks.js +17 -0
- package/lib/client/i18n.js +221 -20
- package/lib/client/pages/calendar.js +23 -9
- package/lib/client/pages/color-swatch.js +3 -0
- package/lib/client/pages/detail.js +58 -8
- package/lib/client/pages/format.js +9 -0
- package/lib/client/pages/growth.js +38 -9
- package/lib/client/pages/memory.js +48 -22
- package/lib/client/pages/quick-create.js +26 -4
- package/lib/client/pages/settings.js +67 -9
- package/lib/client/pages/task-line.js +31 -13
- package/lib/client/pages/tasks.js +31 -28
- package/lib/client/pages/today.js +44 -17
- package/lib/client/pages/wishes.js +32 -18
- package/lib/client/styles.js +166 -65
- package/lib/client/ui.js +60 -2
- package/lib/client/view-state.js +24 -0
- package/lib/client.js +998 -230
- package/lib/domain.js +68 -1
- package/lib/growth.js +9 -9
- package/lib/index.js +3 -1
- package/lib/opportunity.js +9 -2
- package/lib/pref-policy.js +11 -0
- package/lib/pref-settings.js +3 -1
- package/lib/preset/charts.js +3 -2
- package/lib/preset/hitl.js +33 -6
- package/lib/preset/prompts.js +21 -17
- package/lib/preset/side.js +1 -0
- package/lib/preset/tools.js +65 -48
- package/lib/routes/api.js +81 -90
- package/lib/routes/errors.js +5 -2
- package/lib/routes/index.js +47 -44
- package/lib/routes/pages-html.js +5 -4
- package/lib/session-log-repair.js +20 -1
- package/lib/sqlite.js +10 -2
- package/lib/store.js +117 -19
- package/lib/types/client/api.d.ts +2 -2
- package/lib/types/client/chart-labels.d.ts +32 -0
- package/lib/types/client/hooks.d.ts +5 -0
- package/lib/types/client/i18n.d.ts +111 -11
- package/lib/types/client/pages/format.d.ts +8 -0
- package/lib/types/client/pages/quick-create.d.ts +4 -0
- package/lib/types/client/pages/settings.d.ts +3 -0
- package/lib/types/client/pages/task-line.d.ts +4 -0
- package/lib/types/client/pages/types.d.ts +2 -1
- package/lib/types/client/styles.d.ts +7 -4
- package/lib/types/client/ui.d.ts +16 -1
- package/lib/types/client/view-state.d.ts +15 -0
- package/lib/types/domain.d.ts +9 -0
- package/lib/types/opportunity.d.ts +5 -0
- package/lib/types/pref-policy.d.ts +11 -0
- package/lib/types/preset/hitl.d.ts +7 -2
- package/lib/types/preset/tools.d.ts +3 -0
- package/lib/types/routes/api.d.ts +1 -1
- package/lib/types/routes/errors.d.ts +4 -3
- package/lib/types/routes/index.d.ts +3 -1
- package/lib/types/store.d.ts +30 -0
- package/lib/types/types.d.ts +3 -0
- package/lib/types.js +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,206 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.6] - 2026-08-29
|
|
9
|
+
|
|
10
|
+
> **Note:** this release is the follow-up to the v0.5.5 six-perspective quality
|
|
11
|
+
> pass: the same six reviews re-ran against 0.5.5 and every accepted finding is
|
|
12
|
+
> fixed here — one data-safety gate, honesty fixes in the growth ladder, focus
|
|
13
|
+
> management on the highest-frequency action, and a batch of contrast, copy and
|
|
14
|
+
> localization closures. No data format changes; existing databases and recorded
|
|
15
|
+
> sessions replay as before.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Write-path schema gate** (`makeXingyuanStore`): the storage domain validates
|
|
19
|
+
records only when cold-opening the medium — a write path that skipped a field
|
|
20
|
+
check (e.g. an over-long task name from the model) used to persist silently and
|
|
21
|
+
then brick the next startup (`invalid-record` refuses to open, plugin dead until
|
|
22
|
+
manual surgery). Every `put`/`update`/`global.set` is now guarded by the declared
|
|
23
|
+
zod schema at the single store assembly point and rejected at write time with a
|
|
24
|
+
stable `invalid_record` code; valid records parse as identity, reads pay nothing.
|
|
25
|
+
Locked by `test/domain-guard.test.ts`.
|
|
26
|
+
- **Network-failure copy**: connection-level fetch failures (dsh restarting) no
|
|
27
|
+
longer surface the browser's raw English (`Failed to fetch` / `Load failed`) in
|
|
28
|
+
the Chinese UI; they map to a localized "network request failed, retry" message.
|
|
29
|
+
- **Task rows keep keyboard focus after group-migrating actions**: a successful
|
|
30
|
+
check-in moves the row between the Today page's open/done lists and destroyed the
|
|
31
|
+
pressed button, dropping focus to `<body>`; focus now falls back to the page
|
|
32
|
+
title (the same pattern as undo). The same treatment covers a check-in that
|
|
33
|
+
closes a task anywhere task rows render, and claiming a task on the Tasks page,
|
|
34
|
+
where the row moves between status groups.
|
|
35
|
+
- **Memory page**: the search empty-state no longer flashes between keystrokes
|
|
36
|
+
while the debounce is pending, a settled zero-hit search now shows the "no
|
|
37
|
+
matches" copy instead of the "no memories yet" empty-library one (the server's
|
|
38
|
+
`total` is query-filtered, so the two were indistinguishable before), and the
|
|
39
|
+
key field's server minimum (2 characters) is validated inline with its guidance
|
|
40
|
+
message instead of a generic error.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- **Level rewards describe what exists**: the growth ladder no longer promises
|
|
44
|
+
avatar frames, task templates or style gating that the product does not have
|
|
45
|
+
(coach styles were always free in Settings); rewards are now the honorary title
|
|
46
|
+
itself, capped by the Lv.10 pinnacle. Server table and both dictionaries updated
|
|
47
|
+
together (the Chinese dictionary is locked to the server table by
|
|
48
|
+
`test/growth.test.ts`).
|
|
49
|
+
- **Levels read "荣誉" not "权益"** on the Growth page hero and in `get_growth_stats`
|
|
50
|
+
output, matching the honorary rewards.
|
|
51
|
+
- **Today plan keeps the achieved-closing row**: checking the last opportunity day
|
|
52
|
+
closes the task, which used to remove the row from the Today page entirely (the
|
|
53
|
+
counter shrank right after checking in). A closed-achieved task with a check-in
|
|
54
|
+
on that date stays in the done group with its undo entry; cancelling the check-in
|
|
55
|
+
revives the task through the existing freshness path.
|
|
56
|
+
- **Calendar hover preserves state colors**: hovering a partially/fully-completed
|
|
57
|
+
day no longer washes the semantic amber/green chip with the accent hover tint —
|
|
58
|
+
the state color is exactly what the hover was inspecting.
|
|
59
|
+
- **Contrast closures**: micro-action card secondary text and skipped steps use the
|
|
60
|
+
`label-on-2` pair and a higher muted opacity so both themes stay above the
|
|
61
|
+
4.5:1 line (the same lift applies to deleted wish cards in chat); calendar/
|
|
62
|
+
check-in-cell state foreground colors moved into the theme token region as
|
|
63
|
+
paired `--xyd-on-c2` / `--xyd-on-c3` / `--xyd-on-dcell` tokens.
|
|
64
|
+
- **Date validation accepts only real calendar dates**: `2026-02-30` used to pass
|
|
65
|
+
the format check (`Date.parse` rolls it over instead of returning NaN) and then
|
|
66
|
+
silently produce a task with an empty opportunity sequence — or land in the
|
|
67
|
+
check-in table as a fake day that skews the streak replay. Task due dates, wish
|
|
68
|
+
estimated dates and check-in target dates (including the any-date back-fill on
|
|
69
|
+
no-deadline tasks) now share the opportunity calculator's semantic `isIsoDate`
|
|
70
|
+
round-trip check, and the routes' duplicate implementation converges onto it.
|
|
71
|
+
- **Prompt coherence**: the check-in example moved out of the "no-confirm mode"
|
|
72
|
+
section into confirm mode (check-in is a confirm-gated write) and no longer
|
|
73
|
+
demonstrates a streak number the tool does not return; list-tool outputs tell the
|
|
74
|
+
model to use bracketed IDs only for follow-up calls, not to render them to the
|
|
75
|
+
user; `generate_chart`'s `wishId` parameter names the camelCase chart keys.
|
|
76
|
+
- **Memory search bar wraps** so the saved notice is not clipped off narrow panels.
|
|
77
|
+
- **Copy/wording**: the Settings preference-unavailable hint no longer hardcodes a
|
|
78
|
+
stale count; the English "clear all memories" dialog is grammatical for one item;
|
|
79
|
+
the Growth page EXP/experience units are consistent in Chinese; an unknown level
|
|
80
|
+
falls back to a localized name instead of an empty label; the 404 API error and
|
|
81
|
+
the wish-not-found tool/route error carry localizable codes; the English
|
|
82
|
+
check-in grid summary is grammatical for one cell; README wording aligned with
|
|
83
|
+
the UI ("连续加成", straight quotes in English).
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- **sqlite backend**: a corrupted table row now reports `malformed-medium` like the
|
|
87
|
+
global slot does, instead of a raw `SyntaxError` with a split troubleshooting path.
|
|
88
|
+
- **Mock generator**: the detail-panel fixture in `debug/gen-mock.ts` takes an id
|
|
89
|
+
parameter so the wish/task scenarios no longer emit duplicate DOM ids.
|
|
90
|
+
|
|
91
|
+
## [0.5.5] - 2026-08-28
|
|
92
|
+
|
|
93
|
+
> **Note:** this release is a six-perspective quality pass (product, visual design,
|
|
94
|
+
> end user, dsh-platform conformance, architecture, UX) over every interaction,
|
|
95
|
+
> display and code path. It closes the failure side of the habit loop — an expired
|
|
96
|
+
> task used to be indistinguishable from an achieved one and was a dead end on the
|
|
97
|
+
> pages — and makes the plugin honest in English wherever it previously leaked
|
|
98
|
+
> built-in Chinese vocabulary. No data format changes; existing databases and
|
|
99
|
+
> recorded sessions replay as before.
|
|
100
|
+
|
|
101
|
+
### Added
|
|
102
|
+
- **Expired vs achieved, end to end**: task views carry `closedReason`; the Tasks
|
|
103
|
+
page splits the former "Closed" bucket into **Achieved** and **Expired** groups,
|
|
104
|
+
task rows say which one they are, and an expired task's detail panel explains the
|
|
105
|
+
state and offers an in-page **extend due date → restart** action (same store write
|
|
106
|
+
path as the chat-side `update_task`). An expired task is no longer a dead row with
|
|
107
|
+
a delete button.
|
|
108
|
+
- **Confirm card language** (`xingyuan-pref.confirmLang`, default Chinese): the
|
|
109
|
+
platform does not expose UI language to host-side plugins (verified against rc.2),
|
|
110
|
+
so the in-chat confirmation card's header, buttons and question text now follow
|
|
111
|
+
this explicit setting with full English strings; the Settings page gains a
|
|
112
|
+
"Confirm card language" select with optimistic save and write verification.
|
|
113
|
+
- **Built-in chart vocabulary localizes on the client**: chart cards map known
|
|
114
|
+
built-in Chinese titles, enum labels (weekdays, hour buckets, task statuses,
|
|
115
|
+
series names) and fixed subtitles to the interface language, while user data
|
|
116
|
+
(wish/task names) and unknown values replay verbatim. Coverage is locked by
|
|
117
|
+
`test/chart-labels.test.ts` against the real chart builder, so a new built-in
|
|
118
|
+
word without a mapping fails the build.
|
|
119
|
+
- **Level names localize**: the Growth page renders level names and rewards from
|
|
120
|
+
the interface language by level number (server strings remain the Chinese
|
|
121
|
+
fallback); locked to the server table by the same test.
|
|
122
|
+
- **Route contract compliance**: `/xingyuan` prefix registration is effect-scoped —
|
|
123
|
+
the disposer returned by `webServer.register` is no longer discarded, so hot
|
|
124
|
+
reload / re-activation can no longer die on a duplicate-prefix throw or leave a
|
|
125
|
+
stale handler serving a closed domain. A loader-level remount test with a
|
|
126
|
+
duplicate-throwing webServer stub pins this.
|
|
127
|
+
- New contract tests: `style-contract` (no `color-mix(` anywhere; semi-transparent
|
|
128
|
+
colors only inside the token zone; key semantic tokens must exist in both
|
|
129
|
+
themes), `chart-labels` (built-in chart vocabulary fully covered by the
|
|
130
|
+
localization maps, and client level names locked verbatim to the server table),
|
|
131
|
+
`sqlite-backend` (version-mismatch rejection, corrupt-global rejection,
|
|
132
|
+
write → cold-read persistence), plus a tool-suite audit pinning the 10-minute
|
|
133
|
+
HITL timeout on every confirmation-waiting tool and the "use real IDs from list
|
|
134
|
+
results" note on every ID parameter.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
- **System prompts no longer assert gated behavior**: six spots claimed the system
|
|
138
|
+
"will pop a confirmation card" unconditionally — wrong when the write-confirmation
|
|
139
|
+
setting is off (tool descriptions already hedged; the prompts now hedge the same
|
|
140
|
+
way, "删除始终弹出" stays asserted since deletes always confirm).
|
|
141
|
+
- **Memory overwrite asks once**: creating a memory with an existing key showed the
|
|
142
|
+
overwrite confirmation, then the server rejection re-asked the identical question;
|
|
143
|
+
the confirmed path now submits with `overwrite: true` directly.
|
|
144
|
+
- **Settings tab-visibility tells the truth**: memory-mode (non-loopback) and
|
|
145
|
+
read-only states get their own notices instead of silently grey controls, and
|
|
146
|
+
writes verify post-settle snapshot like the chat-preferences controls, toasting
|
|
147
|
+
on silent revert (`scope.set()` resolves even on failure).
|
|
148
|
+
- **Honest stale-data degradation**: when a refetch fails after a successful action
|
|
149
|
+
(or on focus refetch), pages keep the loaded data and show a "refresh failed,
|
|
150
|
+
data may be outdated" banner with inline retry instead of flipping to a full-page
|
|
151
|
+
error; Today's retry now reloads both endpoints at once.
|
|
152
|
+
- **Motion & touch discipline**: the growth bar drops its layout-animating `height`
|
|
153
|
+
transition and the toggle knob animates `transform` only; every purely decorative
|
|
154
|
+
hover is gated behind `(hover:hover) and (pointer:fine)` so touch screens no
|
|
155
|
+
longer stick hover tints; error toasts use a dedicated assertive `role=alert`
|
|
156
|
+
container; the confirm dialog locks body scroll; after deletes/undoes destroy the
|
|
157
|
+
triggered row, focus moves to the page title instead of falling to `<body>`.
|
|
158
|
+
- **Cross-tab state survives**: memory search query, calendar month offset and
|
|
159
|
+
expanded task details persist across view-tab switches via a module-scoped
|
|
160
|
+
snapshot store; view pages scroll back to top on mount.
|
|
161
|
+
- **Visual tokens**: new `--xyd-label-on-2` pair fixes secondary text on secondary
|
|
162
|
+
surfaces (≈4.3:1 → ≥5:1 in both themes) across the task-card preview strip, group
|
|
163
|
+
count pills, level chips, future grid cells and the growth chart axis ticks;
|
|
164
|
+
success/warn washes, calendar state fills,
|
|
165
|
+
shadows and hero colors are explicit themed token pairs (no more shared-alpha
|
|
166
|
+
washes that vanished on dark); calendar hover wash no longer paints over the
|
|
167
|
+
picked chip; 待打卡 chips gain the inset ring their legend dot always had;
|
|
168
|
+
standalone dark "complete" chip darkens to AA contrast; 11px CJK text raises to
|
|
169
|
+
12px; month-adjacent days brighten from 1.8:1 to readable-but-dim; the mock
|
|
170
|
+
generator itself is `color-mix`-free again.
|
|
171
|
+
- **Error codes completed end-to-end**: `not_claimed`, `no_opportunity_left`,
|
|
172
|
+
`no_checkins`, `title_too_long`, `name_too_long`, `once_today_only`,
|
|
173
|
+
`payload_too_large`, `bad_json_body`, `bad_coach_style`, `bad_interests` now flow
|
|
174
|
+
from store/routes to localized client strings (English users no longer receive
|
|
175
|
+
raw Chinese domain messages); HTTP status suffixes localize their brackets.
|
|
176
|
+
- **Dates**: hover tooltips on the check-in grid and calendar cells localize while
|
|
177
|
+
aria keeps ISO precision; memory timestamps render as medium dates; the growth
|
|
178
|
+
chart axis uses Intl ticks (Aug 27 / 8-27); the English "Commit {date}" button
|
|
179
|
+
becomes "Early check-in · {date}" so the button itself carries check-in semantics.
|
|
180
|
+
- **Writes stay in one place**: memory delete/clear and profile updates go through
|
|
181
|
+
store use cases (`deleteMemory`, `clearMemories`, `updateProfileGlobal`) shared by
|
|
182
|
+
tools and routes; profile fields clamp at the same limits on both faces (nickname
|
|
183
|
+
50 / occupation 100 / interests 20×50) — the tool path was previously unbounded
|
|
184
|
+
and injects into every system prompt turn.
|
|
185
|
+
- **Batch reads honor the store contract**: wish/task list paths use
|
|
186
|
+
`freshWishes` and a single shared checkin-count index instead of per-item
|
|
187
|
+
full-table rescans for the completed-day counts; the
|
|
188
|
+
chart builder computes its checkin index only for the one chart that consumes it;
|
|
189
|
+
semantically invalid dates (`2026-13-01`) are rejected with a stable `bad_date`
|
|
190
|
+
code instead of an unlocalizable RangeError; session-log repair sweeps its
|
|
191
|
+
`.xy-repair-*` atomic-write temp files; `./types` export resolves types and
|
|
192
|
+
runtime to the same module.
|
|
193
|
+
- **Copy honesty**: delete confirmations (chat and pages) now mention the
|
|
194
|
+
micro-step plan that the cascade removes; quick-create forms soft-confirm on an
|
|
195
|
+
exact duplicate name/title (the chat path dedupes via `check_similar_*`, the
|
|
196
|
+
page path previously did not); wish progress bars expose `role=progressbar`; the
|
|
197
|
+
four create tools declare the same 10-minute HITL timeout as their siblings;
|
|
198
|
+
task-related ID parameters all carry the "use real IDs from list results"
|
|
199
|
+
discipline.
|
|
200
|
+
|
|
201
|
+
### Fixed
|
|
202
|
+
- `start_micro_action` gates on a freshened task status, so a task that expired
|
|
203
|
+
since its last write can no longer start a breakdown.
|
|
204
|
+
- `create_wish_with_tasks` no longer over-claims atomicity in its pre-validation
|
|
205
|
+
comment (task-field failures are honestly reported; the wish and created tasks
|
|
206
|
+
are kept).
|
|
207
|
+
|
|
8
208
|
## [0.5.4] - 2026-08-28
|
|
9
209
|
|
|
10
210
|
> **Note:** trailing chart statistics no longer count future pre-checked days (see
|
package/README.en-US.md
CHANGED
|
@@ -28,7 +28,7 @@ Tools are only mounted on sessions using the XingYuan preset; other sessions are
|
|
|
28
28
|
dsh plugin --profile web add @starwish-ai/xingyuan-dsh
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
After starting the Web GUI, installation succeeded when
|
|
31
|
+
After starting the Web GUI, installation succeeded when "星愿" (XingYuan) appears in the agent picker.
|
|
32
32
|
|
|
33
33
|
## Data & Backup
|
|
34
34
|
|
|
@@ -43,6 +43,7 @@ Business data lives at `~/.dsh/xingyuan/xingyuan.sqlite` and survives uninstall
|
|
|
43
43
|
| Write confirmation | Whether create / check-in writes need confirmation (deletion always does) |
|
|
44
44
|
| Memory injection limit | Max memories injected per turn |
|
|
45
45
|
| Tab visibility | Three modes (follow session / always show / always hide) plus per-tab toggles; defaults to follow session |
|
|
46
|
+
| Confirm card language | Language of the in-chat confirmation card (Chinese / English; the platform does not expose UI language to plugins, defaults to Chinese) |
|
|
46
47
|
|
|
47
48
|
## Development
|
|
48
49
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- **对话驱动**:自然语言创建愿望 / 任务,自动查重相似项
|
|
14
14
|
- **机会日打卡**:周期任务打卡、补卡、取消、未来预勾,进度跨天恒为最新
|
|
15
15
|
- **微行动拆解**:把无从下手的目标拆成 3–7 个小步,引导式逐个完成或跳过
|
|
16
|
-
- **成长体系**:等级 Lv.1–Lv.10
|
|
16
|
+
- **成长体系**:等级 Lv.1–Lv.10、连续加成、统计卡与近 30 天柱状图
|
|
17
17
|
- **会话视图页**:今日 / 愿望 / 任务 / 日历 / 成长 / 记忆 六个标签页,页面按钮直连动作接口;
|
|
18
18
|
默认仅星愿预设的会话显示,可在设置里切换「始终显示 / 始终隐藏」并按标签勾选
|
|
19
19
|
- **记忆**:重要记忆自动注入上下文(上限可配),支持增删改查
|
|
@@ -44,6 +44,7 @@ dsh plugin --profile web add @starwish-ai/xingyuan-dsh
|
|
|
44
44
|
| 写操作二次确认 | 创建 / 打卡类写操作是否确认(删除始终确认) |
|
|
45
45
|
| 记忆注入上限 | 每次注入上下文的记忆条数上限 |
|
|
46
46
|
| 标签页显示 | 三态模式(跟随会话 / 始终显示 / 始终隐藏)+ 六个标签逐个勾选;默认跟随会话 |
|
|
47
|
+
| 确认卡语言 | 对话中确认卡的显示语言(中文 / English;平台不向插件暴露界面语言,默认中文) |
|
|
47
48
|
|
|
48
49
|
## 开发
|
|
49
50
|
|
package/lib/client/api.js
CHANGED
|
@@ -24,13 +24,31 @@ const ERROR_KEY = {
|
|
|
24
24
|
already_claimed: 'err.already_claimed',
|
|
25
25
|
task_closed: 'err.task_closed',
|
|
26
26
|
due_past: 'err.due_past',
|
|
27
|
+
due_too_far: 'err.due_too_far',
|
|
27
28
|
bad_category_name: 'err.bad_category_name',
|
|
28
29
|
bad_color_key: 'err.bad_color_key',
|
|
29
30
|
bad_date: 'err.bad_date',
|
|
30
31
|
overwrite_required: 'err.overwrite_required',
|
|
32
|
+
not_claimed: 'err.not_claimed',
|
|
33
|
+
no_opportunity_left: 'err.no_opportunity_left',
|
|
34
|
+
no_checkins: 'err.no_checkins',
|
|
35
|
+
title_too_long: 'err.title_too_long',
|
|
36
|
+
name_too_long: 'err.name_too_long',
|
|
37
|
+
once_today_only: 'err.once_today_only',
|
|
38
|
+
payload_too_large: 'err.payload_too_large',
|
|
39
|
+
bad_json_body: 'err.bad_json_body',
|
|
40
|
+
bad_coach_style: 'err.bad_coach_style',
|
|
41
|
+
bad_interests: 'err.bad_interests',
|
|
31
42
|
};
|
|
32
|
-
/**
|
|
43
|
+
/** 网络层失败的浏览器原文是英文技术串(Chrome "Failed to fetch"、Safari "Load failed"、
|
|
44
|
+
* Firefox "NetworkError…"、undici "fetch failed"),直出会打破本地化承诺——识别后统一
|
|
45
|
+
* 转本地化文案。仅匹配 TypeError(fetch 在请求发出前失败的规范类型)中的已知模式,
|
|
46
|
+
* 其余 TypeError(代码缺陷)仍原样抛出便于排障。 */
|
|
47
|
+
const NETWORK_ERROR_PATTERN = /failed to fetch|load failed|networkerror|fetch failed/i;
|
|
48
|
+
/** 把任意抛错转成用户可读文案:网络失败/ActionError 按键本地化;其余直出消息。 */
|
|
33
49
|
export function describeError(e) {
|
|
50
|
+
if (e instanceof TypeError && NETWORK_ERROR_PATTERN.test(e.message))
|
|
51
|
+
return t('common.networkError');
|
|
34
52
|
if (!(e instanceof ActionError))
|
|
35
53
|
return e instanceof Error ? e.message : String(e);
|
|
36
54
|
// 服务端新增未知 code 时回落原始消息,避免渲染 undefined 或插值崩溃
|
|
@@ -72,18 +90,20 @@ export async function getJson(path) {
|
|
|
72
90
|
return parsePayload(response, () => t('common.requestFailed'));
|
|
73
91
|
}
|
|
74
92
|
async function parsePayload(response, fallback) {
|
|
93
|
+
// HTTP 层错误的状态码后缀走本地化键(zh 全角 / en 半角括号),不硬编码全角括号
|
|
94
|
+
const withStatus = (message) => `${message}${t('common.httpStatus', { status: response.status })}`;
|
|
75
95
|
let payload;
|
|
76
96
|
try {
|
|
77
97
|
payload = await response.json();
|
|
78
98
|
}
|
|
79
99
|
catch {
|
|
80
|
-
throw new Error(
|
|
100
|
+
throw new Error(withStatus(fallback()));
|
|
81
101
|
}
|
|
82
102
|
if (!response.ok) {
|
|
83
103
|
if (typeof payload.code === 'string' && payload.code !== '') {
|
|
84
104
|
throw new ActionError(payload.code, payload.error ?? fallback(), payload.params);
|
|
85
105
|
}
|
|
86
|
-
throw new Error(payload.error
|
|
106
|
+
throw new Error(payload.error !== undefined ? withStatus(payload.error) : withStatus(fallback()));
|
|
87
107
|
}
|
|
88
108
|
return payload;
|
|
89
109
|
}
|
package/lib/client/cards.js
CHANGED
|
@@ -10,6 +10,7 @@ import { postAction, ActionError, describeError } from './api.js';
|
|
|
10
10
|
import { toast, toastError } from './ui.js';
|
|
11
11
|
import { localYmd, softConfirm } from './hooks.js';
|
|
12
12
|
import { useXyT, t, activeLocale } from './i18n.js';
|
|
13
|
+
import { localizeChartLabel, localizeChartSeries, localizeChartSubtitle, localizeChartTitle } from './chart-labels.js';
|
|
13
14
|
import { categoryVars } from '../category-color.js';
|
|
14
15
|
import { cycleLabel, durationText, dateSuffix, formatMediumDate, formatShortDate } from './pages/format.js';
|
|
15
16
|
function WishView(props) {
|
|
@@ -147,21 +148,23 @@ function ChartView(props) {
|
|
|
147
148
|
const event = props.node.data.data;
|
|
148
149
|
// 悬停柱下标(与成长页悬浮明细同一交互语法;null = 未悬停)
|
|
149
150
|
const [hoverIdx, setHoverIdx] = useState(null);
|
|
150
|
-
|
|
151
|
+
const title = localizeChartTitle(event);
|
|
152
|
+
return createElement('div', { className: 'xy-card xy-chart' }, createElement('div', { className: 'xy-card-head' }, createElement('span', { className: 'xy-badge xy-badge-chart' }, t('badge.chart')), createElement('span', { className: 'xy-title' }, title), event.subtitle !== undefined ? createElement('span', { className: 'xy-meta' }, localizeChartSubtitle(event.subtitle)) : null),
|
|
151
153
|
// 快照时点标注(事件溯源:卡片=当时的事实):无 generatedAt 的旧事件诚实降级不显示。
|
|
152
154
|
// 中格式带年份——回放的旧卡可能跨年,「生成于 3月5日」须能定位到哪一年。
|
|
153
155
|
// ISO → 本地日再格式化:直接 slice 会把 UTC 日期当本地日(UTC+8 零点后 8 小时内差一天)
|
|
154
156
|
event.generatedAt !== undefined
|
|
155
157
|
? createElement('div', { className: 'xy-meta' }, t('chart.generatedAt', { date: formatMediumDate(localYmd(new Date(event.generatedAt))) }))
|
|
156
|
-
: null, chartBody(event, t('chart.noData'), t('chart.noSchedule'), hoverIdx, setHoverIdx));
|
|
158
|
+
: null, chartBody(event, title, t('chart.noData'), t('chart.noSchedule'), hoverIdx, setHoverIdx));
|
|
157
159
|
}
|
|
158
|
-
/** 图表卡主体:四类渲染路径(占比条/占比列表/热力格/柱图)颜色全部走主题变量,深浅色自适应。
|
|
159
|
-
|
|
160
|
+
/** 图表卡主体:四类渲染路径(占比条/占比列表/热力格/柱图)颜色全部走主题变量,深浅色自适应。
|
|
161
|
+
* 标签/序列名经 chart-labels 本地化(服务端内建中文词的展示层映射,未知值原样回显)。 */
|
|
162
|
+
function chartBody(event, title, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
160
163
|
const { chartType, data } = event;
|
|
161
164
|
if (data.length === 0)
|
|
162
165
|
return createElement('div', { className: 'xy-meta' }, noDataText);
|
|
163
166
|
const max = Math.max(...data.filter((d) => !d.inactive).map((d) => d.value), 1);
|
|
164
|
-
const labelValue = (d) => `${d.label}${activeLocale() === 'en' ? ': ' : ':'}${d.inactive === true ? noScheduleText : d.value}`;
|
|
167
|
+
const labelValue = (d) => `${localizeChartLabel(d.label)}${activeLocale() === 'en' ? ': ' : ':'}${d.inactive === true ? noScheduleText : d.value}`;
|
|
165
168
|
if (chartType === 'arcbars') {
|
|
166
169
|
const ratio = Math.min(Math.max(data[0].ratio ?? 0, 0), 1);
|
|
167
170
|
return createElement('div', { className: 'xy-arcwrap' }, createElement('div', { className: 'xy-arcnum' }, `${Math.round(ratio * 100)}%`),
|
|
@@ -171,7 +174,7 @@ function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
|
171
174
|
if (chartType === 'pie' || chartType === 'radar') {
|
|
172
175
|
// 占比分母守卫:总和为 0 时跳过百分比(避免 NaN%)
|
|
173
176
|
const sum = data.reduce((s, x) => s + x.value, 0);
|
|
174
|
-
return createElement('ul', { className: 'xy-rows' }, ...data.map((d, i) => createElement('li', { key: i, className: 'xy-row' }, createElement('span', null, d.label), createElement('span', { className: 'xy-rowval' }, `${d.value}${chartType === 'pie' && sum > 0 ? ` · ${Math.round((d.value / sum) * 100)}%` : ''}`))));
|
|
177
|
+
return createElement('ul', { className: 'xy-rows' }, ...data.map((d, i) => createElement('li', { key: i, className: 'xy-row' }, createElement('span', null, localizeChartLabel(d.label)), createElement('span', { className: 'xy-rowval' }, `${d.value}${chartType === 'pie' && sum > 0 ? ` · ${Math.round((d.value / sum) * 100)}%` : ''}`))));
|
|
175
178
|
}
|
|
176
179
|
if (chartType === 'heatmap') {
|
|
177
180
|
return createElement('div', null, createElement('div', { className: 'xy-heat', 'aria-hidden': 'true' }, ...data.map((d, i) => createElement('span', {
|
|
@@ -181,7 +184,7 @@ function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
|
181
184
|
style: { opacity: String(0.25 + 0.75 * (d.value / max)) },
|
|
182
185
|
}))),
|
|
183
186
|
// 热力格纯视觉(title 不构成可访问名):数据以屏内隐藏文本整体提供给读屏
|
|
184
|
-
createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${d.label} ${d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
187
|
+
createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${localizeChartLabel(d.label)} ${d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
185
188
|
}
|
|
186
189
|
const seriesList = [...new Set(data.map((d) => d.series).filter((s) => s !== undefined))];
|
|
187
190
|
const seriesColor = (index) => index <= 0 ? 'var(--xyd-accent)' : 'var(--dsw-alias-label-secondary)';
|
|
@@ -204,7 +207,8 @@ function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
|
204
207
|
return [createElement('path', {
|
|
205
208
|
key: i,
|
|
206
209
|
d: topRoundedBar(i * step + 2, height - 18 - h, Math.max(step - 4, 2), h),
|
|
207
|
-
|
|
210
|
+
// 次要序列 opacity 0.8:0.55 曾把次要柱对卡底压到 2.1:1(非文字 3:1 下限之下)
|
|
211
|
+
style: { fill: seriesIdx > 0 ? seriesColor(seriesIdx) : 'var(--xyd-accent)', opacity: seriesIdx > 0 ? 0.8 : 0.85 },
|
|
208
212
|
onMouseEnter: () => onHover(i),
|
|
209
213
|
},
|
|
210
214
|
// 每根柱子带 <title>:原生悬停提示;精确数值另由上方悬浮明细条实时呈现
|
|
@@ -218,9 +222,9 @@ function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
|
218
222
|
.map(({ d, i }) => createElement('text', {
|
|
219
223
|
key: `l${i}`, x: i * step + step / 2, y: height - 4,
|
|
220
224
|
fontSize: 11, textAnchor: 'middle', style: { fill: 'var(--dsw-alias-label-secondary)' },
|
|
221
|
-
}, d.label));
|
|
225
|
+
}, localizeChartLabel(d.label)));
|
|
222
226
|
const legend = seriesList.length > 1
|
|
223
|
-
? createElement('div', { className: 'xy-chart-legend' }, ...seriesList.map((name, idx) => createElement('span', { key: name }, createElement('i', { className: 'xy-dot', style: { background: seriesColor(idx), opacity: idx > 0 ? 0.
|
|
227
|
+
? createElement('div', { className: 'xy-chart-legend' }, ...seriesList.map((name, idx) => createElement('span', { key: name }, createElement('i', { className: 'xy-dot', style: { background: seriesColor(idx), opacity: idx > 0 ? 0.8 : 1 } }), localizeChartSeries(name))))
|
|
224
228
|
: null;
|
|
225
229
|
const hovered = hoverIdx !== null ? data[hoverIdx] : undefined;
|
|
226
230
|
return createElement('div', null, legend, createElement('div', { onMouseLeave: () => onHover(null) },
|
|
@@ -229,8 +233,8 @@ function chartBody(event, noDataText, noScheduleText, hoverIdx, onHover) {
|
|
|
229
233
|
// 图表整体作为一张图暴露给读屏;逐条数值以屏内隐藏文本补充
|
|
230
234
|
createElement('svg', {
|
|
231
235
|
viewBox: `0 0 ${width} ${height}`, className: 'xy-svg',
|
|
232
|
-
role: 'img', 'aria-label':
|
|
233
|
-
}, createElement('line', { x1: 0, y1: height - 18, x2: width, y2: height - 18, style: { stroke: 'var(--dsw-alias-border-l2)' } }), ...bars, ...labels)), createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${d.label} ${d.inactive === true ? noScheduleText : d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
236
|
+
role: 'img', 'aria-label': title,
|
|
237
|
+
}, createElement('line', { x1: 0, y1: height - 18, x2: width, y2: height - 18, style: { stroke: 'var(--dsw-alias-border-l2)' } }), ...bars, ...labels)), createElement('span', { className: 'xy-visually-hidden' }, data.map((d) => `${localizeChartLabel(d.label)} ${d.inactive === true ? noScheduleText : d.value}`).join(activeLocale() === 'en' ? '; ' : ';')));
|
|
234
238
|
}
|
|
235
239
|
export const CARD_VIEWS = {
|
|
236
240
|
WishView,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表卡词表的客户端本地化。
|
|
3
|
+
*
|
|
4
|
+
* 背景(终端用户评审 P1):服务端 charts.ts 的内建标题/副标题/枚举标签是中文权威源,
|
|
5
|
+
* en 用户会在图表卡上直接看到「周一 / 本周 / 未分类」。事件是 whole-value 冻结快照,
|
|
6
|
+
* 历史事件只有中文标签,因此本地化放在消费端:按 chartKey 与「已知中文标签 → 键」
|
|
7
|
+
* 的映射表渲染,未知值(模型自定义标题、用户数据标签如愿望名)原样回显——诚实降级。
|
|
8
|
+
*
|
|
9
|
+
* 一致性锁定:映射表的中文键必须与 charts.ts 内建词逐字一致,由 test/chart-labels.test.ts
|
|
10
|
+
* 用真实 buildChart 输出对拍(内建词改动而映射表未跟 → 测试红)。
|
|
11
|
+
*/
|
|
12
|
+
import { t } from './i18n.js';
|
|
13
|
+
/** 各 chartKey 的内建默认标题(与 charts.ts `params.title ?? '…'` 逐字同源)。 */
|
|
14
|
+
export const CHART_DEFAULT_TITLES = {
|
|
15
|
+
checkinTrend: '打卡趋势',
|
|
16
|
+
checkinCalendar: '打卡日历',
|
|
17
|
+
taskCompletionRate: '任务完成率',
|
|
18
|
+
checkinRateTrend: '打卡完成率',
|
|
19
|
+
weekComparison: '本周打卡对比',
|
|
20
|
+
taskStatus: '任务状态分布',
|
|
21
|
+
checkinByCategory: '分类打卡分布',
|
|
22
|
+
wishCategory: '愿望分类分布',
|
|
23
|
+
checkinRanking: '打卡排行',
|
|
24
|
+
taskDistribution: '任务分布',
|
|
25
|
+
wishProgress: '愿望进度',
|
|
26
|
+
wishAchievement: '愿望达成率',
|
|
27
|
+
continuousCheckin: '连续打卡',
|
|
28
|
+
checkinTimeDistribution: '打卡时间分布',
|
|
29
|
+
weeklyActivity: '周度活跃度',
|
|
30
|
+
};
|
|
31
|
+
/** 已知中文枚举标签 → 本地化键(weekComparison/weeklyActivity 星期、时段桶、状态、系列名等)。
|
|
32
|
+
* 导出供 test/chart-labels.test.ts 做「服务端内建词全覆盖」对拍锁定。 */
|
|
33
|
+
export const CHART_LABEL_KEYS = {
|
|
34
|
+
周一: 'chart.weekday.1',
|
|
35
|
+
周二: 'chart.weekday.2',
|
|
36
|
+
周三: 'chart.weekday.3',
|
|
37
|
+
周四: 'chart.weekday.4',
|
|
38
|
+
周五: 'chart.weekday.5',
|
|
39
|
+
周六: 'chart.weekday.6',
|
|
40
|
+
周日: 'chart.weekday.7',
|
|
41
|
+
'凌晨(0-5)': 'chart.hour.0',
|
|
42
|
+
'早晨(6-8)': 'chart.hour.1',
|
|
43
|
+
'上午(9-11)': 'chart.hour.2',
|
|
44
|
+
'中午(12-13)': 'chart.hour.3',
|
|
45
|
+
'下午(14-17)': 'chart.hour.4',
|
|
46
|
+
'傍晚(18-19)': 'chart.hour.5',
|
|
47
|
+
'夜间(20-23)': 'chart.hour.6',
|
|
48
|
+
待领取: 'task.status.pending',
|
|
49
|
+
进行中: 'task.status.in_progress',
|
|
50
|
+
已完结: 'task.status.closed',
|
|
51
|
+
未分类: 'chart.uncategorized',
|
|
52
|
+
未关联: 'chart.unlinked',
|
|
53
|
+
完成率: 'chart.label.completionRate',
|
|
54
|
+
达成率: 'chart.label.achievementRate',
|
|
55
|
+
'当前/最长': 'chart.label.currentVsBest',
|
|
56
|
+
};
|
|
57
|
+
export const CHART_SERIES_KEYS = {
|
|
58
|
+
本周: 'chart.series.this',
|
|
59
|
+
上周: 'chart.series.last',
|
|
60
|
+
};
|
|
61
|
+
/** 数据点标签本地化:已知中文枚举标签映射,其余(用户数据)原样回显。 */
|
|
62
|
+
export function localizeChartLabel(label) {
|
|
63
|
+
const key = CHART_LABEL_KEYS[label];
|
|
64
|
+
return key !== undefined ? t(key) : label;
|
|
65
|
+
}
|
|
66
|
+
/** 分组序列名本地化(图例与柱色对应关系按原字符串分组,故仅展示层替换)。 */
|
|
67
|
+
export function localizeChartSeries(series) {
|
|
68
|
+
const key = CHART_SERIES_KEYS[series];
|
|
69
|
+
return key !== undefined ? t(key) : series;
|
|
70
|
+
}
|
|
71
|
+
/** 标题本地化:内建默认标题按 chartKey 映射;模型自定义标题原样回显。 */
|
|
72
|
+
export function localizeChartTitle(event) {
|
|
73
|
+
const key = `chart.title.${event.chartKey}`;
|
|
74
|
+
if (event.title === CHART_DEFAULT_TITLES[event.chartKey])
|
|
75
|
+
return t(key);
|
|
76
|
+
return event.title;
|
|
77
|
+
}
|
|
78
|
+
/** 已知中文固定副标题 → 本地化键。 */
|
|
79
|
+
export const CHART_SUBTITLE_KEYS = {
|
|
80
|
+
最近一年: 'chart.subtitle.lastYear',
|
|
81
|
+
'按任务 TopN': 'chart.subtitle.byTaskTopN',
|
|
82
|
+
'按愿望 TopN': 'chart.subtitle.byWishTopN',
|
|
83
|
+
'按实际打卡时刻分桶;过滤范围为打卡日区间': 'chart.subtitle.byHourBucket',
|
|
84
|
+
'%(应打天数完成率)': 'chart.subtitle.progressPercent',
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* 副标题本地化:固定短语查表;含数字的模板(近 N 天 / A/B 天 / 当前 A 天 / 最长 B 天 /
|
|
88
|
+
* 含未领取任务)按正则重排——服务端模板变化时未知副标题原样回显(诚实降级)。
|
|
89
|
+
*/
|
|
90
|
+
export function localizeChartSubtitle(subtitle) {
|
|
91
|
+
const fixed = CHART_SUBTITLE_KEYS[subtitle];
|
|
92
|
+
if (fixed !== undefined)
|
|
93
|
+
return t(fixed);
|
|
94
|
+
let m = /^近 (\d+) 天$/.exec(subtitle);
|
|
95
|
+
if (m !== null)
|
|
96
|
+
return t('chart.subtitle.lastDays', { n: m[1] });
|
|
97
|
+
m = /^(\d+)\/(\d+) 天(含未领取任务)$/.exec(subtitle);
|
|
98
|
+
if (m !== null)
|
|
99
|
+
return t('chart.subtitle.daysInclPending', { done: m[1], due: m[2] });
|
|
100
|
+
m = /^(\d+)\/(\d+) 天$/.exec(subtitle);
|
|
101
|
+
if (m !== null)
|
|
102
|
+
return t('chart.subtitle.daysRatio', { done: m[1], due: m[2] });
|
|
103
|
+
m = /^当前 (\d+) 天 \/ 最长 (\d+) 天$/.exec(subtitle);
|
|
104
|
+
if (m !== null)
|
|
105
|
+
return t('chart.subtitle.streak', { current: m[1], best: m[2] });
|
|
106
|
+
return subtitle;
|
|
107
|
+
}
|
package/lib/client/hooks.js
CHANGED
|
@@ -130,6 +130,23 @@ export function useStableScrollbar() {
|
|
|
130
130
|
pin();
|
|
131
131
|
}, [pin]);
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* 视图页挂载时把祖先滚动容器滚回顶部:标签页切换时壳的滚动容器不重挂,
|
|
135
|
+
* 长列表页留下的滚动位置会原样套在下一个(可能更短的)页面上,出现「半屏空白」。
|
|
136
|
+
*/
|
|
137
|
+
export function useScrollTopOnMount() {
|
|
138
|
+
useLayoutEffect(() => {
|
|
139
|
+
let el = document.querySelector('.xy-page');
|
|
140
|
+
for (let depth = 0; el !== null && depth < 16; depth += 1, el = el.parentElement) {
|
|
141
|
+
const overflowY = getComputedStyle(el).overflowY;
|
|
142
|
+
if (overflowY === 'auto' || overflowY === 'scroll') {
|
|
143
|
+
if (el.scrollTop > 0)
|
|
144
|
+
el.scrollTop = 0;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}, []);
|
|
149
|
+
}
|
|
133
150
|
/**
|
|
134
151
|
* 写操作确认(应用内弹窗,替代原生 window.confirm):Promise 结算;
|
|
135
152
|
* 调用方以 `softConfirm(msg).then((ok) => { if (ok) … })` 表达「确认后才继续」。
|