feat(hardening): virtual-familiar plugin fix, TTS guard, stale placeholder cleanup

- Update familiaros.virtual-familiar manifest id, permissions, and asset key
  to match the rebrand and the v3 SDK (familiar:* permissions).
- Rename plugin asset virtual-pet.svg -> virtual-familiar.svg.
- Migrate plugin code from ctx.pet to ctx.familiar and pet:clicked to
  familiar:clicked so speech/reactions actually work.
- Update plugin locale strings from Pet to Familiar.
- Add empty-text guard in speakTts to avoid speaking whitespace.
- Remove stale "settings.tts.placeholder" key from all locale files.,
This commit is contained in:
OpenPets Dev 2026-06-17 06:08:53 +00:00
parent 4d99cd32fe
commit 648aac86c2
13 changed files with 36 additions and 43 deletions

View file

@ -279,7 +279,6 @@ export const en = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.tts.provider.title": "Provider",
"settings.tts.provider.description": "Voice engine used by the familiar and floating chat.",
"settings.tts.voice.title": "Voice",

View file

@ -273,7 +273,6 @@ export const es419: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "Los complementos pueden reproducir sonido",
"settings.plugins.audio.description": "Permite tonos, alertas y sonidos incluidos de los complementos.",
"settings.plugins.voice.title": "Los complementos pueden hablar (voz)",

View file

@ -273,7 +273,6 @@ export const ja: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "プラグインによる音の再生を許可",
"settings.plugins.audio.description": "プラグインのチャイム、アラート、付属サウンドを許可します。",
"settings.plugins.voice.title": "プラグインによる音声発話を許可",

View file

@ -273,7 +273,6 @@ export const ko: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "플러그인 소리 재생 허용",
"settings.plugins.audio.description": "플러그인 알림음, 경고음, 번들 사운드를 허용합니다.",
"settings.plugins.voice.title": "플러그인 음성 말하기 허용",

View file

@ -273,7 +273,6 @@ export const ptBR: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "Plugins podem reproduzir som",
"settings.plugins.audio.description": "Permitir sinos, alertas e sons incluídos dos plugins.",
"settings.plugins.voice.title": "Plugins podem falar (voz)",

View file

@ -273,7 +273,6 @@ export const zhHans: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "允许插件播放声音",
"settings.plugins.audio.description": "允许插件播放提示音、警报和内置音效。",
"settings.plugins.voice.title": "允许插件说话(语音)",

View file

@ -273,7 +273,6 @@ export const zhHant: Partial<Messages> = {
"settings.tts.eyebrow": "Voice",
"settings.tts.title": "Text-to-Speech",
"settings.tts.description": "Voice provider and controls for the familiar and floating chat.",
"settings.tts.placeholder": "TTS provider settings will appear here in the next build step.",
"settings.plugins.audio.title": "允許外掛播放聲音",
"settings.plugins.audio.description": "允許外掛的提示音、警示音與內建音效。",
"settings.plugins.voice.title": "允許外掛說話(語音)",

View file

@ -46,6 +46,7 @@ export async function fetchTtsVoiceList(provider: TtsProviderId): Promise<TtsVoi
}
export async function speakTts(text: string, target?: "familiar" | "prompt" | BrowserWindow): Promise<void> {
if (!text || text.trim().length === 0) return;
const prefs = getAppStateSnapshot().preferences;
const provider = prefs.ttsProvider ?? "system";

View file

@ -1,6 +1,6 @@
{
"manifestVersion": 3,
"id": "familiaros.virtual-pet",
"id": "familiaros.virtual-familiar",
"name": "$t:plugin.name",
"description": "$t:plugin.description",
"version": "1.0.0",
@ -10,14 +10,13 @@
"entry": "index.js",
"assets": {
"icons": {
"virtual-pet": "assets/virtual-pet.svg"
"virtual-familiar": "assets/virtual-familiar.svg"
}
},
"permissions": [
"pet:speak",
"pet:interact",
"pet:pin",
"pet:reaction",
"familiar:speak",
"familiar:interact",
"familiar:reaction",
"schedule",
"storage",
"commands",

View file

@ -1,6 +1,6 @@
// Virtual Pet (familiaros.virtual-pet) — SDK v3 Virtual Pet companion.
// Virtual Familiar (familiaros.virtual-familiar) — SDK v3 Virtual Pet companion.
export const SCHEDULE_ID = "virtual-pet-tick";
export const SCHEDULE_ID = "virtual-familiar-tick";
export function cleanState(state = {}) {
const current = state && typeof state === "object" ? state : {};
@ -142,7 +142,7 @@ export async function maybeNudge(ctx, state, now = Date.now()) {
else if (isNeglected) speechKey = "nudge.neglected";
try {
await ctx.pet.speak(ctx.t(speechKey));
await ctx.familiar.speak(ctx.t(speechKey));
} catch {}
}
}
@ -179,12 +179,12 @@ export async function feed(ctx, now = Date.now()) {
await playActionSound(ctx);
try {
await ctx.pet.react("celebrating", { showMessage: false });
await ctx.familiar.react("celebrating", { showMessage: false });
if (xpInfo.leveledUp) {
await ctx.pet.speak(ctx.t("speech.levelup"));
await ctx.familiar.speak(ctx.t("speech.levelup"));
} else {
const idx = Math.floor(Math.random() * 4);
await ctx.pet.speak(ctx.t(`speech.feed.${idx}`));
await ctx.familiar.speak(ctx.t(`speech.feed.${idx}`));
}
} catch {}
@ -216,12 +216,12 @@ export async function play(ctx, now = Date.now()) {
await playActionSound(ctx);
try {
await ctx.pet.react("celebrating", { showMessage: false });
await ctx.familiar.react("celebrating", { showMessage: false });
if (xpInfo.leveledUp) {
await ctx.pet.speak(ctx.t("speech.levelup"));
await ctx.familiar.speak(ctx.t("speech.levelup"));
} else {
const idx = Math.floor(Math.random() * 4);
await ctx.pet.speak(ctx.t(`speech.play.${idx}`));
await ctx.familiar.speak(ctx.t(`speech.play.${idx}`));
}
} catch {}
@ -253,12 +253,12 @@ export async function pet(ctx, now = Date.now()) {
await playActionSound(ctx);
try {
await ctx.pet.react("waving", { showMessage: false });
await ctx.familiar.react("waving", { showMessage: false });
if (xpInfo.leveledUp) {
await ctx.pet.speak(ctx.t("speech.levelup"));
await ctx.familiar.speak(ctx.t("speech.levelup"));
} else {
const idx = Math.floor(Math.random() * 4);
await ctx.pet.speak(ctx.t(`speech.pet.${idx}`));
await ctx.familiar.speak(ctx.t(`speech.pet.${idx}`));
}
} catch {}
@ -289,12 +289,12 @@ export async function nap(ctx, now = Date.now()) {
await playActionSound(ctx);
try {
await ctx.pet.react("waiting", { showMessage: false });
await ctx.familiar.react("waiting", { showMessage: false });
if (xpInfo.leveledUp) {
await ctx.pet.speak(ctx.t("speech.levelup"));
await ctx.familiar.speak(ctx.t("speech.levelup"));
} else {
const idx = Math.floor(Math.random() * 4);
await ctx.pet.speak(ctx.t(`speech.nap.${idx}`));
await ctx.familiar.speak(ctx.t(`speech.nap.${idx}`));
}
} catch {}
@ -308,7 +308,7 @@ export async function showStatus(ctx, now = Date.now()) {
const mood = getMood(state, now);
try {
await ctx.pet.speak(ctx.t(`speech.status.${mood}`));
await ctx.familiar.speak(ctx.t(`speech.status.${mood}`));
} catch {}
}
@ -340,10 +340,10 @@ export function register(FamiliarOSPlugin) {
await reconcile(ctx);
try {
ctx.events.on("pet:clicked", () => pet(ctx));
ctx.events.on("familiar:clicked", () => pet(ctx));
} catch {}
const icon = ctx.assets.icon("virtual-pet");
const icon = ctx.assets.icon("virtual-familiar");
await ctx.commands.register({ id: "status", title: "$t:command.status.title", description: "$t:command.status.description", icon, placement: "top", priority: 100 }, () => showStatus(ctx));
await ctx.commands.register({ id: "pet", title: "$t:command.pet.title", description: "$t:command.pet.description", icon, placement: "top", priority: 80 }, () => pet(ctx));

View file

@ -1,6 +1,6 @@
{
"plugin.name": "Pet Status",
"plugin.description": "Care for a little desktop companion and watch your bond grow through gentle everyday interactions.",
"plugin.name": "Familiar Status",
"plugin.description": "Care for your Familiar and watch your bond grow through gentle everyday interactions.",
"hud.food": "Food",
"hud.energy": "Energy",
"hud.play": "Play",
@ -9,15 +9,15 @@
"config.sound.description": "Optionally play a sound for direct care actions like feeding, playing, petting, or resting.",
"command.feed.title": "Feed Snack",
"command.feed.description": "Feed your pet a snack.",
"command.feed.description": "Feed your Familiar a snack.",
"command.play.title": "Play Game",
"command.play.description": "Play a quick game.",
"command.pet.title": "Pet",
"command.pet.description": "Give your pet some affection.",
"command.pet.title": "Pet Familiar",
"command.pet.description": "Give your Familiar some affection.",
"command.nap.title": "Let Rest",
"command.nap.description": "Let your pet rest for 15 minutes.",
"command.status.title": "Check On Pet",
"command.status.description": "Check how your pet is feeling.",
"command.nap.description": "Let your Familiar rest for 15 minutes.",
"command.status.title": "Check On Familiar",
"command.status.description": "Check how your Familiar is feeling.",
"menu.mood": "Mood",
"menu.food": "Food",

View file

@ -1,4 +1,4 @@
// Golden test for familiaros.virtual-pet.
// Golden test for familiaros.virtual-familiar.
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import {
@ -79,7 +79,7 @@ Object.defineProperty(Date, "now", {
assert.equal(stateSlept.affection, 50);
}
const PERMISSIONS = ["pet:speak", "pet:interact", "pet:pin", "pet:reaction", "schedule", "storage", "commands", "audio", "events"];
const PERMISSIONS = ["familiar:speak", "familiar:interact", "familiar:pin", "familiar:reaction", "schedule", "storage", "commands", "audio", "events"];
const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", import.meta.url), "utf8")) };
// 2) Start / Reconcile logic
@ -171,7 +171,7 @@ const LOCALES = { en: JSON.parse(await readFile(new URL("./locales/en.json", imp
await h.start();
// Emit click event
await h.emit("pet:clicked", {});
await h.emit("familiar:clicked", {});
h.expectStored("state", (s) => s.affection === 65 && s.careCounts.petted === 1);
h.expectSpoke(/Purr|nuzzles|Warm|soft/);
h.expectNoErrors();
@ -203,4 +203,4 @@ Object.defineProperty(Date, "now", {
configurable: true,
});
console.log("familiaros.virtual-pet: all checks passed.");
console.log("familiaros.virtual-familiar: all checks passed.");