@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.
- package/__tests__/adapters/ai-chat-history.test.js +395 -0
- package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
- package/__tests__/adapters/ai-chat-vendors.test.js +733 -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/system-data-adapter.test.js +440 -0
- package/__tests__/adapters/system-data-disclosure.test.js +153 -0
- package/__tests__/analysis-skills.test.js +409 -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__/longtail-adapters.test.js +217 -0
- package/__tests__/mobile-extractor.test.js +288 -0
- 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 +335 -0
- package/lib/adapters/ai-chat-history/cookie-auth.js +109 -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 +221 -0
- package/lib/adapters/ai-chat-history/vendor-spec.js +85 -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/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/alipay-bill/alipay-bill-adapter.js +307 -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-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/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/content-parser.js +326 -0
- package/lib/adapters/wechat/db-reader.js +209 -0
- package/lib/adapters/wechat/index.js +28 -0
- package/lib/adapters/wechat/key-extractor.js +158 -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 +216 -0
- package/lib/analysis-skills/timeline.js +167 -0
- 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 +115 -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/registry.js +42 -0
- package/lib/sidecar/index.js +15 -0
- package/lib/sidecar/supervisor.js +359 -0
- package/lib/vault.js +266 -0
- package/package.json +29 -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,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
|
+
};
|