タスク自動登録
:LiTarget: 用途
メール・Slack・カレンダーからタスクを自動抽出して登録するロジック。
:LiSparkle: 特徴
- 複数ソース対応
- AI抽出
- 重複除去
:LiCode: 実コード(SCALE Base より自動抽出)
:LiInfo:
lib/auto-register-task.tsの中身そのもの。コピペ即可。
// 手入力したタスク(朝日報・作業開始・作業終了 等の自由記述)を
// タスクシートに自動登録するヘルパー。
// 重複(同じ名前の自分担当・未完了タスク)がある場合は既存IDを返す。
import { isMineAssignee, type Task, type BottomTask } from './tasks-api';
import { getTodayDate, nowISO, genId } from './work-log';
import { normalizeName } from './normalize-name';
export interface AutoRegisterOptions {
taskTexts: string[]; // 登録したいタスク名の配列
baseTasks: Task[]; // 現在のタスクシート
currentUserName: string; // ログイン中ユーザー名
alreadySelectedIds?: string[]; // 既に baseTaskIds として選択済みのタスクID
alreadySelectedRoutineIds?: string[]; // 既に selectedRoutineIds として選択済みの定例タスクID
defaultDue?: string; // 期日(未指定なら今日)
memoPrefix?: string; // memo欄のプリフィックス(例: '朝日報で追加')
origin?: string; // 由来: morning / active / end / manual / slack-bot 等
// タスク名 → 推定時間(分) のマップ(カレンダー由来など、自動でセットしたい場合)
// 既存タスクで estimateMinutes が未設定のものにも適用する
estimateMinutesByName?: Map<string, number> | Record<string, number>;
// 定例タスクと同名のテキスト入力は、新規通常タスクを作らず定例ID として返す
// (routine と通常タスクの重複生成を防ぐ)
bottomTasks?: BottomTask[];
}
export interface AutoRegisterResult {
newTasks: Task[]; // 新規作成されたタスク
mergedBaseTaskIds: string[]; // 自動登録後の baseTaskIds(既存ID + 新規ID)
mergedRoutineIds: string[]; // 自動登録後の selectedRoutineIds(routine 同名で引き継いだIDも含む)
estimateUpdatedIds: string[]; // 既存タスクで estimateMinutes が新たにセットされた ID(カレンダー由来など)
skippedAsRoutine: string[]; // routine 同名でスキップされたタスク名(デバッグ用)
}
export function autoRegisterMemoTasks(opts: AutoRegisterOptions): AutoRegisterResult {
const {
taskTexts,
baseTasks,
currentUserName,
alreadySelectedIds = [],
alreadySelectedRoutineIds = [],
defaultDue,
memoPrefix = 'メモから自動登録',
estimateMinutesByName,
bottomTasks = [],
} = opts;
const today = defaultDue || getTodayDate();
// 複数担当(assignees) 込みで判定
const existingMineUnfinished = baseTasks.filter(t => isMineAssignee(t, currentUserName) && t.status !== '完了');
const selectedBaseNames = new Set(baseTasks.filter(t => alreadySelectedIds.includes(t.id)).map(t => t.name));
// estimateMinutesByName を統一的に扱うためのアクセサ
const getEst = (name: string): number | undefined => {
if (!estimateMinutesByName) return undefined;
if (estimateMinutesByName instanceof Map) return estimateMinutesByName.get(name);
return (estimateMinutesByName as Record<string, number>)[name];
};
const newTasks: Task[] = [];
const mergedBaseTaskIds = [...alreadySelectedIds];
const mergedRoutineIds = [...alreadySelectedRoutineIds];
const estimateUpdatedIds: string[] = [];
const skippedAsRoutine: string[] = [];
for (const rawText of taskTexts) {
const text = (rawText || '').trim();
if (!text) continue;
// 選択済みに同名があれば何もしない(ただし estimate を補完したい既存タスクは別途処理が必要)
if (selectedBaseNames.has(text)) {
// estimate 未設定なら補完候補として記録(呼び出し側で saveTasks に反映する想定)
const t = baseTasks.find(x => x.name === text && alreadySelectedIds.includes(x.id));
const est = getEst(text);
if (t && (typeof t.estimateMinutes !== 'number' || t.estimateMinutes <= 0) && typeof est === 'number' && est > 0) {
estimateUpdatedIds.push(t.id);
}
continue;
}
// ★ 定例タスク(bottomTasks)と同名なら、新規通常タスクを作らず定例ID として引き継ぐ
// これにより「LinkedIn シート更新」のような定例と同名のテキスト入力で
// 通常タスクが重複生成されるのを防ぐ(2026-04-30 ルール化)
const matchedRoutine = bottomTasks.find(b => b.name === text);
if (matchedRoutine) {
if (!mergedRoutineIds.includes(matchedRoutine.id)) mergedRoutineIds.push(matchedRoutine.id);
skippedAsRoutine.push(text);
continue;
}
// 既存で同名・自分担当・未完了 → そのIDを追加
const existing = existingMineUnfinished.find(t => t.name === text);
if (existing) {
if (!mergedBaseTaskIds.includes(existing.id)) mergedBaseTaskIds.push(existing.id);
// estimate 未設定なら補完候補として記録
const est = getEst(text);
if ((typeof existing.estimateMinutes !== 'number' || existing.estimateMinutes <= 0) && typeof est === 'number' && est > 0) {
estimateUpdatedIds.push(existing.id);
}
continue;
}
// 新規作成
const est = getEst(text);
// 担当者名は正準形(「大串」「細川」等)に正規化してプルダウン選択肢と一致させる
// user.name が「大串 勇輝」(フル表記)でも assignee には「大串」が入る
const canonicalUser = normalizeName(currentUserName);
const newTask: Task = {
id: genId('task'),
name: text,
assignee: canonicalUser,
requester: canonicalUser,
due: today,
originalDue: today,
priority: 3,
status: '未着手',
type: 'タスク',
project: '',
memo: memoPrefix,
link: '',
links: [],
description: '',
estimateMinutes: typeof est === 'number' && est > 0 ? est : undefined,
origin: opts.origin || 'manual',
todayFlag: true,
dateChanges: [],
statusChanges: [],
editHistory: [],
comments: [],
createdBy: canonicalUser,
createdAt: nowISO(),
completedDate: null,
confirmedBy: null,
};
newTasks.push(newTask);
mergedBaseTaskIds.push(newTask.id);
}
return { newTasks, mergedBaseTaskIds, mergedRoutineIds, estimateUpdatedIds, skippedAsRoutine };
}
// 名前一致でシートの該当タスクを完了にする(見つからなければ何もしない)
export function markSheetTaskCompleteByName(
baseTasks: Task[],
taskName: string,
currentUserName: string,
nowIso: string,
): { tasks: Task[]; matched: boolean } {
// 複数担当(assignees) 込みで判定
const match = baseTasks.find(t => t.name === taskName && isMineAssignee(t, currentUserName) && t.status !== '完了');
if (!match) return { tasks: baseTasks, matched: false };
const today = getTodayDate();
const next = baseTasks.map(t =>
t.id === match.id
? {
...t,
status: '完了',
completedDate: today,
statusChanges: [...(t.statusChanges || []), { from: t.status, to: '完了', user: currentUserName, time: nowIso, reason: '' }],
}
: t,
);
return { tasks: next, matched: true };
}
// IDで指定されたシートタスクを完了にする
export function markSheetTaskCompleteById(
baseTasks: Task[],
taskId: string,
currentUserName: string,
nowIso: string,
): Task[] {
const today = getTodayDate();
return baseTasks.map(t =>
t.id === taskId
? {
...t,
status: '完了',
completedDate: today,
statusChanges: [...(t.statusChanges || []), { from: t.status, to: '完了', user: currentUserName, time: nowIso, reason: '' }],
}
: t,
);
}
// シートタスクのステータスを変更(完了以外)
export function updateSheetTaskStatus(
baseTasks: Task[],
taskId: string,
newStatus: string,
currentUserName: string,
nowIso: string,
): Task[] {
return baseTasks.map(t => {
if (t.id !== taskId) return t;
if (t.status === newStatus) return t;
return {
...t,
status: newStatus,
completedDate: newStatus === '完了' ? getTodayDate() : (newStatus === '未着手' ? null : t.completedDate),
statusChanges: [...(t.statusChanges || []), { from: t.status, to: newStatus, user: currentUserName, time: nowIso, reason: '' }],
};
});
}
// 定例タスクを「今日実行した」扱いにする(lastDoneを今日に)
export function markRoutineDoneToday(bottomTasks: BottomTask[], routineId: string): BottomTask[] {
const today = getTodayDate();
return bottomTasks.map(b => b.id === routineId ? { ...b, lastDone: today } : b);
}
:LiFolder: ソースファイルのパス
/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/lib/auto-register-task.ts
:LiHandPointer: 使い方
対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。
:LiAlertCircle: 注意事項
- 依存パッケージを忘れず追加