merge: 合併 origin/main,情感注入改由 rules.ts 的 PAD 增減換算
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,14 @@
|
|||||||
/jsc-mod-companion:ingest chapter1.md 章節編號 1
|
/jsc-mod-companion:ingest chapter1.md 章節編號 1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 評估標籤
|
||||||
|
|
||||||
|
使用者每句話會對應到一個評估標籤,再由 `rules.ts` 的規則表換成心情增減量;模型只負責分類,不給數值。
|
||||||
|
|
||||||
|
- 正則先判明確的情況(責罵、感謝、完成)。
|
||||||
|
- 正則沒判出時,採用模型於回覆末尾補的 `<評估:標籤>`;標籤須在 `tags.ts` 的白名單內,缺失或不在白名單就當作沒有標籤。
|
||||||
|
- 白名單約 20 個,參考 wiki 第 9.4 章;不含戀愛與親密類標籤。
|
||||||
|
|
||||||
## 情緒模型
|
## 情緒模型
|
||||||
|
|
||||||
- 8 維度各有基準與半衰期(心情 8h、精力 6h、害羞 2h、忌妒 4h、想念 12h、心跳 1h、想家 24h、不悅 4h,神經質 50 時),數值集中在 `hooks/companion/emotion.ts`。
|
- 8 維度各有基準與半衰期(心情 8h、精力 6h、害羞 2h、忌妒 4h、想念 12h、心跳 1h、想家 24h、不悅 4h,神經質 50 時),數值集中在 `hooks/companion/emotion.ts`。
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { applyEvent, baselineOf, decayEmotions, dominant, fromPad, gauge, ranked
|
|||||||
import type { Emotions } from './emotion'
|
import type { Emotions } from './emotion'
|
||||||
import { keywordEvent, MOOD_EVENTS, moodLabel } from './mood'
|
import { keywordEvent, MOOD_EVENTS, moodLabel } from './mood'
|
||||||
import type { MoodEvent } from './mood'
|
import type { MoodEvent } from './mood'
|
||||||
|
import { extractTag } from './tags'
|
||||||
import { createCare } from './care'
|
import { createCare } from './care'
|
||||||
import type { Care, CareProfile, CareSettings, CareEngine, Signals } from './care'
|
import type { Care, CareProfile, CareSettings, CareEngine, Signals } from './care'
|
||||||
import { createMemory } from './memory'
|
import { createMemory } from './memory'
|
||||||
@@ -212,6 +213,8 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
|||||||
settings: { dailyCap: DEFAULT_USER.careLimit, tzOffsetMin: 480 },
|
settings: { dailyCap: DEFAULT_USER.careLimit, tzOffsetMin: 480 },
|
||||||
}
|
}
|
||||||
let lastText = ''
|
let lastText = ''
|
||||||
|
// 這回合使用者文字是否已由正則判出標籤;是的話不再採用模型補的標
|
||||||
|
let regexHit = false
|
||||||
let recalled: string[] = []
|
let recalled: string[] = []
|
||||||
|
|
||||||
// 目前啟用角色的記憶、待辦與關心引擎;關心的個人化參數每次取用時更新
|
// 目前啟用角色的記憶、待辦與關心引擎;關心的個人化參數每次取用時更新
|
||||||
@@ -252,6 +255,7 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
|||||||
async function onPrompt(text: string): Promise<string[]> {
|
async function onPrompt(text: string): Promise<string[]> {
|
||||||
lastText = text
|
lastText = text
|
||||||
const ev = keywordEvent(text)
|
const ev = keywordEvent(text)
|
||||||
|
regexHit = ev !== null
|
||||||
if (ev) await moodEvent(ev)
|
if (ev) await moodEvent(ev)
|
||||||
if (ev === 'scold') await affinityEvent('negative')
|
if (ev === 'scold') await affinityEvent('negative')
|
||||||
else if (ev || text.trim().length >= 4) await affinityEvent('meaningful')
|
else if (ev || text.trim().length >= 4) await affinityEvent('meaningful')
|
||||||
@@ -275,9 +279,14 @@ export function createCompanion({ fs, clock, root, builtins = [FUBUKI] }: Compan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 一輪回覆結束;有用過工具才算做完事,心情變好並排隊一則反應
|
// 一輪回覆結束;有用過工具才算做完事,心情變好並排隊一則反應
|
||||||
async function onTurnComplete(usedTools: boolean) {
|
async function onTurnComplete(usedTools: boolean, reply = '') {
|
||||||
const b = await bundleFor()
|
const b = await bundleFor()
|
||||||
if (!b || !usedTools) return
|
if (!b) return
|
||||||
|
// 正則沒判出時,採用模型於回覆末尾補的標籤;不在白名單或缺失就當作沒標
|
||||||
|
const tag = regexHit ? null : extractTag(reply)
|
||||||
|
regexHit = true
|
||||||
|
if (tag) await moodEvent(tag)
|
||||||
|
if (!usedTools) return
|
||||||
await b.care.onTurnComplete(usedTools)
|
await b.care.onTurnComplete(usedTools)
|
||||||
await moodEvent('success')
|
await moodEvent('success')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { clamp } from './mood'
|
import { clamp, MOOD_EVENTS } from './mood'
|
||||||
import type { MoodEvent } from './mood'
|
import type { MoodEvent } from './mood'
|
||||||
import { bar } from './seg'
|
import { bar } from './seg'
|
||||||
import type { Pad } from './types'
|
import type { Pad } from './types'
|
||||||
@@ -94,20 +94,30 @@ export function fromPad(mood: Pad & { at: number }, baseline: Emotions, basePad:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 情感注入:事件對各維度的增減。mood/upset/energy/shy 的合成效果與舊 PAD 事件一致
|
// 情感注入:事件(評估標籤)對各維度的增減。mood/upset/energy/shy 由 rules.ts 的 PAD 增減換算
|
||||||
export const EMOTION_EVENTS = {
|
// (p>0 → 心情 p/2;p<0 → 心情 0.3p、不悅 −0.4p;a/2 → 精力;−d → 害羞),合成後與 PAD 增減一致;
|
||||||
thanks: { mood: 0.075, energy: 0.025, shy: -0.05, heartbeat: 0.05 },
|
// 心跳、想念、想家等新維度的額外效果寫在 EXTRA
|
||||||
scold: { mood: -0.06, upset: 0.08, energy: 0.075, shy: 0.1 },
|
const EXTRA: Partial<Record<MoodEvent, Partial<Emotions>>> = {
|
||||||
success: { mood: 0.04, energy: 0.015 },
|
thanks: { heartbeat: 0.05 },
|
||||||
toolFail: { mood: -0.02, upset: 0.04, energy: 0.05, shy: 0.08 },
|
return: { missing: -0.3, heartbeat: 0.1 },
|
||||||
return: { mood: 0.05, energy: 0.04, missing: -0.3, heartbeat: 0.1 },
|
lateNight: { homesick: 0.05 },
|
||||||
lateNight: { mood: -0.025, energy: -0.05, homesick: 0.05 },
|
favorite: { heartbeat: 0.08 },
|
||||||
favorite: { mood: 0.06, energy: 0.04, heartbeat: 0.08 },
|
ignored: { missing: 0.1 },
|
||||||
ignored: { mood: -0.025, energy: -0.015, missing: 0.1 },
|
cared: { heartbeat: 0.05, missing: -0.1 },
|
||||||
} as const satisfies Record<MoodEvent, Partial<Emotions>>
|
remembered: { heartbeat: 0.05 },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emotionEffect(ev: MoodEvent): Partial<Emotions> {
|
||||||
|
const { p, a, d } = MOOD_EVENTS[ev]
|
||||||
|
return {
|
||||||
|
mood: p > 0 ? p / 2 : p * 0.3,
|
||||||
|
upset: p < 0 ? -p * 0.4 : 0,
|
||||||
|
energy: a / 2,
|
||||||
|
shy: -d,
|
||||||
|
...EXTRA[ev],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const POSITIVE: ReadonlySet<MoodEvent> = new Set(['thanks', 'success', 'return', 'favorite'])
|
|
||||||
const NEGATIVE: ReadonlySet<MoodEvent> = new Set(['scold', 'toolFail', 'ignored'])
|
|
||||||
// 「壞」方向:心情下降,或不悅、忌妒上升
|
// 「壞」方向:心情下降,或不悅、忌妒上升
|
||||||
const isBad = (d: Dim, x: number) => (d === 'mood' ? x < 0 : (d === 'upset' || d === 'jealous') && x > 0)
|
const isBad = (d: Dim, x: number) => (d === 'mood' ? x < 0 : (d === 'upset' || d === 'jealous') && x > 0)
|
||||||
|
|
||||||
@@ -133,11 +143,12 @@ const habitCount = (h: Habit, ev: MoodEvent, now: number) => {
|
|||||||
*/
|
*/
|
||||||
export function applyEvent(cur: Emotions, habit: Habit, ev: MoodEvent, now: number): { emotions: Emotions; habit: Habit } {
|
export function applyEvent(cur: Emotions, habit: Habit, ev: MoodEvent, now: number): { emotions: Emotions; habit: Habit } {
|
||||||
const n = habitCount(habit, ev, now)
|
const n = habitCount(habit, ev, now)
|
||||||
const gain = POSITIVE.has(ev) ? Math.pow(HABITUATION, n) : 1
|
const p = MOOD_EVENTS[ev].p
|
||||||
const harm = NEGATIVE.has(ev) ? Math.min(SENSITIZATION_MAX, 1 + SENSITIZATION * n) : 1
|
const gain = p > 0 ? Math.pow(HABITUATION, n) : 1
|
||||||
|
const harm = p < 0 ? Math.min(SENSITIZATION_MAX, 1 + SENSITIZATION * n) : 1
|
||||||
const irritable = cur.energy < TIRED ? IRRITABLE : 1
|
const irritable = cur.energy < TIRED ? IRRITABLE : 1
|
||||||
const out = { ...cur }
|
const out = { ...cur }
|
||||||
for (const [d, raw] of Object.entries(EMOTION_EVENTS[ev]) as [Dim, number][]) {
|
for (const [d, raw] of Object.entries(emotionEffect(ev)) as [Dim, number][]) {
|
||||||
const dx = raw * (isBad(d, raw) ? harm * irritable : gain)
|
const dx = raw * (isBad(d, raw) ? harm * irritable : gain)
|
||||||
out[d] = clampDim(out[d] + clamp(dx, -TURN_CAP, TURN_CAP))
|
out[d] = clampDim(out[d] + clamp(dx, -TURN_CAP, TURN_CAP))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
|
import { regexTag, RULES } from './rules'
|
||||||
|
import type { Tag } from './tags'
|
||||||
import type { Pad } from './types'
|
import type { Pad } from './types'
|
||||||
|
|
||||||
export const clamp = (x: number, lo = -1, hi = 1) => Math.max(lo, Math.min(hi, x))
|
export const clamp = (x: number, lo = -1, hi = 1) => Math.max(lo, Math.min(hi, x))
|
||||||
|
|
||||||
export const MOOD_EVENTS = {
|
// 心情事件即評估標籤,增減量由 rules.ts 的規則表決定
|
||||||
thanks: { p: 0.15, a: 0.05, d: 0.05 },
|
export const MOOD_EVENTS = RULES
|
||||||
scold: { p: -0.2, a: 0.15, d: -0.1 },
|
export type MoodEvent = Tag
|
||||||
success: { p: 0.08, a: 0.03, d: 0.05 },
|
|
||||||
toolFail: { p: -0.08, a: 0.1, d: -0.08 },
|
|
||||||
return: { p: 0.1, a: 0.08, d: 0 },
|
|
||||||
lateNight: { p: -0.05, a: -0.1, d: 0 },
|
|
||||||
favorite: { p: 0.12, a: 0.08, d: 0.03 },
|
|
||||||
ignored: { p: -0.05, a: -0.03, d: -0.03 },
|
|
||||||
} as const satisfies Record<string, Pad>
|
|
||||||
export type MoodEvent = keyof typeof MOOD_EVENTS
|
|
||||||
|
|
||||||
// 八分區(Mehrabian 的 PAD 八分類)→ 情緒標籤與顏文字
|
// 八分區(Mehrabian 的 PAD 八分類)→ 情緒標籤與顏文字
|
||||||
const OCTANTS: Record<string, { label: string; kaomoji: string }> = {
|
const OCTANTS: Record<string, { label: string; kaomoji: string }> = {
|
||||||
@@ -34,14 +28,5 @@ export function moodLabel(m: Pad): { label: string; kaomoji: string } {
|
|||||||
return OCTANTS[s(m.p) + s(m.a) + s(m.d)]!
|
return OCTANTS[s(m.p) + s(m.a) + s(m.d)]!
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEYWORDS: [MoodEvent, RegExp][] = [
|
|
||||||
['scold', /笨|白痴|廢物|爛|你很煩|閉嘴|stupid|useless|idiot/i],
|
|
||||||
['thanks', /謝謝|感謝|太棒|好棒|厲害|讚|thanks?|thank you|great job|awesome/i],
|
|
||||||
['success', /成功|完成了|通過|搞定|passed|done|fixed/i],
|
|
||||||
]
|
|
||||||
|
|
||||||
/** 以使用者文字的關鍵字對應心情事件 */
|
/** 以使用者文字的關鍵字對應心情事件 */
|
||||||
export function keywordEvent(text: string): MoodEvent | null {
|
export const keywordEvent = (text: string): MoodEvent | null => regexTag(text)
|
||||||
for (const [ev, re] of KEYWORDS) if (re.test(text)) return ev
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { View } from './core'
|
import type { View } from './core'
|
||||||
|
import { TAGS } from './tags'
|
||||||
|
|
||||||
// 語氣只套用於對話文字與過場話;技術內容原樣保留
|
// 語氣只套用於對話文字與過場話;技術內容原樣保留
|
||||||
export type Extras = { memories?: string[]; todos?: string[] }
|
export type Extras = { memories?: string[]; todos?: string[] }
|
||||||
@@ -14,6 +15,7 @@ export function buildPrompt(v: View, nickname: string, extras: Extras = {}): str
|
|||||||
...(extras.todos?.length ? ['使用者尚未完成的待辦與你答應過的事(適時提醒):', ...extras.todos.map(t => `- ${t}`)] : []),
|
...(extras.todos?.length ? ['使用者尚未完成的待辦與你答應過的事(適時提醒):', ...extras.todos.map(t => `- ${t}`)] : []),
|
||||||
'每則回覆開頭用一句角色口吻的話回應;工具呼叫前後的過場話也維持角色口吻。',
|
'每則回覆開頭用一句角色口吻的話回應;工具呼叫前後的過場話也維持角色口吻。',
|
||||||
'程式碼、指令、路徑、錯誤訊息、安全警告必須逐字保持原樣,不得改寫或加語氣詞。',
|
'程式碼、指令、路徑、錯誤訊息、安全警告必須逐字保持原樣,不得改寫或加語氣詞。',
|
||||||
|
`回覆最後另起一行,用 <評估:標籤> 標出使用者這句話的情境(可省略),標籤只能是:${TAGS.join('、')}。不要寫數值。`,
|
||||||
'關鍵技術結論放在最前面或單獨一行,不因角色扮演而含糊。',
|
'關鍵技術結論放在最前面或單獨一行,不因角色扮演而含糊。',
|
||||||
]
|
]
|
||||||
return `<companion>\n${lines.join('\n')}\n</companion>`
|
return `<companion>\n${lines.join('\n')}\n</companion>`
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, test } from 'claude-code/testing'
|
||||||
|
|
||||||
|
import { createCompanion } from './core'
|
||||||
|
import { fakeClock, memoryFs } from './fs'
|
||||||
|
import { keywordEvent, MOOD_EVENTS } from './mood'
|
||||||
|
import { classify, regexTag, RULES } from './rules'
|
||||||
|
import { extractTag, isTag, stripTag, TAGS } from './tags'
|
||||||
|
import type { CompanionCore } from './types'
|
||||||
|
|
||||||
|
const MIKO: CompanionCore = {
|
||||||
|
id: 'miko',
|
||||||
|
name: '月見ミコ',
|
||||||
|
facts: {
|
||||||
|
birthday: 'unknown', zodiac: 'unknown', bloodType: 'unknown', hair: 'unknown',
|
||||||
|
face: 'unknown', height: 'unknown', weight: 'unknown', measurements: 'unknown',
|
||||||
|
},
|
||||||
|
neuroticism: 50,
|
||||||
|
baseline: { p: 0, a: 0, d: 0 },
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('正則', () => {
|
||||||
|
test('明確情況判出對應標籤,優先序為責罵 > 感謝 > 完成', () => {
|
||||||
|
expect(regexTag('你很煩')).toBe('scold')
|
||||||
|
expect(regexTag('謝謝你')).toBe('thanks')
|
||||||
|
expect(regexTag('tests passed')).toBe('success')
|
||||||
|
expect(regexTag('謝謝,但你真笨')).toBe('scold')
|
||||||
|
expect(regexTag('請幫我看一下這個函式')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keywordEvent 與舊行為相容', () => {
|
||||||
|
expect(keywordEvent('thanks a lot')).toBe('thanks')
|
||||||
|
expect(keywordEvent('hello')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('白名單', () => {
|
||||||
|
test('每個標籤都有規則,MOOD_EVENTS 由規則表驅動', () => {
|
||||||
|
for (const t of TAGS) expect(RULES[t]).toBeDefined()
|
||||||
|
expect(Object.keys(RULES).length).toBe(TAGS.length)
|
||||||
|
expect(MOOD_EVENTS).toBe(RULES)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('既有八個心情事件的數值不變', () => {
|
||||||
|
expect(RULES.thanks).toEqual({ p: 0.15, a: 0.05, d: 0.05 })
|
||||||
|
expect(RULES.scold).toEqual({ p: -0.2, a: 0.15, d: -0.1 })
|
||||||
|
expect(RULES.lateNight).toEqual({ p: -0.05, a: -0.1, d: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('白名單外的標籤被拒絕,也不含戀愛與親密類', () => {
|
||||||
|
expect(isTag('thanks')).toBe(true)
|
||||||
|
expect(isTag('kiss')).toBe(false)
|
||||||
|
expect(isTag(undefined)).toBe(false)
|
||||||
|
expect(extractTag('好喔 <評估:kiss>')).toBeNull()
|
||||||
|
for (const t of ['confess', 'flirt', 'praiseLooks', 'seenThrough']) expect(isTag(t)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('回覆末尾的標籤可取出與移除', () => {
|
||||||
|
expect(extractTag('好的!\n<評估:amused>')).toBe('amused')
|
||||||
|
expect(extractTag('<評估:amused> 後面還有字')).toBeNull()
|
||||||
|
expect(stripTag('好的!\n<評估:amused>')).toBe('好的!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('缺失退回', () => {
|
||||||
|
test('正則優先;正則沒判出才用模型標籤;模型標籤缺失或不合法就沒有標籤', () => {
|
||||||
|
expect(classify('謝謝', 'scold')).toBe('thanks')
|
||||||
|
expect(classify('今天好累', 'soothe')).toBe('soothe')
|
||||||
|
expect(classify('今天好累', 'bogus')).toBeNull()
|
||||||
|
expect(classify('今天好累', undefined)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('模型補標進入心情;正則已判出時不重複套用;不合法標籤沒有效果', async () => {
|
||||||
|
const mk = () => createCompanion({ fs: memoryFs(), clock: fakeClock(), root: '/h/.companion', builtins: [MIKO] })
|
||||||
|
const a = mk()
|
||||||
|
await a.ensureActivated()
|
||||||
|
await a.onPrompt('今天好累')
|
||||||
|
await a.onTurnComplete(false, '辛苦了\n<評估:soothe>')
|
||||||
|
expect(Math.abs((await a.view())!.mood.p - RULES.soothe.p)).toBeLessThan(1e-5)
|
||||||
|
|
||||||
|
const b = mk()
|
||||||
|
await b.ensureActivated()
|
||||||
|
await b.onPrompt('謝謝你')
|
||||||
|
await b.onTurnComplete(false, '不客氣\n<評估:scold>')
|
||||||
|
expect(Math.abs((await b.view())!.mood.p - RULES.thanks.p)).toBeLessThan(1e-5)
|
||||||
|
|
||||||
|
const c = mk()
|
||||||
|
await c.ensureActivated()
|
||||||
|
await c.onPrompt('今天好累')
|
||||||
|
await c.onTurnComplete(false, '辛苦了\n<評估:kiss>')
|
||||||
|
expect((await c.view())!.mood.p).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// 規則表:標籤 → 數值增減。所有數值集中在這裡;模型只給標籤,不給數值。
|
||||||
|
import type { Pad } from './types'
|
||||||
|
import { isTag, type Tag } from './tags'
|
||||||
|
|
||||||
|
export const RULES = {
|
||||||
|
thanks: { p: 0.15, a: 0.05, d: 0.05 },
|
||||||
|
scold: { p: -0.2, a: 0.15, d: -0.1 },
|
||||||
|
success: { p: 0.08, a: 0.03, d: 0.05 },
|
||||||
|
toolFail: { p: -0.08, a: 0.1, d: -0.08 },
|
||||||
|
return: { p: 0.1, a: 0.08, d: 0 },
|
||||||
|
lateNight: { p: -0.05, a: -0.1, d: 0 },
|
||||||
|
favorite: { p: 0.12, a: 0.08, d: 0.03 },
|
||||||
|
ignored: { p: -0.05, a: -0.03, d: -0.03 },
|
||||||
|
praiseSkill: { p: 0.1, a: 0.04, d: 0.06 },
|
||||||
|
amused: { p: 0.1, a: 0.06, d: 0.02 },
|
||||||
|
cared: { p: 0.08, a: -0.02, d: -0.02 },
|
||||||
|
apology: { p: 0.06, a: -0.06, d: 0.03 },
|
||||||
|
soothe: { p: 0.05, a: -0.08, d: 0.02 },
|
||||||
|
brushOff: { p: -0.06, a: 0.02, d: -0.04 },
|
||||||
|
overstep: { p: -0.15, a: 0.12, d: 0.02 },
|
||||||
|
remembered: { p: 0.08, a: 0.02, d: 0.02 },
|
||||||
|
selfDisclosure: { p: 0.04, a: 0.03, d: -0.02 },
|
||||||
|
gladForYou: { p: 0.08, a: 0.05, d: 0 },
|
||||||
|
liedCaught: { p: -0.18, a: 0.1, d: -0.05 },
|
||||||
|
brokenPromise: { p: -0.12, a: 0.04, d: -0.08 },
|
||||||
|
} as const satisfies Record<Tag, Pad>
|
||||||
|
|
||||||
|
// 正則先判明確的情況;順序即優先序
|
||||||
|
const PATTERNS: [Tag, RegExp][] = [
|
||||||
|
['scold', /笨|白痴|廢物|爛|你很煩|閉嘴|stupid|useless|idiot/i],
|
||||||
|
['thanks', /謝謝|感謝|太棒|好棒|厲害|讚|thanks?|thank you|great job|awesome/i],
|
||||||
|
['success', /成功|完成了|通過|搞定|passed|done|fixed/i],
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 以正則判斷使用者文字的標籤;沒有明確情況回 null */
|
||||||
|
export function regexTag(text: string): Tag | null {
|
||||||
|
for (const [tag, re] of PATTERNS) if (re.test(text)) return tag
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 正則優先;正則沒判出才用模型補的標籤(須在白名單內),缺失或不合法就是沒有標籤 */
|
||||||
|
export function classify(text: string, modelTag?: string | null): Tag | null {
|
||||||
|
return regexTag(text) ?? (isTag(modelTag) ? modelTag : null)
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// 評估標籤白名單(第 9.4 章)。模型只能從這裡挑標籤,數值由 rules.ts 決定。
|
||||||
|
// 不含戀愛與親密類標籤(稱讚外表、被看穿、告白等)。
|
||||||
|
export const TAGS = [
|
||||||
|
'thanks', // 被感謝
|
||||||
|
'scold', // 被罵
|
||||||
|
'success', // 做完事
|
||||||
|
'toolFail', // 工具失敗
|
||||||
|
'return', // 久別回來
|
||||||
|
'lateNight', // 深夜
|
||||||
|
'favorite', // 提到喜歡的事物
|
||||||
|
'ignored', // 被忽略
|
||||||
|
'praiseSkill', // 稱讚能力
|
||||||
|
'amused', // 被逗笑
|
||||||
|
'cared', // 被關心
|
||||||
|
'apology', // 道歉
|
||||||
|
'soothe', // 安撫
|
||||||
|
'brushOff', // 被敷衍
|
||||||
|
'overstep', // 越界
|
||||||
|
'remembered', // 被記得
|
||||||
|
'selfDisclosure', // 自我揭露
|
||||||
|
'gladForYou', // 為你高興
|
||||||
|
'liedCaught', // 說謊被發現
|
||||||
|
'brokenPromise', // 失約
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type Tag = (typeof TAGS)[number]
|
||||||
|
|
||||||
|
const SET: ReadonlySet<string> = new Set(TAGS)
|
||||||
|
|
||||||
|
export const isTag = (s: unknown): s is Tag => typeof s === 'string' && SET.has(s)
|
||||||
|
|
||||||
|
// 模型於回覆末尾補標,格式:<評估:標籤>
|
||||||
|
const TAG_AT_END = /<評估:\s*([A-Za-z]+)\s*>\s*$/
|
||||||
|
|
||||||
|
/** 取出回覆末尾的標籤;不在白名單或沒有標就回 null(呼叫端退回正則) */
|
||||||
|
export function extractTag(reply: string): Tag | null {
|
||||||
|
const m = TAG_AT_END.exec(reply)
|
||||||
|
return m && isTag(m[1]) ? m[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除回覆末尾的評估標記 */
|
||||||
|
export const stripTag = (reply: string) => reply.replace(TAG_AT_END, '').trimEnd()
|
||||||
@@ -75,7 +75,7 @@ export const register: Register = on => {
|
|||||||
on('turn.complete', async ($, e, next) => {
|
on('turn.complete', async ($, e, next) => {
|
||||||
replying = false
|
replying = false
|
||||||
// 只有主迴圈正常答完、且這輪用過工具,才算做完一件事
|
// 只有主迴圈正常答完、且這輪用過工具,才算做完一件事
|
||||||
if (e.reason === 'answer' && !e.agentId) companion?.onTurnComplete(usedTools).catch(() => {})
|
if (e.reason === 'answer' && !e.agentId) companion?.onTurnComplete(usedTools, e.answer).catch(() => {})
|
||||||
return next(e)
|
return next(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user