@memtensor/memos-local-openclaw-plugin 1.0.4-beta.7 → 1.0.4-beta.8
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.md +1 -1
- package/dist/capture/index.d.ts.map +1 -1
- package/dist/capture/index.js +6 -0
- package/dist/capture/index.js.map +1 -1
- package/dist/client/connector.d.ts.map +1 -1
- package/dist/client/connector.js +17 -4
- package/dist/client/connector.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -2
- package/dist/config.js.map +1 -1
- package/dist/hub/server.d.ts +7 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/server.js +160 -5
- package/dist/hub/server.js.map +1 -1
- package/dist/ingest/providers/index.d.ts.map +1 -1
- package/dist/ingest/providers/index.js +37 -6
- package/dist/ingest/providers/index.js.map +1 -1
- package/dist/shared/llm-call.d.ts +1 -0
- package/dist/shared/llm-call.d.ts.map +1 -1
- package/dist/shared/llm-call.js +82 -8
- package/dist/shared/llm-call.js.map +1 -1
- package/dist/skill/evolver.d.ts +2 -0
- package/dist/skill/evolver.d.ts.map +1 -1
- package/dist/skill/evolver.js +3 -0
- package/dist/skill/evolver.js.map +1 -1
- package/dist/storage/sqlite.d.ts +4 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +8 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/telemetry.d.ts +12 -5
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +135 -38
- package/dist/telemetry.js.map +1 -1
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +473 -191
- package/dist/viewer/html.js.map +1 -1
- package/dist/viewer/server.d.ts +14 -0
- package/dist/viewer/server.d.ts.map +1 -1
- package/dist/viewer/server.js +233 -20
- package/dist/viewer/server.js.map +1 -1
- package/index.ts +26 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
- package/scripts/postinstall.cjs +1 -1
- package/src/capture/index.ts +8 -0
- package/src/client/connector.ts +17 -4
- package/src/config.ts +0 -2
- package/src/hub/server.ts +157 -5
- package/src/ingest/providers/index.ts +41 -7
- package/src/shared/llm-call.ts +97 -9
- package/src/skill/evolver.ts +5 -0
- package/src/storage/sqlite.ts +11 -6
- package/src/telemetry.ts +152 -39
- package/src/types.ts +1 -2
- package/src/viewer/html.ts +473 -191
- package/src/viewer/server.ts +208 -20
package/src/shared/llm-call.ts
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import type { SummarizerConfig, Logger, PluginContext, OpenClawAPI } from "../types";
|
|
3
|
+
import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect provider type from provider key name or base URL.
|
|
7
|
+
*/
|
|
8
|
+
function detectProvider(providerKey: string | undefined, baseUrl: string): SummaryProvider {
|
|
9
|
+
const key = providerKey?.toLowerCase() ?? "";
|
|
10
|
+
const url = baseUrl.toLowerCase();
|
|
11
|
+
if (key.includes("anthropic") || url.includes("anthropic")) return "anthropic";
|
|
12
|
+
if (key.includes("gemini") || url.includes("generativelanguage.googleapis.com")) {
|
|
13
|
+
return "gemini";
|
|
14
|
+
}
|
|
15
|
+
if (key.includes("bedrock") || url.includes("bedrock")) return "bedrock";
|
|
16
|
+
return "openai_compatible";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Return the correct default endpoint for a given provider.
|
|
21
|
+
*/
|
|
22
|
+
function defaultEndpointForProvider(provider: SummaryProvider, baseUrl: string): string {
|
|
23
|
+
const stripped = baseUrl.replace(/\/+$/, "");
|
|
24
|
+
if (provider === "anthropic") {
|
|
25
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
26
|
+
return `${stripped}/v1/messages`;
|
|
27
|
+
}
|
|
28
|
+
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
29
|
+
if (stripped.endsWith("/completions")) return stripped;
|
|
30
|
+
return `${stripped}/chat/completions`;
|
|
31
|
+
}
|
|
4
32
|
|
|
5
33
|
/**
|
|
6
34
|
* Build a SummarizerConfig from OpenClaw's native model configuration (openclaw.json).
|
|
@@ -30,13 +58,12 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
|
|
|
30
58
|
const apiKey: string | undefined = providerCfg.apiKey;
|
|
31
59
|
if (!baseUrl || !apiKey) return undefined;
|
|
32
60
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
: baseUrl.replace(/\/+$/, "") + "/chat/completions";
|
|
61
|
+
const provider = detectProvider(providerKey, baseUrl);
|
|
62
|
+
const endpoint = defaultEndpointForProvider(provider, baseUrl);
|
|
36
63
|
|
|
37
|
-
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl}`);
|
|
64
|
+
log.debug(`OpenClaw fallback model: ${modelId} via ${baseUrl} (${provider})`);
|
|
38
65
|
return {
|
|
39
|
-
provider
|
|
66
|
+
provider,
|
|
40
67
|
endpoint,
|
|
41
68
|
apiKey,
|
|
42
69
|
model: modelId,
|
|
@@ -70,23 +97,34 @@ export interface LLMCallOptions {
|
|
|
70
97
|
openclawAPI?: OpenClawAPI;
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
function
|
|
100
|
+
function normalizeOpenAIEndpoint(url: string): string {
|
|
74
101
|
const stripped = url.replace(/\/+$/, "");
|
|
75
102
|
if (stripped.endsWith("/chat/completions")) return stripped;
|
|
76
103
|
if (stripped.endsWith("/completions")) return stripped;
|
|
77
104
|
return `${stripped}/chat/completions`;
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
function normalizeAnthropicEndpoint(url: string): string {
|
|
108
|
+
const stripped = url.replace(/\/+$/, "");
|
|
109
|
+
if (stripped.endsWith("/v1/messages")) return stripped;
|
|
110
|
+
if (stripped.endsWith("/messages")) return stripped;
|
|
111
|
+
return `${stripped}/v1/messages`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isAnthropicProvider(cfg: SummarizerConfig): boolean {
|
|
115
|
+
return cfg.provider === "anthropic";
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
/**
|
|
81
119
|
* Make a single LLM call with the given config. Throws on failure.
|
|
82
120
|
* When cfg.provider === "openclaw", delegates to the OpenClaw host completion API.
|
|
121
|
+
* Dispatches to Anthropic or OpenAI-compatible format based on provider.
|
|
83
122
|
*/
|
|
84
123
|
export async function callLLMOnce(
|
|
85
124
|
cfg: SummarizerConfig,
|
|
86
125
|
prompt: string,
|
|
87
126
|
opts: LLMCallOptions = {},
|
|
88
127
|
): Promise<string> {
|
|
89
|
-
// Handle openclaw provider via host completion API
|
|
90
128
|
if (cfg.provider === "openclaw") {
|
|
91
129
|
const api = opts.openclawAPI;
|
|
92
130
|
if (!api) {
|
|
@@ -101,7 +139,57 @@ export async function callLLMOnce(
|
|
|
101
139
|
return response.text.trim();
|
|
102
140
|
}
|
|
103
141
|
|
|
104
|
-
|
|
142
|
+
if (isAnthropicProvider(cfg)) {
|
|
143
|
+
return callLLMOnceAnthropic(cfg, prompt, opts);
|
|
144
|
+
}
|
|
145
|
+
return callLLMOnceOpenAI(cfg, prompt, opts);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function callLLMOnceAnthropic(
|
|
149
|
+
cfg: SummarizerConfig,
|
|
150
|
+
prompt: string,
|
|
151
|
+
opts: LLMCallOptions = {},
|
|
152
|
+
): Promise<string> {
|
|
153
|
+
const endpoint = normalizeAnthropicEndpoint(
|
|
154
|
+
cfg.endpoint ?? "https://api.anthropic.com/v1/messages",
|
|
155
|
+
);
|
|
156
|
+
const model = cfg.model ?? "claude-3-haiku-20240307";
|
|
157
|
+
const headers: Record<string, string> = {
|
|
158
|
+
"Content-Type": "application/json",
|
|
159
|
+
"x-api-key": cfg.apiKey ?? "",
|
|
160
|
+
"anthropic-version": "2023-06-01",
|
|
161
|
+
...cfg.headers,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const resp = await fetch(endpoint, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers,
|
|
167
|
+
body: JSON.stringify({
|
|
168
|
+
model,
|
|
169
|
+
temperature: opts.temperature ?? 0.1,
|
|
170
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
171
|
+
messages: [{ role: "user", content: prompt }],
|
|
172
|
+
}),
|
|
173
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!resp.ok) {
|
|
177
|
+
const body = await resp.text();
|
|
178
|
+
throw new Error(`LLM call failed (${resp.status}): ${body}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const json = (await resp.json()) as { content: Array<{ type: string; text: string }> };
|
|
182
|
+
return json.content.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function callLLMOnceOpenAI(
|
|
186
|
+
cfg: SummarizerConfig,
|
|
187
|
+
prompt: string,
|
|
188
|
+
opts: LLMCallOptions = {},
|
|
189
|
+
): Promise<string> {
|
|
190
|
+
const endpoint = normalizeOpenAIEndpoint(
|
|
191
|
+
cfg.endpoint ?? "https://api.openai.com/v1/chat/completions",
|
|
192
|
+
);
|
|
105
193
|
const model = cfg.model ?? "gpt-4o-mini";
|
|
106
194
|
const headers: Record<string, string> = {
|
|
107
195
|
"Content-Type": "application/json",
|
package/src/skill/evolver.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { SkillUpgrader } from "./upgrader";
|
|
|
12
12
|
import { SkillInstaller } from "./installer";
|
|
13
13
|
import { buildSkillConfigChain, callLLMWithFallback } from "../shared/llm-call";
|
|
14
14
|
|
|
15
|
+
export type SkillEvolvedCallback = (skillName: string, upgradeType: "created" | "upgraded") => void;
|
|
16
|
+
|
|
15
17
|
export class SkillEvolver {
|
|
16
18
|
private evaluator: SkillEvaluator;
|
|
17
19
|
private generator: SkillGenerator;
|
|
@@ -19,6 +21,7 @@ export class SkillEvolver {
|
|
|
19
21
|
private installer: SkillInstaller;
|
|
20
22
|
private processing = false;
|
|
21
23
|
private queue: Task[] = [];
|
|
24
|
+
onSkillEvolved: SkillEvolvedCallback | null = null;
|
|
22
25
|
|
|
23
26
|
constructor(
|
|
24
27
|
private store: SqliteStore,
|
|
@@ -279,6 +282,7 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
279
282
|
if (upgraded) {
|
|
280
283
|
this.store.linkTaskSkill(task.id, freshSkill.id, "evolved_from", freshSkill.version + 1);
|
|
281
284
|
this.installer.syncIfInstalled(freshSkill.name);
|
|
285
|
+
this.onSkillEvolved?.(freshSkill.name, "upgraded");
|
|
282
286
|
} else {
|
|
283
287
|
this.store.linkTaskSkill(task.id, freshSkill.id, "applied_to", freshSkill.version);
|
|
284
288
|
}
|
|
@@ -307,6 +311,7 @@ Use selectedIndex 0 when none is highly relevant.`;
|
|
|
307
311
|
this.markChunksWithSkill(chunks, skill.id);
|
|
308
312
|
this.store.linkTaskSkill(task.id, skill.id, "generated_from", 1);
|
|
309
313
|
this.store.setTaskSkillMeta(task.id, { skillStatus: "generated", skillReason: evalResult.reason });
|
|
314
|
+
this.onSkillEvolved?.(skill.name, "created");
|
|
310
315
|
|
|
311
316
|
const autoInstall = this.ctx.config.skillEvolution?.autoInstall ?? DEFAULTS.skillAutoInstall;
|
|
312
317
|
if (autoInstall && skill.status === "active") {
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -1818,8 +1818,8 @@ export class SqliteStore {
|
|
|
1818
1818
|
return result.changes > 0;
|
|
1819
1819
|
}
|
|
1820
1820
|
|
|
1821
|
-
updateHubUserActivity(userId: string, ip: string): void {
|
|
1822
|
-
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, Date.now(), userId);
|
|
1821
|
+
updateHubUserActivity(userId: string, ip: string, timestamp?: number): void {
|
|
1822
|
+
this.db.prepare('UPDATE hub_users SET last_ip = ?, last_active_at = ? WHERE id = ?').run(ip, timestamp ?? Date.now(), userId);
|
|
1823
1823
|
}
|
|
1824
1824
|
|
|
1825
1825
|
getHubUserContributions(): Record<string, { memoryCount: number; taskCount: number; skillCount: number }> {
|
|
@@ -2063,6 +2063,11 @@ export class SqliteStore {
|
|
|
2063
2063
|
}));
|
|
2064
2064
|
}
|
|
2065
2065
|
|
|
2066
|
+
listHubChunksByTaskId(hubTaskId: string): HubChunkRecord[] {
|
|
2067
|
+
const rows = this.db.prepare('SELECT * FROM hub_chunks WHERE hub_task_id = ? ORDER BY created_at ASC').all(hubTaskId) as HubChunkRow[];
|
|
2068
|
+
return rows.map(rowToHubChunk);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2066
2071
|
deleteHubTaskById(taskId: string): boolean {
|
|
2067
2072
|
const info = this.db.prepare('DELETE FROM hub_tasks WHERE id = ?').run(taskId);
|
|
2068
2073
|
return info.changes > 0;
|
|
@@ -2233,7 +2238,7 @@ export class SqliteStore {
|
|
|
2233
2238
|
return row ?? null;
|
|
2234
2239
|
}
|
|
2235
2240
|
|
|
2236
|
-
listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2241
|
+
listVisibleHubMemories(userId: string, limit = 40): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2237
2242
|
const rows = this.db.prepare(`
|
|
2238
2243
|
SELECT m.*, u.username AS owner_name, NULL AS group_name
|
|
2239
2244
|
FROM hub_memories m
|
|
@@ -2243,13 +2248,13 @@ export class SqliteStore {
|
|
|
2243
2248
|
`).all(limit) as any[];
|
|
2244
2249
|
return rows.map(r => ({
|
|
2245
2250
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2246
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2251
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2247
2252
|
groupId: r.group_id, groupName: r.group_name ?? null, visibility: r.visibility,
|
|
2248
2253
|
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2249
2254
|
}));
|
|
2250
2255
|
}
|
|
2251
2256
|
|
|
2252
|
-
listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2257
|
+
listAllHubMemories(): Array<{ id: string; sourceChunkId: string; sourceUserId: string; role: string; content: string; summary: string; kind: string; groupId: string | null; groupName: string | null; visibility: string; ownerName: string; createdAt: number; updatedAt: number }> {
|
|
2253
2258
|
const rows = this.db.prepare(`
|
|
2254
2259
|
SELECT m.*, u.username AS owner_name
|
|
2255
2260
|
FROM hub_memories m
|
|
@@ -2258,7 +2263,7 @@ export class SqliteStore {
|
|
|
2258
2263
|
`).all() as any[];
|
|
2259
2264
|
return rows.map(r => ({
|
|
2260
2265
|
id: r.id, sourceChunkId: r.source_chunk_id, sourceUserId: r.source_user_id,
|
|
2261
|
-
role: r.role, summary: r.summary, kind: r.kind,
|
|
2266
|
+
role: r.role, content: r.content ?? "", summary: r.summary, kind: r.kind,
|
|
2262
2267
|
groupId: r.group_id, groupName: null as string | null, visibility: r.visibility,
|
|
2263
2268
|
ownerName: r.owner_name ?? "unknown", createdAt: r.created_at, updatedAt: r.updated_at,
|
|
2264
2269
|
}));
|
package/src/telemetry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telemetry module — anonymous usage analytics via
|
|
2
|
+
* Telemetry module — anonymous usage analytics via Aliyun ARMS RUM.
|
|
3
3
|
*
|
|
4
4
|
* Privacy-first design:
|
|
5
5
|
* - Enabled by default with anonymous data only; opt-out via TELEMETRY_ENABLED=false
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - Only sends aggregate counts, tool names, latencies, and version info
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { PostHog } from "posthog-node";
|
|
12
11
|
import * as fs from "fs";
|
|
13
12
|
import * as path from "path";
|
|
14
13
|
import * as os from "os";
|
|
@@ -17,45 +16,61 @@ import type { Logger } from "./types";
|
|
|
17
16
|
|
|
18
17
|
export interface TelemetryConfig {
|
|
19
18
|
enabled?: boolean;
|
|
20
|
-
posthogApiKey?: string;
|
|
21
|
-
posthogHost?: string;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
const
|
|
25
|
-
|
|
21
|
+
const ARMS_ENDPOINT =
|
|
22
|
+
"https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com" +
|
|
23
|
+
"/rum/web/v2" +
|
|
24
|
+
"?workspace=default-cms-1026429231103299-cn-hangzhou" +
|
|
25
|
+
"&service_id=a3u72ukxmr@066657d42a13a9a9f337f";
|
|
26
|
+
|
|
27
|
+
const ARMS_PID = "a3u72ukxmr@066657d42a13a9a9f337f";
|
|
28
|
+
const ARMS_ENV = "prod";
|
|
29
|
+
|
|
30
|
+
const FLUSH_AT = 10;
|
|
31
|
+
const FLUSH_INTERVAL_MS = 30_000;
|
|
32
|
+
const SEND_TIMEOUT_MS = 30_000;
|
|
33
|
+
const SESSION_TTL_MS = 30 * 60_000; // 30 min inactivity → new session
|
|
34
|
+
interface ArmsEvent {
|
|
35
|
+
event_type: "custom";
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
group: string;
|
|
39
|
+
value: number;
|
|
40
|
+
properties: Record<string, string | number | boolean>;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
event_id: string;
|
|
43
|
+
times: number;
|
|
44
|
+
}
|
|
26
45
|
|
|
27
46
|
export class Telemetry {
|
|
28
|
-
private client: PostHog | null = null;
|
|
29
47
|
private distinctId: string;
|
|
30
48
|
private enabled: boolean;
|
|
31
49
|
private pluginVersion: string;
|
|
32
50
|
private log: Logger;
|
|
33
51
|
private dailyPingSent = false;
|
|
34
52
|
private dailyPingDate = "";
|
|
53
|
+
private buffer: ArmsEvent[] = [];
|
|
54
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
55
|
+
private sessionId: string;
|
|
56
|
+
private firstSeenDate: string;
|
|
35
57
|
|
|
36
58
|
constructor(config: TelemetryConfig, stateDir: string, pluginVersion: string, log: Logger) {
|
|
37
59
|
this.log = log;
|
|
38
60
|
this.pluginVersion = pluginVersion;
|
|
39
61
|
this.enabled = config.enabled !== false;
|
|
40
62
|
this.distinctId = this.loadOrCreateAnonymousId(stateDir);
|
|
63
|
+
this.firstSeenDate = this.loadOrCreateFirstSeen(stateDir);
|
|
64
|
+
this.sessionId = this.loadOrCreateSessionId(stateDir);
|
|
41
65
|
|
|
42
66
|
if (!this.enabled) {
|
|
43
67
|
this.log.debug("Telemetry disabled (opt-out via TELEMETRY_ENABLED=false)");
|
|
44
68
|
return;
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
host: config.posthogHost || DEFAULT_POSTHOG_HOST,
|
|
51
|
-
flushAt: 10,
|
|
52
|
-
flushInterval: 30_000,
|
|
53
|
-
});
|
|
54
|
-
this.log.debug("Telemetry initialized (PostHog)");
|
|
55
|
-
} catch (err) {
|
|
56
|
-
this.log.warn(`Telemetry init failed: ${err}`);
|
|
57
|
-
this.enabled = false;
|
|
58
|
-
}
|
|
71
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
72
|
+
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
73
|
+
this.log.debug("Telemetry initialized (ARMS)");
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
private loadOrCreateAnonymousId(stateDir: string): string {
|
|
@@ -81,24 +96,113 @@ export class Telemetry {
|
|
|
81
96
|
return newId;
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
private loadOrCreateSessionId(stateDir: string): string {
|
|
100
|
+
const filePath = path.join(stateDir, "memos-local", ".session");
|
|
101
|
+
try {
|
|
102
|
+
const raw = fs.readFileSync(filePath, "utf-8").trim();
|
|
103
|
+
const sep = raw.indexOf("|");
|
|
104
|
+
if (sep > 0) {
|
|
105
|
+
const ts = parseInt(raw.slice(0, sep), 10);
|
|
106
|
+
const id = raw.slice(sep + 1);
|
|
107
|
+
if (id.length > 10 && Date.now() - ts < SESSION_TTL_MS) {
|
|
108
|
+
this.touchSession(filePath, id);
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
const newId = uuidv4();
|
|
114
|
+
this.touchSession(filePath, newId);
|
|
115
|
+
return newId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private touchSession(filePath: string, id: string): void {
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
121
|
+
fs.writeFileSync(filePath, `${Date.now()}|${id}`, "utf-8");
|
|
122
|
+
} catch {}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private loadOrCreateFirstSeen(stateDir: string): string {
|
|
126
|
+
const filePath = path.join(stateDir, "memos-local", ".first-seen");
|
|
127
|
+
try {
|
|
128
|
+
const existing = fs.readFileSync(filePath, "utf-8").trim();
|
|
129
|
+
if (existing.length === 10) return existing;
|
|
130
|
+
} catch {}
|
|
131
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(filePath, today, "utf-8");
|
|
135
|
+
} catch {}
|
|
136
|
+
return today;
|
|
137
|
+
}
|
|
138
|
+
|
|
84
139
|
private capture(event: string, properties?: Record<string, unknown>): void {
|
|
85
|
-
if (!this.enabled
|
|
140
|
+
if (!this.enabled) return;
|
|
141
|
+
|
|
142
|
+
const safeProps: Record<string, string | number | boolean> = {
|
|
143
|
+
plugin_version: this.pluginVersion,
|
|
144
|
+
os: os.platform(),
|
|
145
|
+
os_version: os.release(),
|
|
146
|
+
node_version: process.version,
|
|
147
|
+
arch: os.arch(),
|
|
148
|
+
};
|
|
149
|
+
if (properties) {
|
|
150
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
151
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
152
|
+
safeProps[k] = v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.buffer.push({
|
|
158
|
+
event_type: "custom",
|
|
159
|
+
type: "memos_plugin",
|
|
160
|
+
name: event,
|
|
161
|
+
group: "memos_local",
|
|
162
|
+
value: 1,
|
|
163
|
+
properties: safeProps,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
event_id: uuidv4(),
|
|
166
|
+
times: 1,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (this.buffer.length >= FLUSH_AT) {
|
|
170
|
+
this.flush();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private buildPayload(events: ArmsEvent[]): Record<string, unknown> {
|
|
175
|
+
return {
|
|
176
|
+
app: {
|
|
177
|
+
id: ARMS_PID,
|
|
178
|
+
env: ARMS_ENV,
|
|
179
|
+
version: this.pluginVersion,
|
|
180
|
+
type: "node",
|
|
181
|
+
},
|
|
182
|
+
user: { id: this.distinctId },
|
|
183
|
+
session: { id: this.sessionId },
|
|
184
|
+
net: {},
|
|
185
|
+
view: { id: "plugin", name: "memos-local-openclaw" },
|
|
186
|
+
events,
|
|
187
|
+
_v: "1.0.0",
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async flush(): Promise<void> {
|
|
192
|
+
if (this.buffer.length === 0) return;
|
|
193
|
+
const batch = this.buffer.splice(0);
|
|
194
|
+
const payload = this.buildPayload(batch);
|
|
86
195
|
|
|
87
196
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
os: os.platform(),
|
|
94
|
-
os_version: os.release(),
|
|
95
|
-
node_version: process.version,
|
|
96
|
-
arch: os.arch(),
|
|
97
|
-
...properties,
|
|
98
|
-
},
|
|
197
|
+
const resp = await fetch(ARMS_ENDPOINT, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "Content-Type": "text/plain" },
|
|
200
|
+
body: JSON.stringify(payload),
|
|
201
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
99
202
|
});
|
|
100
|
-
|
|
101
|
-
|
|
203
|
+
this.log.debug(`Telemetry flush: ${batch.length} events → ${resp.status}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.log.debug(`Telemetry flush failed: ${err}`);
|
|
102
206
|
}
|
|
103
207
|
}
|
|
104
208
|
|
|
@@ -131,7 +235,7 @@ export class Telemetry {
|
|
|
131
235
|
});
|
|
132
236
|
}
|
|
133
237
|
|
|
134
|
-
trackSkillEvolved(skillName: string, upgradeType:
|
|
238
|
+
trackSkillEvolved(skillName: string, upgradeType: "created" | "upgraded"): void {
|
|
135
239
|
this.capture("skill_evolved", {
|
|
136
240
|
skill_name: skillName,
|
|
137
241
|
upgrade_type: upgradeType,
|
|
@@ -150,19 +254,28 @@ export class Telemetry {
|
|
|
150
254
|
});
|
|
151
255
|
}
|
|
152
256
|
|
|
257
|
+
trackError(source: string, errorType: string): void {
|
|
258
|
+
this.capture("plugin_error", {
|
|
259
|
+
error_source: source,
|
|
260
|
+
error_type: errorType,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
153
264
|
private maybeSendDailyPing(): void {
|
|
154
265
|
const today = new Date().toISOString().slice(0, 10);
|
|
155
266
|
if (this.dailyPingSent && this.dailyPingDate === today) return;
|
|
156
267
|
this.dailyPingSent = true;
|
|
157
268
|
this.dailyPingDate = today;
|
|
158
|
-
this.capture("daily_active"
|
|
269
|
+
this.capture("daily_active", {
|
|
270
|
+
first_seen_date: this.firstSeenDate,
|
|
271
|
+
});
|
|
159
272
|
}
|
|
160
273
|
|
|
161
274
|
async shutdown(): Promise<void> {
|
|
162
|
-
if (this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
} catch {}
|
|
275
|
+
if (this.flushTimer) {
|
|
276
|
+
clearInterval(this.flushTimer);
|
|
277
|
+
this.flushTimer = null;
|
|
166
278
|
}
|
|
279
|
+
await this.flush();
|
|
167
280
|
}
|
|
168
281
|
}
|
package/src/types.ts
CHANGED
|
@@ -255,8 +255,6 @@ export interface SkillEvolutionConfig {
|
|
|
255
255
|
|
|
256
256
|
export interface TelemetryConfig {
|
|
257
257
|
enabled?: boolean;
|
|
258
|
-
posthogApiKey?: string;
|
|
259
|
-
posthogHost?: string;
|
|
260
258
|
}
|
|
261
259
|
|
|
262
260
|
export type SharingRole = "hub" | "client";
|
|
@@ -277,6 +275,7 @@ export interface ClientModeConfig {
|
|
|
277
275
|
hubAddress?: string;
|
|
278
276
|
userToken?: string;
|
|
279
277
|
teamToken?: string;
|
|
278
|
+
nickname?: string;
|
|
280
279
|
pendingUserId?: string;
|
|
281
280
|
}
|
|
282
281
|
|