sheetnext 0.1.8 → 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/LICENSE +1 -1
- package/README.md +161 -76
- package/README_CN.md +161 -0
- package/dist/sheetnext.css +1 -1
- package/dist/sheetnext.es.js +4 -4
- package/dist/sheetnext.locale.zh-CN.es.js +20 -0
- package/dist/sheetnext.locale.zh-CN.umd.js +20 -0
- package/dist/sheetnext.umd.js +4 -4
- package/docs/docs-detail.md +2998 -0
- package/docs/image_en.png +0 -0
- package/package.json +25 -35
- package/AGENT.md +0 -330
- package/DOCS.md +0 -826
- package/docs/demo.png +0 -0
- package/types/index.d.ts +0 -171
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sheetnext",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A pure front-end spreadsheet component with Excel-like capabilities, built-in native AI workflows, and flexible LLM integration for data operations.",
|
|
5
5
|
"homepage": "https://www.sheetnext.com",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -10,59 +10,49 @@
|
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/wyyazlz/sheetnext/issues"
|
|
12
12
|
},
|
|
13
|
+
"license": "Apache-2.0",
|
|
13
14
|
"main": "dist/sheetnext.umd.js",
|
|
14
15
|
"module": "dist/sheetnext.es.js",
|
|
15
|
-
"types": "types/index.d.ts",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
18
|
"import": "./dist/sheetnext.es.js",
|
|
19
19
|
"require": "./dist/sheetnext.umd.js"
|
|
20
20
|
},
|
|
21
|
-
"./dist/sheetnext.css": "./dist/sheetnext.css"
|
|
21
|
+
"./dist/sheetnext.css": "./dist/sheetnext.css",
|
|
22
|
+
"./locales/zh-CN": {
|
|
23
|
+
"import": "./dist/sheetnext.locale.zh-CN.es.js",
|
|
24
|
+
"require": "./dist/sheetnext.locale.zh-CN.umd.js"
|
|
25
|
+
}
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
24
28
|
"dist",
|
|
25
|
-
"types",
|
|
26
29
|
"README.md",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
30
|
+
"README_CN.md",
|
|
31
|
+
"docs/docs-detail.md",
|
|
29
32
|
"docs/logo.png",
|
|
30
|
-
"docs/
|
|
33
|
+
"docs/image_en.png"
|
|
31
34
|
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "vite",
|
|
37
|
+
"build": "vite build",
|
|
38
|
+
"preview": "vite preview"
|
|
39
|
+
},
|
|
32
40
|
"keywords": [
|
|
33
41
|
"spreadsheet",
|
|
34
42
|
"excel",
|
|
35
43
|
"editor",
|
|
36
44
|
"table",
|
|
37
45
|
"ai",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"pivot-table",
|
|
44
|
-
"chart",
|
|
45
|
-
"business-intelligence",
|
|
46
|
-
"collaborative-editing"
|
|
46
|
+
"sheetnext",
|
|
47
|
+
"sheet",
|
|
48
|
+
"spreadsheet editor",
|
|
49
|
+
"ai excel",
|
|
50
|
+
"ai spreadsheet"
|
|
47
51
|
],
|
|
48
|
-
"author": "SheetNext Team",
|
|
49
|
-
"license": "Apache-2.0",
|
|
50
52
|
"dependencies": {
|
|
51
|
-
"
|
|
52
|
-
"fast-xml-parser": "^5.3.0",
|
|
53
|
-
"file-saver": "^2.0.5",
|
|
54
|
-
"jszip": "^2.6.1"
|
|
53
|
+
"fast-xml-parser": "^5.3.0"
|
|
55
54
|
},
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"openai": ">=6.0.0"
|
|
59
|
-
},
|
|
60
|
-
"peerDependenciesMeta": {
|
|
61
|
-
"@anthropic-ai/sdk": {
|
|
62
|
-
"optional": true
|
|
63
|
-
},
|
|
64
|
-
"openai": {
|
|
65
|
-
"optional": true
|
|
66
|
-
}
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"vite": "^7.1.9"
|
|
67
57
|
}
|
|
68
|
-
}
|
|
58
|
+
}
|
package/AGENT.md
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
# SheetNext AI 中转配置指南
|
|
2
|
-
|
|
3
|
-
> 一句话总结超级简单:写一个接口将前端传入的message消息分发给你想对接的大模型,然后在前端配置好接口地址即可开始工作!
|
|
4
|
-
|
|
5
|
-
## 文档导航
|
|
6
|
-
|
|
7
|
-
- [← 返回 README](https://github.com/wyyazlz/sheetnext/blob/master/README.md) - 快速开始和特点介绍
|
|
8
|
-
- [← API 文档](https://github.com/wyyazlz/sheetnext/blob/master/DOCS.md) - 详细的类、方法和属性说明
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 目录
|
|
13
|
-
|
|
14
|
-
- [SheetNext AI 中转配置指南](#sheetnext-ai-中转配置指南)
|
|
15
|
-
- [文档导航](#文档导航)
|
|
16
|
-
- [目录](#目录)
|
|
17
|
-
- [功能说明](#功能说明)
|
|
18
|
-
- [核心功能](#核心功能)
|
|
19
|
-
- [核心架构](#核心架构)
|
|
20
|
-
- [工作流程](#工作流程)
|
|
21
|
-
- [完整示例](#完整示例)
|
|
22
|
-
- [安装依赖](#安装依赖)
|
|
23
|
-
- [完整代码](#完整代码)
|
|
24
|
-
- [配置说明](#配置说明)
|
|
25
|
-
- [消息格式](#消息格式)
|
|
26
|
-
- [请求格式](#请求格式)
|
|
27
|
-
- [响应格式](#响应格式)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 功能说明
|
|
32
|
-
|
|
33
|
-
AI 服务中转层是连接 SheetNext 前端与大模型 API 的桥梁,主要负责以下核心功能:
|
|
34
|
-
|
|
35
|
-
### 核心功能
|
|
36
|
-
|
|
37
|
-
1. **消息格式转换** - 将 SheetNext 提供的通用消息结构转换为目标大模型(如 Claude、GPT 等)所需的标准格式
|
|
38
|
-
2. **流式数据处理** - 实现 AI 响应的流式接收与转发,提升用户交互体验
|
|
39
|
-
3. **安全隔离** - 在服务端隐藏真实的 API Key,避免密钥泄露风险
|
|
40
|
-
4. **使用统计** - 企业可在中转层统计 Token 消耗、请求次数等关键数据
|
|
41
|
-
|
|
42
|
-
---
|
|
43
|
-
|
|
44
|
-
## 核心架构
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
48
|
-
│ │ │ │ │ │
|
|
49
|
-
│ SheetNext │────────▶│ 中转服务器 │────────▶│各种大模型API│
|
|
50
|
-
│ 前端 │ HTTP │ (您的服务器) │ HTTPS │ (Claude等) │
|
|
51
|
-
│ │◀────────│ │◀────────│ │
|
|
52
|
-
└─────────────┘ SSE流 └──────────────┘ Stream └─────────────┘
|
|
53
|
-
│
|
|
54
|
-
▼
|
|
55
|
-
┌──────────────┐
|
|
56
|
-
│ 使用统计/日志 │
|
|
57
|
-
└──────────────┘
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### 工作流程
|
|
61
|
-
|
|
62
|
-
1. **前端请求** - SheetNext 发送包含 `messages` 数组的 POST 请求到中转服务器
|
|
63
|
-
2. **格式转换** - 中转服务器将通用格式转换为目标大模型的专用格式
|
|
64
|
-
3. **API 调用** - 使用服务端存储的 API Key 调用大模型 API
|
|
65
|
-
4. **流式响应** - 接收大模型的流式响应,转换后通过 SSE (Server-Sent Events) 返回前端
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## 完整示例
|
|
70
|
-
|
|
71
|
-
通用中转完整实现示例:
|
|
72
|
-
|
|
73
|
-
### 安装依赖
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm install @anthropic-ai/sdk openai
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 完整代码
|
|
80
|
-
|
|
81
|
-
```javascript
|
|
82
|
-
/**
|
|
83
|
-
* SheetNext AI & claude/openai 中转服务器示例 Node.js 版本
|
|
84
|
-
* 2025.10.17 v1.0.0
|
|
85
|
-
*/
|
|
86
|
-
|
|
87
|
-
const http = require('http');
|
|
88
|
-
const Anthropic = require('@anthropic-ai/sdk');
|
|
89
|
-
const OpenAI = require('openai');
|
|
90
|
-
|
|
91
|
-
// ======= 配置 =======
|
|
92
|
-
const CONFIG = {
|
|
93
|
-
model: 'claude-sonnet-4-5-20250929', // 设置模型名称,自动判断使用 claude 还是 openai
|
|
94
|
-
claude: {
|
|
95
|
-
apiKey: 'your-apiKey',
|
|
96
|
-
baseURL: 'https://xx.xx.xx/'
|
|
97
|
-
},
|
|
98
|
-
openai: {
|
|
99
|
-
apiKey: 'your-apiKey',
|
|
100
|
-
baseURL: 'https://xx.xx.xx/v1'
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
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 });
|
|
106
|
-
|
|
107
|
-
// ======= message默认是openai格式,claude请求时转为它适配格式 =======
|
|
108
|
-
const convertToClaudeMessages = (messages) => {
|
|
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
|
-
};
|
|
129
|
-
|
|
130
|
-
for (const msg of messages) {
|
|
131
|
-
if (msg.role === 'system') {
|
|
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
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
// user/assistant 消息
|
|
143
|
-
claudeMessages.push({ role: msg.role, content: convertContent(msg.content) });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { system, messages: claudeMessages };
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// ======= Claude SDK =======
|
|
151
|
-
async function callClaudeSDK(messages, model, onChunk) {
|
|
152
|
-
const { system, messages: claudeMessages } = convertToClaudeMessages(messages);
|
|
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
|
-
|
|
170
|
-
const stream = await anthropic.messages.create({
|
|
171
|
-
model: model,
|
|
172
|
-
max_tokens: 8192,
|
|
173
|
-
system,
|
|
174
|
-
messages: claudeMessages,
|
|
175
|
-
stream: true,
|
|
176
|
-
thinking: { type: "enabled", budget_tokens: 2000 }
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
for await (const event of stream) {
|
|
180
|
-
if (event.type === 'content_block_delta') {
|
|
181
|
-
const { delta } = event;
|
|
182
|
-
if (delta?.type === 'thinking_delta' && delta.thinking) {
|
|
183
|
-
onChunk({ type: 'think', delta: delta.thinking });
|
|
184
|
-
} else if (delta?.type === 'text_delta') {
|
|
185
|
-
onChunk({ type: 'text', delta: delta.text });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
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
|
-
|
|
207
|
-
// ======= HTTP 处理 =======
|
|
208
|
-
async function handleChat(messages, res) {
|
|
209
|
-
res.writeHead(200, {
|
|
210
|
-
'Content-Type': 'text/event-stream',
|
|
211
|
-
'Cache-Control': 'no-cache',
|
|
212
|
-
'Connection': 'keep-alive',
|
|
213
|
-
'Access-Control-Allow-Origin': '*'
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
let ended = false;
|
|
217
|
-
const write = (data) => !ended && !res.writableEnded && res.write(data);
|
|
218
|
-
const onChunk = (chunk) => write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
219
|
-
|
|
220
|
-
try {
|
|
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`);
|
|
229
|
-
} catch (error) {
|
|
230
|
-
write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
|
231
|
-
} finally {
|
|
232
|
-
ended = true;
|
|
233
|
-
res.end();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ======= HTTP 服务器 =======
|
|
238
|
-
http.createServer(async (req, res) => {
|
|
239
|
-
const corsHeaders = {
|
|
240
|
-
'Access-Control-Allow-Origin': '*',
|
|
241
|
-
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
242
|
-
'Access-Control-Allow-Headers': 'Content-Type'
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
if (req.method === 'OPTIONS') {
|
|
246
|
-
res.writeHead(200, corsHeaders);
|
|
247
|
-
return res.end();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (req.url === '/sheetnextAI' && req.method === 'POST') {
|
|
251
|
-
let body = '';
|
|
252
|
-
req.on('data', chunk => body += chunk);
|
|
253
|
-
req.on('end', async () => {
|
|
254
|
-
try {
|
|
255
|
-
const { messages } = JSON.parse(body);
|
|
256
|
-
if (!Array.isArray(messages)) throw new Error('Invalid messages');
|
|
257
|
-
await handleChat(messages, res);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
260
|
-
res.end(JSON.stringify({ error: error.message }));
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
} else {
|
|
264
|
-
res.writeHead(404);
|
|
265
|
-
res.end('Not Found');
|
|
266
|
-
}
|
|
267
|
-
}).listen(3000, () => console.log('🚀 Server running on http://localhost:3000'));
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
### 配置说明
|
|
271
|
-
|
|
272
|
-
**判断规则:**
|
|
273
|
-
- 如果模型名称包含 `claude`(不区分大小写) → 使用 Claude SDK
|
|
274
|
-
- 其他情况 → 使用 OpenAI SDK
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## 消息格式
|
|
279
|
-
|
|
280
|
-
### 请求格式
|
|
281
|
-
|
|
282
|
-
SheetNext 发送的请求体格式:
|
|
283
|
-
|
|
284
|
-
```json
|
|
285
|
-
{
|
|
286
|
-
"messages": [
|
|
287
|
-
{
|
|
288
|
-
"role": "system",
|
|
289
|
-
"content": "你是一个电子表格助手..."
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
"role": "user",
|
|
293
|
-
"content": "帮我分析销售数据"
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
"role": "assistant",
|
|
297
|
-
"content": "好的,我来帮您分析..."
|
|
298
|
-
},
|
|
299
|
-
{
|
|
300
|
-
"role": "user",
|
|
301
|
-
"content": [
|
|
302
|
-
{
|
|
303
|
-
"type": "text",
|
|
304
|
-
"text": "某区域图片"
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
"type": "image_url",
|
|
308
|
-
"image_url": {
|
|
309
|
-
"url": "data:image/png;base64,iVBORw0KGgoAAAANS..."
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
]
|
|
313
|
-
}
|
|
314
|
-
]
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### 响应格式
|
|
319
|
-
|
|
320
|
-
您的服务器应该返回 SSE 流:
|
|
321
|
-
|
|
322
|
-
```
|
|
323
|
-
data: {"type":"text","delta":"我"}
|
|
324
|
-
|
|
325
|
-
data: {"type":"text","delta":"来"}
|
|
326
|
-
|
|
327
|
-
data: {"type":"text","delta":"帮"}
|
|
328
|
-
|
|
329
|
-
data: [DONE]
|
|
330
|
-
```
|