@chainlesschain/personal-data-hub 0.1.0 → 0.2.1
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 +396 -0
- package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
- package/__tests__/adapters/ai-chat-vendors.test.js +874 -0
- package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
- package/__tests__/adapters/email-adapter.test.js +138 -1
- package/__tests__/adapters/email-classifier.test.js +347 -0
- package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
- package/__tests__/adapters/email-retry-progress.test.js +294 -0
- package/__tests__/adapters/email-templates.test.js +699 -0
- package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
- package/__tests__/adapters/system-data-adapter.test.js +440 -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/system-data-disclosure.test.js +153 -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 +191 -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 +556 -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__/entity-resolver-ingest-hook.test.js +177 -0
- package/__tests__/entity-resolver-stages.test.js +411 -0
- package/__tests__/entity-resolver-vault.test.js +246 -0
- package/__tests__/entity-resolver.test.js +526 -0
- package/__tests__/fixtures/entity-resolver-200-mock.json +96 -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/wechat-bootstrap-end-to-end.test.js +390 -0
- package/__tests__/longtail-adapters.test.js +217 -0
- package/__tests__/mobile-extractor.test.js +288 -0
- package/__tests__/registry.test.js +4 -2
- package/__tests__/shopping-adapters.test.js +296 -0
- package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
- package/__tests__/sidecar-supervisor.test.js +120 -0
- package/__tests__/social-adapters.test.js +206 -0
- package/__tests__/travel-adapters.test.js +325 -0
- package/__tests__/vault.test.js +3 -3
- package/__tests__/wechat-adapter.test.js +476 -0
- package/__tests__/whatsapp-adapter.test.js +135 -0
- package/lib/adapter-spec.js +12 -0
- package/lib/adapters/_python-sidecar-base.js +207 -0
- package/lib/adapters/ai-chat-history/ai-chat-adapter.js +374 -0
- package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
- 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/http-client.js +211 -0
- package/lib/adapters/ai-chat-history/index.js +28 -0
- package/lib/adapters/ai-chat-history/schema-map.js +258 -0
- package/lib/adapters/ai-chat-history/vendor-spec.js +86 -0
- package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
- package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
- package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
- package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
- package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
- package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
- package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
- package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
- package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
- package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
- package/lib/adapters/alipay-bill/alipay-bill-adapter.js +311 -0
- package/lib/adapters/alipay-bill/counterparty.js +129 -0
- package/lib/adapters/alipay-bill/csv-parser.js +217 -0
- package/lib/adapters/alipay-bill/index.js +41 -0
- package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
- package/lib/adapters/email-imap/classifier.js +495 -0
- package/lib/adapters/email-imap/email-adapter.js +419 -8
- package/lib/adapters/email-imap/index.js +42 -0
- package/lib/adapters/email-imap/pdf-extractor.js +192 -0
- package/lib/adapters/email-imap/templates/bill.js +232 -0
- package/lib/adapters/email-imap/templates/government.js +120 -0
- package/lib/adapters/email-imap/templates/index.js +78 -0
- package/lib/adapters/email-imap/templates/order.js +186 -0
- package/lib/adapters/email-imap/templates/other.js +114 -0
- package/lib/adapters/email-imap/templates/register.js +113 -0
- package/lib/adapters/email-imap/templates/travel.js +157 -0
- package/lib/adapters/email-imap/templates/utils.js +275 -0
- package/lib/adapters/email-imap/transactions.js +234 -0
- package/lib/adapters/messaging-qq/index.js +158 -0
- package/lib/adapters/messaging-telegram/index.js +142 -0
- package/lib/adapters/messaging-whatsapp/index.js +189 -0
- package/lib/adapters/shopping-base/index.js +208 -0
- package/lib/adapters/shopping-jd/index.js +150 -0
- package/lib/adapters/shopping-meituan/index.js +154 -0
- package/lib/adapters/shopping-taobao/index.js +176 -0
- package/lib/adapters/social-bilibili/index.js +171 -0
- package/lib/adapters/social-douyin/index.js +116 -0
- package/lib/adapters/social-kuaishou/index.js +237 -0
- package/lib/adapters/social-toutiao/index.js +236 -0
- package/lib/adapters/social-weibo/index.js +164 -0
- package/lib/adapters/social-xiaohongshu/index.js +96 -0
- package/lib/adapters/system-data/disclosure.js +166 -0
- package/lib/adapters/system-data/index.js +34 -0
- package/lib/adapters/system-data/system-data-adapter.js +344 -0
- package/lib/adapters/system-data-android/adapter.js +348 -0
- package/lib/adapters/system-data-android/index.js +76 -0
- package/lib/adapters/travel-12306/index.js +151 -0
- package/lib/adapters/travel-amap/index.js +164 -0
- package/lib/adapters/travel-baidu-map/index.js +162 -0
- package/lib/adapters/travel-base/index.js +240 -0
- package/lib/adapters/travel-ctrip/index.js +151 -0
- package/lib/adapters/wechat/bootstrap.js +146 -0
- package/lib/adapters/wechat/content-parser.js +326 -0
- package/lib/adapters/wechat/db-reader.js +209 -0
- package/lib/adapters/wechat/env-probe.js +218 -0
- package/lib/adapters/wechat/frida-agent/loader.js +67 -0
- package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +126 -0
- package/lib/adapters/wechat/index.js +37 -0
- package/lib/adapters/wechat/key-extractor.js +158 -0
- package/lib/adapters/wechat/key-providers/frida-key-provider.js +244 -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 +220 -0
- package/lib/adapters/wechat/wechat-adapter.js +205 -0
- package/lib/analysis-skills/base.js +113 -0
- package/lib/analysis-skills/footprint.js +167 -0
- package/lib/analysis-skills/index.js +58 -0
- package/lib/analysis-skills/interests.js +161 -0
- package/lib/analysis-skills/relations.js +226 -0
- package/lib/analysis-skills/spending.js +219 -0
- package/lib/analysis-skills/timeline.js +167 -0
- package/lib/analysis.js +191 -2
- package/lib/entity-resolver/embedding-stage.js +198 -0
- package/lib/entity-resolver/entity-resolver.js +384 -0
- package/lib/entity-resolver/index.js +42 -0
- package/lib/entity-resolver/llm-stage.js +191 -0
- package/lib/entity-resolver/rule-stage.js +208 -0
- package/lib/entity-resolver/worker.js +149 -0
- package/lib/index.js +131 -0
- package/lib/migrations.js +73 -0
- package/lib/mobile-extractor/android.js +193 -0
- package/lib/mobile-extractor/index.js +9 -0
- package/lib/mobile-extractor/ios.js +223 -0
- package/lib/prompt-builder.js +11 -1
- package/lib/query-parser.js +7 -1
- package/lib/registry.js +42 -0
- package/lib/sidecar/index.js +15 -0
- package/lib/sidecar/supervisor.js +359 -0
- package/lib/vault.js +343 -0
- package/package.json +36 -3
- package/scripts/_make-fixture-all.js +126 -0
- package/scripts/_make-fixture-contacts.js +84 -0
- package/scripts/evaluate-entity-resolver.js +213 -0
- package/scripts/smoke-phase-5-5.js +196 -0
- package/scripts/smoke-phase-5-7.js +181 -0
- package/scripts/smoke-system-data-contacts.js +309 -0
- package/scripts/smoke-system-data.js +312 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扣子 (Coze, ByteDance) vendor adapter — Phase 10.2 wiring.
|
|
3
|
+
*
|
|
4
|
+
* Reference: docs/design/Adapter_AIChat_History.md §6.7
|
|
5
|
+
* - login https://www.coze.cn/
|
|
6
|
+
* - convs GET /api/conversation/list
|
|
7
|
+
* - msgs GET /api/conversation/<id>/message
|
|
8
|
+
* - cookies 字节通用 s_v_web_id (+ session)
|
|
9
|
+
* - 特别 — agent 平台:tool_calls 多,workflow 可能嵌套。v1 flatten 为消息序列;
|
|
10
|
+
* 保留 extra.toolCalls 与 extra.botId.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
"use strict";
|
|
14
|
+
|
|
15
|
+
const BASE = "https://www.coze.cn";
|
|
16
|
+
const CONV_LIST_PATH = "/api/conversation/list";
|
|
17
|
+
const MSG_LIST_PATH = (id) => `/api/conversation/${encodeURIComponent(id)}/message`;
|
|
18
|
+
const USER_INFO_PATH = "/api/user/info";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
21
|
+
|
|
22
|
+
function _ensureClient(ctx) {
|
|
23
|
+
if (!ctx || !ctx.httpClient) throw new Error("coze: ctx.httpClient required");
|
|
24
|
+
return ctx.httpClient;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function validateCookie(ctx) {
|
|
28
|
+
const client = _ensureClient(ctx);
|
|
29
|
+
try {
|
|
30
|
+
const data = await client.getJson(BASE + USER_INFO_PATH, {
|
|
31
|
+
session: ctx.session,
|
|
32
|
+
matchDomain: "www.coze.cn",
|
|
33
|
+
});
|
|
34
|
+
if (data && data.code === 0 && data.data) {
|
|
35
|
+
return { ok: true, userId: data.data.user_id || data.data.uid };
|
|
36
|
+
}
|
|
37
|
+
return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { ok: false, reason: err.code || err.message };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function *listConversations(ctx, opts = {}) {
|
|
44
|
+
const client = _ensureClient(ctx);
|
|
45
|
+
const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
|
|
46
|
+
const sinceTs = opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
|
|
47
|
+
|
|
48
|
+
let cursor = "0";
|
|
49
|
+
let safety = 0;
|
|
50
|
+
while (safety < 200) {
|
|
51
|
+
safety++;
|
|
52
|
+
const url = new URL(BASE + CONV_LIST_PATH);
|
|
53
|
+
url.searchParams.set("limit", String(pageSize));
|
|
54
|
+
url.searchParams.set("cursor", cursor);
|
|
55
|
+
const data = await client.getJson(url.toString(), {
|
|
56
|
+
session: ctx.session,
|
|
57
|
+
matchDomain: "www.coze.cn",
|
|
58
|
+
});
|
|
59
|
+
const items = _extractList(data);
|
|
60
|
+
if (items.length === 0) return;
|
|
61
|
+
|
|
62
|
+
let stopped = false;
|
|
63
|
+
for (const c of items) {
|
|
64
|
+
const updatedAt = _toMs(c.last_updated_time || c.updated_at || c.created_at);
|
|
65
|
+
if (sinceTs && updatedAt <= sinceTs) {
|
|
66
|
+
stopped = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
yield {
|
|
70
|
+
vendor: "coze",
|
|
71
|
+
originalId: String(c.conversation_id || c.id),
|
|
72
|
+
title: c.title || c.name || undefined,
|
|
73
|
+
modelName: undefined,
|
|
74
|
+
createdAt: _toMs(c.created_at || c.create_time),
|
|
75
|
+
updatedAt,
|
|
76
|
+
messageCount: c.message_count || undefined,
|
|
77
|
+
archived: Boolean(c.archived),
|
|
78
|
+
extra: { botId: c.bot_id, scene: c.scene },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (stopped) return;
|
|
82
|
+
cursor = (data && data.data && data.data.next_cursor) || "";
|
|
83
|
+
if (!cursor || items.length < pageSize) return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function *listMessages(ctx, conversationId, _opts = {}) {
|
|
88
|
+
const client = _ensureClient(ctx);
|
|
89
|
+
const url = new URL(BASE + MSG_LIST_PATH(conversationId));
|
|
90
|
+
url.searchParams.set("limit", "200");
|
|
91
|
+
const data = await client.getJson(url.toString(), {
|
|
92
|
+
session: ctx.session,
|
|
93
|
+
matchDomain: "www.coze.cn",
|
|
94
|
+
});
|
|
95
|
+
const msgs = _extractList(data);
|
|
96
|
+
|
|
97
|
+
msgs.sort((a, b) => _toMs(a.created_at || a.create_time) - _toMs(b.created_at || b.create_time));
|
|
98
|
+
|
|
99
|
+
for (const m of msgs) {
|
|
100
|
+
yield {
|
|
101
|
+
vendor: "coze",
|
|
102
|
+
originalId: String(m.message_id || m.id),
|
|
103
|
+
conversationId: String(conversationId),
|
|
104
|
+
role: _normalizeRole(m.role || m.sender),
|
|
105
|
+
content: _buildContent(m),
|
|
106
|
+
createdAt: _toMs(m.created_at || m.create_time),
|
|
107
|
+
parentMessageId: m.parent_id ? String(m.parent_id) : undefined,
|
|
108
|
+
modelName: undefined,
|
|
109
|
+
extra: {
|
|
110
|
+
toolCalls: Array.isArray(m.tool_calls) ? m.tool_calls : undefined,
|
|
111
|
+
botId: m.bot_id,
|
|
112
|
+
workflowRunId: m.workflow_run_id,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _extractList(data) {
|
|
119
|
+
if (!data) return [];
|
|
120
|
+
if (data.data && Array.isArray(data.data.list)) return data.data.list;
|
|
121
|
+
if (data.data && Array.isArray(data.data.conversations)) return data.data.conversations;
|
|
122
|
+
if (data.data && Array.isArray(data.data.messages)) return data.data.messages;
|
|
123
|
+
if (data.data && Array.isArray(data.data.message_list)) return data.data.message_list;
|
|
124
|
+
if (Array.isArray(data.data)) return data.data;
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _normalizeRole(r) {
|
|
129
|
+
if (r === "user" || r === "USER") return "user";
|
|
130
|
+
if (r === "assistant" || r === "ASSISTANT" || r === "bot") return "assistant";
|
|
131
|
+
if (r === "system" || r === "SYSTEM") return "system";
|
|
132
|
+
if (r === "tool" || r === "TOOL") return "tool";
|
|
133
|
+
return r ? String(r).toLowerCase() : "assistant";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function _buildContent(m) {
|
|
137
|
+
const content = { text: m.content || m.text || "" };
|
|
138
|
+
if (Array.isArray(m.attachments) && m.attachments.length > 0) {
|
|
139
|
+
content.attachments = m.attachments
|
|
140
|
+
.map((a) => ({
|
|
141
|
+
type: (a.type === "image" || /image/i.test(a.mime_type || "")) ? "image" : "file",
|
|
142
|
+
filename: a.name,
|
|
143
|
+
url: a.url,
|
|
144
|
+
mimeType: a.mime_type,
|
|
145
|
+
size: a.size,
|
|
146
|
+
}))
|
|
147
|
+
.filter((a) => a.url || a.filename);
|
|
148
|
+
}
|
|
149
|
+
return content;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _toMs(t) {
|
|
153
|
+
if (typeof t === "number") return t > 1e12 ? t : t * 1000;
|
|
154
|
+
if (typeof t === "string") {
|
|
155
|
+
const n = Number(t);
|
|
156
|
+
if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
|
|
157
|
+
const d = Date.parse(t);
|
|
158
|
+
return Number.isFinite(d) ? d : 0;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const SPEC = {
|
|
164
|
+
name: "coze",
|
|
165
|
+
displayName: "扣子",
|
|
166
|
+
androidPackage: "com.coze.space",
|
|
167
|
+
loginUrl: "https://www.coze.cn/",
|
|
168
|
+
cookieDomains: ["www.coze.cn", ".coze.cn"],
|
|
169
|
+
rateLimits: { perMinute: 20, minIntervalMs: 2000 },
|
|
170
|
+
|
|
171
|
+
validateCookie,
|
|
172
|
+
listConversations,
|
|
173
|
+
listMessages,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
SPEC,
|
|
178
|
+
_internal: { _toMs, _normalizeRole, _buildContent, _extractList, BASE },
|
|
179
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek vendor adapter — Phase 10.2 wiring.
|
|
3
|
+
*
|
|
4
|
+
* Reference: docs/design/Adapter_AIChat_History.md §6.1
|
|
5
|
+
* - login https://chat.deepseek.com/
|
|
6
|
+
* - convs GET /api/v0/chat_session/fetch_page?count=N&before=<cursor>
|
|
7
|
+
* - msgs GET /api/v0/chat/history_messages?chat_session_id=<id>
|
|
8
|
+
* - cookies userToken / session cookie
|
|
9
|
+
*
|
|
10
|
+
* The vendor exposes three async surfaces:
|
|
11
|
+
*
|
|
12
|
+
* validateCookie(ctx) — HEAD on userinfo endpoint, classifies cookie state.
|
|
13
|
+
* listConversations(ctx, o) — paginate via `before` cursor, yield RawConversation.
|
|
14
|
+
* listMessages(ctx, id, o) — return chronological RawMessage[] for one conversation.
|
|
15
|
+
*
|
|
16
|
+
* Each surface receives `ctx.httpClient` (a configured HttpClient bound to
|
|
17
|
+
* this vendor's rateLimits + cookie session). Tests pass a stub HttpClient
|
|
18
|
+
* with deterministic responses; production wires the real one via
|
|
19
|
+
* AIChatHistoryAdapter.
|
|
20
|
+
*
|
|
21
|
+
* Response shape notes (reverse-engineered, may shift — capture in fixtures
|
|
22
|
+
* before changing wire parser):
|
|
23
|
+
*
|
|
24
|
+
* /chat_session/fetch_page →
|
|
25
|
+
* { data: { biz_data: { chat_sessions: [{ id, title, model, inserted_at,
|
|
26
|
+
* updated_at, current_message_id, ... }] } } }
|
|
27
|
+
*
|
|
28
|
+
* /chat/history_messages →
|
|
29
|
+
* { data: { biz_data: { chat_messages: [{ id, parent_id, role,
|
|
30
|
+
* message_content, content, inserted_at, model, thinking_enabled,
|
|
31
|
+
* files: [...] }] } } }
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
"use strict";
|
|
35
|
+
|
|
36
|
+
const BASE = "https://chat.deepseek.com";
|
|
37
|
+
const CONV_PAGE_PATH = "/api/v0/chat_session/fetch_page";
|
|
38
|
+
const MSG_PATH = "/api/v0/chat/history_messages";
|
|
39
|
+
const USER_INFO_PATH = "/api/v0/user/get_user_info";
|
|
40
|
+
|
|
41
|
+
const DEFAULT_PAGE_SIZE = 30;
|
|
42
|
+
|
|
43
|
+
function _ensureClient(ctx) {
|
|
44
|
+
if (!ctx || !ctx.httpClient) {
|
|
45
|
+
throw new Error("deepseek: ctx.httpClient required (AIChatHistoryAdapter must wire one)");
|
|
46
|
+
}
|
|
47
|
+
return ctx.httpClient;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function validateCookie(ctx) {
|
|
51
|
+
const client = _ensureClient(ctx);
|
|
52
|
+
try {
|
|
53
|
+
const data = await client.getJson(BASE + USER_INFO_PATH, { session: ctx.session });
|
|
54
|
+
if (data && data.code === 0 && data.data) {
|
|
55
|
+
return { ok: true, userId: data.data.biz_data && data.data.biz_data.user_id };
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { ok: false, reason: err.code || err.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Yield one RawConversation per remote chat session, newest first. Pagination
|
|
65
|
+
* uses the `before` cursor returned by deepseek (the inserted_at of the last
|
|
66
|
+
* returned session). Stop when `chat_sessions` is empty OR we hit
|
|
67
|
+
* opts.since (timestamp watermark from prior sync).
|
|
68
|
+
*/
|
|
69
|
+
async function *listConversations(ctx, opts = {}) {
|
|
70
|
+
const client = _ensureClient(ctx);
|
|
71
|
+
const limit = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
|
|
72
|
+
const sinceTs = opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
|
|
73
|
+
let cursor = null;
|
|
74
|
+
|
|
75
|
+
while (true) {
|
|
76
|
+
const url = new URL(BASE + CONV_PAGE_PATH);
|
|
77
|
+
url.searchParams.set("count", String(limit));
|
|
78
|
+
if (cursor != null) url.searchParams.set("before", String(cursor));
|
|
79
|
+
|
|
80
|
+
const data = await client.getJson(url.toString(), { session: ctx.session });
|
|
81
|
+
const sessions = data && data.data && data.data.biz_data && data.data.biz_data.chat_sessions;
|
|
82
|
+
if (!Array.isArray(sessions) || sessions.length === 0) return;
|
|
83
|
+
|
|
84
|
+
let stopped = false;
|
|
85
|
+
for (const s of sessions) {
|
|
86
|
+
const updatedAt = _toMs(s.updated_at || s.inserted_at);
|
|
87
|
+
if (sinceTs && updatedAt <= sinceTs) {
|
|
88
|
+
stopped = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
yield {
|
|
92
|
+
vendor: "deepseek",
|
|
93
|
+
originalId: String(s.id),
|
|
94
|
+
title: s.title || undefined,
|
|
95
|
+
modelName: s.model || undefined,
|
|
96
|
+
createdAt: _toMs(s.inserted_at),
|
|
97
|
+
updatedAt,
|
|
98
|
+
messageCount: s.message_count || undefined,
|
|
99
|
+
archived: false,
|
|
100
|
+
extra: {
|
|
101
|
+
currentMessageId: s.current_message_id,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (stopped) return;
|
|
106
|
+
// Advance cursor — `before` expects the oldest timestamp from this page.
|
|
107
|
+
const last = sessions[sessions.length - 1];
|
|
108
|
+
cursor = last.inserted_at || last.updated_at;
|
|
109
|
+
if (!cursor) return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Yield each message in a conversation. DeepSeek already returns messages in
|
|
115
|
+
* chronological order — we re-yield them as-is.
|
|
116
|
+
*/
|
|
117
|
+
async function *listMessages(ctx, conversationId, _opts = {}) {
|
|
118
|
+
const client = _ensureClient(ctx);
|
|
119
|
+
const url = new URL(BASE + MSG_PATH);
|
|
120
|
+
url.searchParams.set("chat_session_id", String(conversationId));
|
|
121
|
+
|
|
122
|
+
const data = await client.getJson(url.toString(), { session: ctx.session });
|
|
123
|
+
const msgs = data && data.data && data.data.biz_data && data.data.biz_data.chat_messages;
|
|
124
|
+
if (!Array.isArray(msgs)) return;
|
|
125
|
+
|
|
126
|
+
for (const m of msgs) {
|
|
127
|
+
yield {
|
|
128
|
+
vendor: "deepseek",
|
|
129
|
+
originalId: String(m.id),
|
|
130
|
+
conversationId: String(conversationId),
|
|
131
|
+
role: _normalizeRole(m.role),
|
|
132
|
+
content: _buildContent(m),
|
|
133
|
+
createdAt: _toMs(m.inserted_at),
|
|
134
|
+
parentMessageId: m.parent_id ? String(m.parent_id) : undefined,
|
|
135
|
+
modelName: m.model || undefined,
|
|
136
|
+
extra: {
|
|
137
|
+
thinking: m.thinking_content || undefined,
|
|
138
|
+
thinkingEnabled: Boolean(m.thinking_enabled),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _normalizeRole(r) {
|
|
145
|
+
if (r === "USER" || r === "user") return "user";
|
|
146
|
+
if (r === "ASSISTANT" || r === "assistant") return "assistant";
|
|
147
|
+
if (r === "SYSTEM" || r === "system") return "system";
|
|
148
|
+
if (r === "TOOL" || r === "tool") return "tool";
|
|
149
|
+
return r || "assistant";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _buildContent(m) {
|
|
153
|
+
const content = { text: m.content || m.message_content || "" };
|
|
154
|
+
if (Array.isArray(m.files) && m.files.length > 0) {
|
|
155
|
+
content.attachments = m.files
|
|
156
|
+
.map((f) => ({
|
|
157
|
+
type: f.kind === "image" ? "image" : "file",
|
|
158
|
+
filename: f.name || f.file_name,
|
|
159
|
+
url: f.url || f.download_url,
|
|
160
|
+
size: f.size,
|
|
161
|
+
mimeType: f.mime_type,
|
|
162
|
+
}))
|
|
163
|
+
.filter((a) => a.url || a.filename);
|
|
164
|
+
}
|
|
165
|
+
return content;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _toMs(t) {
|
|
169
|
+
if (typeof t === "number") {
|
|
170
|
+
// deepseek uses seconds since epoch on this endpoint.
|
|
171
|
+
return t > 1e12 ? t : t * 1000;
|
|
172
|
+
}
|
|
173
|
+
if (typeof t === "string") {
|
|
174
|
+
const n = Number(t);
|
|
175
|
+
if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
|
|
176
|
+
const d = Date.parse(t);
|
|
177
|
+
return Number.isFinite(d) ? d : 0;
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SPEC = {
|
|
183
|
+
name: "deepseek",
|
|
184
|
+
displayName: "DeepSeek",
|
|
185
|
+
androidPackage: "com.deepseek.chat",
|
|
186
|
+
loginUrl: "https://chat.deepseek.com/",
|
|
187
|
+
cookieDomains: ["chat.deepseek.com", ".deepseek.com"],
|
|
188
|
+
rateLimits: { perMinute: 30, minIntervalMs: 1500 },
|
|
189
|
+
|
|
190
|
+
validateCookie,
|
|
191
|
+
listConversations,
|
|
192
|
+
listMessages,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
SPEC,
|
|
197
|
+
// exported for tests
|
|
198
|
+
_internal: { _toMs, _normalizeRole, _buildContent, BASE, CONV_PAGE_PATH, MSG_PATH },
|
|
199
|
+
};
|
|
@@ -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
|
+
};
|