Skip to content

Commit 8258fa1

Browse files
chore: prepare release v2.4.0
* chore: prepare release v2.4.0 - Bump version to 2.4.0 across web/server-ts/desktop/tauri/Cargo - Add CHANGELOG entry for v2.4.0 (Groq, Mistral, AivisSpeech/OpenAI TTS, Cron trigger) * fix: レビュー指摘を修正 - validator.ts: mistral-llm/groq-llm/aivis-tts/openai-tts を API_KEY_NODE_TYPES と GLOBAL_SETTINGS_MAP に追加 (executor.ts との同期漏れを修正) - cron-trigger/node.ts: timezone config を実装 (Intl.DateTimeFormat による IANA timezone対応) - cron-trigger/node.ts: イベント payload のキーを tick → executionCount に修正 (manifest outputs と execute 戻り値との一貫性を確保) * fix: Biome formatter修正 (API_KEY_NODE_TYPES を複数行に展開) --------- Co-authored-by: kasumi-oboroge <kasumi-oboroge@users.noreply.github.com>
1 parent abe9cc5 commit 8258fa1

9 files changed

Lines changed: 101 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
フォーマットは [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に基づいており、
66
[セマンティックバージョニング](https://semver.org/lang/ja/) に準拠しています。
77

8+
## [2.4.0] - 2026-05-28
9+
10+
### 追加
11+
12+
- **Groq LLMプラグイン** - Groqの高速推論APIを使用したLLMノードを追加。Llama 3.3 70B Versatile / Llama 3.1 8B Instant などのモデルをサポート (#128)
13+
- **Mistral AI LLMプラグイン** - Mistral AIのAPIを使用したLLMノードを追加。Mistral Large / Medium / Small / Codestral などのモデルをサポート (#129)
14+
- **AivisSpeech・OpenAI TTSプラグイン** - AivisSpeech(VOICEVOX互換)とOpenAI Text-to-SpeechのTTSノードを追加 (#130)
15+
- **Cronトリガーノード** - cron式でワークフローをスケジュール実行できるトリガーノードを追加 (#131)
16+
817
## [2.3.2] - 2026-05-23
918

1019
### 修正

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aituber-flow/desktop",
3-
"version": "2.3.2",
3+
"version": "2.4.0",
44
"private": true,
55
"scripts": {
66
"prepare-runtime": "cd ../web && npm run build:desktop && cd ../desktop && node scripts/build-sidecar.js && node scripts/copy-resources.js",

apps/desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aituber-flow"
3-
version = "2.3.2"
3+
version = "2.4.0"
44
description = "AITuberFlow Desktop - Visual workflow editor for AI VTuber streaming"
55
authors = ["AITuberFlow"]
66
edition = "2021"

apps/desktop/src-tauri/tauri.conf.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"productName": "AITuberFlow",
3-
"version": "2.3.2",
3+
"version": "2.4.0",
44
"identifier": "com.aituberflow.app",
55
"build": {
66
"beforeBuildCommand": "",
@@ -26,15 +26,21 @@
2626
},
2727
"bundle": {
2828
"active": true,
29-
"targets": ["app", "dmg", "nsis"],
29+
"targets": [
30+
"app",
31+
"dmg",
32+
"nsis"
33+
],
3034
"icon": [
3135
"icons/32x32.png",
3236
"icons/128x128.png",
3337
"icons/128x128@2x.png",
3438
"icons/icon.icns",
3539
"icons/icon.ico"
3640
],
37-
"externalBin": ["binaries/server"],
41+
"externalBin": [
42+
"binaries/server"
43+
],
3844
"resources": [
3945
"resources/plugins/**/*",
4046
"resources/templates/**/*",
@@ -59,5 +65,3 @@
5965
}
6066
}
6167
}
62-
63-

apps/server-ts/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aituber-flow/server",
3-
"version": "2.3.2",
3+
"version": "2.4.0",
44
"type": "module",
55
"scripts": {
66
"dev": "bun run --hot src/index.ts",

apps/server-ts/src/engine/validator.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,15 @@ interface PluginManifest {
6666
};
6767
config?: ManifestConfig;
6868
}
69-
70-
// ─── Global Settings Map ─────────────────────────────────────────
69+
/** Node types that use API keys (not local-only services) */
70+
const API_KEY_NODE_TYPES = new Set([
71+
"openai-llm",
72+
"anthropic-llm",
73+
"google-llm",
74+
"mistral-llm",
75+
"groq-llm",
76+
"openai-tts",
77+
]);
7178

7279
/**
7380
* Maps node types to their config fields and corresponding global setting keys.
@@ -78,14 +85,15 @@ const GLOBAL_SETTINGS_MAP: Record<string, Record<string, string>> = {
7885
"anthropic-llm": { apiKey: "anthropic.apiKey", model: "anthropic.model" },
7986
"google-llm": { apiKey: "google.apiKey", model: "google.model" },
8087
"ollama-llm": { host: "ollama.host", model: "ollama.model" },
88+
"mistral-llm": { apiKey: "mistral.apiKey", model: "mistral.model" },
89+
"groq-llm": { apiKey: "groq.apiKey", model: "groq.model" },
8190
"voicevox-tts": { host: "voicevox.host" },
8291
"coeiroink-tts": { host: "coeiroink.host" },
8392
"sbv2-tts": { host: "sbv2.host" },
93+
"aivis-tts": { host: "aivis.host" },
94+
"openai-tts": { apiKey: "openai.apiKey" },
8495
};
8596

86-
/** Node types that use API keys (not local-only services) */
87-
const API_KEY_NODE_TYPES = new Set(["openai-llm", "anthropic-llm", "google-llm"]);
88-
8997
// ─── Manifest Cache ──────────────────────────────────────────────
9098

9199
const manifestCache = new Map<string, PluginManifest | null>();

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aituber-flow-web",
3-
"version": "2.3.2",
3+
"version": "2.4.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --webpack",

plugins/cron-trigger/node.ts

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* "0 9 * * *" → daily at 9:00
1010
* "*/5 * * * *" → every 5 minutes
1111
* "0 9 * * 1" every Monday at 9:00
12+
*
13+
* Timezone support uses the IANA timezone database via Intl.DateTimeFormat.
1214
*/
1315

1416
import { InputNode } from "@aituber-flow/sdk";
@@ -96,25 +98,71 @@ function parseCron(expression: string): CronSchedule {
9698
};
9799
}
98100

99-
function matchesCron(schedule: CronSchedule, date: Date): boolean {
101+
/**
102+
* Get the local time parts (minute/hour/date/month/weekday) in the given IANA timezone.
103+
* Falls back to system local time if the timezone is empty or invalid.
104+
*/
105+
function getTimeParts(
106+
date: Date,
107+
timezone: string,
108+
): { minute: number; hour: number; day: number; month: number; weekday: number } {
109+
try {
110+
const tz = timezone || undefined;
111+
const fmt = new Intl.DateTimeFormat("en-US", {
112+
timeZone: tz,
113+
minute: "numeric",
114+
hour: "numeric",
115+
day: "numeric",
116+
month: "numeric",
117+
weekday: "narrow",
118+
hour12: false,
119+
});
120+
const parts = Object.fromEntries(
121+
fmt.formatToParts(date).map((p) => [p.type, p.value]),
122+
);
123+
// Intl weekday narrow: Sun=Su, Mon=Mo, Tue=Tu, Wed=We, Thu=Th, Fri=Fr, Sat=Sa
124+
const weekdayMap: Record<string, number> = {
125+
Su: 0, Mo: 1, Tu: 2, We: 3, Th: 4, Fr: 5, Sa: 6,
126+
};
127+
return {
128+
minute: parseInt(parts.minute, 10),
129+
hour: parseInt(parts.hour, 10) % 24, // 24:xx → 0:xx
130+
day: parseInt(parts.day, 10),
131+
month: parseInt(parts.month, 10),
132+
weekday: weekdayMap[parts.weekday] ?? date.getDay(),
133+
};
134+
} catch {
135+
// Invalid timezone — fall back to system local
136+
return {
137+
minute: date.getMinutes(),
138+
hour: date.getHours(),
139+
day: date.getDate(),
140+
month: date.getMonth() + 1,
141+
weekday: date.getDay(),
142+
};
143+
}
144+
}
145+
146+
function matchesCron(schedule: CronSchedule, date: Date, timezone: string): boolean {
147+
const { minute, hour, day, month, weekday } = getTimeParts(date, timezone);
100148
return (
101-
schedule.minutes.has(date.getMinutes()) &&
102-
schedule.hours.has(date.getHours()) &&
103-
schedule.daysOfMonth.has(date.getDate()) &&
104-
schedule.months.has(date.getMonth() + 1) &&
105-
schedule.daysOfWeek.has(date.getDay())
149+
schedule.minutes.has(minute) &&
150+
schedule.hours.has(hour) &&
151+
schedule.daysOfMonth.has(day) &&
152+
schedule.months.has(month) &&
153+
schedule.daysOfWeek.has(weekday)
106154
);
107155
}
108156

109-
function getNextCronTime(schedule: CronSchedule, from: Date): Date {
157+
function getNextCronTime(schedule: CronSchedule, from: Date, timezone: string): Date {
110158
const next = new Date(from);
111159
next.setSeconds(0, 0);
112160
next.setMinutes(next.getMinutes() + 1);
113161

114162
// Search up to 366 days ahead
115163
const maxIterations = 366 * 24 * 60;
116164
for (let i = 0; i < maxIterations; i++) {
117-
if (matchesCron(schedule, next)) {
165+
if (matchesCron(schedule, next, timezone)) {
118166
return next;
119167
}
120168
next.setMinutes(next.getMinutes() + 1);
@@ -126,6 +174,7 @@ function getNextCronTime(schedule: CronSchedule, from: Date): Date {
126174

127175
export default class CronTriggerNode extends InputNode {
128176
private cronExpression: string = "0 * * * *";
177+
private timezone: string = "";
129178
private enabled: boolean = true;
130179
private executionCount: number = 0;
131180
private running: boolean = false;
@@ -136,6 +185,7 @@ export default class CronTriggerNode extends InputNode {
136185
context: NodeContext,
137186
): Promise<void> {
138187
this.cronExpression = config.cron ?? "0 * * * *";
188+
this.timezone = config.timezone ?? "";
139189
this.enabled = config.enabled ?? true;
140190
this.executionCount = 0;
141191
this.running = true;
@@ -152,9 +202,10 @@ export default class CronTriggerNode extends InputNode {
152202
return;
153203
}
154204

155-
const nextRun = getNextCronTime(this.schedule, new Date());
205+
const tzLabel = this.timezone || "system local";
206+
const nextRun = getNextCronTime(this.schedule, new Date(), this.timezone);
156207
await context.log(
157-
`Cron configured: "${this.cronExpression}" — next run: ${nextRun.toLocaleString()}`,
208+
`Cron configured: "${this.cronExpression}" (timezone: ${tzLabel}) — next run: ${nextRun.toLocaleString()}`,
158209
);
159210

160211
context.createTask((signal) => this.cronLoop(signal, context));
@@ -168,7 +219,7 @@ export default class CronTriggerNode extends InputNode {
168219

169220
while (!signal.aborted && this.running) {
170221
const now = new Date();
171-
const nextRun = getNextCronTime(this.schedule, now);
222+
const nextRun = getNextCronTime(this.schedule, now, this.timezone);
172223
const delayMs = nextRun.getTime() - now.getTime();
173224

174225
// Wait until the next cron time
@@ -188,7 +239,7 @@ export default class CronTriggerNode extends InputNode {
188239

189240
// Verify we're at the right time (within 30s tolerance)
190241
const currentTime = new Date();
191-
if (matchesCron(this.schedule, currentTime)) {
242+
if (matchesCron(this.schedule, currentTime, this.timezone)) {
192243
await this.emitTick(context);
193244

194245
// Wait until the current minute passes to avoid double-firing
@@ -217,9 +268,10 @@ export default class CronTriggerNode extends InputNode {
217268
);
218269
await context.emitEvent(
219270
createEvent("timer.tick", {
220-
tick: this.executionCount,
271+
executionCount: this.executionCount,
221272
timestamp,
222273
cronExpression: this.cronExpression,
274+
timezone: this.timezone || null,
223275
}),
224276
);
225277
}

0 commit comments

Comments
 (0)