@linxin666/dsh-pet 0.3.2 → 0.3.3

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/lib/index.js CHANGED
@@ -4241,7 +4241,7 @@ function makePetRoutes(deps) {
4241
4241
  //#region src/mount-once.ts
4242
4242
  /**
4243
4243
  * Host single-instance guard shared by the plugin family. The family bundle
4244
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
4244
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
4245
4245
  * the loader accepts a standalone install of the same package side by side;
4246
4246
  * without this guard the second instance would still re-register the same
4247
4247
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -4254,7 +4254,7 @@ function makePetRoutes(deps) {
4254
4254
  * `ctx.effect` runs its callback immediately and treats the callback's
4255
4255
  * return value as the fiber disposer, so the unmarker is returned, not run.
4256
4256
  */
4257
- const MOUNTED = Symbol.for("dsh-web-ui.mounted-plugins");
4257
+ const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
4258
4258
  function mountedSet() {
4259
4259
  const registry = globalThis;
4260
4260
  return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
@@ -23,7 +23,7 @@ export type { PetDefinition } from '../registry.ts';
23
23
  declare module '@deepseek-ai/cordis' {
24
24
  interface Context {
25
25
  /**
26
- * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
26
+ * Optional rc.6 compatibility binder provided by dsh-web-settings;
27
27
  * absent when that group plugin is not installed, so callers fall back to
28
28
  * the official settings scope.
29
29
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAgEnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAoR9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AAiEnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAwR9C"}
@@ -19,6 +19,7 @@ import { live2dRenderer } from "./renderers/live2d.js";
19
19
  import { registerPetUiTeardown, takeoverPetUiTeardown } from "./ui-teardown.js";
20
20
  import { PetSettingsSection, PetSettingsCardController } from "./PetSettingsCard.js";
21
21
  import { NS, en, zh, t } from "./locales.js";
22
+ import { reportDailyHeartbeat } from "./telemetry.js";
22
23
  /** Same-origin JSON fetch helper (GET without body, POST with JSON body). */
23
24
  async function petFetch(path, body) {
24
25
  const response = await fetch(path, body === undefined
@@ -56,6 +57,9 @@ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'remote
56
57
  * @param ctx - client root context.
57
58
  */
58
59
  export function apply(ctx) {
60
+ // Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
61
+ // UTC day, package name only, silent failure.
62
+ reportDailyHeartbeat([{ name: '@linxin666/dsh-pet' }]);
59
63
  ctx.effect(() => {
60
64
  try {
61
65
  return ctx.locale.register(NS, { zh, en });
@@ -173,7 +173,7 @@ export declare class CardForm<T> {
173
173
  /**
174
174
  * Write every staged edit, then re-seed from what the Host accepted.
175
175
  *
176
- * When the scope carries the optional batch surface (the dsh-web-ui
176
+ * When the scope carries the optional batch surface (the dsh-web
177
177
  * bridge scope), every planned write rides one mutation so cross-field
178
178
  * validate hooks (baseURL+model) judge the batch as a unit instead of
179
179
  * deadlocking on per-field writes. Otherwise the per-field loop runs.
@@ -170,7 +170,7 @@ export class CardForm {
170
170
  /**
171
171
  * Write every staged edit, then re-seed from what the Host accepted.
172
172
  *
173
- * When the scope carries the optional batch surface (the dsh-web-ui
173
+ * When the scope carries the optional batch surface (the dsh-web
174
174
  * bridge scope), every planned write rides one mutation so cross-field
175
175
  * validate hooks (baseURL+model) judge the batch as a unit instead of
176
176
  * deadlocking on per-field writes. Otherwise the per-field loop runs.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Anonymous install telemetry shared by the DSH Web UI family plugins.
3
+ *
4
+ * Once per UTC day per browser, each wired plugin sends one heartbeat to
5
+ * the dsh-market edge API listing its package name (and version when known).
6
+ * The payload carries no conversation data and no identifiers beyond a random
7
+ * UUID generated in localStorage; the server hashes it with a deployment salt
8
+ * before storage and never persists IP addresses. Sends are fire-and-forget:
9
+ * failures stay silent and simply retry on a later mount or day. The system
10
+ * is documented in docs/telemetry.md.
11
+ */
12
+ export type TelemetryChannel = 'market' | 'npm' | 'unknown';
13
+ export interface TelemetryItem {
14
+ /** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
15
+ name: string;
16
+ /** Installed version when known; omitted otherwise. */
17
+ version?: string;
18
+ /** Install channel when determinable (market = Workshop install). */
19
+ channel?: TelemetryChannel;
20
+ }
21
+ /**
22
+ * Fire the daily heartbeat for the given items at most once per UTC day per
23
+ * browser. Never throws and never blocks the caller. Items without an explicit
24
+ * version inherit the bundle's baked build version.
25
+ */
26
+ export declare function reportDailyHeartbeat(items: readonly TelemetryItem[]): void;
27
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../../src/client/telemetry.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAA;AAE3D,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,qEAAqE;IACrE,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AA+CD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CA8B1E"}
@@ -0,0 +1,97 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/client/telemetry.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * Anonymous install telemetry shared by the DSH Web UI family plugins.
4
+ *
5
+ * Once per UTC day per browser, each wired plugin sends one heartbeat to
6
+ * the dsh-market edge API listing its package name (and version when known).
7
+ * The payload carries no conversation data and no identifiers beyond a random
8
+ * UUID generated in localStorage; the server hashes it with a deployment salt
9
+ * before storage and never persists IP addresses. Sends are fire-and-forget:
10
+ * failures stay silent and simply retry on a later mount or day. The system
11
+ * is documented in docs/telemetry.md.
12
+ */
13
+ const VISITOR_KEY = 'dsh-web-ui-telemetry-visitor';
14
+ const DAY_KEY_PREFIX = 'dsh-web-ui-telemetry-day:';
15
+ const ENDPOINT = 'https://dsh-market.com/api/telemetry/event';
16
+ /** The building package's version, when the bundle carries it. */
17
+ function bakedVersion() {
18
+ try {
19
+ return typeof __DSH_PKG_VERSION__ === 'string' && __DSH_PKG_VERSION__ !== ''
20
+ ? __DSH_PKG_VERSION__
21
+ : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ /** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
28
+ function visitorId() {
29
+ try {
30
+ const existing = localStorage.getItem(VISITOR_KEY);
31
+ if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing))
32
+ return existing;
33
+ const fresh = crypto.randomUUID().replaceAll('-', '');
34
+ localStorage.setItem(VISITOR_KEY, fresh);
35
+ return fresh;
36
+ }
37
+ catch {
38
+ // Storage unavailable (privacy mode, sandboxed frame): report nothing.
39
+ return null;
40
+ }
41
+ }
42
+ /** Drop stale per-day dedup keys so localStorage does not grow forever. */
43
+ function pruneDayKeys(today) {
44
+ try {
45
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
46
+ const key = localStorage.key(index);
47
+ if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) {
48
+ localStorage.removeItem(key);
49
+ }
50
+ }
51
+ }
52
+ catch { /* best effort */ }
53
+ }
54
+ /**
55
+ * Fire the daily heartbeat for the given items at most once per UTC day per
56
+ * browser. Never throws and never blocks the caller. Items without an explicit
57
+ * version inherit the bundle's baked build version.
58
+ */
59
+ export function reportDailyHeartbeat(items) {
60
+ try {
61
+ if (items.length === 0)
62
+ return;
63
+ const today = new Date().toISOString().slice(0, 10);
64
+ // Automation contexts (CDP-driven QA browsers, fresh isolated profiles)
65
+ // mint a new visitor id per run and would inflate instance counts.
66
+ if (navigator.webdriver)
67
+ return;
68
+ if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null)
69
+ return;
70
+ const visitor = visitorId();
71
+ if (visitor === null)
72
+ return;
73
+ pruneDayKeys(today);
74
+ const payloadItems = items.map((item) => {
75
+ const out = { name: item.name };
76
+ const version = item.version ?? bakedVersion();
77
+ if (version !== undefined)
78
+ out.version = version;
79
+ if (item.channel !== undefined)
80
+ out.channel = item.channel;
81
+ return out;
82
+ });
83
+ const body = JSON.stringify({ kind: 'heartbeat', visitor, items: payloadItems });
84
+ void fetch(ENDPOINT, {
85
+ method: 'POST',
86
+ headers: { 'content-type': 'application/json' },
87
+ body,
88
+ keepalive: true,
89
+ }).then((response) => {
90
+ // Mark the day only after an accepted send, so offline browsers retry
91
+ // on a later mount instead of going dark until tomorrow.
92
+ if (response.ok)
93
+ localStorage.setItem(DAY_KEY_PREFIX + today, '1');
94
+ }).catch(() => { });
95
+ }
96
+ catch { /* never break the host UI over telemetry */ }
97
+ }
@@ -10,7 +10,7 @@
10
10
  * edit this shared source and re-run the sync instead of editing a copy.
11
11
  * Consumer code is migrated onto it in follow-up waves; no call site changes
12
12
  * belong in the same change as its introduction.
13
- * @module dsh-web-ui-shared/host/http
13
+ * @module dsh-web-shared/host/http
14
14
  */
15
15
  import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http';
16
16
  /**
package/lib/types/http.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * edit this shared source and re-run the sync instead of editing a copy.
12
12
  * Consumer code is migrated onto it in follow-up waves; no call site changes
13
13
  * belong in the same change as its introduction.
14
- * @module dsh-web-ui-shared/host/http
14
+ * @module dsh-web-shared/host/http
15
15
  */
16
16
  /** Default body cap for readJsonBody: 64 KiB. */
17
17
  const DEFAULT_JSON_BODY_MAX_BYTES = 64 * 1024;
@@ -6,7 +6,7 @@
6
6
  * the package assets, the hatch-pet custom pets directory, and composed
7
7
  * config entries; adding a pet means dropping a manifest + atlas into one of
8
8
  * those sources, never touching host or client code. Install via
9
- * 'dsh plugin --profile web add link:<dsh-web-ui>/packages/dsh-pet'; the
9
+ * 'dsh plugin --profile web add link:<dsh-web>/packages/dsh-pet'; the
10
10
  * cordis.patch.yml inserts this plugin row.
11
11
  * @module @linxin666/dsh-pet
12
12
  */
@@ -6,7 +6,7 @@
6
6
  * the package assets, the hatch-pet custom pets directory, and composed
7
7
  * config entries; adding a pet means dropping a manifest + atlas into one of
8
8
  * those sources, never touching host or client code. Install via
9
- * 'dsh plugin --profile web add link:<dsh-web-ui>/packages/dsh-pet'; the
9
+ * 'dsh plugin --profile web add link:<dsh-web>/packages/dsh-pet'; the
10
10
  * cordis.patch.yml inserts this plugin row.
11
11
  * @module @linxin666/dsh-pet
12
12
  */
@@ -7,7 +7,7 @@
7
7
  * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
8
8
  * remote-web-ui gate), localhost hostnames, plus the browser same-origin
9
9
  * markers (sec-fetch-site and Origin) for the request-level fence.
10
- * @module dsh-web-ui-shared/host/loopback
10
+ * @module dsh-web-shared/host/loopback
11
11
  */
12
12
  import type { IncomingMessage } from 'node:http';
13
13
  /** IPv4 127/8 predicate (four decimal octets, first == 127). */
@@ -8,7 +8,7 @@
8
8
  * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
9
9
  * remote-web-ui gate), localhost hostnames, plus the browser same-origin
10
10
  * markers (sec-fetch-site and Origin) for the request-level fence.
11
- * @module dsh-web-ui-shared/host/loopback
11
+ * @module dsh-web-shared/host/loopback
12
12
  */
13
13
  /** IPv4 127/8 predicate (four decimal octets, first == 127). */
14
14
  export function isIPv4Loopback(v4) {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Host single-instance guard shared by the plugin family. The family bundle
3
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
3
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
4
4
  * the loader accepts a standalone install of the same package side by side;
5
5
  * without this guard the second instance would still re-register the same
6
6
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -1,7 +1,7 @@
1
1
  // Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
2
  /**
3
3
  * Host single-instance guard shared by the plugin family. The family bundle
4
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
4
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
5
5
  * the loader accepts a standalone install of the same package side by side;
6
6
  * without this guard the second instance would still re-register the same
7
7
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -14,7 +14,7 @@
14
14
  * `ctx.effect` runs its callback immediately and treats the callback's
15
15
  * return value as the fiber disposer, so the unmarker is returned, not run.
16
16
  */
17
- const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins');
17
+ const MOUNTED = Symbol.for('dsh-web.mounted-plugins');
18
18
  function mountedSet() {
19
19
  const registry = globalThis;
20
20
  return (registry[MOUNTED] ??= new Set());
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
- "version": "0.3.2",
4
+ "version": "0.3.3",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -87,7 +87,7 @@
87
87
  },
88
88
  "repository": {
89
89
  "type": "git",
90
- "url": "https://github.com/zhu1090093659/dsh-web-ui.git"
90
+ "url": "https://github.com/zhu1090093659/dsh-web.git"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "tsc -b && tsdown",
@@ -31,6 +31,7 @@ import { live2dRenderer } from './renderers/live2d.ts'
31
31
  import { registerPetUiTeardown, takeoverPetUiTeardown } from './ui-teardown.ts'
32
32
  import { PetSettingsSection, PetSettingsCardController, type PetSettings } from './PetSettingsCard.tsx'
33
33
  import { NS, en, zh, t } from './locales.ts'
34
+ import { reportDailyHeartbeat } from './telemetry.ts'
34
35
 
35
36
  /** The host pet API as the browser sees it (same-origin JSON endpoints). */
36
37
  interface PetHttpApi {
@@ -89,7 +90,7 @@ export type { PetDefinition } from '../registry.ts'
89
90
  declare module '@deepseek-ai/cordis' {
90
91
  interface Context {
91
92
  /**
92
- * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
93
+ * Optional rc.6 compatibility binder provided by dsh-web-settings;
93
94
  * absent when that group plugin is not installed, so callers fall back to
94
95
  * the official settings scope.
95
96
  */
@@ -104,6 +105,10 @@ declare module '@deepseek-ai/cordis' {
104
105
  * @param ctx - client root context.
105
106
  */
106
107
  export function apply(ctx: ClientContext): void {
108
+ // Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
109
+ // UTC day, package name only, silent failure.
110
+ reportDailyHeartbeat([{ name: '@linxin666/dsh-pet' }])
111
+
107
112
  ctx.effect(() => {
108
113
  try {
109
114
  return ctx.locale.register(NS, { zh, en })
@@ -320,7 +320,7 @@ export class CardForm<T> {
320
320
  /**
321
321
  * Write every staged edit, then re-seed from what the Host accepted.
322
322
  *
323
- * When the scope carries the optional batch surface (the dsh-web-ui
323
+ * When the scope carries the optional batch surface (the dsh-web
324
324
  * bridge scope), every planned write rides one mutation so cross-field
325
325
  * validate hooks (baseURL+model) judge the batch as a unit instead of
326
326
  * deadlocking on per-field writes. Otherwise the per-field loop runs.
@@ -0,0 +1,105 @@
1
+ // Generated by scripts/sync-shared.mjs from shared/client/telemetry.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
+ /**
3
+ * Anonymous install telemetry shared by the DSH Web UI family plugins.
4
+ *
5
+ * Once per UTC day per browser, each wired plugin sends one heartbeat to
6
+ * the dsh-market edge API listing its package name (and version when known).
7
+ * The payload carries no conversation data and no identifiers beyond a random
8
+ * UUID generated in localStorage; the server hashes it with a deployment salt
9
+ * before storage and never persists IP addresses. Sends are fire-and-forget:
10
+ * failures stay silent and simply retry on a later mount or day. The system
11
+ * is documented in docs/telemetry.md.
12
+ */
13
+
14
+ export type TelemetryChannel = 'market' | 'npm' | 'unknown'
15
+
16
+ export interface TelemetryItem {
17
+ /** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
18
+ name: string
19
+ /** Installed version when known; omitted otherwise. */
20
+ version?: string
21
+ /** Install channel when determinable (market = Workshop install). */
22
+ channel?: TelemetryChannel
23
+ }
24
+
25
+ const VISITOR_KEY = 'dsh-web-ui-telemetry-visitor'
26
+ const DAY_KEY_PREFIX = 'dsh-web-ui-telemetry-day:'
27
+ const ENDPOINT = 'https://dsh-market.com/api/telemetry/event'
28
+
29
+ // Baked into each client bundle by shared/tsdown.client.ts (the package's own
30
+ // package.json version at build time). Undefined in dev/test builds.
31
+ declare const __DSH_PKG_VERSION__: string | undefined
32
+
33
+ /** The building package's version, when the bundle carries it. */
34
+ function bakedVersion(): string | undefined {
35
+ try {
36
+ return typeof __DSH_PKG_VERSION__ === 'string' && __DSH_PKG_VERSION__ !== ''
37
+ ? __DSH_PKG_VERSION__
38
+ : undefined
39
+ } catch {
40
+ return undefined
41
+ }
42
+ }
43
+
44
+ /** Read or lazily create the anonymous visitor id; null when storage is unavailable. */
45
+ function visitorId(): string | null {
46
+ try {
47
+ const existing = localStorage.getItem(VISITOR_KEY)
48
+ if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing
49
+ const fresh = crypto.randomUUID().replaceAll('-', '')
50
+ localStorage.setItem(VISITOR_KEY, fresh)
51
+ return fresh
52
+ } catch {
53
+ // Storage unavailable (privacy mode, sandboxed frame): report nothing.
54
+ return null
55
+ }
56
+ }
57
+
58
+ /** Drop stale per-day dedup keys so localStorage does not grow forever. */
59
+ function pruneDayKeys(today: string): void {
60
+ try {
61
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
62
+ const key = localStorage.key(index)
63
+ if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) {
64
+ localStorage.removeItem(key)
65
+ }
66
+ }
67
+ } catch { /* best effort */ }
68
+ }
69
+
70
+ /**
71
+ * Fire the daily heartbeat for the given items at most once per UTC day per
72
+ * browser. Never throws and never blocks the caller. Items without an explicit
73
+ * version inherit the bundle's baked build version.
74
+ */
75
+ export function reportDailyHeartbeat(items: readonly TelemetryItem[]): void {
76
+ try {
77
+ if (items.length === 0) return
78
+ const today = new Date().toISOString().slice(0, 10)
79
+ // Automation contexts (CDP-driven QA browsers, fresh isolated profiles)
80
+ // mint a new visitor id per run and would inflate instance counts.
81
+ if (navigator.webdriver) return
82
+ if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return
83
+ const visitor = visitorId()
84
+ if (visitor === null) return
85
+ pruneDayKeys(today)
86
+ const payloadItems = items.map((item) => {
87
+ const out: Record<string, string> = { name: item.name }
88
+ const version = item.version ?? bakedVersion()
89
+ if (version !== undefined) out.version = version
90
+ if (item.channel !== undefined) out.channel = item.channel
91
+ return out
92
+ })
93
+ const body = JSON.stringify({ kind: 'heartbeat', visitor, items: payloadItems })
94
+ void fetch(ENDPOINT, {
95
+ method: 'POST',
96
+ headers: { 'content-type': 'application/json' },
97
+ body,
98
+ keepalive: true,
99
+ }).then((response) => {
100
+ // Mark the day only after an accepted send, so offline browsers retry
101
+ // on a later mount instead of going dark until tomorrow.
102
+ if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, '1')
103
+ }).catch(() => { /* unreachable endpoint: stay silent */ })
104
+ } catch { /* never break the host UI over telemetry */ }
105
+ }
package/src/http.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * edit this shared source and re-run the sync instead of editing a copy.
12
12
  * Consumer code is migrated onto it in follow-up waves; no call site changes
13
13
  * belong in the same change as its introduction.
14
- * @module dsh-web-ui-shared/host/http
14
+ * @module dsh-web-shared/host/http
15
15
  */
16
16
 
17
17
  import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http'
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * the package assets, the hatch-pet custom pets directory, and composed
7
7
  * config entries; adding a pet means dropping a manifest + atlas into one of
8
8
  * those sources, never touching host or client code. Install via
9
- * 'dsh plugin --profile web add link:<dsh-web-ui>/packages/dsh-pet'; the
9
+ * 'dsh plugin --profile web add link:<dsh-web>/packages/dsh-pet'; the
10
10
  * cordis.patch.yml inserts this plugin row.
11
11
  * @module @linxin666/dsh-pet
12
12
  */
package/src/loopback.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
9
9
  * remote-web-ui gate), localhost hostnames, plus the browser same-origin
10
10
  * markers (sec-fetch-site and Origin) for the request-level fence.
11
- * @module dsh-web-ui-shared/host/loopback
11
+ * @module dsh-web-shared/host/loopback
12
12
  */
13
13
 
14
14
  import type { IncomingMessage } from 'node:http'
package/src/mount-once.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // Generated by scripts/sync-shared.mjs from shared/host/mount-once.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
2
2
  /**
3
3
  * Host single-instance guard shared by the plugin family. The family bundle
4
- * (dsh-web-ui-all / dsh-skins) namespaces every child row id (web-ui-*), so
4
+ * (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
5
5
  * the loader accepts a standalone install of the same package side by side;
6
6
  * without this guard the second instance would still re-register the same
7
7
  * webserver routes, tools, settings namespaces, and system-prompt sections
@@ -15,7 +15,7 @@
15
15
  * return value as the fiber disposer, so the unmarker is returned, not run.
16
16
  */
17
17
 
18
- const MOUNTED = Symbol.for('dsh-web-ui.mounted-plugins')
18
+ const MOUNTED = Symbol.for('dsh-web.mounted-plugins')
19
19
 
20
20
  interface MountRegistry {
21
21
  [MOUNTED]?: Set<string>