skyloom 1.12.0 → 1.13.0
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/workflows/ci.yml +36 -36
- package/README.md +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/main.js +103 -51
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +8 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/main.ts +425 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -269
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
package/src/core/filter.ts
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 输出过滤模块 — sensitive information sanitization.
|
|
3
|
-
*
|
|
4
|
-
* Before agent responses reach the user (or are persisted),
|
|
5
|
-
* scan for and redact sensitive patterns like API keys,
|
|
6
|
-
* tokens, passwords, PII, and internal paths.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/* ═══════════════════════════════════════
|
|
10
|
-
Detection patterns — compiled once at module load
|
|
11
|
-
═══════════════════════════════════════ */
|
|
12
|
-
const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
|
|
13
|
-
// API keys & tokens
|
|
14
|
-
[/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
|
|
15
|
-
[/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
|
|
16
|
-
[/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
17
|
-
[/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
18
|
-
|
|
19
|
-
// AWS credentials
|
|
20
|
-
[/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
|
|
21
|
-
[/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
|
|
22
|
-
|
|
23
|
-
// Passwords
|
|
24
|
-
[/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
|
|
25
|
-
[/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
|
|
26
|
-
|
|
27
|
-
// Connection strings
|
|
28
|
-
[/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
29
|
-
[/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
30
|
-
|
|
31
|
-
// Private keys
|
|
32
|
-
[/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
|
|
33
|
-
|
|
34
|
-
// IP addresses (local only)
|
|
35
|
-
[/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
36
|
-
[/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
37
|
-
[/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
38
|
-
|
|
39
|
-
// File paths
|
|
40
|
-
[/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
/* Email masking (function-based, handled separately) */
|
|
44
|
-
const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/* ═══════════════════════════════════════
|
|
48
|
-
Filter function
|
|
49
|
-
═══════════════════════════════════════ */
|
|
50
|
-
export interface FilterResult {
|
|
51
|
-
clean: string;
|
|
52
|
-
redacted: boolean;
|
|
53
|
-
count: number;
|
|
54
|
-
details: string[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function filterOutput(text: string): FilterResult {
|
|
58
|
-
if (!text) return { clean: "", redacted: false, count: 0, details: [] };
|
|
59
|
-
|
|
60
|
-
let clean = text;
|
|
61
|
-
let count = 0;
|
|
62
|
-
const details: string[] = [];
|
|
63
|
-
|
|
64
|
-
// Email masking (function-based replacement)
|
|
65
|
-
let emailCount = 0;
|
|
66
|
-
clean = clean.replace(EMAIL_RE, (full, user, domain) => {
|
|
67
|
-
emailCount++;
|
|
68
|
-
return (user as string).slice(0, 2) + "***@" + (domain as string);
|
|
69
|
-
});
|
|
70
|
-
if (emailCount > 0) {
|
|
71
|
-
count += emailCount;
|
|
72
|
-
details.push(`Masked ${emailCount}x email addresses`);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
|
|
76
|
-
const matches = clean.match(pattern);
|
|
77
|
-
if (matches) {
|
|
78
|
-
count += matches.length;
|
|
79
|
-
if (typeof replacement === "string") {
|
|
80
|
-
details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
|
|
81
|
-
} else {
|
|
82
|
-
details.push(`Masked ${matches.length}x email addresses`);
|
|
83
|
-
}
|
|
84
|
-
clean = clean.replace(pattern, replacement as string);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { clean, redacted: count > 0, count, details };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* ═══════════════════════════════════════
|
|
92
|
-
Quick check — is filtering needed?
|
|
93
|
-
═══════════════════════════════════════ */
|
|
94
|
-
export function needsFiltering(text: string): boolean {
|
|
95
|
-
if (!text) return false;
|
|
96
|
-
// Quick scan with the most common patterns
|
|
97
|
-
if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
|
|
98
|
-
if (/api_key.*[:=]/.test(text)) return true;
|
|
99
|
-
if (/password.*[:=]/.test(text)) return true;
|
|
100
|
-
if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
|
|
101
|
-
if (EMAIL_RE.test(text)) return true;
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 输出过滤模块 — sensitive information sanitization.
|
|
3
|
+
*
|
|
4
|
+
* Before agent responses reach the user (or are persisted),
|
|
5
|
+
* scan for and redact sensitive patterns like API keys,
|
|
6
|
+
* tokens, passwords, PII, and internal paths.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* ═══════════════════════════════════════
|
|
10
|
+
Detection patterns — compiled once at module load
|
|
11
|
+
═══════════════════════════════════════ */
|
|
12
|
+
const SENSITIVE_PATTERNS: Array<[RegExp, string]> = [
|
|
13
|
+
// API keys & tokens
|
|
14
|
+
[/sk-[a-zA-Z0-9]{32,}/g, "[REDACTED:API_KEY]"],
|
|
15
|
+
[/(?:api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*["']?[^\s"']{8,}["']?/gi, "$1: [REDACTED]"],
|
|
16
|
+
[/ghp_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
17
|
+
[/gho_[a-zA-Z0-9]{36}/g, "[REDACTED:GITHUB_TOKEN]"],
|
|
18
|
+
|
|
19
|
+
// AWS credentials
|
|
20
|
+
[/AKIA[0-9A-Z]{16}/g, "[REDACTED:AWS_KEY]"],
|
|
21
|
+
[/(?:aws_access_key_id|aws_secret_access_key)\s*[:=]\s*["']?[^\s"']+/gi, "$1: [REDACTED]"],
|
|
22
|
+
|
|
23
|
+
// Passwords
|
|
24
|
+
[/(?:password|passwd|pwd)\s*[:=]\s*["']?[^\s"']{4,}["']?/gi, "$1: [REDACTED]"],
|
|
25
|
+
[/(?:密码|口令)\s*[:=]\s*["']?[^\s"']{2,}["']?/g, "$1: [已脱敏]"],
|
|
26
|
+
|
|
27
|
+
// Connection strings
|
|
28
|
+
[/(?:mongodb|postgres|mysql|redis):\/\/[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
29
|
+
[/(?:jdbc|odbc):[^\s]+/g, "[REDACTED:DB_URI]"],
|
|
30
|
+
|
|
31
|
+
// Private keys
|
|
32
|
+
[/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END .*?PRIVATE KEY-----/g, "[REDACTED:PRIVATE_KEY]"],
|
|
33
|
+
|
|
34
|
+
// IP addresses (local only)
|
|
35
|
+
[/192\.168\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
36
|
+
[/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
37
|
+
[/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}/g, "[REDACTED:LAN_IP]"],
|
|
38
|
+
|
|
39
|
+
// File paths
|
|
40
|
+
[/(?:\/etc\/(?:passwd|shadow|hosts|sudoers))/g, "[REDACTED:SYSTEM_PATH]"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/* Email masking (function-based, handled separately) */
|
|
44
|
+
const EMAIL_RE = /([a-zA-Z0-9._%+-]{3,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/* ═══════════════════════════════════════
|
|
48
|
+
Filter function
|
|
49
|
+
═══════════════════════════════════════ */
|
|
50
|
+
export interface FilterResult {
|
|
51
|
+
clean: string;
|
|
52
|
+
redacted: boolean;
|
|
53
|
+
count: number;
|
|
54
|
+
details: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function filterOutput(text: string): FilterResult {
|
|
58
|
+
if (!text) return { clean: "", redacted: false, count: 0, details: [] };
|
|
59
|
+
|
|
60
|
+
let clean = text;
|
|
61
|
+
let count = 0;
|
|
62
|
+
const details: string[] = [];
|
|
63
|
+
|
|
64
|
+
// Email masking (function-based replacement)
|
|
65
|
+
let emailCount = 0;
|
|
66
|
+
clean = clean.replace(EMAIL_RE, (full, user, domain) => {
|
|
67
|
+
emailCount++;
|
|
68
|
+
return (user as string).slice(0, 2) + "***@" + (domain as string);
|
|
69
|
+
});
|
|
70
|
+
if (emailCount > 0) {
|
|
71
|
+
count += emailCount;
|
|
72
|
+
details.push(`Masked ${emailCount}x email addresses`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
|
|
76
|
+
const matches = clean.match(pattern);
|
|
77
|
+
if (matches) {
|
|
78
|
+
count += matches.length;
|
|
79
|
+
if (typeof replacement === "string") {
|
|
80
|
+
details.push(`Redacted ${matches.length}x ${pattern.source.slice(0, 30)}`);
|
|
81
|
+
} else {
|
|
82
|
+
details.push(`Masked ${matches.length}x email addresses`);
|
|
83
|
+
}
|
|
84
|
+
clean = clean.replace(pattern, replacement as string);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { clean, redacted: count > 0, count, details };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ═══════════════════════════════════════
|
|
92
|
+
Quick check — is filtering needed?
|
|
93
|
+
═══════════════════════════════════════ */
|
|
94
|
+
export function needsFiltering(text: string): boolean {
|
|
95
|
+
if (!text) return false;
|
|
96
|
+
// Quick scan with the most common patterns
|
|
97
|
+
if (/sk-[a-zA-Z0-9]{32,}/.test(text)) return true;
|
|
98
|
+
if (/api_key.*[:=]/.test(text)) return true;
|
|
99
|
+
if (/password.*[:=]/.test(text)) return true;
|
|
100
|
+
if (/-----BEGIN.*PRIVATE KEY-----/.test(text)) return true;
|
|
101
|
+
if (EMAIL_RE.test(text)) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
package/src/core/graph.ts
CHANGED
|
@@ -1,156 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 简易知识图谱 — entity-relation storage in SQLite.
|
|
3
|
-
*
|
|
4
|
-
* Lightweight triple store: (subject, predicate, object) with metadata.
|
|
5
|
-
* Used for: project info, tool preferences, dependency relationships.
|
|
6
|
-
*
|
|
7
|
-
* Schema:
|
|
8
|
-
* CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
|
|
9
|
-
*
|
|
10
|
-
* Queries:
|
|
11
|
-
* - Find all relations for an entity
|
|
12
|
-
* - Find all entities matching a predicate
|
|
13
|
-
* - Transitive closure (2-hop max for performance)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import * as fs from "fs";
|
|
17
|
-
import * as path from "path";
|
|
18
|
-
import { USER_CONFIG_DIR } from "./config";
|
|
19
|
-
import { getLogger } from "./logger";
|
|
20
|
-
|
|
21
|
-
const log = getLogger("graph");
|
|
22
|
-
|
|
23
|
-
/* ═══════════════════════════════════════
|
|
24
|
-
Triple store — in-memory + optional persistence
|
|
25
|
-
═══════════════════════════════════════ */
|
|
26
|
-
interface Triple {
|
|
27
|
-
subj: string;
|
|
28
|
-
pred: string;
|
|
29
|
-
obj: string;
|
|
30
|
-
agent: string;
|
|
31
|
-
ts: string;
|
|
32
|
-
meta?: Record<string, string>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class KnowledgeGraph {
|
|
36
|
-
private triples: Triple[] = [];
|
|
37
|
-
private indexPath: string;
|
|
38
|
-
|
|
39
|
-
constructor(name: string = "default") {
|
|
40
|
-
this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
|
|
41
|
-
this.load();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Add a fact: (subject, predicate, object). */
|
|
45
|
-
add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
|
|
46
|
-
// Deduplicate
|
|
47
|
-
const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
|
|
48
|
-
if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
|
|
49
|
-
|
|
50
|
-
this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
|
|
51
|
-
if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
|
|
52
|
-
this.save();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Find all facts about an entity. */
|
|
56
|
-
about(entity: string, limit: number = 20): Triple[] {
|
|
57
|
-
return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Find all subjects matching a predicate. */
|
|
61
|
-
byPredicate(pred: string): Triple[] {
|
|
62
|
-
return this.triples.filter(t => t.pred === pred);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Find all objects for a subject-predicate pair. */
|
|
66
|
-
find(subj: string, pred: string): Triple[] {
|
|
67
|
-
return this.triples.filter(t => t.subj === subj && t.pred === pred);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Transitive expansion: 2-hop from a starting entity. */
|
|
71
|
-
expand(entity: string, maxDepth: number = 2): Triple[] {
|
|
72
|
-
const seen = new Set<Triple>();
|
|
73
|
-
const queue = [entity];
|
|
74
|
-
for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
|
|
75
|
-
const current = queue.shift()!;
|
|
76
|
-
const facts = this.about(current, 10);
|
|
77
|
-
for (const f of facts) {
|
|
78
|
-
if (seen.has(f)) continue;
|
|
79
|
-
seen.add(f);
|
|
80
|
-
if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
|
|
81
|
-
if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return Array.from(seen);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Remove a fact. */
|
|
88
|
-
remove(subj: string, pred: string, obj: string): void {
|
|
89
|
-
this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
|
|
90
|
-
this.save();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Search for entities or predicates containing a keyword. */
|
|
94
|
-
search(keyword: string, limit: number = 15): Triple[] {
|
|
95
|
-
const k = keyword.toLowerCase();
|
|
96
|
-
return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Format facts as readable text. */
|
|
100
|
-
format(entity?: string): string {
|
|
101
|
-
const facts = entity ? this.about(entity) : this.triples.slice(-30);
|
|
102
|
-
if (facts.length === 0) return "(no facts)";
|
|
103
|
-
const bySubj = new Map<string, string[]>();
|
|
104
|
-
for (const f of facts) {
|
|
105
|
-
if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
|
|
106
|
-
bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
|
|
107
|
-
}
|
|
108
|
-
const lines: string[] = [];
|
|
109
|
-
for (const [subj, preds] of bySubj) {
|
|
110
|
-
lines.push(`**${subj}**: ${preds.join(", ")}`);
|
|
111
|
-
}
|
|
112
|
-
return lines.join("\n");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
get size(): number { return this.triples.length; }
|
|
116
|
-
|
|
117
|
-
private save(): void {
|
|
118
|
-
try {
|
|
119
|
-
const dir = path.dirname(this.indexPath);
|
|
120
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
121
|
-
fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
|
|
122
|
-
} catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private load(): void {
|
|
126
|
-
try {
|
|
127
|
-
if (fs.existsSync(this.indexPath)) {
|
|
128
|
-
this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
|
|
129
|
-
}
|
|
130
|
-
} catch { this.triples = []; }
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/* ── Auto-extract facts from conversation ── */
|
|
135
|
-
const RELATION_PATTERNS: Array<[RegExp, string]> = [
|
|
136
|
-
[/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
|
|
137
|
-
[/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
|
|
138
|
-
[/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
|
|
139
|
-
[/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
|
|
140
|
-
[/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
|
|
141
|
-
];
|
|
142
|
-
|
|
143
|
-
export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
|
|
144
|
-
const facts: Array<[string, string, string]> = [];
|
|
145
|
-
for (const [pattern, pred] of RELATION_PATTERNS) {
|
|
146
|
-
let match;
|
|
147
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
148
|
-
const subj = match[1].trim().toLowerCase();
|
|
149
|
-
const obj = match[2].trim();
|
|
150
|
-
if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
|
|
151
|
-
facts.push([subj, pred, obj]);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return facts;
|
|
156
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 简易知识图谱 — entity-relation storage in SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight triple store: (subject, predicate, object) with metadata.
|
|
5
|
+
* Used for: project info, tool preferences, dependency relationships.
|
|
6
|
+
*
|
|
7
|
+
* Schema:
|
|
8
|
+
* CREATE TABLE triples (subj, pred, obj, agent, ts, meta)
|
|
9
|
+
*
|
|
10
|
+
* Queries:
|
|
11
|
+
* - Find all relations for an entity
|
|
12
|
+
* - Find all entities matching a predicate
|
|
13
|
+
* - Transitive closure (2-hop max for performance)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "fs";
|
|
17
|
+
import * as path from "path";
|
|
18
|
+
import { USER_CONFIG_DIR } from "./config";
|
|
19
|
+
import { getLogger } from "./logger";
|
|
20
|
+
|
|
21
|
+
const log = getLogger("graph");
|
|
22
|
+
|
|
23
|
+
/* ═══════════════════════════════════════
|
|
24
|
+
Triple store — in-memory + optional persistence
|
|
25
|
+
═══════════════════════════════════════ */
|
|
26
|
+
interface Triple {
|
|
27
|
+
subj: string;
|
|
28
|
+
pred: string;
|
|
29
|
+
obj: string;
|
|
30
|
+
agent: string;
|
|
31
|
+
ts: string;
|
|
32
|
+
meta?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class KnowledgeGraph {
|
|
36
|
+
private triples: Triple[] = [];
|
|
37
|
+
private indexPath: string;
|
|
38
|
+
|
|
39
|
+
constructor(name: string = "default") {
|
|
40
|
+
this.indexPath = path.join(USER_CONFIG_DIR, `kg_${name}.json`);
|
|
41
|
+
this.load();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Add a fact: (subject, predicate, object). */
|
|
45
|
+
add(subj: string, pred: string, obj: string, agent: string = "system", meta?: Record<string, string>): void {
|
|
46
|
+
// Deduplicate
|
|
47
|
+
const exists = this.triples.find(t => t.subj === subj && t.pred === pred && t.obj === obj);
|
|
48
|
+
if (exists) { exists.ts = new Date().toISOString(); if (meta) exists.meta = { ...exists.meta, ...meta }; return; }
|
|
49
|
+
|
|
50
|
+
this.triples.push({ subj, pred, obj, agent, ts: new Date().toISOString(), meta });
|
|
51
|
+
if (this.triples.length > 5000) this.triples.splice(0, this.triples.length - 5000);
|
|
52
|
+
this.save();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Find all facts about an entity. */
|
|
56
|
+
about(entity: string, limit: number = 20): Triple[] {
|
|
57
|
+
return this.triples.filter(t => t.subj === entity || t.obj === entity).slice(-limit);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Find all subjects matching a predicate. */
|
|
61
|
+
byPredicate(pred: string): Triple[] {
|
|
62
|
+
return this.triples.filter(t => t.pred === pred);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Find all objects for a subject-predicate pair. */
|
|
66
|
+
find(subj: string, pred: string): Triple[] {
|
|
67
|
+
return this.triples.filter(t => t.subj === subj && t.pred === pred);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Transitive expansion: 2-hop from a starting entity. */
|
|
71
|
+
expand(entity: string, maxDepth: number = 2): Triple[] {
|
|
72
|
+
const seen = new Set<Triple>();
|
|
73
|
+
const queue = [entity];
|
|
74
|
+
for (let depth = 0; depth < maxDepth && queue.length > 0; depth++) {
|
|
75
|
+
const current = queue.shift()!;
|
|
76
|
+
const facts = this.about(current, 10);
|
|
77
|
+
for (const f of facts) {
|
|
78
|
+
if (seen.has(f)) continue;
|
|
79
|
+
seen.add(f);
|
|
80
|
+
if (f.subj === current && !queue.includes(f.obj)) queue.push(f.obj);
|
|
81
|
+
if (f.obj === current && !queue.includes(f.subj)) queue.push(f.subj);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Array.from(seen);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Remove a fact. */
|
|
88
|
+
remove(subj: string, pred: string, obj: string): void {
|
|
89
|
+
this.triples = this.triples.filter(t => !(t.subj === subj && t.pred === pred && t.obj === obj));
|
|
90
|
+
this.save();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Search for entities or predicates containing a keyword. */
|
|
94
|
+
search(keyword: string, limit: number = 15): Triple[] {
|
|
95
|
+
const k = keyword.toLowerCase();
|
|
96
|
+
return this.triples.filter(t => t.subj.toLowerCase().includes(k) || t.pred.toLowerCase().includes(k) || t.obj.toLowerCase().includes(k)).slice(-limit);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Format facts as readable text. */
|
|
100
|
+
format(entity?: string): string {
|
|
101
|
+
const facts = entity ? this.about(entity) : this.triples.slice(-30);
|
|
102
|
+
if (facts.length === 0) return "(no facts)";
|
|
103
|
+
const bySubj = new Map<string, string[]>();
|
|
104
|
+
for (const f of facts) {
|
|
105
|
+
if (!bySubj.has(f.subj)) bySubj.set(f.subj, []);
|
|
106
|
+
bySubj.get(f.subj)!.push(`${f.pred} → ${f.obj}`);
|
|
107
|
+
}
|
|
108
|
+
const lines: string[] = [];
|
|
109
|
+
for (const [subj, preds] of bySubj) {
|
|
110
|
+
lines.push(`**${subj}**: ${preds.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get size(): number { return this.triples.length; }
|
|
116
|
+
|
|
117
|
+
private save(): void {
|
|
118
|
+
try {
|
|
119
|
+
const dir = path.dirname(this.indexPath);
|
|
120
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
121
|
+
fs.writeFileSync(this.indexPath, JSON.stringify(this.triples.slice(-2000)), "utf-8");
|
|
122
|
+
} catch (e) { log.warn("kg_save_failed", { error: String(e) }); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private load(): void {
|
|
126
|
+
try {
|
|
127
|
+
if (fs.existsSync(this.indexPath)) {
|
|
128
|
+
this.triples = JSON.parse(fs.readFileSync(this.indexPath, "utf-8"));
|
|
129
|
+
}
|
|
130
|
+
} catch { this.triples = []; }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── Auto-extract facts from conversation ── */
|
|
135
|
+
const RELATION_PATTERNS: Array<[RegExp, string]> = [
|
|
136
|
+
[/(\w+) (?:是|为|属于|使用|用|用到了|采用) (.+?)(?:[。,,.\n]|$)/g, "is"],
|
|
137
|
+
[/(\w+) (?:版本|version|v) (?:是|为)? ?(\d[\d.]*)/gi, "version"],
|
|
138
|
+
[/(\w+) (?:depends|依赖|需要|requires) (\w+)/gi, "depends_on"],
|
|
139
|
+
[/(\w+) (?:config|配置) (?:为|是)? (.+?)(?:[。,,.\n]|$)/gi, "config"],
|
|
140
|
+
[/(\w+) (?:file|path|文件|路径) (?:在|为|at) (.+?)(?:[。,,.\n]|$)/gi, "located_at"],
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export function extractFacts(text: string, agent: string): Array<[string, string, string]> {
|
|
144
|
+
const facts: Array<[string, string, string]> = [];
|
|
145
|
+
for (const [pattern, pred] of RELATION_PATTERNS) {
|
|
146
|
+
let match;
|
|
147
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
148
|
+
const subj = match[1].trim().toLowerCase();
|
|
149
|
+
const obj = match[2].trim();
|
|
150
|
+
if (subj.length >= 2 && obj.length >= 2 && subj !== obj) {
|
|
151
|
+
facts.push([subj, pred, obj]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return facts;
|
|
156
|
+
}
|
package/src/core/icons.ts
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Agent icon system — dynamic status indicators.
|
|
3
|
-
*
|
|
4
|
-
* Each agent is identified by its display name with color styling.
|
|
5
|
-
* During processing, agent spinners provide dynamic status.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as path from 'path';
|
|
9
|
-
|
|
10
|
-
const ICONS_DIR = path.resolve(__dirname, '..', 'assets', 'icons');
|
|
11
|
-
|
|
12
|
-
export const AGENT_COLORS: Record<string, string> = {
|
|
13
|
-
fog: 'bright_white',
|
|
14
|
-
rain: 'blue',
|
|
15
|
-
frost: 'cyan',
|
|
16
|
-
snow: 'bright_white',
|
|
17
|
-
dew: 'green',
|
|
18
|
-
fair: '#FFD700',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const AGENT_EMOJI: Record<string, string> = {
|
|
22
|
-
fog: '≋',
|
|
23
|
-
rain: '⸽',
|
|
24
|
-
frost: '✱',
|
|
25
|
-
snow: '❉',
|
|
26
|
-
dew: '∘',
|
|
27
|
-
fair: '☼',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Return the filesystem path to an agent's SVG icon file.
|
|
32
|
-
*/
|
|
33
|
-
export function svgPath(name: string): string {
|
|
34
|
-
return path.join(ICONS_DIR, `${name}.svg`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Return the plain-text icon for an agent.
|
|
39
|
-
*
|
|
40
|
-
* Used in dashboards, prompts, logs, and any UI where SVG can't render.
|
|
41
|
-
* The glyphs are deliberately chosen from Unicode blocks that render
|
|
42
|
-
* as monochrome text on virtually every terminal.
|
|
43
|
-
*
|
|
44
|
-
* fog ≋ three wavy lines — drifting mist
|
|
45
|
-
* rain ⸽ six vertical dots — falling rain streaks
|
|
46
|
-
* frost ✱ pointed asterisk — frost crystal
|
|
47
|
-
* snow ❉ balloon-spoked star — snowflake
|
|
48
|
-
* dew ∘ ring — dewdrop
|
|
49
|
-
* fair ☼ sun with rays — clear sky
|
|
50
|
-
*/
|
|
51
|
-
export function iconText(name: string): string {
|
|
52
|
-
return AGENT_EMOJI[name] ?? name;
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Agent icon system — dynamic status indicators.
|
|
3
|
+
*
|
|
4
|
+
* Each agent is identified by its display name with color styling.
|
|
5
|
+
* During processing, agent spinners provide dynamic status.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
const ICONS_DIR = path.resolve(__dirname, '..', 'assets', 'icons');
|
|
11
|
+
|
|
12
|
+
export const AGENT_COLORS: Record<string, string> = {
|
|
13
|
+
fog: 'bright_white',
|
|
14
|
+
rain: 'blue',
|
|
15
|
+
frost: 'cyan',
|
|
16
|
+
snow: 'bright_white',
|
|
17
|
+
dew: 'green',
|
|
18
|
+
fair: '#FFD700',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const AGENT_EMOJI: Record<string, string> = {
|
|
22
|
+
fog: '≋',
|
|
23
|
+
rain: '⸽',
|
|
24
|
+
frost: '✱',
|
|
25
|
+
snow: '❉',
|
|
26
|
+
dew: '∘',
|
|
27
|
+
fair: '☼',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return the filesystem path to an agent's SVG icon file.
|
|
32
|
+
*/
|
|
33
|
+
export function svgPath(name: string): string {
|
|
34
|
+
return path.join(ICONS_DIR, `${name}.svg`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return the plain-text icon for an agent.
|
|
39
|
+
*
|
|
40
|
+
* Used in dashboards, prompts, logs, and any UI where SVG can't render.
|
|
41
|
+
* The glyphs are deliberately chosen from Unicode blocks that render
|
|
42
|
+
* as monochrome text on virtually every terminal.
|
|
43
|
+
*
|
|
44
|
+
* fog ≋ three wavy lines — drifting mist
|
|
45
|
+
* rain ⸽ six vertical dots — falling rain streaks
|
|
46
|
+
* frost ✱ pointed asterisk — frost crystal
|
|
47
|
+
* snow ❉ balloon-spoked star — snowflake
|
|
48
|
+
* dew ∘ ring — dewdrop
|
|
49
|
+
* fair ☼ sun with rays — clear sky
|
|
50
|
+
*/
|
|
51
|
+
export function iconText(name: string): string {
|
|
52
|
+
return AGENT_EMOJI[name] ?? name;
|
|
53
|
+
}
|