@huaqiu/dsh-analytics 0.4.7

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.
@@ -0,0 +1,48 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ import { IncomingMessage, ServerResponse } from "node:http";
3
+ //#region src/tool-events.d.ts
4
+ interface AnalyticsEvent {
5
+ event: string;
6
+ properties?: Record<string, unknown>;
7
+ }
8
+ declare function isTrackedTool(name: string): boolean;
9
+ interface ToolExecutionLike {
10
+ callId: string | {
11
+ toString(): string;
12
+ };
13
+ name: string;
14
+ arguments?: unknown;
15
+ }
16
+ interface ToolResultLike {
17
+ isError: boolean;
18
+ error?: {
19
+ message?: unknown;
20
+ };
21
+ content?: ReadonlyArray<unknown>;
22
+ }
23
+ declare function toToolAnalyticsEvent(exec: ToolExecutionLike, result: ToolResultLike, responseTime: number): AnalyticsEvent;
24
+ declare class AnalyticsEventQueue {
25
+ private pending;
26
+ push(event: AnalyticsEvent): void;
27
+ drain(): AnalyticsEvent[];
28
+ }
29
+ //#endregion
30
+ //#region src/routes.d.ts
31
+ declare const ANALYTICS_ROUTE_PREFIX = "/api/v1/huaqiu/analytics";
32
+ type AnalyticsHandler = (req: IncomingMessage, res: ServerResponse) => void;
33
+ declare function createAnalyticsHandler(queue: AnalyticsEventQueue): AnalyticsHandler;
34
+ //#endregion
35
+ //#region src/index.d.ts
36
+ declare const name = "@huaqiu/dsh-analytics";
37
+ declare const inject: readonly ["webServer", "tools"];
38
+ interface HuaqiuAnalyticsService {
39
+ track(event: string, properties?: Record<string, unknown>): void;
40
+ }
41
+ declare module '@deepseek-ai/cordis' {
42
+ interface Context {
43
+ huaqiuAnalytics: HuaqiuAnalyticsService;
44
+ }
45
+ }
46
+ declare function apply(ctx: Context): void;
47
+ //#endregion
48
+ export { ANALYTICS_ROUTE_PREFIX, type AnalyticsEvent, AnalyticsEventQueue, HuaqiuAnalyticsService, apply, createAnalyticsHandler, inject, isTrackedTool, name, toToolAnalyticsEvent };
package/lib/index.mjs ADDED
@@ -0,0 +1,106 @@
1
+ import { getLogger } from "@huaqiu/dsh-plugin-log";
2
+ //#region src/routes.ts
3
+ const ANALYTICS_ROUTE_PREFIX = "/api/v1/huaqiu/analytics";
4
+ function createAnalyticsHandler(queue) {
5
+ return (req, res) => {
6
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
7
+ res.setHeader("content-type", "application/json; charset=utf-8");
8
+ res.setHeader("cache-control", "no-store");
9
+ if (req.method === "GET" && pathname === `/api/v1/huaqiu/analytics/events`) {
10
+ res.statusCode = 200;
11
+ res.end(JSON.stringify({ events: queue.drain() }));
12
+ return;
13
+ }
14
+ res.statusCode = 404;
15
+ res.end(JSON.stringify({ error: "not_found" }));
16
+ };
17
+ }
18
+ //#endregion
19
+ //#region src/tool-events.ts
20
+ const TRACKED_TOOLS = /* @__PURE__ */ new Set([
21
+ "generate_symbol_from_image",
22
+ "generate_footprint_from_image",
23
+ "generate_footprint_from_dimensions",
24
+ "generate_schematic_from_description",
25
+ "generate_system_module_graph",
26
+ "search_hqsch_parts",
27
+ "get_hqsch_part",
28
+ "get_hqsch_part_models",
29
+ "get_hqsch_supply_chain",
30
+ "pcb_preview",
31
+ "get_project_netlist",
32
+ "get_selection_netlist",
33
+ "get_active_page_netlist",
34
+ "get_eda_host_info",
35
+ "get_pcb_selection",
36
+ "get_pcb_board",
37
+ "get_eda_host_capabilities",
38
+ "kicad_ipc_diagnose",
39
+ "kicad_ipc_verify_live",
40
+ "kicad_pcb_create_track",
41
+ "kicad_pcb_create_via",
42
+ "kicad_pcb_create_copper_zone",
43
+ "kicad_pcb_add_footprint_from_template",
44
+ "kicad_pcb_move_rotate_footprint",
45
+ "kicad_pcb_update_selected_track_width",
46
+ "kicad_pcb_refill_zones",
47
+ "kicad_pcb_remove_selected_items"
48
+ ]);
49
+ function isTrackedTool(name) {
50
+ return TRACKED_TOOLS.has(name);
51
+ }
52
+ function toToolAnalyticsEvent(exec, result, responseTime) {
53
+ return {
54
+ event: "tool_call",
55
+ properties: {
56
+ tool_name: exec.name,
57
+ success: !result.isError,
58
+ response_time: responseTime,
59
+ err_msg: result.isError ? String(result.error?.message ?? "") : ""
60
+ }
61
+ };
62
+ }
63
+ var AnalyticsEventQueue = class {
64
+ pending = [];
65
+ push(event) {
66
+ this.pending.push(event);
67
+ }
68
+ drain() {
69
+ return this.pending.splice(0);
70
+ }
71
+ };
72
+ //#endregion
73
+ //#region src/index.ts
74
+ const name = "@huaqiu/dsh-analytics";
75
+ const inject = ["webServer", "tools"];
76
+ function apply(ctx) {
77
+ const log = getLogger("dsh-analytics");
78
+ const queue = new AnalyticsEventQueue();
79
+ const toolStartTimes = /* @__PURE__ */ new WeakMap();
80
+ const service = { track(event, properties) {
81
+ queue.push({
82
+ event,
83
+ properties
84
+ });
85
+ } };
86
+ ctx.effect(() => ctx.provide("huaqiuAnalytics", service));
87
+ ctx.effect(() => ctx.webServer.register({
88
+ kind: "prefix",
89
+ path: ANALYTICS_ROUTE_PREFIX,
90
+ handler: createAnalyticsHandler(queue)
91
+ }));
92
+ ctx.on("tools/pre-execute", async (exec, next) => {
93
+ if (isTrackedTool(exec.name)) toolStartTimes.set(exec, Date.now());
94
+ return next();
95
+ });
96
+ ctx.on("tools/result", (exec, result) => {
97
+ if (!isTrackedTool(exec.name)) return;
98
+ const startedAt = toolStartTimes.get(exec);
99
+ toolStartTimes.delete(exec);
100
+ const event = toToolAnalyticsEvent(exec, result, startedAt === void 0 ? 0 : Date.now() - startedAt);
101
+ queue.push(event);
102
+ });
103
+ log.info("analytics ready", { route: ANALYTICS_ROUTE_PREFIX });
104
+ }
105
+ //#endregion
106
+ export { ANALYTICS_ROUTE_PREFIX, AnalyticsEventQueue, apply, createAnalyticsHandler, inject, isTrackedTool, name, toToolAnalyticsEvent };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@huaqiu/dsh-analytics",
3
+ "version": "0.4.7",
4
+ "description": "Huaqiu Sensors Analytics integration for DeepSeek Harness",
5
+ "type": "module",
6
+ "main": "./lib/index.mjs",
7
+ "types": "./lib/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.mts",
11
+ "default": "./lib/index.mjs"
12
+ },
13
+ "./client": {
14
+ "default": "./lib/client.js"
15
+ },
16
+ "./cordis.patch.yml": "./cordis.patch.yml",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ },
23
+ "client": {
24
+ "platform": "web",
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-runtime",
27
+ "@huaqiu/dsh-auth"
28
+ ]
29
+ }
30
+ },
31
+ "peerDependencies": {
32
+ "@deepseek-ai/cordis": "^4.0.1",
33
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.0",
34
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.0"
35
+ },
36
+ "dependencies": {
37
+ "sa-sdk-javascript": "1.16.16",
38
+ "@huaqiu/dsh-auth": "0.4.7",
39
+ "@huaqiu/dsh-plugin-log": "0.4.7"
40
+ },
41
+ "files": [
42
+ "lib",
43
+ "src",
44
+ "cordis.patch.yml"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "scripts": {
50
+ "typecheck": "tsc --noEmit",
51
+ "build": "tsdown",
52
+ "test": "vitest run"
53
+ }
54
+ }
@@ -0,0 +1,57 @@
1
+ export const SENSORS_SERVER_URL = 'https://sensorsapi.hqchip.com/sa?project=eda'
2
+
3
+ export type ReqSource = 'website' | 'kicad' | 'huaqiu_eda_desktop'
4
+ export type AnalyticsProperties = Record<string, unknown>
5
+
6
+ export interface SensorsClient {
7
+ init(options: {
8
+ server_url: string
9
+ heatmap: { scroll_notice_map: 'not_collect' }
10
+ use_client_time: true
11
+ send_type: 'beacon'
12
+ show_log?: boolean
13
+ }): void
14
+ register(properties: AnalyticsProperties): void
15
+ quick(name: string, properties?: AnalyticsProperties): void
16
+ track(name: string, properties?: AnalyticsProperties): void
17
+ login(id: string): void
18
+ }
19
+
20
+ export interface HuaqiuAnalytics {
21
+ track(event: string, properties?: AnalyticsProperties): void
22
+ login(userId: string): void
23
+ }
24
+
25
+ export function trackLogin(analytics: HuaqiuAnalytics, uid: string): void {
26
+ analytics.login(uid)
27
+ analytics.track('login', { uid })
28
+ }
29
+
30
+ export function resolveReqSource(hostMode: boolean, targetHost: string): ReqSource {
31
+ if (!hostMode) return 'website'
32
+ return targetHost === 'hq-eda' ? 'huaqiu_eda_desktop' : 'kicad'
33
+ }
34
+
35
+ export function createAnalytics(
36
+ sensors: SensorsClient,
37
+ host: { hostMode: boolean; targetHost: string },
38
+ ): HuaqiuAnalytics {
39
+ sensors.init({
40
+ server_url: SENSORS_SERVER_URL,
41
+ heatmap: { scroll_notice_map: 'not_collect' },
42
+ use_client_time: true,
43
+ send_type: 'beacon',
44
+ show_log: true,
45
+ })
46
+ sensors.register({
47
+ env: 'prod',
48
+ project: 'dsh_copilot',
49
+ req_source: resolveReqSource(host.hostMode, host.targetHost),
50
+ })
51
+ sensors.quick('autoTrack')
52
+
53
+ return {
54
+ track: (event, properties) => { sensors.track(event, properties) },
55
+ login: userId => { sensors.login(userId) },
56
+ }
57
+ }
@@ -0,0 +1,103 @@
1
+ import sensors from 'sa-sdk-javascript'
2
+ import {
3
+ createAnalytics,
4
+ trackLogin,
5
+ type HuaqiuAnalytics,
6
+ type SensorsClient,
7
+ } from './analytics.js'
8
+ import type { AnalyticsEvent } from '../tool-events.js'
9
+
10
+ export const inject: string[] = []
11
+
12
+ interface AuthInfo {
13
+ id?: string
14
+ userId?: string
15
+ }
16
+
17
+ interface AuthApi {
18
+ isAuthenticated(): boolean
19
+ getUserInfo(): Promise<AuthInfo | null>
20
+ onAuthStateChanged(listener: (info: AuthInfo | null) => void): () => void
21
+ }
22
+
23
+ interface HqEdgeService {
24
+ context?: { getTargetHost?(): string }
25
+ }
26
+
27
+ interface ClientContext {
28
+ provide?(name: string, value: unknown): () => void
29
+ get?<T>(name: string): T | undefined
30
+ }
31
+
32
+ const POLL_INTERVAL_MS = 1000
33
+
34
+ async function resolveHostMode(): Promise<boolean> {
35
+ try {
36
+ const response = await fetch('/api/v1/huaqiu/auth/config', {
37
+ headers: { accept: 'application/json' },
38
+ })
39
+ if (!response.ok) return false
40
+ const body = await response.json() as { hostMode?: unknown }
41
+ return body.hostMode === true
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ async function drainNodeEvents(analytics: HuaqiuAnalytics): Promise<void> {
48
+ try {
49
+ const response = await fetch('/api/v1/huaqiu/analytics/events', {
50
+ headers: { accept: 'application/json' },
51
+ })
52
+ if (!response.ok) return
53
+ const body = await response.json() as { events?: AnalyticsEvent[] }
54
+ for (const item of body.events ?? []) {
55
+ if (typeof item?.event === 'string' && item.event.length > 0) {
56
+ analytics.track(item.event, item.properties)
57
+ }
58
+ }
59
+ } catch {
60
+ // Analytics must never affect the host application.
61
+ }
62
+ }
63
+
64
+ export function apply(ctx: ClientContext): () => void {
65
+ const disposers: Array<() => void> = []
66
+ let disposed = false
67
+
68
+ void resolveHostMode().then((hostMode) => {
69
+ if (disposed) return
70
+ const targetHost = ctx.get?.<HqEdgeService>('hqEdge')?.context?.getTargetHost?.() ?? ''
71
+ const analytics = createAnalytics(sensors as unknown as SensorsClient, { hostMode, targetHost })
72
+
73
+ const disposeProvide = ctx.provide?.('huaqiuAnalytics', analytics)
74
+ if (disposeProvide) disposers.push(disposeProvide)
75
+
76
+ const auth = ctx.get?.<{ auth?: AuthApi }>('huaqiuAuth')?.auth
77
+ if (auth) {
78
+ let wasAuthenticated = auth.isAuthenticated()
79
+ void auth.getUserInfo().then((info) => {
80
+ const userId = info?.id ?? info?.userId
81
+ if (userId) analytics.login(userId)
82
+ }).catch(() => undefined)
83
+ disposers.push(auth.onAuthStateChanged((info) => {
84
+ const authenticated = info !== null
85
+ const userId = info?.id ?? info?.userId
86
+ if (userId && !wasAuthenticated && authenticated) trackLogin(analytics, userId)
87
+ else if (userId) analytics.login(userId)
88
+ wasAuthenticated = authenticated
89
+ }))
90
+ }
91
+
92
+ void drainNodeEvents(analytics)
93
+ const timer = window.setInterval(() => { void drainNodeEvents(analytics) }, POLL_INTERVAL_MS)
94
+ disposers.push(() => { window.clearInterval(timer) })
95
+ })
96
+
97
+ return () => {
98
+ disposed = true
99
+ for (const dispose of disposers.reverse()) dispose()
100
+ }
101
+ }
102
+
103
+ export { createAnalytics, resolveReqSource, trackLogin, type HuaqiuAnalytics } from './analytics.js'
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '@deepseek-ai/dsh-host-webserver'
3
+ import type {} from '@deepseek-ai/dsh-tools'
4
+ import { getLogger } from '@huaqiu/dsh-plugin-log'
5
+ import { ANALYTICS_ROUTE_PREFIX, createAnalyticsHandler } from './routes.js'
6
+ import { AnalyticsEventQueue, isTrackedTool, toToolAnalyticsEvent, type AnalyticsEvent } from './tool-events.js'
7
+
8
+ export const name = '@huaqiu/dsh-analytics'
9
+ export const inject = ['webServer', 'tools'] as const
10
+
11
+ export interface HuaqiuAnalyticsService {
12
+ track(event: string, properties?: Record<string, unknown>): void
13
+ }
14
+
15
+ declare module '@deepseek-ai/cordis' {
16
+ interface Context {
17
+ huaqiuAnalytics: HuaqiuAnalyticsService
18
+ }
19
+ }
20
+
21
+ export function apply(ctx: Context): void {
22
+ const log = getLogger('dsh-analytics')
23
+ const queue = new AnalyticsEventQueue()
24
+ const toolStartTimes = new WeakMap<object, number>()
25
+ const service: HuaqiuAnalyticsService = {
26
+ track(event, properties) {
27
+ queue.push({ event, properties })
28
+ },
29
+ }
30
+
31
+ ctx.effect(() => ctx.provide('huaqiuAnalytics', service))
32
+ ctx.effect(() => ctx.webServer.register({
33
+ kind: 'prefix',
34
+ path: ANALYTICS_ROUTE_PREFIX,
35
+ handler: createAnalyticsHandler(queue),
36
+ }))
37
+ ctx.on('tools/pre-execute', async (exec, next) => {
38
+ const tracked = isTrackedTool(exec.name)
39
+ if (tracked) toolStartTimes.set(exec, Date.now())
40
+ return next()
41
+ })
42
+ ctx.on('tools/result', (exec, result) => {
43
+ const tracked = isTrackedTool(exec.name)
44
+ if (!tracked) return
45
+ const startedAt = toolStartTimes.get(exec)
46
+ toolStartTimes.delete(exec)
47
+ const event = toToolAnalyticsEvent(exec, result, startedAt === undefined ? 0 : Date.now() - startedAt)
48
+ queue.push(event)
49
+ })
50
+
51
+ log.info('analytics ready', { route: ANALYTICS_ROUTE_PREFIX })
52
+ }
53
+
54
+ export { ANALYTICS_ROUTE_PREFIX, createAnalyticsHandler } from './routes.js'
55
+ export { AnalyticsEventQueue, isTrackedTool, toToolAnalyticsEvent, type AnalyticsEvent } from './tool-events.js'
package/src/routes.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http'
2
+ import type { AnalyticsEventQueue } from './tool-events.js'
3
+
4
+ export const ANALYTICS_ROUTE_PREFIX = '/api/v1/huaqiu/analytics'
5
+
6
+ export type AnalyticsHandler = (req: IncomingMessage, res: ServerResponse) => void
7
+
8
+ export function createAnalyticsHandler(queue: AnalyticsEventQueue): AnalyticsHandler {
9
+ return (req, res) => {
10
+ const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
11
+ res.setHeader('content-type', 'application/json; charset=utf-8')
12
+ res.setHeader('cache-control', 'no-store')
13
+
14
+ if (req.method === 'GET' && pathname === `${ANALYTICS_ROUTE_PREFIX}/events`) {
15
+ res.statusCode = 200
16
+ res.end(JSON.stringify({ events: queue.drain() }))
17
+ return
18
+ }
19
+
20
+ res.statusCode = 404
21
+ res.end(JSON.stringify({ error: 'not_found' }))
22
+ }
23
+ }
@@ -0,0 +1,79 @@
1
+ export interface AnalyticsEvent {
2
+ event: string
3
+ properties?: Record<string, unknown>
4
+ }
5
+
6
+ // 埋点工具白名单
7
+ const TRACKED_TOOLS = new Set([
8
+ 'generate_symbol_from_image',
9
+ 'generate_footprint_from_image',
10
+ 'generate_footprint_from_dimensions',
11
+ 'generate_schematic_from_description',
12
+ 'generate_system_module_graph',
13
+ 'search_hqsch_parts',
14
+ 'get_hqsch_part',
15
+ 'get_hqsch_part_models',
16
+ 'get_hqsch_supply_chain',
17
+ 'pcb_preview',
18
+ 'get_project_netlist',
19
+ 'get_selection_netlist',
20
+ 'get_active_page_netlist',
21
+ 'get_eda_host_info',
22
+ 'get_pcb_selection',
23
+ 'get_pcb_board',
24
+ 'get_eda_host_capabilities',
25
+ 'kicad_ipc_diagnose',
26
+ 'kicad_ipc_verify_live',
27
+ 'kicad_pcb_create_track',
28
+ 'kicad_pcb_create_via',
29
+ 'kicad_pcb_create_copper_zone',
30
+ 'kicad_pcb_add_footprint_from_template',
31
+ 'kicad_pcb_move_rotate_footprint',
32
+ 'kicad_pcb_update_selected_track_width',
33
+ 'kicad_pcb_refill_zones',
34
+ 'kicad_pcb_remove_selected_items',
35
+ ])
36
+
37
+ export function isTrackedTool(name: string): boolean {
38
+ return TRACKED_TOOLS.has(name)
39
+ }
40
+
41
+ interface ToolExecutionLike {
42
+ callId: string | { toString(): string }
43
+ name: string
44
+ arguments?: unknown
45
+ }
46
+
47
+ interface ToolResultLike {
48
+ isError: boolean
49
+ error?: { message?: unknown }
50
+ content?: ReadonlyArray<unknown>
51
+ }
52
+
53
+ export function toToolAnalyticsEvent(
54
+ exec: ToolExecutionLike,
55
+ result: ToolResultLike,
56
+ responseTime: number,
57
+ ): AnalyticsEvent {
58
+ return {
59
+ event: 'tool_call',
60
+ properties: {
61
+ tool_name: exec.name,
62
+ success: !result.isError,
63
+ response_time: responseTime,
64
+ err_msg: result.isError ? String(result.error?.message ?? '') : '',
65
+ },
66
+ }
67
+ }
68
+
69
+ export class AnalyticsEventQueue {
70
+ private pending: AnalyticsEvent[] = []
71
+
72
+ push(event: AnalyticsEvent): void {
73
+ this.pending.push(event)
74
+ }
75
+
76
+ drain(): AnalyticsEvent[] {
77
+ return this.pending.splice(0)
78
+ }
79
+ }