@chainlesschain/personal-data-hub 0.2.0 → 0.2.2
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.
- package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +211 -0
- package/__tests__/adapters/ai-chat-health-checker.test.js +262 -0
- package/__tests__/adapters/ai-chat-history.test.js +8 -7
- package/__tests__/adapters/ai-chat-vendors.test.js +149 -8
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
- package/__tests__/adapters/system-data-android-ingest.test.js +144 -0
- package/__tests__/adapters/system-data-android.test.js +387 -0
- package/__tests__/adapters/wechat-bootstrap.test.js +240 -0
- package/__tests__/adapters/wechat-env-probe.test.js +162 -0
- package/__tests__/adapters/wechat-frida-agent.test.js +322 -0
- package/__tests__/adapters/wechat-frida-integration.test.js +149 -0
- package/__tests__/adapters/wechat-frida-key-provider.test.js +188 -0
- package/__tests__/adapters/wechat-md5-key-provider.test.js +101 -0
- package/__tests__/analysis-skills.test.js +147 -0
- package/__tests__/analysis.test.js +329 -1
- package/__tests__/e2e/ai-chat-cross-source-journey.test.js +213 -0
- package/__tests__/e2e/full-user-journey.test.js +188 -0
- package/__tests__/integration/ai-chat-history-registry.test.js +228 -0
- package/__tests__/integration/aichat-wizard-end-to-end.test.js +282 -0
- package/__tests__/integration/cross-adapter-pipelines.test.js +396 -0
- package/__tests__/integration/social-bilibili-pipeline.test.js +261 -0
- package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +390 -0
- package/__tests__/registry.test.js +4 -2
- package/__tests__/social-adapters.test.js +63 -14
- package/__tests__/social-bilibili-snapshot.test.js +278 -0
- package/__tests__/wechat-adapter.test.js +118 -0
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +55 -16
- package/lib/adapters/ai-chat-history/cookie-capture-spec.js +331 -0
- package/lib/adapters/ai-chat-history/health-checker.js +210 -0
- package/lib/adapters/ai-chat-history/schema-map.js +42 -5
- package/lib/adapters/ai-chat-history/vendor-spec.js +1 -0
- package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
- package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +4 -0
- package/lib/adapters/social-bilibili/adapter.js +500 -0
- package/lib/adapters/social-bilibili/index.js +21 -169
- package/lib/adapters/social-kuaishou/index.js +237 -0
- package/lib/adapters/social-toutiao/index.js +236 -0
- package/lib/adapters/system-data-android/adapter.js +348 -0
- package/lib/adapters/system-data-android/index.js +76 -0
- package/lib/adapters/wechat/bootstrap.js +146 -0
- package/lib/adapters/wechat/content-parser.js +11 -2
- package/lib/adapters/wechat/db-reader.js +88 -10
- package/lib/adapters/wechat/env-probe.js +218 -0
- package/lib/adapters/wechat/frida-agent/loader.js +74 -0
- package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +248 -0
- package/lib/adapters/wechat/index.js +9 -0
- package/lib/adapters/wechat/key-providers/frida-key-provider.js +252 -0
- package/lib/adapters/wechat/key-providers/index.js +22 -0
- package/lib/adapters/wechat/key-providers/key-provider-base.js +44 -0
- package/lib/adapters/wechat/key-providers/md5-key-provider.js +81 -0
- package/lib/adapters/wechat/normalize.js +12 -3
- package/lib/analysis-skills/spending.js +4 -1
- package/lib/analysis.js +191 -2
- package/lib/index.js +16 -0
- package/lib/prompt-builder.js +11 -1
- package/lib/query-parser.js +7 -1
- package/lib/vault.js +77 -0
- package/package.json +8 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doubao / 豆包 (ByteDance text AI) vendor adapter — Phase 10.2(+) scaffold.
|
|
3
|
+
*
|
|
4
|
+
* Doubao is ByteDance's flagship text AI assistant — sibling to Dreamina
|
|
5
|
+
* (image/video) but on a separate domain and surface. Treated as the 9th
|
|
6
|
+
* AIChatHistory vendor.
|
|
7
|
+
*
|
|
8
|
+
* Reference: docs/design/Adapter_AIChat_History.md §6.9 (added 2026-05-20).
|
|
9
|
+
*
|
|
10
|
+
* - login https://www.doubao.com/chat/
|
|
11
|
+
* - convs POST /samantha/conversation/list
|
|
12
|
+
* body: { cursor: "<opaque>", count: N }
|
|
13
|
+
* response: { data: { conversation_list: [...], cursor, has_more } }
|
|
14
|
+
* - msgs POST /samantha/conversation/<id>/message/list
|
|
15
|
+
* body: { conversation_id, cursor, count }
|
|
16
|
+
* response: { data: { message_list: [...], cursor, has_more } }
|
|
17
|
+
* - user info POST /samantha/user/info
|
|
18
|
+
*
|
|
19
|
+
* Phase 10.2(+) scope (this file): scaffold only.
|
|
20
|
+
* - SPEC declares endpoint shape + rate limits + cookie domain
|
|
21
|
+
* - validateCookie / listConversations / listMessages parse the documented
|
|
22
|
+
* response shape but the exact field names are TBD until Phase 10.4
|
|
23
|
+
* real-account fixture pin
|
|
24
|
+
* - response shape uses defensive `_extractList` fallback to absorb minor
|
|
25
|
+
* field-name drift without breaking sync
|
|
26
|
+
*
|
|
27
|
+
* Phase 10.4 will pin field names from real-account har capture and add the
|
|
28
|
+
* remaining response variants (search-result attachments, image gen inside
|
|
29
|
+
* Doubao, voice replies, etc).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
const BASE = "https://www.doubao.com";
|
|
35
|
+
const CONV_LIST_PATH = "/samantha/conversation/list";
|
|
36
|
+
const MSG_LIST_PATH = (id) =>
|
|
37
|
+
`/samantha/conversation/${encodeURIComponent(id)}/message/list`;
|
|
38
|
+
const USER_INFO_PATH = "/samantha/user/info";
|
|
39
|
+
|
|
40
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
41
|
+
|
|
42
|
+
function _ensureClient(ctx) {
|
|
43
|
+
if (!ctx || !ctx.httpClient) {
|
|
44
|
+
throw new Error("doubao: ctx.httpClient required (AIChatHistoryAdapter must wire one)");
|
|
45
|
+
}
|
|
46
|
+
return ctx.httpClient;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function validateCookie(ctx) {
|
|
50
|
+
const client = _ensureClient(ctx);
|
|
51
|
+
try {
|
|
52
|
+
const data = await client.postJson(
|
|
53
|
+
BASE + USER_INFO_PATH,
|
|
54
|
+
{},
|
|
55
|
+
{ session: ctx.session },
|
|
56
|
+
);
|
|
57
|
+
if (data && (data.code === 0 || data.code === "0") && data.data) {
|
|
58
|
+
return { ok: true, userId: data.data.user_id || data.data.uid };
|
|
59
|
+
}
|
|
60
|
+
return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { ok: false, reason: err.code || err.message };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Yield one RawConversation per remote chat session, newest first.
|
|
68
|
+
* Pagination uses an opaque `cursor` returned by the previous page plus
|
|
69
|
+
* `has_more` boolean (Doubao does not use offset / page numbers).
|
|
70
|
+
*/
|
|
71
|
+
async function *listConversations(ctx, opts = {}) {
|
|
72
|
+
const client = _ensureClient(ctx);
|
|
73
|
+
const limit = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
|
|
74
|
+
const sinceTs =
|
|
75
|
+
opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
|
|
76
|
+
|
|
77
|
+
let cursor = "";
|
|
78
|
+
let safety = 0;
|
|
79
|
+
while (true) {
|
|
80
|
+
safety++;
|
|
81
|
+
if (safety > 200) return; // hard cap
|
|
82
|
+
|
|
83
|
+
const body = { count: limit };
|
|
84
|
+
if (cursor) body.cursor = cursor;
|
|
85
|
+
|
|
86
|
+
const data = await client.postJson(BASE + CONV_LIST_PATH, body, {
|
|
87
|
+
session: ctx.session,
|
|
88
|
+
});
|
|
89
|
+
const list = _extractConvList(data);
|
|
90
|
+
if (list.length === 0) return;
|
|
91
|
+
|
|
92
|
+
let stopped = false;
|
|
93
|
+
for (const c of list) {
|
|
94
|
+
const updatedAt = _toMs(c.last_message_time || c.update_time || c.create_time);
|
|
95
|
+
if (sinceTs && updatedAt <= sinceTs) {
|
|
96
|
+
stopped = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
yield {
|
|
100
|
+
vendor: "doubao",
|
|
101
|
+
originalId: String(c.conversation_id || c.id),
|
|
102
|
+
title: c.name || c.title || undefined,
|
|
103
|
+
modelName: c.bot_name || c.model || undefined,
|
|
104
|
+
createdAt: _toMs(c.create_time),
|
|
105
|
+
updatedAt,
|
|
106
|
+
messageCount: c.message_count || undefined,
|
|
107
|
+
archived: Boolean(c.archived || c.deleted),
|
|
108
|
+
extra: { botId: c.bot_id, botName: c.bot_name },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (stopped) return;
|
|
112
|
+
|
|
113
|
+
const meta = (data && data.data) || {};
|
|
114
|
+
const hasMore = meta.has_more === true || meta.hasMore === true;
|
|
115
|
+
const nextCursor = meta.cursor || meta.next_cursor || "";
|
|
116
|
+
if (!hasMore || !nextCursor) return;
|
|
117
|
+
cursor = nextCursor;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Yield each message in a conversation in chronological order.
|
|
123
|
+
* Doubao paginates messages too — historical conversations can have
|
|
124
|
+
* thousands of turns.
|
|
125
|
+
*/
|
|
126
|
+
async function *listMessages(ctx, conversationId, _opts = {}) {
|
|
127
|
+
const client = _ensureClient(ctx);
|
|
128
|
+
let cursor = "";
|
|
129
|
+
let safety = 0;
|
|
130
|
+
const collected = [];
|
|
131
|
+
|
|
132
|
+
while (true) {
|
|
133
|
+
safety++;
|
|
134
|
+
if (safety > 200) break;
|
|
135
|
+
|
|
136
|
+
const body = { conversation_id: String(conversationId), count: 100 };
|
|
137
|
+
if (cursor) body.cursor = cursor;
|
|
138
|
+
|
|
139
|
+
const data = await client.postJson(BASE + MSG_LIST_PATH(conversationId), body, {
|
|
140
|
+
session: ctx.session,
|
|
141
|
+
});
|
|
142
|
+
const list = _extractMsgList(data);
|
|
143
|
+
if (list.length === 0) break;
|
|
144
|
+
|
|
145
|
+
for (const m of list) collected.push(m);
|
|
146
|
+
|
|
147
|
+
const meta = (data && data.data) || {};
|
|
148
|
+
const hasMore = meta.has_more === true || meta.hasMore === true;
|
|
149
|
+
const nextCursor = meta.cursor || meta.next_cursor || "";
|
|
150
|
+
if (!hasMore || !nextCursor) break;
|
|
151
|
+
cursor = nextCursor;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Newest-first → reverse to chronological
|
|
155
|
+
collected.sort((a, b) => _toMs(a.create_time) - _toMs(b.create_time));
|
|
156
|
+
|
|
157
|
+
for (const m of collected) {
|
|
158
|
+
yield {
|
|
159
|
+
vendor: "doubao",
|
|
160
|
+
originalId: String(m.id || m.message_id),
|
|
161
|
+
conversationId: String(conversationId),
|
|
162
|
+
role: _normalizeRole(m.sender_type || m.role),
|
|
163
|
+
content: _buildContent(m),
|
|
164
|
+
createdAt: _toMs(m.create_time),
|
|
165
|
+
parentMessageId: m.parent_id ? String(m.parent_id) : undefined,
|
|
166
|
+
modelName: m.model || undefined,
|
|
167
|
+
extra: {
|
|
168
|
+
botId: m.bot_id,
|
|
169
|
+
thinking: m.thinking || undefined,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function _extractConvList(data) {
|
|
176
|
+
if (!data || !data.data) return [];
|
|
177
|
+
if (Array.isArray(data.data.conversation_list)) return data.data.conversation_list;
|
|
178
|
+
if (Array.isArray(data.data.conversations)) return data.data.conversations;
|
|
179
|
+
if (Array.isArray(data.data.list)) return data.data.list;
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function _extractMsgList(data) {
|
|
184
|
+
if (!data || !data.data) return [];
|
|
185
|
+
if (Array.isArray(data.data.message_list)) return data.data.message_list;
|
|
186
|
+
if (Array.isArray(data.data.messages)) return data.data.messages;
|
|
187
|
+
if (Array.isArray(data.data.list)) return data.data.list;
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _normalizeRole(r) {
|
|
192
|
+
// Doubao uses sender_type "USER" / "ASSISTANT" / "SYSTEM" or numeric codes.
|
|
193
|
+
if (r === 1 || r === "1" || r === "USER" || r === "user") return "user";
|
|
194
|
+
if (r === 2 || r === "2" || r === "ASSISTANT" || r === "assistant") return "assistant";
|
|
195
|
+
if (r === 3 || r === "3" || r === "SYSTEM" || r === "system") return "system";
|
|
196
|
+
return r || "assistant";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _buildContent(m) {
|
|
200
|
+
// Doubao messages can carry text + attachments (images uploaded by user,
|
|
201
|
+
// search refs, code blocks). For scaffold we keep text + attachment slot —
|
|
202
|
+
// Phase 10.4 will refine after fixture pin.
|
|
203
|
+
const text = m.content || m.text || m.message_content || "";
|
|
204
|
+
const out = { text };
|
|
205
|
+
if (Array.isArray(m.attachments) && m.attachments.length > 0) {
|
|
206
|
+
out.attachments = m.attachments
|
|
207
|
+
.map((a) => ({
|
|
208
|
+
type: a.kind === "image" ? "image" : "file",
|
|
209
|
+
filename: a.name || a.file_name,
|
|
210
|
+
url: a.url || a.download_url,
|
|
211
|
+
size: a.size,
|
|
212
|
+
mimeType: a.mime_type,
|
|
213
|
+
}))
|
|
214
|
+
.filter((a) => a.url || a.filename);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _toMs(t) {
|
|
220
|
+
if (typeof t === "number") return t > 1e12 ? t : t * 1000;
|
|
221
|
+
if (typeof t === "string") {
|
|
222
|
+
const n = Number(t);
|
|
223
|
+
if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
|
|
224
|
+
const d = Date.parse(t);
|
|
225
|
+
return Number.isFinite(d) ? d : 0;
|
|
226
|
+
}
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const SPEC = {
|
|
231
|
+
name: "doubao",
|
|
232
|
+
displayName: "豆包 Doubao",
|
|
233
|
+
androidPackage: "com.larus.nova",
|
|
234
|
+
loginUrl: "https://www.doubao.com/chat/",
|
|
235
|
+
cookieDomains: ["www.doubao.com", ".doubao.com"],
|
|
236
|
+
rateLimits: { perMinute: 20, minIntervalMs: 2000 },
|
|
237
|
+
|
|
238
|
+
validateCookie,
|
|
239
|
+
listConversations,
|
|
240
|
+
listMessages,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
SPEC,
|
|
245
|
+
_internal: {
|
|
246
|
+
_toMs,
|
|
247
|
+
_normalizeRole,
|
|
248
|
+
_buildContent,
|
|
249
|
+
_extractConvList,
|
|
250
|
+
_extractMsgList,
|
|
251
|
+
BASE,
|
|
252
|
+
CONV_LIST_PATH,
|
|
253
|
+
MSG_LIST_PATH,
|
|
254
|
+
},
|
|
255
|
+
};
|