@chainlesschain/personal-data-hub 0.1.0 → 0.2.0

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.
Files changed (116) hide show
  1. package/__tests__/adapters/ai-chat-history.test.js +395 -0
  2. package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
  3. package/__tests__/adapters/ai-chat-vendors.test.js +733 -0
  4. package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
  5. package/__tests__/adapters/email-adapter.test.js +138 -1
  6. package/__tests__/adapters/email-classifier.test.js +347 -0
  7. package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
  8. package/__tests__/adapters/email-retry-progress.test.js +294 -0
  9. package/__tests__/adapters/email-templates.test.js +699 -0
  10. package/__tests__/adapters/system-data-adapter.test.js +440 -0
  11. package/__tests__/adapters/system-data-disclosure.test.js +153 -0
  12. package/__tests__/analysis-skills.test.js +409 -0
  13. package/__tests__/entity-resolver-ingest-hook.test.js +177 -0
  14. package/__tests__/entity-resolver-stages.test.js +411 -0
  15. package/__tests__/entity-resolver-vault.test.js +246 -0
  16. package/__tests__/entity-resolver.test.js +526 -0
  17. package/__tests__/fixtures/entity-resolver-200-mock.json +96 -0
  18. package/__tests__/longtail-adapters.test.js +217 -0
  19. package/__tests__/mobile-extractor.test.js +288 -0
  20. package/__tests__/shopping-adapters.test.js +296 -0
  21. package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
  22. package/__tests__/sidecar-supervisor.test.js +120 -0
  23. package/__tests__/social-adapters.test.js +206 -0
  24. package/__tests__/travel-adapters.test.js +325 -0
  25. package/__tests__/vault.test.js +3 -3
  26. package/__tests__/wechat-adapter.test.js +476 -0
  27. package/__tests__/whatsapp-adapter.test.js +135 -0
  28. package/lib/adapter-spec.js +12 -0
  29. package/lib/adapters/_python-sidecar-base.js +207 -0
  30. package/lib/adapters/ai-chat-history/ai-chat-adapter.js +335 -0
  31. package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
  32. package/lib/adapters/ai-chat-history/http-client.js +211 -0
  33. package/lib/adapters/ai-chat-history/index.js +28 -0
  34. package/lib/adapters/ai-chat-history/schema-map.js +221 -0
  35. package/lib/adapters/ai-chat-history/vendor-spec.js +85 -0
  36. package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
  37. package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
  38. package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
  39. package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
  40. package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
  41. package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
  42. package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
  43. package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
  44. package/lib/adapters/alipay-bill/alipay-bill-adapter.js +307 -0
  45. package/lib/adapters/alipay-bill/counterparty.js +129 -0
  46. package/lib/adapters/alipay-bill/csv-parser.js +217 -0
  47. package/lib/adapters/alipay-bill/index.js +41 -0
  48. package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
  49. package/lib/adapters/email-imap/classifier.js +495 -0
  50. package/lib/adapters/email-imap/email-adapter.js +419 -8
  51. package/lib/adapters/email-imap/index.js +42 -0
  52. package/lib/adapters/email-imap/pdf-extractor.js +192 -0
  53. package/lib/adapters/email-imap/templates/bill.js +232 -0
  54. package/lib/adapters/email-imap/templates/government.js +120 -0
  55. package/lib/adapters/email-imap/templates/index.js +78 -0
  56. package/lib/adapters/email-imap/templates/order.js +186 -0
  57. package/lib/adapters/email-imap/templates/other.js +114 -0
  58. package/lib/adapters/email-imap/templates/register.js +113 -0
  59. package/lib/adapters/email-imap/templates/travel.js +157 -0
  60. package/lib/adapters/email-imap/templates/utils.js +275 -0
  61. package/lib/adapters/email-imap/transactions.js +234 -0
  62. package/lib/adapters/messaging-qq/index.js +158 -0
  63. package/lib/adapters/messaging-telegram/index.js +142 -0
  64. package/lib/adapters/messaging-whatsapp/index.js +189 -0
  65. package/lib/adapters/shopping-base/index.js +208 -0
  66. package/lib/adapters/shopping-jd/index.js +150 -0
  67. package/lib/adapters/shopping-meituan/index.js +154 -0
  68. package/lib/adapters/shopping-taobao/index.js +176 -0
  69. package/lib/adapters/social-bilibili/index.js +171 -0
  70. package/lib/adapters/social-douyin/index.js +116 -0
  71. package/lib/adapters/social-weibo/index.js +164 -0
  72. package/lib/adapters/social-xiaohongshu/index.js +96 -0
  73. package/lib/adapters/system-data/disclosure.js +166 -0
  74. package/lib/adapters/system-data/index.js +34 -0
  75. package/lib/adapters/system-data/system-data-adapter.js +344 -0
  76. package/lib/adapters/travel-12306/index.js +151 -0
  77. package/lib/adapters/travel-amap/index.js +164 -0
  78. package/lib/adapters/travel-baidu-map/index.js +162 -0
  79. package/lib/adapters/travel-base/index.js +240 -0
  80. package/lib/adapters/travel-ctrip/index.js +151 -0
  81. package/lib/adapters/wechat/content-parser.js +326 -0
  82. package/lib/adapters/wechat/db-reader.js +209 -0
  83. package/lib/adapters/wechat/index.js +28 -0
  84. package/lib/adapters/wechat/key-extractor.js +158 -0
  85. package/lib/adapters/wechat/normalize.js +220 -0
  86. package/lib/adapters/wechat/wechat-adapter.js +205 -0
  87. package/lib/analysis-skills/base.js +113 -0
  88. package/lib/analysis-skills/footprint.js +167 -0
  89. package/lib/analysis-skills/index.js +58 -0
  90. package/lib/analysis-skills/interests.js +161 -0
  91. package/lib/analysis-skills/relations.js +226 -0
  92. package/lib/analysis-skills/spending.js +216 -0
  93. package/lib/analysis-skills/timeline.js +167 -0
  94. package/lib/entity-resolver/embedding-stage.js +198 -0
  95. package/lib/entity-resolver/entity-resolver.js +384 -0
  96. package/lib/entity-resolver/index.js +42 -0
  97. package/lib/entity-resolver/llm-stage.js +191 -0
  98. package/lib/entity-resolver/rule-stage.js +208 -0
  99. package/lib/entity-resolver/worker.js +149 -0
  100. package/lib/index.js +115 -0
  101. package/lib/migrations.js +73 -0
  102. package/lib/mobile-extractor/android.js +193 -0
  103. package/lib/mobile-extractor/index.js +9 -0
  104. package/lib/mobile-extractor/ios.js +223 -0
  105. package/lib/registry.js +42 -0
  106. package/lib/sidecar/index.js +15 -0
  107. package/lib/sidecar/supervisor.js +359 -0
  108. package/lib/vault.js +266 -0
  109. package/package.json +29 -3
  110. package/scripts/_make-fixture-all.js +126 -0
  111. package/scripts/_make-fixture-contacts.js +84 -0
  112. package/scripts/evaluate-entity-resolver.js +213 -0
  113. package/scripts/smoke-phase-5-5.js +196 -0
  114. package/scripts/smoke-phase-5-7.js +181 -0
  115. package/scripts/smoke-system-data-contacts.js +309 -0
  116. package/scripts/smoke-system-data.js +312 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * 腾讯混元 / 元宝 (Tencent Hunyuan / Yuanbao) vendor adapter — Phase 10.2.
3
+ *
4
+ * Reference: docs/design/Adapter_AIChat_History.md §6.5
5
+ * - login https://yuanbao.tencent.com/
6
+ * - convs POST /api/user/conv/list
7
+ * - msgs POST /api/user/conv/<id>/message/list
8
+ * - cookies hy_token + 腾讯 uin series
9
+ * - 风控 strong — keep conservative rateLimits.
10
+ *
11
+ * Special: 微信生态联动 messages may reference 微信公众号 articles via
12
+ * `extra.linkedArticles` (link cards). We preserve them as attachments[].
13
+ */
14
+
15
+ "use strict";
16
+
17
+ const BASE = "https://yuanbao.tencent.com";
18
+ const CONV_LIST_PATH = "/api/user/conv/list";
19
+ const MSG_LIST_PATH = (id) => `/api/user/conv/${encodeURIComponent(id)}/message/list`;
20
+ const USER_INFO_PATH = "/api/user/info";
21
+
22
+ const DEFAULT_PAGE_SIZE = 30;
23
+
24
+ function _ensureClient(ctx) {
25
+ if (!ctx || !ctx.httpClient) throw new Error("hunyuan: ctx.httpClient required");
26
+ return ctx.httpClient;
27
+ }
28
+
29
+ async function validateCookie(ctx) {
30
+ const client = _ensureClient(ctx);
31
+ try {
32
+ const data = await client.getJson(BASE + USER_INFO_PATH, { session: ctx.session });
33
+ if (data && (data.ret === 0 || data.code === 0) && (data.data || data.user)) {
34
+ const u = data.data || data.user;
35
+ return { ok: true, userId: u.userId || u.uin || u.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 = null;
49
+ let safety = 0;
50
+ while (safety < 200) {
51
+ safety++;
52
+ const body = { count: pageSize, ...(cursor ? { cursor } : {}) };
53
+ const data = await client.postJson(BASE + CONV_LIST_PATH, body, { session: ctx.session });
54
+ const items = _extractList(data);
55
+ if (items.length === 0) return;
56
+
57
+ let stopped = false;
58
+ for (const c of items) {
59
+ const updatedAt = _toMs(c.updateTime || c.update_time || c.createTime);
60
+ if (sinceTs && updatedAt <= sinceTs) {
61
+ stopped = true;
62
+ break;
63
+ }
64
+ yield {
65
+ vendor: "hunyuan",
66
+ originalId: String(c.convId || c.conv_id || c.id),
67
+ title: c.title || c.summary || undefined,
68
+ modelName: c.model || undefined,
69
+ createdAt: _toMs(c.createTime || c.create_time),
70
+ updatedAt,
71
+ messageCount: c.msgCount || c.message_count,
72
+ archived: Boolean(c.archived),
73
+ };
74
+ }
75
+ if (stopped) return;
76
+ cursor = (data && data.data && data.data.cursor) || (items[items.length - 1] && items[items.length - 1].cursor);
77
+ if (!cursor || items.length < pageSize) return;
78
+ }
79
+ }
80
+
81
+ async function *listMessages(ctx, conversationId, _opts = {}) {
82
+ const client = _ensureClient(ctx);
83
+ const url = BASE + MSG_LIST_PATH(conversationId);
84
+ const data = await client.postJson(url, { convId: String(conversationId), count: 200 }, { session: ctx.session });
85
+ const msgs = _extractList(data);
86
+
87
+ msgs.sort((a, b) => _toMs(a.createTime || a.create_time) - _toMs(b.createTime || b.create_time));
88
+
89
+ for (const m of msgs) {
90
+ yield {
91
+ vendor: "hunyuan",
92
+ originalId: String(m.msgId || m.msg_id || m.id),
93
+ conversationId: String(conversationId),
94
+ role: _normalizeRole(m.speaker || m.role || m.type),
95
+ content: _buildContent(m),
96
+ createdAt: _toMs(m.createTime || m.create_time),
97
+ parentMessageId: m.parentMsgId ? String(m.parentMsgId) : undefined,
98
+ modelName: m.model || undefined,
99
+ extra: m.linkedArticles ? { linkedArticles: m.linkedArticles } : undefined,
100
+ };
101
+ }
102
+ }
103
+
104
+ function _extractList(data) {
105
+ if (!data) return [];
106
+ if (data.data && Array.isArray(data.data.list)) return data.data.list;
107
+ if (data.data && Array.isArray(data.data.convs)) return data.data.convs;
108
+ if (data.data && Array.isArray(data.data.messages)) return data.data.messages;
109
+ if (Array.isArray(data.data)) return data.data;
110
+ if (Array.isArray(data.list)) return data.list;
111
+ return [];
112
+ }
113
+
114
+ function _normalizeRole(r) {
115
+ if (r === "user" || r === "USER" || r === "human") return "user";
116
+ if (r === "assistant" || r === "ASSISTANT" || r === "bot" || r === "ai") return "assistant";
117
+ if (r === "system" || r === "SYSTEM") return "system";
118
+ return r ? String(r).toLowerCase() : "assistant";
119
+ }
120
+
121
+ function _buildContent(m) {
122
+ const content = { text: m.content || m.text || "" };
123
+ const attachments = [];
124
+ if (Array.isArray(m.files)) {
125
+ for (const f of m.files) {
126
+ attachments.push({
127
+ type: (f.type === "image" || /image/i.test(f.mimeType || "")) ? "image" : "file",
128
+ filename: f.name,
129
+ url: f.url,
130
+ mimeType: f.mimeType,
131
+ size: f.size,
132
+ });
133
+ }
134
+ }
135
+ if (Array.isArray(m.linkedArticles)) {
136
+ for (const link of m.linkedArticles) {
137
+ attachments.push({
138
+ type: "file",
139
+ filename: link.title,
140
+ url: link.url,
141
+ mimeType: "text/html",
142
+ });
143
+ }
144
+ }
145
+ if (attachments.length > 0) content.attachments = attachments;
146
+ return content;
147
+ }
148
+
149
+ function _toMs(t) {
150
+ if (typeof t === "number") return t > 1e12 ? t : t * 1000;
151
+ if (typeof t === "string") {
152
+ const n = Number(t);
153
+ if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
154
+ const d = Date.parse(t);
155
+ return Number.isFinite(d) ? d : 0;
156
+ }
157
+ return 0;
158
+ }
159
+
160
+ const SPEC = {
161
+ name: "hunyuan",
162
+ displayName: "腾讯元宝",
163
+ androidPackage: "com.tencent.hunyuan.app.chat",
164
+ loginUrl: "https://yuanbao.tencent.com/",
165
+ cookieDomains: ["yuanbao.tencent.com", ".tencent.com"],
166
+ rateLimits: { perMinute: 20, minIntervalMs: 2000 },
167
+
168
+ validateCookie,
169
+ listConversations,
170
+ listMessages,
171
+ };
172
+
173
+ module.exports = {
174
+ SPEC,
175
+ _internal: { _toMs, _normalizeRole, _buildContent, _extractList, BASE },
176
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Kimi (Moonshot) vendor adapter — Phase 10.2 wiring.
3
+ *
4
+ * Reference: docs/design/Adapter_AIChat_History.md §6.2
5
+ * - login https://kimi.moonshot.cn/
6
+ * - convs GET https://kimi.moonshot.cn/api/chat/list?offset=N&size=M
7
+ * - msgs POST https://kimi.moonshot.cn/api/chat/{convId}/segment/scroll
8
+ * body {last:0|"<id>", limit:N}
9
+ * - userinfo GET https://kimi.moonshot.cn/api/user
10
+ *
11
+ * Kimi pages messages via a `last` cursor (message id) rather than offset.
12
+ * `last:0` returns the most recent N; subsequent calls pass the oldest id
13
+ * from the previous page to walk backward. We collect all then sort by
14
+ * `created_at` ascending before yielding.
15
+ *
16
+ * Response shape (subject to drift; capture fixtures before re-parsing):
17
+ *
18
+ * /api/chat/list →
19
+ * { items: [{ id, name, created_at, updated_at, status, last_message,
20
+ * message_count, status }], total }
21
+ *
22
+ * /api/chat/{id}/segment/scroll →
23
+ * { items: [{ id, role, content, created_at, group_id, files: [...] }],
24
+ * has_more, last_id }
25
+ */
26
+
27
+ "use strict";
28
+
29
+ const BASE = "https://kimi.moonshot.cn";
30
+ const CONV_LIST_PATH = "/api/chat/list";
31
+ const USER_PATH = "/api/user";
32
+
33
+ const DEFAULT_PAGE_SIZE = 30;
34
+
35
+ function _ensureClient(ctx) {
36
+ if (!ctx || !ctx.httpClient) {
37
+ throw new Error("kimi: ctx.httpClient required");
38
+ }
39
+ return ctx.httpClient;
40
+ }
41
+
42
+ async function validateCookie(ctx) {
43
+ const client = _ensureClient(ctx);
44
+ try {
45
+ const data = await client.getJson(BASE + USER_PATH, { session: ctx.session });
46
+ if (data && (data.id || data.user_id)) {
47
+ return { ok: true, userId: data.id || data.user_id };
48
+ }
49
+ return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
50
+ } catch (err) {
51
+ return { ok: false, reason: err.code || err.message };
52
+ }
53
+ }
54
+
55
+ async function *listConversations(ctx, opts = {}) {
56
+ const client = _ensureClient(ctx);
57
+ const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
58
+ const sinceTs = opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
59
+
60
+ let offset = 0;
61
+ while (true) {
62
+ const url = new URL(BASE + CONV_LIST_PATH);
63
+ url.searchParams.set("offset", String(offset));
64
+ url.searchParams.set("size", String(pageSize));
65
+
66
+ const data = await client.getJson(url.toString(), { session: ctx.session });
67
+ const items = data && Array.isArray(data.items) ? data.items : [];
68
+ if (items.length === 0) return;
69
+
70
+ let stopped = false;
71
+ for (const c of items) {
72
+ const updatedAt = _toMs(c.updated_at || c.created_at);
73
+ if (sinceTs && updatedAt <= sinceTs) {
74
+ stopped = true;
75
+ break;
76
+ }
77
+ yield {
78
+ vendor: "kimi",
79
+ originalId: String(c.id),
80
+ title: c.name || undefined,
81
+ modelName: undefined, // kimi doesn't expose per-conv model on list
82
+ createdAt: _toMs(c.created_at),
83
+ updatedAt,
84
+ messageCount: c.message_count || undefined,
85
+ archived: c.status === "archived",
86
+ extra: {
87
+ lastMessage: c.last_message,
88
+ },
89
+ };
90
+ }
91
+ if (stopped) return;
92
+ offset += items.length;
93
+ if (typeof data.total === "number" && offset >= data.total) return;
94
+ }
95
+ }
96
+
97
+ async function *listMessages(ctx, conversationId, opts = {}) {
98
+ const client = _ensureClient(ctx);
99
+ const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
100
+ const url = BASE + "/api/chat/" + encodeURIComponent(conversationId) + "/segment/scroll";
101
+
102
+ const all = [];
103
+ let last = "0"; // kimi convention: 0 = most recent
104
+ let safety = 0;
105
+ while (safety < 50) {
106
+ safety++;
107
+ const data = await client.postJson(url, { last, limit: pageSize }, { session: ctx.session });
108
+ const items = data && Array.isArray(data.items) ? data.items : [];
109
+ if (items.length === 0) break;
110
+ all.push(...items);
111
+ if (!data.has_more) break;
112
+ last = String(data.last_id || items[items.length - 1].id);
113
+ }
114
+
115
+ // Sort by created_at ascending for stable chronological yield.
116
+ all.sort((a, b) => _toMs(a.created_at) - _toMs(b.created_at));
117
+ for (const m of all) {
118
+ yield {
119
+ vendor: "kimi",
120
+ originalId: String(m.id),
121
+ conversationId: String(conversationId),
122
+ role: _normalizeRole(m.role),
123
+ content: _buildContent(m),
124
+ createdAt: _toMs(m.created_at),
125
+ parentMessageId: m.parent_id ? String(m.parent_id) : undefined,
126
+ modelName: m.model || undefined,
127
+ extra: m.group_id ? { groupId: m.group_id } : undefined,
128
+ };
129
+ }
130
+ }
131
+
132
+ function _normalizeRole(r) {
133
+ if (r === "user" || r === "USER") return "user";
134
+ if (r === "assistant" || r === "ASSISTANT" || r === "bot") return "assistant";
135
+ if (r === "system" || r === "SYSTEM") return "system";
136
+ return r || "assistant";
137
+ }
138
+
139
+ function _buildContent(m) {
140
+ const content = { text: m.content || "" };
141
+ if (Array.isArray(m.files) && m.files.length > 0) {
142
+ content.attachments = m.files
143
+ .map((f) => ({
144
+ type: (f.type === "image" || /image/i.test(f.mime_type || "")) ? "image" : "file",
145
+ filename: f.name,
146
+ url: f.preview_url || f.url || f.download_url,
147
+ size: f.size,
148
+ mimeType: f.mime_type,
149
+ }))
150
+ .filter((a) => a.url || a.filename);
151
+ }
152
+ return content;
153
+ }
154
+
155
+ function _toMs(t) {
156
+ if (typeof t === "number") return t > 1e12 ? t : t * 1000;
157
+ if (typeof t === "string") {
158
+ const n = Number(t);
159
+ if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
160
+ const d = Date.parse(t);
161
+ return Number.isFinite(d) ? d : 0;
162
+ }
163
+ return 0;
164
+ }
165
+
166
+ const SPEC = {
167
+ name: "kimi",
168
+ displayName: "Kimi",
169
+ androidPackage: "com.moonshot.kimichat",
170
+ loginUrl: "https://kimi.moonshot.cn/",
171
+ cookieDomains: ["kimi.moonshot.cn", ".moonshot.cn"],
172
+ rateLimits: { perMinute: 30, minIntervalMs: 1500 },
173
+
174
+ validateCookie,
175
+ listConversations,
176
+ listMessages,
177
+ };
178
+
179
+ module.exports = {
180
+ SPEC,
181
+ _internal: { _toMs, _normalizeRole, _buildContent, BASE, CONV_LIST_PATH },
182
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 百度千帆 / 文心一言 (Baidu Qianfan / Yiyan) vendor adapter — Phase 10.2.
3
+ *
4
+ * Reference: docs/design/Adapter_AIChat_History.md §6.6
5
+ * - login https://yiyan.baidu.com/ (个人) or https://qianfan.cloud.baidu.com/
6
+ * - convs POST /aichat/conversation/list
7
+ * - msgs POST /aichat/conversation/getMessages
8
+ * - cookies BAIDUID + BDUSS
9
+ * - 风控 strong — Baidu full-stack anti-bot.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ const BASE = "https://yiyan.baidu.com";
15
+ const CONV_LIST_PATH = "/aichat/conversation/list";
16
+ const MSG_LIST_PATH = "/aichat/conversation/getMessages";
17
+ const USER_INFO_PATH = "/aichat/user/info";
18
+
19
+ const DEFAULT_PAGE_SIZE = 30;
20
+
21
+ function _ensureClient(ctx) {
22
+ if (!ctx || !ctx.httpClient) throw new Error("qianfan: ctx.httpClient required");
23
+ return ctx.httpClient;
24
+ }
25
+
26
+ async function validateCookie(ctx) {
27
+ const client = _ensureClient(ctx);
28
+ try {
29
+ const data = await client.postJson(BASE + USER_INFO_PATH, {}, { session: ctx.session });
30
+ if (data && (data.code === 0 || data.errno === 0) && (data.data || data.user)) {
31
+ const u = data.data || data.user;
32
+ return { ok: true, userId: u.uk || u.userId || u.uid };
33
+ }
34
+ return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
35
+ } catch (err) {
36
+ return { ok: false, reason: err.code || err.message };
37
+ }
38
+ }
39
+
40
+ async function *listConversations(ctx, opts = {}) {
41
+ const client = _ensureClient(ctx);
42
+ const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
43
+ const sinceTs = opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
44
+
45
+ let pageNo = 1;
46
+ while (true) {
47
+ const body = { pageNo, pageSize };
48
+ const data = await client.postJson(BASE + CONV_LIST_PATH, body, { session: ctx.session });
49
+ const list = _extractList(data);
50
+ if (list.length === 0) return;
51
+
52
+ let stopped = false;
53
+ for (const c of list) {
54
+ const updatedAt = _toMs(c.updateTime || c.update_time);
55
+ if (sinceTs && updatedAt <= sinceTs) {
56
+ stopped = true;
57
+ break;
58
+ }
59
+ yield {
60
+ vendor: "qianfan",
61
+ originalId: String(c.sessionId || c.session_id || c.id),
62
+ title: c.sessionName || c.title || undefined,
63
+ modelName: c.model || c.modelName || undefined,
64
+ createdAt: _toMs(c.createTime || c.create_time),
65
+ updatedAt,
66
+ messageCount: c.messageCount || undefined,
67
+ archived: Boolean(c.archived),
68
+ };
69
+ }
70
+ if (stopped) return;
71
+ if (list.length < pageSize) return;
72
+ pageNo++;
73
+ if (pageNo > 200) return;
74
+ }
75
+ }
76
+
77
+ async function *listMessages(ctx, conversationId, _opts = {}) {
78
+ const client = _ensureClient(ctx);
79
+ const body = { sessionId: String(conversationId), pageSize: 200 };
80
+ const data = await client.postJson(BASE + MSG_LIST_PATH, body, { session: ctx.session });
81
+ const msgs = _extractList(data);
82
+
83
+ msgs.sort((a, b) => _toMs(a.createTime || a.create_time) - _toMs(b.createTime || b.create_time));
84
+
85
+ for (const m of msgs) {
86
+ yield {
87
+ vendor: "qianfan",
88
+ originalId: String(m.messageId || m.message_id || m.id),
89
+ conversationId: String(conversationId),
90
+ role: _normalizeRole(m.role || m.type || (m.fromUser ? "user" : "assistant")),
91
+ content: _buildContent(m),
92
+ createdAt: _toMs(m.createTime || m.create_time),
93
+ parentMessageId: m.parentMessageId ? String(m.parentMessageId) : undefined,
94
+ modelName: m.model || undefined,
95
+ extra: m.references ? { references: m.references } : undefined,
96
+ };
97
+ }
98
+ }
99
+
100
+ function _extractList(data) {
101
+ if (!data) return [];
102
+ if (data.data && Array.isArray(data.data.list)) return data.data.list;
103
+ if (data.data && Array.isArray(data.data.sessions)) return data.data.sessions;
104
+ if (data.data && Array.isArray(data.data.messages)) return data.data.messages;
105
+ if (Array.isArray(data.data)) return data.data;
106
+ if (Array.isArray(data.list)) return data.list;
107
+ return [];
108
+ }
109
+
110
+ function _normalizeRole(r) {
111
+ if (r === "user" || r === "USER" || r === 1) return "user";
112
+ if (r === "assistant" || r === "ASSISTANT" || r === "bot" || r === 2) return "assistant";
113
+ if (r === "system" || r === "SYSTEM" || r === 0) return "system";
114
+ return r ? String(r).toLowerCase() : "assistant";
115
+ }
116
+
117
+ function _buildContent(m) {
118
+ const content = { text: m.content || m.text || "" };
119
+ if (Array.isArray(m.attachments) && m.attachments.length > 0) {
120
+ content.attachments = m.attachments
121
+ .map((a) => ({
122
+ type: (a.type === "image" || /image/i.test(a.mimeType || "")) ? "image" : "file",
123
+ filename: a.name,
124
+ url: a.url,
125
+ mimeType: a.mimeType,
126
+ size: a.size,
127
+ }))
128
+ .filter((a) => a.url || a.filename);
129
+ }
130
+ return content;
131
+ }
132
+
133
+ function _toMs(t) {
134
+ if (typeof t === "number") return t > 1e12 ? t : t * 1000;
135
+ if (typeof t === "string") {
136
+ const n = Number(t);
137
+ if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
138
+ const d = Date.parse(t);
139
+ return Number.isFinite(d) ? d : 0;
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ const SPEC = {
145
+ name: "qianfan",
146
+ displayName: "百度千帆",
147
+ androidPackage: "com.baidu.qianfan.llmkitchat",
148
+ loginUrl: "https://yiyan.baidu.com/",
149
+ cookieDomains: ["yiyan.baidu.com", ".baidu.com"],
150
+ rateLimits: { perMinute: 20, minIntervalMs: 2000 },
151
+
152
+ validateCookie,
153
+ listConversations,
154
+ listMessages,
155
+ };
156
+
157
+ module.exports = {
158
+ SPEC,
159
+ _internal: { _toMs, _normalizeRole, _buildContent, _extractList, BASE },
160
+ };