metame-cli 1.4.33 → 1.5.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/README.md +187 -48
- package/index.js +148 -9
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +254 -9
- package/scripts/daemon-agent-commands.js +64 -6
- package/scripts/daemon-agent-tools.js +26 -5
- package/scripts/daemon-bridges.js +110 -20
- package/scripts/daemon-claude-engine.js +704 -268
- package/scripts/daemon-command-router.js +24 -8
- package/scripts/daemon-default.yaml +28 -4
- package/scripts/daemon-engine-runtime.js +275 -0
- package/scripts/daemon-exec-commands.js +10 -4
- package/scripts/daemon-notify.js +37 -1
- package/scripts/daemon-runtime-lifecycle.js +2 -1
- package/scripts/daemon-session-commands.js +52 -4
- package/scripts/daemon-session-store.js +2 -1
- package/scripts/daemon-task-scheduler.js +87 -28
- package/scripts/daemon-user-acl.js +26 -9
- package/scripts/daemon.js +81 -17
- package/scripts/distill.js +323 -18
- package/scripts/docs/agent-guide.md +12 -0
- package/scripts/docs/maintenance-manual.md +119 -0
- package/scripts/docs/pointer-map.md +88 -0
- package/scripts/feishu-adapter.js +6 -1
- package/scripts/hooks/stop-session-capture.js +243 -0
- package/scripts/memory-extract.js +100 -5
- package/scripts/memory-nightly-reflect.js +196 -11
- package/scripts/memory.js +134 -3
- package/scripts/mentor-engine.js +405 -0
- package/scripts/platform.js +2 -0
- package/scripts/providers.js +169 -21
- package/scripts/schema.js +12 -0
- package/scripts/session-analytics.js +245 -12
- package/scripts/skill-changelog.js +245 -0
- package/scripts/skill-evolution.js +288 -5
- package/scripts/usage-classifier.js +1 -1
- package/scripts/daemon-admin-commands.test.js +0 -333
- package/scripts/daemon-task-envelope.test.js +0 -59
- package/scripts/daemon-task-scheduler.test.js +0 -106
- package/scripts/reliability-core.test.js +0 -280
- package/scripts/skill-evolution.test.js +0 -113
- package/scripts/task-board.test.js +0 -83
- package/scripts/test_daemon.js +0 -1407
- package/scripts/utils.test.js +0 -192
package/scripts/memory.js
CHANGED
|
@@ -122,6 +122,17 @@ function getDb() {
|
|
|
122
122
|
)
|
|
123
123
|
`);
|
|
124
124
|
|
|
125
|
+
// Optional concept label side-table (non-invasive, no ALTER on facts schema)
|
|
126
|
+
_db.exec(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS fact_labels (
|
|
128
|
+
fact_id TEXT NOT NULL REFERENCES facts(id) ON DELETE CASCADE,
|
|
129
|
+
label TEXT NOT NULL,
|
|
130
|
+
domain TEXT,
|
|
131
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
132
|
+
PRIMARY KEY (fact_id, label)
|
|
133
|
+
)
|
|
134
|
+
`);
|
|
135
|
+
|
|
125
136
|
// FTS5 index for facts (separate from sessions_fts, zero compatibility risk)
|
|
126
137
|
try {
|
|
127
138
|
_db.exec(`
|
|
@@ -159,6 +170,8 @@ function getDb() {
|
|
|
159
170
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
|
|
160
171
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch {}
|
|
161
172
|
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
|
|
173
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch {}
|
|
174
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch {}
|
|
162
175
|
|
|
163
176
|
// Backward-compatible migration for old DBs without `scope`
|
|
164
177
|
try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
@@ -369,7 +382,43 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
369
382
|
|
|
370
383
|
if (conflicts > 0) log('WARN', `[MEMORY] ${conflicts} conflict(s) detected`);
|
|
371
384
|
|
|
372
|
-
return { saved, skipped, superseded, conflicts };
|
|
385
|
+
return { saved, skipped, superseded, conflicts, savedFacts };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Save concept labels for facts (side-table).
|
|
390
|
+
*
|
|
391
|
+
* @param {Array<{fact_id:string,label:string,domain?:string}>} rows
|
|
392
|
+
* @returns {{ saved: number, skipped: number }}
|
|
393
|
+
*/
|
|
394
|
+
function saveFactLabels(rows) {
|
|
395
|
+
if (!Array.isArray(rows) || rows.length === 0) return { saved: 0, skipped: 0 };
|
|
396
|
+
const db = getDb();
|
|
397
|
+
const upsert = db.prepare(`
|
|
398
|
+
INSERT INTO fact_labels (fact_id, label, domain)
|
|
399
|
+
VALUES (?, ?, ?)
|
|
400
|
+
ON CONFLICT(fact_id, label) DO UPDATE SET
|
|
401
|
+
domain = COALESCE(excluded.domain, fact_labels.domain)
|
|
402
|
+
`);
|
|
403
|
+
|
|
404
|
+
let saved = 0;
|
|
405
|
+
let skipped = 0;
|
|
406
|
+
for (const row of rows) {
|
|
407
|
+
const factId = String(row && row.fact_id ? row.fact_id : '').trim();
|
|
408
|
+
const label = String(row && row.label ? row.label : '').trim();
|
|
409
|
+
const domainRaw = row && row.domain != null ? String(row.domain).trim() : '';
|
|
410
|
+
const domain = domainRaw || null;
|
|
411
|
+
if (!factId || !label) { skipped++; continue; }
|
|
412
|
+
if (label.length > 60) { skipped++; continue; }
|
|
413
|
+
if (domain && domain.length > 60) { skipped++; continue; }
|
|
414
|
+
try {
|
|
415
|
+
upsert.run(factId, label, domain);
|
|
416
|
+
saved++;
|
|
417
|
+
} catch {
|
|
418
|
+
skipped++;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { saved, skipped };
|
|
373
422
|
}
|
|
374
423
|
|
|
375
424
|
/**
|
|
@@ -613,12 +662,46 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
613
662
|
}
|
|
614
663
|
const ftsResults = db.prepare(sql).all(...params);
|
|
615
664
|
if (ftsResults.length > 0) {
|
|
665
|
+
// Supplement with fact_labels matches (concepts written by memory-extract).
|
|
666
|
+
const ftsIds = new Set(ftsResults.map(r => r.id));
|
|
667
|
+
const remaining = limit - ftsResults.length;
|
|
668
|
+
if (remaining > 0) {
|
|
669
|
+
try {
|
|
670
|
+
const labelLike = '%' + query.trim() + '%';
|
|
671
|
+
let labelSql = `
|
|
672
|
+
SELECT DISTINCT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at
|
|
673
|
+
FROM fact_labels fl JOIN facts f ON f.id = fl.fact_id
|
|
674
|
+
WHERE fl.label LIKE ?
|
|
675
|
+
AND f.superseded_by IS NULL
|
|
676
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
|
|
677
|
+
const labelParams = [labelLike];
|
|
678
|
+
if (scope && project) {
|
|
679
|
+
labelSql += ` AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))`;
|
|
680
|
+
labelParams.push(scope, project);
|
|
681
|
+
} else if (scope) {
|
|
682
|
+
labelSql += ` AND (f.scope = ? OR f.scope = '*')`;
|
|
683
|
+
labelParams.push(scope);
|
|
684
|
+
} else if (project) {
|
|
685
|
+
labelSql += ` AND (f.project = ? OR f.project = '*')`;
|
|
686
|
+
labelParams.push(project);
|
|
687
|
+
}
|
|
688
|
+
labelSql += ` LIMIT ?`;
|
|
689
|
+
labelParams.push(remaining + ftsResults.length);
|
|
690
|
+
const labelRows = db.prepare(labelSql).all(...labelParams);
|
|
691
|
+
for (const row of labelRows) {
|
|
692
|
+
if (!ftsIds.has(row.id) && ftsResults.length < limit) {
|
|
693
|
+
ftsIds.add(row.id);
|
|
694
|
+
ftsResults.push(row);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} catch { /* fact_labels table may not exist yet */ }
|
|
698
|
+
}
|
|
616
699
|
_trackSearch(ftsResults.map(r => r.id));
|
|
617
700
|
return ftsResults;
|
|
618
701
|
}
|
|
619
702
|
} catch { /* FTS error, fall through */ }
|
|
620
703
|
|
|
621
|
-
// LIKE fallback
|
|
704
|
+
// LIKE fallback (also check fact_labels)
|
|
622
705
|
const like = '%' + query.trim() + '%';
|
|
623
706
|
const likeSql = scope && project
|
|
624
707
|
? `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
@@ -651,6 +734,39 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
651
734
|
: project
|
|
652
735
|
? db.prepare(likeSql).all(like, like, like, project, limit)
|
|
653
736
|
: db.prepare(likeSql).all(like, like, like, limit);
|
|
737
|
+
// Supplement LIKE results with fact_labels matches.
|
|
738
|
+
if (likeResults.length < limit) {
|
|
739
|
+
try {
|
|
740
|
+
const labelLike = '%' + query.trim() + '%';
|
|
741
|
+
const likeIds = new Set(likeResults.map(r => r.id));
|
|
742
|
+
let labelSql2 = `
|
|
743
|
+
SELECT DISTINCT f.id, f.entity, f.relation, f.value, f.confidence, f.project, f.scope, f.tags, f.created_at
|
|
744
|
+
FROM fact_labels fl JOIN facts f ON f.id = fl.fact_id
|
|
745
|
+
WHERE fl.label LIKE ?
|
|
746
|
+
AND f.superseded_by IS NULL
|
|
747
|
+
AND (f.conflict_status IS NULL OR f.conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
|
|
748
|
+
const labelParams2 = [labelLike];
|
|
749
|
+
if (scope && project) {
|
|
750
|
+
labelSql2 += ` AND ((f.scope = ? OR f.scope = '*') OR (f.scope IS NULL AND (f.project = ? OR f.project = '*')))`;
|
|
751
|
+
labelParams2.push(scope, project);
|
|
752
|
+
} else if (scope) {
|
|
753
|
+
labelSql2 += ` AND (f.scope = ? OR f.scope = '*')`;
|
|
754
|
+
labelParams2.push(scope);
|
|
755
|
+
} else if (project) {
|
|
756
|
+
labelSql2 += ` AND (f.project = ? OR f.project = '*')`;
|
|
757
|
+
labelParams2.push(project);
|
|
758
|
+
}
|
|
759
|
+
labelSql2 += ` LIMIT ?`;
|
|
760
|
+
labelParams2.push(limit);
|
|
761
|
+
const labelRows = db.prepare(labelSql2).all(...labelParams2);
|
|
762
|
+
for (const row of labelRows) {
|
|
763
|
+
if (!likeIds.has(row.id) && likeResults.length < limit) {
|
|
764
|
+
likeIds.add(row.id);
|
|
765
|
+
likeResults.push(row);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
} catch { /* fact_labels table may not exist yet */ }
|
|
769
|
+
}
|
|
654
770
|
if (likeResults.length > 0) _trackSearch(likeResults.map(r => r.id));
|
|
655
771
|
return likeResults;
|
|
656
772
|
}
|
|
@@ -839,4 +955,19 @@ function forceClose() {
|
|
|
839
955
|
if (_db) { _db.close(); _db = null; }
|
|
840
956
|
}
|
|
841
957
|
|
|
842
|
-
module.exports = {
|
|
958
|
+
module.exports = {
|
|
959
|
+
saveSession,
|
|
960
|
+
saveFacts,
|
|
961
|
+
saveFactLabels,
|
|
962
|
+
searchFacts,
|
|
963
|
+
searchFactsAsync,
|
|
964
|
+
searchSessions,
|
|
965
|
+
recentSessions,
|
|
966
|
+
getSession,
|
|
967
|
+
stats,
|
|
968
|
+
acquire,
|
|
969
|
+
release,
|
|
970
|
+
close,
|
|
971
|
+
forceClose,
|
|
972
|
+
DB_PATH,
|
|
973
|
+
};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_COOLDOWN_MS = 30 * 60 * 1000;
|
|
8
|
+
const FATIGUE_COOLDOWN_MS = 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
function runtimeFilePath() {
|
|
11
|
+
const override = String(process.env.METAME_MENTOR_RUNTIME || '').trim();
|
|
12
|
+
if (override) return override;
|
|
13
|
+
return path.join(os.homedir(), '.metame', 'mentor_runtime.json');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defaultRuntime() {
|
|
17
|
+
return {
|
|
18
|
+
emotion_breaker_until: null,
|
|
19
|
+
debts: [],
|
|
20
|
+
last_fatigue_alert: null,
|
|
21
|
+
last_pattern_check: null,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeNow(nowMs) {
|
|
26
|
+
return Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureParentDir(file) {
|
|
30
|
+
const dir = path.dirname(file);
|
|
31
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadRuntime() {
|
|
35
|
+
const file = runtimeFilePath();
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(file)) return defaultRuntime();
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
+
return {
|
|
40
|
+
...defaultRuntime(),
|
|
41
|
+
...(data && typeof data === 'object' ? data : {}),
|
|
42
|
+
debts: Array.isArray(data && data.debts) ? data.debts : [],
|
|
43
|
+
};
|
|
44
|
+
} catch {
|
|
45
|
+
return defaultRuntime();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveRuntime(runtime) {
|
|
50
|
+
const file = runtimeFilePath();
|
|
51
|
+
ensureParentDir(file);
|
|
52
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(runtime, null, 2), 'utf8');
|
|
54
|
+
fs.renameSync(tmp, file);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeText(input) {
|
|
58
|
+
return String(input || '').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function tokenize(text) {
|
|
62
|
+
const input = normalizeText(text).toLowerCase();
|
|
63
|
+
if (!input) return [];
|
|
64
|
+
const out = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
const push = (t) => {
|
|
67
|
+
const v = String(t || '').trim();
|
|
68
|
+
if (!v || seen.has(v)) return;
|
|
69
|
+
seen.add(v);
|
|
70
|
+
out.push(v);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ascii = input.match(/[a-z0-9_./-]{2,}/g) || [];
|
|
74
|
+
for (const t of ascii) push(t);
|
|
75
|
+
|
|
76
|
+
const hanRuns = input.match(/[\u4e00-\u9fff]{2,}/g) || [];
|
|
77
|
+
for (const run of hanRuns) {
|
|
78
|
+
if (run.length === 2) push(run);
|
|
79
|
+
else {
|
|
80
|
+
for (let i = 0; i < run.length - 1; i++) push(run.slice(i, i + 2));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function overlapRatio(a, b) {
|
|
87
|
+
const sa = new Set(Array.isArray(a) ? a : []);
|
|
88
|
+
const sb = new Set(Array.isArray(b) ? b : []);
|
|
89
|
+
if (!sa.size || !sb.size) return 0;
|
|
90
|
+
let common = 0;
|
|
91
|
+
for (const x of sa) if (sb.has(x)) common++;
|
|
92
|
+
const base = Math.min(sa.size, sb.size);
|
|
93
|
+
return base > 0 ? common / base : 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function endOfTodayMs(nowMs) {
|
|
97
|
+
const d = new Date(safeNow(nowMs));
|
|
98
|
+
d.setHours(23, 59, 59, 999);
|
|
99
|
+
return d.getTime();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveMode(config = {}) {
|
|
103
|
+
const mode = String(config.mode || '').toLowerCase().trim();
|
|
104
|
+
if (mode === 'gentle' || mode === 'active' || mode === 'intense') return mode;
|
|
105
|
+
const level = Number(config.friction_level);
|
|
106
|
+
if (Number.isFinite(level)) {
|
|
107
|
+
if (level >= 8) return 'intense';
|
|
108
|
+
if (level >= 4) return 'active';
|
|
109
|
+
}
|
|
110
|
+
return 'gentle';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkEmotionBreaker(userMessage, config = {}, nowMs = Date.now()) {
|
|
114
|
+
const text = normalizeText(userMessage);
|
|
115
|
+
const runtime = loadRuntime();
|
|
116
|
+
const now = safeNow(nowMs);
|
|
117
|
+
const until = Number(runtime.emotion_breaker_until || 0);
|
|
118
|
+
|
|
119
|
+
if (until > now) {
|
|
120
|
+
return {
|
|
121
|
+
tripped: true,
|
|
122
|
+
reason: 'cooldown_active',
|
|
123
|
+
response: '已暂停导师模式,先专注把问题解决。',
|
|
124
|
+
remaining_ms: until - now,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const baseRe = /[操草靠妈tmd]|fuck|shit|wtf|!!{2,}|??{2,}|急|崩|炸|烦死/i;
|
|
129
|
+
const extras = Array.isArray(config.emotion_keywords_extra) ? config.emotion_keywords_extra : [];
|
|
130
|
+
const hitExtra = extras.find(k => k && text.toLowerCase().includes(String(k).toLowerCase()));
|
|
131
|
+
const hit = baseRe.test(text) || !!hitExtra;
|
|
132
|
+
if (!hit) return { tripped: false };
|
|
133
|
+
|
|
134
|
+
runtime.emotion_breaker_until = now + DEFAULT_COOLDOWN_MS;
|
|
135
|
+
saveRuntime(runtime);
|
|
136
|
+
return {
|
|
137
|
+
tripped: true,
|
|
138
|
+
reason: hitExtra ? `keyword:${hitExtra}` : 'emotion_keyword',
|
|
139
|
+
response: '已暂停导师模式,先专注把问题解决。',
|
|
140
|
+
remaining_ms: DEFAULT_COOLDOWN_MS,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function computeZone(skeleton = {}) {
|
|
145
|
+
const toolErrors = Number(skeleton.tool_error_count || 0);
|
|
146
|
+
const retries = Number(skeleton.retry_sequences || 0);
|
|
147
|
+
const repetition = Number(skeleton.semantic_repetition || 0);
|
|
148
|
+
const durationMin = Number(skeleton.duration_min || 0);
|
|
149
|
+
const toolCalls = Number(skeleton.total_tool_calls || 0);
|
|
150
|
+
const avgPause = Number(skeleton.avg_pause_sec || 0);
|
|
151
|
+
const recovered = !!skeleton.error_recovered;
|
|
152
|
+
|
|
153
|
+
let panicScore = 0;
|
|
154
|
+
if (toolErrors >= 3) panicScore++;
|
|
155
|
+
if (retries >= 6) panicScore++;
|
|
156
|
+
if (repetition >= 0.6) panicScore++;
|
|
157
|
+
if (durationMin >= 75 && toolErrors >= 1) panicScore++;
|
|
158
|
+
if (avgPause >= 180) panicScore++;
|
|
159
|
+
|
|
160
|
+
let comfortScore = 0;
|
|
161
|
+
if (toolErrors === 0) comfortScore++;
|
|
162
|
+
if (retries <= 2) comfortScore++;
|
|
163
|
+
if (repetition < 0.35) comfortScore++;
|
|
164
|
+
if (toolCalls >= 3 && durationMin <= 45) comfortScore++;
|
|
165
|
+
if (recovered && toolErrors <= 1) comfortScore++;
|
|
166
|
+
|
|
167
|
+
let zone = 'stretch';
|
|
168
|
+
let dominant = Math.max(panicScore, comfortScore);
|
|
169
|
+
if (panicScore >= 2) zone = 'panic';
|
|
170
|
+
else if (comfortScore >= 3 && panicScore === 0) zone = 'comfort';
|
|
171
|
+
|
|
172
|
+
const confidence = Math.min(0.95, 0.6 + dominant * 0.08);
|
|
173
|
+
return {
|
|
174
|
+
zone,
|
|
175
|
+
confidence: Number(confidence.toFixed(2)),
|
|
176
|
+
signals: {
|
|
177
|
+
tool_error_count: toolErrors,
|
|
178
|
+
retry_sequences: retries,
|
|
179
|
+
semantic_repetition: repetition,
|
|
180
|
+
duration_min: durationMin,
|
|
181
|
+
tool_calls: toolCalls,
|
|
182
|
+
avg_pause_sec: avgPause,
|
|
183
|
+
error_recovered: recovered,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function registerDebt(projectId, topic, codeLineCount, nowMs = Date.now()) {
|
|
189
|
+
const pid = normalizeText(projectId);
|
|
190
|
+
const lines = Number(codeLineCount || 0);
|
|
191
|
+
if (!pid || lines <= 30) return null;
|
|
192
|
+
|
|
193
|
+
const t = normalizeText(topic) || 'unknown-topic';
|
|
194
|
+
const now = safeNow(nowMs);
|
|
195
|
+
const runtime = loadRuntime();
|
|
196
|
+
const topicKeywords = tokenize(t).slice(0, 8);
|
|
197
|
+
|
|
198
|
+
const debt = {
|
|
199
|
+
project_id: pid,
|
|
200
|
+
topic: t,
|
|
201
|
+
topic_keywords: topicKeywords,
|
|
202
|
+
code_summary: `Generated ${lines} lines`,
|
|
203
|
+
recorded_at: now,
|
|
204
|
+
expires_at: endOfTodayMs(now),
|
|
205
|
+
};
|
|
206
|
+
runtime.debts.push(debt);
|
|
207
|
+
if (runtime.debts.length > 100) runtime.debts = runtime.debts.slice(-100);
|
|
208
|
+
saveRuntime(runtime);
|
|
209
|
+
return debt;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function collectDebt(projectId, currentTopic, nowMs = Date.now()) {
|
|
213
|
+
const pid = normalizeText(projectId);
|
|
214
|
+
if (!pid) return null;
|
|
215
|
+
const runtime = loadRuntime();
|
|
216
|
+
const now = safeNow(nowMs);
|
|
217
|
+
|
|
218
|
+
const valid = [];
|
|
219
|
+
let matched = null;
|
|
220
|
+
const currentKeywords = tokenize(currentTopic).slice(0, 12);
|
|
221
|
+
|
|
222
|
+
for (const debt of runtime.debts) {
|
|
223
|
+
if (!debt || typeof debt !== 'object') continue;
|
|
224
|
+
if (Number(debt.expires_at || 0) < now) continue;
|
|
225
|
+
|
|
226
|
+
if (!matched && debt.project_id === pid) {
|
|
227
|
+
const ratio = overlapRatio(currentKeywords, debt.topic_keywords || []);
|
|
228
|
+
if (ratio > 0.3) {
|
|
229
|
+
matched = {
|
|
230
|
+
...debt,
|
|
231
|
+
overlap_ratio: Number(ratio.toFixed(2)),
|
|
232
|
+
prompt: `刚才那段 ${debt.topic} 的代码,核心逻辑是什么?`,
|
|
233
|
+
};
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
valid.push(debt);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
runtime.debts = valid;
|
|
241
|
+
saveRuntime(runtime);
|
|
242
|
+
return matched;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function gcExpiredDebts(nowMs = Date.now()) {
|
|
246
|
+
const runtime = loadRuntime();
|
|
247
|
+
const now = safeNow(nowMs);
|
|
248
|
+
const before = runtime.debts.length;
|
|
249
|
+
runtime.debts = runtime.debts.filter(d => d && Number(d.expires_at || 0) >= now);
|
|
250
|
+
const removed = before - runtime.debts.length;
|
|
251
|
+
if (removed > 0) saveRuntime(runtime);
|
|
252
|
+
return { removed, remaining: runtime.debts.length };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function repetitionFromTexts(texts) {
|
|
256
|
+
if (!Array.isArray(texts) || texts.length < 3) return 0;
|
|
257
|
+
let maxOverlap = 0;
|
|
258
|
+
for (let i = 2; i < texts.length; i++) {
|
|
259
|
+
const a = new Set(tokenize(texts[i - 2]));
|
|
260
|
+
const b = new Set(tokenize(texts[i - 1]));
|
|
261
|
+
const c = new Set(tokenize(texts[i]));
|
|
262
|
+
const union = new Set([...a, ...b, ...c]);
|
|
263
|
+
if (!union.size) continue;
|
|
264
|
+
let common = 0;
|
|
265
|
+
for (const t of a) if (b.has(t) && c.has(t)) common++;
|
|
266
|
+
maxOverlap = Math.max(maxOverlap, common / union.size);
|
|
267
|
+
}
|
|
268
|
+
return Number(maxOverlap.toFixed(3));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function extractErrorClass(text) {
|
|
272
|
+
const src = normalizeText(text).toLowerCase();
|
|
273
|
+
if (!src) return '';
|
|
274
|
+
if (/(timeout|timed out|超时)/.test(src)) return 'timeout';
|
|
275
|
+
if (/(permission|eacces|denied|权限)/.test(src)) return 'permission';
|
|
276
|
+
if (/(not found|enoent|找不到)/.test(src)) return 'not_found';
|
|
277
|
+
if (/(typeerror|referenceerror|syntaxerror|报错|异常|error)/.test(src)) return 'runtime_error';
|
|
278
|
+
return '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function detectPatterns(recentMessages, sessionStartTime, opts = {}) {
|
|
282
|
+
const now = safeNow(opts.nowMs);
|
|
283
|
+
const runtime = loadRuntime();
|
|
284
|
+
let dirty = false;
|
|
285
|
+
const prevPatternTs = Number(runtime.last_pattern_check || 0);
|
|
286
|
+
if (!prevPatternTs || (now - prevPatternTs) > 30000) {
|
|
287
|
+
runtime.last_pattern_check = now;
|
|
288
|
+
dirty = true;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const normalized = (Array.isArray(recentMessages) ? recentMessages : [])
|
|
292
|
+
.map(m => (typeof m === 'string' ? { text: m } : (m && typeof m === 'object' ? m : null)))
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
|
|
295
|
+
const texts = normalized.map(m => normalizeText(m.text || m.message || ''));
|
|
296
|
+
const shortCount = texts.filter(t => t && t.length < 20).length;
|
|
297
|
+
const toolCalls = normalized.reduce((acc, m) => acc + (Number(m.tool_calls || m.toolCalls || 0) || 0), 0);
|
|
298
|
+
const errorClasses = normalized.map(m => extractErrorClass(m.text || m.message || '')).filter(Boolean);
|
|
299
|
+
const classCount = new Map();
|
|
300
|
+
for (const c of errorClasses) classCount.set(c, (classCount.get(c) || 0) + 1);
|
|
301
|
+
const repeatedError = [...classCount.values()].some(v => v >= 3);
|
|
302
|
+
const semanticRepetition = repetitionFromTexts(texts);
|
|
303
|
+
|
|
304
|
+
const autopilot = shortCount >= 3 && toolCalls >= 3 && errorClasses.length === 0;
|
|
305
|
+
const stuck = repeatedError && semanticRepetition > 0.6;
|
|
306
|
+
|
|
307
|
+
const sessionMs = Math.max(0, now - safeNow(new Date(sessionStartTime).getTime()));
|
|
308
|
+
const isFatiguedRaw = sessionMs > 90 * 60 * 1000;
|
|
309
|
+
const lastFatigue = Number(runtime.last_fatigue_alert || 0);
|
|
310
|
+
const fatigued = isFatiguedRaw && (!lastFatigue || (now - lastFatigue) > FATIGUE_COOLDOWN_MS);
|
|
311
|
+
if (fatigued) {
|
|
312
|
+
runtime.last_fatigue_alert = now;
|
|
313
|
+
dirty = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let suggestion = '';
|
|
317
|
+
if (stuck) suggestion = '检测到你在反复遇到类似问题,建议先退一步梳理整体思路。';
|
|
318
|
+
else if (fatigued) suggestion = '你已连续工作较久,建议短暂休息后再继续。';
|
|
319
|
+
else if (autopilot) suggestion = '你在高效执行模式,建议确认一下当前方向是否仍然正确。';
|
|
320
|
+
|
|
321
|
+
if (dirty) saveRuntime(runtime);
|
|
322
|
+
return { autopilot, stuck, fatigued, suggestion, semantic_repetition: semanticRepetition };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getRuntimeStatus(nowMs = Date.now()) {
|
|
326
|
+
const runtime = loadRuntime();
|
|
327
|
+
const now = safeNow(nowMs);
|
|
328
|
+
const until = Number(runtime.emotion_breaker_until || 0);
|
|
329
|
+
return {
|
|
330
|
+
debt_count: Array.isArray(runtime.debts) ? runtime.debts.length : 0,
|
|
331
|
+
cooldown_until: until || null,
|
|
332
|
+
cooldown_remaining_ms: until > now ? (until - now) : 0,
|
|
333
|
+
last_fatigue_alert: runtime.last_fatigue_alert || null,
|
|
334
|
+
last_pattern_check: runtime.last_pattern_check || null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function shouldSkipByCompetence(profile, sessionState = {}) {
|
|
339
|
+
const map = profile && typeof profile.user_competence_map === 'object'
|
|
340
|
+
? profile.user_competence_map
|
|
341
|
+
: null;
|
|
342
|
+
if (!map) return false;
|
|
343
|
+
const text = `${sessionState.topic || ''} ${sessionState.currentTopic || ''} ${sessionState.lastUserMessage || ''}`.toLowerCase();
|
|
344
|
+
if (!text) return false;
|
|
345
|
+
for (const [domain, level] of Object.entries(map)) {
|
|
346
|
+
if (String(level || '').toLowerCase() !== 'expert') continue;
|
|
347
|
+
if (text.includes(String(domain || '').toLowerCase())) return true;
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function buildMentorPrompt(sessionState = {}, profile = {}, config = {}, nowMs = Date.now()) {
|
|
353
|
+
if (!config || config.enabled === false) return '';
|
|
354
|
+
const now = safeNow(nowMs);
|
|
355
|
+
|
|
356
|
+
const quietUntil = profile && profile.growth ? profile.growth.quiet_until : null;
|
|
357
|
+
const quietMs = quietUntil ? new Date(quietUntil).getTime() : 0;
|
|
358
|
+
if (quietMs && quietMs > now) return '';
|
|
359
|
+
if (shouldSkipByCompetence(profile, sessionState)) return '';
|
|
360
|
+
|
|
361
|
+
const mode = resolveMode(config);
|
|
362
|
+
const zone = sessionState.zone || computeZone(sessionState.skeleton || {}).zone;
|
|
363
|
+
const lines = [];
|
|
364
|
+
lines.push('[Mentor mode protocol - keep concise and practical:');
|
|
365
|
+
lines.push(`- mode=${mode}, zone=${zone}`);
|
|
366
|
+
|
|
367
|
+
if (mode === 'gentle') {
|
|
368
|
+
lines.push('- Before solution, optionally ask one short guiding question.');
|
|
369
|
+
} else if (mode === 'active') {
|
|
370
|
+
lines.push('- Ask user for their design idea first, then provide improvements.');
|
|
371
|
+
lines.push('- End with one-line "关键收获".');
|
|
372
|
+
} else {
|
|
373
|
+
lines.push('- Prefer scaffold/pseudocode first; avoid dumping full solution immediately.');
|
|
374
|
+
lines.push('- Use Socratic prompts to force active reasoning.');
|
|
375
|
+
lines.push('- Apply knowledge firewall: do not fill user logic gaps with unstated assumptions.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (zone === 'comfort') lines.push('- Increase challenge slightly (new method or stronger abstraction).');
|
|
379
|
+
if (zone === 'panic') lines.push('- Reduce friction: provide step-by-step scaffold and reassurance.');
|
|
380
|
+
|
|
381
|
+
const pattern = detectPatterns(sessionState.recentMessages || [], sessionState.sessionStartTime || Date.now(), { nowMs: now });
|
|
382
|
+
if (pattern.suggestion) lines.push(`- Pattern nudge: ${pattern.suggestion}`);
|
|
383
|
+
|
|
384
|
+
lines.push(']');
|
|
385
|
+
return lines.join('\n');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
module.exports = {
|
|
389
|
+
checkEmotionBreaker,
|
|
390
|
+
buildMentorPrompt,
|
|
391
|
+
computeZone,
|
|
392
|
+
registerDebt,
|
|
393
|
+
collectDebt,
|
|
394
|
+
gcExpiredDebts,
|
|
395
|
+
detectPatterns,
|
|
396
|
+
getRuntimeStatus,
|
|
397
|
+
_private: {
|
|
398
|
+
runtimeFilePath,
|
|
399
|
+
loadRuntime,
|
|
400
|
+
saveRuntime,
|
|
401
|
+
tokenize,
|
|
402
|
+
overlapRatio,
|
|
403
|
+
repetitionFromTexts,
|
|
404
|
+
},
|
|
405
|
+
};
|
package/scripts/platform.js
CHANGED
|
@@ -121,6 +121,7 @@ const _icons = IS_WIN ? {
|
|
|
121
121
|
phone: '[TEL]',
|
|
122
122
|
feishu: '[FS]',
|
|
123
123
|
check: '[v]',
|
|
124
|
+
tool: '[T]',
|
|
124
125
|
} : {
|
|
125
126
|
ok: '\u2705', // ✅
|
|
126
127
|
fail: '\u274C', // ❌
|
|
@@ -149,6 +150,7 @@ const _icons = IS_WIN ? {
|
|
|
149
150
|
phone: '\uD83D\uDCF1', // 📱
|
|
150
151
|
feishu: '\uD83D\uDCD8', // 📘
|
|
151
152
|
check: '\u2714', // ✔
|
|
153
|
+
tool: '\uD83D\uDEE0\uFE0F', // 🛠️
|
|
152
154
|
};
|
|
153
155
|
|
|
154
156
|
/**
|