sheetnext 0.1.5 → 0.1.7

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/AGENT.md CHANGED
@@ -1,22 +1,30 @@
1
- # SheetNext AI 配置指南
1
+ # SheetNext AI 中转配置指南
2
2
 
3
- > AI 中转服务搭建和配置说明
3
+ > 一句话总结超级简单:写一个接口将前端传入的message消息分发给你想对接的大模型,然后在前端配置好接口地址即可开始工作!
4
4
 
5
5
  ## 文档导航
6
6
 
7
- - [← 返回 README](./README.md) - 快速开始和特点介绍
8
- - [← API 文档](./DOCS.md) - 详细的类、方法和属性说明
7
+ - [← 返回 README](https://github.com/wyyazlz/sheetnext/blob/master/README.md) - 快速开始和特点介绍
8
+ - [← API 文档](https://github.com/wyyazlz/sheetnext/blob/master/DOCS.md) - 详细的类、方法和属性说明
9
9
 
10
10
  ---
11
11
 
12
12
  ## 目录
13
13
 
14
- - [功能说明](#功能说明)
15
- - [核心架构](#核心架构)
16
- - [完整示例](#完整示例)
17
- - [消息格式](#消息格式)
18
- - [其他大模型对接](#其他大模型对接)
19
- - [部署建议](#部署建议)
14
+ - [SheetNext AI 中转配置指南](#sheetnext-ai-中转配置指南)
15
+ - [文档导航](#文档导航)
16
+ - [目录](#目录)
17
+ - [功能说明](#功能说明)
18
+ - [核心功能](#核心功能)
19
+ - [核心架构](#核心架构)
20
+ - [工作流程](#工作流程)
21
+ - [完整示例](#完整示例)
22
+ - [安装依赖](#安装依赖)
23
+ - [完整代码](#完整代码)
24
+ - [配置说明](#配置说明)
25
+ - [消息格式](#消息格式)
26
+ - [请求格式](#请求格式)
27
+ - [响应格式](#响应格式)
20
28
 
21
29
  ---
22
30
 
@@ -30,14 +38,6 @@ AI 服务中转层是连接 SheetNext 前端与大模型 API 的桥梁,主要
30
38
  2. **流式数据处理** - 实现 AI 响应的流式接收与转发,提升用户交互体验
31
39
  3. **安全隔离** - 在服务端隐藏真实的 API Key,避免密钥泄露风险
32
40
  4. **使用统计** - 企业可在中转层统计 Token 消耗、请求次数等关键数据
33
- 5. **多模态支持** - 处理文本和图片(Base64)输入,要求对接的大模型必须支持视觉能力
34
-
35
- ### 关键要点
36
-
37
- - ✅ 中转接口编写完毕后,将请求地址在前端编辑器初始化时通过 `AI_URL` 填入
38
- - ✅ 编辑器在对话时会调用此 API,将整个对话历史以 `messages` 数组传入后端
39
- - ✅ 后端根据 `messages` 对大模型进行分发、格式转换和流式处理
40
- - ✅ 中转层可统计使用数据,保障 API Key 安全
41
41
 
42
42
  ---
43
43
 
@@ -63,107 +63,112 @@ AI 服务中转层是连接 SheetNext 前端与大模型 API 的桥梁,主要
63
63
  2. **格式转换** - 中转服务器将通用格式转换为目标大模型的专用格式
64
64
  3. **API 调用** - 使用服务端存储的 API Key 调用大模型 API
65
65
  4. **流式响应** - 接收大模型的流式响应,转换后通过 SSE (Server-Sent Events) 返回前端
66
- 5. **数据统计** - 在中转层记录使用情况(可选)
67
66
 
68
67
  ---
69
68
 
70
69
  ## 完整示例
71
70
 
72
- 以下是 **Node.js + Claude API** 的完整实现示例:
71
+ 通用中转完整实现示例:
73
72
 
74
73
  ### 安装依赖
75
74
 
76
75
  ```bash
77
- npm install @anthropic-ai/sdk
76
+ npm install @anthropic-ai/sdk openai
78
77
  ```
79
78
 
80
79
  ### 完整代码
81
80
 
82
81
  ```javascript
83
82
  /**
84
- * SheetNext AI 中转服务器 - Claude 流式接收/发送示例
85
- * 功能: 消息格式转换、流式响应、安全中转
83
+ * SheetNext AI & claude/openai 中转服务器示例 Node.js 版本
84
+ * 2025.10.17 v1.0.0
86
85
  */
87
86
 
88
87
  const http = require('http');
89
88
  const Anthropic = require('@anthropic-ai/sdk');
89
+ const OpenAI = require('openai');
90
90
 
91
91
  // ======= 配置 =======
92
92
  const CONFIG = {
93
- apiKey: 'your-api-key-here', // 您的 API Key
94
- model: 'claude-sonnet-4-5-20250929', // 模型名称
95
- baseURL: 'https://api.anthropic.com' // API 基础地址
93
+ model: 'claude-sonnet-4-5-20250929', // 设置模型名称,自动判断使用 claude 还是 openai
94
+ claude: {
95
+ apiKey: 'sk-xWp4TFA81arQCudIbLRmE0h1TtmM0lQWz4Lt7lKryUhk5HhN',
96
+ baseURL: 'https://m5.aitoo.fun/'
97
+ },
98
+ openai: {
99
+ apiKey: 'sk-UWq0SWmDIEFTI5hswtg5uONjQ95ECUneWtp46Pp9Kdujg9py',
100
+ baseURL: 'https://m5.aitoo.fun/v1'
101
+ }
96
102
  };
97
103
 
98
- const anthropic = new Anthropic({
99
- apiKey: CONFIG.apiKey,
100
- baseURL: CONFIG.baseURL
101
- });
104
+ const anthropic = new Anthropic({ apiKey: CONFIG.claude.apiKey, baseURL: CONFIG.claude.baseURL });
105
+ const openai = new OpenAI({ apiKey: CONFIG.openai.apiKey, baseURL: CONFIG.openai.baseURL });
102
106
 
103
- // ======= 消息转换器 =======
107
+ // ======= message默认是openai格式,claude请求时转为它适配格式 =======
104
108
  const convertToClaudeMessages = (messages) => {
105
- const system = [], userMessages = [];
109
+ const system = [];
110
+ const claudeMessages = [];
111
+ let isFirstSystem = true;
112
+
113
+ // 转换内容部分的辅助函数
114
+ const convertContent = (content) => {
115
+ const parts = Array.isArray(content) ? content : [{ type: 'text', text: content }];
116
+ return parts.map(part => {
117
+ if (part.type === 'text') {
118
+ return { type: 'text', text: part.text };
119
+ }
120
+ if (part.type === 'image_url') {
121
+ const [, mediaType, base64Data] = part.image_url.url.match(/data:(.*?);base64,(.*)/) || [];
122
+ if (base64Data) {
123
+ return { type: 'image', source: { type: 'base64', media_type: mediaType || 'image/jpeg', data: base64Data } };
124
+ }
125
+ }
126
+ return null;
127
+ }).filter(Boolean);
128
+ };
106
129
 
107
130
  for (const msg of messages) {
108
131
  if (msg.role === 'system') {
109
- // 处理系统消息
110
- const parts = Array.isArray(msg.content)
111
- ? msg.content
112
- : [{ type: 'text', text: msg.content }];
113
-
114
- parts.forEach(part => {
115
- if (part.type === 'text') {
116
- system.push({
117
- type: 'text',
118
- text: part.text,
119
- ...(part.cacheable && { cache_control: { type: 'ephemeral' } })
120
- });
121
- } else if (part.type === 'image_url') {
122
- // 转换 Base64 图片
123
- const [, mediaType, base64Data] =
124
- part.image_url.url.match(/data:(.*?);base64,(.*)/) || [];
125
- system.push({
126
- type: 'image',
127
- source: {
128
- type: 'base64',
129
- media_type: mediaType || 'image/jpeg',
130
- data: base64Data
131
- }
132
- });
133
- }
134
- });
132
+ if (isFirstSystem) {
133
+ // 第一个 system:提取文本作为 system 参数(约定无图片)
134
+ const text = typeof msg.content === 'string' ? msg.content : msg.content[0]?.text || '';
135
+ if (text) system.push({ type: 'text', text });
136
+ isFirstSystem = false;
137
+ } else {
138
+ // 其他 system:转为 user
139
+ claudeMessages.push({ role: 'user', content: convertContent(msg.content) });
140
+ }
135
141
  } else {
136
- // 处理用户/助手消息
137
- const content = typeof msg.content === 'string'
138
- ? msg.content
139
- : msg.content.map(part => {
140
- if (part.type === 'image_url') {
141
- const [, mediaType, base64Data] =
142
- part.image_url.url.match(/data:(.*?);base64,(.*)/) || [];
143
- return {
144
- type: 'image',
145
- source: {
146
- type: 'base64',
147
- media_type: mediaType || 'image/jpeg',
148
- data: base64Data
149
- }
150
- };
151
- }
152
- return part;
153
- });
154
- userMessages.push({ role: msg.role, content });
142
+ // user/assistant 消息
143
+ claudeMessages.push({ role: msg.role, content: convertContent(msg.content) });
155
144
  }
156
145
  }
157
146
 
158
- return { system, messages: userMessages };
147
+ return { system, messages: claudeMessages };
159
148
  };
160
149
 
161
- // ======= Claude SDK 调用 =======
162
- async function callClaudeSDK(messages, onChunk) {
150
+ // ======= Claude SDK =======
151
+ async function callClaudeSDK(messages, model, onChunk) {
163
152
  const { system, messages: claudeMessages } = convertToClaudeMessages(messages);
164
153
 
154
+ // 打印请求结构(省略 base64 数据)
155
+ const printableRequest = {
156
+ system: system.map(s => s.type === 'image'
157
+ ? { type: 'image', source: { ...s.source, data: `[${s.source.data?.length || 0} chars]` } }
158
+ : s
159
+ ),
160
+ messages: claudeMessages.map(msg => ({
161
+ role: msg.role,
162
+ content: typeof msg.content === 'string' ? msg.content :
163
+ msg.content.map(c => c.type === 'image'
164
+ ? { type: 'image', source: { ...c.source, data: `[${c.source.data?.length || 0} chars]` } }
165
+ : c
166
+ )
167
+ }))
168
+ };
169
+
165
170
  const stream = await anthropic.messages.create({
166
- model: CONFIG.model,
171
+ model: model,
167
172
  max_tokens: 8192,
168
173
  system,
169
174
  messages: claudeMessages,
@@ -174,21 +179,33 @@ async function callClaudeSDK(messages, onChunk) {
174
179
  for await (const event of stream) {
175
180
  if (event.type === 'content_block_delta') {
176
181
  const { delta } = event;
177
- // 思考过程
178
182
  if (delta?.type === 'thinking_delta' && delta.thinking) {
179
183
  onChunk({ type: 'think', delta: delta.thinking });
180
- }
181
- // 文本响应
182
- else if (delta?.type === 'text_delta') {
184
+ } else if (delta?.type === 'text_delta') {
183
185
  onChunk({ type: 'text', delta: delta.text });
184
186
  }
185
187
  }
186
188
  }
187
189
  }
188
190
 
191
+ // ======= OpenAI SDK =======
192
+ async function callOpenAISDK(messages, model, onChunk) {
193
+ const stream = await openai.chat.completions.create({
194
+ model: model,
195
+ messages: messages, // 直接使用 OpenAI 格式的 messages
196
+ stream: true
197
+ });
198
+
199
+ for await (const chunk of stream) {
200
+ const delta = chunk.choices[0]?.delta;
201
+ if (delta?.content) {
202
+ onChunk({ type: 'text', delta: delta.content });
203
+ }
204
+ }
205
+ }
206
+
189
207
  // ======= HTTP 处理 =======
190
208
  async function handleChat(messages, res) {
191
- // 设置 SSE 响应头
192
209
  res.writeHead(200, {
193
210
  'Content-Type': 'text/event-stream',
194
211
  'Cache-Control': 'no-cache',
@@ -198,14 +215,19 @@ async function handleChat(messages, res) {
198
215
 
199
216
  let ended = false;
200
217
  const write = (data) => !ended && !res.writableEnded && res.write(data);
201
- const onChunk = (chunk) => write(`data: ${JSON.stringify(chunk)}\\n\\n`);
218
+ const onChunk = (chunk) => write(`data: ${JSON.stringify(chunk)}\n\n`);
202
219
 
203
220
  try {
204
- await callClaudeSDK(messages, onChunk);
205
- write(`data: [DONE]\\n\\n`);
221
+ // 根据模型名称自动判断使用哪个 provider
222
+ const provider = CONFIG.model.toLowerCase().includes('claude') ? 'claude' : 'openai';
223
+ if (provider === 'openai') {
224
+ await callOpenAISDK(messages, CONFIG.model, onChunk);
225
+ } else {
226
+ await callClaudeSDK(messages, CONFIG.model, onChunk);
227
+ }
228
+ write(`data: [DONE]\n\n`);
206
229
  } catch (error) {
207
- console.error('AI 调用错误:', error);
208
- write(`data: ${JSON.stringify({ error: error.message })}\\n\\n`);
230
+ write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
209
231
  } finally {
210
232
  ended = true;
211
233
  res.end();
@@ -217,25 +239,21 @@ http.createServer(async (req, res) => {
217
239
  const corsHeaders = {
218
240
  'Access-Control-Allow-Origin': '*',
219
241
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
220
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
242
+ 'Access-Control-Allow-Headers': 'Content-Type'
221
243
  };
222
244
 
223
- // 处理 CORS 预检请求
224
245
  if (req.method === 'OPTIONS') {
225
246
  res.writeHead(200, corsHeaders);
226
247
  return res.end();
227
248
  }
228
249
 
229
- // 处理 AI 请求
230
250
  if (req.url === '/sheetnextAI' && req.method === 'POST') {
231
251
  let body = '';
232
252
  req.on('data', chunk => body += chunk);
233
253
  req.on('end', async () => {
234
254
  try {
235
255
  const { messages } = JSON.parse(body);
236
- if (!Array.isArray(messages)) {
237
- throw new Error('Invalid messages format');
238
- }
256
+ if (!Array.isArray(messages)) throw new Error('Invalid messages');
239
257
  await handleChat(messages, res);
240
258
  } catch (error) {
241
259
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -246,20 +264,14 @@ http.createServer(async (req, res) => {
246
264
  res.writeHead(404);
247
265
  res.end('Not Found');
248
266
  }
249
- }).listen(3000, () => {
250
- console.log('🚀 SheetNext AI 中转服务器运行在 http://localhost:3000');
251
- console.log('📡 AI 端点: http://localhost:3000/sheetnextAI');
252
- });
267
+ }).listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
253
268
  ```
254
269
 
255
- ### 前端配置
270
+ ### 配置说明
256
271
 
257
- ```javascript
258
- const SN = new SheetNext(document.querySelector('#container'), {
259
- AI_URL: "http://localhost:3000/sheetnextAI",
260
- AI_TOKEN: "your-optional-token" // 可选,用于鉴权
261
- });
262
- ```
272
+ **判断规则:**
273
+ - 如果模型名称包含 `claude`(不区分大小写) → 使用 Claude SDK
274
+ - 其他情况 → 使用 OpenAI SDK
263
275
 
264
276
  ---
265
277
 
@@ -289,7 +301,7 @@ SheetNext 发送的请求体格式:
289
301
  "content": [
290
302
  {
291
303
  "type": "text",
292
- "text": "这是表格截图"
304
+ "text": "某区域图片"
293
305
  },
294
306
  {
295
307
  "type": "image_url",
@@ -316,88 +328,3 @@ data: {"type":"text","delta":"帮"}
316
328
 
317
329
  data: [DONE]
318
330
  ```
319
-
320
- **响应对象格式:**
321
-
322
- ```typescript
323
- {
324
- type: "text" | "think" | "error", // 消息类型
325
- delta: string // 增量文本
326
- }
327
- ```
328
-
329
- ---
330
-
331
- ## 其他大模型对接
332
-
333
- ### OpenAI GPT
334
-
335
- ```javascript
336
- const OpenAI = require('openai');
337
-
338
- const openai = new OpenAI({
339
- apiKey: 'your-openai-key'
340
- });
341
-
342
- async function callOpenAI(messages, onChunk) {
343
- const stream = await openai.chat.completions.create({
344
- model: 'gpt-4',
345
- messages: messages, // OpenAI 格式已兼容
346
- stream: true
347
- });
348
-
349
- for await (const chunk of stream) {
350
- const delta = chunk.choices[0]?.delta?.content;
351
- if (delta) {
352
- onChunk({ type: 'text', delta });
353
- }
354
- }
355
- }
356
- ```
357
-
358
- ### 其他模型
359
-
360
- 只需要:
361
- 1. 将 `messages` 转换为目标 API 的格式
362
- 2. 调用 API 获取流式响应
363
- 3. 将响应转换为 `{type, delta}` 格式
364
- 4. 通过 `onChunk` 回调返回
365
-
366
- ---
367
-
368
- ### 安全建议
369
-
370
- 1. **使用环境变量存储 API Key**
371
- ```javascript
372
- const CONFIG = {
373
- apiKey: process.env.ANTHROPIC_API_KEY,
374
- // ...
375
- };
376
- ```
377
-
378
- 2. **添加请求鉴权**
379
- ```javascript
380
- const authToken = req.headers.authorization;
381
- if (authToken !== process.env.AUTH_TOKEN) {
382
- res.writeHead(401);
383
- return res.end('Unauthorized');
384
- }
385
- ```
386
-
387
- 3. **限制请求频率**
388
- ```javascript
389
- const rateLimit = require('express-rate-limit');
390
- // 使用限流中间件
391
- ```
392
-
393
- 4. **记录使用日志**
394
- ```javascript
395
- console.log(`[${new Date().toISOString()}] Token使用: ${usage.total_tokens}`);
396
- ```
397
-
398
- ---
399
-
400
- ## 相关文档
401
-
402
- - [← 返回 README](./README.md) - 快速开始和特点介绍
403
- - [← API 文档](./DOCS.md) - 详细的类、方法和属性说明
package/DOCS.md CHANGED
@@ -2,11 +2,6 @@
2
2
 
3
3
  > 详细的类、方法和属性说明
4
4
 
5
- ## 文档导航
6
-
7
- - [← 返回 README](./README.md) - 快速开始和特点介绍
8
- - [→ AI 配置指南](./AGENT.md) - AI 中转服务搭建
9
-
10
5
  ---
11
6
 
12
7
  ## 目录
@@ -35,7 +30,7 @@ const SN = new SheetNext(dom: HTMLElement, options?: object)
35
30
 
36
31
  **参数:**
37
32
  - `dom`: 容器 DOM 元素(必需)
38
- - `options`: 可选配置对象(详见 [README 初始化配置](./README.md#初始化配置))
33
+ - `options`: 可选配置对象(详见 [README 初始化配置](https://github.com/wyyazlz/sheetnext/blob/master/README.md#初始化配置))
39
34
 
40
35
  ### 核心属性
41
36
 
@@ -112,13 +107,6 @@ for (let i = 0; i < 100; i++) {
112
107
  SN.r();
113
108
  ```
114
109
 
115
- #### `activateLicense(licenseKey: string)`
116
- 激活 License 授权。
117
-
118
- ```javascript
119
- SN.activateLicense("your-license-key");
120
- ```
121
-
122
110
  #### `getLicenseInfo()`
123
111
  获取 License 信息。
124
112
 
@@ -699,10 +687,3 @@ SN.UndoRedo.redo();
699
687
  - 撤销/重做会自动记录大部分用户操作
700
688
  - 通过 API 修改的内容也会被记录
701
689
  - 历史记录栈有大小限制,过旧的操作会被清除
702
-
703
- ---
704
-
705
- ## 相关文档
706
-
707
- - [← 返回 README](./README.md) - 快速开始和特点介绍
708
- - [→ AI 配置指南](./AGENT.md) - AI 中转服务搭建