gemini-reverse 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,101 +4,160 @@ const { GRPC } = require('../constants');
4
4
  const { RPCData } = require('../types/grpc');
5
5
  const { Candidate } = require('../types/candidate');
6
6
  const { ModelOutput } = require('../types/modeloutput');
7
- const { ConversationTurn } = require('../types/conversation');
7
+ const { ChatTurn, ChatHistory } = require('../types/chathistory');
8
+ const { ChatInfo } = require('../types/chatinfo');
8
9
  const { extractJsonFromResponse, getNestedValue } = require('../utils/parsing');
9
10
 
10
11
  class ChatMixin {
11
- async _fetchChatTurns(cid, maxTurns = 10) {
12
- const response = await this._batchExecute([
13
- new RPCData({ rpcid: GRPC.READ_CHAT, payload: JSON.stringify([cid, maxTurns, null, 1, [0], [4], null, 1]) }),
14
- ]);
12
+ constructor() {
13
+ this._recentChats = null;
14
+ }
15
15
 
16
- const responseJson = extractJsonFromResponse(response.data);
16
+ async _fetchRecentChats(recent = 13) {
17
+ const fetchBatch = async (payload) => {
18
+ return this._batchExecute([
19
+ new RPCData({ rpcid: GRPC.LIST_CHATS, payload: JSON.stringify([recent, null, payload]) }),
20
+ ]);
21
+ };
22
+
23
+ const [resp1, resp2] = await Promise.all([
24
+ fetchBatch([1, null, 1]),
25
+ fetchBatch([0, null, 1]),
26
+ ]);
17
27
 
18
- for (const part of responseJson) {
19
- const bodyStr = getNestedValue(part, [2]);
20
- if (!bodyStr) continue;
21
- const body = JSON.parse(bodyStr);
22
- const turns = getNestedValue(body, [0]);
23
- if (!turns || !Array.isArray(turns)) continue;
24
- return turns;
28
+ const recentChats = [];
29
+ const seenCids = new Set();
30
+
31
+ for (const response of [resp1, resp2]) {
32
+ const chatsJson = extractJsonFromResponse(response.data);
33
+ for (const part of chatsJson) {
34
+ const bodyStr = getNestedValue(part, [2]);
35
+ if (!bodyStr) continue;
36
+ let body;
37
+ try { body = JSON.parse(bodyStr); } catch { continue; }
38
+ const chatList = getNestedValue(body, [2]);
39
+ if (!Array.isArray(chatList)) continue;
40
+ for (const chatData of chatList) {
41
+ if (!Array.isArray(chatData) || chatData.length < 2) continue;
42
+ const cid = getNestedValue(chatData, [0], '');
43
+ const title = getNestedValue(chatData, [1], '');
44
+ const is_pinned = Boolean(getNestedValue(chatData, [2]));
45
+ const tsData = getNestedValue(chatData, [5]);
46
+ let timestamp = 0;
47
+ if (Array.isArray(tsData) && tsData.length >= 2) {
48
+ timestamp = Number(tsData[0]) + Number(tsData[1]) / 1e9;
49
+ }
50
+ if (cid && !seenCids.has(cid)) {
51
+ seenCids.add(cid);
52
+ recentChats.push(new ChatInfo({ cid, title, is_pinned, timestamp }));
53
+ }
54
+ }
55
+ break;
56
+ }
25
57
  }
26
58
 
27
- console.warn(`_fetchChatTurns(${cid}) found no turns`);
28
- return null;
59
+ this._recentChats = recentChats;
29
60
  }
30
61
 
31
- async fetchLatestChatResponse(cid) {
32
- try {
33
- const turns = await this._fetchChatTurns(cid, 10);
34
- if (!turns) return null;
35
-
36
- const convTurn = turns[turns.length - 1];
37
- if (!convTurn) return null;
38
-
39
- const candidatesList = getNestedValue(convTurn, [3, 0]);
40
- if (!candidatesList) return null;
41
-
42
- const candidateData = getNestedValue(candidatesList, [0]);
43
- if (!candidateData) return null;
62
+ listChats() {
63
+ return this._recentChats;
64
+ }
44
65
 
45
- const rcid = getNestedValue(candidateData, [0], '');
46
- const text = getNestedValue(candidateData, [1, 0], '');
47
- if (!text) return null;
66
+ async readChat(cid, limit = 10) {
67
+ try {
68
+ const response = await this._batchExecute([
69
+ new RPCData({
70
+ rpcid: GRPC.READ_CHAT,
71
+ payload: JSON.stringify([cid, limit, null, 1, [1], [4], null, 1]),
72
+ }),
73
+ ]);
74
+
75
+ const responseJson = extractJsonFromResponse(response.data);
76
+
77
+ for (const part of responseJson) {
78
+ const bodyStr = getNestedValue(part, [2]);
79
+ if (!bodyStr) continue;
80
+ let body;
81
+ try { body = JSON.parse(bodyStr); } catch { continue; }
82
+ const turnsData = getNestedValue(body, [0]);
83
+ if (!turnsData) continue;
84
+
85
+ const chatTurns = [];
86
+ for (const convTurn of turnsData) {
87
+ const rid = getNestedValue(convTurn, [0, 1], '');
88
+
89
+ const candidatesList = getNestedValue(convTurn, [3, 0]);
90
+ if (candidatesList) {
91
+ const outputCandidates = [];
92
+ for (const candidateData of candidatesList) {
93
+ const completionStatus = getNestedValue(candidateData, [8, 0]);
94
+ const hasProgressSignal = getNestedValue(candidateData, [12, 6, 0]) != null;
95
+
96
+ if (completionStatus !== 2 && hasProgressSignal) {
97
+ return null;
98
+ }
99
+
100
+ const rcid = getNestedValue(candidateData, [0]);
101
+ if (!rcid) continue;
102
+
103
+ const parsed = this._parseCandidate(candidateData, cid, rid, rcid);
104
+ const [text, thoughts, webImages, genImages, genVideos, genMedia] = parsed;
105
+
106
+ outputCandidates.push(new Candidate({
107
+ rcid,
108
+ text,
109
+ text_delta: text,
110
+ thoughts,
111
+ thoughts_delta: thoughts,
112
+ web_images: webImages,
113
+ generated_images: genImages,
114
+ generated_videos: genVideos,
115
+ generated_media: genMedia,
116
+ }));
117
+ }
118
+ if (outputCandidates.length) {
119
+ const modelOutput = new ModelOutput([cid, rid], outputCandidates);
120
+ chatTurns.push(new ChatTurn({ role: 'model', text: modelOutput.text, model_output: modelOutput }));
121
+ }
122
+ }
123
+
124
+ const userText = getNestedValue(convTurn, [2, 0, 0], '');
125
+ if (userText) {
126
+ chatTurns.push(new ChatTurn({ role: 'user', text: userText }));
127
+ }
128
+ }
48
129
 
49
- const turnMeta = getNestedValue(convTurn, [0]);
50
- const rid = Array.isArray(turnMeta) && turnMeta.length >= 2 && typeof turnMeta[1] === 'string' ? turnMeta[1] : '';
51
- const metadata = rid ? [cid, rid] : [cid];
130
+ return new ChatHistory({ cid, turns: chatTurns });
131
+ }
52
132
 
53
- return new ModelOutput(metadata, [new Candidate({ rcid, text })]);
54
- } catch (e) {
55
- console.warn(`fetchLatestChatResponse(${cid}) error: ${e.message}`);
133
+ return null;
134
+ } catch {
56
135
  return null;
57
136
  }
58
137
  }
59
138
 
60
- async readChat(cid, maxTurns = 100) {
139
+ async fetchLatestChatResponse(cid) {
61
140
  try {
62
- const turns = await this._fetchChatTurns(cid, maxTurns);
63
- if (!turns) return [];
64
-
65
- const result = [];
66
- for (const turn of turns) {
67
- if (!Array.isArray(turn) || turn.length < 4) continue;
68
-
69
- const turnMeta = getNestedValue(turn, [0]);
70
- const rid = Array.isArray(turnMeta) ? (getNestedValue(turnMeta, [1], '') || '') : '';
71
- const userPrompt = getNestedValue(turn, [2, 0, 0], '');
72
- const candidateData = getNestedValue(turn, [3, 0, 0]);
73
- if (!candidateData) continue;
74
-
75
- const rcid = getNestedValue(candidateData, [0], '');
76
- const text = getNestedValue(candidateData, [1, 0], '');
77
- if (!text) continue;
78
-
79
- const thoughts = getNestedValue(candidateData, [37, 0, 0]) || null;
80
- const tsData = getNestedValue(turn, [4]);
81
- let timestamp = null;
82
- if (Array.isArray(tsData) && tsData.length && typeof tsData[0] === 'number') {
83
- try { timestamp = new Date(tsData[0] * 1000); } catch {}
84
- }
85
-
86
- result.push(new ConversationTurn({ rid, user_prompt: userPrompt, assistant_response: text, rcid, thoughts, timestamp }));
141
+ const history = await this.readChat(cid, 5);
142
+ if (!history || !history.turns || !history.turns.length) return null;
143
+ for (const turn of history.turns) {
144
+ if (turn.role === 'model' && turn.model_output) return turn.model_output;
87
145
  }
88
-
89
- result.reverse();
90
- return result;
146
+ return null;
91
147
  } catch (e) {
92
- console.warn(`readChat(${cid}) error: ${e.message}`);
93
- return [];
148
+ console.warn(`fetchLatestChatResponse(${cid}) error: ${e.message}`);
149
+ return null;
94
150
  }
95
151
  }
96
152
 
97
153
  async deleteChat(cid) {
98
154
  await this._batchExecute([
99
- new RPCData({ rpcid: GRPC.DELETE_CHAT, payload: JSON.stringify([cid]) }),
155
+ new RPCData({ rpcid: GRPC.DELETE_CHAT_1, payload: JSON.stringify([cid]) }),
156
+ ]);
157
+ await this._batchExecute([
158
+ new RPCData({ rpcid: GRPC.DELETE_CHAT_2, payload: JSON.stringify([cid, [1, null, 0, 1]]) }),
100
159
  ]);
101
160
  }
102
161
  }
103
162
 
104
- module.exports = { ChatMixin };
163
+ module.exports = { ChatMixin };
@@ -2,5 +2,6 @@
2
2
 
3
3
  const { ChatMixin } = require('./chatMixin');
4
4
  const { GemMixin } = require('./gemMixin');
5
+ const { ResearchMixin } = require('./researchMixin');
5
6
 
6
- module.exports = { ChatMixin, GemMixin };
7
+ module.exports = { ChatMixin, GemMixin, ResearchMixin };
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+
3
+ const { GRPC } = require('../constants');
4
+ const { APIError, GeminiError, ModelInvalid, TemporarilyBlocked, TimeoutError, UsageLimitExceeded } = require('../exceptions');
5
+ const { DeepResearchPlan, DeepResearchStatus } = require('../types/research');
6
+ const { DeepResearchResult } = require('../types/researchresult');
7
+ const { RPCData } = require('../types/grpc');
8
+ const { extractJsonFromResponse, getNestedValue } = require('../utils/parsing');
9
+ const { extractDeepResearchStatusPayload } = require('../utils/research');
10
+
11
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
12
+
13
+ class ResearchMixin {
14
+ async inspectAccountStatus() {
15
+ const probes = [
16
+ ['activity', GRPC.BARD_SETTINGS, '[[["bard_activity_enabled"]]]'],
17
+ ['bootstrap', GRPC.DEEP_RESEARCH_BOOTSTRAP, '["en",null,null,null,4,null,null,[2,4,7,15],null,[[5]]]'],
18
+ ['model_state', GRPC.DEEP_RESEARCH_MODEL_STATE, '[[[1,4],[6,6],[1,15]]]'],
19
+ ['quota', GRPC.DEEP_RESEARCH_MODEL_STATE, '[[[1,11],[2,11],[6,11]]]'],
20
+ ['caps', GRPC.DEEP_RESEARCH_CAPS, '[]'],
21
+ ];
22
+
23
+ const result = { source_path: '/app', rpc: {} };
24
+
25
+ for (const [probeName, rpcid, payload] of probes) {
26
+ try {
27
+ const response = await this._batchExecute([new RPCData({ rpcid, payload })], 2, false);
28
+ const parsed = [];
29
+ let rejectCode = null;
30
+ const parts = extractJsonFromResponse(response.data);
31
+ for (const part of parts) {
32
+ if (getNestedValue(part, [0]) !== 'wrb.fr') continue;
33
+ if (getNestedValue(part, [1]) !== rpcid) continue;
34
+ const code = getNestedValue(part, [5, 0]);
35
+ if (typeof code === 'number') rejectCode = code;
36
+ const body = getNestedValue(part, [2]);
37
+ if (typeof body === 'string') {
38
+ try { parsed.push(JSON.parse(body)); } catch { parsed.push(body); }
39
+ } else if (body != null) {
40
+ parsed.push(body);
41
+ }
42
+ }
43
+ result.rpc[probeName] = {
44
+ rpcid,
45
+ ok: true,
46
+ status_code: response.status,
47
+ parsed,
48
+ reject_code: rejectCode,
49
+ };
50
+ } catch (e) {
51
+ result.rpc[probeName] = { rpcid, ok: false, error: `${e.constructor.name}: ${e.message}` };
52
+ }
53
+ }
54
+
55
+ const drProbes = ['bootstrap', 'model_state', 'caps'];
56
+ const drAvailable = drProbes.every(p => {
57
+ const probe = result.rpc[p];
58
+ return probe && probe.ok && probe.reject_code == null;
59
+ });
60
+
61
+ const rejected = Object.entries(result.rpc)
62
+ .filter(([, v]) => v && v.reject_code === 7)
63
+ .map(([k]) => k);
64
+
65
+ result.summary = { deep_research_feature_present: drAvailable, rejected_probes: rejected };
66
+ return result;
67
+ }
68
+
69
+ async _assertDeepResearchCapable() {
70
+ const snapshot = await this.inspectAccountStatus();
71
+ const summary = snapshot.summary || {};
72
+ if (!summary.deep_research_feature_present) {
73
+ const rejected = summary.rejected_probes || [];
74
+ const rpc = snapshot.rpc || {};
75
+ const failed = Object.entries(rpc)
76
+ .filter(([, v]) => v && !v.ok)
77
+ .map(([k]) => k);
78
+ throw new GeminiError(`Current account/session appears not eligible for deep research. Rejected: ${JSON.stringify(rejected)}, Failed: ${JSON.stringify(failed)}`);
79
+ }
80
+ }
81
+
82
+ async _deepResearchPreflight() {
83
+ const bestEffort = async (payloads) => {
84
+ try { await this._batchExecute(payloads, 2, false); } catch (e) {
85
+ console.warn(`Skipping non-critical preflight RPC: ${e.message}`);
86
+ }
87
+ };
88
+
89
+ await bestEffort([new RPCData({ rpcid: GRPC.BARD_SETTINGS, payload: '[[["bard_activity_enabled"]]]' })]);
90
+ await bestEffort([new RPCData({ rpcid: GRPC.DEEP_RESEARCH_BOOTSTRAP, payload: '["en",null,null,null,4,null,null,[2,4,7,15],null,[[5]]]' })]);
91
+ }
92
+
93
+ async _collectResearchOutput(chat, prompt) {
94
+ let recoverableError = null;
95
+ try {
96
+ const output = await chat.sendMessage({ prompt, deep_research: true });
97
+ const preview = (output.text || '').trim();
98
+ if (output.deep_research_plan || preview) {
99
+ chat.lastOutput = output;
100
+ return output;
101
+ }
102
+ } catch (e) {
103
+ if (e instanceof UsageLimitExceeded || e instanceof TimeoutError || e instanceof ModelInvalid || e instanceof TemporarilyBlocked) throw e;
104
+ if (e instanceof GeminiError || e instanceof APIError) recoverableError = e;
105
+ else throw e;
106
+ }
107
+
108
+ if (chat.cid) {
109
+ const fallback = await this.fetchLatestChatResponse(chat.cid);
110
+ if (fallback) {
111
+ chat.lastOutput = fallback;
112
+ return fallback;
113
+ }
114
+ }
115
+
116
+ if (recoverableError) throw recoverableError;
117
+ throw new GeminiError(`Gemini returned no usable output for deep research. chat.cid=${chat.cid}`);
118
+ }
119
+
120
+ async createDeepResearchPlan(prompt, chat = null, model = null) {
121
+ if (!chat) chat = this.startChat(model ? { model } : {});
122
+ await this._assertDeepResearchCapable();
123
+ await this._deepResearchPreflight();
124
+ const output = await this._collectResearchOutput(chat, prompt);
125
+ const plan = output.deep_research_plan;
126
+ if (!plan) {
127
+ const preview = (output.text || '').slice(0, 1200);
128
+ throw new GeminiError(`Gemini did not return a deep research plan. Preview: ${preview}`);
129
+ }
130
+ plan.metadata = [...chat.metadata];
131
+ plan.cid = chat.cid || plan.cid;
132
+ if (!plan.confirm_prompt) plan.confirm_prompt = 'Start research';
133
+ if (!plan.response_text) plan.response_text = output.text;
134
+ return plan;
135
+ }
136
+
137
+ async startDeepResearch(plan, chat = null, confirmPrompt = null) {
138
+ if (!chat) chat = this.startChat({ metadata: [...plan.metadata], cid: plan.cid });
139
+ await this._deepResearchPreflight();
140
+ const prompt = confirmPrompt || plan.confirm_prompt || 'Start research';
141
+ return await this._collectResearchOutput(chat, prompt);
142
+ }
143
+
144
+ async getDeepResearchStatus(researchId) {
145
+ const response = await this._batchExecute([
146
+ new RPCData({ rpcid: GRPC.DEEP_RESEARCH_STATUS, payload: JSON.stringify([researchId]) }),
147
+ ]);
148
+ const responseJson = extractJsonFromResponse(response.data);
149
+ for (const part of responseJson) {
150
+ const bodyStr = getNestedValue(part, [2]);
151
+ if (!bodyStr) continue;
152
+ let body;
153
+ try { body = JSON.parse(bodyStr); } catch { continue; }
154
+ const parsed = extractDeepResearchStatusPayload(body);
155
+ if (parsed) return new DeepResearchStatus(parsed);
156
+ }
157
+ return null;
158
+ }
159
+
160
+ async waitForDeepResearch(plan, pollInterval = 10000, timeout = 600000, onStatus = null) {
161
+ if (!plan.research_id) {
162
+ throw new GeminiError('Cannot poll deep research status: plan.research_id is missing.');
163
+ }
164
+
165
+ const start = Date.now();
166
+ const statuses = [];
167
+ const chat = this.startChat({ metadata: [...plan.metadata], cid: plan.cid });
168
+
169
+ while ((Date.now() - start) < timeout) {
170
+ let status = null;
171
+ if (plan.research_id) {
172
+ status = await this.getDeepResearchStatus(plan.research_id);
173
+ }
174
+ if (status) {
175
+ statuses.push(status);
176
+ if (onStatus) onStatus(status);
177
+ if (status.done) break;
178
+ }
179
+ await sleep(pollInterval);
180
+ }
181
+
182
+ if (!statuses.length || !statuses[statuses.length - 1].done) {
183
+ console.warn(`Deep research [${plan.research_id}] timed out after ${timeout}ms with ${statuses.length} status updates`);
184
+ }
185
+
186
+ let finalOutput = null;
187
+ if (chat.cid) finalOutput = await this.fetchLatestChatResponse(chat.cid);
188
+
189
+ const done = statuses.length > 0 && statuses[statuses.length - 1].done;
190
+ return new DeepResearchResult({ plan, statuses, final_output: finalOutput, done });
191
+ }
192
+
193
+ async deepResearch(prompt, pollInterval = 10000, timeout = 600000, onStatus = null) {
194
+ const plan = await this.createDeepResearchPlan(prompt);
195
+ const startOutput = await this.startDeepResearch(plan);
196
+ const result = await this.waitForDeepResearch(plan, pollInterval, timeout, onStatus);
197
+ result.start_output = startOutput;
198
+ return result;
199
+ }
200
+ }
201
+
202
+ module.exports = { ResearchMixin };