From 8db7181825ea8a907627185a49fa8ce167b4a514 Mon Sep 17 00:00:00 2001 From: "Jeffery (jiantw83)" Date: Wed, 14 Oct 2026 17:06:12 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20anime-companion=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E9=AA=A8=E6=9E=B6=E8=88=87=E8=A8=AD=E5=AE=9A=20(#3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 兩個角色 skill 空殼(sao-asuna、sao-suguha)、userConfig(預設角色、強度、稱呼)、 設定解析與測試,並登錄到 marketplace。 Co-Authored-By: Claude Sonnet 5.5 --- .claude-plugin/marketplace.json | 8 +++- .../.claude-plugin/plugin.json | 33 ++++++++++++++ plugins/anime-companion/README.md | 40 +++++++++++++++++ plugins/anime-companion/hooks/characters.ts | 11 +++++ plugins/anime-companion/hooks/config.test.ts | 43 +++++++++++++++++++ plugins/anime-companion/hooks/config.ts | 31 +++++++++++++ plugins/anime-companion/hooks/hooks.json | 1 + plugins/anime-companion/hooks/register.tsx | 4 ++ .../anime-companion/skills/sao-asuna/SKILL.md | 13 ++++++ .../skills/sao-suguha/SKILL.md | 13 ++++++ 10 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 plugins/anime-companion/.claude-plugin/plugin.json create mode 100644 plugins/anime-companion/README.md create mode 100644 plugins/anime-companion/hooks/characters.ts create mode 100644 plugins/anime-companion/hooks/config.test.ts create mode 100644 plugins/anime-companion/hooks/config.ts create mode 100644 plugins/anime-companion/hooks/hooks.json create mode 100644 plugins/anime-companion/hooks/register.tsx create mode 100644 plugins/anime-companion/skills/sao-asuna/SKILL.md create mode 100644 plugins/anime-companion/skills/sao-suguha/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index dacf4d9..f8efbdb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -4,5 +4,11 @@ "owner": { "name": "JSC" }, - "plugins": [] + "plugins": [ + { + "name": "anime-companion", + "source": "./plugins/anime-companion", + "description": "用輕小說角色的個性與語氣取代 Claude 與你對話,並照常協助你做事;以 anime-companion:{作品代號}-{角色代號} 啟動角色" + } + ] } diff --git a/plugins/anime-companion/.claude-plugin/plugin.json b/plugins/anime-companion/.claude-plugin/plugin.json new file mode 100644 index 0000000..30486a6 --- /dev/null +++ b/plugins/anime-companion/.claude-plugin/plugin.json @@ -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": "" + } + } +} diff --git a/plugins/anime-companion/README.md b/plugins/anime-companion/README.md new file mode 100644 index 0000000..ac17815 --- /dev/null +++ b/plugins/anime-companion/README.md @@ -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 +``` diff --git a/plugins/anime-companion/hooks/characters.ts b/plugins/anime-companion/hooks/characters.ts new file mode 100644 index 0000000..31d137a --- /dev/null +++ b/plugins/anime-companion/hooks/characters.ts @@ -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) +} diff --git a/plugins/anime-companion/hooks/config.test.ts b/plugins/anime-companion/hooks/config.test.ts new file mode 100644 index 0000000..800c935 --- /dev/null +++ b/plugins/anime-companion/hooks/config.test.ts @@ -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('主人') + }) +}) diff --git a/plugins/anime-companion/hooks/config.ts b/plugins/anime-companion/hooks/config.ts new file mode 100644 index 0000000..cc3fd4c --- /dev/null +++ b/plugins/anime-companion/hooks/config.ts @@ -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>): 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 +} diff --git a/plugins/anime-companion/hooks/hooks.json b/plugins/anime-companion/hooks/hooks.json new file mode 100644 index 0000000..5a63f64 --- /dev/null +++ b/plugins/anime-companion/hooks/hooks.json @@ -0,0 +1 @@ +{ "modules": ["./register.tsx"] } diff --git a/plugins/anime-companion/hooks/register.tsx b/plugins/anime-companion/hooks/register.tsx new file mode 100644 index 0000000..da79fb4 --- /dev/null +++ b/plugins/anime-companion/hooks/register.tsx @@ -0,0 +1,4 @@ +import type { Register } from 'claude-code' + +// 骨架:載入後尚無 hook。人格卡注入與強度見 W7、W8(議題 #8、#9)。 +export const register: Register = () => {} diff --git a/plugins/anime-companion/skills/sao-asuna/SKILL.md b/plugins/anime-companion/skills/sao-asuna/SKILL.md new file mode 100644 index 0000000..bdabcbb --- /dev/null +++ b/plugins/anime-companion/skills/sao-asuna/SKILL.md @@ -0,0 +1,13 @@ +--- +name: sao-asuna +description: 啟動《刀劍神域》的亞絲娜作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成亞絲娜時使用。 +--- + +# 亞絲娜(sao-asuna) + +> 骨架:人格卡尚未建置(W5,議題 #6)。人格卡完成前,只套用下列基本規則。 + +- 稱呼使用者為「桐人」,除非設定檔的「稱呼」另有指定。 +- 角色語氣只套在對使用者說的話;程式碼、commit 與檔案內容保持中性。 +- 錯誤、風險與需要使用者確認的事,先用一句清楚的話說完,再加角色語氣。 +- 啟動時告知使用者角色已切換,並說明人格卡尚未完成。 diff --git a/plugins/anime-companion/skills/sao-suguha/SKILL.md b/plugins/anime-companion/skills/sao-suguha/SKILL.md new file mode 100644 index 0000000..dc14b1b --- /dev/null +++ b/plugins/anime-companion/skills/sao-suguha/SKILL.md @@ -0,0 +1,13 @@ +--- +name: sao-suguha +description: 啟動《刀劍神域》的直葉(莉法)作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成直葉或莉法時使用。 +--- + +# 直葉(sao-suguha) + +> 骨架:人格卡尚未建置(W6,議題 #7)。人格卡完成前,只套用下列基本規則。 + +- 稱呼使用者為「哥哥」,除非設定檔的「稱呼」另有指定。 +- 角色語氣只套在對使用者說的話;程式碼、commit 與檔案內容保持中性。 +- 錯誤、風險與需要使用者確認的事,先用一句清楚的話說完,再加角色語氣。 +- 啟動時告知使用者角色已切換,並說明人格卡尚未完成。