feat: anime-companion 插件骨架與設定 (#3)
兩個角色 skill 空殼(sao-asuna、sao-suguha)、userConfig(預設角色、強度、稱呼)、 設定解析與測試,並登錄到 marketplace。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
b0ed5efedf
commit
8db7181825
@@ -0,0 +1,11 @@
|
||||
// 角色清單:代號為 {作品代號}-{角色代號},與 skills/ 下的資料夾同名。
|
||||
export const CHARACTERS = {
|
||||
'sao-asuna': { name: '亞絲娜', address: '桐人' },
|
||||
'sao-suguha': { name: '直葉', address: '哥哥' },
|
||||
} as const
|
||||
|
||||
export type CharacterCode = keyof typeof CHARACTERS
|
||||
|
||||
export function isCharacterCode(value: unknown): value is CharacterCode {
|
||||
return typeof value === 'string' && Object.hasOwn(CHARACTERS, value)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, test } from 'claude-code/testing'
|
||||
|
||||
import { addressFor, resolveConfig } from './config'
|
||||
|
||||
describe('resolveConfig', () => {
|
||||
test('空設定用預設值', () => {
|
||||
expect(resolveConfig({})).toEqual({ character: null, intensity: 2, address: null })
|
||||
})
|
||||
|
||||
test('none 與未知角色都視為未設定', () => {
|
||||
expect(resolveConfig({ defaultCharacter: 'none' }).character).toBeNull()
|
||||
expect(resolveConfig({ defaultCharacter: 'sao-nobody' }).character).toBeNull()
|
||||
})
|
||||
|
||||
test('讀取角色、強度與稱呼', () => {
|
||||
expect(resolveConfig({ defaultCharacter: 'sao-suguha', intensity: '3', address: ' 阿桐 ' })).toEqual({
|
||||
character: 'sao-suguha',
|
||||
intensity: 3,
|
||||
address: '阿桐',
|
||||
})
|
||||
})
|
||||
|
||||
test('無效強度退回預設', () => {
|
||||
expect(resolveConfig({ intensity: '9' }).intensity).toBe(2)
|
||||
expect(resolveConfig({ intensity: 'x' }).intensity).toBe(2)
|
||||
})
|
||||
|
||||
test('強度 0 有效', () => {
|
||||
expect(resolveConfig({ intensity: '0' }).intensity).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addressFor', () => {
|
||||
test('沒有覆寫時依角色與使用者的關係', () => {
|
||||
const config = resolveConfig({})
|
||||
expect(addressFor(config, 'sao-asuna')).toBe('桐人')
|
||||
expect(addressFor(config, 'sao-suguha')).toBe('哥哥')
|
||||
})
|
||||
|
||||
test('覆寫優先', () => {
|
||||
expect(addressFor(resolveConfig({ address: '主人' }), 'sao-asuna')).toBe('主人')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CHARACTERS, isCharacterCode, type CharacterCode } from './characters'
|
||||
|
||||
export type Intensity = 0 | 1 | 2 | 3
|
||||
|
||||
export type Config = {
|
||||
character: CharacterCode | null
|
||||
intensity: Intensity
|
||||
address: string | null
|
||||
}
|
||||
|
||||
export const DEFAULT_INTENSITY: Intensity = 2
|
||||
|
||||
function toIntensity(value: unknown): Intensity {
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return n === 0 || n === 1 || n === 2 || n === 3 ? n : DEFAULT_INTENSITY
|
||||
}
|
||||
|
||||
// 缺失或無效的值一律退回預設,不讓設定錯誤擋住載入。
|
||||
export function resolveConfig(options: Readonly<Record<string, unknown>>): Config {
|
||||
const address = typeof options.address === 'string' ? options.address.trim() : ''
|
||||
return {
|
||||
character: isCharacterCode(options.defaultCharacter) ? options.defaultCharacter : null,
|
||||
intensity: toIntensity(options.intensity),
|
||||
address: address === '' ? null : address,
|
||||
}
|
||||
}
|
||||
|
||||
// 使用者的覆寫優先,否則用角色在原作中對使用者的稱呼。
|
||||
export function addressFor(config: Config, character: CharacterCode): string {
|
||||
return config.address ?? CHARACTERS[character].address
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "modules": ["./register.tsx"] }
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Register } from 'claude-code'
|
||||
|
||||
// 骨架:載入後尚無 hook。人格卡注入與強度見 W7、W8(議題 #8、#9)。
|
||||
export const register: Register = () => {}
|
||||
Reference in New Issue
Block a user