dsh-cindy-host 0.1.32 → 0.1.34

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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.34 — 2026-11-05
4
+
5
+ - Project durable native failed turns as stable, sanitized error rows and terminal maker events in both cold history and live delivery. Preserve native failure/retry ownership, expose only fixed known-code messages and validated HTTP status, and repair reconnect state without stopping newer turns or leaking provider diagnostics.
6
+
7
+ - Register user questions before either surface displays them, including sessions a phone has never visited. Replay the existing pending cards on session subscribe/reconnect; keep them through temporary relay recovery and remove the hidden Host question timeout, without changing permission ownership/deadlines.
8
+ - Settle local and phone answers through the same pending completion; preserve native cancellation/timeout errors and clear both cards on task stop instead of returning empty/undefined answers. Keep the existing Cindy protocol and DSH native UI. Add focused Host regressions and opt-in tests against installed Cordis/userQuestions/Gateway; these are not live-handset acceptance or a production deployment.
9
+
10
+ ## 0.1.33 — 2026-11-04
11
+
12
+ - Add authorized current-surface reference-source history through native observations, real message identities, filters/cursors and explicit clipping metadata. Keep frozen target-reference capability false and reject structured contexts before every admission/edit/preparation/replay boundary; do not disguise source reads or plain links as target consumption.
13
+
14
+ - Adapt the existing sidebar custom-order calls to native registered Workspaces and insertBefore, with verified account/device/source stamps, all-key prevalidation, serial native moves and truthful partial/concurrent failure snapshots. Refuse activity and cross-device/unregistered/ambiguous variants; do not create another order store.
15
+
16
+ - Adapt existing-dialogue recurring reminder CRUD to the native schedule owner with complete-record update CAS, strict execution-field refusals, genuine one-time read/delete and known-created-ID recovery. Forward only committed ready notifications with authorized peer-lifetime cleanup; do not fabricate pause, run history, cost, completion or notifications. Cover the slice with focused Host tests only.
17
+
18
+ - Adapt the three existing subagent read endpoints to native parent/descendant catalogs, actual consumed-work/live-state evidence and private-context-safe child-owned transcripts. Keep all controls and unproved returned-result capability false; add signed 25-entry/byte-bounded continuation/tail paging and Host-only focused tests. Consume the SDK work reader as a host-provided peer, without installing core/profile packages.
19
+
20
+ - Add provider-backed Mainland/Global settings-page login: Apple/other enabled social providers, enterprise SSO, regional discovery/confirmation, code delivery, account selection, required binding and SSO verification. Use official endpoint manifests, hosted callback/PKCE/poll-secret or configured loopback fallback; keep all authentication tickets and tokens backend-only. CAPTCHA uses the official challenge page with an explicitly manual successful-URL return in a normal browser.
21
+ - Fence login, cancellation, logout and refresh on the serialized encrypted-credential owner; reject stale UI steps and cross-origin/non-JSON mutations. Rebuild an immutable regional connection context so device-link, media and ICE cannot receive another region's credentials. Include isolated protocol, HTTP, actual local callback, UI state and runtime URL regressions. These regressions do not establish real Apple/enterprise login or iOS acceptance.
22
+
23
+ - Adapt the official background-task roster to the native owner-fenced jobs registry and reuse native control frames for real task-status updates. Keep stopping nonterminal, omit unknown continuation claims, and never consume output, invent a file/provider/tool origin, or expose fictional stop/output channels. Cover Host owner/state mapping and stale-frame/source teardown fences with focused tests.
24
+
25
+ - Serve native literal keyword search with real current-message identities/counts, all accepted filters/sorts, genuine limits over 20, bounded scans and cancellation. Refuse explicit semantic/hybrid requests without an enabled native backend; do not pretend global FTS is mounted. Add focused Host protocol field-mapping tests using the official schema as a read-only reference.
26
+
27
+ - Adapt ordinary fork using committed message identities and exact native user-before/completed-assistant-turn cuts. Return the real child row, publish the existing created event, and retain child IDs across attachment/discovery/source failures without automatically creating another child. Unsupported branch/rewind/encrypted variants stay refused.
28
+
29
+ - Serve the existing compact-session channel using verified native scoped DSH commands, with strict instructions refusal, real no-op/error results, in-process duplicate-envelope protection, stop/disconnect cancellation, and history invalidation. The Claude queue-compaction protocol is not treated as an alias.
30
+
31
+ - Wire pin/unpin metadata to the native DSH workspace registry, with membership-only stable wire projection, committed desktop-to-controller refresh, idempotent retries, legacy-overlay retirement, and pre-write mixed-operation validation. No Cindy protocol extension or native timestamp/order claim.
32
+
33
+ - Fix mobile thinking visibility: send structured `{ text }` content instead of the prose string the mobile normalizer silently drops. Keep ordinary user/assistant prose unchanged.
34
+ - Forward public DSH reasoning chunks during generation, coalesce native thinking pushes, expose the active prefix through history/work details, and retire temporary rows when the durable or interrupted assistant message arrives. Do not transmit private replay metadata or count previews as durable activity.
35
+ - Add regression coverage for the real pinned mobile thinking normalizer, cold/live history, preview retirement, cancellation, routing and teardown. Package/runtime checks are not live-handset acceptance.
36
+ - Route native archive, reminder and project-order event subscriptions through the Host-owned guarded boundary, and pin native replacement fixtures to the real SDK `startSeq`/`endSeq` contract. Synchronize the development lock on the verified 0.2.1-rc.5 carrier while preserving the declared peer compatibility range.
37
+ - Improve the read-only channel audit for imported scalar constants, generic/double-quoted call sites and independent mobile consumers. Explicitly document actual refusals of optional controlled-model catalog, usage widget, machine status and unmapped migration/cloud/provider protocols; this does not enable those features or certify all new Cindy protocols.
38
+
3
39
  ## 0.1.32 — 2026-11-03
4
40
 
5
41
  - Remove keytar and its native npm build chain. Linux/macOS keep system credential vaults through secret-tool/security; Windows encrypts with built-in DPAPI CurrentUser and persists ciphertext through the native DSH credential record service. Login, renewal and logout share one installation-scoped store; failures never fall back to plaintext or pretend a stored login is absent. Windows upgrades may require one explicit login; legacy vault entries are left untouched.
package/README.md CHANGED
@@ -4,10 +4,12 @@ A Host-side adapter connecting current Cindy clients to DeepSeek Harness.
4
4
 
5
5
  ## Requirements and installation
6
6
 
7
- Version **0.1.32** additionally supports **DSH 0.2.2-alpha.2**, while retaining `0.2.1-rc.3`, `0.2.1-rc.4` and `0.2.1-rc.5`. Alpha requires **Node.js >=24.9** (tested with 24.12); the older carriers retain this plugin's Node.js >=22.16 minimum. Session/settings peers and alpha Schemastery are explicitly allowed, not a blanket promise for future prereleases. Install through the Desktop DSH plugin manager. CLI-managed profiles can use:
7
+ Version **0.1.34** improves native/phone question synchronization and cancellation, and safely displays durable model/turn failures. It retains 0.1.33's regional authentication UI, public reasoning streaming and native capability subsets.
8
+
9
+ It supports the exact **DSH 0.2.2-alpha.2** carrier, while retaining `0.2.1-rc.3`, `0.2.1-rc.4` and `0.2.1-rc.5`. Alpha requires **Node.js >=24.9** (tested with 24.12); the older carriers retain this plugin's Node.js >=22.16 minimum. Agent/session/settings peers and alpha Schemastery are explicitly allowed, not a blanket promise for future prereleases. The full Web installation gate uses DSH **0.2.1-rc.5**; alpha core-contract checks do not establish alpha Web/relay/handset acceptance. Install through the Desktop DSH plugin manager. CLI-managed profiles can use:
8
10
 
9
11
  ```sh
10
- dsh plugin --profile <profile> add dsh-cindy-host@0.1.32
12
+ dsh plugin --profile <profile> add dsh-cindy-host@0.1.34
11
13
  ```
12
14
 
13
15
  Reload the Host plugin after installation. A full DSH restart is separate. Keep existing Cindy sign-in, device identity and settings; do not run two Host processes with the same identity.
@@ -79,6 +81,38 @@ The ledger lives under the active DSH profile state directory. Pending payloads
79
81
 
80
82
  All changes are Host-only: no Cindy or DSH core patch is required. The handshake compatibility stamp is separate from the actual plugin version and explicit v1 capability. Unsupported optional features are refused; this is not a promise of all native Cindy features or exactly-once external tool effects.
81
83
 
84
+ ## Apple, enterprise SSO and regional login (0.1.33)
85
+
86
+ In **Settings → Cindy 手机连接**, choose the same **Mainland China / Global** account region as the phone, then load sign-in options. The Host discovers enabled providers from that region: Apple/social browser authorization, phone/email codes and enterprise SSO follow Cindy's official protocol. Provider availability is server-controlled; the same Apple ID does not merge accounts across regions. The browser opens the official Cindy authorization page; return to DSH for membership selection, required contact binding or extra SSO verification. Secrets and tickets stay in the Host, not settings or browser storage.
87
+
88
+ Organization ID/slug/domain and email discovery check both regions. A found organization in the other region requires explicit confirmation; ambiguous or partly failed discovery never silently picks a region. Cancel/start-over fences late browser, code and vault-write results. Login while the phone-connection switch is off saves only; it does not enable transport. Account/region changes rebuild the whole runtime so relay, device directory, media staging and ICE remain in the credential's region. Existing encrypted credentials are preserved; failures never erase them.
89
+
90
+ **CAPTCHA browser limitation:** when a region requires it for code delivery, open the official verification page, solve it, then copy its complete successful URL (including `#cindy-captcha=ok.…`) into the DSH form. The standard browser cannot read the cross-origin result automatically. The verification proof is sensitive and short-lived: do not share it. The Host validates its official origin and forwards only the proof to the requested auth action; it does not bypass verification or proxy/inject the challenge page. Apple/SSO browser authorization does not use this manual code-delivery step.
91
+
92
+ This feature ships in 0.1.33; installation and runtime activation are verified separately. Tests cover protocol, real local callbacks, settings-panel state, credential races and both regions' actual runtime URL routing with fake credentials. Real Apple/enterprise identity providers and iOS discovery/file round trips still require an authorized test account after deployment. See [authentication flow notes](doc/auth-login.md). The new browser flow is exposed by the settings page; the older interactive CLI is not a replacement for that UI on Windows.
93
+
94
+ ## Thinking content on Cindy mobile (0.1.33)
95
+
96
+ The Host sends DSH-provided public reasoning text/summaries as native structured `thinking` content (`{ text }`), not ordinary prose strings. Expand the work/activity group, then its thinking row, to read it. No Cindy or DSH core patch is needed. Models that expose no reasoning text still have nothing to show; signatures and provider-private replay metadata are never forwarded.
97
+
98
+ During generation, `assistant/chunk` reasoning deltas are displayed as temporary thinking rows. Pushes coalesce over 100 ms; the work-grouped history uses the existing coalesced view refresh. Opening a session mid-generation reconstructs the current prefix from the log. The final assistant message replaces the preview, including interrupted prefixes, without duplicating it. Temporary rows do not become durable message-count/grouping evidence, and timers are cleared on Host disconnect.
99
+
100
+ This ships in 0.1.33; handset acceptance is separate from package checks and local Host activation. Offline regressions execute the pinned real Cindy mobile thinking normalizer; live phone/network acceptance remains separate.
101
+
102
+ ## Questions on DSH and Cindy
103
+
104
+ While the Host bridge is enabled, questions are registered before presentation, even if no phone has opened that session. DSH's native question UI and the existing Cindy card can both answer; the first accepted answer settles the shared pending entry and dismisses the other surface. Session open/reconnect replays pending cards with the same request IDs. Temporary relay loss does not cancel questions.
105
+
106
+ Questions have no extra Host timeout. The caller's stop/cancel and DSH's own timed-question lifecycle still apply; explicit Host switch-off/unload cancels outstanding bridge waits. Permission cards retain their existing ownership and deadline. No Cindy/DSH patch or phone protocol upgrade is needed. This does not add cross-process recovery or mobile late-answer support after DSH has continued a timed call.
107
+
108
+ Run the focused regressions with:
109
+
110
+ ```sh
111
+ node --test test/user-question-bridge.test.js test/host-approvals.test.js test/dsh-plugin.test.js
112
+ ```
113
+
114
+ The opt-in installed-runtime suite additionally exercises real Cordis, userQuestions and Gateway event delivery/replay/cancellation. Point `DSH_TEST_NATIVE_MODULES` at a built DSH node_modules directory, then run `node --test --test-isolation=none test/user-question-native.test.js`. For an Electron asar installation use the installed executable with `ELECTRON_RUN_AS_NODE=1`. These checks use isolated contexts and do not reload the running GUI; real-handset acceptance is separate.
115
+
82
116
  ## Model switching follows DSH
83
117
 
84
118
  When native `sessionController.selectModel` is composed, the Host tells Cindy to delegate model-window decisions to DSH. Users may select another model during a running turn and select the original model again. DSH validates the provider/model/effort and records the selection for the **next model request**; an already-issued request is not interrupted. DSH owns context preparation and compaction, not Cindy's legacy 90%-window precheck or a Host-side summary/rebuild implementation.
@@ -89,7 +123,67 @@ Missing capacity metadata is not a Host-side veto. Native DSH refusals still pro
89
123
 
90
124
  Controller archive/restore metadata now calls the same durable `workspaceRegistry.archiveSession` / `unarchiveSession` operations as DSH itself. Lists, exact reads and activity badges fold the native archive set; committed desktop changes invalidate controller lists and publish row patches. Archiving drops the pin without deleting history or changing workspace membership.
91
125
 
92
- The existing archive gesture does not request `stopActivity`, so DSH refuses sessions with running work; stop the work explicitly before archiving. Missing native archive services and storage failures are not downgraded to local-only success. Old controller-only archive flags remain visible until an explicit archive/restore retires that session's flag; upgrades do not bulk-archive native sessions. Delete remains a controller-only hide, and pin timestamps remain Host metadata.
126
+ The existing archive gesture does not request `stopActivity`, so DSH refuses sessions with running work; stop the work explicitly before archiving. Missing native archive services and storage failures are not downgraded to local-only success. Old controller-only archive flags remain visible until an explicit archive/restore retires that session's flag; upgrades do not bulk-archive native sessions. Delete remains a controller-only hide. Native pin membership is now projected separately as described below.
127
+
128
+ ## Native DSH pin membership
129
+
130
+ Pin/unpin writes use native `workspaceRegistry.pinSession` / `unpinSession`; native desktop changes refresh controller lists and row patches. Repeating a pin does not reorder native pins. Archiving clears the native pin, and hidden/archived sessions cannot be newly pinned. Missing native pin APIs or persistence failures never become local-only success.
131
+
132
+ DSH exposes an ordered ID set, not pin timestamps. The existing Cindy wire field therefore uses the fixed `1970-01-01T00:00:00.000Z` membership sentinel, **not** a claimed native timestamp or exact manual pin order. Legacy local pin flags remain readable until an explicit operation or corresponding native commit retires them; no automatic bulk migration occurs. Mixed rename/status/pin writes that native APIs cannot commit atomically are rejected before any side effect.
133
+
134
+ ## Native manual compaction subset
135
+
136
+ `maker:compact-session` executes the target ordinary Agent's verified native scoped `/compact` command through `commands.execute`, not a text prompt or a Host summary engine. Only omitted or whitespace-only instructions are supported; nonempty instructions and the separate Claude queue-compaction protocol remain explicitly unsupported. Missing services/preset commands, busy maintenance and native errors never become successful no-ops. Benign no-history success maps to `{ noop: true }`; no native token counts are invented.
137
+
138
+ The existing 11-minute Cindy timeout applies. The Host waits for the real command result, invalidates transcript/history reads after success or failure, and forwards source/link teardown or an accepted stop to the native cancellation signal. Cancellation is not a rollback guarantee. Repeated copies of the same envelope share an in-process bounded receipt, not a cross-restart exactly-once promise; new request identities are not automatically replayed.
139
+
140
+ ## Ordinary native fork subset
141
+
142
+ `maker:fork(sourceSessionId, messageClientId)` locates the actual committed row using the shared message fold and one leased native observation. A human user point copies the prefix before that prompt; an assistant point includes its entire completed turn. DSH alone creates the child and selects its inherited cwd/preset/default model behavior. The source is not rewound or edited. The Host reads the real new row and emits the existing created notification.
143
+
144
+ Unknown, ambiguous, shadowed, incomplete/aborted-turn, first-user-without-prior-assistant and unsafe mid-tool steering cuts are refused. Native history is bounded at 100,000 events; encrypted-stripping forks, branch navigation and in-place rewind remain unsupported. A known created child ID is retained and reported if Workspace attachment, discovery or source teardown fails; such a failure must not automatically trigger another fork. Native fork has no caller cancellation or durable idempotency key: duplicate envelopes are deduped only in-process, and disconnection is not a rollback guarantee.
145
+
146
+ ## Native literal keyword search
147
+
148
+ The existing conversation-search channel uses the mounted native `sessionQuery.filterEvents` literal-text/current-surface API, even when global FTS is disabled. It searches only Host-visible ordinary sessions without waking an Agent, preserves real message/client identities and counts, and applies every accepted status/archive, display-agent alias, activity, ID and grouped-working-directory filter. Title matching uses the same unnamed-label string as Cindy. Relevance is deterministic keyword ranking, not a claimed native FTS/vector score.
149
+
150
+ Keyword mode (also the Host default when omitted) returns `vectorUsed:false` with an explicit literal-only diagnostic; explicit hybrid requests are refused. Limits up to 200, including values over 20, are honored; larger limits and unknown fields refuse rather than silently narrowing. A scan has bounded candidates/events/text/time/concurrency/response size; exhausted budgets report `poolCapped:true`, while storage/read errors stay errors. Late cancelled reads are discarded and observation leases released. Up to three real message previews accompany a result. Host-only focused tests assert the request/response fields against the read-only official protocol reference.
151
+
152
+ ## Native background jobs: read-only subset
153
+
154
+ The existing session-background-task list reads native `jobs.list(sessionId)`, the same public, owner-fenced registry operation used by DSH's job-controller roster stream. It includes that ordinary session's own and unowned active jobs, not other sessions' jobs, and never activates an Agent. Native `stopping` remains running (and is labelled stopping); terminal states are sent only from real native status evidence through existing `agent_task_update` events. Unknown/malformed/over-budget rosters refuse instead of becoming empty success.
155
+
156
+ Cindy roster snapshots contain active tasks only. The optional `pendingContinuations` is omitted as unknown, never fabricated as zero. Native IDs receive stable job namespaces; titles identify DSH. No vendor provider, originating tool-call, output file, model token use or completion is invented. Status pushes reuse the source-owned DSH control stream with visibility/generation checks and bounded transient deduplication; no extra per-phone job stream or consuming output cursor is opened.
157
+
158
+ Precise stop and output-tail channels are not in the verified official remote protocol and are not added. The native DSH kill/follow APIs remain DSH features, not newly exposed Cindy controls; ordinary input-stop retains its existing meaning. These are source/offline-contract changes, not a production reload or handset acceptance claim.
159
+
160
+ ## Native durable children: read-only subset
161
+
162
+ The three existing subagent-run list/detail/transcript endpoints recheck an ordinary parent and its native durable child catalog on every request. Native descendant traversal is used when composed; disconnected/cyclic relations, external children and unknown/corrupt branches refuse explicitly rather than disappearing or becoming completed. No arbitrary session lookup or Agent resolution is used to grant access. Stable run IDs bind parent and native child IDs; the existing pi provider value is only a transport alias, with DSH clearly named in the title/description.
163
+
164
+ Run status uses the real live-Agent lookup, registered native inbox view and host-provided public `foldConsumedWork` reader. Inactive does not mean completed; queued/unknown/unaccounted work is not presented as terminal success. The SDK package is a peer, never a private installed dependency graph. Missing services/helpers refuse dynamically. Resume, steer and stop stay false; returned-result capability stays false because a last assistant message is not evidence of delivery to the parent.
165
+
166
+ History includes only the child-owned, committed public text and real tool-call/result pairs. Inherited parent history, constructor repair closers, injected/system/developer context, private request metadata and tool arguments are not exported. Long public text is split into stable ordered chunks, not silently clipped. Pages contain at most 25 entries with serialized-byte budgets and signed parent/child/catalog/source-bound continuation and tail cursors; teardown invalidates them and releases observations. The non-allowlisted subagent-runs-changed push is not emitted. Validation is focused Host tests only; Cindy source is read-only reference, not imported or executed. No deployment or handset acceptance is claimed.
167
+
168
+ ## Native reminders: existing-dialogue subset
169
+
170
+ The five existing schedule CRUD calls use the mounted native schedule catalog/create/update/delete APIs. Writes accept an existing active ordinary dialogue, recurring native five-field cron or whole-second intervals of at least 60 seconds, the target's current agent alias, no model/workspace override and all notification switches off. Updates carry the complete observed native record as expected; conflicts are errors, never reread-and-overwrite retries. Explicit intervalMs:null clears an interval only with an expressible replacement cron rule.
171
+
172
+ Read/delete includes existing native one-time records. Native inactive maps to expired, not paused; delivery timestamps are not model execution completion, run history, costs or unread counts. Unrepresentable fractional calendar rules and oversized data fail explicitly. Pause/resume/run-now/templates/scripts/run reports remain unsupported; this is not an equivalent automation runner or a claim that every scheduler-page control works.
173
+
174
+ Committed native change notifications produce the existing ready list-invalidation event only for authorized linked peers. One shared observer closes on final unlink, revocation, source replacement or disconnect. Deleting a reminder does not stop an Agent or retract inbox input. Cancellation after persistence is not rollback; known created IDs survive readback/source failures. Write-envelope deduplication is bounded in-process, not durable exactly-once. Validation: 19 focused Host cases; no Cindy source execution, real reminder creation, publication or phone acceptance.
175
+
176
+ ## Native registered Workspace order
177
+
178
+ Existing sidebar project-order get/apply calls project the native registered Workspace order as custom. Only unique local path keys are accepted; unknown paths, ambiguous worktree groups, duplicate keys, device/dialogue keys and activity mode refuse before any move. An account/device/source-bound owner stamp is available only after a verified relay handshake; it is authorization, not a fabricated order revision.
179
+
180
+ Native insertBefore moves are serialized and checked against the actual registry after each step. Unrequested Workspaces retain relative order. This is not an atomic replace: concurrent desktop edits or partial persistence failures stop the sequence and report the real same-owner snapshot, without rollback. Native order changes feed the existing sidebar changed event. Twelve focused Host tests cover mapping, ownership, partial/concurrent failures and real Host handshake/source rotation; no live workspaces were reordered.
181
+
182
+ ## Session-reference source and consumption boundary
183
+
184
+ The existing source-history call reads one authorized ordinary session through a single native observation and current-surface fold, without resolving an Agent. It implements user/assistant role, workdir/agent/time filters, native-position cursor pagination, bounded content with explicit truncation metadata, and a safe terminal-error hint captured from that same prefix. Rewound/all-role history is explicitly unavailable.
185
+
186
+ Source readability does not imply target consumption. The existing target capability probe returns supported:false/version:1: native current-surface reference resolution cannot atomically admit Cindy frozen/anchored/cross-device snapshots. Nonempty structured references are refused before send/enqueue/steer/edit, durable preparation and replay, rather than silently dropping them or turning them into ordinary text. Empty carriers and literal text links retain ordinary behavior. Twelve focused source/boundary Host tests and 28 directly affected delivery cases passed; Cindy source remained read-only and unexecuted.
93
187
 
94
188
  ## Previous release and rollback
95
189
 
package/lib/client.js CHANGED
@@ -52,7 +52,7 @@ window.__ModuleLoader__.load({
52
52
  stateFailed: "连接失败",
53
53
  reconnect: "重新连接",
54
54
  loginTitle: "登录 Cindy",
55
- loginHint: "本机还没有 Cindy 登录态。用与手机相同的账号登录。",
55
+ loginHint: "请选择与手机相同的账号和区域;国内版与国际版账户不互通。",
56
56
  kindPhone: "手机号",
57
57
  kindEmail: "邮箱",
58
58
  identifierPhone: "手机号",
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
100
100
  stateFailed: "Connection failed",
101
101
  reconnect: "Reconnect",
102
102
  loginTitle: "Sign in to Cindy",
103
- loginHint: "This Host has no Cindy session yet. Use the same account as your phone.",
103
+ loginHint: "Use the same account AND region as your phone. Mainland and Global accounts are separate.",
104
104
  kindPhone: "Phone",
105
105
  kindEmail: "Email",
106
106
  identifierPhone: "Phone number",
@@ -203,7 +203,7 @@ window.__ModuleLoader__.load({
203
203
  login,
204
204
  host: (status && status.host) || null,
205
205
  deviceName: typeof value.deviceName === "string" && value.deviceName.trim() ? value.deviceName.trim() : "DSH Host",
206
- showLogin: login.required === true,
206
+ showLogin: login.authenticated !== true,
207
207
  showReconnect: (status ? status.state : "disconnected") === "failed" && login.required !== true,
208
208
  };
209
209
  }
@@ -380,106 +380,156 @@ window.__ModuleLoader__.load({
380
380
  );
381
381
  }
382
382
 
383
- /** The Cindy sign-in form; shown only while the Host reports it needs one. */
384
- function LoginForm({ busy, error, notice, accounts, onSendCode, onSignIn, onSelectAccount }) {
385
- const [kind, setKind] = react.useState("phone");
386
- const [identifier, setIdentifier] = react.useState("");
387
- const [code, setCode] = react.useState("");
388
-
389
- const identifierLabel = kind === "phone" ? TEXT.identifierPhone : TEXT.identifierEmail;
390
-
391
- return h(
392
- "div",
393
- { style: styles.form },
394
- h("span", { style: styles.label }, TEXT.loginTitle),
395
- h("span", { style: styles.hint }, TEXT.loginHint),
396
- h(
397
- "div",
398
- { style: styles.field },
399
- h("span", { style: styles.hint }, TEXT.kindPhone + " / " + TEXT.kindEmail),
400
- h(
401
- "div",
402
- { style: { display: "flex", gap: "6px" } },
403
- ["phone", "email"].map((value) =>
404
- h(
405
- "button",
406
- {
407
- key: value,
408
- type: "button",
409
- onClick: () => setKind(value),
410
- style: Object.assign({}, styles.buttonGhost, {
411
- flex: 1,
412
- color: kind === value ? "var(--dsw-alias-label-primary, #e6edf3)" : undefined,
413
- borderColor: kind === value ? "var(--dsw-alias-brand-primary, #2f81f7)" : undefined,
414
- }),
415
- },
416
- value === "phone" ? TEXT.kindPhone : TEXT.kindEmail,
417
- ),
418
- ),
419
- ),
420
- ),
421
- h(
422
- "label",
423
- { style: styles.field },
424
- h("span", { style: styles.hint }, identifierLabel),
425
- h("input", {
426
- style: styles.input,
427
- value: identifier,
428
- inputMode: kind === "phone" ? "tel" : "email",
429
- autoComplete: kind === "phone" ? "tel" : "email",
430
- onChange: (event) => setIdentifier(event.target.value),
431
- disabled: busy,
432
- }),
433
- ),
434
- h(
435
- "div",
436
- { style: { display: "flex", gap: "6px", alignItems: "flex-end" } },
437
- h(
438
- "label",
439
- { style: Object.assign({}, styles.field, { flex: 1 }) },
440
- h("span", { style: styles.hint }, TEXT.code),
441
- h("input", {
442
- style: styles.input,
443
- value: code,
444
- inputMode: "numeric",
445
- autoComplete: "one-time-code",
446
- onChange: (event) => setCode(event.target.value),
447
- disabled: busy,
448
- }),
449
- ),
450
- h(
451
- "button",
452
- {
453
- type: "button",
454
- style: Object.assign({}, styles.buttonGhost, { height: "32px" }),
455
- disabled: busy,
456
- onClick: () => onSendCode({ kind, identifier }),
457
- },
458
- busy ? TEXT.sending : TEXT.sendCode,
459
- ),
460
- ),
461
- accounts && accounts.length > 0
462
- ? h(
463
- "div",
464
- { style: styles.field },
465
- h("span", { style: styles.hint }, TEXT.selectAccount),
466
- accounts.map((account) =>
467
- h(
468
- "button",
469
- { key: account.id, type: "button", style: styles.buttonGhost, disabled: busy, onClick: () => onSelectAccount(account.id) },
470
- account.displayName || account.id,
471
- ),
472
- ),
473
- )
474
- : null,
475
- error ? h("p", { style: styles.error }, error) : null,
476
- notice ? h("p", { style: styles.ok }, notice) : null,
477
- h(
478
- "button",
479
- { type: "button", style: styles.button, disabled: busy, onClick: () => onSignIn({ kind, identifier, code }) },
480
- busy ? TEXT.signingIn : TEXT.signIn,
481
- ),
482
- );
383
+ const tr = (zh, en) => ZH ? zh : en;
384
+ function loginError(code) {
385
+ const messages = {
386
+ LOGIN_BUSY: tr("其他页面正在登录,可取消后重新开始。", "Another page is signing in. Cancel it explicitly to restart."),
387
+ LOGIN_STEP_CHANGED: tr("登录步骤已更新,请重试当前操作。", "The login step changed. Retry the current action."),
388
+ LOGIN_SUPERSEDED: tr("这次登录已取消或被替换,请重新开始。", "This sign-in was cancelled or replaced. Start again."),
389
+ ORG_REALM_UNAVAILABLE: tr("无法完整确认企业所在区域,请检查国内/国际网络后重试。", "Cannot verify both regions. Check connectivity and retry."),
390
+ ORG_REALM_AMBIGUOUS: tr("两个区域都有同名企业,不能自动选择,请联系管理员。", "The organization exists in both regions. Contact its administrator."),
391
+ ORG_SSO_NOT_FOUND: tr("未找到已启用 SSO 的企业,请检查组织 ID 或域名。", "No SSO-enabled organization found. Check its ID or domain."),
392
+ INVALID_CAPTCHA_RESULT: tr("请粘贴官方验证页成功后的完整地址栏链接。", "Paste the complete successful verification URL from the official page."),
393
+ LOGIN_COMMITTING: tr("登录正在安全保存,请等待完成。", "Sign-in is being saved securely. Please wait."),
394
+ REQUEST_TIMEOUT: tr("登录请求已超时,请重新开始。", "Sign-in timed out. Please start again."),
395
+ LOGIN_EXPIRED: tr("这次登录已过期,请重新开始。", "This sign-in expired. Please start again."),
396
+ };
397
+ return messages[code] || tr("登录未完成,请检查输入或网络后重试", "Sign-in did not complete. Check your input or network and retry") + (code && /^[A-Z0-9_]{1,80}$/.test(code) ? " (" + code + ")" : "");
398
+ }
399
+ function safeLoginUrl(value, region) {
400
+ try {
401
+ const url = new URL(value);
402
+ const domain = region === "global" ? "cindy.app" : "cindy.com.cn";
403
+ return url.protocol === "https:" && !url.username && !url.password && !url.port &&
404
+ (url.hostname === domain || url.hostname.endsWith("." + domain)) ? url.href : null;
405
+ } catch { return null; }
406
+ }
407
+ /** Pure renderer. Attempt IDs/revisions are capabilities; all backend tickets stay off-page. */
408
+ function LoginForm({ flow = null, fields = {}, onField = () => {}, onAction = () => {}, onStart = () => {}, busy = false, error = null }) {
409
+ const region = flow?.region || fields.region || "cn";
410
+ const step = flow?.step;
411
+ const disabled = busy || flow?.busy === true;
412
+ const providers = flow?.providers;
413
+ const kind = providers?.[fields.kind] ? fields.kind : providers?.phone ? "phone" : "email";
414
+ const button = (label, action, data = {}, off = disabled) => h("button", { key: action + JSON.stringify(data), type: "button", style: styles.buttonGhost, disabled: off,
415
+ onClick: () => onAction(action, data) }, label);
416
+ const input = (name, label, mode = "text", maxLength = 254) => h("label", { style: styles.field }, h("span", { style: styles.hint }, label),
417
+ h("input", { style: styles.input, value: fields[name] || "", inputMode: mode, maxLength,
418
+ autoComplete: name === "code" ? "one-time-code" : name === "identifier" ? kind === "phone" ? "tel" : "email" : "off",
419
+ disabled, onChange: event => onField(name, event.target.value) }));
420
+ const link = (url, label) => url ? h("a", { href: url, target: "_blank", rel: "noopener noreferrer", referrerPolicy: "no-referrer", style: styles.buttonGhost }, label) : null;
421
+ let body;
422
+ if (!flow) body = h("div", { style: styles.field },
423
+ h("label", { style: styles.field }, tr("账号区域", "Account region"), h("select", { style: styles.input, value: region, disabled,
424
+ onChange: event => onField("region", event.target.value) }, h("option", { value: "cn" }, tr("国内版", "Mainland China")), h("option", { value: "global" }, tr("国际版", "Global")))),
425
+ h("button", { type: "button", style: styles.button, disabled, onClick: () => onStart(region, false) }, tr("加载登录方式", "Load sign-in options")));
426
+ else if (step === "identifier") body = h("div", { style: styles.field },
427
+ h("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" } }, (providers?.social || []).map(provider => button(
428
+ ({ apple: tr("通过 Apple 登录", "Sign in with Apple"), google: "Google", wechat: tr("微信登录", "WeChat") })[provider] || provider,
429
+ "start_browser", { kind: "social", providerOrConnectionId: provider }))),
430
+ providers && (providers.phone || providers.email) ? h("div", { style: styles.field },
431
+ h("div", { style: { display: "flex", gap: "8px" } }, ["phone", "email"].filter(value => providers[value]).map(value =>
432
+ h("button", { key: value, type: "button", disabled, style: styles.buttonGhost, "aria-pressed": kind === value,
433
+ onClick: () => onField("kind", value) }, value === "phone" ? TEXT.kindPhone : TEXT.kindEmail))),
434
+ input("identifier", kind === "phone" ? TEXT.identifierPhone : TEXT.identifierEmail, kind === "phone" ? "tel" : "email"),
435
+ button(kind === "email" ? tr("继续(检测邮箱登录方式)", "Continue (discover email sign-in)") : TEXT.sendCode,
436
+ kind === "email" ? "discover_email" : "request_code", { kind, identifier: fields.identifier || "" })) : null,
437
+ input("org", tr("企业组织 ID / slug / 已验证域名", "Organization ID / slug / verified domain")),
438
+ button(tr("企业 SSO 登录", "Enterprise SSO"), "discover_org", { org: fields.org || "" }));
439
+ else if (step === "method-choice") body = h("div", { style: styles.field }, (flow.methods || []).map(method =>
440
+ method.type === "sso" ? button((method.orgName || "") + " · " + (method.connectionName || "SSO"), "start_browser", { kind: "sso", providerOrConnectionId: method.connectionId })
441
+ : button(tr("使用个人邮箱验证码(保留原区域)", "Use personal email code (keep original region)"), "personal_email")));
442
+ else if (step === "region-confirmation") body = h("div", { style: styles.field },
443
+ h("p", { style: styles.hint }, tr("企业位于", "This organization is in ") + (flow.targetRegion === "global" ? tr("国际区", "Global") : tr("国内区", "Mainland China")) + tr(",确认切换后继续。", ". Confirm the region change to continue.")),
444
+ button(tr("确认切换区域", "Confirm region change"), "confirm_region"));
445
+ else if (step === "browser") body = h("div", { style: styles.field },
446
+ h("p", { style: styles.hint, role: "status" }, tr("请在官方页面完成登录。本页会自动接收结果;5 分钟内有效。", "Complete sign-in on the official page. This page receives the result automatically; valid for 5 minutes.")),
447
+ link(safeLoginUrl(flow.authorizationUrl, region), tr("打开 Cindy 官方登录页面", "Open official Cindy sign-in")));
448
+ else if (step === "account-selection") body = h("div", { style: styles.field }, h("span", { style: styles.label }, TEXT.selectAccount),
449
+ (flow.accounts || []).map(account => button((account.displayName || account.id) + (account.orgName ? " · " + account.orgName : ""), "select_account", { accountId: account.id })));
450
+ else if (step === "captcha") body = h("div", { style: styles.field },
451
+ h("p", { style: styles.hint }, tr("请在官方页面完成人机验证,再复制成功页的完整地址栏链接(含 #cindy-captcha=ok.…)粘贴到下方。浏览器安全限制使本页无法自动读取该链接。请勿分享它。", "Complete verification on the official page, then copy its complete URL (including #cindy-captcha=ok.…) below. Browser security prevents automatic return. Do not share this URL.")),
452
+ link(safeLoginUrl(flow.challengeUrl, region), tr("打开官方人机验证", "Open official verification")),
453
+ input("captchaResult", tr("验证成功后的完整链接", "Complete successful verification URL"), "url", 12000),
454
+ button(TEXT.sendCode, "captcha_complete", { resultUrl: fields.captchaResult || "" }));
455
+ else if (step === "binding") body = h("div", { style: styles.field },
456
+ h("p", { style: styles.hint }, tr("此账号需要先绑定联系方式。", "This account requires a verified contact.")),
457
+ input("binding", flow.bindType === "phone" ? TEXT.identifierPhone : TEXT.identifierEmail, flow.bindType === "phone" ? "tel" : "email"),
458
+ button(TEXT.sendCode, "binding_request", { identifier: fields.binding || "" }), input("code", TEXT.code, "numeric", 100),
459
+ button(tr("验证并绑定", "Verify and bind"), "binding_verify", { identifier: fields.binding || "", code: fields.code || "" }));
460
+ else if (step === "sso-verification") body = h("div", { style: styles.field },
461
+ h("p", { style: styles.hint }, tr("企业 SSO 需要额外验证:", "Enterprise SSO needs additional verification: ") + (flow.targetMasked || "")),
462
+ button(TEXT.sendCode, "sso_request"), input("code", TEXT.code, "numeric", 100), button(TEXT.signIn, "sso_verify", { code: fields.code || "" }));
463
+ else if (step === "verification-code") body = h("div", { style: styles.field }, h("p", { style: styles.hint }, TEXT.codeSent + " · " + (flow.targetMasked || "")),
464
+ input("code", TEXT.code, "numeric", 100), button(TEXT.signIn, "verify_code", { code: fields.code || "" }),
465
+ button(tr("重新发送验证码", "Resend code"), "request_code", { kind: flow.kind, identifier: fields.identifier || "" }));
466
+ else body = h("p", { style: styles.hint, role: "status" }, step === "completed" ? TEXT.signedIn : step === "committing" ? tr("正在安全保存登录…", "Saving sign-in securely…") : tr("请重新开始登录。", "Please start sign-in again."));
467
+ return h("div", { style: styles.form }, h("span", { style: styles.label }, TEXT.loginTitle), h("span", { style: styles.hint }, TEXT.loginHint),
468
+ flow ? h("p", { style: styles.hint }, tr("账号区域:", "Account region: ") + (region === "global" ? tr("国际版", "Global") : tr("国内版", "Mainland China"))) : null,
469
+ body, (error || flow?.error) ? h("p", { style: styles.error, role: "alert" }, error || loginError(flow.error)) : null,
470
+ error === loginError("LOGIN_BUSY") ? h("button", { type: "button", style: styles.buttonGhost, disabled,
471
+ onClick: () => onStart(region, true) }, tr("取消其他登录并重新开始", "Cancel other sign-in and restart")) : null,
472
+ flow && !["completed", "committing", "cancelled", "error"].includes(step) ? button(tr("取消这次登录", "Cancel this sign-in"), "cancel", {}, false) : null,
473
+ flow && step !== "committing" ? h("div", { style: { display: "flex", gap: "8px" } },
474
+ h("button", { type: "button", style: styles.buttonGhost, disabled, onClick: () => onStart(region, true) }, tr("重新开始", "Start again")),
475
+ h("button", { type: "button", style: styles.buttonGhost, disabled, onClick: () => onStart(region === "cn" ? "global" : "cn", true) }, tr("切换国内 / 国际区域", "Switch Mainland / Global"))) : null);
476
+ }
477
+ function AuthLoginPanel({ onAuthenticated }) {
478
+ const [flow, setFlow] = react.useState(null);
479
+ const [fields, setFields] = react.useState({ region: "cn", kind: "phone" });
480
+ const [busy, setBusy] = react.useState(false);
481
+ const [error, setError] = react.useState(null);
482
+ const state = react.useRef({ flow: null, generation: 0, alive: true, busy: false, polling: false });
483
+ const apply = (result, generation) => {
484
+ if (!state.current.alive || generation !== state.current.generation) return;
485
+ const next = result.view;
486
+ if (next && (!state.current.flow || next.attemptId !== state.current.flow.attemptId || next.revision >= state.current.flow.revision)) {
487
+ const previous = state.current.flow; state.current.flow = next; setFlow(next);
488
+ if (previous?.step !== next.step) setFields(value => ({ ...value, code: "", captchaResult: "" }));
489
+ if (next.step === "completed" && previous?.step !== "completed") onAuthenticated?.();
490
+ }
491
+ if (!result.ok) setError(loginError(result.code));
492
+ };
493
+ const act = async (action, data = {}) => {
494
+ const read = action === "read";
495
+ if (!read && action !== "cancel" && state.current.busy) return;
496
+ if (read && state.current.polling) return;
497
+ if (read) state.current.polling = true;
498
+ else { state.current.busy = true; setBusy(true); setError(null); }
499
+ const generation = action === "cancel" ? ++state.current.generation : state.current.generation;
500
+ const current = state.current.flow;
501
+ try { apply(await postJson("/login/flow", { action, attemptId: current?.attemptId, revision: current?.revision, ...data }), generation); }
502
+ catch { if (state.current.alive && generation === state.current.generation) setError(tr("请求失败,请重试", "Request failed. Please retry.")); }
503
+ finally { if (read) state.current.polling = false; else if (generation === state.current.generation) { state.current.busy = false; if (state.current.alive) setBusy(false); } }
504
+ };
505
+ const start = async (region, takeover) => {
506
+ if (state.current.busy) return;
507
+ const generation = ++state.current.generation; state.current.busy = true; setBusy(true); setError(null);
508
+ try {
509
+ const prior = state.current.flow;
510
+ if (prior && !["completed", "cancelled", "error"].includes(prior.step)) {
511
+ const cancelled = await postJson("/login/flow", { action: "cancel", attemptId: prior.attemptId });
512
+ if (!cancelled.ok) { apply(cancelled, generation); return; }
513
+ }
514
+ state.current.flow = null; setFlow(null); setFields({ region, kind: region === "cn" ? "phone" : "email" });
515
+ apply(await postJson("/login/flow", { action: "start", region, takeover }), generation);
516
+ } catch { if (state.current.alive && generation === state.current.generation) setError(tr("读取登录方式失败,请检查网络后重试", "Cannot load sign-in options. Check your connection and retry.")); }
517
+ finally { if (generation === state.current.generation) { state.current.busy = false; if (state.current.alive) setBusy(false); } }
518
+ };
519
+ react.useEffect(() => {
520
+ state.current.alive = true;
521
+ return () => {
522
+ state.current.alive = false; state.current.generation++;
523
+ const current = state.current.flow;
524
+ if (current && !["completed", "cancelled", "error"].includes(current.step)) void postJson("/login/flow", { action: "cancel", attemptId: current.attemptId }).catch(() => {});
525
+ };
526
+ }, []);
527
+ react.useEffect(() => {
528
+ if (!flow || !["browser", "committing"].includes(flow.step)) return;
529
+ const timer = setInterval(() => { void act("read"); }, POLL_MS);
530
+ return () => clearInterval(timer);
531
+ }, [flow?.attemptId, flow?.step]);
532
+ return h(LoginForm, { flow, fields, busy, error, onField: (name, value) => setFields(old => ({ ...old, [name]: value })), onAction: act, onStart: start });
483
533
  }
484
534
 
485
535
  /** Keep an unsaved draft independent of status polling and settings snapshots. */
@@ -568,8 +618,6 @@ window.__ModuleLoader__.load({
568
618
  const [busy, setBusy] = react.useState(false);
569
619
  const [error, setError] = react.useState(null);
570
620
  const [notice, setNotice] = react.useState(null);
571
- const [accounts, setAccounts] = react.useState(null);
572
- const [loginTicket, setLoginTicket] = react.useState(null);
573
621
 
574
622
  const refresh = react.useCallback(() => setRefreshKey((value) => value + 1), []);
575
623
 
@@ -625,43 +673,6 @@ window.__ModuleLoader__.load({
625
673
  if (result.status) setStatus(result.status);
626
674
  });
627
675
 
628
- const onSendCode = ({ kind, identifier }) =>
629
- guard(async () => {
630
- setNotice(null);
631
- const result = await postJson("/login/request-code", { kind, identifier });
632
- if (!result.ok) {
633
- setError(result.message || "验证码发送失败");
634
- return;
635
- }
636
- setNotice(TEXT.codeSent);
637
- });
638
-
639
- const onSignIn = ({ kind, identifier, code }) =>
640
- guard(async () => {
641
- setNotice(null);
642
- setAccounts(null);
643
- const result = await postJson("/login/verify-code", { kind, identifier, code });
644
- if (!result.ok) {
645
- if (result.status === "select_account" && Array.isArray(result.accounts)) {
646
- setAccounts(result.accounts);
647
- setLoginTicket(result.loginTicket);
648
- }
649
- setError(result.message || "登录失败");
650
- return;
651
- }
652
- setNotice(null);
653
- });
654
-
655
- const onSelectAccount = (accountId) =>
656
- guard(async () => {
657
- const result = await postJson("/login/select-account", { loginTicket, accountId });
658
- if (!result.ok) {
659
- setError(result.message || "账户选择失败");
660
- return;
661
- }
662
- setAccounts(null);
663
- });
664
-
665
676
  const onReconnect = () => guard(async () => {
666
677
  const result = await postJson("/reconnect", {});
667
678
  if (!result.ok) setError(result.message || "重新连接失败");
@@ -720,12 +731,12 @@ window.__ModuleLoader__.load({
720
731
  ? h(
721
732
  "div",
722
733
  { style: styles.row },
723
- h("span", { style: styles.hint }, TEXT.signedIn + (maskIdentifier(view.login.identifier) ? " · " + maskIdentifier(view.login.identifier) : "")),
734
+ h("span", { style: styles.hint }, TEXT.signedIn + (view.login.realm ? ' · ' + (view.login.realm === 'global' ? tr('国际版', 'Global') : tr('国内版', 'Mainland China')) : '') + (maskIdentifier(view.login.identifier) ? " · " + maskIdentifier(view.login.identifier) : "")),
724
735
  h("button", { type: "button", style: styles.buttonGhost, disabled: busy, onClick: onLogout }, TEXT.logout),
725
736
  )
726
737
  : null,
727
738
 
728
- view.showLogin ? h(LoginForm, { busy, error, notice, accounts, onSendCode, onSignIn, onSelectAccount }) : null,
739
+ view.showLogin ? h(AuthLoginPanel, { onAuthenticated: refresh }) : null,
729
740
 
730
741
  view.showReconnect
731
742
  ? h(
@@ -735,7 +746,7 @@ window.__ModuleLoader__.load({
735
746
  )
736
747
  : null,
737
748
 
738
- error && !view.showLogin ? h("p", { style: styles.error }, error) : null,
749
+ error ? h("p", { style: styles.error }, error) : null,
739
750
 
740
751
  h(
741
752
  "div",
@@ -851,7 +862,7 @@ window.__ModuleLoader__.load({
851
862
  // Test seam: the presentation helpers and sub-components, so the markup
852
863
  // rules can be rendered and asserted without a browser. Not part of the
853
864
  // plugin contract — the runner reads only `apply` and `inject`.
854
- exports.__internals = { TEXT, NAMESPACE, API, SECTION_ORDER, resolveCardView, bindSettingsPage, stateText, stateColor, platformText, writeSwitch, validHostName, writeHostName, HostNameForm, readStatus, maskIdentifier, Switch, LoginForm, DeviceRow, createCindySettingsPage };
865
+ exports.__internals = { TEXT, NAMESPACE, API, SECTION_ORDER, resolveCardView, bindSettingsPage, stateText, stateColor, platformText, writeSwitch, validHostName, writeHostName, HostNameForm, readStatus, maskIdentifier, Switch, LoginForm, AuthLoginPanel, safeLoginUrl, loginError, DeviceRow, createCindySettingsPage };
855
866
  return module.exports;
856
867
  },
857
868
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cindy-host",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "private": false,
5
5
  "description": "Cindy 手机客户端连接 DSH 的 Host 侧插件:会话/消息/审批/todo/goal 的手机通道,以及配套的设置卡片",
6
6
  "license": "MIT",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "//peerDependencies": "DSH 自己的包必须由宿主提供,绝不能声明成 dependency/optionalDependency:pnpm 会把它们**连同整代依赖图**装进 profile,宿主再组合这个混合体就起不来(实测:`^0.1.1-rc.2` 会把 28 个 0.1.1-rc.2 的包装进一个 0.1.5-rc.2 的宿主,profile 启动失败于 dsh-client-file-upload / dsh-session)。区间必须覆盖宿主那一代(semver 的预发布规则:`^0.1.1-rc.2` 匹配不到 `0.1.5-rc.2`),与生态里 dsh-codex / dsh-cost-meter 的写法一致;`0.2.0-rc.2` 需要 `>=0.2.0-rc.1 <0.3.0-0`。",
70
70
  "peerDependencies": {
71
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2",
71
72
  "@deepseek-ai/dsh-session": "0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2",
72
73
  "@deepseek-ai/dsh-settings": "0.2.1-rc.3 || 0.2.1-rc.4 || 0.2.1-rc.5 || 0.2.2-alpha.2",
73
74
  "@deepseek-ai/schemastery": "^3.18.1 || 3.18.6-alpha.1"