@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.
- package/LICENSE +21 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +3221 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +48 -0
- package/lib/index.mjs +106 -0
- package/package.json +54 -0
- package/src/client/analytics.ts +57 -0
- package/src/client/index.ts +103 -0
- package/src/index.ts +55 -0
- package/src/routes.ts +23 -0
- package/src/tool-events.ts +79 -0
package/lib/index.d.mts
ADDED
|
@@ -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
|
+
}
|