opc-agent 4.1.0 → 4.1.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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/USABILITY-ISSUES.md +73 -0
- package/dist/channels/web.js +8 -2
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +200 -85
- package/dist/core/runtime.js +37 -15
- package/dist/deploy/index.js +56 -56
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +105 -10
- package/dist/memory/deepbrain.d.ts +1 -1
- package/dist/memory/deepbrain.js +95 -4
- package/dist/scheduler/cron-engine.js +3 -36
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/web.ts +8 -2
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +195 -92
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +25 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +98 -11
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/deepbrain.ts +99 -5
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import type { MCPServerConfig } from '../../protocols/mcp/types';
|
|
2
|
-
|
|
3
|
-
export function createWebServer(): MCPServerConfig {
|
|
4
|
-
return {
|
|
5
|
-
name: 'web',
|
|
6
|
-
version: '1.0.0',
|
|
7
|
-
tools: [
|
|
8
|
-
{
|
|
9
|
-
name: 'web_fetch',
|
|
10
|
-
description: 'Fetch a URL and return its content',
|
|
11
|
-
inputSchema: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: { type: 'object' }, body: { type: 'string' } }, required: ['url'] },
|
|
12
|
-
handler: async (args: { url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
|
|
13
|
-
const res = await fetch(args.url, { method: args.method || 'GET', headers: args.headers, body: args.body });
|
|
14
|
-
const contentType = res.headers.get('content-type') || '';
|
|
15
|
-
const text = await res.text();
|
|
16
|
-
return { status: res.status, contentType, body: text.slice(0, 50000), truncated: text.length > 50000 };
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: 'web_extract_text',
|
|
21
|
-
description: 'Fetch a URL and extract readable text (strips HTML tags)',
|
|
22
|
-
inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
|
|
23
|
-
handler: async (args: { url: string }) => {
|
|
24
|
-
const res = await fetch(args.url);
|
|
25
|
-
const html = await res.text();
|
|
26
|
-
const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
27
|
-
return { text: text.slice(0, 30000), truncated: text.length > 30000 };
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: 'web_search',
|
|
32
|
-
description: 'Search the web (simulated — returns search URL for manual use)',
|
|
33
|
-
inputSchema: { type: 'object', properties: { query: { type: 'string' }, engine: { type: 'string', enum: ['google', 'bing', 'duckduckgo'], default: 'duckduckgo' } }, required: ['query'] },
|
|
34
|
-
handler: async (args: { query: string; engine?: string }) => {
|
|
35
|
-
const engines: Record<string, string> = {
|
|
36
|
-
google: `https://www.google.com/search?q=${encodeURIComponent(args.query)}`,
|
|
37
|
-
bing: `https://www.bing.com/search?q=${encodeURIComponent(args.query)}`,
|
|
38
|
-
duckduckgo: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`,
|
|
39
|
-
};
|
|
40
|
-
const url = engines[args.engine || 'duckduckgo'];
|
|
41
|
-
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 opc-mcp/1.0' } });
|
|
42
|
-
const html = await res.text();
|
|
43
|
-
const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
44
|
-
return { query: args.query, engine: args.engine || 'duckduckgo', results: text.slice(0, 20000) };
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
1
|
+
import type { MCPServerConfig } from '../../protocols/mcp/types';
|
|
2
|
+
|
|
3
|
+
export function createWebServer(): MCPServerConfig {
|
|
4
|
+
return {
|
|
5
|
+
name: 'web',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
tools: [
|
|
8
|
+
{
|
|
9
|
+
name: 'web_fetch',
|
|
10
|
+
description: 'Fetch a URL and return its content',
|
|
11
|
+
inputSchema: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: { type: 'object' }, body: { type: 'string' } }, required: ['url'] },
|
|
12
|
+
handler: async (args: { url: string; method?: string; headers?: Record<string, string>; body?: string }) => {
|
|
13
|
+
const res = await fetch(args.url, { method: args.method || 'GET', headers: args.headers, body: args.body });
|
|
14
|
+
const contentType = res.headers.get('content-type') || '';
|
|
15
|
+
const text = await res.text();
|
|
16
|
+
return { status: res.status, contentType, body: text.slice(0, 50000), truncated: text.length > 50000 };
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'web_extract_text',
|
|
21
|
+
description: 'Fetch a URL and extract readable text (strips HTML tags)',
|
|
22
|
+
inputSchema: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
|
|
23
|
+
handler: async (args: { url: string }) => {
|
|
24
|
+
const res = await fetch(args.url);
|
|
25
|
+
const html = await res.text();
|
|
26
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
27
|
+
return { text: text.slice(0, 30000), truncated: text.length > 30000 };
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'web_search',
|
|
32
|
+
description: 'Search the web (simulated — returns search URL for manual use)',
|
|
33
|
+
inputSchema: { type: 'object', properties: { query: { type: 'string' }, engine: { type: 'string', enum: ['google', 'bing', 'duckduckgo'], default: 'duckduckgo' } }, required: ['query'] },
|
|
34
|
+
handler: async (args: { query: string; engine?: string }) => {
|
|
35
|
+
const engines: Record<string, string> = {
|
|
36
|
+
google: `https://www.google.com/search?q=${encodeURIComponent(args.query)}`,
|
|
37
|
+
bing: `https://www.bing.com/search?q=${encodeURIComponent(args.query)}`,
|
|
38
|
+
duckduckgo: `https://html.duckduckgo.com/html/?q=${encodeURIComponent(args.query)}`,
|
|
39
|
+
};
|
|
40
|
+
const url = engines[args.engine || 'duckduckgo'];
|
|
41
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 opc-mcp/1.0' } });
|
|
42
|
+
const html = await res.text();
|
|
43
|
+
const text = html.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
44
|
+
return { query: args.query, engine: args.engine || 'duckduckgo', results: text.slice(0, 20000) };
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
import type { Message } from '../core/types';
|
|
2
|
-
|
|
3
|
-
export interface CompressorConfig {
|
|
4
|
-
maxTokens: number;
|
|
5
|
-
compressThreshold: number;
|
|
6
|
-
preserveRecent: number;
|
|
7
|
-
brain?: any;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface CompressResult {
|
|
11
|
-
messages: Message[];
|
|
12
|
-
learnedCount: number;
|
|
13
|
-
savedTokens: number;
|
|
14
|
-
summary: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const DEFAULT_CONFIG: CompressorConfig = {
|
|
18
|
-
maxTokens: 8000,
|
|
19
|
-
compressThreshold: 0.8,
|
|
20
|
-
preserveRecent: 10,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Context compression with optional DeepBrain memory offloading.
|
|
25
|
-
*/
|
|
26
|
-
export class ContextCompressor {
|
|
27
|
-
private config: CompressorConfig;
|
|
28
|
-
|
|
29
|
-
constructor(config: Partial<CompressorConfig> = {}) {
|
|
30
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Estimate token count using language-aware heuristic.
|
|
35
|
-
* English: ~1 token per 4 chars. Chinese: ~1 token per 2 chars.
|
|
36
|
-
*/
|
|
37
|
-
estimateTokens(text: string): number {
|
|
38
|
-
let tokens = 0;
|
|
39
|
-
for (const char of text) {
|
|
40
|
-
// CJK Unicode range detection
|
|
41
|
-
const code = char.codePointAt(0) ?? 0;
|
|
42
|
-
if (
|
|
43
|
-
(code >= 0x4e00 && code <= 0x9fff) || // CJK Unified
|
|
44
|
-
(code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
|
|
45
|
-
(code >= 0x3000 && code <= 0x303f) // CJK Punctuation
|
|
46
|
-
) {
|
|
47
|
-
tokens += 0.5; // 1 token per 2 chars
|
|
48
|
-
} else {
|
|
49
|
-
tokens += 0.25; // 1 token per 4 chars
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return Math.ceil(tokens);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private estimateMessagesTokens(messages: Message[]): number {
|
|
56
|
-
return messages.reduce((sum, m) => sum + this.estimateTokens(m.content), 0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Extract key insights from messages for brain storage.
|
|
61
|
-
*/
|
|
62
|
-
private extractInsights(messages: Message[]): Array<{ content: string; type: string }> {
|
|
63
|
-
const insights: Array<{ content: string; type: string }> = [];
|
|
64
|
-
for (const msg of messages) {
|
|
65
|
-
const c = msg.content;
|
|
66
|
-
// Decisions
|
|
67
|
-
if (/\b(decided|decision|choose|chose|will use|going with|let's go|确定|决定)\b/i.test(c)) {
|
|
68
|
-
insights.push({ content: c.slice(0, 500), type: 'decision' });
|
|
69
|
-
}
|
|
70
|
-
// Facts / definitions
|
|
71
|
-
else if (/\b(is defined as|means|equals|refers to|是指|定义)\b/i.test(c)) {
|
|
72
|
-
insights.push({ content: c.slice(0, 500), type: 'fact' });
|
|
73
|
-
}
|
|
74
|
-
// Preferences
|
|
75
|
-
else if (/\b(prefer|like|want|don't like|不喜欢|喜欢|偏好)\b/i.test(c)) {
|
|
76
|
-
insights.push({ content: c.slice(0, 500), type: 'preference' });
|
|
77
|
-
}
|
|
78
|
-
// Code snippets
|
|
79
|
-
else if (/```[\s\S]{20,}```/.test(c)) {
|
|
80
|
-
insights.push({ content: c.slice(0, 800), type: 'code' });
|
|
81
|
-
}
|
|
82
|
-
// Long assistant messages likely contain useful info
|
|
83
|
-
else if (msg.role === 'assistant' && c.length > 200) {
|
|
84
|
-
insights.push({ content: c.slice(0, 500), type: 'knowledge' });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return insights;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Generate a simple summary from messages (no-brain fallback).
|
|
92
|
-
*/
|
|
93
|
-
private summarize(messages: Message[]): string {
|
|
94
|
-
const topics = new Set<string>();
|
|
95
|
-
const keyLines: string[] = [];
|
|
96
|
-
|
|
97
|
-
for (const msg of messages) {
|
|
98
|
-
// Extract first meaningful sentence
|
|
99
|
-
const firstLine = msg.content.split(/[.\n!?。!?]/)[0]?.trim();
|
|
100
|
-
if (firstLine && firstLine.length > 10 && firstLine.length < 200) {
|
|
101
|
-
if (keyLines.length < 5) keyLines.push(`[${msg.role}] ${firstLine}`);
|
|
102
|
-
}
|
|
103
|
-
// Extract topic words (capitalized words, Chinese phrases)
|
|
104
|
-
const words = msg.content.match(/[A-Z][a-z]{2,}/g) ?? [];
|
|
105
|
-
words.forEach(w => topics.add(w));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const topicStr = [...topics].slice(0, 10).join(', ');
|
|
109
|
-
const linesStr = keyLines.join('; ');
|
|
110
|
-
return `Topics: ${topicStr || 'general discussion'}. Key points: ${linesStr || 'varied conversation'}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Compress messages when token count exceeds threshold.
|
|
115
|
-
*/
|
|
116
|
-
async compress(messages: Message[], config?: Partial<CompressorConfig>): Promise<CompressResult> {
|
|
117
|
-
const cfg = { ...this.config, ...config };
|
|
118
|
-
const totalTokens = this.estimateMessagesTokens(messages);
|
|
119
|
-
const threshold = cfg.maxTokens * cfg.compressThreshold;
|
|
120
|
-
|
|
121
|
-
// Under threshold — return as-is
|
|
122
|
-
if (totalTokens <= threshold) {
|
|
123
|
-
return {
|
|
124
|
-
messages: [...messages],
|
|
125
|
-
learnedCount: 0,
|
|
126
|
-
savedTokens: 0,
|
|
127
|
-
summary: '',
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const recentCount = Math.min(cfg.preserveRecent, messages.length);
|
|
132
|
-
const splitIdx = messages.length - recentCount;
|
|
133
|
-
const oldMessages = messages.slice(0, splitIdx);
|
|
134
|
-
const recentMessages = messages.slice(splitIdx);
|
|
135
|
-
|
|
136
|
-
if (oldMessages.length === 0) {
|
|
137
|
-
return { messages: [...messages], learnedCount: 0, savedTokens: 0, summary: '' };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const oldTokens = this.estimateMessagesTokens(oldMessages);
|
|
141
|
-
let learnedCount = 0;
|
|
142
|
-
let summary: string;
|
|
143
|
-
|
|
144
|
-
if (cfg.brain) {
|
|
145
|
-
// Extract and learn insights
|
|
146
|
-
const insights = this.extractInsights(oldMessages);
|
|
147
|
-
for (const insight of insights) {
|
|
148
|
-
try {
|
|
149
|
-
await cfg.brain.learn(insight.content, { insight_type: insight.type });
|
|
150
|
-
learnedCount++;
|
|
151
|
-
} catch { /* non-critical */ }
|
|
152
|
-
}
|
|
153
|
-
summary = `${oldMessages.length} messages compressed. Extracted ${learnedCount} insights (${insights.map(i => i.type).filter((v, i, a) => a.indexOf(v) === i).join(', ')}).`;
|
|
154
|
-
} else {
|
|
155
|
-
summary = this.summarize(oldMessages);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const compressionMessage: Message = {
|
|
159
|
-
id: `compressed-${Date.now()}`,
|
|
160
|
-
role: 'system',
|
|
161
|
-
content: `[Context compressed: ${oldMessages.length} messages → ${summary}${cfg.brain ? ' Details stored in Brain, use recall() to retrieve.' : ''}]`,
|
|
162
|
-
timestamp: Date.now(),
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
messages: [compressionMessage, ...recentMessages],
|
|
167
|
-
learnedCount,
|
|
168
|
-
savedTokens: oldTokens - this.estimateTokens(compressionMessage.content),
|
|
169
|
-
summary,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Restore context from brain for a given query.
|
|
175
|
-
*/
|
|
176
|
-
async restore(query: string, brain: any): Promise<string[]> {
|
|
177
|
-
if (!brain?.recall) return [];
|
|
178
|
-
try {
|
|
179
|
-
const results = await brain.recall(query);
|
|
180
|
-
if (Array.isArray(results)) {
|
|
181
|
-
return results.map((r: any) => typeof r === 'string' ? r : r.content ?? JSON.stringify(r));
|
|
182
|
-
}
|
|
183
|
-
if (typeof results === 'string') return [results];
|
|
184
|
-
return [];
|
|
185
|
-
} catch {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
1
|
+
import type { Message } from '../core/types';
|
|
2
|
+
|
|
3
|
+
export interface CompressorConfig {
|
|
4
|
+
maxTokens: number;
|
|
5
|
+
compressThreshold: number;
|
|
6
|
+
preserveRecent: number;
|
|
7
|
+
brain?: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CompressResult {
|
|
11
|
+
messages: Message[];
|
|
12
|
+
learnedCount: number;
|
|
13
|
+
savedTokens: number;
|
|
14
|
+
summary: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG: CompressorConfig = {
|
|
18
|
+
maxTokens: 8000,
|
|
19
|
+
compressThreshold: 0.8,
|
|
20
|
+
preserveRecent: 10,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Context compression with optional DeepBrain memory offloading.
|
|
25
|
+
*/
|
|
26
|
+
export class ContextCompressor {
|
|
27
|
+
private config: CompressorConfig;
|
|
28
|
+
|
|
29
|
+
constructor(config: Partial<CompressorConfig> = {}) {
|
|
30
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Estimate token count using language-aware heuristic.
|
|
35
|
+
* English: ~1 token per 4 chars. Chinese: ~1 token per 2 chars.
|
|
36
|
+
*/
|
|
37
|
+
estimateTokens(text: string): number {
|
|
38
|
+
let tokens = 0;
|
|
39
|
+
for (const char of text) {
|
|
40
|
+
// CJK Unicode range detection
|
|
41
|
+
const code = char.codePointAt(0) ?? 0;
|
|
42
|
+
if (
|
|
43
|
+
(code >= 0x4e00 && code <= 0x9fff) || // CJK Unified
|
|
44
|
+
(code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A
|
|
45
|
+
(code >= 0x3000 && code <= 0x303f) // CJK Punctuation
|
|
46
|
+
) {
|
|
47
|
+
tokens += 0.5; // 1 token per 2 chars
|
|
48
|
+
} else {
|
|
49
|
+
tokens += 0.25; // 1 token per 4 chars
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Math.ceil(tokens);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private estimateMessagesTokens(messages: Message[]): number {
|
|
56
|
+
return messages.reduce((sum, m) => sum + this.estimateTokens(m.content), 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract key insights from messages for brain storage.
|
|
61
|
+
*/
|
|
62
|
+
private extractInsights(messages: Message[]): Array<{ content: string; type: string }> {
|
|
63
|
+
const insights: Array<{ content: string; type: string }> = [];
|
|
64
|
+
for (const msg of messages) {
|
|
65
|
+
const c = msg.content;
|
|
66
|
+
// Decisions
|
|
67
|
+
if (/\b(decided|decision|choose|chose|will use|going with|let's go|确定|决定)\b/i.test(c)) {
|
|
68
|
+
insights.push({ content: c.slice(0, 500), type: 'decision' });
|
|
69
|
+
}
|
|
70
|
+
// Facts / definitions
|
|
71
|
+
else if (/\b(is defined as|means|equals|refers to|是指|定义)\b/i.test(c)) {
|
|
72
|
+
insights.push({ content: c.slice(0, 500), type: 'fact' });
|
|
73
|
+
}
|
|
74
|
+
// Preferences
|
|
75
|
+
else if (/\b(prefer|like|want|don't like|不喜欢|喜欢|偏好)\b/i.test(c)) {
|
|
76
|
+
insights.push({ content: c.slice(0, 500), type: 'preference' });
|
|
77
|
+
}
|
|
78
|
+
// Code snippets
|
|
79
|
+
else if (/```[\s\S]{20,}```/.test(c)) {
|
|
80
|
+
insights.push({ content: c.slice(0, 800), type: 'code' });
|
|
81
|
+
}
|
|
82
|
+
// Long assistant messages likely contain useful info
|
|
83
|
+
else if (msg.role === 'assistant' && c.length > 200) {
|
|
84
|
+
insights.push({ content: c.slice(0, 500), type: 'knowledge' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return insights;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate a simple summary from messages (no-brain fallback).
|
|
92
|
+
*/
|
|
93
|
+
private summarize(messages: Message[]): string {
|
|
94
|
+
const topics = new Set<string>();
|
|
95
|
+
const keyLines: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const msg of messages) {
|
|
98
|
+
// Extract first meaningful sentence
|
|
99
|
+
const firstLine = msg.content.split(/[.\n!?。!?]/)[0]?.trim();
|
|
100
|
+
if (firstLine && firstLine.length > 10 && firstLine.length < 200) {
|
|
101
|
+
if (keyLines.length < 5) keyLines.push(`[${msg.role}] ${firstLine}`);
|
|
102
|
+
}
|
|
103
|
+
// Extract topic words (capitalized words, Chinese phrases)
|
|
104
|
+
const words = msg.content.match(/[A-Z][a-z]{2,}/g) ?? [];
|
|
105
|
+
words.forEach(w => topics.add(w));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const topicStr = [...topics].slice(0, 10).join(', ');
|
|
109
|
+
const linesStr = keyLines.join('; ');
|
|
110
|
+
return `Topics: ${topicStr || 'general discussion'}. Key points: ${linesStr || 'varied conversation'}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compress messages when token count exceeds threshold.
|
|
115
|
+
*/
|
|
116
|
+
async compress(messages: Message[], config?: Partial<CompressorConfig>): Promise<CompressResult> {
|
|
117
|
+
const cfg = { ...this.config, ...config };
|
|
118
|
+
const totalTokens = this.estimateMessagesTokens(messages);
|
|
119
|
+
const threshold = cfg.maxTokens * cfg.compressThreshold;
|
|
120
|
+
|
|
121
|
+
// Under threshold — return as-is
|
|
122
|
+
if (totalTokens <= threshold) {
|
|
123
|
+
return {
|
|
124
|
+
messages: [...messages],
|
|
125
|
+
learnedCount: 0,
|
|
126
|
+
savedTokens: 0,
|
|
127
|
+
summary: '',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const recentCount = Math.min(cfg.preserveRecent, messages.length);
|
|
132
|
+
const splitIdx = messages.length - recentCount;
|
|
133
|
+
const oldMessages = messages.slice(0, splitIdx);
|
|
134
|
+
const recentMessages = messages.slice(splitIdx);
|
|
135
|
+
|
|
136
|
+
if (oldMessages.length === 0) {
|
|
137
|
+
return { messages: [...messages], learnedCount: 0, savedTokens: 0, summary: '' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const oldTokens = this.estimateMessagesTokens(oldMessages);
|
|
141
|
+
let learnedCount = 0;
|
|
142
|
+
let summary: string;
|
|
143
|
+
|
|
144
|
+
if (cfg.brain) {
|
|
145
|
+
// Extract and learn insights
|
|
146
|
+
const insights = this.extractInsights(oldMessages);
|
|
147
|
+
for (const insight of insights) {
|
|
148
|
+
try {
|
|
149
|
+
await cfg.brain.learn(insight.content, { insight_type: insight.type });
|
|
150
|
+
learnedCount++;
|
|
151
|
+
} catch { /* non-critical */ }
|
|
152
|
+
}
|
|
153
|
+
summary = `${oldMessages.length} messages compressed. Extracted ${learnedCount} insights (${insights.map(i => i.type).filter((v, i, a) => a.indexOf(v) === i).join(', ')}).`;
|
|
154
|
+
} else {
|
|
155
|
+
summary = this.summarize(oldMessages);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const compressionMessage: Message = {
|
|
159
|
+
id: `compressed-${Date.now()}`,
|
|
160
|
+
role: 'system',
|
|
161
|
+
content: `[Context compressed: ${oldMessages.length} messages → ${summary}${cfg.brain ? ' Details stored in Brain, use recall() to retrieve.' : ''}]`,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
messages: [compressionMessage, ...recentMessages],
|
|
167
|
+
learnedCount,
|
|
168
|
+
savedTokens: oldTokens - this.estimateTokens(compressionMessage.content),
|
|
169
|
+
summary,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Restore context from brain for a given query.
|
|
175
|
+
*/
|
|
176
|
+
async restore(query: string, brain: any): Promise<string[]> {
|
|
177
|
+
if (!brain?.recall) return [];
|
|
178
|
+
try {
|
|
179
|
+
const results = await brain.recall(query);
|
|
180
|
+
if (Array.isArray(results)) {
|
|
181
|
+
return results.map((r: any) => typeof r === 'string' ? r : r.content ?? JSON.stringify(r));
|
|
182
|
+
}
|
|
183
|
+
if (typeof results === 'string') return [results];
|
|
184
|
+
return [];
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/memory/deepbrain.ts
CHANGED
|
@@ -1,9 +1,103 @@
|
|
|
1
1
|
import type { Message, MemoryStore } from '../core/types';
|
|
2
2
|
import { InMemoryStore } from './index';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 本地 JSON 文件持久化存储,作为 DeepBrain 不可用时的 fallback。
|
|
8
|
+
* 数据保存在 .opc/memory.json,进程重启后记忆不会丢失。
|
|
9
|
+
*/
|
|
10
|
+
class FileBackedStore implements MemoryStore {
|
|
11
|
+
private store: Map<string, unknown> = new Map();
|
|
12
|
+
private conversations: Map<string, Message[]> = new Map();
|
|
13
|
+
private filePath: string;
|
|
14
|
+
private dirty = false;
|
|
15
|
+
private saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
16
|
+
|
|
17
|
+
constructor(baseDir: string = '.') {
|
|
18
|
+
const opcDir = join(resolve(baseDir), '.opc');
|
|
19
|
+
if (!existsSync(opcDir)) mkdirSync(opcDir, { recursive: true });
|
|
20
|
+
this.filePath = join(opcDir, 'memory.json');
|
|
21
|
+
this.loadFromFile();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private loadFromFile(): void {
|
|
25
|
+
if (!existsSync(this.filePath)) return;
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
|
|
28
|
+
if (data.store) {
|
|
29
|
+
for (const [k, v] of Object.entries(data.store)) {
|
|
30
|
+
this.store.set(k, v);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (data.conversations) {
|
|
34
|
+
for (const [k, v] of Object.entries(data.conversations)) {
|
|
35
|
+
this.conversations.set(k, v as Message[]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch { /* 文件损坏则忽略,从空开始 */ }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private scheduleSave(): void {
|
|
42
|
+
this.dirty = true;
|
|
43
|
+
if (this.saveTimer) return; // 已经有定时器在等了
|
|
44
|
+
// 延迟 1 秒批量写入,避免高频写磁盘
|
|
45
|
+
this.saveTimer = setTimeout(() => {
|
|
46
|
+
this.saveTimer = null;
|
|
47
|
+
if (this.dirty) this.saveToFile();
|
|
48
|
+
}, 1000);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private saveToFile(): void {
|
|
52
|
+
try {
|
|
53
|
+
const data = {
|
|
54
|
+
store: Object.fromEntries(this.store),
|
|
55
|
+
conversations: Object.fromEntries(this.conversations),
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
59
|
+
this.dirty = false;
|
|
60
|
+
} catch { /* 写入失败不影响运行 */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async get(key: string): Promise<unknown> {
|
|
64
|
+
return this.store.get(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async set(key: string, value: unknown): Promise<void> {
|
|
68
|
+
this.store.set(key, value);
|
|
69
|
+
this.scheduleSave();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getConversation(sessionId: string): Promise<Message[]> {
|
|
73
|
+
return this.conversations.get(sessionId) ?? [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async addMessage(sessionId: string, message: Message): Promise<void> {
|
|
77
|
+
if (!this.conversations.has(sessionId)) {
|
|
78
|
+
this.conversations.set(sessionId, []);
|
|
79
|
+
}
|
|
80
|
+
const conv = this.conversations.get(sessionId)!;
|
|
81
|
+
conv.push(message);
|
|
82
|
+
// 每个 session 最多保留 200 条消息,避免文件无限增长
|
|
83
|
+
if (conv.length > 200) conv.splice(0, conv.length - 200);
|
|
84
|
+
this.scheduleSave();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async clear(sessionId?: string): Promise<void> {
|
|
88
|
+
if (sessionId) {
|
|
89
|
+
this.conversations.delete(sessionId);
|
|
90
|
+
} else {
|
|
91
|
+
this.store.clear();
|
|
92
|
+
this.conversations.clear();
|
|
93
|
+
}
|
|
94
|
+
this.scheduleSave();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
3
97
|
|
|
4
98
|
/**
|
|
5
99
|
* DeepBrain-backed memory store for long-term semantic memory.
|
|
6
|
-
* Falls back to
|
|
100
|
+
* Falls back to local JSON file storage (.opc/memory.json) if deepbrain package is not installed.
|
|
7
101
|
*/
|
|
8
102
|
export interface DeepBrainClient {
|
|
9
103
|
store(collection: string, id: string, content: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
@@ -12,13 +106,13 @@ export interface DeepBrainClient {
|
|
|
12
106
|
}
|
|
13
107
|
|
|
14
108
|
export class DeepBrainMemoryStore implements MemoryStore {
|
|
15
|
-
private fallback:
|
|
109
|
+
private fallback: FileBackedStore;
|
|
16
110
|
private client: DeepBrainClient | null = null;
|
|
17
111
|
private collection: string;
|
|
18
112
|
private ready: Promise<boolean>;
|
|
19
113
|
|
|
20
114
|
constructor(options: { collection?: string; config?: Record<string, unknown> } = {}) {
|
|
21
|
-
this.fallback = new
|
|
115
|
+
this.fallback = new FileBackedStore();
|
|
22
116
|
this.collection = options.collection ?? 'agent-memory';
|
|
23
117
|
this.ready = this.initClient(options.config);
|
|
24
118
|
}
|
|
@@ -29,12 +123,12 @@ export class DeepBrainMemoryStore implements MemoryStore {
|
|
|
29
123
|
const deepbrain = await import(/* webpackIgnore: true */ 'deepbrain');
|
|
30
124
|
this.client = (deepbrain as any).createClient?.(config) ?? (deepbrain as any).default?.createClient?.(config);
|
|
31
125
|
if (!this.client) {
|
|
32
|
-
console.warn('[DeepBrainMemory] Could not create client, using
|
|
126
|
+
console.warn('[DeepBrainMemory] Could not create client, using file-backed fallback (.opc/memory.json)');
|
|
33
127
|
return false;
|
|
34
128
|
}
|
|
35
129
|
return true;
|
|
36
130
|
} catch {
|
|
37
|
-
console.warn('[DeepBrainMemory] deepbrain package not found, using
|
|
131
|
+
console.warn('[DeepBrainMemory] deepbrain package not found, using file-backed fallback (.opc/memory.json)');
|
|
38
132
|
return false;
|
|
39
133
|
}
|
|
40
134
|
}
|