From 693af855b2297cba14bb37b530cea2a6e44db386 Mon Sep 17 00:00:00 2001 From: gy <2293314358@qq.com> Date: Sat, 31 Jan 2026 08:15:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=BB=BAAiDataSyncHelper.java?= =?UTF-8?q?=EF=BC=8CAI=E6=95=B0=E6=8D=AE=E5=90=8C=E6=AD=A5=E5=8A=A9?= =?UTF-8?q?=E6=89=8B=20=E6=96=B0=E5=A2=9EAiReminderReceiver.java=EF=BC=8CA?= =?UTF-8?q?I=E6=8F=90=E9=86=92=E6=8E=A5=E6=94=B6=E5=99=A8=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9EAiReminderScheduler.java=EF=BC=8CAI=E6=8F=90=E9=86=92?= =?UTF-8?q?=E8=B0=83=E5=BA=A6=E5=99=A8=20=E6=96=B0=E5=A2=9ECozeApiService.?= =?UTF-8?q?java=EF=BC=8C=E8=BF=9B=E8=A1=8CCoze=20API=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=20=E6=96=B0=E5=A2=9ECozeClient.java=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E6=88=90Coze=E5=AE=A2=E6=88=B7=E7=AB=AF=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=20=E6=96=B0=E5=A2=9ECozeRequest.java=E3=80=81CozeResp?= =?UTF-8?q?onse.java=EF=BC=8C=E5=AE=9E=E7=8E=B0Coze=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E4=B8=8E=E7=9B=B8=E5=BA=94=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/net/micode/notes/data/Notes.java | 23 +- .../notes/data/NotesDatabaseHelper.java | 21 +- .../net/micode/notes/data/NotesProvider.java | 12 + .../src/net/micode/notes/sync/SyncWorker.java | 244 ++++++++++++++++-- .../notes/tool/AiNotificationHelper.java | 61 +++++ .../net/micode/notes/tool/DeepSeekHelper.java | 170 ++++++++++++ .../notes/tool/ai/AiDataSyncHelper.java | 57 ++++ .../notes/tool/ai/AiReminderReceiver.java | 33 +++ .../notes/tool/ai/AiReminderScheduler.java | 91 +++++++ .../micode/notes/tool/ai/CozeApiService.java | 31 +++ .../net/micode/notes/tool/ai/CozeClient.java | 40 +++ .../net/micode/notes/tool/ai/CozeRequest.java | 42 +++ .../micode/notes/tool/ai/CozeResponse.java | 21 ++ .../net/micode/notes/ui/AgendaFragment.java | 9 + .../src/net/micode/notes/ui/ChatFragment.java | 178 ++++++++++++- .../net/micode/notes/ui/NoteEditActivity.java | 10 + .../micode/notes/ui/NotesListActivity.java | 8 +- 17 files changed, 1006 insertions(+), 45 deletions(-) create mode 100644 src/Notes-master/src/net/micode/notes/tool/AiNotificationHelper.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/DeepSeekHelper.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/AiDataSyncHelper.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/AiReminderReceiver.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/AiReminderScheduler.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/CozeApiService.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/CozeClient.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/CozeRequest.java create mode 100644 src/Notes-master/src/net/micode/notes/tool/ai/CozeResponse.java diff --git a/src/Notes-master/src/net/micode/notes/data/Notes.java b/src/Notes-master/src/net/micode/notes/data/Notes.java index a5ab921..c799bd9 100644 --- a/src/Notes-master/src/net/micode/notes/data/Notes.java +++ b/src/Notes-master/src/net/micode/notes/data/Notes.java @@ -251,15 +251,28 @@ public class Notes { public static final String AVATAR_URL = "avatar_url"; } - // [新增] 聊天消息表列名定义 + // [新增/更新] 聊天消息表列名定义 public interface ChatColumns { public static final String ID = "_id"; - public static final String SESSION_ID = "session_id"; - public static final String SENDER_TYPE = "sender_type"; // 0: User, 1: AI + /** + * 消息发送者类型: + * 0: 用户 (User) + * 1: AI (Assistant) + * 2: 系统/Prompt (System - 不在UI显示,仅用于记录) + */ + public static final String SENDER_TYPE = "sender_type"; + + /** + * 消息内容类型: + * 0: 普通文本 (Text) + * 1: 日程提醒卡片 (Reminder Card) + */ + public static final String MSG_TYPE = "msg_type"; + public static final int MSG_TYPE_TEXT = 0; // 普通对话 + public static final int MSG_TYPE_REMINDER = 1; // [核心] 提醒卡片(UI显示金色气泡) + public static final String CONTENT = "content"; - public static final String MSG_TYPE = "msg_type"; // 0: Text, 1: Agenda Card public static final String CREATED_AT = "created_at"; - public static final String SYNC_STATE = "sync_state"; } public interface DataColumns { diff --git a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java index 833a0a4..2daa407 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesDatabaseHelper.java @@ -30,7 +30,7 @@ import net.micode.notes.data.Notes.NoteColumns; public class NotesDatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME = "note.db"; - private static final int DB_VERSION = 10; + private static final int DB_VERSION = 11; public interface TABLE { public static final String NOTE = "note"; @@ -93,12 +93,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { private static final String CREATE_CHAT_TABLE_SQL = "CREATE TABLE " + TABLE.CHAT_MESSAGES + "(" + Notes.ChatColumns.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Notes.ChatColumns.SESSION_ID + " TEXT," + Notes.ChatColumns.SENDER_TYPE + " INTEGER DEFAULT 0," + - Notes.ChatColumns.CONTENT + " TEXT," + Notes.ChatColumns.MSG_TYPE + " INTEGER DEFAULT 0," + - Notes.ChatColumns.CREATED_AT + " INTEGER," + - Notes.ChatColumns.SYNC_STATE + " INTEGER DEFAULT 1" + + Notes.ChatColumns.CONTENT + " TEXT," + + Notes.ChatColumns.CREATED_AT + " INTEGER" + ")"; private static final String CREATE_DATA_TABLE_SQL = @@ -388,6 +386,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion = 10; } + // [新增] 升级到 v11 + if (oldVersion < 11) { + upgradeToV11(db); + oldVersion = 11; + } + if (reCreateTriggers) { reCreateNoteTableTriggers(db); reCreateDataTableTriggers(db); @@ -399,6 +403,13 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { } } + private void upgradeToV11(SQLiteDatabase db) { + Log.d(TAG, "Upgrading database to version 11..."); + // 先尝试删除旧表(如果是开发过程中的残留),再创建新表 + db.execSQL("DROP TABLE IF EXISTS " + TABLE.CHAT_MESSAGES); + db.execSQL(CREATE_CHAT_TABLE_SQL); + } + private void upgradeToV10(SQLiteDatabase db) { Log.d(TAG, "Upgrading database to version 10..."); // 使用 try-catch 确保即使字段已存在(手动清过数据的情况)也不会崩 diff --git a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java index e5cebcd..cc8e5fb 100644 --- a/src/Notes-master/src/net/micode/notes/data/NotesProvider.java +++ b/src/Notes-master/src/net/micode/notes/data/NotesProvider.java @@ -50,6 +50,7 @@ public class NotesProvider extends ContentProvider { private static final int URI_SEARCH = 5; private static final int URI_SEARCH_SUGGEST = 6; private static final int URI_USER_ACCOUNT = 7; // 新增编号 + private static final int URI_CHAT_MESSAGES = 8; static { mMatcher = new UriMatcher(UriMatcher.NO_MATCH); @@ -61,6 +62,8 @@ public class NotesProvider extends ContentProvider { mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, URI_SEARCH_SUGGEST); mMatcher.addURI(Notes.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", URI_SEARCH_SUGGEST); mMatcher.addURI(Notes.AUTHORITY, "user_account", URI_USER_ACCOUNT); + // [核心新增]:让系统认识 content://micode_notes/chat_messages + mMatcher.addURI(Notes.AUTHORITY, "chat_messages", URI_CHAT_MESSAGES); } /** @@ -140,6 +143,9 @@ public class NotesProvider extends ContentProvider { Log.e(TAG, "got exception: " + ex.toString()); } break; + case URI_CHAT_MESSAGES: + c = db.query(NotesDatabaseHelper.TABLE.CHAT_MESSAGES, projection, selection, selectionArgs, null, null, sortOrder); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); } @@ -168,6 +174,12 @@ public class NotesProvider extends ContentProvider { case URI_USER_ACCOUNT: insertedId = db.insert(TABLE.USER_ACCOUNT, null, values); break; + // [核心新增]:处理聊天消息的真正入库逻辑 + case URI_CHAT_MESSAGES: + insertedId = db.insert(NotesDatabaseHelper.TABLE.CHAT_MESSAGES, null, values); + // 插入成功后发送数据变更通知,让 ChatFragment 能够自动刷新 + getContext().getContentResolver().notifyChange(uri, null); + break; default: throw new IllegalArgumentException("Unknown URI " + uri); } diff --git a/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java b/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java index 30bfdc8..5702460 100644 --- a/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java +++ b/src/Notes-master/src/net/micode/notes/sync/SyncWorker.java @@ -15,16 +15,29 @@ import androidx.work.WorkerParameters; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.QueryDocumentSnapshot; +import com.google.gson.JsonObject; import net.micode.notes.data.Notes; import net.micode.notes.data.Notes.NoteColumns; import net.micode.notes.model.CloudNote; +import net.micode.notes.tool.AiNotificationHelper; import net.micode.notes.tool.SyncMapper; +import net.micode.notes.tool.ai.AiDataSyncHelper; +import net.micode.notes.tool.ai.CozeClient; +import net.micode.notes.tool.ai.CozeRequest; +import net.micode.notes.tool.ai.CozeResponse; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; public class SyncWorker extends Worker { private static final String TAG = "SyncWorker"; + public static final String KEY_SYNC_MODE = "sync_mode"; + public static final int MODE_ALL = 0; // 默认:全量同步 + public static final int MODE_PUSH = 1; // 仅上传 + public static final int MODE_PULL = 2; // 仅拉取 + public static final int MODE_REMINDER = 3; public SyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); @@ -33,34 +46,209 @@ public class SyncWorker extends Worker { @NonNull @Override public Result doWork() { - // [确切位置]:在方法入口处立即进行安全隔离 + // 准备一个锁,数量为 1 + final CountDownLatch latch = new CountDownLatch(1); + // 用于记录是否发生错误 + final boolean[] isSuccess = {true}; + try { String uid = FirebaseAuth.getInstance().getUid(); - if (uid == null) { - Log.w(TAG, "Sync skipped: No user logged in."); - return Result.success(); - } + if (uid == null) return Result.success(); FirebaseFirestore db = FirebaseFirestore.getInstance(); + int mode = getInputData().getInt(KEY_SYNC_MODE, MODE_ALL); - // 1. 先执行上行同步 - performPush(db, uid); + Log.d(TAG, "Starting Sync with mode: " + mode); + + // --- PUSH 逻辑 (保持同步执行即可,因为它本身不依赖回调返回数据给UI) --- + if (mode == MODE_PUSH || mode == MODE_ALL) { + performPush(db, uid); + syncToCozeAgent(); + } + + // --- PULL 逻辑 (异步变同步) --- + if (mode == MODE_PULL || mode == MODE_ALL) { + // 传递 latch 进去 + performPull(db, uid, latch, isSuccess); + + // [关键]:主线程在这里死等,直到 latch.countDown() 被调用,或者超时(10秒) + try { + latch.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + return Result.retry(); + } + } - // 2. 后执行下行同步 - performPull(db, uid); + // 2. 在 doWork() 中增加判断 + if (mode == MODE_REMINDER) { + String title = getInputData().getString("reminder_title"); + requestAiReminderReply(title); + } - return Result.success(); + return isSuccess[0] ? Result.success() : Result.failure(); - } catch (SecurityException e) { - // [核心修复]:捕获 "Unknown calling package name 'com.google.android.gms'" - Log.e(TAG, "GMS Security Exception caught, retrying later: " + e.getMessage()); - return Result.retry(); // 告诉系统稍后重试,不要直接闪退进程 } catch (Exception e) { Log.e(TAG, "General sync error: " + e.getMessage()); return Result.failure(); } } + // 3. 实现提醒请求逻辑 + private void requestAiReminderReply(String agendaTitle) { + try { + // 1. 构造请求参数 + com.google.gson.JsonObject json = new com.google.gson.JsonObject(); + json.addProperty("intent", "reminder"); + json.addProperty("payload", "日程即将开始:" + agendaTitle); + json.addProperty("current_time", AiDataSyncHelper.getCurrentTime()); + + // 2. 发起对话 + CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", json.toString()); + retrofit2.Response response = CozeClient.getInstance() + .chat(CozeClient.getAuthToken(), request).execute(); + + // 声明变量,用于存放最终结果 + String finalAnswer = null; + + if (response.isSuccessful() && response.body() != null && response.body().data != null) { + String chatId = response.body().data.id; + String convId = response.body().data.conversation_id; + + // 3. 轮询状态直到完成 + String status = ""; + int retries = 0; + while (!"completed".equals(status) && retries < 15) { + Thread.sleep(2000); + retrofit2.Response pollResp = CozeClient.getInstance() + .retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute(); + + if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) { + status = pollResp.body().data.status; + } + retries++; + } + + // 4. [核心修复点]:状态完成后,请求消息列表并解析出 content + if ("completed".equals(status)) { + retrofit2.Response msgListResp = CozeClient.getInstance() + .getMessageList(CozeClient.getAuthToken(), chatId, convId).execute(); + + if (msgListResp.isSuccessful() && msgListResp.body() != null) { + com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data"); + + // 遍历寻找 AI 的回答内容 + for (com.google.gson.JsonElement el : messages) { + com.google.gson.JsonObject m = el.getAsJsonObject(); + if ("assistant".equals(m.get("role").getAsString()) && + "answer".equals(m.get("type").getAsString())) { + finalAnswer = m.get("content").getAsString(); + break; + } + } + } + } + } + + // 5. 统一处理最终结果 + if (finalAnswer != null && !finalAnswer.isEmpty()) { + saveReminderToLocal(finalAnswer); + AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer); + Log.d("AiReminder", "Successfully got AI reminder: " + finalAnswer); + } else { + Log.w("AiReminder", "AI returned empty answer for reminder."); + } + + } catch (Exception e) { + Log.e("AiReminder", "Error in requestAiReminderReply", e); + } + } + + private void saveReminderToLocal(String content) { + android.content.ContentValues values = new android.content.ContentValues(); + values.put(Notes.ChatColumns.SENDER_TYPE, 1); // AI + values.put(Notes.ChatColumns.MSG_TYPE, 1); // [重要] 设为提醒卡片类型 + values.put(Notes.ChatColumns.CONTENT, content); + values.put(Notes.ChatColumns.CREATED_AT, System.currentTimeMillis()); + getApplicationContext().getContentResolver().insert( + android.net.Uri.parse("content://micode_notes/chat_messages"), values); + } + + // 在 SyncWorker 类中添加 + private void syncToCozeAgent() { + try { + com.google.gson.JsonObject payloadJson = new com.google.gson.JsonObject(); + payloadJson.addProperty("intent", "sync"); + payloadJson.addProperty("payload", AiDataSyncHelper.getSyncPayload(getApplicationContext())); + payloadJson.addProperty("current_time", AiDataSyncHelper.getCurrentTime()); + + CozeRequest request = new CozeRequest(CozeClient.BOT_ID, "user_demo", payloadJson.toString()); + + // 1. 发起对话 + retrofit2.Response response = CozeClient.getInstance() + .chat(CozeClient.getAuthToken(), request).execute(); + + if (response.isSuccessful() && response.body() != null && response.body().data != null) { + String chatId = response.body().data.id; + String convId = response.body().data.conversation_id; // [关键] 获取会话ID + + String status = ""; + int retries = 0; + while (!"completed".equals(status) && retries < 15) { + Thread.sleep(2000); + // [关键] 传入两个 ID 轮询 + retrofit2.Response pollResp = CozeClient.getInstance() + .retrieveChat(CozeClient.getAuthToken(), chatId, convId).execute(); + + if (pollResp.isSuccessful() && pollResp.body() != null && pollResp.body().data != null) { + status = pollResp.body().data.status; + } + retries++; + } + + // 2. 完成后抓取真正的“Answer” + if ("completed".equals(status)) { + retrofit2.Response msgListResp = CozeClient.getInstance() + .getMessageList(CozeClient.getAuthToken(), chatId, convId).execute(); + + if (msgListResp.isSuccessful() && msgListResp.body() != null) { + com.google.gson.JsonArray messages = msgListResp.body().getAsJsonArray("data"); + String finalAnswer = null; + + // 遍历寻找 role=assistant 且 type=answer 的内容 + for (com.google.gson.JsonElement el : messages) { + com.google.gson.JsonObject m = el.getAsJsonObject(); + if ("assistant".equals(m.get("role").getAsString()) && + "answer".equals(m.get("type").getAsString())) { + finalAnswer = m.get("content").getAsString(); + break; + } + } + + if (finalAnswer != null && !finalAnswer.isEmpty()) { + // 插入本地数据库并弹通知 + saveChatMessageToLocal(finalAnswer); + AiNotificationHelper.sendAiNotification(getApplicationContext(), finalAnswer); + } + } + } + } + } catch (Exception e) { + Log.e("CozeSync", "Sync Failed", e); + } + } + + // 辅助方法:将 AI 消息存入本地 chat_messages 表 + private void saveChatMessageToLocal(String content) { + android.content.ContentValues values = new android.content.ContentValues(); + values.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 1); // AI + values.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0); // 文本 + values.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, content); + values.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis()); + getApplicationContext().getContentResolver().insert( + android.net.Uri.parse("content://micode_notes/chat_messages"), values); + } + /** * [上行逻辑]:查询本地所有 sync_state = 1 的记录并上传 */ @@ -114,19 +302,33 @@ public class SyncWorker extends Worker { /** * [下行逻辑]:拉取云端所有数据并合并 */ - private void performPull(FirebaseFirestore db, String uid) { + // 修改 performPull 方法签名,增加 latch 和 状态标记 + private void performPull(FirebaseFirestore db, String uid, CountDownLatch latch, boolean[] isSuccess) { db.collection("users").document(uid).collection("notes") .get() .addOnSuccessListener(queryDocumentSnapshots -> { - for (QueryDocumentSnapshot doc : queryDocumentSnapshots) { - CloudNote cloudNote = doc.toObject(CloudNote.class); - if (cloudNote != null) { - mergeCloudNoteToLocal(cloudNote); + try { + for (QueryDocumentSnapshot doc : queryDocumentSnapshots) { + CloudNote cloudNote = doc.toObject(CloudNote.class); + if (cloudNote != null) { + mergeCloudNoteToLocal(cloudNote); + } } + Log.d(TAG, "Pull complete. Cloud docs processed."); + } catch (Exception e) { + Log.e(TAG, "Error merging data", e); + isSuccess[0] = false; + } finally { + // [关键]:通知主线程,活干完了,可以放行了 + latch.countDown(); } - Log.d(TAG, "Pull complete. Cloud docs processed."); }) - .addOnFailureListener(e -> Log.e(TAG, "Pull failed", e)); + .addOnFailureListener(e -> { + Log.e(TAG, "Pull failed", e); + isSuccess[0] = false; + // [关键]:失败了也要通知放行,否则会死锁 + latch.countDown(); + }); } /** diff --git a/src/Notes-master/src/net/micode/notes/tool/AiNotificationHelper.java b/src/Notes-master/src/net/micode/notes/tool/AiNotificationHelper.java new file mode 100644 index 0000000..0735acb --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/AiNotificationHelper.java @@ -0,0 +1,61 @@ +package net.micode.notes.tool; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.core.app.NotificationCompat; +import net.micode.notes.R; +import net.micode.notes.ui.NotesListActivity; + +public class AiNotificationHelper { + private static final String CHANNEL_ID = "mi_chat_channel"; + private static final String CHANNEL_NAME = "Mi Chat AI Assistant"; + private static final int NOTIFICATION_ID = 1001; + + public static void sendAiNotification(Context context, String content) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // 1. 创建通知渠道 (Android 8.0+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_HIGH // 重要通知,会弹出 + ); + channel.setDescription("Notifications from AI Assistant"); + channel.enableLights(true); + channel.enableVibration(true); + manager.createNotificationChannel(channel); + } + + // 2. 设置点击跳转意图 + // 跳转到 NotesListActivity,并携带参数指引它打开 ChatFragment + Intent intent = new Intent(context, NotesListActivity.class); + intent.putExtra("open_nav_ai", true); // 这个 Extra 需要在 NotesListActivity 中处理 + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + PendingIntent pendingIntent = PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + // 3. 构建通知 + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.icon_app) // 确保有这个资源 + .setContentTitle("Mi Chat") + .setContentText(content) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setDefaults(Notification.DEFAULT_ALL); + + // 4. 发送 + manager.notify(NOTIFICATION_ID, builder.build()); + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/DeepSeekHelper.java b/src/Notes-master/src/net/micode/notes/tool/DeepSeekHelper.java new file mode 100644 index 0000000..816505e --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/DeepSeekHelper.java @@ -0,0 +1,170 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class DeepSeekHelper { + private static final String TAG = "DeepSeekHelper"; + private static final String API_KEY = "sk-6fd917bdb96a48fba119dc64e58bf458"; + private static final String API_URL = "https://api.deepseek.com/chat/completions"; + + public interface AICallback { + void onSuccess(List results); + void onFailure(String error); + } + + public static class AgendaResult { + public String title; + public long startTime; + public long endTime; + public String timeLabel; // 例如 "16:00", "下周", "12月", "全天" + + public AgendaResult(String title, long startTime, long endTime, String timeLabel) { + this.title = title; + this.startTime = startTime; + this.endTime = endTime; + this.timeLabel = timeLabel; + } + } + + public static void analyzeContent(String content, AICallback callback) { + new AnalyzeTask(content, callback).execute(); + } + + private static class AnalyzeTask extends AsyncTask { + private String content; + private AICallback callback; + private List parsedResults = new ArrayList<>(); + + public AnalyzeTask(String content, AICallback callback) { + this.content = content; + this.callback = callback; + } + + @Override + protected String doInBackground(Void... voids) { + try { + // 获取当前时间 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd EEEE HH:mm", Locale.CHINA); + String currentTime = sdf.format(new Date()); + + // [核心优化] 更加智能的 Prompt + String systemPrompt = + "Current Time: " + currentTime + ".\n" + + "Role: You are a strict JSON schedule parser.\n" + + "Task: Extract unique events from user text.\n" + + "Rules:\n" + + "1. ONE EVENT = ONE JSON OBJECT. Never duplicate an event.\n" + + "2. If an event has a specific time (e.g. 8:00), set 'start' and 'end' to that exact timestamp.\n" + + "3. If an event is for a whole day, set 'start' to 00:00 and 'end' to 23:59 of that day.\n" + + "4. Format: [{'title': string, 'start': 'yyyy-MM-dd HH:mm', 'end': 'yyyy-MM-dd HH:mm', 'label': string}].\n" + + "5. The 'label' MUST be either 'HH:mm' for specific points, or '全天', '本周', '本月'.\n" + + "No explanations, return ONLY JSON."; + + // 3. 构建 JSON Body + JSONObject userMsg = new JSONObject(); + userMsg.put("role", "user"); + userMsg.put("content", content); + + JSONObject sysMsg = new JSONObject(); + sysMsg.put("role", "system"); + sysMsg.put("content", systemPrompt); + + JSONArray messages = new JSONArray(); + messages.put(sysMsg); + messages.put(userMsg); + + JSONObject jsonBody = new JSONObject(); + jsonBody.put("model", "deepseek-chat"); + jsonBody.put("messages", messages); + jsonBody.put("temperature", 0.1); // 低温度以保证格式稳定 + + // 4. 发起 HTTP 请求 + URL url = new URL(API_URL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Authorization", "Bearer " + API_KEY); + conn.setDoOutput(true); + + try(OutputStream os = conn.getOutputStream()) { + byte[] input = jsonBody.toString().getBytes("utf-8"); + os.write(input, 0, input.length); + } + + // 5. 读取响应 + int code = conn.getResponseCode(); + if (code == 200) { + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); + StringBuilder response = new StringBuilder(); + String responseLine = null; + while ((responseLine = br.readLine()) != null) { + response.append(responseLine.trim()); + } + + // 6. 解析 DeepSeek 返回的 JSON + JSONObject responseJson = new JSONObject(response.toString()); + String aiContent = responseJson.getJSONArray("choices") + .getJSONObject(0).getJSONObject("message").getString("content"); + + // 清洗可能存在的 Markdown 代码块标记 ```json ... ``` + if (aiContent.startsWith("```")) { + aiContent = aiContent.replaceAll("```json", "").replaceAll("```", ""); + } + + // 更新解析逻辑 + JSONArray events = new JSONArray(aiContent.trim()); + SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + + for (int i = 0; i < events.length(); i++) { + JSONObject event = events.getJSONObject(i); + String title = event.getString("title"); + String startStr = event.getString("start"); + String endStr = event.getString("end"); + String label = event.getString("label"); + + Date start = parser.parse(startStr); + Date end = parser.parse(endStr); + + if (start != null && end != null) { + parsedResults.add(new AgendaResult(title, start.getTime(), end.getTime(), label)); + } + } + return null; // Success + } else { + return "Error Code: " + code; + } + + } catch (Exception e) { + Log.e(TAG, "AI Request Failed", e); + return e.getMessage(); + } + } + + @Override + protected void onPostExecute(String error) { + if (error == null) { + if (parsedResults.isEmpty()) { + callback.onFailure("未识别到包含时间的日程信息"); + } else { + callback.onSuccess(parsedResults); + } + } else { + callback.onFailure("AI 服务连接失败: " + error); + } + } + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/AiDataSyncHelper.java b/src/Notes-master/src/net/micode/notes/tool/ai/AiDataSyncHelper.java new file mode 100644 index 0000000..d3672ab --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/AiDataSyncHelper.java @@ -0,0 +1,57 @@ +package net.micode.notes.tool.ai; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class AiDataSyncHelper { + + public static String getSyncPayload(Context context) { + ContentResolver cr = context.getContentResolver(); + StringBuilder sb = new StringBuilder(); + + // 1. 提取最近修改的便签 (取前10条以防Prompt过长) + Cursor cursor = cr.query(Notes.CONTENT_NOTE_URI, null, + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, + null, NoteColumns.MODIFIED_DATE + " DESC LIMIT 10"); + + sb.append("【用户便签改动】\n"); + if (cursor != null) { + while (cursor.moveToNext()) { + String title = cursor.getString(cursor.getColumnIndex(NoteColumns.TITLE)); + String content = cursor.getString(cursor.getColumnIndex(NoteColumns.SNIPPET)); + long time = cursor.getLong(cursor.getColumnIndex(NoteColumns.MODIFIED_DATE)); + sb.append("- 标题: ").append(title != null ? title : "无") + .append(" | 内容: ").append(content) + .append(" | 时间: ").append(new Date(time).toString()).append("\n"); + } + cursor.close(); + } + + // 2. 提取日程安排 (IS_AGENDA = 1) + Cursor agendaCursor = cr.query(Notes.CONTENT_NOTE_URI, null, + NoteColumns.IS_AGENDA + "=1", null, null); + + sb.append("\n【当前日程安排】\n"); + if (agendaCursor != null) { + while (agendaCursor.moveToNext()) { + String snippet = agendaCursor.getString(agendaCursor.getColumnIndex(NoteColumns.SNIPPET)); + long date = agendaCursor.getLong(agendaCursor.getColumnIndex(NoteColumns.AGENDA_DATE)); + sb.append("- 事项: ").append(snippet) + .append(" | 预定时间: ").append(new Date(date).toString()).append("\n"); + } + agendaCursor.close(); + } + + return sb.toString(); + } + + public static String getCurrentTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA).format(new Date()); + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderReceiver.java b/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderReceiver.java new file mode 100644 index 0000000..a813615 --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderReceiver.java @@ -0,0 +1,33 @@ +package net.micode.notes.tool.ai; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import net.micode.notes.sync.SyncWorker; + +public class AiReminderReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (AiReminderScheduler.ACTION_AI_REMINDER.equals(intent.getAction())) { + String title = intent.getStringExtra("title"); + Log.d("AiReminder", "Alarm trigger for: " + title); + + // 唤醒 WorkManager 执行一次性提醒请求 + // 借用 SyncWorker 里的逻辑,或者单独建一个业务逻辑 + Data inputData = new Data.Builder() + .putInt(SyncWorker.KEY_SYNC_MODE, 3) // 我们定义一个新模式:MODE_REMINDER = 3 + .putString("reminder_title", title) + .build(); + + OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(SyncWorker.class) + .setInputData(inputData) + .build(); + + WorkManager.getInstance(context).enqueue(request); + } + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderScheduler.java b/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderScheduler.java new file mode 100644 index 0000000..2e0cf1a --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/AiReminderScheduler.java @@ -0,0 +1,91 @@ +package net.micode.notes.tool.ai; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import net.micode.notes.data.Notes; +import net.micode.notes.data.Notes.NoteColumns; +import java.util.Calendar; + +public class AiReminderScheduler { + public static final String ACTION_AI_REMINDER = "net.micode.notes.ACTION_AI_REMINDER"; + + /** + * 核心方法:扫描所有日程并设置系统闹钟 + */ + public static void updateAllReminders(Context context) { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + // 1. 查询所有有效日程 (未完成且是日程) + Cursor c = context.getContentResolver().query(Notes.CONTENT_NOTE_URI, + null, NoteColumns.IS_AGENDA + "=1 AND " + NoteColumns.IS_COMPLETED + "=0", + null, null); + + if (c != null) { + while (c.moveToNext()) { + long noteId = c.getLong(c.getColumnIndex(NoteColumns.ID)); + String title = c.getString(c.getColumnIndex(NoteColumns.SNIPPET)); + long startTime = c.getLong(c.getColumnIndex(NoteColumns.AGENDA_DATE)); + long endTime = c.getLong(c.getColumnIndex(NoteColumns.AGENDA_END_DATE)); + String timeLabel = c.getString(c.getColumnIndex(NoteColumns.TIME_LABEL)); + + long triggerTime = calculateTriggerTime(startTime, timeLabel); + + // 只有未来的时间才设闹钟 + if (triggerTime > System.currentTimeMillis()) { + setExactAlarm(context, am, noteId, title, triggerTime); + } + } + c.close(); + } + } + + private static long calculateTriggerTime(long startTime, String timeLabel) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(startTime); + + if (timeLabel != null && timeLabel.contains(":")) { + // A. 具体时间点事件:提前1小时 + return startTime - (60 * 60 * 1000); + } else if ("全天".equals(timeLabel)) { + // B. 全天事件:当天早上 6:00 + cal.set(Calendar.HOUR_OF_DAY, 6); + cal.set(Calendar.MINUTE, 0); + return cal.getTimeInMillis(); + } else { + // C. 跨度/周期事件:当天早上 8:00 + cal.set(Calendar.HOUR_OF_DAY, 8); + cal.set(Calendar.MINUTE, 0); + return cal.getTimeInMillis(); + } + } + + private static void setExactAlarm(Context context, AlarmManager am, long id, String title, long time) { + try { + Intent intent = new Intent(context, AiReminderReceiver.class); + intent.setAction(ACTION_AI_REMINDER); + intent.putExtra("note_id", id); + intent.putExtra("title", title); + + PendingIntent pi = PendingIntent.getBroadcast(context, (int)id, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + // 核心修复:增加权限判断 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + if (am.canScheduleExactAlarms()) { + am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi); + } else { + // 退而求其次,使用非精准闹钟,防止崩溃 + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi); + } + } else { + am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi); + } + } catch (Exception e) { + android.util.Log.e("AiReminder", "Failed to set alarm", e); + } + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/CozeApiService.java b/src/Notes-master/src/net/micode/notes/tool/ai/CozeApiService.java new file mode 100644 index 0000000..602891c --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/CozeApiService.java @@ -0,0 +1,31 @@ +package net.micode.notes.tool.ai; + +import com.google.gson.JsonObject; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Query; + +public interface CozeApiService { + @POST("v3/chat") + Call chat(@Header("Authorization") String token, @Body CozeRequest request); + + // [核心修复] 轮询接口:使用 Query 参数传递 IDs + @POST("v3/chat/retrieve") + Call retrieveChat( + @Header("Authorization") String token, + @Query("chat_id") String chatId, + @Query("conversation_id") String conversationId + ); + + // [核心新增] 获取消息列表:用于抓取 AI 的文本回复 + @GET("v3/chat/message/list") + Call getMessageList( + @Header("Authorization") String token, + @Query("chat_id") String chatId, + @Query("conversation_id") String conversationId + ); +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/CozeClient.java b/src/Notes-master/src/net/micode/notes/tool/ai/CozeClient.java new file mode 100644 index 0000000..12ab6be --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/CozeClient.java @@ -0,0 +1,40 @@ +package net.micode.notes.tool.ai; + +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class CozeClient { + private static final String BASE_URL = "https://api.coze.cn/"; + + // TODO: 填入你的个人访问令牌 (PAT) + private static final String API_TOKEN = "Bearer pat_U2WS2ta9XWxz08ePQNloPw3mHxhmIGDf22ezG5JgyVC3CGrggE4SITQC4jZuIWE8"; + + // TODO: 填入你的智能体 ID (Bot ID) + public static final String BOT_ID = "7600011117091864585"; + + private static CozeApiService apiService; + + public static CozeApiService getInstance() { + if (apiService == null) { + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + apiService = retrofit.create(CozeApiService.class); + } + return apiService; + } + + public static String getAuthToken() { + return API_TOKEN; + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/CozeRequest.java b/src/Notes-master/src/net/micode/notes/tool/ai/CozeRequest.java new file mode 100644 index 0000000..5d1e023 --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/CozeRequest.java @@ -0,0 +1,42 @@ +package net.micode.notes.tool.ai; + +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.List; + +public class CozeRequest { + @SerializedName("bot_id") + private String botId; + + @SerializedName("user_id") + private String userId; + + @SerializedName("additional_messages") + private List additionalMessages; + + @SerializedName("stream") + private boolean stream = false; // 我们采用非流式,方便后台处理 + + public CozeRequest(String botId, String userId, String content) { + this.botId = botId; + this.userId = userId; + this.additionalMessages = new ArrayList<>(); + // 将意图和数据组合成一条用户消息发给智能体 + this.additionalMessages.add(new Message("user", content, "text")); + } + + public static class Message { + @SerializedName("role") + private String role; + @SerializedName("content") + private String content; + @SerializedName("content_type") + private String contentType; + + public Message(String role, String content, String contentType) { + this.role = role; + this.content = content; + this.contentType = contentType; + } + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/tool/ai/CozeResponse.java b/src/Notes-master/src/net/micode/notes/tool/ai/CozeResponse.java new file mode 100644 index 0000000..0423e6f --- /dev/null +++ b/src/Notes-master/src/net/micode/notes/tool/ai/CozeResponse.java @@ -0,0 +1,21 @@ +package net.micode.notes.tool.ai; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +public class CozeResponse { + @SerializedName("code") + public int code; + + @SerializedName("msg") + public String msg; + + @SerializedName("data") + public ChatData data; + + public static class ChatData { + public String id; // chat_id + public String conversation_id; // [核心修复] 必须添加此字段 + public String status; + } +} \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java b/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java index a85da6e..e54f84f 100644 --- a/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java +++ b/src/Notes-master/src/net/micode/notes/ui/AgendaFragment.java @@ -20,6 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import net.micode.notes.R; import net.micode.notes.data.Notes; +import net.micode.notes.tool.ai.AiReminderScheduler; import net.micode.notes.data.Notes.NoteColumns; import java.util.ArrayList; import java.util.Calendar; @@ -151,6 +152,14 @@ public class AgendaFragment extends Fragment { cv.put(NoteColumns.LOCAL_MODIFIED, 1); getContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, cv); + // [新增] 核心补丁:让 AI 助理感知到新日程 + try { + AiReminderScheduler.updateAllReminders(getContext()); + android.util.Log.d("AiReminder", "Scheduled from Agenda Quick Add"); + } catch (Exception e) { + e.printStackTrace(); + } + // 重置 UI et.setText(""); tvQuickTime.setText("全天"); diff --git a/src/Notes-master/src/net/micode/notes/ui/ChatFragment.java b/src/Notes-master/src/net/micode/notes/ui/ChatFragment.java index 60aa21e..d8ded54 100644 --- a/src/Notes-master/src/net/micode/notes/ui/ChatFragment.java +++ b/src/Notes-master/src/net/micode/notes/ui/ChatFragment.java @@ -20,6 +20,9 @@ public class ChatFragment extends Fragment { private List mMessages = new ArrayList<>(); private EditText mInput; + private android.database.ContentObserver mChatObserver; + private static final android.net.Uri CHAT_URI = android.net.Uri.parse("content://micode_notes/chat_messages"); + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_chat, container, false); @@ -31,19 +34,172 @@ public class ChatFragment extends Fragment { mRecyclerView.setAdapter(mAdapter); view.findViewById(R.id.btn_chat_send).setOnClickListener(v -> { - String text = mInput.getText().toString().trim(); - if (!text.isEmpty()) { - // 1. 本地插入用户消息 - ChatMessage userMsg = new ChatMessage(0, 0, text); - mMessages.add(userMsg); - mAdapter.notifyItemInserted(mMessages.size() - 1); - mRecyclerView.smoothScrollToPosition(mMessages.size() - 1); - mInput.setText(""); - - // TODO: Step 3 将在此处调用 CozeClient - } + final String text = mInput.getText().toString().trim(); + if (text.isEmpty()) return; + + // --- 第一部分:将用户消息存入数据库 --- + // 之前我们是手动 mMessages.add(...),现在不用了! + // 因为 ContentObserver 监听到数据库插入后,会自动调用 loadHistory 刷新列表。 + new Thread(() -> { + android.content.ContentValues userValues = new android.content.ContentValues(); + userValues.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 0); // 0 代表用户 + userValues.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0); // 普通文本 + userValues.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, text); + userValues.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis()); + + // 执行插入 + getContext().getContentResolver().insert(CHAT_URI, userValues); + }).start(); + + // 清空输入框 + mInput.setText(""); + + // --- 第二部分:向 AI 发起请求 (逻辑保持之前的异步方式) --- + new Thread(() -> { + try { + // 1. 组装请求 (intent=chat) + com.google.gson.JsonObject json = new com.google.gson.JsonObject(); + json.addProperty("intent", "chat"); + json.addProperty("payload", text); + json.addProperty("current_time", net.micode.notes.tool.ai.AiDataSyncHelper.getCurrentTime()); + + net.micode.notes.tool.ai.CozeRequest request = new net.micode.notes.tool.ai.CozeRequest( + net.micode.notes.tool.ai.CozeClient.BOT_ID, "user_demo", json.toString()); + + // 2. 发起 API 请求并轮询完成状态 + retrofit2.Response response = + net.micode.notes.tool.ai.CozeClient.getInstance().chat( + net.micode.notes.tool.ai.CozeClient.getAuthToken(), request).execute(); + + if (response.isSuccessful() && response.body() != null && response.body().data != null) { + String chatId = response.body().data.id; + String convId = response.body().data.conversation_id; + android.util.Log.d("MiChat", "开始轮询 AI 响应, ChatID: " + chatId); + + String status = ""; + int retries = 0; + while (!"completed".equals(status) && retries < 30) { // 最多等 60 秒 + Thread.sleep(2000); + retrofit2.Response poll = + net.micode.notes.tool.ai.CozeClient.getInstance().retrieveChat( + net.micode.notes.tool.ai.CozeClient.getAuthToken(), chatId, convId).execute(); + + if (poll.isSuccessful() && poll.body() != null && poll.body().data != null) { + status = poll.body().data.status; + android.util.Log.d("MiChat", "当前状态: " + status); + if ("failed".equals(status)) break; + } + retries++; + } + + if ("completed".equals(status)) { + android.util.Log.d("MiChat", "轮询完成,准备抓取回复内容..."); + retrofit2.Response msgList = + net.micode.notes.tool.ai.CozeClient.getInstance().getMessageList( + net.micode.notes.tool.ai.CozeClient.getAuthToken(), chatId, convId).execute(); + + if (msgList.isSuccessful() && msgList.body() != null) { + com.google.gson.JsonArray dataArray = msgList.body().getAsJsonArray("data"); + boolean foundAnswer = false; + for (com.google.gson.JsonElement el : dataArray) { + com.google.gson.JsonObject m = el.getAsJsonObject(); + + // 【核心检查点】打印出 AI 返回的所有消息类型,看看为什么没匹配到 + android.util.Log.d("MiChat", "收到消息: role=" + m.get("role").getAsString() + + ", type=" + m.get("type").getAsString()); + + // 寻找 AI 发送的真正的回答内容 + if ("assistant".equals(m.get("role").getAsString()) && + "answer".equals(m.get("type").getAsString())) { + + String answer = m.get("content").getAsString(); + foundAnswer = true; + + // 存入数据库,触发 ContentObserver 刷新 UI + android.content.ContentValues aiValues = new android.content.ContentValues(); + aiValues.put(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE, 1); + aiValues.put(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE, 0); + aiValues.put(net.micode.notes.data.Notes.ChatColumns.CONTENT, answer); + aiValues.put(net.micode.notes.data.Notes.ChatColumns.CREATED_AT, System.currentTimeMillis()); + + getContext().getContentResolver().insert(CHAT_URI, aiValues); + android.util.Log.d("MiChat", "AI 回复已入库: " + answer); + break; + } + } + if (!foundAnswer) { + android.util.Log.w("MiChat", "未在消息列表中找到 type 为 answer 的回复!"); + } + } + } else { + android.util.Log.e("MiChat", "轮询超时或失败,最终状态: " + status); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); }); + // 1. 初次加载历史记录 + loadHistory(); + + // 2. 注册观察者:监听数据库变化 + mChatObserver = new android.database.ContentObserver(new android.os.Handler()) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + android.util.Log.d("ChatFragment", "Database changed, reloading..."); + loadHistory(); // 数据库一变就重新加载 + } + }; + getContext().getContentResolver().registerContentObserver(CHAT_URI, true, mChatObserver); + return view; } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mChatObserver != null) { + getContext().getContentResolver().unregisterContentObserver(mChatObserver); + } + } + + private void loadHistory() { + new Thread(() -> { + android.database.Cursor cursor = getContext().getContentResolver().query( + CHAT_URI, + null, null, null, "created_at ASC" // 按时间顺序排列 + ); + + List history = new ArrayList<>(); + if (cursor != null) { + while (cursor.moveToNext()) { + // 根据 Step 1 定义的列顺序取值 + int senderType = cursor.getInt(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.SENDER_TYPE)); + int msgType = cursor.getInt(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.MSG_TYPE)); + String content = cursor.getString(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.CONTENT)); + long time = cursor.getLong(cursor.getColumnIndex(net.micode.notes.data.Notes.ChatColumns.CREATED_AT)); + + ChatMessage msg = new ChatMessage(senderType, msgType, content); + msg.createdAt = time; + history.add(msg); + } + cursor.close(); + } + + // 回到主线程更新 UI + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + mMessages.clear(); + mMessages.addAll(history); + mAdapter.notifyDataSetChanged(); + if (mMessages.size() > 0) { + mRecyclerView.scrollToPosition(mMessages.size() - 1); + } + }); + } + }).start(); + } } \ No newline at end of file diff --git a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java index ec0ee41..3dc8567 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NoteEditActivity.java @@ -78,6 +78,7 @@ import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; +import net.micode.notes.tool.ai.AiReminderScheduler; import net.micode.notes.widget.NoteWidgetProvider_2x; import net.micode.notes.widget.NoteWidgetProvider_4x; @@ -1612,6 +1613,15 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen saveImages(); setResult(RESULT_OK); + + // [新增] 刷新 AI 助理的闹钟计划 + try { + // 每次保存成功,都让调度器重新扫描一遍所有日程,更新系统闹钟 + AiReminderScheduler.updateAllReminders(this); + android.util.Log.d("AiAssistant", "Reminders scheduled successfully after save."); + } catch (Exception e) { + android.util.Log.e("AiAssistant", "Failed to schedule reminders", e); + } } return saved; } diff --git a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java index 9928b28..2c1e705 100644 --- a/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java +++ b/src/Notes-master/src/net/micode/notes/ui/NotesListActivity.java @@ -1442,19 +1442,21 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe } }); break; - // [新增] 处理 Push case R.id.menu_push: - Toast.makeText(this, "正在上传数据到云端...", Toast.LENGTH_SHORT).show(); + // 手术点:更新提示语 + Toast.makeText(this, "正在备份数据并同步 AI 大脑...", Toast.LENGTH_SHORT).show(); - // 构造参数:MODE_PUSH + // [修复] 去掉前面的 androidx.work.Data,直接赋值 inputData = new androidx.work.Data.Builder() .putInt(net.micode.notes.sync.SyncWorker.KEY_SYNC_MODE, net.micode.notes.sync.SyncWorker.MODE_PUSH) .build(); + // [修复] 去掉前面的 androidx.work.OneTimeWorkRequest,直接赋值 syncRequest = new androidx.work.OneTimeWorkRequest.Builder(net.micode.notes.sync.SyncWorker.class) .setInputData(inputData) .build(); + // 提交任务 androidx.work.WorkManager.getInstance(this).enqueue(syncRequest); break; case R.id.menu_setting: { -- 2.34.1