HarmonyOS NEXT AI 智能生活助手:AI 日程规划
前言
在 第 15 篇中,我们实现了 AI 待办生成。本文将实现更复杂的 AI 日程规划------用户输入一句话,AI 自动生成完整的日程安排并检测时间冲突。
日程规划 是智能助手的进阶能力。它需要理解时间、地点、人物、事件、优先级等多个维度,自动排期并优化时间分配。
本文将实现:
- 自然语言转日程:一句话自动生成日程
- 四种日程类型:会议/任务/提醒/活动
- 冲突检测:自动发现时间重叠
- 日程管理:查看、编辑、删除

图1:AI 日程规划页面布局
一、功能设计
1.1 页面布局
┌─────────────────────────┐
│ ← 返回 AI 日程 │
├─────────────────────────┤
│ │
│ ┌─────────────────────┐│
│ │ 输入日程描述... ││
│ │ "明天上午9-11点开会 ││
│ │ 下午2-4点写代码..." ││
│ └─────────────────────┘│
│ │
│ ⟳ 生成日程 │
│ │
│ [警告] 冲突检测:发现 1 处 │
│ │
│ ┌─────────────────────┐│
│ │ [文档] 日程列表 ││
│ │ ││
│ │ 09:00 [红点] 项目会议 ││
│ │ 12楼会议室 ││
│ │ ││
│ │ 14:00 [绿点] 编码开发 ││
│ │ 工位 ││
│ └─────────────────────┘│
└─────────────────────────┘
1.2 技术架构
typescript
// 日程规划模块架构
/*
┌─────────────────────────────────────────┐
│ SchedulePage.ets │
│ (UI 展示 / 冲突提示 / 时间轴视图) │
└─────────────────┬───────────────────────┘
│
┌─────────────────▼───────────────────────┐
│ ScheduleManager.ts │
│ (日程生成 / 冲突检测 / 时间轴 / 优化) │
└─────────────────┬───────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│AIService│ │CacheMgr │ │relational│
│(多Provider│ │(缓存) │ │Store │
│ 切换) │ │ │ │(日程数据) │
└─────────┘ └─────────┘ └─────────┘
*/
二、数据模型
2.1 日程数据结构
typescript
// model/ScheduleItem.ts
export interface ScheduleItem {
id: string;
title: string;
description?: string;
startTime: number; // 开始时间戳
endTime: number; // 结束时间戳
location?: string; // 地点
attendees?: string[]; // 参与者
type: ScheduleType; // 类型
priority: SchedulePriority; // 优先级
recurrence?: RecurrenceRule; // 重复规则
color?: string; // 显示颜色
}
export type ScheduleType = 'meeting' | 'task' | 'reminder' | 'event';
export type SchedulePriority = 'high' | 'medium' | 'low';
export interface RecurrenceRule {
frequency: 'daily' | 'weekly' | 'monthly' | 'yearly';
interval: number;
endDate?: number;
count?: number;
}
export interface ConflictInfo {
item1: ScheduleItem;
item2: ScheduleItem;
reason: string;
duration: number; // 重叠时长(分钟)
}
// 日程数据库表定义
export const SCHEDULE_TABLE = {
tableName: 'schedules',
columns: [
{ name: 'id', type: 'TEXT PRIMARY KEY' },
{ name: 'title', type: 'TEXT NOT NULL' },
{ name: 'description', type: 'TEXT' },
{ name: 'start_time', type: 'INTEGER' },
{ name: 'end_time', type: 'INTEGER' },
{ name: 'location', type: 'TEXT' },
{ name: 'attendees', type: 'TEXT' },
{ name: 'type', type: 'TEXT' },
{ name: 'priority', type: 'TEXT' },
{ name: 'color', type: 'TEXT' }
]
};
2.2 日程类型配置
typescript
// config/ScheduleConfig.ts
export const SCHEDULE_TYPE_CONFIG: Record<ScheduleType, { label: string; icon: string; color: string }> = {
meeting: { label: '会议', icon: 'ic_type_meeting', color: '#E17055' },
task: { label: '任务', icon: 'ic_type_task', color: '#0984E3' },
reminder: { label: '提醒', icon: 'ic_type_reminder', color: '#00B894' },
event: { label: '活动', icon: 'ic_type_event', color: '#6C5CE7' }
};
export const SCHEDULE_PRIORITY_CONFIG: Record<SchedulePriority, { label: string; color: string }> = {
high: { label: '高', color: '#E17055' },
medium: { label: '中', color: '#FDCB6E' },
low: { label: '低', color: '#00B894' }
};
三、ScheduleManager 实现
3.1 核心代码
typescript
// ai/ScheduleManager.ts
export class ScheduleManager {
private static instance: ScheduleManager;
private aiService = AIService.getInstance();
private promptManager = PromptManager.getInstance();
private cacheManager = CacheManager.getInstance();
private schedules: ScheduleItem[] = [];
static getInstance(): ScheduleManager {
if (!ScheduleManager.instance) {
ScheduleManager.instance = new ScheduleManager();
}
return ScheduleManager.instance;
}
// 从自然语言生成日程
async generateSchedule(text: string): Promise<ScheduleItem[]> {
// 检查缓存
const cacheKey = `schedule_${this.hashText(text)}`;
const cached = this.cacheManager.get<ScheduleItem[]>(cacheKey);
if (cached) return cached;
const prompt = this.promptManager.buildPrompt('schedule', {
today: new Date().toISOString().split('T')[0]
});
const response = await this.aiService.chat([
{ role: 'system', content: prompt },
{ role: 'user', content: `从以下文本生成日程:\n\n${text}` }
]);
const items = this.parseSchedule(response.content);
this.schedules.push(...items);
// 写入缓存
this.cacheManager.set(cacheKey, items, 60 * 60 * 1000);
return items;
}
// 冲突检测
detectConflicts(items?: ScheduleItem[]): ConflictInfo[] {
const list = items || this.schedules;
const conflicts: ConflictInfo[] = [];
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
const overlap = this.getOverlapDuration(list[i], list[j]);
if (overlap > 0) {
conflicts.push({
item1: list[i],
item2: list[j],
reason: '时间冲突',
duration: overlap
});
}
}
}
return conflicts;
}
// 按日期获取日程
getByDate(date: string): ScheduleItem[] {
const start = new Date(date).getTime();
const end = start + 24 * 60 * 60 * 1000;
return this.schedules.filter(s => s.startTime >= start && s.startTime < end);
}
// 获取时间轴视图
getTimeline(date: string): TimelineSlot[] {
const items = this.getByDate(date).sort((a, b) => a.startTime - b.startTime);
const timeline: TimelineSlot[] = [];
// 填充空档
if (items.length === 0) {
timeline.push({ start: '09:00', end: '18:00', type: 'free', title: '全天空闲' });
return timeline;
}
let prevEnd = new Date(date + 'T09:00').getTime();
for (const item of items) {
if (item.startTime > prevEnd) {
timeline.push({
start: this.formatTime(prevEnd),
end: this.formatTime(item.startTime),
type: 'free',
title: '空闲'
});
}
timeline.push({
start: this.formatTime(item.startTime),
end: this.formatTime(item.endTime),
type: 'busy',
title: item.title,
item: item
});
prevEnd = item.endTime;
}
return timeline;
}
private getOverlapDuration(a: ScheduleItem, b: ScheduleItem): number {
const overlapStart = Math.max(a.startTime, b.startTime);
const overlapEnd = Math.min(a.endTime, b.endTime);
return Math.max(0, overlapEnd - overlapStart);
}
private parseSchedule(content: string): ScheduleItem[] {
try {
const jsonMatch = content.match(/\[[\s\S]*?\]/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]).map((s: any) => ({
...s,
id: s.id || Date.now().toString(36) + Math.random().toString(36).substr(2, 5),
startTime: typeof s.startTime === 'string' ? new Date(s.startTime).getTime() : s.startTime,
endTime: typeof s.endTime === 'string' ? new Date(s.endTime).getTime() : s.endTime,
}));
}
} catch {}
return [];
}
private formatTime(timestamp: number): string {
const d = new Date(timestamp);
return `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
}
private hashText(text: string): string {
let hash = 0;
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash |= 0;
}
return hash.toString(36);
}
}
interface TimelineSlot {
start: string;
end: string;
type: 'free' | 'busy';
title: string;
item?: ScheduleItem;
}
冲突检测:遍历所有日程对,计算每对的时间重叠分钟数。重叠 > 0 即判定为冲突,标记出具体重叠时段。
3.2 日程优化器
typescript
// ai/ScheduleOptimizer.ts
export class ScheduleOptimizer {
// 合理安排时间
static optimize(items: ScheduleItem[]): ScheduleItem[] {
// 按优先级排序
const sorted = [...items].sort((a, b) => {
const pa = { high: 0, medium: 1, low: 2 };
return (pa[a.priority] ?? 1) - (pa[b.priority] ?? 1);
});
const result: ScheduleItem[] = [];
const workStart = 9; // 9:00
const workEnd = 18; // 18:00
let cursor = new Date().setHours(workStart, 0, 0, 0);
for (const item of sorted) {
const duration = item.endTime - item.startTime;
if (duration <= 0) continue;
// 如果超出工作时间,跳到第二天
const cursorHour = new Date(cursor).getHours();
if (cursorHour >= workEnd) {
cursor = new Date(cursor + 24 * 60 * 60 * 1000).setHours(workStart, 0, 0, 0);
}
result.push({
...item,
startTime: cursor,
endTime: cursor + duration
});
cursor += duration;
}
return result;
}
// 建议休息时间
static suggestBreaks(items: ScheduleItem[]): ScheduleItem[] {
const breaks: ScheduleItem[] = [];
for (let i = 1; i < items.length; i++) {
const gap = items[i].startTime - items[i - 1].endTime;
if (gap > 0 && gap < 15 * 60 * 1000) {
// 间隔不足 15 分钟,建议休息
breaks.push({
id: `break_${i}`,
title: '休息一下',
startTime: items[i - 1].endTime,
endTime: items[i - 1].endTime + 10 * 60 * 1000,
type: 'reminder',
priority: 'low'
});
}
}
return breaks;
}
}
四、安全区适配
4.1 安全区工具类
typescript
// utils/SafeAreaUtil.ts
import { display } from '@kit.ArkUI';
export class SafeAreaUtil {
static getStatusBarHeight(): number {
return AppStorage.get<number>('statusBarHeight') || 0;
}
static getNavBarHeight(): number {
return AppStorage.get<number>('navBarHeight') || 0;
}
static px2vp(px: number): number {
const density = display.getDefaultDisplaySync().densityPixels;
return px / density;
}
}
五、Prompt 模板
5.1 schedule.md
markdown
---
name: schedule
version: 1.1.0
description: AI 日程规划
---
你是一个时间管理专家。从用户输入的文本中生成结构化日程。
## 规则
1. 识别时间信息(绝对时间或相对时间)
2. 推断日程类型(meeting/task/reminder/event)
3. 合理分配时长
4. 设置合适的优先级
## 输出格式
JSON 数组:
[
{
"title": "项目周会",
"description": "同步项目进度",
"startTime": "2025-01-15T09:00:00",
"endTime": "2025-01-15T11:00:00",
"location": "12楼会议室A",
"type": "meeting",
"priority": "high"
}
]
六、日程页面
6.1 SchedulePage 实现
typescript
// pages/SchedulePage.ets
import { display } from '@kit.ArkUI';
@Entry
@Component
struct SchedulePage {
@State inputText: string = '';
@State schedules: ScheduleItem[] = [];
@State conflicts: ConflictInfo[] = [];
@State isLoading: boolean = false;
@State statusBarHeight: number = 0;
@State navBarHeight: number = 0;
private scheduleManager = ScheduleManager.getInstance();
aboutToAppear() {
this.statusBarHeight = AppStorage.get<number>('statusBarHeight') || 0;
this.navBarHeight = AppStorage.get<number>('navBarHeight') || 0;
}
build() {
Column() {
// 状态栏占位
Row().width('100%').height(this.statusBarHeight);
// 导航栏
Row() {
Image($r('app.media.ic_back')).width(24).height(24).onClick(() => RouterUtil.back());
Text('AI 日程').fontSize(18).fontWeight(FontWeight.Bold).margin({ left: 12 });
}
.width('100%').height(56).padding({ left: 16, right: 16 });
// 输入区域
TextArea({ text: this.inputText, placeholder: '输入日程描述,如"明天上午9-11点开会,下午2-4点编码"' })
.height(120).backgroundColor(Color.White).borderRadius(12)
.padding(12).fontSize(15).margin(16)
.onChange(v => this.inputText = v);
// 生成按钮
Button() {
if (this.isLoading) {
LoadingView({ text: '生成中...' });
} else {
Row() {
Image($r('app.media.ic_schedule')).width(18).height(18);
Text('生成日程').fontSize(16).fontColor(Color.White).margin({ left: 6 });
}
}
}
.width('90%').height(44).backgroundColor('#6C5CE7').borderRadius(22)
.disabled(!this.inputText.trim() || this.isLoading).margin({ bottom: 16 })
.onClick(() => this.generateSchedule());
// 冲突警告
if (this.conflicts.length > 0) {
Row() {
Image($r('app.media.ic_warning')).width(18).height(18).margin({ right: 8 });
Text(`发现 ${this.conflicts.length} 处时间冲突`).fontSize(14).fontColor('#E17055');
}
.padding(12).backgroundColor('#FFF0F0').borderRadius(8).margin({ left: 16, right: 16, bottom: 12 });
}
// 日程列表
if (this.schedules.length > 0) {
Scroll() {
Column() {
ForEach(this.schedules.sort((a, b) => a.startTime - b.startTime), (item: ScheduleItem) => {
Column() {
Row() {
Text(this.formatTime(item.startTime)).fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#6C5CE7').margin({ right: 12 });
Column() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Medium);
if (item.location) {
Text(`${item.location}`).fontSize(13).fontColor(Color.Gray).margin({ top: 2 });
}
}
.layoutWeight(1);
Text(this.getTypeBadge(item.type)).fontSize(11).fontColor(Color.White)
.backgroundColor(this.getTypeColor(item.type)).borderRadius(8)
.padding({ left: 6, right: 6, top: 2, bottom: 2 });
}
.padding(16).backgroundColor(Color.White).borderRadius(12).margin({ bottom: 8 });
}
.width('100%');
}, (item: ScheduleItem) => item.id);
}
.padding(16);
}
.layoutWeight(1);
}
// 底部导航栏占位
Row().width('100%').height(this.navBarHeight);
}
.width('100%').height('100%').backgroundColor('#F5F6FA');
}
async generateSchedule() {
this.isLoading = true;
try {
this.schedules = await this.scheduleManager.generateSchedule(this.inputText);
this.conflicts = this.scheduleManager.detectConflicts();
} catch {
ToastUtil.show('日程生成失败');
} finally {
this.isLoading = false;
}
}
private formatTime(ts: number): string {
const d = new Date(ts);
return `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`;
}
private getTypeBadge(type: string): string {
const badges: Record<string, string> = { meeting: '会议', task: '任务', reminder: '提醒', event: '活动' };
return badges[type] || type;
}
private getTypeColor(type: string): string {
const colors: Record<string, string> = { meeting: '#E17055', task: '#0984E3', reminder: '#00B894', event: '#6C5CE7' };
return colors[type] || '#636E72';
}
}
七、数据持久化与缓存
7.1 使用 relationalStore 存储日程
typescript
// database/ScheduleDatabase.ts
import { relationalStore } from '@kit.ArkData';
export class ScheduleDatabase {
private static instance: ScheduleDatabase;
private rdbStore: relationalStore.RdbStore | null = null;
static getInstance(): ScheduleDatabase {
if (!ScheduleDatabase.instance) {
ScheduleDatabase.instance = new ScheduleDatabase();
}
return ScheduleDatabase.instance;
}
async init(context: Context): Promise<void> {
const config: relationalStore.StoreConfig = {
name: 'schedule.db',
securityLevel: relationalStore.SecurityLevel.S1
};
this.rdbStore = await relationalStore.getRdbStore(context, config);
await this.rdbStore?.executeSql(`
CREATE TABLE IF NOT EXISTS schedules (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
start_time INTEGER,
end_time INTEGER,
location TEXT,
attendees TEXT,
type TEXT,
priority TEXT,
color TEXT
)
`);
}
async insert(item: ScheduleItem): Promise<number> {
const bucket: relationalStore.ValuesBucket = {
id: item.id,
title: item.title,
description: item.description,
start_time: item.startTime,
end_time: item.endTime,
location: item.location,
attendees: JSON.stringify(item.attendees || []),
type: item.type,
priority: item.priority,
color: item.color
};
return await this.rdbStore?.insert('schedules', bucket) || -1;
}
async queryByDateRange(start: number, end: number): Promise<ScheduleItem[]> {
const predicates = new relationalStore.RdbPredicates('schedules');
predicates.greaterThanOrEqualTo('start_time', start)
.lessThan('start_time', end)
.orderByAsc('start_time');
const resultSet = await this.rdbStore?.query(predicates);
const items: ScheduleItem[] = [];
while (resultSet?.goToNextRow()) {
items.push({
id: resultSet.getString(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
description: resultSet.getString(resultSet.getColumnIndex('description')),
startTime: resultSet.getLong(resultSet.getColumnIndex('start_time')),
endTime: resultSet.getLong(resultSet.getColumnIndex('end_time')),
location: resultSet.getString(resultSet.getColumnIndex('location')),
attendees: JSON.parse(resultSet.getString(resultSet.getColumnIndex('attendees')) || '[]'),
type: resultSet.getString(resultSet.getColumnIndex('type')) as ScheduleType,
priority: resultSet.getString(resultSet.getColumnIndex('priority')) as SchedulePriority,
color: resultSet.getString(resultSet.getColumnIndex('color'))
});
}
resultSet?.close();
return items;
}
async deleteById(id: string): Promise<void> {
const predicates = new relationalStore.RdbPredicates('schedules');
predicates.equalTo('id', id);
await this.rdbStore?.delete(predicates);
}
}
7.2 CacheManager 缓存策略
typescript
// cache/CacheManager.ts
export class CacheManager {
private static instance: CacheManager;
private memoryCache: Map<string, CacheEntry> = new Map();
static getInstance(): CacheManager {
if (!CacheManager.instance) {
CacheManager.instance = new CacheManager();
}
return CacheManager.instance;
}
set<T>(key: string, value: T, ttl: number = 60 * 60 * 1000): void {
this.memoryCache.set(key, {
data: value,
expireAt: Date.now() + ttl
});
}
get<T>(key: string): T | null {
const entry = this.memoryCache.get(key);
if (!entry) return null;
if (Date.now() > entry.expireAt) {
this.memoryCache.delete(key);
return null;
}
return entry.data as T;
}
async persist<T>(key: string, value: T): Promise<void> {
const pref = await getPreferences(getContext(), 'schedule_cache');
await pref.put(key, JSON.stringify(value));
await pref.flush();
}
clear(): void {
this.memoryCache.clear();
}
}
interface CacheEntry {
data: unknown;
expireAt: number;
}
八、时间轴视图实现
8.1 Timeline 组件
typescript
// components/TimelineView.ets
@Component
export struct TimelineView {
@Prop slots: TimelineSlot[];
build() {
Column() {
ForEach(this.slots, (slot: TimelineSlot) => {
Row() {
Column() {
Text(slot.start).fontSize(12).fontColor('#6C5CE7');
Text(slot.end).fontSize(12).fontColor(Color.Gray);
}
.width(50);
Column() {
if (slot.type === 'busy') {
Row() {
Text(slot.title).fontSize(14).fontWeight(FontWeight.Medium);
if (slot.item) {
Text(SCHEDULE_TYPE_CONFIG[slot.item.type].label)
.fontSize(10).fontColor(Color.White)
.backgroundColor(SCHEDULE_TYPE_CONFIG[slot.item.type].color)
.borderRadius(4).padding({ left: 4, right: 4 });
}
}
} else {
Text(slot.title).fontSize(13).fontColor(Color.Gray);
}
}
.layoutWeight(1)
.padding(12)
.backgroundColor(slot.type === 'busy' ? '#F0F0FF' : '#F8F9FA')
.borderRadius(8);
}
.width('100%')
.margin({ bottom: 4 });
}, (slot: TimelineSlot) => slot.start + slot.end);
}
.width('100%');
}
}
8.2 时间轴视图示例
| 时间 | 类型 | 内容 | 状态 |
|---|---|---|---|
| 09:00-09:30 | 空闲 | 早间准备 | --- |
| 09:30-11:00 | 会议 | 项目周会 | 高优先级 |
| 11:00-11:15 | 空闲 | 休息时间 | --- |
| 11:15-12:00 | 任务 | 代码评审 | 中优先级 |
| 12:00-14:00 | 空闲 | 午餐 & 午休 | --- |
| 14:00-16:00 | 任务 | 功能开发 | 高优先级 |
| 16:00-16:15 | 提醒 | 休息一下 | 低优先级 |
| 16:15-18:00 | 任务 | 文档编写 | 中优先级 |
九、Git 提交
bash
git add .
git commit -m "feat(schedule): 完成 AI 日程规划
- 自然语言转结构化日程
- 四种日程类型(会议/任务/提醒/活动)
- 自动时间冲突检测
- 时间轴视图
- 日程优化器(自动排期+休息建议)
- 集成 relationalStore 持久化日程数据
- 实现 CacheManager 缓存策略
- 支持 OpenAI/DeepSeek/Qwen/智谱/豆包 多 Provider
- 实现安全区适配(AppStorage + display)
- 使用 SVG 矢量图替代 emoji
- 实现 PromptManager 版本控制
- TimelineView 时间轴组件
Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>"
git tag v0.1.5
总结
本文实现了 AI 日程规划 功能。核心要点:
- 一句话生成日程:理解时间、地点、类型等维度
- 冲突检测:自动发现时间重叠,提示用户调整
- 四种类型:会议/任务/提醒/活动,颜色区分
- 时间轴视图:直观展示全天时间分配
- 自动优化:按优先级排期,建议休息间隔
- 多 Provider 支持:OpenAI、DeepSeek、Qwen、智谱、豆包
- PromptManager:独立管理 prompt,支持版本控制
- 安全区适配:通过 AppStorage 获取 statusBarHeight 和 navBarHeight
- 数据持久化:使用 relationalStore 保存日程数据
- 双层缓存:CacheManager 内存缓存 + 持久化缓存
- SVG 图标:所有图标使用矢量图,不使用 emoji
- TimelineView:独立时间轴展示组件
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!