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.
Files changed (258) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
  3. package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
  4. package/CHANGELOG.md +48 -48
  5. package/CONTRIBUTING.md +36 -36
  6. package/README.zh-CN.md +497 -497
  7. package/USABILITY-ISSUES.md +73 -0
  8. package/dist/channels/web.js +8 -2
  9. package/dist/channels/wechat.js +6 -6
  10. package/dist/cli.js +200 -85
  11. package/dist/core/runtime.js +37 -15
  12. package/dist/deploy/index.js +56 -56
  13. package/dist/doctor.d.ts +1 -0
  14. package/dist/doctor.js +105 -10
  15. package/dist/memory/deepbrain.d.ts +1 -1
  16. package/dist/memory/deepbrain.js +95 -4
  17. package/dist/scheduler/cron-engine.js +3 -36
  18. package/dist/studio/server.js +30 -1
  19. package/dist/studio-ui/index.html +230 -10
  20. package/dist/ui/components.js +105 -105
  21. package/examples/README.md +22 -22
  22. package/examples/basic-agent.ts +90 -90
  23. package/examples/brain-integration.ts +71 -71
  24. package/examples/multi-channel.ts +74 -74
  25. package/fix-sidebar.mjs +188 -188
  26. package/install.ps1 +154 -154
  27. package/install.sh +164 -164
  28. package/package.json +1 -1
  29. package/scripts/install.ps1 +31 -31
  30. package/scripts/install.sh +40 -40
  31. package/serve-studio.js +13 -13
  32. package/serve-test.js +25 -25
  33. package/src/channels/dingtalk.ts +46 -46
  34. package/src/channels/email.ts +351 -351
  35. package/src/channels/feishu.ts +349 -349
  36. package/src/channels/googlechat.ts +42 -42
  37. package/src/channels/imessage.ts +31 -31
  38. package/src/channels/irc.ts +82 -82
  39. package/src/channels/line.ts +32 -32
  40. package/src/channels/matrix.ts +33 -33
  41. package/src/channels/mattermost.ts +57 -57
  42. package/src/channels/msteams.ts +32 -32
  43. package/src/channels/nostr.ts +32 -32
  44. package/src/channels/qq.ts +33 -33
  45. package/src/channels/signal.ts +32 -32
  46. package/src/channels/sms.ts +33 -33
  47. package/src/channels/telegram.ts +616 -616
  48. package/src/channels/twitch.ts +65 -65
  49. package/src/channels/voice-call.ts +100 -100
  50. package/src/channels/web.ts +8 -2
  51. package/src/channels/websocket.ts +399 -399
  52. package/src/channels/wechat.ts +329 -329
  53. package/src/channels/whatsapp.ts +32 -32
  54. package/src/cli/chat.ts +99 -99
  55. package/src/cli/setup.ts +314 -314
  56. package/src/cli.ts +195 -92
  57. package/src/core/agent.ts +476 -476
  58. package/src/core/api-server.ts +277 -277
  59. package/src/core/audio.ts +98 -98
  60. package/src/core/collaboration.ts +275 -275
  61. package/src/core/context-discovery.ts +85 -85
  62. package/src/core/context-refs.ts +140 -140
  63. package/src/core/gateway.ts +106 -106
  64. package/src/core/heartbeat.ts +51 -51
  65. package/src/core/hooks.ts +105 -105
  66. package/src/core/ide-bridge.ts +133 -133
  67. package/src/core/node-network.ts +86 -86
  68. package/src/core/profiles.ts +122 -122
  69. package/src/core/runtime.ts +25 -0
  70. package/src/core/scheduler.ts +187 -187
  71. package/src/core/session-manager.ts +137 -137
  72. package/src/core/subagent.ts +98 -98
  73. package/src/core/vision.ts +180 -180
  74. package/src/core/workflow-graph.ts +365 -365
  75. package/src/daemon.ts +96 -96
  76. package/src/deploy/index.ts +255 -255
  77. package/src/doctor.ts +98 -11
  78. package/src/eval/index.ts +211 -211
  79. package/src/eval/suites/basic.json +16 -16
  80. package/src/eval/suites/memory.json +12 -12
  81. package/src/eval/suites/safety.json +14 -14
  82. package/src/hub/brain-seed.ts +54 -54
  83. package/src/hub/client.ts +60 -60
  84. package/src/mcp/servers/calculator-mcp.ts +65 -65
  85. package/src/mcp/servers/crypto-mcp.ts +73 -73
  86. package/src/mcp/servers/database-mcp.ts +72 -72
  87. package/src/mcp/servers/datetime-mcp.ts +69 -69
  88. package/src/mcp/servers/filesystem.ts +66 -66
  89. package/src/mcp/servers/github-mcp.ts +58 -58
  90. package/src/mcp/servers/index.ts +63 -63
  91. package/src/mcp/servers/json-mcp.ts +102 -102
  92. package/src/mcp/servers/memory-mcp.ts +56 -56
  93. package/src/mcp/servers/regex-mcp.ts +53 -53
  94. package/src/mcp/servers/web-mcp.ts +49 -49
  95. package/src/memory/context-compressor.ts +189 -189
  96. package/src/memory/deepbrain.ts +99 -5
  97. package/src/memory/seed-loader.ts +212 -212
  98. package/src/memory/user-profiler.ts +215 -215
  99. package/src/plugins/content-filter.ts +23 -23
  100. package/src/plugins/logger.ts +18 -18
  101. package/src/plugins/rate-limiter.ts +38 -38
  102. package/src/protocols/a2a/client.ts +132 -132
  103. package/src/protocols/a2a/index.ts +8 -8
  104. package/src/protocols/a2a/server.ts +333 -333
  105. package/src/protocols/a2a/types.ts +88 -88
  106. package/src/protocols/a2a/utils.ts +50 -50
  107. package/src/protocols/agui/client.ts +83 -83
  108. package/src/protocols/agui/index.ts +4 -4
  109. package/src/protocols/agui/server.ts +218 -218
  110. package/src/protocols/agui/types.ts +153 -153
  111. package/src/protocols/index.ts +2 -2
  112. package/src/protocols/mcp/agent-tools.ts +134 -134
  113. package/src/protocols/mcp/index.ts +8 -8
  114. package/src/protocols/mcp/server.ts +262 -262
  115. package/src/protocols/mcp/types.ts +69 -69
  116. package/src/providers/index.ts +632 -632
  117. package/src/publish/index.ts +376 -376
  118. package/src/scheduler/cron-engine.ts +191 -191
  119. package/src/scheduler/index.ts +2 -2
  120. package/src/schema/oad.ts +217 -217
  121. package/src/security/approval.ts +131 -131
  122. package/src/security/approvals.ts +143 -143
  123. package/src/security/elevated.ts +105 -105
  124. package/src/security/guardrails.ts +248 -248
  125. package/src/security/index.ts +9 -9
  126. package/src/security/keys.ts +87 -87
  127. package/src/security/secrets.ts +129 -129
  128. package/src/skills/builtin/index.ts +408 -408
  129. package/src/skills/marketplace.ts +113 -113
  130. package/src/skills/types.ts +42 -42
  131. package/src/studio/server.ts +31 -1
  132. package/src/studio/templates-data.ts +178 -178
  133. package/src/studio-ui/index.html +230 -10
  134. package/src/telemetry/index.ts +324 -324
  135. package/src/tools/builtin/browser.ts +299 -299
  136. package/src/tools/builtin/datetime.ts +41 -41
  137. package/src/tools/builtin/file.ts +107 -107
  138. package/src/tools/builtin/home-assistant.ts +116 -116
  139. package/src/tools/builtin/rl-tools.ts +243 -243
  140. package/src/tools/builtin/shell.ts +43 -43
  141. package/src/tools/builtin/vision.ts +64 -64
  142. package/src/tools/builtin/web-search.ts +126 -126
  143. package/src/tools/builtin/web.ts +35 -35
  144. package/src/tools/document-processor.ts +213 -213
  145. package/src/tools/image-generator.ts +150 -150
  146. package/src/tools/integrations/calendar.ts +73 -73
  147. package/src/tools/integrations/code-exec.ts +39 -39
  148. package/src/tools/integrations/csv-analyzer.ts +92 -92
  149. package/src/tools/integrations/database.ts +44 -44
  150. package/src/tools/integrations/email-send.ts +76 -76
  151. package/src/tools/integrations/git-tool.ts +42 -42
  152. package/src/tools/integrations/github-tool.ts +76 -76
  153. package/src/tools/integrations/image-gen.ts +56 -56
  154. package/src/tools/integrations/index.ts +92 -92
  155. package/src/tools/integrations/jira.ts +83 -83
  156. package/src/tools/integrations/notion.ts +71 -71
  157. package/src/tools/integrations/npm-tool.ts +48 -48
  158. package/src/tools/integrations/pdf-reader.ts +58 -58
  159. package/src/tools/integrations/slack.ts +65 -65
  160. package/src/tools/integrations/summarizer.ts +49 -49
  161. package/src/tools/integrations/translator.ts +48 -48
  162. package/src/tools/integrations/trello.ts +60 -60
  163. package/src/tools/integrations/vector-search.ts +42 -42
  164. package/src/tools/integrations/web-scraper.ts +47 -47
  165. package/src/tools/integrations/web-search.ts +58 -58
  166. package/src/tools/integrations/webhook.ts +38 -38
  167. package/src/tools/mcp-client.ts +131 -131
  168. package/src/tools/web-scraper.ts +179 -179
  169. package/src/tools/web-search.ts +180 -180
  170. package/src/ui/components.ts +127 -127
  171. package/srv-out.txt +1 -1
  172. package/templates/ecommerce-assistant/README.md +45 -45
  173. package/templates/ecommerce-assistant/oad.yaml +47 -47
  174. package/templates/tech-support/README.md +43 -43
  175. package/templates/tech-support/oad.yaml +45 -45
  176. package/test-agent/Dockerfile +9 -9
  177. package/test-agent/README.md +50 -50
  178. package/test-agent/agent.yaml +23 -23
  179. package/test-agent/docker-compose.yml +11 -11
  180. package/test-agent/oad.yaml +31 -31
  181. package/test-agent/package-lock.json +1492 -1492
  182. package/test-agent/package.json +17 -17
  183. package/test-agent/src/index.ts +24 -24
  184. package/test-agent/src/skills/echo.ts +15 -15
  185. package/test-agent/tsconfig.json +24 -24
  186. package/test-full.js +43 -43
  187. package/test-sidebar.js +22 -22
  188. package/test-studio3.js +75 -75
  189. package/test-studio4.js +41 -41
  190. package/tests/a2a-protocol.test.ts +285 -285
  191. package/tests/agui-protocol.test.ts +246 -246
  192. package/tests/api-server.test.ts +148 -148
  193. package/tests/approvals.test.ts +89 -89
  194. package/tests/audio.test.ts +40 -40
  195. package/tests/brain-seed-extended.test.ts +490 -490
  196. package/tests/brain-seed.test.ts +239 -239
  197. package/tests/browser.test.ts +179 -179
  198. package/tests/channels/discord.test.ts +79 -79
  199. package/tests/channels/email.test.ts +148 -148
  200. package/tests/channels/feishu.test.ts +123 -123
  201. package/tests/channels/telegram.test.ts +129 -129
  202. package/tests/channels/websocket.test.ts +53 -53
  203. package/tests/channels/wechat.test.ts +170 -170
  204. package/tests/channels-extra.test.ts +45 -45
  205. package/tests/chat-cli.test.ts +160 -160
  206. package/tests/cli.test.ts +46 -46
  207. package/tests/context-compressor.test.ts +172 -172
  208. package/tests/context-refs.test.ts +121 -121
  209. package/tests/cron-engine.test.ts +101 -101
  210. package/tests/daemon.test.ts +135 -135
  211. package/tests/deepbrain-wire.test.ts +234 -234
  212. package/tests/deploy-and-dag.test.ts +196 -196
  213. package/tests/doctor.test.ts +38 -38
  214. package/tests/document-processor.test.ts +69 -69
  215. package/tests/e2e-nocode.test.ts +442 -442
  216. package/tests/elevated.test.ts +69 -69
  217. package/tests/eval.test.ts +173 -173
  218. package/tests/gateway.test.ts +63 -63
  219. package/tests/guardrails.test.ts +177 -177
  220. package/tests/home-assistant.test.ts +40 -40
  221. package/tests/hooks.test.ts +79 -79
  222. package/tests/ide-bridge.test.ts +38 -38
  223. package/tests/image-generator.test.ts +84 -84
  224. package/tests/init-role.test.ts +124 -124
  225. package/tests/integrations.test.ts +249 -249
  226. package/tests/mcp-client.test.ts +92 -92
  227. package/tests/mcp-server.test.ts +178 -178
  228. package/tests/mcp-servers.test.ts +260 -260
  229. package/tests/node-network.test.ts +74 -74
  230. package/tests/plugin-a2a-enhanced.test.ts +230 -230
  231. package/tests/profiles.test.ts +61 -61
  232. package/tests/publish.test.ts +231 -231
  233. package/tests/rl-tools.test.ts +93 -93
  234. package/tests/sandbox-manager.test.ts +46 -46
  235. package/tests/scheduler.test.ts +200 -200
  236. package/tests/secrets.test.ts +107 -107
  237. package/tests/security-enhanced.test.ts +233 -233
  238. package/tests/settings-api.test.ts +148 -148
  239. package/tests/setup.test.ts +73 -73
  240. package/tests/subagent.test.ts +193 -193
  241. package/tests/telegram-discord.test.ts +60 -60
  242. package/tests/telemetry.test.ts +186 -186
  243. package/tests/user-profiler.test.ts +169 -169
  244. package/tests/v090-features.test.ts +254 -254
  245. package/tests/vision.test.ts +61 -61
  246. package/tests/voice-call.test.ts +47 -47
  247. package/tests/voice-enhanced.test.ts +169 -169
  248. package/tests/voice-interaction.test.ts +38 -38
  249. package/tests/web-search.test.ts +155 -155
  250. package/tests/workflow-graph.test.ts +279 -279
  251. package/tutorial/customer-service-agent/README.md +612 -612
  252. package/tutorial/customer-service-agent/SOUL.md +26 -26
  253. package/tutorial/customer-service-agent/agent.yaml +63 -63
  254. package/tutorial/customer-service-agent/package.json +19 -19
  255. package/tutorial/customer-service-agent/src/index.ts +69 -69
  256. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
  257. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
  258. 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
+ }
@@ -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 InMemoryStore if deepbrain package is not installed.
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: InMemoryStore;
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 InMemoryStore();
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 in-memory fallback');
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 in-memory fallback');
131
+ console.warn('[DeepBrainMemory] deepbrain package not found, using file-backed fallback (.opc/memory.json)');
38
132
  return false;
39
133
  }
40
134
  }