@memtensor/memos-local-openclaw-plugin 1.0.9-beta.1 → 1.0.9-beta.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@memtensor/memos-local-openclaw-plugin)](https://www.npmjs.com/package/@memtensor/memos-local-openclaw-plugin)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/MemTensor/MemOS/blob/main/LICENSE)
5
- [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
+ [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
6
6
  [![GitHub](https://img.shields.io/badge/GitHub-Source-181717?logo=github)](https://github.com/MemTensor/MemOS/tree/main/apps/memos-local-openclaw)
7
7
 
8
8
  Persistent local conversation memory for [OpenClaw](https://github.com/nicepkg/openclaw) AI Agents. Every conversation is automatically captured, semantically indexed, and instantly recallable — with **task summarization & skill evolution**, **team sharing for memories and skills**, and **multi-agent collaborative memory**.
@@ -618,7 +618,7 @@ openclaw plugins install @memtensor/memos-local-openclaw-plugin
618
618
 
619
619
  ## Troubleshooting
620
620
 
621
- > 📖 **详细排查指南 / Detailed troubleshooting guide:** [docs/troubleshooting.html](https://memtensor.github.io/MemOS/apps/memos-local-openclaw/docs/troubleshooting.html) — 包含逐步排查流程、日志查看方法、完全重装步骤等。
621
+ > 📖 **详细排查指南 / Detailed troubleshooting guide:** [Troubleshooting](https://memos-claw.openmem.net/docs/troubleshooting.html) — 包含逐步排查流程、日志查看方法、完全重装步骤等。
622
622
  >
623
623
  > 📦 **better-sqlite3 official troubleshooting:** [better-sqlite3 Troubleshooting](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) — the upstream guide for native module build issues.
624
624
 
@@ -640,7 +640,7 @@ openclaw plugins install @memtensor/memos-local-openclaw-plugin
640
640
  Search for `memos-local`, `failed to load`, `Error`, `Cannot find module`.
641
641
 
642
642
  4. **Check environment**
643
- - Node version: `node -v` (requires **>= 18**)
643
+ - Node version: `node -v` (requires **>= 22**)
644
644
  - Plugin directory exists: `ls ~/.openclaw/extensions/memos-local-openclaw-plugin/package.json`
645
645
  - Dependencies installed: `ls ~/.openclaw/extensions/memos-local-openclaw-plugin/node_modules/@sinclair/typebox`
646
646
  If missing: `cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm install --omit=dev`
@@ -693,7 +693,7 @@ This section is for contributors who want to develop, test, or modify the plugin
693
693
 
694
694
  ### Prerequisites
695
695
 
696
- - **Node.js >= 18** (`node -v`)
696
+ - **Node.js >= 22** (`node -v`)
697
697
  - **npm >= 9** (`npm -v`)
698
698
  - **C++ build tools** (for `better-sqlite3` native module):
699
699
  - macOS: `xcode-select --install`
@@ -701,7 +701,7 @@ This section is for contributors who want to develop, test, or modify the plugin
701
701
  - Windows: usually not needed (prebuilt binaries available for LTS Node.js); if build fails, install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
702
702
  - **OpenClaw CLI** installed and available in PATH (`openclaw --version`)
703
703
 
704
- > **`better-sqlite3` build issues?** This is the most common installation problem on macOS and Linux. If `npm install` fails, first install the C++ build tools above, then run `npm rebuild better-sqlite3`. For detailed platform-specific solutions, see the [official better-sqlite3 troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) and our [installation troubleshooting page](https://memtensor.github.io/MemOS/apps/memos-local-openclaw/docs/troubleshooting.html).
704
+ > **`better-sqlite3` build issues?** This is the most common installation problem on macOS and Linux. If `npm install` fails, first install the C++ build tools above, then run `npm rebuild better-sqlite3`. For detailed platform-specific solutions, see the [official better-sqlite3 troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) and our [installation troubleshooting page](https://memos-claw.openmem.net/docs/troubleshooting.html).
705
705
 
706
706
  ### Clone & Setup
707
707
 
@@ -761,8 +761,7 @@ apps/memos-local-openclaw/
761
761
  | `node_modules/` | npm dependencies | `npm install` |
762
762
  | `dist/` | Compiled JavaScript output | `npm run build` |
763
763
  | `package-lock.json` | Dependency lock file | `npm install` (auto-generated) |
764
- | `www/` | Memory Viewer static site (local preview) | Started automatically by the plugin |
765
- | `docs/` | Documentation HTML pages | Built from source or viewed at the hosted URL |
764
+ | `www/` | Memory Viewer static site & documentation pages | Started automatically by the plugin |
766
765
  | `ppt/` | Presentation files (internal use) | Not needed for development |
767
766
  | `.env` | Local environment variables | Copy from `.env.example` |
768
767
 
package/index.ts CHANGED
@@ -389,6 +389,8 @@ const memosLocalPlugin = {
389
389
  let currentAgentId = "main";
390
390
  const getCurrentOwner = () => `agent:${currentAgentId}`;
391
391
 
392
+ const globalRef = globalThis as any;
393
+
392
394
  // ─── Check allowPromptInjection policy ───
393
395
  // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
394
396
  // will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
@@ -2382,8 +2384,45 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2382
2384
 
2383
2385
  let serviceStarted = false;
2384
2386
 
2385
- const startServiceCore = async () => {
2387
+ function isGatewayStartCommand(): boolean {
2388
+ const args = process.argv.map(a => String(a || "").toLowerCase());
2389
+ const gIdx = args.lastIndexOf("gateway");
2390
+ if (gIdx === -1) return false;
2391
+ const next = args[gIdx + 1];
2392
+ return !next || next.startsWith("-") || next === "start" || next === "restart";
2393
+ }
2394
+
2395
+ const startServiceCore = async (isHostStart = false) => {
2396
+ if (!isGatewayStartCommand()) {
2397
+ api.logger.info("memos-local: not a gateway start command, skipping service startup.");
2398
+ return;
2399
+ }
2400
+
2401
+ if (globalRef.__memosLocalPluginStopPromise) {
2402
+ await globalRef.__memosLocalPluginStopPromise;
2403
+ globalRef.__memosLocalPluginStopPromise = undefined;
2404
+ }
2386
2405
  if (serviceStarted) return;
2406
+
2407
+ if (!isHostStart) {
2408
+ if (globalRef.__memosLocalPluginActiveService && globalRef.__memosLocalPluginActiveService !== service) {
2409
+ api.logger.info("memos-local: aborting startServiceCore because a newer plugin instance is active.");
2410
+ return;
2411
+ }
2412
+ }
2413
+
2414
+ if (globalRef.__memosLocalPluginActiveService && globalRef.__memosLocalPluginActiveService !== service) {
2415
+ api.logger.info("memos-local: Stopping previous plugin instance due to start of new instance...");
2416
+ const oldService = globalRef.__memosLocalPluginActiveService;
2417
+ globalRef.__memosLocalPluginActiveService = undefined;
2418
+ try {
2419
+ await oldService.stop({ preserveDb: true });
2420
+ } catch (e: any) {
2421
+ api.logger.warn(`memos-local: Error stopping previous instance: ${e}`);
2422
+ }
2423
+ }
2424
+
2425
+ globalRef.__memosLocalPluginActiveService = service;
2387
2426
  serviceStarted = true;
2388
2427
 
2389
2428
  if (hubServer) {
@@ -2425,33 +2464,45 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2425
2464
  );
2426
2465
  };
2427
2466
 
2428
- api.registerService({
2467
+ const service = {
2429
2468
  id: "memos-local-openclaw-plugin",
2430
- start: async () => { await startServiceCore(); },
2431
- stop: async () => {
2469
+ start: async () => { await startServiceCore(true); },
2470
+ stop: async (options?: { preserveDb?: boolean }) => {
2471
+ await viewer.stop();
2472
+ if (globalRef.__memosLocalPluginActiveService === service) {
2473
+ globalRef.__memosLocalPluginActiveService = undefined;
2474
+ }
2432
2475
  await worker.flush();
2433
2476
  await telemetry.shutdown();
2434
2477
  await hubServer?.stop();
2435
- viewer.stop();
2436
- store.close();
2478
+
2479
+ // If we are hot-reloading, the new instance is already using the SAME
2480
+ // database file on disk. Closing the sqlite store here might kill the
2481
+ // connection for the new instance or cause locking issues depending on
2482
+ // how the native binding manages handles.
2483
+ if (!options?.preserveDb) {
2484
+ store.close();
2485
+ }
2437
2486
  api.logger.info("memos-local: stopped");
2438
2487
  },
2439
- });
2488
+ };
2489
+
2490
+ api.registerService(service);
2440
2491
 
2441
2492
  // Fallback: OpenClaw may load this plugin via deferred reload after
2442
2493
  // startPluginServices has already run, so service.start() never fires.
2443
- // Start on the next tick instead of waiting several seconds; the
2444
- // serviceStarted guard still prevents duplicate startup if the host calls
2445
- // service.start() immediately after registration.
2446
- const SELF_START_DELAY_MS = 0;
2447
- setTimeout(() => {
2448
- if (!serviceStarted) {
2494
+ // Start on a delay instead of next tick so the host has time to call
2495
+ // service.start() during normal startup if this is a fresh launch.
2496
+ const SELF_START_DELAY_MS = 2000;
2497
+ const selfStartTimer = setTimeout(() => {
2498
+ if (!serviceStarted && isGatewayStartCommand()) {
2449
2499
  api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
2450
2500
  startServiceCore().catch((err) => {
2451
2501
  api.logger.warn(`memos-local: self-start failed: ${err}`);
2452
2502
  });
2453
2503
  }
2454
2504
  }, SELF_START_DELAY_MS);
2505
+ if (selfStartTimer.unref) selfStartTimer.unref();
2455
2506
  },
2456
2507
  };
2457
2508
 
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "requirements": {
23
- "node": ">=18.0.0",
23
+ "node": ">=22.0.0",
24
24
  "openclaw": ">=2026.2.0"
25
25
  },
26
26
  "setup": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.9-beta.1",
3
+ "version": "1.0.9-beta.2",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -45,7 +45,7 @@
45
45
  ],
46
46
  "license": "MIT",
47
47
  "engines": {
48
- "node": ">=18.0.0 <25.0.0"
48
+ "node": ">=22.0.0"
49
49
  },
50
50
  "dependencies": {
51
51
  "@huggingface/transformers": "^3.8.0",
@@ -121,12 +121,12 @@ export async function generateTaskTitleAnthropic(
121
121
  headers,
122
122
  body: JSON.stringify({
123
123
  model,
124
- max_tokens: 100,
124
+ max_tokens: 2000,
125
125
  temperature: 0,
126
126
  system: TASK_TITLE_PROMPT,
127
127
  messages: [{ role: "user", content: text }],
128
128
  }),
129
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
129
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
130
130
  });
131
131
 
132
132
  if (!resp.ok) {
@@ -189,12 +189,12 @@ export async function judgeNewTopicAnthropic(
189
189
  headers,
190
190
  body: JSON.stringify({
191
191
  model,
192
- max_tokens: 10,
192
+ max_tokens: 2000,
193
193
  temperature: 0,
194
194
  system: TOPIC_JUDGE_PROMPT,
195
195
  messages: [{ role: "user", content: userContent }],
196
196
  }),
197
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
197
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
198
198
  });
199
199
 
200
200
  if (!resp.ok) {
@@ -223,10 +223,12 @@ RULES:
223
223
 
224
224
  OUTPUT — JSON only:
225
225
  {"relevant":[1,3],"sufficient":true}
226
- - "relevant": candidate numbers whose content helps answer the query. [] if none can help.
227
- - "sufficient": true only if the selected memories fully answer the query.`;
226
+ - "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
227
+ - "sufficient": true only if the selected memories fully answer the query.
228
228
 
229
- import type { FilterResult } from "./openai";
229
+ IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
230
+
231
+ import { parseFilterResult, type FilterResult } from "./openai";
230
232
  export type { FilterResult } from "./openai";
231
233
 
232
234
  export async function filterRelevantAnthropic(
@@ -256,12 +258,12 @@ export async function filterRelevantAnthropic(
256
258
  headers,
257
259
  body: JSON.stringify({
258
260
  model,
259
- max_tokens: 200,
261
+ max_tokens: 2000,
260
262
  temperature: 0,
261
263
  system: FILTER_RELEVANT_PROMPT,
262
264
  messages: [{ role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }],
263
265
  }),
264
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
266
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
265
267
  });
266
268
 
267
269
  if (!resp.ok) {
@@ -275,23 +277,6 @@ export async function filterRelevantAnthropic(
275
277
  return parseFilterResult(raw, log);
276
278
  }
277
279
 
278
- function parseFilterResult(raw: string, log: Logger): FilterResult {
279
- try {
280
- const match = raw.match(/\{[\s\S]*\}/);
281
- if (match) {
282
- const obj = JSON.parse(match[0]);
283
- if (obj && Array.isArray(obj.relevant)) {
284
- return {
285
- relevant: obj.relevant.filter((n: any) => typeof n === "number"),
286
- sufficient: obj.sufficient === true,
287
- };
288
- }
289
- }
290
- } catch {}
291
- log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
292
- return { relevant: [], sufficient: false };
293
- }
294
-
295
280
  export async function summarizeAnthropic(
296
281
  text: string,
297
282
  cfg: SummarizerConfig,
@@ -311,7 +296,7 @@ export async function summarizeAnthropic(
311
296
  headers,
312
297
  body: JSON.stringify({
313
298
  model,
314
- max_tokens: 100,
299
+ max_tokens: 2000,
315
300
  temperature: cfg.temperature ?? 0,
316
301
  system: SYSTEM_PROMPT,
317
302
  messages: [{ role: "user", content: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }],
@@ -358,12 +343,12 @@ export async function judgeDedupAnthropic(
358
343
  headers,
359
344
  body: JSON.stringify({
360
345
  model,
361
- max_tokens: 300,
346
+ max_tokens: 2000,
362
347
  temperature: 0,
363
348
  system: DEDUP_JUDGE_PROMPT,
364
349
  messages: [{ role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }],
365
350
  }),
366
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
351
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
367
352
  });
368
353
 
369
354
  if (!resp.ok) {
@@ -126,9 +126,9 @@ export async function generateTaskTitleBedrock(
126
126
  body: JSON.stringify({
127
127
  system: [{ text: TASK_TITLE_PROMPT }],
128
128
  messages: [{ role: "user", content: [{ text }] }],
129
- inferenceConfig: { temperature: 0, maxTokens: 100 },
129
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
130
130
  }),
131
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
131
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
132
132
  });
133
133
 
134
134
  if (!resp.ok) {
@@ -195,9 +195,9 @@ export async function judgeNewTopicBedrock(
195
195
  body: JSON.stringify({
196
196
  system: [{ text: TOPIC_JUDGE_PROMPT }],
197
197
  messages: [{ role: "user", content: [{ text: userContent }] }],
198
- inferenceConfig: { temperature: 0, maxTokens: 10 },
198
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
199
199
  }),
200
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
200
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
201
201
  });
202
202
 
203
203
  if (!resp.ok) {
@@ -226,10 +226,12 @@ RULES:
226
226
 
227
227
  OUTPUT — JSON only:
228
228
  {"relevant":[1,3],"sufficient":true}
229
- - "relevant": candidate numbers whose content helps answer the query. [] if none can help.
230
- - "sufficient": true only if the selected memories fully answer the query.`;
229
+ - "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
230
+ - "sufficient": true only if the selected memories fully answer the query.
231
231
 
232
- import type { FilterResult } from "./openai";
232
+ IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
233
+
234
+ import { parseFilterResult, type FilterResult } from "./openai";
233
235
  export type { FilterResult } from "./openai";
234
236
 
235
237
  export async function filterRelevantBedrock(
@@ -263,9 +265,9 @@ export async function filterRelevantBedrock(
263
265
  body: JSON.stringify({
264
266
  system: [{ text: FILTER_RELEVANT_PROMPT }],
265
267
  messages: [{ role: "user", content: [{ text: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }] }],
266
- inferenceConfig: { temperature: 0, maxTokens: 200 },
268
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
267
269
  }),
268
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
270
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
269
271
  });
270
272
 
271
273
  if (!resp.ok) {
@@ -279,23 +281,6 @@ export async function filterRelevantBedrock(
279
281
  return parseFilterResult(raw, log);
280
282
  }
281
283
 
282
- function parseFilterResult(raw: string, log: Logger): FilterResult {
283
- try {
284
- const match = raw.match(/\{[\s\S]*\}/);
285
- if (match) {
286
- const obj = JSON.parse(match[0]);
287
- if (obj && Array.isArray(obj.relevant)) {
288
- return {
289
- relevant: obj.relevant.filter((n: any) => typeof n === "number"),
290
- sufficient: obj.sufficient === true,
291
- };
292
- }
293
- }
294
- } catch {}
295
- log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
296
- return { relevant: [], sufficient: false };
297
- }
298
-
299
284
  export async function summarizeBedrock(
300
285
  text: string,
301
286
  cfg: SummarizerConfig,
@@ -321,7 +306,7 @@ export async function summarizeBedrock(
321
306
  messages: [{ role: "user", content: [{ text: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }] }],
322
307
  inferenceConfig: {
323
308
  temperature: cfg.temperature ?? 0,
324
- maxTokens: 100,
309
+ maxTokens: 2000,
325
310
  },
326
311
  }),
327
312
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
@@ -364,9 +349,9 @@ export async function judgeDedupBedrock(
364
349
  body: JSON.stringify({
365
350
  system: [{ text: DEDUP_JUDGE_PROMPT }],
366
351
  messages: [{ role: "user", content: [{ text: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }] }],
367
- inferenceConfig: { temperature: 0, maxTokens: 300 },
352
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
368
353
  }),
369
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
354
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
370
355
  });
371
356
 
372
357
  if (!resp.ok) {
@@ -124,9 +124,9 @@ export async function generateTaskTitleGemini(
124
124
  body: JSON.stringify({
125
125
  systemInstruction: { parts: [{ text: TASK_TITLE_PROMPT }] },
126
126
  contents: [{ parts: [{ text }] }],
127
- generationConfig: { temperature: 0, maxOutputTokens: 100 },
127
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
128
128
  }),
129
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
129
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
130
130
  });
131
131
 
132
132
  if (!resp.ok) {
@@ -192,9 +192,9 @@ export async function judgeNewTopicGemini(
192
192
  body: JSON.stringify({
193
193
  systemInstruction: { parts: [{ text: TOPIC_JUDGE_PROMPT }] },
194
194
  contents: [{ parts: [{ text: userContent }] }],
195
- generationConfig: { temperature: 0, maxOutputTokens: 10 },
195
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
196
196
  }),
197
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
197
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
198
198
  });
199
199
 
200
200
  if (!resp.ok) {
@@ -223,10 +223,12 @@ RULES:
223
223
 
224
224
  OUTPUT — JSON only:
225
225
  {"relevant":[1,3],"sufficient":true}
226
- - "relevant": candidate numbers whose content helps answer the query. [] if none can help.
227
- - "sufficient": true only if the selected memories fully answer the query.`;
226
+ - "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
227
+ - "sufficient": true only if the selected memories fully answer the query.
228
228
 
229
- import type { FilterResult } from "./openai";
229
+ IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
230
+
231
+ import { parseFilterResult, type FilterResult } from "./openai";
230
232
  export type { FilterResult } from "./openai";
231
233
 
232
234
  export async function filterRelevantGemini(
@@ -259,9 +261,9 @@ export async function filterRelevantGemini(
259
261
  body: JSON.stringify({
260
262
  systemInstruction: { parts: [{ text: FILTER_RELEVANT_PROMPT }] },
261
263
  contents: [{ parts: [{ text: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }] }],
262
- generationConfig: { temperature: 0, maxOutputTokens: 200 },
264
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
263
265
  }),
264
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
266
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
265
267
  });
266
268
 
267
269
  if (!resp.ok) {
@@ -275,23 +277,6 @@ export async function filterRelevantGemini(
275
277
  return parseFilterResult(raw, log);
276
278
  }
277
279
 
278
- function parseFilterResult(raw: string, log: Logger): FilterResult {
279
- try {
280
- const match = raw.match(/\{[\s\S]*\}/);
281
- if (match) {
282
- const obj = JSON.parse(match[0]);
283
- if (obj && Array.isArray(obj.relevant)) {
284
- return {
285
- relevant: obj.relevant.filter((n: any) => typeof n === "number"),
286
- sufficient: obj.sufficient === true,
287
- };
288
- }
289
- }
290
- } catch {}
291
- log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
292
- return { relevant: [], sufficient: false };
293
- }
294
-
295
280
  export async function summarizeGemini(
296
281
  text: string,
297
282
  cfg: SummarizerConfig,
@@ -314,7 +299,7 @@ export async function summarizeGemini(
314
299
  body: JSON.stringify({
315
300
  systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
316
301
  contents: [{ parts: [{ text: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }] }],
317
- generationConfig: { temperature: cfg.temperature ?? 0, maxOutputTokens: 100 },
302
+ generationConfig: { temperature: cfg.temperature ?? 0, maxOutputTokens: 2000 },
318
303
  }),
319
304
  signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
320
305
  });
@@ -355,9 +340,9 @@ export async function judgeDedupGemini(
355
340
  body: JSON.stringify({
356
341
  systemInstruction: { parts: [{ text: DEDUP_JUDGE_PROMPT }] },
357
342
  contents: [{ parts: [{ text: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }] }],
358
- generationConfig: { temperature: 0, maxOutputTokens: 300 },
343
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
359
344
  }),
360
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
345
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
361
346
  });
362
347
 
363
348
  if (!resp.ok) {
@@ -230,8 +230,13 @@ export class Summarizer {
230
230
  return ruleFallback(cleaned);
231
231
  }
232
232
 
233
- const accept = (s: string | undefined): s is string =>
234
- !!s && s.length > 0 && s.length < cleaned.length;
233
+ const accept = (s: string | undefined): s is string => {
234
+ if (!s || s.length === 0) return false;
235
+ // Allow the LLM result if it's shorter than input, or if the input is quite short itself (<= 20 chars).
236
+ // Summarizing a very short text (e.g. "hi there") might yield a slightly longer formal summary (e.g. "User said hi").
237
+ if (cleaned.length <= 20) return true;
238
+ return s.length < cleaned.length;
239
+ };
235
240
 
236
241
  let llmCalled = false;
237
242
  try {
@@ -122,13 +122,13 @@ export async function generateTaskTitleOpenAI(
122
122
  body: JSON.stringify(buildRequestBody(cfg, {
123
123
  model,
124
124
  temperature: 0,
125
- max_tokens: 100,
125
+ max_tokens: 1000,
126
126
  messages: [
127
127
  { role: "system", content: TASK_TITLE_PROMPT },
128
128
  { role: "user", content: text },
129
129
  ],
130
130
  })),
131
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
131
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
132
132
  });
133
133
 
134
134
  if (!resp.ok) {
@@ -233,13 +233,13 @@ export async function judgeNewTopicOpenAI(
233
233
  body: JSON.stringify(buildRequestBody(cfg, {
234
234
  model,
235
235
  temperature: 0,
236
- max_tokens: 10,
236
+ max_tokens: 1000,
237
237
  messages: [
238
238
  { role: "system", content: TOPIC_JUDGE_PROMPT },
239
239
  { role: "user", content: userContent },
240
240
  ],
241
241
  })),
242
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
242
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
243
243
  });
244
244
 
245
245
  if (!resp.ok) {
@@ -289,13 +289,13 @@ export async function classifyTopicOpenAI(
289
289
  body: JSON.stringify(buildRequestBody(cfg, {
290
290
  model,
291
291
  temperature: 0,
292
- max_tokens: 60,
292
+ max_tokens: 1000,
293
293
  messages: [
294
294
  { role: "system", content: TOPIC_CLASSIFIER_PROMPT },
295
295
  { role: "user", content: userContent },
296
296
  ],
297
297
  })),
298
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
298
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
299
299
  });
300
300
 
301
301
  if (!resp.ok) {
@@ -336,13 +336,13 @@ export async function arbitrateTopicSplitOpenAI(
336
336
  body: JSON.stringify(buildRequestBody(cfg, {
337
337
  model,
338
338
  temperature: 0,
339
- max_tokens: 10,
339
+ max_tokens: 1000,
340
340
  messages: [
341
341
  { role: "system", content: TOPIC_ARBITRATION_PROMPT },
342
342
  { role: "user", content: userContent },
343
343
  ],
344
344
  })),
345
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
345
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
346
346
  });
347
347
 
348
348
  if (!resp.ok) {
@@ -432,13 +432,13 @@ export async function filterRelevantOpenAI(
432
432
  body: JSON.stringify(buildRequestBody(cfg, {
433
433
  model,
434
434
  temperature: 0,
435
- max_tokens: 200,
435
+ max_tokens: 2000,
436
436
  messages: [
437
437
  { role: "system", content: FILTER_RELEVANT_PROMPT },
438
438
  { role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` },
439
439
  ],
440
440
  })),
441
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
441
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
442
442
  });
443
443
 
444
444
  if (!resp.ok) {
@@ -453,8 +453,13 @@ export async function filterRelevantOpenAI(
453
453
  }
454
454
 
455
455
  export function parseFilterResult(raw: string, log: Logger): FilterResult {
456
+ let cleaned = raw.replace(/<think>[\s\S]*?<\/think>/gi, "");
457
+ cleaned = cleaned.replace(/think>[\s\S]*?<\/think>/gi, "");
458
+ // Remove markdown code block markers if present (e.g. ```json ... ```)
459
+ cleaned = cleaned.replace(/```json\s*/gi, "").replace(/```\s*/gi, "");
460
+
456
461
  try {
457
- const match = raw.match(/\{[\s\S]*\}/);
462
+ const match = cleaned.match(/\{[\s\S]*\}/);
458
463
  if (match) {
459
464
  const obj = JSON.parse(match[0]);
460
465
  if (obj && Array.isArray(obj.relevant)) {
@@ -465,6 +470,35 @@ export function parseFilterResult(raw: string, log: Logger): FilterResult {
465
470
  }
466
471
  }
467
472
  } catch {}
473
+
474
+ try {
475
+ // 尝试匹配可能被截断的 JSON 数组结构:`{"relevant":[` 或 `{"relevant": [1, 2`
476
+ const truncatedMatch = cleaned.match(/\{\s*"relevant"\s*:\s*\[([^\]]*)/);
477
+ if (truncatedMatch) {
478
+ const numbers = truncatedMatch[1]
479
+ .split(",")
480
+ .map(s => parseInt(s.trim(), 10))
481
+ .filter(n => !isNaN(n));
482
+
483
+ // 如果至少成功解析出一个数字,且原字符串确实无法通过正常 JSON 解析,就当做截断处理
484
+ if (numbers.length > 0) {
485
+ log.warn(`filterRelevant: JSON truncated, extracted from partial array: ${numbers.join(", ")}`);
486
+ return { relevant: numbers, sufficient: cleaned.includes('"sufficient":true') || cleaned.includes('"sufficient": true') };
487
+ } else if (cleaned.includes('"relevant":[]') || cleaned.includes('"relevant": []')) {
488
+ return { relevant: [], sufficient: cleaned.includes('"sufficient":true') || cleaned.includes('"sufficient": true') };
489
+ }
490
+ }
491
+ } catch {}
492
+
493
+ // Regex fallback for reasoning models that fail to output JSON after thinking
494
+ const candidatesMatch = raw.match(/(?:candidate|候选)[\s::]*(\d+)/gi);
495
+ if (candidatesMatch && candidatesMatch.length > 0) {
496
+ const extracted = candidatesMatch.map(m => parseInt(m.replace(/\D/g, ""), 10));
497
+ const unique = Array.from(new Set(extracted)).filter(n => !isNaN(n));
498
+ log.warn(`filterRelevant: JSON missing, fallback regex extracted: ${unique.join(", ")}`);
499
+ return { relevant: unique, sufficient: false };
500
+ }
501
+
468
502
  log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
469
503
  return { relevant: [], sufficient: false };
470
504
  }
@@ -524,13 +558,13 @@ export async function judgeDedupOpenAI(
524
558
  body: JSON.stringify(buildRequestBody(cfg, {
525
559
  model,
526
560
  temperature: 0,
527
- max_tokens: 300,
561
+ max_tokens: 2000,
528
562
  messages: [
529
563
  { role: "system", content: DEDUP_JUDGE_PROMPT },
530
564
  { role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` },
531
565
  ],
532
566
  })),
533
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
567
+ signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
534
568
  });
535
569
 
536
570
  if (!resp.ok) {
@@ -544,8 +578,13 @@ export async function judgeDedupOpenAI(
544
578
  }
545
579
 
546
580
  export function parseDedupResult(raw: string, log: Logger): DedupResult {
581
+ let cleaned = raw.replace(/<think>[\s\S]*?<\/think>/gi, "");
582
+ cleaned = cleaned.replace(/think>[\s\S]*?<\/think>/gi, "");
583
+ // Remove markdown code block markers if present
584
+ cleaned = cleaned.replace(/```json\s*/gi, "").replace(/```\s*/gi, "");
585
+
547
586
  try {
548
- const match = raw.match(/\{[\s\S]*\}/);
587
+ const match = cleaned.match(/\{[\s\S]*\}/);
549
588
  if (match) {
550
589
  const obj = JSON.parse(match[0]);
551
590
  if (obj && typeof obj.action === "string") {
@@ -558,6 +597,24 @@ export function parseDedupResult(raw: string, log: Logger): DedupResult {
558
597
  }
559
598
  }
560
599
  } catch {}
600
+
601
+ try {
602
+ // 处理可能的 JSON 截断:如 `{"action":"DUPLICATE","targetIndex":2`
603
+ const actionMatch = cleaned.match(/"action"\s*:\s*"([^"]+)"/);
604
+ if (actionMatch) {
605
+ const actionStr = actionMatch[1];
606
+ if (actionStr === "DUPLICATE" || actionStr === "UPDATE" || actionStr === "NEW") {
607
+ let targetIndex: number | undefined;
608
+ const targetMatch = cleaned.match(/"targetIndex"\s*:\s*(\d+)/);
609
+ if (targetMatch) {
610
+ targetIndex = parseInt(targetMatch[1], 10);
611
+ }
612
+ log.warn(`judgeDedup: JSON truncated, regex extracted action=${actionStr}, targetIndex=${targetIndex}`);
613
+ return { action: actionStr, targetIndex, reason: "extracted from truncated JSON" };
614
+ }
615
+ }
616
+ } catch {}
617
+
561
618
  log.warn(`judgeDedup: failed to parse LLM output: "${raw}", fallback to NEW`);
562
619
  return { action: "NEW", reason: "parse_failed" };
563
620
  }
@@ -124,7 +124,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
124
124
  .topbar-center{flex:1;display:flex;justify-content:center}
125
125
  .topbar .actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
126
126
 
127
- .main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}
127
+ .main-content{display:grid;grid-template-columns:260px 1fr;grid-template-rows:auto 1fr;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}
128
128
 
129
129
  /* ─── Sidebar ─── */
130
130
  .sidebar{width:260px;min-width:260px;flex-shrink:0;position:sticky;top:84px;max-height:calc(100vh - 112px);display:flex;flex-direction:column}
@@ -1500,6 +1500,16 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1500
1500
  </div>
1501
1501
  </div>
1502
1502
  <div class="settings-card-body">
1503
+ <div class="emb-banner warning" id="settingsWarningBanner" style="margin: 0 0 24px 0; display: block; border-left: 4px solid #f59e0b; position: relative;">
1504
+ <button onclick="document.getElementById('settingsWarningBanner').style.display='none'" style="position: absolute; right: 12px; top: 12px; background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 16px; opacity: 0.7; transition: opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7" title="关闭" aria-label="关闭">\u2715</button>
1505
+ <div style="font-weight: 700; margin-bottom: 8px; font-size: 14px; color: #d97706;" data-i18n-html="settings.warn.title">\u{1F514} 模型配置重要提醒</div>
1506
+ <ul style="margin: 0; padding-left: 20px; line-height: 1.7; color: var(--text-sec); font-size: 13px;">
1507
+ <li data-i18n-html="settings.warn.emb"><strong>嵌入模型 (Embedding):</strong>插件内置模型规模较小。为获得更精准的记忆检索体验,强烈建议配置 <code>bge-m3</code> 等专业嵌入模型。</li>
1508
+ <li data-i18n-html="settings.warn.sum"><strong>摘要模型 (Summarizer):</strong>此项为<strong>必填项</strong>,否则无法自动提取记忆摘要。建议配置<strong>非思考型</strong>大模型,以保障处理速度和流畅度。</li>
1509
+ <li data-i18n-html="settings.warn.skill"><strong>技能模型 (Skill Evolution):</strong>用于自动提取可复用技能。建议配置<strong>思考型</strong>大模型,以获得最佳的生成效果和稳定性。</li>
1510
+ </ul>
1511
+ </div>
1512
+
1503
1513
  <!-- Embedding Model section -->
1504
1514
  <div class="settings-card-subtitle">\u{1F4E1} <span data-i18n="settings.embedding">Embedding Model</span></div>
1505
1515
  <div class="field-hint" style="margin-bottom:10px" data-i18n="settings.embedding.desc">Vector embedding model for memory search and retrieval</div>
@@ -1609,7 +1619,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1609
1619
  </div>
1610
1620
  <div style="margin-top:14px">
1611
1621
  <div class="settings-card-subtitle" style="margin-bottom:4px" data-i18n="settings.skill.model">Skill Dedicated Model</div>
1612
- <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
1622
+ <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated thinking model here for higher quality skill output.</div>
1613
1623
  <div class="settings-grid">
1614
1624
  <div class="settings-field">
1615
1625
  <label data-i18n="settings.provider">Provider</label>
@@ -2126,6 +2136,7 @@ const I18N={
2126
2136
  'skills.search.placeholder':'Search skills...',
2127
2137
  'skills.search.local':'Local',
2128
2138
  'skills.search.noresult':'No matching skills found',
2139
+
2129
2140
  'skills.load.error':'Failed to load skills',
2130
2141
  'skills.hub.title':'\u{1F310} Team Skills',
2131
2142
  'skills.hub.empty':'No extra team skills to list here — either the hub has none yet, or every hub skill already appears in your local list above (same source skill).',
@@ -2273,7 +2284,10 @@ const I18N={
2273
2284
  'confirm.clearall2':'Are you absolutely sure?',
2274
2285
  'embed.on':'Embedding: ',
2275
2286
  'embed.off':'No embedding model',
2276
- 'embed.warn.local':'Using built-in mini model (384d). Search quality is limited configure an embedding model in Settings for best results.',
2287
+ 'embed.warn.local':'<strong>Embedding</strong>: Using built-in mini model (384d). Search quality is limited. It is highly recommended to configure a dedicated Embedding model (like bge-m3) in Settings for best results.',
2288
+ 'fallback.banner.sum': '<strong>Summarizer</strong>: Summarizer model is not configured, automatic memory summarization is paused. A fast, non-reasoning model is recommended for best performance.',
2289
+ 'fallback.banner.skill': '<strong>Skill Evolution</strong>: Skill Evolution model is not configured. A reasoning/thinking model is recommended for best stability and generation quality.',
2290
+ 'fallback.banner.goto': 'Configure models',
2277
2291
  'embed.err.fail':'Embedding model error detected. Check Settings → Model Health.',
2278
2292
  'embed.banner.goto':'Go to Settings',
2279
2293
  'lang.switch':'中',
@@ -2298,6 +2312,10 @@ const I18N={
2298
2312
  'tab.settings':'\u2699 Settings',
2299
2313
  'settings.modelconfig':'Model Configuration',
2300
2314
  'settings.models':'AI Models',
2315
+ 'settings.warn.title':'\u{1F514} Model Configuration Important Reminder',
2316
+ 'settings.warn.emb':'<strong>Embedding Model:</strong> The built-in model is small. For a more accurate memory retrieval experience, it is highly recommended to configure a professional embedding model such as <code>bge-m3</code>.',
2317
+ 'settings.warn.sum':'<strong>Summarizer Model:</strong> This is <strong>required</strong>, otherwise automatic memory summarization will fail. A fast, non-reasoning model is recommended for best performance.',
2318
+ 'settings.warn.skill':'<strong>Skill Evolution:</strong> Used to automatically extract reusable skills. A reasoning/thinking model is recommended for best stability and generation quality.',
2301
2319
  'settings.models.desc':'Configure embedding, summarizer and skill evolution models',
2302
2320
  'settings.modelhealth':'Model Health',
2303
2321
  'settings.embedding':'Embedding Model',
@@ -2321,7 +2339,7 @@ const I18N={
2321
2339
  'settings.skill.confidence':'Min Confidence',
2322
2340
  'settings.skill.minchunks':'Min Chunks',
2323
2341
  'settings.skill.model':'Skill Dedicated Model',
2324
- 'settings.skill.model.hint':'Leave empty to reuse the Summarizer model. Set a dedicated one for higher quality.',
2342
+ 'settings.skill.model.hint':'Leave empty to reuse the Summarizer model. Set a dedicated thinking model for higher quality.',
2325
2343
  'settings.optional':'Optional',
2326
2344
  'settings.skill.usemain':'Use Main Summarizer',
2327
2345
  'settings.telemetry':'Telemetry',
@@ -2899,6 +2917,7 @@ const I18N={
2899
2917
  'skills.search.placeholder':'搜索技能...',
2900
2918
  'skills.search.local':'本地',
2901
2919
  'skills.search.noresult':'未找到匹配的技能',
2920
+
2902
2921
  'skills.load.error':'加载技能失败',
2903
2922
  'skills.hub.title':'\u{1F310} 团队共享技能',
2904
2923
  'skills.hub.empty':'下方只列出「Hub 上有、但上方本机列表尚未包含」的技能;若 Hub 条目已与本机同源同步,则只会在上方显示,此处为空属正常。',
@@ -3046,7 +3065,10 @@ const I18N={
3046
3065
  'confirm.clearall2':'你真的确定吗?',
3047
3066
  'embed.on':'嵌入模型:',
3048
3067
  'embed.off':'无嵌入模型',
3049
- 'embed.warn.local':'当前使用内置迷你模型(384维),搜索效果有限。强烈建议在「设置」中配置专用 Embedding 模型以获得最佳效果。',
3068
+ 'embed.warn.local':'<strong>嵌入模型 (Embedding)</strong>:当前使用内置迷你模型(384维),搜索效果有限。强烈建议在「设置」中配置专用 Embedding 模型(如bge-m3等模型)以获得最佳效果。',
3069
+ 'fallback.banner.sum': '<strong>摘要模型 (Summarizer)</strong>:摘要模型未配置,无法自动提取记忆摘要。建议配置非思考型大模型,以保障处理速度和流畅度。',
3070
+ 'fallback.banner.skill': '<strong>技能模型 (Skill Evolution)</strong>:技能模型未配置,建议配置思考型大模型,以获得最佳的生成效果和稳定性。',
3071
+ 'fallback.banner.goto': '前往配置',
3050
3072
  'embed.err.fail':'Embedding 模型调用异常,请前往「设置 → 模型健康」检查。',
3051
3073
  'embed.banner.goto':'前往设置',
3052
3074
  'lang.switch':'EN',
@@ -3071,6 +3093,10 @@ const I18N={
3071
3093
  'tab.settings':'\u2699 设置',
3072
3094
  'settings.modelconfig':'模型配置',
3073
3095
  'settings.models':'AI 模型',
3096
+ 'settings.warn.title':'\u{1F514} 模型配置重要提醒',
3097
+ 'settings.warn.emb':'<strong>嵌入模型 (Embedding):</strong>插件内置模型规模较小。为获得更精准的记忆检索体验,强烈建议配置 <code>bge-m3</code> 等专业嵌入模型。',
3098
+ 'settings.warn.sum':'<strong>摘要模型 (Summarizer):</strong>此项为<strong>必填项</strong>,否则无法自动提取记忆摘要。建议配置<strong>非思考型</strong>大模型,以保障处理速度和流畅度。',
3099
+ 'settings.warn.skill':'<strong>技能模型 (Skill Evolution):</strong>用于自动提取可复用技能。建议配置<strong>思考型</strong>大模型,以获得最佳的生成效果和稳定性。',
3074
3100
  'settings.models.desc':'配置嵌入模型、摘要模型和技能进化模型',
3075
3101
  'settings.modelhealth':'模型健康',
3076
3102
  'settings.embedding':'嵌入模型',
@@ -3094,7 +3120,7 @@ const I18N={
3094
3120
  'settings.skill.confidence':'最低置信度',
3095
3121
  'settings.skill.minchunks':'最少记忆片段',
3096
3122
  'settings.skill.model':'技能专用模型',
3097
- 'settings.skill.model.hint':'不配置则复用摘要模型。如需更高质量可单独指定。',
3123
+ 'settings.skill.model.hint':'不配置则复用摘要模型。如需更高质量可单独指定思考模型。',
3098
3124
  'settings.optional':'可选',
3099
3125
  'settings.skill.usemain':'使用主摘要模型',
3100
3126
  'settings.telemetry':'数据统计',
@@ -3640,6 +3666,10 @@ function applyI18n(){
3640
3666
  const key=el.getAttribute('data-i18n');
3641
3667
  if(key) el.textContent=t(key);
3642
3668
  });
3669
+ document.querySelectorAll('[data-i18n-html]').forEach(el=>{
3670
+ const key=el.getAttribute('data-i18n-html');
3671
+ if(key) el.innerHTML=t(key);
3672
+ });
3643
3673
  document.querySelectorAll('[data-i18n-ph]').forEach(el=>{
3644
3674
  const key=el.getAttribute('data-i18n-ph');
3645
3675
  if(key) el.placeholder=t(key);
@@ -3647,7 +3677,15 @@ function applyI18n(){
3647
3677
  const step2=document.getElementById('resetStep2Desc');
3648
3678
  if(step2) step2.innerHTML=t('reset.step2.desc.pre')+'<span style="font-family:monospace;font-size:12px;color:var(--pri)">password reset token: <strong>a1b2c3d4e5f6...</strong></span>'+t('reset.step2.desc.post');
3649
3679
  document.title=t('title')+' - OpenClaw';
3650
- if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){loadStats();}
3680
+ if(typeof loadStats==='function' && document.getElementById('app').style.display==='flex'){
3681
+ _embeddingWarningShown = false;
3682
+ _fallbackWarningShown = false;
3683
+ _lastStatsFp = '';
3684
+ const embB = document.getElementById('embBanner'); if (embB) embB.remove();
3685
+ const fSumB = document.getElementById('fallbackBannerSum'); if (fSumB) fSumB.remove();
3686
+ const fSkillB = document.getElementById('fallbackBannerSkill'); if (fSkillB) fSkillB.remove();
3687
+ loadStats();
3688
+ }
3651
3689
  if(document.querySelector('.analytics-view.show') && typeof loadMetrics==='function'){loadMetrics();}
3652
3690
  }
3653
3691
 
@@ -8263,6 +8301,7 @@ async function loadAll(){
8263
8301
  }
8264
8302
 
8265
8303
  var _lastStatsFp='';
8304
+ var _fallbackWarningShown=false;
8266
8305
  async function loadStats(ownerFilter){
8267
8306
  let d;
8268
8307
  try{
@@ -8312,6 +8351,19 @@ async function loadStats(ownerFilter){
8312
8351
  }).catch(()=>{});
8313
8352
  }
8314
8353
 
8354
+ if(!_fallbackWarningShown){
8355
+ _fallbackWarningShown=true;
8356
+ var sumWarn = (d.summarizerProvider === 'none') ? t('fallback.banner.sum') : '';
8357
+ var skillWarn = (d.skillEvolutionProvider === 'none') ? t('fallback.banner.skill') : '';
8358
+
8359
+ if (sumWarn) {
8360
+ showFallbackBanner('fallbackBannerSum', '<div>'+sumWarn+'</div>', 'warning');
8361
+ }
8362
+ if (skillWarn) {
8363
+ showFallbackBanner('fallbackBannerSkill', '<div>'+skillWarn+'</div>', 'warning');
8364
+ }
8365
+ }
8366
+
8315
8367
  const memorySessions=d.sessions||[];
8316
8368
  const taskSessions=d.taskSessions||[];
8317
8369
  const skillSessions=d.skillSessions||[];
@@ -9623,7 +9675,22 @@ function showEmbeddingBanner(msg,type){
9623
9675
  var el=document.createElement('div');
9624
9676
  el.id='embBanner';
9625
9677
  el.className=cls;
9626
- el.innerHTML=icon+' <span>'+esc(msg)+'</span>'+btn+close;
9678
+ el.innerHTML=icon+' <span>'+msg+'</span>'+btn+close;
9679
+ var mc=document.querySelector('.main-content');
9680
+ if(mc) mc.parentElement.insertBefore(el,mc);
9681
+ }
9682
+
9683
+ /* ─── Fallback Banner ─── */
9684
+ function showFallbackBanner(id, msgHtml,type){
9685
+ if(document.getElementById(id)) return;
9686
+ var cls=type==='error'?'emb-banner error':'emb-banner warning';
9687
+ var icon=type==='error'?'\\u274C':'\\u26A0\\uFE0F';
9688
+ var btn='<button class="emb-banner-btn" onclick="switchView(\\'settings\\');this.parentElement.remove()" style="margin-left:auto;">'+t('fallback.banner.goto')+'</button>';
9689
+ var close='<button class="emb-banner-close" onclick="this.parentElement.remove()">&times;</button>';
9690
+ var el=document.createElement('div');
9691
+ el.id=id;
9692
+ el.className=cls;
9693
+ el.innerHTML=icon+' <div style="line-height:1.5;flex:1;">'+msgHtml+'</div>'+btn+close;
9627
9694
  var mc=document.querySelector('.main-content');
9628
9695
  if(mc) mc.parentElement.insertBefore(el,mc);
9629
9696
  }
@@ -211,13 +211,24 @@ export class ViewerServer {
211
211
  }
212
212
  }
213
213
 
214
- stop(): void {
215
- this.stopHubHeartbeat();
216
- this.stopNotifPoll();
217
- for (const c of this.notifSSEClients) { try { c.end(); } catch {} }
218
- this.notifSSEClients = [];
219
- this.server?.close();
220
- this.server = null;
214
+ stop(): Promise<void> {
215
+ return new Promise((resolve) => {
216
+ this.stopHubHeartbeat();
217
+ this.stopNotifPoll();
218
+ for (const c of this.notifSSEClients) { try { c.end(); } catch {} }
219
+ this.notifSSEClients = [];
220
+ if (this.server) {
221
+ if ("closeAllConnections" in this.server) {
222
+ (this.server as any).closeAllConnections();
223
+ }
224
+ this.server.close(() => {
225
+ this.server = null;
226
+ resolve();
227
+ });
228
+ } else {
229
+ resolve();
230
+ }
231
+ });
221
232
  }
222
233
 
223
234
  getResetToken(): string {
@@ -904,6 +915,9 @@ export class ViewerServer {
904
915
  totalMemories: total.count, totalSessions: sessions.count, totalEmbeddings: embCount,
905
916
  totalSkills: skillCount, totalTasks: taskCount,
906
917
  embeddingProvider: this.embedder.provider,
918
+ summarizerProvider: this.ctx?.config?.summarizer?.provider ?? "none",
919
+ skillEvolutionProvider: this.ctx?.config?.skillEvolution?.enabled ? (this.ctx?.config?.summarizer?.provider ?? "none") : "none",
920
+ isSummarizerDegraded: this.ctx ? !this.hasUsableSummarizerProvider(this.ctx.config) : false,
907
921
  dedupBreakdown,
908
922
  timeRange: { earliest: timeRange.earliest, latest: timeRange.latest },
909
923
  sessions: sessionList,
@@ -1,5 +0,0 @@
1
- {
2
- "endpoint": "https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/rum/web/v2?workspace=default-cms-1026429231103299-cn-hangzhou&service_id=a3u72ukxmr@066657d42a13a9a9f337f",
3
- "pid": "a3u72ukxmr@066657d42a13a9a9f337f",
4
- "env": "prod"
5
- }