@wwkit/llmproxy 1.0.3 → 1.0.5

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.
@@ -0,0 +1,279 @@
1
+ // providers/deepseek/pow.js — DeepSeek Web PoW (DeepSeekHashV1) 计算
2
+ //
3
+ // 算法(逆向自 DeepSeek web 前端 worker,见 docs/6608.8f2a9fa413.js):
4
+ // 1. POST /api/v0/chat/create_pow_challenge { target_path } → 获取 challenge
5
+ // 2. prefix = salt + "_" + expire_at + "_"
6
+ // 3. 遍历 nonce = 0..difficulty,DeepSeekHashV1(prefix + nonce) == challenge 时命中
7
+ // 4. 构建 X-DS-PoW-Response header = base64(JSON({algorithm, challenge, salt, answer, signature, target_path}))
8
+ //
9
+ // 注意:DeepSeekHashV1 是基于 Keccak-f[1600] 的自定义海绵函数,字节序与 NIST SHA3-256
10
+ // 不同(既非 sha3-256 也非 Ethereum keccak256),必须用下方的独立实现,不能用 crypto。
11
+ //
12
+ // 参考: docs/pow-analysis.md
13
+
14
+ // 端点(固定值,无需用户配置)
15
+ // 2026-09: DeepSeek 将端点从 /api/v0/users/create_guest_challenge 改为 /api/v0/chat/create_pow_challenge
16
+ export const POW_ENDPOINT = "/api/v0/chat/create_pow_challenge";
17
+
18
+ // ---- DeepSeekHashV1(Keccak-f[1600] 自定义变体)----
19
+ // 以下为 worker 88387 模块的逐行移植。
20
+
21
+ const RHO = [10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1];
22
+ const PI = [1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44];
23
+ const RC = new Uint32Array([
24
+ 0, 1, 0, 32898, 0x80000000, 32906, 0x80000000, 0x80008000, 0, 32907, 0, 0x80000001,
25
+ 0x80000000, 0x80008081, 0x80000000, 32777, 0, 138, 0, 136, 0, 0x80008009, 0, 0x8000000a,
26
+ 0, 0x8000808b, 0x80000000, 139, 0x80000000, 32905, 0x80000000, 32771, 0x80000000, 32770,
27
+ 0x80000000, 128, 0, 32778, 0x80000000, 0x8000000a, 0x80000000, 0x80008081, 0x80000000, 32896,
28
+ 0, 0x80000001, 0x80000000, 0x80008008,
29
+ ]);
30
+
31
+ // 复制一个 64-bit lane(两字):dst[2n]=src[2e], dst[2n+1]=src[2e+1]
32
+ function laneCopy(src, e) {
33
+ return (dst, n) => {
34
+ dst[2 * n] = src[2 * e];
35
+ dst[2 * n + 1] = src[2 * e + 1];
36
+ };
37
+ }
38
+
39
+ // θ (theta)
40
+ function theta(A, C, D, W) {
41
+ for (let t = 0; t < 5; t++) {
42
+ const n = 2 * t, i = (t + 5) * 2, o = (t + 10) * 2, f = (t + 15) * 2, s = (t + 20) * 2;
43
+ C[n] = A[n] ^ A[i] ^ A[o] ^ A[f] ^ A[s];
44
+ C[n + 1] = A[n + 1] ^ A[i + 1] ^ A[o + 1] ^ A[f + 1] ^ A[s + 1];
45
+ }
46
+ for (let t = 0; t < 5; t++) {
47
+ laneCopy(C, (t + 1) % 5)(W, 0);
48
+ const o = W[0], f = W[1];
49
+ W[0] = (o << 1) | (f >>> 31);
50
+ W[1] = (f << 1) | (o >>> 31);
51
+ D[2 * t] = C[(t + 4) % 5 * 2] ^ W[0];
52
+ D[2 * t + 1] = C[(t + 4) % 5 * 2 + 1] ^ W[1];
53
+ for (let r = 0; r < 25; r += 5) {
54
+ A[(r + t) * 2] ^= D[2 * t];
55
+ A[(r + t) * 2 + 1] ^= D[2 * t + 1];
56
+ }
57
+ }
58
+ }
59
+
60
+ // ρ (rho) + π (pi)
61
+ function rhoPi(A, C, W) {
62
+ const n = new Uint32Array(2); // n[0]=高字, n[1]=低字
63
+ laneCopy(A, 1)(n, 0);
64
+ for (let i = 0; i < 24; i++) {
65
+ const t = RHO[i], a = PI[i];
66
+ laneCopy(A, t)(C, 0);
67
+ const o = n[0], f = n[1];
68
+ const s = a < 32 ? 0 : 1;
69
+ const u = 32 - a;
70
+ n[s] = (o << a) | (f >>> u);
71
+ n[(s + 1) % 2] = (f << a) | (o >>> u);
72
+ laneCopy(n, 0)(A, t);
73
+ laneCopy(C, 0)(n, 0);
74
+ }
75
+ }
76
+
77
+ // χ (chi)
78
+ function chi(A, C) {
79
+ for (let t = 0; t < 25; t += 5) {
80
+ for (let n = 0; n < 5; n++) laneCopy(A, t + n)(C, n);
81
+ for (let n = 0; n < 5; n++) {
82
+ const i = (t + n) * 2, o = (n + 1) % 5 * 2, f = (n + 2) % 5 * 2;
83
+ A[i] ^= ~C[o] & C[f];
84
+ A[i + 1] ^= ~C[o + 1] & C[f + 1];
85
+ }
86
+ }
87
+ }
88
+
89
+ // ι (iota)
90
+ function iota(A, rnd) {
91
+ const n = 2 * rnd;
92
+ A[0] ^= RC[n];
93
+ A[1] ^= RC[n + 1];
94
+ }
95
+
96
+ // Keccak-f[1600]:24 轮
97
+ function keccakF(A) {
98
+ const C = new Uint32Array(10), D = new Uint32Array(10), W = new Uint32Array(2);
99
+ for (let i = 1; i < 24; i++) {
100
+ theta(A, C, D, W);
101
+ rhoPi(A, C, W);
102
+ chi(A, C);
103
+ iota(A, i);
104
+ }
105
+ }
106
+
107
+ // 吸收 queue 字节到 state(DeepSeek 的字节序)
108
+ function absorbInto(queue, state) {
109
+ for (let r = 0; r < queue.length; r += 8) {
110
+ const n = r / 4;
111
+ state[n] ^= (queue[r + 7] << 24) | (queue[r + 6] << 16) | (queue[r + 5] << 8) | queue[r + 4];
112
+ state[n + 1] ^= (queue[r + 3] << 24) | (queue[r + 2] << 16) | (queue[r + 1] << 8) | queue[r];
113
+ }
114
+ }
115
+
116
+ // 从 state 挤出字节到 buffer(DeepSeek 的字节序)
117
+ function squeezeOut(state, buffer) {
118
+ for (let r = 0; r < buffer.length; r += 8) {
119
+ const n = r / 4;
120
+ buffer[r] = state[n + 1];
121
+ buffer[r + 1] = state[n + 1] >>> 8;
122
+ buffer[r + 2] = state[n + 1] >>> 16;
123
+ buffer[r + 3] = state[n + 1] >>> 24;
124
+ buffer[r + 4] = state[n];
125
+ buffer[r + 5] = state[n] >>> 8;
126
+ buffer[r + 6] = state[n] >>> 16;
127
+ buffer[r + 7] = state[n] >>> 24;
128
+ }
129
+ }
130
+
131
+ const RATE = 136;
132
+ const OUT_BYTES = 32;
133
+
134
+ // DeepSeekHashV1:对输入字节计算 32 字节哈希,返回 hex
135
+ export function dsHash(inputBytes) {
136
+ const state = new Uint32Array(50);
137
+ const queue = new Uint8Array(RATE);
138
+ let off = 0;
139
+ for (let e = 0; e < inputBytes.length; e++) {
140
+ queue[off] = inputBytes[e];
141
+ off++;
142
+ if (off >= RATE) {
143
+ absorbInto(queue, state);
144
+ keccakF(state);
145
+ off = 0;
146
+ }
147
+ }
148
+ // SHA3 padding:0x06 在消息末尾,0x80 在 rate 块最后字节
149
+ queue.fill(0, off);
150
+ queue[off] |= 0x06;
151
+ queue[RATE - 1] |= 0x80;
152
+ absorbInto(queue, state);
153
+ const buffer = new Uint8Array(OUT_BYTES);
154
+ for (let t = 0; t < OUT_BYTES; t += RATE) {
155
+ keccakF(state);
156
+ squeezeOut(state, buffer.subarray(t, t + RATE));
157
+ }
158
+ return Buffer.from(buffer).toString("hex");
159
+ }
160
+
161
+ /**
162
+ * 从服务器获取 PoW challenge
163
+ * @param {object} opts
164
+ * @param {object} opts.cred auth 凭证 { headers: { Authorization, Cookie } }
165
+ * @param {string} opts.targetPath 目标接口路径,如 "/api/v0/chat/completion"
166
+ * @returns {Promise<object>} challenge 对象(含 algorithm/challenge/salt/difficulty/signature/expire_at)
167
+ */
168
+ export async function fetchChallenge(cred, targetPath) {
169
+ const res = await fetch(`https://chat.deepseek.com${POW_ENDPOINT}`, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/json",
173
+ ...(cred.headers || {}),
174
+ },
175
+ body: JSON.stringify({ target_path: targetPath }),
176
+ });
177
+ if (!res.ok) {
178
+ const text = await res.text();
179
+ throw new Error(`获取 PoW challenge 失败 ${res.status}: ${text.slice(0, 200)}`);
180
+ }
181
+ const data = await res.json();
182
+ const biz = data?.data?.biz_data || {};
183
+ // 2026-09: 字段名从 guest_challenge 改为 challenge,且 target_path 现在由服务端返回
184
+ const ch = biz.challenge || biz.guest_challenge;
185
+ if (!ch) {
186
+ throw new Error(`challenge not found: ${JSON.stringify(data).slice(0, 500)}`);
187
+ }
188
+ return {
189
+ algorithm: ch.algorithm,
190
+ challenge: ch.challenge,
191
+ salt: ch.salt,
192
+ difficulty: ch.difficulty,
193
+ signature: ch.signature,
194
+ // expire_at 可能是秒或毫秒,统一转为毫秒
195
+ expireAt: ch.expire_at > 1e12 ? ch.expire_at : ch.expire_at * 1000,
196
+ // target_path 优先用请求传入的,否则用服务端返回的
197
+ targetPath: targetPath || ch.target_path,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 求解 PoW:找到 nonce 使 DeepSeekHashV1(salt_"_"expireAt_"_"nonce) == challenge
203
+ * @param {object} challenge fetchChallenge 返回的 challenge
204
+ * @returns {Promise<{answer: number, durationMs: number}>}
205
+ */
206
+ export async function solvePow(challenge) {
207
+ const prefix = `${challenge.salt}_${challenge.expireAt}_`;
208
+ const target = challenge.challenge.toLowerCase();
209
+ const t0 = Date.now();
210
+
211
+ const encoder = new TextEncoder();
212
+ for (let nonce = 0; nonce < challenge.difficulty; nonce++) {
213
+ if (dsHash(encoder.encode(prefix + String(nonce))) === target) {
214
+ return { answer: nonce, durationMs: Date.now() - t0 };
215
+ }
216
+ }
217
+ throw new Error(
218
+ `PoW 无解: algorithm=${challenge.algorithm} difficulty=${challenge.difficulty} prefix=${prefix}`
219
+ );
220
+ }
221
+
222
+ /**
223
+ * 完整求解:fetch challenge + solve PoW,返回可注入 header 的值
224
+ * @param {object} opts
225
+ * @param {object} opts.cred auth 凭证 { headers }
226
+ * @param {string} opts.targetPath 目标路径
227
+ * @param {object} [opts.state] 可选的缓存状态 { challenge, answer, expireAt }
228
+ * @returns {Promise<{ headerValue: string, challenge: object, answer: number }>}
229
+ */
230
+ export async function getPowHeader(cred, targetPath, state) {
231
+ // 缓存:若 challenge 未过期且 targetPath 相同,复用已计算好的 answer
232
+ if (state?.challenge && state?.answer !== undefined) {
233
+ const ch = state.challenge;
234
+ if (ch.targetPath === targetPath && isNotExpired(ch.expireAt)) {
235
+ return {
236
+ headerValue: buildPowHeader(ch, state.answer, targetPath),
237
+ challenge: ch,
238
+ answer: state.answer,
239
+ };
240
+ }
241
+ }
242
+
243
+ const challenge = await fetchChallenge(cred, targetPath);
244
+ const { answer } = await solvePow(challenge);
245
+
246
+ // 更新缓存
247
+ if (state) {
248
+ state.challenge = challenge;
249
+ state.answer = answer;
250
+ state.targetPath = targetPath;
251
+ }
252
+
253
+ return { headerValue: buildPowHeader(challenge, answer, targetPath), challenge, answer };
254
+ }
255
+
256
+ // expireAt 兼容 epoch 秒 / 毫秒,且余量 60s
257
+ function isNotExpired(expireAt) {
258
+ const ts = expireAt > 1e12 ? expireAt : expireAt * 1000;
259
+ return ts - 60_000 > Date.now();
260
+ }
261
+
262
+ /**
263
+ * 构建 X-DS-PoW-Response header 值(base64(JSON))
264
+ * @param {object} challenge challenge 对象
265
+ * @param {number} answer PoW 答案
266
+ * @param {string} targetPath 目标路径
267
+ * @returns {string}
268
+ */
269
+ export function buildPowHeader(challenge, answer, targetPath) {
270
+ const payload = {
271
+ algorithm: challenge.algorithm,
272
+ challenge: challenge.challenge,
273
+ salt: challenge.salt,
274
+ answer,
275
+ signature: challenge.signature,
276
+ target_path: targetPath,
277
+ };
278
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
279
+ }
@@ -0,0 +1,10 @@
1
+ // providers/deepseek/sign.js — DeepSeek Web 签名
2
+ //
3
+ // DeepSeek web 的"签名"是 PoW(DeepSeekHashV1),由 client 内部完成
4
+ // (需要异步 fetch challenge + auth 凭证,不符合本接口的同步 sign)。
5
+ // 这里直接使用 NoopSigner。
6
+
7
+ import { NoopSigner } from "../../core/sign.js";
8
+
9
+ export { SIGN_CONFIG } from "./config.js";
10
+ export default NoopSigner;
@@ -0,0 +1,202 @@
1
+ // providers/deepseek/sse.js — DeepSeek 自定义 SSE → OpenAI SSE 转换
2
+ //
3
+ // DeepSeek web 的 /api/v0/chat/completion 返回自定义 SSE:
4
+ // - 命名事件: ready / update_session / title / close
5
+ // - 完整 response 对象: { v: { response: { fragments: [{type:"THINK",content:"H"},...] } } }
6
+ // - fragment 追加: { p:"response/fragments", o:"APPEND", v:[{type:"RESPONSE",content:"Hello"}] }
7
+ // - content 增量: { p:"response/fragments/-1/content", (o:"APPEND"), v:"text" }
8
+ // - 裸文本增量: { v:"text" }
9
+ // - 状态: { p:"response/status", o:"SET", v:"FINISHED" }
10
+ // - 收尾: event: close
11
+ //
12
+ // 转换为 OpenAI SSE:
13
+ // - THINK 内容 → choices[0].delta.reasoning_content
14
+ // - RESPONSE 内容 → choices[0].delta.content
15
+ // - FINISHED → finish_reason:"stop"
16
+ // - close → data: [DONE]
17
+ //
18
+ // 流式转换:输入 upstream.body (ReadableStream),输出 OpenAI SSE (ReadableStream)。
19
+
20
+ import crypto from "node:crypto";
21
+
22
+ class SseTransformer {
23
+ constructor(model) {
24
+ this.model = model;
25
+ this.sentStart = false;
26
+ this.sentStop = false;
27
+ this.thinking = false; // 当前写入的是 THINK 还是 RESPONSE fragment
28
+ this.fragmentId = null; // 当前 fragment id(避免重复发送完整内容)
29
+ this.sentDone = false; // 是否已发送 [DONE]
30
+ this.buffer = ""; // 跨 chunk 的 SSE 行缓冲
31
+ }
32
+
33
+ /** 输入 Uint8Array chunk,返回 Uint8Array[] */
34
+ transform(byteChunk) {
35
+ this.buffer += new TextDecoder().decode(byteChunk, { stream: true });
36
+ const outputs = [];
37
+ let idx;
38
+ while ((idx = this.buffer.indexOf("\n\n")) >= 0) {
39
+ const raw = this.buffer.slice(0, idx);
40
+ this.buffer = this.buffer.slice(idx + 2);
41
+ const out = this.handleEvent(raw);
42
+ if (out) outputs.push(out);
43
+ }
44
+ return outputs;
45
+ }
46
+
47
+ handleEvent(raw) {
48
+ let event = "message";
49
+ const dataLines = [];
50
+ for (const line of raw.split("\n")) {
51
+ if (line.startsWith("event:")) event = line.slice(6).trim();
52
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
53
+ }
54
+ if (dataLines.length === 0) return null;
55
+ if (event === "close") {
56
+ if (this.sentDone) return null;
57
+ this.sentDone = true;
58
+ return this.emitData("[DONE]");
59
+ }
60
+
61
+ for (const line of dataLines) {
62
+ let obj;
63
+ try { obj = JSON.parse(line) } catch { continue }
64
+ const out = this.handleData(obj);
65
+ if (out) return out;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ handleData(obj) {
71
+ if (!obj || typeof obj !== "object") return null;
72
+
73
+ // 1) 完整 response 状态(含当前 fragment 初始内容)
74
+ if (obj.v?.response) return this.handleFullResponse(obj.v.response);
75
+
76
+ // 2) fragment 追加(新 fragment 及其初始内容)
77
+ if (obj.p === "response/fragments" && obj.o === "APPEND" && Array.isArray(obj.v) && obj.v.length) {
78
+ const last = obj.v[obj.v.length - 1];
79
+ if (last && typeof last.type === "string") {
80
+ this.thinking = last.type === "THINK";
81
+ // 新 fragment:发送初始内容(排除重复同步)
82
+ if (last.id !== this.fragmentId) {
83
+ this.fragmentId = last.id ?? null;
84
+ if (typeof last.content === "string" && last.content.length) {
85
+ return this.emitDelta(last.content);
86
+ }
87
+ return null;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // 3) content 增量补丁:p 以 /content 结尾、v 为字符串 → 追加到当前 fragment
94
+ if (typeof obj.p === "string" && obj.p.endsWith("/content") && typeof obj.v === "string") {
95
+ return this.emitDelta(obj.v);
96
+ }
97
+
98
+ // 3b) 状态(须在裸文本 case 之前判断,避免把状态值当内容发出)
99
+ if (obj.p === "response/status" && obj.o === "SET" && typeof obj.v === "string") {
100
+ return this.handleStatus(obj.v);
101
+ }
102
+
103
+ // 4) 裸文本增量 { v:"text" } → 追加到当前 fragment
104
+ if (typeof obj.v === "string") {
105
+ return this.emitDelta(obj.v);
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ // 完整 response 对象:设置模式,只在 fragment 变化时发送其内容
112
+ handleFullResponse(resp) {
113
+ if (Array.isArray(resp.fragments) && resp.fragments.length) {
114
+ const last = resp.fragments[resp.fragments.length - 1];
115
+ if (last && typeof last.type === "string" && last.id !== this.fragmentId) {
116
+ this.thinking = last.type === "THINK";
117
+ this.fragmentId = last.id ?? null;
118
+ if (typeof last.content === "string" && last.content.length) {
119
+ return this.emitDelta(last.content);
120
+ }
121
+ return null;
122
+ }
123
+ }
124
+ if (typeof resp.status === "string") {
125
+ return this.handleStatus(resp.status);
126
+ }
127
+ return null;
128
+ }
129
+
130
+ handleStatus(status) {
131
+ if (status === "FINISHED" && !this.sentStop) {
132
+ this.sentStop = true;
133
+ return this.emitChunk({}, "stop");
134
+ }
135
+ if (status !== "FINISHED" && !this.sentStart) {
136
+ this.sentStart = true;
137
+ return this.emitChunk({ role: "assistant" });
138
+ }
139
+ return null;
140
+ }
141
+
142
+ emitDelta(text) {
143
+ const key = this.thinking ? "reasoning_content" : "content";
144
+ if (!this.sentStart) this.sentStart = true;
145
+ return this.emitChunk({ role: "assistant", [key]: text });
146
+ }
147
+
148
+ emitChunk(delta, finishReason) {
149
+ const chunk = {
150
+ id: `chatcmpl-${crypto.randomUUID().slice(0, 8)}`,
151
+ object: "chat.completion.chunk",
152
+ created: Math.floor(Date.now() / 1000),
153
+ model: this.model,
154
+ choices: [{ index: 0, delta, finish_reason: finishReason ?? null }],
155
+ };
156
+ return this.emitData(JSON.stringify(chunk));
157
+ }
158
+
159
+ emitData(data) {
160
+ return new TextEncoder().encode(`data: ${data}\n\n`);
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 把 DeepSeek upstream.body 包装成 OpenAI SSE ReadableStream
166
+ * @param {ReadableStream} upstreamBody upstream.body
167
+ * @param {string} model 模型名
168
+ * @returns {ReadableStream} OpenAI SSE 流
169
+ */
170
+ export function createSseTransformStream(upstreamBody, model) {
171
+ const tr = new SseTransformer(model);
172
+ const reader = upstreamBody.getReader();
173
+ return new ReadableStream({
174
+ async pull(controller) {
175
+ try {
176
+ // 循环读取上游,直到产出至少一个 chunk 或流结束
177
+ // (某些上游 chunk 如 event:ready 不产出,需继续读避免 stall)
178
+ while (true) {
179
+ const { done, value } = await reader.read();
180
+ if (done) {
181
+ if (!tr.sentDone) controller.enqueue(tr.emitData("[DONE]"));
182
+ controller.close();
183
+ return;
184
+ }
185
+ const outputs = tr.transform(value);
186
+ if (outputs.length > 0) {
187
+ for (const c of outputs) controller.enqueue(c);
188
+ return;
189
+ }
190
+ }
191
+ } catch (e) {
192
+ controller.error(e);
193
+ }
194
+ },
195
+ cancel() {
196
+ reader.releaseLock();
197
+ upstreamBody.cancel().catch(() => {});
198
+ },
199
+ });
200
+ }
201
+
202
+ export { SseTransformer }
@@ -0,0 +1,53 @@
1
+ // providers/deepseek-web/tools.js — DeepSeek Web 消息格式化
2
+ //
3
+ // 将 OpenAI 消息数组转为 DeepSeek web prompt 文本(拼接原生调优标签)。
4
+ // Tool 注入/解析由 openai-tool-bridge 通用层处理,Provider 只负责消息格式化。
5
+ //
6
+ // DeepSeek web 模型在 <|System|> / <|end▁of▁sentence|><|User|> /
7
+ // <|Assistant|> 这些"调优标签"下训练,自定义的 [System]/[User]/[Tool result]
8
+ // 格式不被识别,多轮工具历史会失效。本模块用原生标签组装 prompt。
9
+
10
+ import crypto from "node:crypto";
11
+
12
+ // ---- DeepSeek 原生调优标签 ----
13
+ export const DS_TAGS = {
14
+ system: "<|System|>",
15
+ user: "<|end▁of▁sentence|><|User|>",
16
+ assistant: "<|Assistant|>",
17
+ toolBegin: "<|tool▁output▁begin|>",
18
+ toolEnd: "<|tool▁output▁end|>",
19
+ };
20
+
21
+ /**
22
+ * 构建 prompt:把全部消息(含 system + 多轮历史)拼接为单个 prompt 文本。
23
+ * openai-tool-bridge 会把 tools 注入到 system 消息的 content 里,
24
+ * Provider 只需用原生角色标签包裹每条消息即可。
25
+ */
26
+ export function buildSimplePrompt(messages) {
27
+ if (!Array.isArray(messages) || messages.length === 0) return "";
28
+ return messages.map((msg) => formatMessage(msg)).join("\n");
29
+ }
30
+
31
+ /**
32
+ * 格式化单条消息为 DeepSeek prompt 片段
33
+ */
34
+ function formatMessage(msg) {
35
+ const role = msg.role || "user";
36
+ const content = extractContent(msg.content);
37
+ const { system, user, assistant } = DS_TAGS;
38
+
39
+ if (role === "system") return `${system}${content}`;
40
+ if (role === "user") return `${user}${content}`;
41
+ if (role === "assistant") return `${assistant}${content}`;
42
+ return `${assistant}${content}`;
43
+ }
44
+
45
+ function extractContent(content) {
46
+ if (typeof content === "string") return content;
47
+ if (Array.isArray(content)) {
48
+ return content
49
+ .map((p) => (typeof p === "string" ? p : p?.text || ""))
50
+ .join("\n");
51
+ }
52
+ return "";
53
+ }