@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,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,174 @@
1
+ /**
2
+ * Dreamina / 即梦 (ByteDance AI image / video) vendor adapter — Phase 10.2.
3
+ *
4
+ * Reference: docs/design/Adapter_AIChat_History.md §6.8
5
+ * - login https://jimeng.jianying.com/ (国内即梦)
6
+ * - convs POST /api/workspace/list ("workspace" = creative project)
7
+ * - msgs POST /api/workspace/<id>/items (every item = prompt + generated images/videos)
8
+ * - 特别 — schema downstream:
9
+ * * message.subtype = "ai-image-generation" (set by schema-map.js
10
+ * when content.generatedImages is non-empty).
11
+ * * content.generatedImages = [{url, prompt, model, params}].
12
+ *
13
+ * Item shape mapping: a Dreamina "item" is one prompt + N output images.
14
+ * We emit:
15
+ * - 1 user-role RawMessage carrying the prompt text
16
+ * - 1 assistant-role RawMessage carrying generatedImages[].
17
+ *
18
+ * Image CDN URLs are SIGNED and expire — schema-map records URL as-is, the
19
+ * UI must download thumbnails on first display + cache locally.
20
+ */
21
+
22
+ "use strict";
23
+
24
+ const BASE = "https://jimeng.jianying.com";
25
+ const WORKSPACE_LIST_PATH = "/api/workspace/list";
26
+ const WORKSPACE_ITEMS_PATH = (id) => `/api/workspace/${encodeURIComponent(id)}/items`;
27
+ const USER_INFO_PATH = "/api/user/info";
28
+
29
+ const DEFAULT_PAGE_SIZE = 30;
30
+
31
+ function _ensureClient(ctx) {
32
+ if (!ctx || !ctx.httpClient) throw new Error("dreamina: ctx.httpClient required");
33
+ return ctx.httpClient;
34
+ }
35
+
36
+ async function validateCookie(ctx) {
37
+ const client = _ensureClient(ctx);
38
+ try {
39
+ const data = await client.getJson(BASE + USER_INFO_PATH, { session: ctx.session });
40
+ if (data && data.code === 0 && data.data) {
41
+ return { ok: true, userId: data.data.user_id || data.data.uid };
42
+ }
43
+ return { ok: false, reason: "UNEXPECTED_RESPONSE_SHAPE" };
44
+ } catch (err) {
45
+ return { ok: false, reason: err.code || err.message };
46
+ }
47
+ }
48
+
49
+ async function *listConversations(ctx, opts = {}) {
50
+ const client = _ensureClient(ctx);
51
+ const pageSize = Number.isFinite(opts.pageSize) ? opts.pageSize : DEFAULT_PAGE_SIZE;
52
+ const sinceTs = opts.since && opts.since.lastUpdatedAt ? Number(opts.since.lastUpdatedAt) : 0;
53
+
54
+ let page = 1;
55
+ while (true) {
56
+ const body = { page, page_size: pageSize };
57
+ const data = await client.postJson(BASE + WORKSPACE_LIST_PATH, body, { session: ctx.session });
58
+ const list = _extractList(data);
59
+ if (list.length === 0) return;
60
+
61
+ let stopped = false;
62
+ for (const w of list) {
63
+ const updatedAt = _toMs(w.update_time || w.updated_at || w.create_time);
64
+ if (sinceTs && updatedAt <= sinceTs) {
65
+ stopped = true;
66
+ break;
67
+ }
68
+ yield {
69
+ vendor: "dreamina",
70
+ originalId: String(w.workspace_id || w.id),
71
+ title: w.name || w.title || "(无标题作品集)",
72
+ modelName: w.default_model || undefined,
73
+ createdAt: _toMs(w.create_time),
74
+ updatedAt,
75
+ messageCount: w.item_count || undefined,
76
+ archived: Boolean(w.archived),
77
+ extra: { kind: "creative-workspace" },
78
+ };
79
+ }
80
+ if (stopped) return;
81
+ if (list.length < pageSize) return;
82
+ page++;
83
+ if (page > 200) return;
84
+ }
85
+ }
86
+
87
+ async function *listMessages(ctx, workspaceId, _opts = {}) {
88
+ const client = _ensureClient(ctx);
89
+ const url = BASE + WORKSPACE_ITEMS_PATH(workspaceId);
90
+ const data = await client.postJson(url, { workspace_id: String(workspaceId), page_size: 200 }, { session: ctx.session });
91
+ const items = _extractList(data);
92
+
93
+ items.sort((a, b) => _toMs(a.create_time) - _toMs(b.create_time));
94
+
95
+ for (const item of items) {
96
+ // Yield user prompt message
97
+ const promptText = item.prompt || item.prompt_text || "";
98
+ if (promptText) {
99
+ yield {
100
+ vendor: "dreamina",
101
+ originalId: String(item.id) + ":prompt",
102
+ conversationId: String(workspaceId),
103
+ role: "user",
104
+ content: { text: promptText },
105
+ createdAt: _toMs(item.create_time),
106
+ modelName: item.model,
107
+ extra: { itemId: item.id },
108
+ };
109
+ }
110
+ // Yield assistant message with generated images
111
+ const generatedImages = (Array.isArray(item.outputs) ? item.outputs : [])
112
+ .map((o) => ({
113
+ url: o.url || o.image_url,
114
+ prompt: promptText,
115
+ model: item.model,
116
+ params: o.params || item.params,
117
+ }))
118
+ .filter((g) => g.url);
119
+ if (generatedImages.length > 0) {
120
+ yield {
121
+ vendor: "dreamina",
122
+ originalId: String(item.id) + ":output",
123
+ conversationId: String(workspaceId),
124
+ role: "assistant",
125
+ content: {
126
+ text: undefined,
127
+ generatedImages,
128
+ },
129
+ createdAt: _toMs(item.complete_time || item.create_time),
130
+ parentMessageId: String(item.id) + ":prompt",
131
+ modelName: item.model,
132
+ extra: { itemId: item.id, status: item.status },
133
+ };
134
+ }
135
+ }
136
+ }
137
+
138
+ function _extractList(data) {
139
+ if (!data) return [];
140
+ if (data.data && Array.isArray(data.data.workspaces)) return data.data.workspaces;
141
+ if (data.data && Array.isArray(data.data.list)) return data.data.list;
142
+ if (data.data && Array.isArray(data.data.items)) return data.data.items;
143
+ if (Array.isArray(data.data)) return data.data;
144
+ return [];
145
+ }
146
+
147
+ function _toMs(t) {
148
+ if (typeof t === "number") return t > 1e12 ? t : t * 1000;
149
+ if (typeof t === "string") {
150
+ const n = Number(t);
151
+ if (Number.isFinite(n)) return n > 1e12 ? n : n * 1000;
152
+ const d = Date.parse(t);
153
+ return Number.isFinite(d) ? d : 0;
154
+ }
155
+ return 0;
156
+ }
157
+
158
+ const SPEC = {
159
+ name: "dreamina",
160
+ displayName: "Dreamina",
161
+ androidPackage: "com.bytedance.dreamina",
162
+ loginUrl: "https://jimeng.jianying.com/",
163
+ cookieDomains: ["jimeng.jianying.com", ".jianying.com"],
164
+ rateLimits: { perMinute: 15, minIntervalMs: 2500 },
165
+
166
+ validateCookie,
167
+ listConversations,
168
+ listMessages,
169
+ };
170
+
171
+ module.exports = {
172
+ SPEC,
173
+ _internal: { _toMs, _extractList, BASE },
174
+ };