memory-eternal 0.17.0 → 0.18.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/.claude-plugin/plugin.json +27 -27
- package/.codex-plugin/plugin.json +24 -24
- package/.cursor-plugin/plugin.json +14 -14
- package/README.md +138 -138
- package/README.zh-CN.md +138 -138
- package/cursor-extension/package.json +54 -54
- package/dsh-tui-plugin/dsh-plugin.json +42 -42
- package/dsh-tui-plugin/package.json +51 -51
- package/index.js +60 -8
- package/lib/api.js +48 -6
- package/lib/capture.js +37 -2
- package/lib/client.js +33 -21
- package/lib/core-memory.js +45 -0
- package/lib/db.js +2 -0
- package/lib/setup.js +132 -33
- package/lib/vault.js +20 -0
- package/orca-plugin/orca-plugin.json +36 -36
- package/package.json +2 -2
- package/web/app.js +38 -26
package/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { migrateFromMarkdown, setAuditConfig, backupDb } from './lib/db.js'
|
|
|
31
31
|
import { evaluateStall, MAX_STAMPS } from './lib/stall.js'
|
|
32
32
|
import { resolveVaultDir, currentWorkspace } from './lib/vault-resolve.js'
|
|
33
33
|
import { createHub } from './lib/sse.js'
|
|
34
|
-
import { summarizeTurn, summarizeTurnDetailed, routeCandidates, sessionRouteFrom, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors, hasUsableContent, deriveTitle, looksTruncated, maxTokenLadder, DEFAULT_CAPTURE_MAX_TOKENS, MAX_CAPTURE_MAX_TOKENS } from './lib/capture.js'
|
|
34
|
+
import { summarizeTurn, summarizeTurnDetailed, routeCandidates, sessionRouteFrom, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors, hasUsableContent, deriveTitle, looksTruncated, maxTokenLadder, guessRawKind, DEFAULT_CAPTURE_MAX_TOKENS, MAX_CAPTURE_MAX_TOKENS } from './lib/capture.js'
|
|
35
35
|
import { createApi, json, encodeBody, crossOriginError, emptyNumericFields } from './lib/api.js'
|
|
36
36
|
import { appendCaptureLog, readCaptureLog, rotateCaptureLog } from './lib/capture-log.js'
|
|
37
37
|
import { recordLedger } from './lib/ledger.js'
|
|
@@ -54,6 +54,10 @@ export const Config = z.object({
|
|
|
54
54
|
enabled: z.boolean().default(true),
|
|
55
55
|
autoCapture: z.boolean().default(true),
|
|
56
56
|
autoRecall: z.boolean().default(true),
|
|
57
|
+
// 核心记忆:人工批准的核心卡,每次会话强制注入 systemPrompt(不依赖 autoRecall / 工具调用)
|
|
58
|
+
coreMemoryEnabled: z.boolean().default(true),
|
|
59
|
+
coreMemoryLimit: z.number().min(1).max(20).default(6),
|
|
60
|
+
coreMemoryMaxChars: z.number().min(200).max(10000).default(2000),
|
|
57
61
|
vaultDir: z.string().default(''),
|
|
58
62
|
dedupThreshold: z.number().min(0).max(1).default(0.62),
|
|
59
63
|
captureMinChars: z.number().default(200),
|
|
@@ -634,6 +638,8 @@ export function apply(ctx, config) {
|
|
|
634
638
|
|
|
635
639
|
// 自动审核:根据配置决定新卡 status(免审→approved,否则 pending)
|
|
636
640
|
const resolveAuditStatus = (cfg, kind, submittedBy) => {
|
|
641
|
+
// 核心记忆必须人工审核:自动沉淀/手动写入都先待审,避免一次误判污染每个会话。
|
|
642
|
+
if (kind === 'core') return 'pending'
|
|
637
643
|
if (cfg.auditMode === 'none') return 'approved'
|
|
638
644
|
const agents = Array.isArray(cfg.auditExemptAgents) ? cfg.auditExemptAgents : []
|
|
639
645
|
const kinds = Array.isArray(cfg.auditExemptKinds) ? cfg.auditExemptKinds : []
|
|
@@ -684,18 +690,19 @@ export function apply(ctx, config) {
|
|
|
684
690
|
const source = DSH_AGENT
|
|
685
691
|
if (cfg.distillEnabled === false || !llm) {
|
|
686
692
|
const rawTitle = deriveTitle(text)
|
|
693
|
+
const rawKind = guessRawKind({ title: rawTitle, body: text })
|
|
687
694
|
const out = await captureCard(root, {
|
|
688
|
-
kind:
|
|
695
|
+
kind: rawKind,
|
|
689
696
|
title: rawTitle.length >= 6 && !looksTruncated(rawTitle) ? rawTitle : '对话原文记录',
|
|
690
697
|
tags: ['raw'],
|
|
691
698
|
body: text,
|
|
692
699
|
source,
|
|
693
|
-
status: resolveAuditStatus(cfg,
|
|
700
|
+
status: resolveAuditStatus(cfg, rawKind, source || 'unknown'),
|
|
694
701
|
submittedBy: source || 'unknown',
|
|
695
702
|
severity: 'info',
|
|
696
703
|
reason: 'AI 自动沉淀(原文卡)',
|
|
697
704
|
}, { threshold: cfg.dedupThreshold })
|
|
698
|
-
if (out.ok) { countWrite(); logCapture(sessionId, 'created', '原文卡(蒸馏已关闭)', { path: out.path ?? out.rel, kind:
|
|
705
|
+
if (out.ok) { countWrite(); logCapture(sessionId, 'created', '原文卡(蒸馏已关闭)', { path: out.path ?? out.rel, kind: rawKind }) }
|
|
699
706
|
else if (out.duplicate) { countWrite(); logCapture(sessionId, 'appended', '与已有卡重复 → 追加更新记录', { path: out.duplicate.path }) }
|
|
700
707
|
else logCapture(sessionId, 'fail', `写卡失败:${out.reason || '未知原因'}`)
|
|
701
708
|
return
|
|
@@ -806,20 +813,21 @@ export function apply(ctx, config) {
|
|
|
806
813
|
if (!result) {
|
|
807
814
|
// 蒸馏失败不能让内容白丢:退成原文卡(与「关闭蒸馏」同一条降级路径)。
|
|
808
815
|
const rawTitle = deriveTitle(text)
|
|
816
|
+
const rawKind = guessRawKind({ title: rawTitle, body: text })
|
|
809
817
|
const raw = await captureCard(root, {
|
|
810
|
-
kind:
|
|
818
|
+
kind: rawKind,
|
|
811
819
|
title: rawTitle.length >= 6 && !looksTruncated(rawTitle) ? rawTitle : '对话原文记录',
|
|
812
820
|
// 蒸馏失败产出的原文卡与被批准的噪声卡是审核中心的主要污染源(#15 评论):
|
|
813
821
|
// 打上独立 tag,便于按 tag 过滤 / 批量处理,不再和正常卡混作一团。
|
|
814
822
|
tags: ['raw', 'distill-failed'],
|
|
815
823
|
body: text,
|
|
816
824
|
source,
|
|
817
|
-
status: resolveAuditStatus(cfg,
|
|
825
|
+
status: resolveAuditStatus(cfg, rawKind, source),
|
|
818
826
|
submittedBy: source,
|
|
819
827
|
severity: 'info',
|
|
820
828
|
reason: `AI 自动沉淀(蒸馏无输出 → 原文卡兜底;主因:${why})`,
|
|
821
829
|
}, { threshold: cfg.dedupThreshold })
|
|
822
|
-
if (raw.ok) { countWrite(); logCapture(sessionId, 'created', `蒸馏失败(${why})→ 原文卡兜底`, { path: raw.path ?? raw.rel, kind:
|
|
830
|
+
if (raw.ok) { countWrite(); logCapture(sessionId, 'created', `蒸馏失败(${why})→ 原文卡兜底`, { path: raw.path ?? raw.rel, kind: rawKind, model: route.model }) }
|
|
823
831
|
else if (raw.duplicate) { countWrite(); logCapture(sessionId, 'appended', '蒸馏无输出 + 与已有卡重复 → 追加更新', { path: raw.duplicate.path, model: route.model }) }
|
|
824
832
|
else logCapture(sessionId, 'fail', `蒸馏无输出,且兜底原文卡也失败:${raw.reason || '未知原因'}`, { model: route.model })
|
|
825
833
|
return
|
|
@@ -966,6 +974,50 @@ export function apply(ctx, config) {
|
|
|
966
974
|
}
|
|
967
975
|
}, 'memory-eternal: recall section')
|
|
968
976
|
|
|
977
|
+
// -- 2b. 核心记忆:每次会话强制注入(不依赖 autoRecall / 工具调用) ---------
|
|
978
|
+
ctx.effect(() => {
|
|
979
|
+
let disposeSection = null
|
|
980
|
+
let timer = null
|
|
981
|
+
let alive = true
|
|
982
|
+
let lastText = ''
|
|
983
|
+
const clearSection = () => {
|
|
984
|
+
if (!disposeSection) return
|
|
985
|
+
const dispose = disposeSection
|
|
986
|
+
disposeSection = null
|
|
987
|
+
dispose()
|
|
988
|
+
}
|
|
989
|
+
const refresh = async () => {
|
|
990
|
+
try {
|
|
991
|
+
const cfg = settings.get() ?? {}
|
|
992
|
+
if (cfg.enabled === false || cfg.coreMemoryEnabled === false) { clearSection(); lastText = ''; return }
|
|
993
|
+
const { coreMemoryPrompt } = await import('./lib/core-memory.js')
|
|
994
|
+
const out = await coreMemoryPrompt(vaultDir(), {
|
|
995
|
+
enabled: true,
|
|
996
|
+
limit: cfg.coreMemoryLimit,
|
|
997
|
+
maxChars: cfg.coreMemoryMaxChars,
|
|
998
|
+
})
|
|
999
|
+
if (!alive) return
|
|
1000
|
+
if (!out.text) { clearSection(); lastText = ''; return }
|
|
1001
|
+
if (out.text === lastText) return
|
|
1002
|
+
lastText = out.text
|
|
1003
|
+
clearSection()
|
|
1004
|
+
disposeSection = ctx.systemPrompt.section({
|
|
1005
|
+
name: 'memory-eternal: core-memory',
|
|
1006
|
+
order: 120,
|
|
1007
|
+
text: out.text,
|
|
1008
|
+
})
|
|
1009
|
+
} catch { /* 核心记忆失败不能影响普通对话 */ }
|
|
1010
|
+
}
|
|
1011
|
+
refresh()
|
|
1012
|
+
timer = setInterval(refresh, 5000)
|
|
1013
|
+
if (timer && typeof timer.unref === 'function') timer.unref()
|
|
1014
|
+
return () => {
|
|
1015
|
+
alive = false
|
|
1016
|
+
if (timer) clearInterval(timer)
|
|
1017
|
+
clearSection()
|
|
1018
|
+
}
|
|
1019
|
+
}, 'memory-eternal: core memory section')
|
|
1020
|
+
|
|
969
1021
|
const tools = ctx.get('tools')
|
|
970
1022
|
if (tools !== undefined) {
|
|
971
1023
|
tools.register(defineTool({
|
|
@@ -1220,7 +1272,7 @@ export function apply(ctx, config) {
|
|
|
1220
1272
|
const cfg = settings.get() ?? {}
|
|
1221
1273
|
// 只暴露可安全展示/回填的字段
|
|
1222
1274
|
const safe = {
|
|
1223
|
-
autoCapture: cfg.autoCapture, autoRecall: cfg.autoRecall, recallLimit: cfg.recallLimit, recallSummaryLen: cfg.recallSummaryLen, recallIncludeBody: cfg.recallIncludeBody, secretHint: cfg.secretHint || '',
|
|
1275
|
+
autoCapture: cfg.autoCapture, autoRecall: cfg.autoRecall, coreMemoryEnabled: cfg.coreMemoryEnabled, coreMemoryLimit: cfg.coreMemoryLimit, coreMemoryMaxChars: cfg.coreMemoryMaxChars, recallLimit: cfg.recallLimit, recallSummaryLen: cfg.recallSummaryLen, recallIncludeBody: cfg.recallIncludeBody, secretHint: cfg.secretHint || '',
|
|
1224
1276
|
captureMinChars: cfg.captureMinChars, captureCooldownMs: cfg.captureCooldownMs, dedupThreshold: cfg.dedupThreshold, maxCardsPerDay: cfg.maxCardsPerDay,
|
|
1225
1277
|
distillEnabled: cfg.distillEnabled, dedupByLLM: cfg.dedupByLLM, captureMaxTokens: cfg.captureMaxTokens, recallMinScore: cfg.recallMinScore,
|
|
1226
1278
|
autoWeb: cfg.autoWeb, autoWebMode: cfg.autoWebMode, webPort: cfg.webPort, webCheckIntervalMs: cfg.webCheckIntervalMs, webMaxRestart: cfg.webMaxRestart, watchdogAutoSpawn: cfg.watchdogAutoSpawn, autoRestartOnDrift: cfg.autoRestartOnDrift, autoMcpSetup: cfg.autoMcpSetup,
|
package/lib/api.js
CHANGED
|
@@ -16,8 +16,8 @@ import os from 'node:os'
|
|
|
16
16
|
import fs, { readFileSync } from 'node:fs'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import { spawn } from 'node:child_process'
|
|
19
|
-
import { listCards, countCards, readCard, search, searchAll, graph, graphAll, overview, exportCards, deleteCard, writeCard, parseCard, stats, optimizeCandidates, optimizeApply, addFeedback, dailyCounts, mergeCards, ensureVault, auditQueue, recycleList, purgeExpired, setCardStatus, restoreCard, mainStoreBodies } from './vault.js'
|
|
20
|
-
import { compressExcerpt, DEFAULT_CAPTURE_MAX_TOKENS } from './capture.js'
|
|
19
|
+
import { listCards, countCards, readCard, search, searchAll, graph, graphAll, overview, exportCards, deleteCard, writeCard, parseCard, stats, optimizeCandidates, optimizeApply, addFeedback, dailyCounts, mergeCards, ensureVault, auditQueue, recycleList, purgeExpired, setCardStatus, setCardKind, restoreCard, mainStoreBodies } from './vault.js'
|
|
20
|
+
import { compressExcerpt, guessRawKind, DEFAULT_CAPTURE_MAX_TOKENS } from './capture.js'
|
|
21
21
|
import { buildDiagnostics } from './feedback.js'
|
|
22
22
|
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
23
23
|
import { ledgerSummary } from './ledger.js'
|
|
@@ -625,17 +625,44 @@ export function createApi(deps = {}) {
|
|
|
625
625
|
json(res, 200, { ok: true, soft: !permanent })
|
|
626
626
|
return
|
|
627
627
|
}
|
|
628
|
+
case '/reclassify': {
|
|
629
|
+
// 重新识别待审原文卡的类型:修复旧版本「自动沉淀全是 content」的历史数据。
|
|
630
|
+
// 只处理带 raw / 原文卡标记的待审卡,不动人工创建的内容。
|
|
631
|
+
const dryRun = query.get('dry') === '1'
|
|
632
|
+
const { getDb } = await import('./db.js')
|
|
633
|
+
const db = getDb(vaultRoot)
|
|
634
|
+
const rows = db.prepare("SELECT id, path, title, body, tags, reason FROM quarantine WHERE status = 'pending'").all()
|
|
635
|
+
let scanned = 0, changed = 0
|
|
636
|
+
const byKind = {}
|
|
637
|
+
for (const row of rows) {
|
|
638
|
+
let tags = []
|
|
639
|
+
try { tags = JSON.parse(row.tags || '[]') } catch { tags = [] }
|
|
640
|
+
const isRaw = (Array.isArray(tags) && tags.includes('raw')) || /原文卡|自动沉淀/.test(String(row.reason || ''))
|
|
641
|
+
if (!isRaw) continue
|
|
642
|
+
scanned++
|
|
643
|
+
const kind = guessRawKind({ title: row.title, body: row.body })
|
|
644
|
+
if (kind === 'content') continue
|
|
645
|
+
changed++
|
|
646
|
+
byKind[kind] = (byKind[kind] || 0) + 1
|
|
647
|
+
if (!dryRun) db.prepare('UPDATE quarantine SET kind = ?, updated_at = ? WHERE id = ?').run(kind, new Date().toISOString(), row.id)
|
|
648
|
+
}
|
|
649
|
+
json(res, 200, { ok: true, dryRun, scanned, changed, byKind })
|
|
650
|
+
return
|
|
651
|
+
}
|
|
628
652
|
case '/audit/list': {
|
|
629
653
|
const { pending, rejected } = await auditQueue(vaultRoot)
|
|
630
654
|
json(res, 200, { ok: true, pending, rejected })
|
|
631
655
|
return
|
|
632
656
|
}
|
|
633
657
|
case '/audit/approve': {
|
|
634
|
-
// POST {path} 或 GET ?path= → status=approved
|
|
635
|
-
const
|
|
658
|
+
// POST {path, kind?} 或 GET ?path=&kind= → status=approved;kind=core 时同时设为核心
|
|
659
|
+
const body = query.get('path') ? null : await readBody(req)
|
|
660
|
+
const rel = query.get('path') || body?.path || ''
|
|
661
|
+
const kind = String(query.get('kind') || body?.kind || '').trim()
|
|
636
662
|
if (!rel) return json(res, 400, { ok: false, error: '缺少 path' })
|
|
637
663
|
await setCardStatus(vaultRoot, rel, 'approved')
|
|
638
|
-
|
|
664
|
+
if (kind === 'core' || kind === 'knowledge') await setCardKind(vaultRoot, rel, kind)
|
|
665
|
+
json(res, 200, { ok: true, path: rel, status: 'approved', kind: kind || undefined })
|
|
639
666
|
return
|
|
640
667
|
}
|
|
641
668
|
case '/audit/reject': {
|
|
@@ -650,6 +677,21 @@ export function createApi(deps = {}) {
|
|
|
650
677
|
json(res, 200, { ok: true, path: rel, status: 'rejected', reason })
|
|
651
678
|
return
|
|
652
679
|
}
|
|
680
|
+
case '/card-kind': {
|
|
681
|
+
// POST {path, kind:'core'|'knowledge'|...}:人工把已批准卡片设为核心/取消核心
|
|
682
|
+
if (req.method !== 'POST') return json(res, 405, { ok: false, error: '需要 POST' })
|
|
683
|
+
const body = await readBody(req)
|
|
684
|
+
const rel = String(query.get('path') || body.path || '').trim()
|
|
685
|
+
const kind = String(body.kind || '').trim()
|
|
686
|
+
if (!rel || !kind) return json(res, 400, { ok: false, error: '缺少 path 或 kind' })
|
|
687
|
+
try {
|
|
688
|
+
const out = await setCardKind(vaultRoot, rel, kind)
|
|
689
|
+
json(res, 200, { ok: true, ...out })
|
|
690
|
+
} catch (e) {
|
|
691
|
+
json(res, 400, { ok: false, error: String(e?.message || e) })
|
|
692
|
+
}
|
|
693
|
+
return
|
|
694
|
+
}
|
|
653
695
|
case '/recycle/list': {
|
|
654
696
|
const items = await recycleList(vaultRoot)
|
|
655
697
|
json(res, 200, { ok: true, items })
|
|
@@ -996,7 +1038,7 @@ export function createApi(deps = {}) {
|
|
|
996
1038
|
const cfg = getSettings() ?? {}
|
|
997
1039
|
const env = typeof process !== 'undefined' ? process.env : {}
|
|
998
1040
|
const safe = {
|
|
999
|
-
autoCapture: cfg.autoCapture ?? true, autoRecall: cfg.autoRecall ?? true, recallLimit: cfg.recallLimit ?? 5, recallSummaryLen: cfg.recallSummaryLen ?? 130, recallIncludeBody: cfg.recallIncludeBody ?? false, secretHint: cfg.secretHint || '',
|
|
1041
|
+
autoCapture: cfg.autoCapture ?? true, autoRecall: cfg.autoRecall ?? true, coreMemoryEnabled: cfg.coreMemoryEnabled ?? true, coreMemoryLimit: cfg.coreMemoryLimit ?? 6, coreMemoryMaxChars: cfg.coreMemoryMaxChars ?? 2000, recallLimit: cfg.recallLimit ?? 5, recallSummaryLen: cfg.recallSummaryLen ?? 130, recallIncludeBody: cfg.recallIncludeBody ?? false, secretHint: cfg.secretHint || '',
|
|
1000
1042
|
captureMinChars: cfg.captureMinChars ?? 200, captureCooldownMs: cfg.captureCooldownMs ?? 300000, dedupThreshold: cfg.dedupThreshold ?? 0.62, maxCardsPerDay: cfg.maxCardsPerDay ?? 60,
|
|
1001
1043
|
distillEnabled: cfg.distillEnabled ?? true, dedupByLLM: cfg.dedupByLLM ?? true, captureMaxTokens: cfg.captureMaxTokens ?? DEFAULT_CAPTURE_MAX_TOKENS, recallMinScore: cfg.recallMinScore ?? 2,
|
|
1002
1044
|
autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0', autoWebMode: cfg.autoWebMode || 'init', webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999, webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000, webMaxRestart: Number(cfg.webMaxRestart) || 10, watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1', autoRestartOnDrift: cfg.autoRestartOnDrift ?? true, autoMcpSetup: cfg.autoMcpSetup ?? false,
|
package/lib/capture.js
CHANGED
|
@@ -316,8 +316,9 @@ export async function summarizeTurnDetailed(llm, route, conversation, opts = {})
|
|
|
316
316
|
'输出严格 JSON(不要 markdown 代码块、不要多余文字):',
|
|
317
317
|
'{"save": false} 表示不值得保存;',
|
|
318
318
|
'否则输出:',
|
|
319
|
-
'{"save": true, "title": "简短标题", "kind": "knowledge|project|content|prompt|business|tool|mistake", "tags": ["标签"], "body": "300-800字的压缩正文,markdown,含要点列表,首行是# 标题"}',
|
|
319
|
+
'{"save": true, "title": "简短标题", "kind": "knowledge|project|content|prompt|business|tool|mistake|core", "tags": ["标签"], "body": "300-800字的压缩正文,markdown,含要点列表,首行是# 标题"}',
|
|
320
320
|
'kind 含义:project=项目背景/进度;knowledge=通用知识/技术方案/设计决策;content=内容素材/资料;prompt=提示词/工作流;business=业务/商业;tool=工具链/CLI/配置/环境坑;mistake=错误/反模式/踩坑/“别这么做”/debug 教训。',
|
|
321
|
+
'core 只在“用户明确要求长期/每次都遵守的偏好、硬约束、核心记忆”时使用(如“以后都…/永远记住/每次都…/不要再用…”);一次性偏好或普通知识不要标 core。core 卡一律进待审核,由用户决定是否批准。',
|
|
321
322
|
'正文要求:去掉对话口水,只留可复用的结论、参数、步骤、原因。语言与对话一致。',
|
|
322
323
|
'已有卡片规则:若新知识与某张已有卡片是同一主题(同一技术、同一项目、同一决策),不要新建——输出',
|
|
323
324
|
'{"append_to": "已有卡片的 path 字段原样", "update": "追加/修正的短文本(一段话)"}',
|
|
@@ -704,7 +705,7 @@ export function isUsableTitle(title) {
|
|
|
704
705
|
|
|
705
706
|
/** 组装最终卡片对象(标题已体检通过)。 */
|
|
706
707
|
function finishCard(parsed, title, body) {
|
|
707
|
-
const kind = ['project', 'knowledge', 'content', 'prompt', 'business', 'tool', 'mistake'].includes(parsed.kind) ? parsed.kind : 'knowledge'
|
|
708
|
+
const kind = ['project', 'knowledge', 'content', 'prompt', 'business', 'tool', 'mistake', 'core'].includes(parsed.kind) ? parsed.kind : 'knowledge'
|
|
708
709
|
const tags = Array.isArray(parsed.tags)
|
|
709
710
|
? parsed.tags.map((t) => String(t).trim().slice(0, 30)).filter(Boolean).slice(0, 8)
|
|
710
711
|
: []
|
|
@@ -1006,3 +1007,37 @@ export async function resolveRoute(llm, opts = {}) {
|
|
|
1006
1007
|
const list = await routeCandidates(llm, opts)
|
|
1007
1008
|
return list[0] ?? null
|
|
1008
1009
|
}
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
const RAW_KIND_SIGNALS = [
|
|
1013
|
+
['mistake', /(报错|错误|异常|失败|根因|原因|修复|解决|踩坑|教训|bug|error|exception|crash|stack trace|排查|回滚)/i],
|
|
1014
|
+
['tool', /(命令|CLI|终端|安装|配置|环境变量|端口|依赖|版本|npm|pnpm|yarn|\bgit\b|docker|powershell|bash|PATH|shell|SDK|API key|token)/i],
|
|
1015
|
+
['prompt', /(提示词|prompt|模板|工作流|角色设定|系统指令|AI 指令)/i],
|
|
1016
|
+
['project', /(项目|进度|里程碑|需求|交付|上线|排期|任务|计划|迭代|版本发布|roadmap)/i],
|
|
1017
|
+
['business', /(客户|合同|报价|价格|商业|营收|成本|预算|市场|销售|付费|订阅)/i],
|
|
1018
|
+
['content', /(文案|素材|文章|博客|视频脚本|标题|封面|宣传|内容创作|草稿)/i],
|
|
1019
|
+
]
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* 不调 LLM(关闭蒸馏 / 蒸馏失败兜底)时的轻量类型判断。
|
|
1023
|
+
* 只做启发式:宁可回退 knowledge,也不要把所有原文卡都塞成 content。
|
|
1024
|
+
* 明确出现长期偏好/硬约束时返回 core(仍会强制进待审)。
|
|
1025
|
+
*/
|
|
1026
|
+
export function guessRawKind({ title = '', body = '' } = {}) {
|
|
1027
|
+
const head = String(title || '')
|
|
1028
|
+
const text = String(body || '').slice(0, 6000)
|
|
1029
|
+
if (/(核心记忆|永远记住|以后都|永久(记住|遵守)|必须(遵守|记住)|不要再)/.test(head + '\n' + text)) return 'core'
|
|
1030
|
+
const scores = {}
|
|
1031
|
+
for (const [kind, re] of RAW_KIND_SIGNALS) {
|
|
1032
|
+
const h = (head.match(re) || []).length
|
|
1033
|
+
const b = (text.match(re) || []).length
|
|
1034
|
+
const score = Math.min(5, h * 2 + b)
|
|
1035
|
+
if (score) scores[kind] = score
|
|
1036
|
+
}
|
|
1037
|
+
let best = 'knowledge'
|
|
1038
|
+
let bestScore = 0
|
|
1039
|
+
for (const kind of ['mistake', 'tool', 'prompt', 'project', 'business', 'content']) {
|
|
1040
|
+
if ((scores[kind] || 0) > bestScore) { best = kind; bestScore = scores[kind] }
|
|
1041
|
+
}
|
|
1042
|
+
return best
|
|
1043
|
+
}
|