feat: 階段 2 路由、改寫與好感度
- Haiku 一次分類(難度、模型、思考程度、語氣),角色立刻回應 - turn.step 覆寫模型與思考程度,額度低於門檻自動降一級,支援手動鎖定 - 主模型回覆改寫為角色口吻,程式碼/路徑/指令/數字/URL 原樣保留,失敗回原文並另存原文 log - 好感度規則、五階段、不跨階段掉落、個性倍率、狀態列愛心、/jsc-anima:anima-affinity - 年齡與關係規則(未成年友情、成年移轉、戀愛式依賴需明確開啟) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
export type ModelName = 'haiku' | 'sonnet' | 'opus'
|
||||
export type Effort = 'low' | 'medium' | 'high'
|
||||
export type Tone = 'neutral' | 'praise' | 'scold'
|
||||
export type Route = { difficulty: number; model: ModelName; effort: Effort; tone: Tone; ack: string }
|
||||
|
||||
export const LADDER: readonly ModelName[] = ['haiku', 'sonnet', 'opus']
|
||||
|
||||
export const FALLBACK_ROUTE: Route = { difficulty: 5, model: 'sonnet', effort: 'medium', tone: 'neutral', ack: '' }
|
||||
|
||||
export const CLASSIFY_PROMPT =
|
||||
'你是路由器。讀下面使用者輸入,只回傳一行 JSON:' +
|
||||
'{"difficulty":1-10,"model":"haiku|sonnet|opus","effort":"low|medium|high","tone":"neutral|praise|scold","ack":"角色以口吻立刻回應的一句話"}。' +
|
||||
'difficulty 低用 haiku,中用 sonnet,高或需深入推理用 opus;tone 是使用者對角色的態度(誇獎/感謝為 praise,責罵為 scold)。\n\n使用者輸入:\n'
|
||||
|
||||
const isModel = (v: unknown): v is ModelName => v === 'haiku' || v === 'sonnet' || v === 'opus'
|
||||
const isEffort = (v: unknown): v is Effort => v === 'low' || v === 'medium' || v === 'high'
|
||||
const isTone = (v: unknown): v is Tone => v === 'neutral' || v === 'praise' || v === 'scold'
|
||||
|
||||
export const parseRoute = (text: string): Route => {
|
||||
const m = /\{[\s\S]*\}/.exec(text)
|
||||
if (!m) return FALLBACK_ROUTE
|
||||
try {
|
||||
const j = JSON.parse(m[0]) as Record<string, unknown>
|
||||
return {
|
||||
difficulty: typeof j.difficulty === 'number' ? j.difficulty : FALLBACK_ROUTE.difficulty,
|
||||
model: isModel(j.model) ? j.model : FALLBACK_ROUTE.model,
|
||||
effort: isEffort(j.effort) ? j.effort : FALLBACK_ROUTE.effort,
|
||||
tone: isTone(j.tone) ? j.tone : 'neutral',
|
||||
ack: typeof j.ack === 'string' ? j.ack.slice(0, 200) : '',
|
||||
}
|
||||
} catch {
|
||||
return FALLBACK_ROUTE
|
||||
}
|
||||
}
|
||||
|
||||
export type ChooseOptions = {
|
||||
candidates: readonly ModelName[]
|
||||
lockModel?: ModelName
|
||||
lockEffort?: Effort
|
||||
/** 最緊的額度剩餘百分比;低於 downgradeAt 就降一級。 */
|
||||
quotaRemain?: number
|
||||
downgradeAt: number
|
||||
}
|
||||
|
||||
export const choose = (r: Route, o: ChooseOptions): { model: ModelName; effort: Effort; downgraded: boolean } => {
|
||||
const effort = o.lockEffort ?? r.effort
|
||||
if (o.lockModel) return { model: o.lockModel, effort, downgraded: false }
|
||||
const allowed = LADDER.filter(m => o.candidates.includes(m))
|
||||
const pool = allowed.length ? allowed : LADDER
|
||||
let idx = Math.max(0, pool.indexOf(r.model))
|
||||
if (!pool.includes(r.model)) idx = pool.length - 1 // 選到不在候選內的模型時取候選最強者
|
||||
let downgraded = false
|
||||
if (o.quotaRemain !== undefined && o.quotaRemain < o.downgradeAt && idx > 0) {
|
||||
idx -= 1
|
||||
downgraded = true
|
||||
}
|
||||
return { model: pool[idx]!, effort, downgraded }
|
||||
}
|
||||
Reference in New Issue
Block a user