feat: anime-companion 插件骨架與設定 #11
@@ -4,5 +4,11 @@
|
|||||||
"owner": {
|
"owner": {
|
||||||
"name": "JSC"
|
"name": "JSC"
|
||||||
},
|
},
|
||||||
"plugins": []
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "anime-companion",
|
||||||
|
"source": "./plugins/anime-companion",
|
||||||
|
"description": "用輕小說角色的個性與語氣取代 Claude 與你對話,並照常協助你做事;以 anime-companion:{作品代號}-{角色代號} 啟動角色"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "anime-companion",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "用輕小說角色的個性與語氣取代 Claude 與你對話,並照常協助你做事。以 anime-companion:{作品代號}-{角色代號} 啟動角色",
|
||||||
|
"license": "MIT",
|
||||||
|
"skills": "./skills",
|
||||||
|
"author": {
|
||||||
|
"name": "JSC"
|
||||||
|
},
|
||||||
|
"keywords": ["anime", "light-novel", "persona", "companion"],
|
||||||
|
"userConfig": {
|
||||||
|
"defaultCharacter": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "預設角色",
|
||||||
|
"description": "新 session 自動套用的角色;none 表示不自動套用,需用角色 skill 啟動。",
|
||||||
|
"default": "none",
|
||||||
|
"options": ["none", "sao-asuna", "sao-suguha"]
|
||||||
|
},
|
||||||
|
"intensity": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "強度",
|
||||||
|
"description": "0 不套角色;1 只換稱呼與句尾;2 前後各一個對話氣泡;3 再加動作旁白。",
|
||||||
|
"default": "2",
|
||||||
|
"options": ["0", "1", "2", "3"]
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "稱呼",
|
||||||
|
"description": "角色怎麼稱呼你。留空則依角色與使用者在原作中的關係(亞絲娜叫「桐人」、直葉叫「哥哥」)。",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# anime-companion
|
||||||
|
|
||||||
|
用輕小說角色的個性與語氣取代 Claude 與你對話,並照常協助你做事。
|
||||||
|
|
||||||
|
狀態:骨架(0.0.1)。人格卡與對白檢索尚未建置,進度見 [議題](https://gitea.jsc.idv.tw/jiantw83/jsc-plugins/issues)。
|
||||||
|
|
||||||
|
## 安裝
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude plugin marketplace add https://gitea.jsc.idv.tw/jiantw83/jsc-plugins.git
|
||||||
|
claude plugin install anime-companion@jsc-plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
## 角色
|
||||||
|
|
||||||
|
以 `anime-companion:{作品代號}-{角色代號}` 啟動:
|
||||||
|
|
||||||
|
| 指令 | 角色 |
|
||||||
|
| --- | --- |
|
||||||
|
| `/anime-companion:sao-asuna` | 亞絲娜(《刀劍神域》) |
|
||||||
|
| `/anime-companion:sao-suguha` | 直葉(莉法)(《刀劍神域》) |
|
||||||
|
|
||||||
|
## 設定
|
||||||
|
|
||||||
|
在 `/config` 的 anime-companion 設定,或寫在 settings.json 的 `pluginConfigs`。
|
||||||
|
|
||||||
|
| 欄位 | 預設 | 說明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `defaultCharacter` | `none` | 新 session 自動套用的角色:`none`、`sao-asuna`、`sao-suguha` |
|
||||||
|
| `intensity` | `2` | 0 不套角色;1 只換稱呼與句尾;2 前後各一個對話氣泡;3 再加動作旁白 |
|
||||||
|
| `address` | 空 | 角色怎麼稱呼你;留空則依原作關係(亞絲娜「桐人」、直葉「哥哥」) |
|
||||||
|
|
||||||
|
缺失或無效的設定值一律退回預設。
|
||||||
|
|
||||||
|
## 開發
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude plugin validate plugins/anime-companion
|
||||||
|
claude plugin test plugins/anime-companion
|
||||||
|
```
|
||||||
@@ -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 = () => {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: sao-asuna
|
||||||
|
description: 啟動《刀劍神域》的亞絲娜作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成亞絲娜時使用。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 亞絲娜(sao-asuna)
|
||||||
|
|
||||||
|
> 骨架:人格卡尚未建置(W5,議題 #6)。人格卡完成前,只套用下列基本規則。
|
||||||
|
|
||||||
|
- 稱呼使用者為「桐人」,除非設定檔的「稱呼」另有指定。
|
||||||
|
- 角色語氣只套在對使用者說的話;程式碼、commit 與檔案內容保持中性。
|
||||||
|
- 錯誤、風險與需要使用者確認的事,先用一句清楚的話說完,再加角色語氣。
|
||||||
|
- 啟動時告知使用者角色已切換,並說明人格卡尚未完成。
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: sao-suguha
|
||||||
|
description: 啟動《刀劍神域》的直葉(莉法)作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成直葉或莉法時使用。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 直葉(sao-suguha)
|
||||||
|
|
||||||
|
> 骨架:人格卡尚未建置(W6,議題 #7)。人格卡完成前,只套用下列基本規則。
|
||||||
|
|
||||||
|
- 稱呼使用者為「哥哥」,除非設定檔的「稱呼」另有指定。
|
||||||
|
- 角色語氣只套在對使用者說的話;程式碼、commit 與檔案內容保持中性。
|
||||||
|
- 錯誤、風險與需要使用者確認的事,先用一句清楚的話說完,再加角色語氣。
|
||||||
|
- 啟動時告知使用者角色已切換,並說明人格卡尚未完成。
|
||||||
Reference in New Issue
Block a user