@linxin666/dsh-pet 0.3.1 → 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/README.i18n.yaml +2 -2
- package/README.md +8 -4
- package/README.zh.md +8 -4
- package/lib/client.js +72 -1
- package/lib/client.js.map +1 -1
- package/lib/index.js +2 -2
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +4 -0
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/settings-form.js +1 -1
- package/lib/types/client/telemetry.d.ts +27 -0
- package/lib/types/client/telemetry.d.ts.map +1 -0
- package/lib/types/client/telemetry.js +97 -0
- package/lib/types/http.d.ts +1 -1
- package/lib/types/http.js +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/loopback.js +1 -1
- package/lib/types/mount-once.d.ts +1 -1
- package/lib/types/mount-once.js +2 -2
- package/package.json +2 -2
- package/src/client/index.ts +6 -1
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/http.ts +1 -1
- package/src/index.ts +1 -1
- package/src/loopback.ts +1 -1
- package/src/mount-once.ts +2 -2
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-
|
|
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
|
|
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-
|
|
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;
|
|
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
|
|
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
|
|
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
|
+
}
|
package/lib/types/http.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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;
|
package/lib/types/index.d.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
|
|
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/lib/types/index.js
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
|
|
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/lib/types/loopback.d.ts
CHANGED
|
@@ -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-
|
|
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). */
|
package/lib/types/loopback.js
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-
|
|
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-
|
|
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
|
package/lib/types/mount-once.js
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-
|
|
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
|
|
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.
|
|
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
|
|
90
|
+
"url": "https://github.com/zhu1090093659/dsh-web.git"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"build": "tsc -b && tsdown",
|
package/src/client/index.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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-
|
|
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
|
|
18
|
+
const MOUNTED = Symbol.for('dsh-web.mounted-plugins')
|
|
19
19
|
|
|
20
20
|
interface MountRegistry {
|
|
21
21
|
[MOUNTED]?: Set<string>
|