body method
Defines the actual body code. path is passed relative to lib, baseName is the filename, and className is the filename converted to Pascal case.
実際の本体コードを定義します。pathにlibからの相対パス、baseNameにファイル名が渡され、classNameにファイル名をパスカルケースに変換した値が渡されます。
Implementation
@override
String body(String baseName, String className) {
return """
`Workflow`は下記のように利用する。
## 概要
$excerpt
組織・プロジェクト・ワークフロー・タスク・アクションの階層構造でワークフローを管理し、Firestoreにデータを保存する。
**階層構造:**
```
組織(Organization)
└── プロジェクト(Project)
└── ワークフロー(Workflow)
└── タスク(Task)
└── アクション(Action)
```
## データ継承とその理由
### なぜ各階層でIDを継承するのか
タスク作成時に組織ID・プロジェクトIDを継承する理由:
1. **権限チェック**: タスク実行時に組織/プロジェクトレベルの権限を確認
2. **使用量追跡**: 組織/プロジェクト単位でのAPI使用量を計算
3. **フィルタリング**: 組織/プロジェクト別のタスク一覧表示
4. **課金処理**: 組織単位での課金管理
### 継承フロー図
```
組織(設定:APIキー、制限値)
↓ 継承
プロジェクト(設定:サービスアカウント、環境変数)
↓ 継承
ワークフロー(設定:アクション配列、プロンプト)
↓ 継承
タスク(実行:継承した設定で各アクションを実行)
```
### タスク作成時の継承関係
| フィールド | 継承元 | 目的 |
|-----------|--------|------|
| `organization` | ワークフロー | 権限管理、組織別フィルタリング |
| `project` | ワークフロー | プロジェクト別分析 |
| `actions` | ワークフロー | 実行するアクション定義 |
| `prompt` | ワークフロー | タスク固有のプロンプト |
| `materials` | ワークフロー | アクション実行に必要な素材 |
**注意**: このパッケージはアダプターを使用しません。データモデルのみを提供します。
## 設定方法
1. `katana.yaml`にワークフロー設定を追加。
使用したい機能の`enable`を`true`にしてください。
```yaml
firebase:
project_id: your_project_id
firestore:
enable: true
functions:
enable: true
workflow:
# アセット生成機能
generate_audio_with_google_tts:
enable: false
generate_image_with_gemini:
enable: false
generate_text_from_multimodal:
enable: false
# マーケティング分析機能
research_market:
enable: false
collect_from_app_store:
enable: false
collect_from_google_play_console:
enable: false
collect_from_firebase_analytics:
enable: false
analyze_marketing_data:
enable: false
analyze_github:
enable: false
analyze_market_research:
enable: false
generate_marketing_pdf:
enable: false
generate_marketing_markdown:
enable: false
# セールス機能
collect_google_play_developers:
enable: false
collect_app_store_developers:
enable: false
```
2. `katana apply`コマンドを実行して設定を自動適用。
```bash
katana apply
```
このコマンドにより以下が自動実行されます:
- Flutter側に`masamune_workflow`パッケージを追加
- NPM側に有効化されたワークフロー用パッケージを追加
- Firebase Functionsの`index.ts`を自動更新(importsとfunctionsを追加)
- Firebase Functionsのデプロイをリクエスト
3. 利用するファイルでインポート。
```dart
import 'package:masamune_workflow/masamune_workflow.dart';
```
## 利用方法
### 組織の管理
組織を作成・取得します。
```dart
class OrganizationPage extends PageScopedWidget {
@override
Widget build(BuildContext context, PageRef ref) {
// 組織一覧を取得
final organizations = ref.app.model(
WorkflowOrganizationModel.collection(),
)..load();
return Scaffold(
appBar: AppBar(title: Text("組織一覧")),
body: ListView.builder(
itemCount: organizations.length,
itemBuilder: (context, index) {
final org = organizations[index];
return ListTile(
leading: org.value?.icon != null
? Image.network(org.value!.icon!)
: Icon(Icons.business),
title: Text(org.value?.name ?? ""),
subtitle: Text(org.value?.description ?? ""),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 新しい組織を作成
final newOrg = organizations.create();
await newOrg.save(
WorkflowOrganizationModel(
name: "新規組織",
description: "組織の説明",
ownerId: ref.app.userId, // 現在のユーザーをオーナーに設定
),
);
},
child: Icon(Icons.add),
),
);
}
}
```
### 組織メンバーの管理
組織にメンバーを追加・管理します。
```dart
class MemberManagementPage extends PageScopedWidget {
const MemberManagementPage({required this.organizationId});
final String organizationId;
@override
Widget build(BuildContext context, PageRef ref) {
// 組織のメンバー一覧を取得
final members = ref.app.model(
WorkflowOrganizationMemberModel.collection(organizationId: organizationId),
)..load();
return Scaffold(
appBar: AppBar(title: Text("メンバー管理")),
body: ListView.builder(
itemCount: members.length,
itemBuilder: (context, index) {
final member = members[index];
return ListTile(
title: Text(member.value?.userId ?? ""),
subtitle: Text(_roleToString(member.value?.role)),
trailing: PopupMenuButton<WorkflowRole>(
onSelected: (role) async {
// ロールを更新
await member.save(
member.value!.copyWith(role: role),
);
},
itemBuilder: (context) => [
PopupMenuItem(value: WorkflowRole.admin, child: Text("管理者")),
PopupMenuItem(value: WorkflowRole.editor, child: Text("編集者")),
PopupMenuItem(value: WorkflowRole.viewer, child: Text("閲覧者")),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 新しいメンバーを追加
final newMember = members.create();
await newMember.save(
WorkflowOrganizationMemberModel(
role: WorkflowRole.viewer,
userId: "user_id_to_add",
organization: WorkflowOrganizationModelRefPath(organizationId),
),
);
},
child: Icon(Icons.person_add),
),
);
}
String _roleToString(WorkflowRole? role) {
switch (role) {
case WorkflowRole.admin:
return "管理者";
case WorkflowRole.editor:
return "編集者";
case WorkflowRole.viewer:
return "閲覧者";
default:
return "未設定";
}
}
}
```
**WorkflowRole列挙型:**
| 値 | 説明 |
|---|---|
| `admin` | 管理者。全ての操作が可能 |
| `editor` | 編集者。編集・実行が可能 |
| `viewer` | 閲覧者。閲覧のみ可能 |
### プロジェクトの管理
組織内でプロジェクトを管理します。
```dart
class ProjectPage extends PageScopedWidget {
const ProjectPage({required this.organizationId});
final String organizationId;
@override
Widget build(BuildContext context, PageRef ref) {
// 組織に紐づくプロジェクト一覧を取得
final projects = ref.app.model(
WorkflowProjectModel.collection().organization.equal(
WorkflowOrganizationModelRefPath(organizationId),
),
)..load();
return Scaffold(
appBar: AppBar(title: Text("プロジェクト一覧")),
body: ListView.builder(
itemCount: projects.length,
itemBuilder: (context, index) {
final project = projects[index];
return ListTile(
title: Text(project.value?.name ?? ""),
subtitle: Text(project.value?.description ?? ""),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
// 新しいプロジェクトを作成
final allProjects = ref.app.model(
WorkflowProjectModel.collection(),
);
final newProject = allProjects.create();
await newProject.save(
WorkflowProjectModel(
name: "新規プロジェクト",
description: "プロジェクトの説明",
concept: "プロジェクトのコンセプト",
goal: "達成目標",
target: "ターゲットユーザー",
locale: ModelLocale.fromLocale(Locale("ja")),
organization: WorkflowOrganizationModelRefPath(organizationId),
),
);
},
child: Icon(Icons.add),
),
);
}
}
```
### ワークフローの作成
繰り返し実行可能なワークフローを定義します。
```dart
class WorkflowCreatePage extends PageScopedWidget {
const WorkflowCreatePage({
required this.organizationId,
required this.projectId,
});
final String organizationId;
final String projectId;
@override
Widget build(BuildContext context, PageRef ref) {
final form = ref.page.form(
WorkflowWorkflowModel.form(
WorkflowWorkflowModel(
repeat: WorkflowRepeat.none,
),
),
);
return Scaffold(
appBar: AppBar(title: Text("ワークフロー作成")),
body: Column(
children: [
FormTextField(
form: form,
name: WorkflowWorkflowModelKeys.name.name,
hintText: "ワークフロー名",
),
FormTextField(
form: form,
name: WorkflowWorkflowModelKeys.prompt.name,
hintText: "プロンプト",
maxLines: 5,
),
FormDropdownField<WorkflowRepeat>(
form: form,
name: WorkflowWorkflowModelKeys.repeat.name,
items: [
DropdownMenuItem(value: WorkflowRepeat.none, child: Text("繰り返しなし")),
DropdownMenuItem(value: WorkflowRepeat.daily, child: Text("毎日")),
DropdownMenuItem(value: WorkflowRepeat.weekly, child: Text("毎週")),
DropdownMenuItem(value: WorkflowRepeat.monthly, child: Text("毎月")),
],
),
FormButton(
form: form,
onPressed: () async {
final workflows = ref.app.model(
WorkflowWorkflowModel.collection(),
);
final newWorkflow = workflows.create();
await newWorkflow.save(
form.value.copyWith(
organization: WorkflowOrganizationModelRefPath(organizationId),
project: WorkflowProjectModelRefPath(projectId),
actions: [
// 実行するアクションを定義
WorkflowActionCommandValue(
command: "generate_content",
index: 0,
data: {"type": "article"},
),
WorkflowActionCommandValue(
command: "publish",
index: 1,
data: {"platform": "web"},
),
],
),
);
context.navigator.pop();
},
child: Text("作成"),
),
],
),
);
}
}
```
**WorkflowRepeat列挙型:**
| 値 | 説明 |
|---|---|
| `none` | 繰り返しなし(1回のみ実行) |
| `daily` | 毎日実行 |
| `weekly` | 毎週実行 |
| `monthly` | 毎月実行 |
### タスクの実行
ワークフローからタスクを作成し、実行状態を管理します。
```dart
class TaskExecutionPage extends PageScopedWidget {
const TaskExecutionPage({required this.workflowId});
final String workflowId;
@override
Widget build(BuildContext context, PageRef ref) {
// ワークフローを取得
final workflow = ref.app.model(
WorkflowWorkflowModel.document(workflowId),
)..load();
// タスク一覧を取得
final tasks = ref.app.model(
WorkflowTaskModel.collection().workflow.equal(
WorkflowWorkflowModelRefPath(workflowId),
),
)..load();
return Scaffold(
appBar: AppBar(title: Text(workflow.value?.name ?? "タスク実行")),
body: Column(
children: [
// タスク一覧
Expanded(
child: ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text("タスク \${task.uid}"),
subtitle: Text(_statusToString(task.value?.status)),
trailing: _buildStatusIcon(task.value?.status),
onTap: () {
// タスク詳細ページへ遷移
},
);
},
),
),
// 新規タスク作成ボタン
Padding(
padding: EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () async {
if (workflow.value == null) return;
final allTasks = ref.app.model(
WorkflowTaskModel.collection(),
);
final newTask = allTasks.create();
await newTask.save(
WorkflowTaskModel(
workflow: WorkflowWorkflowModelRefPath(workflowId),
organization: workflow.value!.organization,
project: workflow.value!.project,
status: WorkflowTaskStatus.waiting,
actions: workflow.value!.actions,
prompt: workflow.value!.prompt,
materials: workflow.value!.materials,
),
);
},
child: Text("新規タスクを作成"),
),
),
],
),
);
}
String _statusToString(WorkflowTaskStatus? status) {
switch (status) {
case WorkflowTaskStatus.waiting:
return "待機中";
case WorkflowTaskStatus.running:
return "実行中";
case WorkflowTaskStatus.completed:
return "完了";
case WorkflowTaskStatus.failed:
return "失敗";
case WorkflowTaskStatus.canceled:
return "キャンセル";
default:
return "不明";
}
}
Widget _buildStatusIcon(WorkflowTaskStatus? status) {
switch (status) {
case WorkflowTaskStatus.waiting:
return Icon(Icons.schedule, color: Colors.grey);
case WorkflowTaskStatus.running:
return CircularProgressIndicator();
case WorkflowTaskStatus.completed:
return Icon(Icons.check_circle, color: Colors.green);
case WorkflowTaskStatus.failed:
return Icon(Icons.error, color: Colors.red);
case WorkflowTaskStatus.canceled:
return Icon(Icons.cancel, color: Colors.orange);
default:
return Icon(Icons.help_outline);
}
}
}
```
**WorkflowTaskStatus列挙型:**
| 値 | 説明 |
|---|---|
| `waiting` | 待機中。実行待ち |
| `running` | 実行中 |
| `completed` | 完了。正常に終了 |
| `failed` | 失敗。エラーが発生 |
| `canceled` | キャンセル。ユーザーにより中止 |
### アクションの管理
タスク内の個別アクションを管理し、ログを記録します。
```dart
class ActionDetailPage extends PageScopedWidget {
const ActionDetailPage({required this.actionId});
final String actionId;
@override
Widget build(BuildContext context, PageRef ref) {
// アクションを取得
final action = ref.app.model(
WorkflowActionModel.document(actionId),
)..load();
return Scaffold(
appBar: AppBar(title: Text("アクション詳細")),
body: action.value == null
? Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// コマンド情報
Text(
"コマンド: \${action.value!.command.command}",
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(height: 8),
Text("インデックス: \${action.value!.command.index}"),
// ステータス
SizedBox(height: 16),
Text(
"ステータス: \${action.value!.status}",
style: Theme.of(context).textTheme.titleMedium,
),
// ログ一覧
SizedBox(height: 16),
Text(
"ログ",
style: Theme.of(context).textTheme.titleMedium,
),
...action.value!.log.map((log) => Card(
child: ListTile(
leading: _buildPhaseIcon(log.phase),
title: Text(log.message ?? ""),
subtitle: Text(log.time?.dateTime.toString() ?? ""),
),
)),
// 結果
if (action.value!.results != null) ...[
SizedBox(height: 16),
Text(
"結果",
style: Theme.of(context).textTheme.titleMedium,
),
Text(action.value!.results.toString()),
],
],
),
),
);
}
Widget _buildPhaseIcon(TaskLogPhase? phase) {
switch (phase) {
case TaskLogPhase.start:
return Icon(Icons.play_arrow, color: Colors.blue);
case TaskLogPhase.end:
return Icon(Icons.stop, color: Colors.green);
case TaskLogPhase.error:
return Icon(Icons.error, color: Colors.red);
case TaskLogPhase.warning:
return Icon(Icons.warning, color: Colors.orange);
case TaskLogPhase.info:
return Icon(Icons.info, color: Colors.grey);
default:
return Icon(Icons.circle);
}
}
}
```
**TaskLogPhase列挙型:**
| 値 | 説明 |
|---|---|
| `start` | 開始ログ |
| `end` | 終了ログ |
| `error` | エラーログ |
| `warning` | 警告ログ |
| `info` | 情報ログ |
**WorkflowActionCommandValue:**
```dart
// アクションコマンドの定義
WorkflowActionCommandValue(
command: "generate_content", // コマンド名
index: 0, // 実行順序
data: { // コマンドに渡すデータ
"type": "article",
"length": 1000,
},
)
```
## アクション実行の詳細
### 実行順序と依存関係
1. **アクションの実行順序**: `WorkflowActionCommandValue.index`の昇順で順次実行
2. **データの受け渡し**: 前のアクションの`results`を次のアクションが参照可能
3. **エラー処理**: 1つのアクションが失敗すると、タスク全体が`failed`になり、残りのアクションは実行されない
### アクション実行フロー
```
タスクステータス: waiting
↓
タスクステータス: running(taskSchedulerによって開始)
↓
アクション1実行(index: 0)
├─成功→ results保存 → アクション2へ
└─失敗→ タスクステータス: failed(処理中断)
↓
アクション2実行(index: 1)
├─成功→ results保存 → アクション3へ
└─失敗→ タスクステータス: failed(処理中断)
↓
全アクション完了
↓
タスクステータス: completed
```
### アクションログの構造
各アクションは`WorkflowTaskLogValue`の配列でログを記録:
| フェーズ | 記録タイミング | 用途 |
|----------|--------------|------|
| `start` | アクション開始時 | 開始時刻記録 |
| `info` | 処理中 | 進捗状況記録 |
| `warning` | 警告発生時 | 非致命的な問題記録 |
| `error` | エラー発生時 | エラー詳細記録 |
| `end` | アクション完了時 | 終了時刻、結果記録 |
## アクション間のデータ受け渡し
### データフローの3つの要素
#### 1. materials(素材)
ワークフロー定義時に設定する静的な素材データ。全アクションから参照可能。
```dart
// ワークフロー定義時
materials: {
"images": ["gs://bucket/product1.jpg"],
"documents": ["gs://bucket/spec.pdf"],
}
```
#### 2. assets(成果物)
アクション実行結果として生成される成果物。Firestoreの`plugins/workflow/asset`に保存される。
```typescript
// アクション実行結果
{
"assets": {
"generatedImage": {
"url": "gs://bucket/generated/image.png",
"public_url": "https://storage.googleapis.com/...",
"content_type": "image/png"
}
}
}
```
#### 3. results(結果)
アクション実行のメタデータと結果データ。次のアクションが参照可能。
```typescript
{
"results": {
"imageGeneration": {
"inputTokens": 100,
"outputTokens": 50,
"cost": 0.005,
"generatedText": "生成された説明文..."
}
}
}
```
### アクション間のデータ受け渡しパターン
#### パターン1: assetsを次のアクションで参照
アクション1で生成した画像を、アクション2で使用する例:
```dart
final workflow = WorkflowWorkflowModel(
name: "画像加工ワークフロー",
actions: [
// アクション1: 画像生成
WorkflowActionCommandValue(
command: "generate_image_with_gemini",
index: 0,
data: {
"prompt": "美しい風景",
},
),
// アクション2: 生成した画像を取得して加工
// ※バックエンド側で前のアクションのassetsを自動的に参照可能
WorkflowActionCommandValue(
command: "process_image",
index: 1,
data: {
// 前のアクションのassets.generatedImageを参照
"input_asset_key": "generatedImage",
},
),
],
);
```
#### パターン2: resultsを次のアクションのパラメータに利用
マーケティング分析の例(バックエンド実装):
```typescript
// Firebase Functions側での実装例
export const analyze_marketing_data = Functions.action({
process: async (context) => {
// 前のアクションのresultsを取得
const previousActions = await context.task.getPreviousActions();
const googlePlayData = previousActions[0]?.results?.googlePlayData;
const analyticsData = previousActions[1]?.results?.analyticsData;
// 統合分析
const analysis = await analyzeData(googlePlayData, analyticsData);
return {
results: {
marketingAnalysis: analysis,
},
};
},
});
```
#### パターン3: materialsとassetsの組み合わせ
```dart
final workflow = WorkflowWorkflowModel(
name: "商品説明生成",
materials: {
"images": ["gs://bucket/product1.jpg"], // 事前に用意した素材
},
actions: [
// アクション1: 商品画像から特徴抽出
WorkflowActionCommandValue(
command: "generate_text_from_multimodal",
index: 0,
data: {
"prompt": "商品の特徴を箇条書きで抽出",
},
// materials.imagesを自動参照
),
// アクション2: 抽出した特徴を元に音声ナレーション生成
WorkflowActionCommandValue(
command: "generate_audio_with_google_tts",
index: 1,
data: {
// 前のアクションのresults.textGeneration.generatedTextを参照
"use_previous_text": true,
},
),
],
);
```
### データ参照の実装詳細
バックエンド側(Firebase Functions)では、以下のようにアクション間のデータを参照:
```typescript
// 現在のアクションから前のアクションのデータにアクセス
const context = {
task: {
// タスクに設定されたmaterials
materials: task.materials,
// 前のアクションの結果を取得
getPreviousActions: async () => {
return task.actions.filter(a => a.index < currentAction.index);
},
// 前のアクションのassetsを取得
getPreviousAssets: async () => {
const previousAction = task.actions[currentAction.index - 1];
return previousAction?.assets;
},
},
};
```
**注意**: 具体的な実装はバックエンドのアクションハンドラーによって異なります。上記はconceptual exampleです。
**WorkflowTaskLogValue:**
```dart
// ログの記録
WorkflowTaskLogValue(
time: ModelTimestamp.now(),
message: "コンテンツ生成を開始しました",
action: "generate_content",
phase: TaskLogPhase.start,
data: {"input_tokens": 100},
)
```
### 使用量・プランの管理
組織の使用量をドルベースで管理し、サブスクリプションプランによる制限を設定できます。
#### WorkflowUsageModelの詳細
`WorkflowUsageModel`は組織のAI・サーバー利用料をドルベースで追跡し、各種APIの使用量を詳細に記録します。
| フィールド | 型 | 説明 |
|-----------|---|------|
| `usage` | `double` | 累積使用量(USD) |
| `currentMonth` | `double` | 今月の使用量(USD) |
| `bucketBalance` | `double` | 繰越可能な残高(USD)、未使用分を翌月に繰り越し |
| `latestPlan` | `String?` | 現在のプランID(WorkflowPlanModelへの参照) |
| `costBreakdown` | `Map<String, double>?` | API別コスト内訳 |
| `tokenUsageBreakdown` | `Map<String, int>?` | トークン使用量内訳 |
| `lastBillingDate` | `DateTime?` | 最終請求日 |
| `nextBillingDate` | `DateTime?` | 次回請求日 |
| `autoRenewalEnabled` | `bool` | 自動更新フラグ |
#### コスト計算の仕組み
各アクションの実行時に以下の料金が自動的に`usage`フィールドに加算されます:
**AI系サービス**
- **Gemini 2.0 Flash**: 入力 \$0.075/1Mトークン、出力 \$0.30/1Mトークン
- **Google TTS**: \$0.000004/文字(約 \$4/100万文字)
- **画像生成**: 約 \$0.005-0.02/画像(解像度による)
- **動画生成**: 約 \$0.10-0.50/分(品質による)
**インフラコスト**
- **Cloud Storage**: \$0.026/GB/月
- **Firebase Functions**: \$0.0000025/GB-秒 + \$0.40/100万呼び出し
- **Firestore**: 読み取り \$0.036/10万件、書き込み \$0.108/10万件
```dart
class UsagePage extends PageScopedWidget {
const UsagePage({required this.organizationId});
final String organizationId;
@override
Widget build(BuildContext context, PageRef ref) {
// 使用量を取得
final usage = ref.app.model(
WorkflowUsageModel.document(organizationId),
)..load();
// 現在のプラン情報を取得
final plan = usage.value?.latestPlan != null
? ref.app.model(
WorkflowPlanModel.document(usage.value!.latestPlan!),
)..load()
: null;
return Scaffold(
appBar: AppBar(title: Text("使用量")),
body: usage.value == null
? Center(child: CircularProgressIndicator())
: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 累積使用量(ドル表示)
Text(
"累積使用量: \${usage.value!.usage.toStringAsFixed(2)}",
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 8),
// 今月の使用量(ドル表示)
Text("今月の使用量: \${usage.value!.currentMonth.toStringAsFixed(2)} USD"),
// バケット残高(繰越可能額)
Text("繰越残高: \${usage.value!.bucketBalance.toStringAsFixed(2)} USD"),
// プラン情報と使用率
if (plan?.value != null) ...[
Divider(),
Text("現在のプラン: \${plan!.value!.name}"),
Text("月額上限: \${plan.value!.monthlyLimit.toStringAsFixed(2)} USD"),
// 使用率のプログレスバー
LinearProgressIndicator(
value: usage.value!.currentMonth / plan.value!.monthlyLimit,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(
usage.value!.currentMonth > plan.value!.monthlyLimit * 0.8
? Colors.red // 80%超過で赤
: Colors.blue,
),
),
Text(
"使用率: \${((usage.value!.currentMonth / plan.value!.monthlyLimit) * 100).toStringAsFixed(1)}%",
),
],
// API別コスト内訳
if (usage.value!.costBreakdown != null) ...[
Divider(),
Text("コスト内訳:", style: Theme.of(context).textTheme.titleMedium),
...usage.value!.costBreakdown!.entries.map((entry) =>
Padding(
padding: EdgeInsets.only(left: 16, top: 4),
child: Text("\${entry.key}: \${entry.value.toStringAsFixed(4)}"),
),
),
],
// 使用量アラート
if (plan?.value != null &&
usage.value!.currentMonth > plan.value!.monthlyLimit * 0.8)
Card(
color: Colors.orange[100],
child: Padding(
padding: EdgeInsets.all(12),
child: Row(
children: [
Icon(Icons.warning, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Text(
"月間使用量の80%を超過しています。プランのアップグレードをご検討ください。",
style: TextStyle(color: Colors.orange[900]),
),
),
],
),
),
),
],
),
),
);
}
}
```
#### ユーザーベース課金システムの重要な注意点
**課金の仕組み**:
- **組織の所有者**:各組織には`ownerId`フィールドがあり、作成したユーザーがオーナーとなります
- **課金の主体**:組織単位で利用量は記録されますが、実際の課金はユーザーに対して行われます
- **リミットチェック**:ユーザーが所有する全組織の合計利用量がチェック対象となります
```dart
// リミットチェックの実際の流れ
// 例:ユーザーAが3つの組織を所有している場合
// 1. ユーザーAのサブスクリプション
// プラン: Professional(月額\$99、上限\$100)
// 2. 所有組織と利用量
// Organization1(ownerId: userA) → 今月の利用量: \$45
// Organization2(ownerId: userA) → 今月の利用量: \$30
// Organization3(ownerId: userA) → 今月の利用量: \$20
// 3. 合計利用量チェック
// 合計: \$45 + \$30 + \$20 = \$95 (上限\$100以内でOK)
// 4. 新規アクション実行時のチェック
Future<bool> canExecuteWorkflow(String userId, double estimatedCost) async {
// ユーザーが所有する全組織の利用量を集計
final ownedOrgs = await WorkflowOrganizationModel.collection()
.ownerId.equal(userId)
.load();
double totalUsage = 0;
for (final org in ownedOrgs) {
final usage = await WorkflowUsageModel.document(org.id).load();
totalUsage += usage.value?.currentMonth ?? 0;
}
// ユーザーのプラン上限と比較
final userSubscription = await WorkflowUserSubscriptionModel
.document(userId)
.load();
final limit = userSubscription.value?.monthlyLimit ?? 0;
// 新規アクションを含めた合計が制限内かチェック
return (totalUsage + estimatedCost) <= limit;
}
```
### サブスクリプション管理
サブスクリプションと課金管理は`masamune_purchase`プラグインを使用します。Workflowプランの設定例は上記の「masamune_purchaseとの統合」セクションを参照してください。
プランの利用上限や機能制限は、PurchaseProductのメタデータとして定義し、WorkflowUsageCheckerクラスで利用量チェックを行います。
#### ユーザー・組織・利用量・課金の関係
```
┌─────────────────────────────────────────────────────┐
│ User(ユーザー) │
│ └─ PurchaseSubscriptionModel (from purchase) │
│ ├─ productId: "workflow_professional" │
│ ├─ status: active │
│ └─ metadata: {monthlyLimit: 100} │
└─────────────────────────────────────────────────────┘
│
├─────────owns─────────┬──────────owns──────┐
↓ ↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Organization1 │ │ Organization2 │ │ Organization3 │
│ ownerId: userA │ │ ownerId: userA │ │ ownerId: userA │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│ │ │
has usage has usage has usage
↓ ↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│WorkflowUsageModel│ │WorkflowUsageModel│ │WorkflowUsageModel│
│currentMonth: \$45 │ │currentMonth: \$30 │ │currentMonth: \$20 │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```
#### Purchase統合での利用量チェック実装
```dart
class WorkflowUsageChecker {
final String userId;
final PageRef ref;
WorkflowUsageChecker({
required this.userId,
required this.ref,
});
/// ユーザーが所有する全組織の合計利用量を取得
Future<double> getTotalUsage() async {
// 1. ユーザーが所有する全組織を取得
final organizations = await ref.app.model(
WorkflowOrganizationModel.collection()
.ownerId.equal(userId),
)..load();
double totalUsage = 0.0;
// 2. 各組織の利用量を集計
for (final org in organizations) {
final usage = await ref.app.model(
WorkflowUsageModel.document(org.uid),
)..load();
if (usage.value != null) {
totalUsage += usage.value!.currentMonth;
}
}
return totalUsage;
}
/// Purchaseプラグインと連携した利用制限チェック
Future<UsageLimitResult> checkLimit(double estimatedCost) async {
// Purchaseコントローラーからサブスクリプション情報を取得
final purchase = ref.app.controller(Purchase.query());
// Workflowのサブスクリプションを検索
final subscription = purchase.subscriptions.firstWhereOrNull(
(s) => s.isActive && s.productId.contains("workflow"),
);
if (subscription == null) {
return UsageLimitResult(
allowed: false,
reason: "有効なWorkflowサブスクリプションがありません",
);
}
// プロダクトメタデータから利用上限を取得
final product = purchase.products.firstWhereOrNull(
(p) => p.productId == subscription.productId,
);
final monthlyLimit = double.tryParse(
product?.metadata["monthlyLimit"] ?? "0"
) ?? 0;
final burstCapacity = double.tryParse(
product?.metadata["burstCapacity"] ?? "0"
) ?? 0;
// 現在の合計利用量を取得
final currentTotal = await getTotalUsage();
final totalLimit = monthlyLimit + burstCapacity;
// 新規アクションを含めた合計をチェック
final projectedTotal = currentTotal + estimatedCost;
if (projectedTotal <= monthlyLimit) {
return UsageLimitResult(
allowed: true,
remainingInPlan: monthlyLimit - projectedTotal,
usagePercentage: (projectedTotal / monthlyLimit) * 100,
);
} else if (projectedTotal <= totalLimit) {
return UsageLimitResult(
allowed: true,
usingBurstCapacity: true,
burstUsed: projectedTotal - monthlyLimit,
reason: "バースト容量を使用します",
);
} else {
return UsageLimitResult(
allowed: false,
exceeded: projectedTotal - totalLimit,
reason: "月間利用上限を超過します",
);
}
}
}
class UsageLimitResult {
final bool allowed;
final String? reason;
final double? remainingInPlan;
final double? usagePercentage;
final bool usingBurstCapacity;
final double? burstUsed;
final double? exceeded;
UsageLimitResult({
required this.allowed,
this.reason,
this.remainingInPlan,
this.usagePercentage,
this.usingBurstCapacity = false,
this.burstUsed,
this.exceeded,
});
}
```
#### Workflowプラン商品の設定例
```dart
// Purchaseプラグインで定義する商品
class WorkflowProducts {
static const free = PurchaseProduct(
productId: "workflow_free",
price: 0,
title: "Workflow Free",
description: "基本的なワークフロー機能",
metadata: {
"monthlyLimit": "10", // \$10/月
"burstCapacity": "0",
"aiTokens": "100000",
"storageGB": "1",
},
);
static const starter = PurchaseProduct(
productId: "workflow_starter",
price: 19.99,
title: "Workflow Starter",
description: "小規模プロジェクト向け",
metadata: {
"monthlyLimit": "50", // \$50/月
"burstCapacity": "10", // +\$10のバースト
"aiTokens": "1000000",
"storageGB": "10",
},
);
static const professional = PurchaseProduct(
productId: "workflow_professional",
price: 49.99,
title: "Workflow Professional",
description: "プロフェッショナル向け",
metadata: {
"monthlyLimit": "200", // \$200/月
"burstCapacity": "50", // +\$50のバースト
"aiTokens": "5000000",
"storageGB": "50",
},
);
}
```
#### 実装例:プラン選択と購入フロー
```dart
class WorkflowSubscriptionPage extends PageScopedWidget {
@override
Widget build(BuildContext context, PageRef ref) {
final purchase = ref.app.controller(Purchase.query());
final userId = ref.userId;
// Workflow関連の商品のみフィルタリング
final workflowProducts = purchase.products.where(
(p) => p.productId.startsWith("workflow_"),
).toList();
// 現在のサブスクリプション
final currentSubscription = purchase.subscriptions.firstWhereOrNull(
(s) => s.isActive && s.productId.contains("workflow"),
);
return Scaffold(
appBar: AppBar(title: Text("Workflowプラン")),
body: Column(
children: [
// 現在のプラン表示
if (currentSubscription != null)
Card(
child: ListTile(
title: Text("現在のプラン"),
subtitle: Text(currentSubscription.productId),
trailing: TextButton(
onPressed: () => _showUsageDetails(context, ref),
child: Text("利用状況"),
),
),
),
// プラン一覧
Expanded(
child: ListView.builder(
itemCount: workflowProducts.length,
itemBuilder: (context, index) {
final product = workflowProducts[index];
final isCurrentPlan = currentSubscription?.productId == product.productId;
return Card(
color: isCurrentPlan ? Colors.blue.shade50 : null,
child: ListTile(
title: Text(product.title),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(product.description),
Text("月額: \${product.price}"),
Text("利用上限: \${product.metadata['monthlyLimit']}/月"),
if (product.metadata['burstCapacity'] != "0")
Text("バースト: +\${product.metadata['burstCapacity']}"),
],
),
trailing: isCurrentPlan
? Chip(label: Text("現在のプラン"))
: ElevatedButton(
onPressed: () => _purchasePlan(purchase, product),
child: Text("選択"),
),
),
);
},
),
),
],
),
);
}
Future<void> _purchasePlan(Purchase purchase, PurchaseProduct product) async {
try {
await purchase.purchase(product);
// 購入成功後の処理
} catch (e) {
// エラー処理
}
}
Future<void> _showUsageDetails(BuildContext context, PageRef ref) async {
final checker = WorkflowUsageChecker(
userId: ref.userId,
ref: ref,
);
final totalUsage = await checker.getTotalUsage();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("利用状況"),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("今月の利用額: \${totalUsage.toStringAsFixed(2)}"),
// 詳細表示
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text("閉じる"),
),
],
),
);
}
}
```
### アセット・ページの管理
ワークフローで生成されたアセットやページを管理します。
```dart
class AssetListPage extends PageScopedWidget {
const AssetListPage({required this.organizationId});
final String organizationId;
@override
Widget build(BuildContext context, PageRef ref) {
// アセット一覧を取得
final assets = ref.app.model(
WorkflowAssetModel.collection().organization.equal(
WorkflowOrganizationModelRefPath(organizationId),
),
)..load();
return Scaffold(
appBar: AppBar(title: Text("アセット一覧")),
body: ListView.builder(
itemCount: assets.length,
itemBuilder: (context, index) {
final asset = assets[index];
return ListTile(
title: Text(asset.value?.path ?? ""),
subtitle: Text(asset.value?.mimtType ?? ""),
trailing: Text(asset.value?.source ?? ""),
);
},
),
);
}
}
```
## モデル一覧
| モデル名 | Firestoreパス | 説明 |
|---------|--------------|------|
| `WorkflowOrganizationModel` | `plugins/workflow/organization` | **組織**<br>・`ownerId`: 所有ユーザーID(課金主体) |
| `WorkflowOrganizationMemberModel` | `plugins/workflow/organization/:id/member` | 組織メンバー |
| `WorkflowProjectModel` | `plugins/workflow/project` | プロジェクト |
| `WorkflowWorkflowModel` | `plugins/workflow/workflow` | ワークフロー定義 |
| `WorkflowTaskModel` | `plugins/workflow/task` | タスク |
| `WorkflowActionModel` | `plugins/workflow/action` | アクション |
| `WorkflowUsageModel` | `plugins/workflow/organization/:id/usage` | **使用量管理**<br>・`usage`: 累積使用量(USD)<br>・`currentMonth`: 今月の使用量(USD)<br>・`costBreakdown`: API別コスト内訳<br>・`ownerId`: 組織オーナーID |
| `WorkflowCampaignModel` | `plugins/workflow/campaign` | キャンペーン |
| `WorkflowCertificateModel` | `plugins/workflow/organization/:id/certificate` | 証明書 |
| `WorkflowAssetModel` | `plugins/workflow/asset` | アセット |
| `WorkflowPageModel` | `plugins/workflow/page` | ページ |
| `WorkflowAddressModel` | `plugins/workflow/address` | アドレス |
## モデル間の関係図
### 階層構造とRefPath参照
```
WorkflowOrganizationModel(組織)
├── WorkflowOrganizationMemberModel(サブコレクション:組織メンバー)
├── WorkflowUsageModel(サブコレクション:使用量)
├── WorkflowCertificateModel(サブコレクション:証明書)
│
└─RefPath参照→ WorkflowProjectModel(プロジェクト)
│
└─RefPath参照→ WorkflowWorkflowModel(ワークフロー)
│
└─RefPath参照→ WorkflowTaskModel(タスク)
│
└─配列→ WorkflowActionModel(アクション)
```
**凡例**:
- `├──`: サブコレクション(親の削除で自動削除される)
- `─RefPath参照→`: RefPathによる参照(親が削除されても残る)
- `─配列→`: ドキュメント内の配列として保存
### サブコレクション vs ルートコレクション + RefPath
| パターン | Firestoreパス | 使用ケース | 例 |
|----------|--------------|-----------|-----|
| **サブコレクション** | `parent/:id/child` | 親と密結合、親削除で自動削除が必要 | 組織メンバー、使用量 |
| **ルートコレクション + RefPath** | `child`(RefPathフィールド保持) | 横断検索が必要、複数の親を持つ可能性 | プロジェクト、ワークフロー、タスク |
この設計により、以下が可能になります:
- プロジェクトを異なる組織間で移動
- タスクの横断検索(全組織のタスクを一覧表示)
- 柔軟な権限管理(RefPathを使った条件付きアクセス)
## 列挙型一覧
### WorkflowRepeat
| 値 | 説明 |
|---|---|
| `none` | 繰り返しなし |
| `daily` | 毎日 |
| `weekly` | 毎週 |
| `monthly` | 毎月 |
### WorkflowTaskStatus
| 値 | 説明 |
|---|---|
| `waiting` | 待機中 |
| `running` | 実行中 |
| `completed` | 完了 |
| `failed` | 失敗 |
| `canceled` | キャンセル |
### WorkflowRole
| 値 | 説明 |
|---|---|
| `admin` | 管理者 |
| `editor` | 編集者 |
| `viewer` | 閲覧者 |
### TaskLogPhase
| 値 | 説明 |
|---|---|
| `start` | 開始 |
| `end` | 終了 |
| `error` | エラー |
| `warning` | 警告 |
| `info` | 情報 |
## 値型一覧
### WorkflowActionCommandValue
アクションコマンドを定義する値型。
| フィールド | 型 | 説明 |
|-----------|---|------|
| `command` | `String` | コマンド名(必須) |
| `index` | `int` | 実行順序(必須) |
| `data` | `Map<String, dynamic>` | コマンドデータ |
### WorkflowTaskLogValue
タスクログを記録する値型。
| フィールド | 型 | 説明 |
|-----------|---|------|
| `time` | `ModelTimestamp?` | ログ時刻 |
| `message` | `String?` | ログメッセージ |
| `action` | `String?` | 関連アクション |
| `phase` | `TaskLogPhase?` | ログフェーズ |
| `data` | `Map<String, dynamic>?` | ログデータ |
## 参照パス(RefPath)について
### RefPathとは
`RefPath`は、Firestoreドキュメント間の参照を型安全に管理するためのMasamuneフレームワークの仕組みです。FirestoreのReference型をラップし、型安全性と使いやすさを提供します。
### RefPath型の一覧
| RefPath型 | 参照先 | 使用例 |
|----------|--------|--------|
| `WorkflowOrganizationModelRefPath` | 組織ドキュメント | プロジェクト、メンバー、タスク等が組織を参照 |
| `WorkflowProjectModelRefPath` | プロジェクトドキュメント | ワークフロー、タスク等がプロジェクトを参照 |
| `WorkflowWorkflowModelRefPath` | ワークフロードキュメント | タスクがワークフローを参照 |
### RefPathの役割と利点
1. **型安全性**: コンパイル時に誤った参照を検出
```dart
// ❌ コンパイルエラー: 型が一致しない
project.organization = WorkflowProjectModelRefPath(projectId);
// ✅ 正しい: 適切な型を使用
project.organization = WorkflowOrganizationModelRefPath(organizationId);
```
2. **クエリフィルタリング**: 特定の親に属するドキュメントを効率的に取得
```dart
// 特定組織のプロジェクトのみ取得
final projects = ref.app.model(
WorkflowProjectModel.collection()
.organization.equal(WorkflowOrganizationModelRefPath(organizationId))
);
```
3. **データ整合性**: 親子関係の明確化と整合性チェック
```dart
// タスク作成時に親の存在を確認
if (workflow.value == null) {
throw Exception("ワークフローが存在しません");
}
```
4. **権限管理**: Firestoreセキュリティルールでの参照チェック
```javascript
// Firestoreセキュリティルール例
match /plugins/workflow/task/{taskId} {
allow read: if request.auth.uid ==
get(resource.data.organization).data.ownerId;
}
```
### RefPathの実装パターン
```dart
// 1. RefPath作成
final orgRef = WorkflowOrganizationModelRefPath(organizationId);
// 2. モデルに設定
final project = WorkflowProjectModel(
name: "新プロジェクト",
organization: orgRef, // RefPathを設定
);
// 3. RefPathを使ったフィルタリング
final projects = ref.app.model(
WorkflowProjectModel.collection()
.organization.equal(orgRef) // 同じ組織のプロジェクトのみ
);
// 4. RefPathからIDを取得
final orgId = project.organization?.id; // 組織IDを取得
```
## Tips
- **モデル参照の活用**: `ModelRef`と`RefPath`を使って、モデル間の関連を型安全に管理できる
- **フィルタリング**: `collection().field.equal(value)`でコレクションをフィルタリングして取得できる
- **ローカル開発**: `RuntimeModelAdapter`の`initialValue`に初期データを設定して、Firestore接続なしでテストできる
```dart
RuntimeModelAdapter(
initialValue: [
WorkflowOrganizationModelInitialCollection({
"org_1": WorkflowOrganizationModel(
name: "テスト組織",
description: "テスト用の組織",
),
}),
],
)
```
- **検索機能**: `@searchParam`が付いたフィールド(例: `WorkflowActionModel.search`)は全文検索に利用できる
- **タイムスタンプ**: `ModelTimestamp.now()`で現在時刻を設定、自動的にFirestoreのサーバータイムスタンプに変換される
- **ロケール**: `ModelLocale`を使って多言語対応のワークフローを構築できる
## バックエンド側の実装
ワークフローの実行にはバックエンド側(Firebase Functions)の実装が必要です。`node_masamune`パッケージ群を使用して、様々な自動化タスクを実行できます。
### Firebase Functionsの設定
ワークフロー機能は`katana apply`コマンドで自動設定されます。手動設定は不要です。
**自動設定の内容:**
1. **パッケージの自動追加**
- `masamune_workflow`(基本機能、常に追加)
- 有効化されたワークフロー機能に応じて以下を自動追加:
- `masamune_workflow_asset`(画像・音声生成機能)
- `masamune_workflow_marketing`(マーケティング分析機能)
- `masamune_workflow_sales`(セールスデータ収集機能)
2. **index.tsの自動更新**
- インポート文の自動追加
- 関数エクスポートの自動追加
- 基本ワークフロー機能(`workflowScheduler`、`taskScheduler`、`taskCleaner`、`asset`)を常に追加
- 有効化された各ワークフロー機能の関数を追加
**自動生成されるindex.tsの例:**
以下は全ての機能を有効化した場合の例です(参考用)。
```typescript
import * as admin from "firebase-admin";
import * as functions from "firebase-functions/v2";
// 基本ワークフロー機能(自動追加)
import * as workflow from "@mathrunet/masamune_workflow";
// アセット生成機能(自動追加)
import * as workflow_asset from "@mathrunet/masamune_workflow_asset";
// マーケティング分析機能(自動追加)
import * as workflow_marketing from "@mathrunet/masamune_workflow_marketing";
// セールス機能(自動追加)
import * as workflow_sales from "@mathrunet/masamune_workflow_sales";
// Firebase Admin初期化
admin.initializeApp();
// Masamune
const m = workflow.Masamune();
// 関数のデプロイ(自動エクスポート)
m.deploy(
exports,
["us-central1"],
[
// 基本ワークフロー機能
workflow.Functions.workflowScheduler(),
workflow.Functions.taskScheduler(),
workflow.Functions.taskCleaner(),
workflow.Functions.asset(),
// アセット生成機能
workflow_asset.Functions.generateImageWithGemini(),
workflow_asset.Functions.generateAudioWithGoogleTTS(),
// マーケティング分析機能
workflow_marketing.Functions.researchMarket(),
workflow_marketing.Functions.collectFromAppStore(),
workflow_marketing.Functions.collectFromGooglePlayConsole(),
workflow_marketing.Functions.collectFromFirebaseAnalytics(),
workflow_marketing.Functions.analyzeMarketingData(),
workflow_marketing.Functions.analyzeGithub(),
workflow_marketing.Functions.analyzeMarketResearch(),
workflow_marketing.Functions.generateMarketingPdf(),
workflow_marketing.Functions.generateMarketingMarkdown(),
// セールス機能
workflow_sales.Functions.collectGooglePlayDevelopers(),
workflow_sales.Functions.collectAppStoreDevelopers(),
],
);
```
**注意:**
- 上記は参考例です。実際には`katana.yaml`で有効化した機能のみが追加されます。
- `index.ts`を手動で編集する必要はありません。`katana apply`が自動的に更新します。
- 既存の`index.ts`に他のFunctionsがある場合、それらは保持されます。
### 基本ワークフロー機能 (masamune_workflow)
#### スケジューラー機能
**workflowScheduler** - ワークフローの自動実行
- **実行間隔**: 1分ごと
- **機能**: `nextTime`が現在時刻を過ぎたワークフローから新しいタスクを自動作成
- **繰り返し設定**: `daily`, `weekly`, `monthly`に対応
**taskScheduler** - タスクの実行管理
- **実行間隔**: 1分ごと
- **機能**: `status=waiting`のタスクを`running`に変更し、アクション実行をキューに追加
**taskCleaner** - 完了タスクのクリーンアップ
- **機能**: 完了したタスクの不要データを削除
#### アセット取得機能
**asset** - アセットデータ取得
- **アクションID**: `asset`
- **パラメータ**:
```typescript
{
"data": {
"id": "asset_id" // アセットID
}
}
```
- **レスポンス**:
```typescript
{
"url": "gs://bucket/path/to/asset",
"public_url": "https://storage.googleapis.com/...",
"content_type": "image/png",
"signedUri": "https://storage.googleapis.com/...?signature=..."
}
```
### アセット生成機能 (masamune_workflow_asset)
#### 画像生成 - generate_image_with_gemini
**アクションID**: `generate_image_with_gemini`
**パラメータ**:
```typescript
{
"prompt": "美しい夕焼けの風景", // 必須: 生成する画像の説明
"negative_prompt": "人物、建物", // 任意: 除外したい要素
"width": 1024, // 任意: 画像幅(デフォルト: 1024)
"height": 1024, // 任意: 画像高さ(デフォルト: 1024)
"input_image": "gs://bucket/input.jpg", // 任意: 入力画像(画像から画像生成用)
"reference_image": "gs://bucket/ref.jpg", // 任意: スタイル参考画像
"model": "gemini-2.0-flash-exp", // 任意: 使用モデル
"seed": 12345, // 任意: シード値(再現性のため)
"output_path": "generated/images", // 任意: 保存先パス
"image_type": "illustration" // 任意: 画像カテゴリ
}
```
**レスポンス**:
```typescript
{
"results": {
"imageGeneration": {
"files": [{
"width": 1024,
"height": 1024,
"format": "png",
"size": 2048576
}],
"inputTokens": 100,
"outputTokens": 50,
"cost": 0.005
}
},
"assets": {
"generatedImage": {
"url": "gs://bucket/generated/image.png",
"public_url": "https://storage.googleapis.com/...",
"content_type": "image/png"
}
}
}
```
#### 音声生成 - generate_audio_with_google_tts
**アクションID**: `generate_audio_with_google_tts`
**パラメータ**:
```typescript
{
"text": "こんにちは、世界", // 必須: 読み上げるテキスト
"voice_name": "ja-JP-Neural2-A", // 任意: 音声の種類
"language": "ja-JP", // 任意: 言語コード
"gender": "FEMALE", // 任意: 性別 (MALE/FEMALE/NEUTRAL)
"output_format": "mp3", // 任意: 出力形式 (mp3/wav/ogg)
"speaking_rate": 1.0, // 任意: 話速 (0.25-4.0)
"pitch": 0.0, // 任意: ピッチ (-20.0-20.0)
"volume_gain_db": 0.0, // 任意: 音量 (-96.0-16.0)
"output_path": "generated/audio" // 任意: 保存先パス
}
```
**レスポンス**:
```typescript
{
"results": {
"audioGeneration": {
"files": [{
"duration": 3.5,
"format": "mp3",
"size": 56320,
"characters": 12
}],
"characters": 12,
"cost": 0.001
}
},
"assets": {
"generatedAudio": {
"url": "gs://bucket/generated/audio.mp3",
"public_url": "https://storage.googleapis.com/...",
"content_type": "audio/mpeg"
}
}
}
```
#### マルチモーダル入力からテキスト生成 - generate_text_from_multimodal
画像、動画、音声、ドキュメントなど複数のメディアファイルを総合的に分析し、Gemini APIを使用してテキストを生成します。`action.materials`フィールドにメディア素材を配置し、プロンプトと組み合わせて高度なテキスト生成が可能です。
**アクションID**: `generate_text_from_multimodal`
##### action.materialsフィールド
メディア素材は`action.materials`フィールドに配置します:
| フィールド | 型 | 説明 |
|----------|-----|------|
| images | string[] | 画像ファイルのgs:// URLリスト |
| videos | string[] | 動画ファイルのgs:// URLリスト |
| audio | string[] | 音声ファイルのgs:// URLリスト |
| documents | string[] | ドキュメントファイルのgs:// URLリスト |
##### パラメータ (ActionCommand.data)
| パラメータ | 型 | 必須 | 説明 |
|-----------|-----|------|------|
| prompt | string | ✓ | メイン生成プロンプト |
| system_prompt | string | | システムインストラクション |
| output_format | string | | 出力形式 ("text" または "markdown"、デフォルト: "text") |
| max_tokens | number | | 最大トークン数(デフォルト: 8192) |
| temperature | number | | 生成温度(0.0-2.0、デフォルト: 0.7) |
| model | string | | Geminiモデル(デフォルト: gemini-2.0-flash-exp) |
| region | string | | GCPリージョン(デフォルト: us-central1) |
##### サポートされているメディア形式
| カテゴリ | サポート形式 |
|---------|------------|
| 画像 | JPEG, PNG, GIF, WebP, BMP |
| 動画 | MP4, MPEG, MOV, AVI, FLV, MPG, WebM, WMV, 3GPP |
| 音声 | WAV, MP3, MPEG, AIFF, AAC, OGG, FLAC |
| ドキュメント | PDF, TXT, Markdown |
##### レスポンス
```typescript
{
"results": {
"textGeneration": {
"files": [{
"path": "generated-texts/xxx.txt",
"content_type": "text/plain",
"size": 1024
}],
"generatedText": "生成されたテキスト内容...",
"inputTokens": 500,
"outputTokens": 300,
"cost": 0.0225,
"processedMaterials": {
"images": 2,
"videos": 1,
"audio": 1,
"documents": 1
}
}
},
"assets": {
"generatedText": {
"url": "gs://bucket/generated-texts/xxx.txt",
"public_url": "https://storage.googleapis.com/...",
"content_type": "text/plain"
}
}
}
```
##### 実装例
**商品説明文生成ワークフロー**:
```dart
final workflow = WorkflowWorkflowModel(
name: "商品説明文生成",
prompt: "商品画像と動画から魅力的な説明文を作成",
materials: {
"images": [
"gs://bucket/products/product-photo1.jpg",
"gs://bucket/products/product-photo2.jpg",
"gs://bucket/products/product-photo3.jpg"
],
"videos": [
"gs://bucket/products/demo-video.mp4"
],
"audio": [
"gs://bucket/products/customer-review.mp3"
]
},
actions: [
WorkflowActionCommandValue(
command: "generate_text_from_multimodal",
index: 0,
data: {
"prompt": \"\"\"
提供された画像、動画、音声レビューを基に、以下の要素を含む商品説明文を作成してください:
1. 商品の主要な特徴(画像から読み取れる内容)
2. 使用方法とデモンストレーション(動画の内容)
3. 実際のユーザーの声(音声レビューの要約)
4. 商品のメリットとユニークセリングポイント
文体:親しみやすく、購買意欲を高める内容
文字数:800-1200文字程度
\"\"\",
"system_prompt": "あなたはECサイトの商品説明文を作成する専門のコピーライターです。視覚的要素と音声情報を総合的に分析し、魅力的な商品説明を作成してください。",
"output_format": "markdown",
"max_tokens": 2000,
"temperature": 0.8
}
)
]
);
```
**マルチメディアコンテンツ分析ワークフロー**:
```dart
final workflow = WorkflowWorkflowModel(
name: "プレゼンテーション資料分析",
prompt: "プレゼン資料の総合分析レポート作成",
materials: {
"images": [
"gs://bucket/presentation/slide1.png",
"gs://bucket/presentation/slide2.png",
"gs://bucket/presentation/slide3.png"
],
"videos": [
"gs://bucket/presentation/demo.mp4"
],
"documents": [
"gs://bucket/presentation/notes.pdf"
]
},
actions: [
WorkflowActionCommandValue(
command: "generate_text_from_multimodal",
index: 0,
data: {
"prompt": \"\"\"
プレゼンテーション資料を分析し、以下の形式でレポートを作成してください:
# プレゼンテーション分析レポート
## 1. 概要
- プレゼンの主題とメッセージ
- ターゲット・オーディエンス
## 2. スライド内容の要約
- 各スライドの主要ポイント
- 視覚的要素の効果
## 3. デモンストレーション内容
- 動画で示された機能や特徴
- デモの有効性評価
## 4. 改善提案
- プレゼンテーションの強化ポイント
- 追加すべき要素
\"\"\",
"output_format": "markdown",
"max_tokens": 4096,
"temperature": 0.5
}
)
]
);
```
**ストーリー生成ワークフロー**:
```dart
final workflow = WorkflowWorkflowModel(
name: "ビジュアルストーリー作成",
prompt: "画像と音楽からストーリーを生成",
materials: {
"images": [
"gs://bucket/story/scene1.jpg",
"gs://bucket/story/scene2.jpg",
"gs://bucket/story/scene3.jpg",
"gs://bucket/story/scene4.jpg"
],
"audio": [
"gs://bucket/story/bgm.mp3"
]
},
actions: [
WorkflowActionCommandValue(
command: "generate_text_from_multimodal",
index: 0,
data: {
"prompt": \"\"\"
提供された画像を順番に見て、BGMの雰囲気も考慮しながら、
これらを繋げた物語を創作してください。
- ジャンル:ファンタジー
- 文体:小説風
- 長さ:2000文字程度
- 各画像をシーンとして必ず含める
- BGMの雰囲気を文章に反映させる
\"\"\",
"system_prompt": "あなたは創造的な物語作家です。視覚的要素と音楽から感じる雰囲気を統合し、読者を引き込む物語を創作してください。",
"output_format": "text",
"max_tokens": 3000,
"temperature": 0.9
}
)
]
);
```
##### コスト計算
Gemini 2.0 Flash Experimentalの料金体系:
- 入力トークン: \$0.075 / 1M トークン
- 出力トークン: \$0.30 / 1M トークン
マルチモーダル入力の場合、メディアファイルはトークンに変換されます:
- 画像: 約258トークン/画像(1024x1024の場合)
- 動画: フレーム数×258トークン(サンプリングレートによる)
- 音声: 約25トークン/秒
### マーケティング分析機能 (masamune_workflow_marketing)
#### アプリストアデータ収集
**collect_from_google_play_console** - Google Playデータ収集
**アクションID**: `collect_from_google_play_console`
**パラメータ**:
```typescript
{
"packageName": "com.example.app", // 必須: アプリのパッケージ名
"startDate": "2024-01-01", // 任意: 開始日
"endDate": "2024-01-31" // 任意: 終了日
}
```
**レスポンス**:
```typescript
{
"results": {
"googlePlayData": {
"downloads": 10000,
"activeInstalls": 5000,
"rating": 4.5,
"ratingCount": 1234,
"recentReviews": [
{
"text": "素晴らしいアプリです",
"rating": 5,
"date": "2024-01-15"
}
]
}
}
}
```
**collect_from_app_store** - App Storeデータ収集
**アクションID**: `collect_from_app_store`
**パラメータ**:
```typescript
{
"appId": "123456789", // 必須: App Store アプリID
"vendorNumber": "87654321", // 任意: ベンダー番号
"startDate": "2024-01-01", // 任意: 開始日
"endDate": "2024-01-31" // 任意: 終了日
}
```
**collect_from_firebase_analytics** - Firebase Analyticsデータ収集
**アクションID**: `collect_from_firebase_analytics`
**パラメータ**:
```typescript
{
"propertyId": "123456789", // 必須: GA4プロパティID
"startDate": "2024-01-01", // 任意: 開始日
"endDate": "2024-01-31" // 任意: 終了日
}
```
**レスポンス**:
```typescript
{
"results": {
"analyticsData": {
"activeUsers": 5000,
"sessions": 15000,
"engagementRate": 0.65,
"averageSessionDuration": 180,
"events": {
"screen_view": 50000,
"purchase": 100
}
}
}
}
```
#### AI分析機能
**analyze_marketing_data** - マーケティングデータのAI分析
**アクションID**: `analyze_marketing_data`
**レスポンス**:
```typescript
{
"results": {
"marketingAnalysis": {
"overallAnalysis": {
"summary": "全体的に成長傾向",
"highlights": ["ユーザー数20%増加"],
"concerns": ["離脱率が上昇傾向"],
"keyMetrics": {...}
},
"improvementSuggestions": [
{
"title": "オンボーディング改善",
"description": "初回起動時の体験を改善",
"priority": "high",
"category": "UX",
"expectedImpact": "離脱率10%減少"
}
],
"trendAnalysis": {...},
"reviewAnalysis": {...},
"competitivePositioning": {...}
}
}
}
```
**generate_marketing_pdf** - PDFレポート生成
**アクションID**: `generate_marketing_pdf`
**パラメータ**:
```typescript
{
"reportType": "monthly", // 必須: daily/weekly/monthly
"startDate": "2024-01-01", // 任意: 開始日
"endDate": "2024-01-31" // 任意: 終了日
}
```
**レスポンス**:
```typescript
{
"assets": {
"marketingAnalyticsPdf": {
"url": "gs://bucket/reports/marketing_report.pdf",
"public_url": "https://storage.googleapis.com/...",
"content_type": "application/pdf"
}
}
}
```
#### GitHub解析機能
**analyze_github_init** - GitHubリポジトリ解析初期化
**アクションID**: `analyze_github_init`
**パラメータ**:
```typescript
{
"githubRepository": "owner/repo", // 必須: リポジトリ名
"githubRepositoryPath": "src" // 任意: 解析対象パス
}
```
**注意**: このアクションは自動的に複数の`analyze_github_process`アクションをタスクに追加します。
**analyze_github_process** - GitHub解析処理(自動生成)
**アクションID**: `analyze_github_process`
**パラメータ**:
```typescript
{
"batchIndex": 0 // 自動設定: バッチインデックス
}
```
**analyze_github_summary** - GitHub解析結果生成
**アクションID**: `analyze_github_summary`
**レスポンス**:
```typescript
{
"results": {
"githubAnalysis": {
"repository": "owner/repo",
"framework": "Flutter",
"overview": "モバイルアプリケーション",
"architecture": {...},
"features": [
{
"name": "認証機能",
"description": "ユーザー認証を管理",
"relatedFiles": ["auth.dart", "login.dart"]
}
]
}
}
}
```
#### 市場調査機能
**research_market** - 市場調査(Gemini + Google検索)
**アクションID**: `research_market`
**パラメータ**:
```typescript
{
"query": "フィットネスアプリ市場", // 必須: 調査クエリ
"region": "日本", // 任意: 対象地域
"includeCompetitors": true // 任意: 競合分析を含むか
}
```
**レスポンス**:
```typescript
{
"results": {
"marketResearch": {
"marketPotential": {
"summary": "成長市場",
"TAM": "1000億円",
"SAM": "100億円",
"SOM": "10億円",
"marketDrivers": [...],
"barriers": [...],
"targetSegments": [...]
},
"competitorAnalysis": {
"competitors": [...],
"marketLandscape": {...},
"competitiveAdvantages": [...],
"marketGaps": [...]
},
"businessOpportunities": [...]
}
}
}
```
**analyze_market_research** - 市場分析戦略生成
**アクションID**: `analyze_market_research`
**レスポンス**:
```typescript
{
"results": {
"marketStrategy": {
"demandForecast": {...},
"revenueStrategies": [...],
"trafficStrategies": [...],
"keyInsights": [...]
}
}
}
```
### セールス機能 (masamune_workflow_sales)
#### Google Play開発者情報収集
**アクションID**: `collect_google_play_developers`
**パラメータ(開発者ID指定モード)**:
```typescript
{
"mode": "developer_ids",
"developerIds": [
"5700313618786177705", // 開発者ID
"6720847872433251783"
],
"lang": "ja", // 任意: 言語(デフォルト: ja)
"country": "jp" // 任意: 国(デフォルト: jp)
}
```
**パラメータ(カテゴリランキングモード)**:
```typescript
{
"mode": "category_ranking",
"categoryConfig": {
"category": "GAME_ACTION", // カテゴリID
"collection": "TOP_FREE", // コレクション(TOP_FREE/TOP_PAID等)
"num": 100 // 取得数(デフォルト: 100)
},
"maxCount": 200 // 最大収集数
}
```
**パラメータ(キーワード検索モード)**:
```typescript
{
"mode": "search_keyword",
"searchConfig": {
"query": "fitness app", // 検索キーワード
"num": 50 // 検索結果数
},
"maxCount": 100
}
```
**レスポンス**:
```typescript
{
"results": {
"googlePlayDevelopers": {
"stats": {
"mode": "category_ranking",
"targetCount": 100,
"collectedCount": 95,
"withEmailCount": 45,
"savedCount": 45
},
"developers": [
{
"developerId": "5700313618786177705",
"developerName": "Example Developer",
"companyName": "Example Inc.",
"email": "support@example.com",
"website": "https://example.com",
"privacyPolicyUrl": "https://example.com/privacy",
"address": "Tokyo, Japan",
"apps": [
{
"appId": "com.example.app",
"title": "Example App",
"score": 4.5,
"price": 0,
"free": true,
"genre": "Tools"
}
]
}
]
}
}
}
```
#### App Store開発者情報収集
**アクションID**: `collect_app_store_developers`
**パラメータ**: Google Playと同様の構造
**レスポンス**:
```typescript
{
"results": {
"appStoreDevelopers": {
"stats": {...},
"developers": [
{
"developerId": 123456789,
"developerName": "Example Developer",
"email": "support@example.com",
"website": "https://example.com",
"apps": [...]
}
]
}
}
}
```
### 認証設定
各サービスの認証情報は、`WorkflowProjectModel`に保存します。
```dart
// Flutter側での認証情報設定例
final project = WorkflowProjectModel(
// Google認証(Google Play Console、Firebase Analytics等)
googleServiceAccount: "...", // サービスアカウントJSON
googleAccessToken: "...", // OAuth2アクセストークン
googleRefreshToken: "...", // OAuth2リフレッシュトークン
// GitHub認証
githubPersonalAccessToken: "ghp_...", // Personal Access Token
// App Store Connect認証
appstoreIssuerId: "...", // Issuer ID
appstoreAuthKeyId: "...", // Key ID
appstoreAuthKey: "-----BEGIN PRIVATE KEY-----...", // 秘密鍵
);
```
### 実装例 - 画像生成ワークフロー
```dart
// ワークフロー作成
final workflow = WorkflowWorkflowModel(
name: "画像生成ワークフロー",
prompt: "ブログ記事用の画像を生成",
actions: [
// 画像を生成
WorkflowActionCommandValue(
command: "generate_image_with_gemini",
index: 0,
data: {
"prompt": "美しい自然の風景、プロフェッショナルな写真",
"width": 1920,
"height": 1080,
"output_path": "blog/images",
},
),
// 音声ナレーションを生成
WorkflowActionCommandValue(
command: "generate_audio_with_google_tts",
index: 1,
data: {
"text": "本日のブログ記事をお届けします",
"voice_name": "ja-JP-Neural2-A",
"output_format": "mp3",
},
),
],
);
```
### 実装例 - マーケティング分析ワークフロー
```dart
// 月次マーケティングレポート生成
final workflow = WorkflowWorkflowModel(
name: "月次マーケティングレポート",
repeat: WorkflowRepeat.monthly,
actions: [
// Google Playデータ収集
WorkflowActionCommandValue(
command: "collect_from_google_play_console",
index: 0,
data: {
"packageName": "com.example.app",
},
),
// Firebase Analyticsデータ収集
WorkflowActionCommandValue(
command: "collect_from_firebase_analytics",
index: 1,
data: {
"propertyId": "123456789",
},
),
// AI分析
WorkflowActionCommandValue(
command: "analyze_marketing_data",
index: 2,
data: {},
),
// PDFレポート生成
WorkflowActionCommandValue(
command: "generate_marketing_pdf",
index: 3,
data: {
"reportType": "monthly",
},
),
],
);
```
### 実装例 - 競合開発者リサーチワークフロー
```dart
// 競合アプリ開発者の情報収集
final workflow = WorkflowWorkflowModel(
name: "競合開発者リサーチ",
actions: [
// カテゴリランキングから開発者情報収集
WorkflowActionCommandValue(
command: "collect_google_play_developers",
index: 0,
data: {
"mode": "category_ranking",
"categoryConfig": {
"category": "HEALTH_AND_FITNESS",
"collection": "TOP_FREE",
"num": 50,
},
"maxCount": 100,
},
),
// App Storeからも収集
WorkflowActionCommandValue(
command: "collect_app_store_developers",
index: 1,
data: {
"mode": "category_ranking",
"categoryConfig": {
"category": "6013", // Health & Fitness
"collection": "TOP_FREE",
"num": 50,
},
"maxCount": 100,
},
),
],
);
```
### エラーハンドリング
各アクションは`WorkflowTaskStatus`と`TaskLogPhase`で状態を管理します。
```dart
// アクションのログ確認
final action = ref.app.model(
WorkflowActionModel.document(actionId),
)..load();
// エラーログの確認
final errorLogs = action.value?.log
.where((log) => log.phase == TaskLogPhase.error)
.toList();
if (errorLogs?.isNotEmpty ?? false) {
for (final log in errorLogs!) {
print("エラー: \${log.message}");
print("詳細: \${log.data}");
}
}
```
### 利用量管理
各アクションの実行コストは`WorkflowUsageModel`で管理されます。
```dart
// 利用量の確認
final usage = ref.app.model(
WorkflowUsageModel.document(organizationId),
)..load();
// 現在の利用量
print("今月の利用量: \${usage.value?.currentMonth}");
print("バケット残高: \${usage.value?.bucketBalance}");
// プラン制限の確認
final plan = ref.app.model(
WorkflowPlanModel.document(usage.value?.latestPlan ?? ""),
)..load();
print("月間制限: \${plan.value?.monthlyLimit}");
print("バースト容量: \${plan.value?.burstCapacity}");
```
### Tips - バックエンド実装
- **環境変数**: Firebase Functions環境変数に各種APIキーを設定
```bash
firebase functions:config:set gemini.api_key="YOUR_API_KEY"
firebase functions:config:set google.tts_credentials="YOUR_CREDENTIALS"
```
- **タイムアウト設定**: 長時間実行タスクには適切なタイムアウトを設定
```typescript
export const longRunningTask = functions
.runWith({ timeoutSeconds: 540, memory: "1GB" })
.https.onRequest(...);
```
- **並列実行**: `analyze_github_process`のように、大量データ処理は並列実行で高速化
- **コスト管理とベストプラクティス**:
**API料金体系の詳細**:
- **Gemini 2.0 Flash**:
- 入力: \$0.075/1Mトークン(日本語は1文字約2トークン)
- 出力: \$0.30/1Mトークン
- 例: 1000文字入力→2000文字出力 = \$0.00075のコスト
- **Google TTS**:
- 標準音声: \$0.000004/文字(約\$4/100万文字)
- WaveNet音声: \$0.000016/文字(約\$16/100万文字)
- Neural2音声: \$0.000016/文字(約\$16/100万文字)
- **Firebase インフラ**:
- Functions: \$0.0000025/GB-秒 + \$0.40/100万呼び出し
- Firestore: 読み取り\$0.036/10万、書き込み\$0.108/10万、削除\$0.012/10万
- Storage: \$0.026/GB/月(保存)+ \$0.12/GB(ダウンロード)
**コスト最適化のベストプラクティス**:
1. **トークン使用量の最適化**:
```dart
// 非効率な例(冗長なプロンプト)
final prompt = \"""
あなたは優秀なAIアシスタントです。以下のテキストを要約してください。
重要な点を抜き出して、わかりやすく説明してください。
\${longText}
\"""; // 不要な説明でトークンを消費
// 効率的な例(簡潔なプロンプト)
final prompt = "要約: \${longText}"; // 必要最小限
```
2. **レスポンスキャッシュの活用**:
```dart
// 同じ入力に対する結果をキャッシュ
class CachedWorkflowAction {
static final _cache = <String, dynamic>{};
static Future<dynamic> execute(String prompt) async {
final cacheKey = prompt.hashCode.toString();
if (_cache.containsKey(cacheKey)) {
return _cache[cacheKey]; // キャッシュから返す(コスト\$0)
}
final result = await WorkflowAction.execute(prompt);
_cache[cacheKey] = result;
return result;
}
}
```
3. **バッチ処理の実装**:
```dart
// 個別処理(非効率)- 各リクエストで課金
for (final item in items) {
await processItem(item); // 100アイテム = 100回の課金
}
// バッチ処理(効率的)- まとめて処理
await processBatch(items); // 100アイテム = 1回の課金
```
4. **使用量モニタリングと自動制限**:
```dart
class CostController {
static Future<bool> canExecute(String organizationId, double estimatedCost) async {
final usage = await WorkflowUsageModel.document(organizationId).load();
final plan = await WorkflowPlanModel.document(usage.latestPlan ?? "").load();
// 月間上限の90%を超えたら高コストアクションを制限
if (usage.currentMonth > plan.monthlyLimit * 0.9) {
if (estimatedCost > 1.0) { // \$1以上のアクションを制限
throw Exception("月間使用量上限に近づいています。高コストアクションは制限されています。");
}
}
// 日次制限のチェック
final dailyLimit = plan.monthlyLimit / 30;
final todayUsage = await getTodayUsage(organizationId);
if (todayUsage + estimatedCost > dailyLimit) {
throw Exception("本日の使用量上限を超過します。");
}
return true;
}
}
```
5. **プロバイダー別コスト比較**:
| サービス | Google | OpenAI | Amazon | 自社ホスト |
|---------|--------|--------|---------|-----------|
| テキスト生成 | Gemini Flash<br>\$0.075/1M入力 | GPT-4<br>\$30/1M入力 | Claude<br>\$15/1M入力 | Llama<br>インフラコストのみ |
| 音声生成 | Google TTS<br>\$4/1M文字 | OpenAI TTS<br>\$15/1M文字 | Polly<br>\$4/1M文字 | - |
| 画像生成 | - | DALL-E 3<br>\$0.04/画像 | - | Stable Diffusion<br>\$0.005/画像 |
6. **請求書の自動化とコスト分析**:
```dart
// 部門別コスト配分
class CostAllocation {
static Future<Map<String, double>> analyzeMonthlyCost(String organizationId) async {
final usage = await WorkflowUsageModel.document(organizationId).load();
return {
"AI生成": usage.costBreakdown?["gemini"] ?? 0.0,
"音声": usage.costBreakdown?["tts"] ?? 0.0,
"ストレージ": usage.costBreakdown?["storage"] ?? 0.0,
"処理": usage.costBreakdown?["functions"] ?? 0.0,
};
}
}
```
7. **複数組織を持つユーザーのコスト管理**:
```dart
// 組織別コスト配分と予算管理
class MultiOrgCostManager {
final String userId;
final PageRef ref;
MultiOrgCostManager({
required this.userId,
required this.ref,
});
/// 組織別の利用量内訳を取得
Future<Map<String, OrgUsageDetail>> getOrganizationBreakdown() async {
final organizations = await ref.app.model(
WorkflowOrganizationModel.collection()
.ownerId.equal(userId),
)..load();
final breakdown = <String, OrgUsageDetail>{};
for (final org in organizations) {
final usage = await ref.app.model(
WorkflowUsageModel.document(org.uid),
)..load();
if (usage.value != null) {
breakdown[org.uid] = OrgUsageDetail(
organizationName: org.value?.name ?? "Unknown",
currentMonthUsage: usage.value!.currentMonth,
percentage: 0.0, // 後で計算
costBreakdown: usage.value!.costBreakdown ?? {},
);
}
}
// 合計に対する割合を計算
final total = breakdown.values
.fold(0.0, (sum, detail) => sum + detail.currentMonthUsage);
for (final entry in breakdown.entries) {
entry.value.percentage =
total > 0 ? (entry.value.currentMonthUsage / total) * 100 : 0;
}
return breakdown;
}
/// 組織ごとの予算を設定
Future<void> setOrganizationBudget(
String organizationId,
double monthlyBudget,
) async {
// 組織の使用量モデルに予算フィールドを追加
final usage = await ref.app.model(
WorkflowUsageModel.document(organizationId),
)..load();
await usage.save(
usage.value!.copyWith(
monthlyBudget: monthlyBudget,
budgetAlertThreshold: monthlyBudget * 0.8, // 80%でアラート
),
);
}
/// コスト最適化の提案を生成
Future<List<CostOptimizationSuggestion>> generateSuggestions() async {
final suggestions = <CostOptimizationSuggestion>[];
final breakdown = await getOrganizationBreakdown();
for (final entry in breakdown.entries) {
final detail = entry.value;
// 高コストAPIの使用を検出
if (detail.costBreakdown["gemini"] != null &&
detail.costBreakdown["gemini"]! > detail.currentMonthUsage * 0.7) {
suggestions.add(
CostOptimizationSuggestion(
organizationId: entry.key,
type: "API_OPTIMIZATION",
message: "\${detail.organizationName}はAI生成コストが70%以上です。"
"プロンプトの最適化やキャッシュの活用を検討してください。",
potentialSaving: detail.costBreakdown["gemini"]! * 0.2,
),
);
}
// 未使用の組織を検出
if (detail.currentMonthUsage < 0.01) {
suggestions.add(
CostOptimizationSuggestion(
organizationId: entry.key,
type: "UNUSED_ORGANIZATION",
message: "\${detail.organizationName}はほとんど使用されていません。"
"削除または統合を検討してください。",
potentialSaving: 0,
),
);
}
}
return suggestions;
}
}
class OrgUsageDetail {
final String organizationName;
final double currentMonthUsage;
double percentage;
final Map<String, double> costBreakdown;
OrgUsageDetail({
required this.organizationName,
required this.currentMonthUsage,
required this.percentage,
required this.costBreakdown,
});
}
class CostOptimizationSuggestion {
final String organizationId;
final String type;
final String message;
final double potentialSaving;
CostOptimizationSuggestion({
required this.organizationId,
required this.type,
required this.message,
required this.potentialSaving,
});
}
```
8. **組織間での利用量バランシング**:
```dart
// 組織間で利用量を最適配分
class UsageBalancer {
/// 利用量が少ない組織に処理を振り分ける
static Future<String> selectOptimalOrganization(
String userId,
double estimatedCost,
) async {
final orgs = await WorkflowOrganizationModel.collection()
.ownerId.equal(userId)
.load();
String optimalOrgId = "";
double lowestUsage = double.infinity;
for (final org in orgs) {
final usage = await WorkflowUsageModel.document(org.uid).load();
final currentUsage = usage.value?.currentMonth ?? 0;
// 予算に余裕がある組織を選択
if (currentUsage < lowestUsage) {
lowestUsage = currentUsage;
optimalOrgId = org.uid;
}
}
return optimalOrgId;
}
}
```
- **セキュリティ**:
- 認証情報は必ず暗号化して保存
- APIキーは環境変数で管理
- Firestoreセキュリティルールで適切なアクセス制御
- **デバッグ**:
- Firebase Functionsログでアクション実行を確認
- `TaskLogPhase.info`でカスタムログを記録
- エミュレータでローカルテスト
""";
}