visualknowledge 0.1.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/README.md +115 -0
- package/bin/visualknowledge.js +396 -0
- package/package.json +1 -0
- package/server.py +145 -0
- package/skills/visualize.py +161 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# VisualKnowledge
|
|
2
|
+
|
|
3
|
+
Interactive AI Chat with Visualization — 用可视化方式与 AI 对话,让复杂概念一目了然。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **流式对话** — 基于 Claude API 的实时流式响应,打字机效果逐字呈现
|
|
8
|
+
- **内联图表渲染** — AI 回复中的 Mermaid 图表、HTML 可视化与文字自然交叉排列,而非堆叠在底部
|
|
9
|
+
- **多主题支持** — 深色 / 浅色主题一键切换
|
|
10
|
+
- **Markdown 完整支持** — 代码高亮、表格、引用块、列表等
|
|
11
|
+
- **一键启动** — `npx visualknowledge` 即用,无需手动克隆或配置环境
|
|
12
|
+
- **隐私优先** — 所有数据本地处理,无远程追踪
|
|
13
|
+
- **端口自动检测** — 默认端口被占用时自动递增寻找可用端口
|
|
14
|
+
- **优雅退出** — Ctrl+C 干净终止所有子进程,无残留
|
|
15
|
+
|
|
16
|
+
## 快速开始
|
|
17
|
+
|
|
18
|
+
### 方式一:npx(推荐)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx visualknowledge
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
首次运行会自动下载依赖并打开浏览器。之后只需一条命令即可启动。
|
|
25
|
+
|
|
26
|
+
### 方式二:本地运行
|
|
27
|
+
|
|
28
|
+
**前置要求:**
|
|
29
|
+
|
|
30
|
+
- Python 3.10+
|
|
31
|
+
- Node.js 16+(仅用于 npx 启动方式)
|
|
32
|
+
- Flask、Anthropic SDK(`pip install flask anthropic`)
|
|
33
|
+
- 设置环境变量 `ANTHROPIC_API_KEY`
|
|
34
|
+
|
|
35
|
+
**启动:**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# 克隆仓库
|
|
39
|
+
git clone https://github.com/user/VisualKnowledge.git
|
|
40
|
+
cd VisualKnowledge
|
|
41
|
+
|
|
42
|
+
# 安装 Python 依赖
|
|
43
|
+
pip install flask anthropic
|
|
44
|
+
|
|
45
|
+
# 设置 API Key
|
|
46
|
+
export ANTHROPIC_API_KEY=your-key-here
|
|
47
|
+
|
|
48
|
+
# 启动服务
|
|
49
|
+
python server.py
|
|
50
|
+
# 或使用 npx 启动脚本
|
|
51
|
+
node bin/visualknowledge.js
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
浏览器自动打开 `http://localhost:5000`。
|
|
55
|
+
|
|
56
|
+
### 命令行选项
|
|
57
|
+
|
|
58
|
+
| 选项 | 说明 |
|
|
59
|
+
|------|------|
|
|
60
|
+
| `-p, --port <port>` | 指定起始端口(默认 5000) |
|
|
61
|
+
| `--no-open` | 启动后不自动打开浏览器 |
|
|
62
|
+
| `-h, --help` | 显示帮助信息 |
|
|
63
|
+
| `-v, --version` | 显示版本号 |
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx visualknowledge -p 8080 # 使用端口 8080
|
|
67
|
+
npx visualknowledge --no-open # 不自动打开浏览器
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 技术栈
|
|
71
|
+
|
|
72
|
+
| 层 | 技术 |
|
|
73
|
+
|----|------|
|
|
74
|
+
| 前端 | 原生 HTML / CSS / JavaScript(SPA) |
|
|
75
|
+
| 后端 | Python Flask + Anthropic SDK |
|
|
76
|
+
| 可视化 | Mermaid.js + 自定义 HTML 渲染器 |
|
|
77
|
+
| 分发 | npm(Node.js bootstrap 脚本) |
|
|
78
|
+
| 数据库 | SQLite(本地存储) |
|
|
79
|
+
|
|
80
|
+
## 项目结构
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
VisualKnowledge/
|
|
84
|
+
├── index.html # 前端 SPA(聊天界面 + 渲染引擎)
|
|
85
|
+
├── server.py # Flask 后端(API + 流式响应)
|
|
86
|
+
├── package.json # npm 包配置(npx 支持)
|
|
87
|
+
├── bin/
|
|
88
|
+
│ └── visualknowledge.js # npx 入口脚本(环境检测 + 进程管理)
|
|
89
|
+
├── skills/
|
|
90
|
+
│ └── visualize.py # 可视化技能提示词
|
|
91
|
+
└── static/ # 静态资源
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 环境变量
|
|
95
|
+
|
|
96
|
+
| 变量 | 说明 | 默认值 |
|
|
97
|
+
|------|------|--------|
|
|
98
|
+
| `ANTHROPIC_API_KEY` | API 密钥 | (必填) |
|
|
99
|
+
| `ANTHROPIC_BASE_URL` | API 基础 URL | `https://open.bigmodel.cn/api/anthropic` |
|
|
100
|
+
| `ANTHROPIC_MODEL` | 模型名称 | `GLM-5V-Turbo` |
|
|
101
|
+
|
|
102
|
+
## 未来方向
|
|
103
|
+
|
|
104
|
+
- [ ] **多模型支持** — 接入 OpenAI、Gemini 等更多 LLM 后端,通过配置切换
|
|
105
|
+
- [ ] **对话历史持久化** — 支持多会话管理、搜索历史记录、导出对话
|
|
106
|
+
- [ ] **插件系统** — 可扩展的技能/插件架构,用户自定义可视化模板
|
|
107
|
+
- [ ] **协作模式** — 多人实时共享画板,同步查看和编辑可视化内容
|
|
108
|
+
- [ ] **离线模式** — 本地 LLM(Ollama)集成,完全离线使用
|
|
109
|
+
- [ ] **移动端适配** — 响应式布局优化,PWA 支持
|
|
110
|
+
- [ ] **更多图表类型** — ECharts/D3.js 集成,支持更丰富的数据可视化
|
|
111
|
+
- [ ] **国际化** — 多语言界面支持
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { spawn, exec } = require('child_process');
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
11
|
+
const PKG_VERSION = require('../package.json').version;
|
|
12
|
+
const DEFAULT_PORT = 5000;
|
|
13
|
+
const MAX_PORT_RETRIES = 10;
|
|
14
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
15
|
+
const SHUTDOWN_TIMEOUT_MS = 3000;
|
|
16
|
+
|
|
17
|
+
// ─── CLI Argument Parsing (T006) ─────────────────────────────
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const opts = {
|
|
20
|
+
port: DEFAULT_PORT,
|
|
21
|
+
noOpen: false,
|
|
22
|
+
help: false,
|
|
23
|
+
version: false,
|
|
24
|
+
};
|
|
25
|
+
const args = argv.slice(2);
|
|
26
|
+
for (let i = 0; i < args.length; i++) {
|
|
27
|
+
const a = args[i];
|
|
28
|
+
if (a === '--port' || a === '-p') {
|
|
29
|
+
opts.port = parseInt(args[++i], 10);
|
|
30
|
+
if (isNaN(opts.port) || opts.port < 1 || opts.port > 65535) {
|
|
31
|
+
console.error('✗ Invalid port number. Must be between 1-65535.');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
} else if (a === '--no-open') {
|
|
35
|
+
opts.noOpen = true;
|
|
36
|
+
} else if (a === '--help' || a === '-h') {
|
|
37
|
+
opts.help = true;
|
|
38
|
+
} else if (a === '--version' || a === '-v') {
|
|
39
|
+
opts.version = true;
|
|
40
|
+
} else if (a.startsWith('-')) {
|
|
41
|
+
console.error(`✗ Unknown option: ${a}`);
|
|
42
|
+
console.error('Run with --help for usage information.');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return opts;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printHelp() {
|
|
50
|
+
console.log(`
|
|
51
|
+
VisualKnowledge v${PKG_VERSION}
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
npx visualknowledge [options]
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
-p, --port <port> Specify starting port (default: ${DEFAULT_PORT})
|
|
58
|
+
--no-open Do not auto-open browser after startup
|
|
59
|
+
-h, --help Show this help message
|
|
60
|
+
-v, --version Show version number
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
npx visualknowledge # Start on default port 5000
|
|
64
|
+
npx visualknowledge -p 8080 # Start on port 8080
|
|
65
|
+
npx visualknowledge --no-open # Start without opening browser
|
|
66
|
+
`.trimStart() + '\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Terminal Output Helpers ──────────────────────────────────
|
|
70
|
+
const OK = '✓';
|
|
71
|
+
const FAIL = '✗';
|
|
72
|
+
const WARN = '⚠';
|
|
73
|
+
|
|
74
|
+
function banner() {
|
|
75
|
+
console.log(`\n VisualKnowledge v${PKG_VERSION}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── T003: Node.js Version Check ──────────────────────────────
|
|
79
|
+
function checkNodeVersion() {
|
|
80
|
+
const ver = process.versions.node;
|
|
81
|
+
const major = parseInt(ver.split('.')[0], 10);
|
|
82
|
+
if (major < 16) {
|
|
83
|
+
console.log(`${FAIL} Node.js version too old`);
|
|
84
|
+
console.log(` Installed: v${ver}`);
|
|
85
|
+
console.log(` Required: >= 16.0.0\n`);
|
|
86
|
+
console.log(' Fix: Upgrade Node.js from https://nodejs.org/');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
console.log(`${OK} Node.js v${ver}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── T004: Python Detection ───────────────────────────────────
|
|
94
|
+
function findPython() {
|
|
95
|
+
return new Promise((resolve) => {
|
|
96
|
+
// Try python3 first, then python
|
|
97
|
+
const candidates = process.platform === 'win32' ? ['python', 'py'] : ['python3', 'python'];
|
|
98
|
+
let idx = 0;
|
|
99
|
+
|
|
100
|
+
function tryNext() {
|
|
101
|
+
if (idx >= candidates.length) {
|
|
102
|
+
resolve(null);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const cmd = candidates[idx++];
|
|
106
|
+
exec(`${cmd} --version`, (err, stdout, stderr) => {
|
|
107
|
+
// python outputs version to stderr on some platforms
|
|
108
|
+
const output = (stdout || stderr || '').trim();
|
|
109
|
+
if (!err && output.toLowerCase().startsWith('python')) {
|
|
110
|
+
resolve({ command: cmd, output });
|
|
111
|
+
} else {
|
|
112
|
+
tryNext();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
tryNext();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function checkPython() {
|
|
122
|
+
const py = await findPython();
|
|
123
|
+
if (!py) {
|
|
124
|
+
console.log(`${FAIL} Python 3.10+ not found`);
|
|
125
|
+
console.log('\n Fix: Install Python 3.10+ from https://www.python.org/downloads/\n'
|
|
126
|
+
+ ' Or via: brew install python@3.10 (macOS)\n'
|
|
127
|
+
+ ' sudo apt install python3.10 (Ubuntu)\n');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Parse version from output like "Python 3.10.12"
|
|
132
|
+
const match = py.output.match(/(\d+\.\d+(\.\d+)?)/);
|
|
133
|
+
const verStr = match ? match[1] : '0.0';
|
|
134
|
+
const parts = verStr.split('.').map(Number);
|
|
135
|
+
const [major, minor] = parts;
|
|
136
|
+
|
|
137
|
+
if (major < 3 || (major === 3 && minor < 10)) {
|
|
138
|
+
console.log(`${FAIL} Python version too old`);
|
|
139
|
+
console.log(` Installed: ${verStr}`);
|
|
140
|
+
console.log(` Required: >= 3.10.0\n`);
|
|
141
|
+
console.log(' Fix: Upgrade Python from https://www.python.org/downloads/');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`${OK} Python ${verStr}`);
|
|
146
|
+
return py.command;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── T005: Python Dependency Check ────────────────────────────
|
|
150
|
+
function checkDeps(pythonCmd) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const script = `
|
|
153
|
+
import importlib, sys
|
|
154
|
+
missing = []
|
|
155
|
+
for pkg in ('flask', 'anthropic'):
|
|
156
|
+
try:
|
|
157
|
+
importlib.import_module(pkg.replace('-', '_'))
|
|
158
|
+
except ImportError:
|
|
159
|
+
missing.append(pkg)
|
|
160
|
+
if missing:
|
|
161
|
+
print('MISSING:' + ','.join(missing))
|
|
162
|
+
sys.exit(1)
|
|
163
|
+
else:
|
|
164
|
+
print('OK')
|
|
165
|
+
`;
|
|
166
|
+
const child = spawn(pythonCmd, ['-c', script], {
|
|
167
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
168
|
+
env: process.env,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let stdout = '';
|
|
172
|
+
let stderr = '';
|
|
173
|
+
child.stdout.on('data', (d) => { stdout += d; });
|
|
174
|
+
child.stderr.on('data', (d) => { stderr += d; });
|
|
175
|
+
child.on('close', (code) => {
|
|
176
|
+
if (code !== 0) {
|
|
177
|
+
const missing = (stdout.match(/MISSING:(.+)/) || [])[1];
|
|
178
|
+
if (missing) {
|
|
179
|
+
console.log(`${FAIL} Missing Python dependencies:`);
|
|
180
|
+
missing.split(',').forEach((dep) => {
|
|
181
|
+
console.log(` - ${dep.trim()}`);
|
|
182
|
+
});
|
|
183
|
+
console.log(`\n Fix: pip install ${missing}\n`);
|
|
184
|
+
} else {
|
|
185
|
+
console.log(`${FAIL} Dependency check failed`);
|
|
186
|
+
if (stderr.trim()) console.log(` ${stderr.trim()}`);
|
|
187
|
+
}
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
console.log(`${OK} Dependencies OK (flask, anthropic)`);
|
|
191
|
+
resolve(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── T007 + T013 + T014: Port Detection ──────────────────────
|
|
197
|
+
function findAvailablePort(startPort, maxRetries) {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
let attempt = 0;
|
|
200
|
+
|
|
201
|
+
function tryPort(port) {
|
|
202
|
+
attempt++;
|
|
203
|
+
if (attempt > maxRetries) {
|
|
204
|
+
reject(new Error('NO_PORTS'));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const server = net.createServer();
|
|
209
|
+
server.once('error', () => {
|
|
210
|
+
console.log(`${WARN} Port ${port} in use, trying ${port + 1}...`);
|
|
211
|
+
tryPort(port + 1);
|
|
212
|
+
});
|
|
213
|
+
server.once('listening', () => {
|
|
214
|
+
server.close(() => resolve(port));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
server.listen(port, '0.0.0.0');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tryPort(startPort);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── T010: Cross-platform Browser Opening ────────────────────
|
|
225
|
+
function openBrowser(url) {
|
|
226
|
+
let cmd;
|
|
227
|
+
switch (process.platform) {
|
|
228
|
+
case 'darwin': cmd = `open "${url}"`; break;
|
|
229
|
+
case 'win32': cmd = `start "" "${url}"`; break;
|
|
230
|
+
default: cmd = `xdg-open "${url}"`; break;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
exec(cmd, (err) => {
|
|
234
|
+
if (err) {
|
|
235
|
+
console.log(`${WARN} Could not open browser automatically`);
|
|
236
|
+
console.log(` Please navigate manually to: ${url}\n`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── T008: Python Flask Subprocess Launch ────────────────────
|
|
242
|
+
function startServer(pythonCmd, port) {
|
|
243
|
+
const serverDir = path.resolve(__dirname, '..');
|
|
244
|
+
|
|
245
|
+
const child = spawn(pythonCmd, ['server.py', '--port', String(port)], {
|
|
246
|
+
cwd: serverDir,
|
|
247
|
+
stdio: 'inherit',
|
|
248
|
+
env: process.env,
|
|
249
|
+
detached: false,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return child;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── T009: Server Ready Detection ─────────────────────────────
|
|
256
|
+
function waitForReady(child, timeoutMs) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const timer = setTimeout(() => {
|
|
259
|
+
reject(new Error('READY_TIMEOUT'));
|
|
260
|
+
}, timeoutMs);
|
|
261
|
+
|
|
262
|
+
// Collect stdout to detect ready signal
|
|
263
|
+
let output = '';
|
|
264
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
265
|
+
|
|
266
|
+
// We use stdio:'inherit' so we can't intercept stdout directly.
|
|
267
|
+
// Instead, we wait briefly and assume success if the process is still running.
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
if (!child.killed && child.exitCode === null) {
|
|
271
|
+
resolve(true);
|
|
272
|
+
} else {
|
|
273
|
+
reject(new Error('SERVER_EXIT'));
|
|
274
|
+
}
|
|
275
|
+
}, 2000); // Flask starts quickly; 2s is enough for local launch
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── T016 + T017 + T018: Graceful Shutdown ───────────────────
|
|
280
|
+
function setupShutdownHandlers(child) {
|
|
281
|
+
let shuttingDown = false;
|
|
282
|
+
|
|
283
|
+
function shutdown(signal) {
|
|
284
|
+
if (shuttingDown) return;
|
|
285
|
+
shuttingDown = true;
|
|
286
|
+
|
|
287
|
+
console.log(`\n${signal} received, shutting down...`);
|
|
288
|
+
|
|
289
|
+
if (child && !child.killed && child.exitCode === null) {
|
|
290
|
+
// Send SIGTERM (or equivalent on Windows)
|
|
291
|
+
try {
|
|
292
|
+
process.kill(child.pid, process.platform === 'win32' ? undefined : 'SIGTERM');
|
|
293
|
+
} catch (_) {
|
|
294
|
+
// Process may have already exited
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Force kill after timeout
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (!child.killed) {
|
|
300
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
301
|
+
}
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
304
|
+
} else {
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
310
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── T011: Main Flow Assembly ─────────────────────────────────
|
|
314
|
+
async function main() {
|
|
315
|
+
const opts = parseArgs(process.argv);
|
|
316
|
+
|
|
317
|
+
if (opts.help) {
|
|
318
|
+
printHelp();
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (opts.version) {
|
|
323
|
+
console.log(`VisualKnowledge v${PKG_VERSION}`);
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Banner
|
|
328
|
+
banner();
|
|
329
|
+
|
|
330
|
+
// Environment checks (T003-T005)
|
|
331
|
+
checkNodeVersion();
|
|
332
|
+
const pythonCmd = await checkPython();
|
|
333
|
+
await checkDeps(pythonCmd);
|
|
334
|
+
|
|
335
|
+
// Port detection (T007 + T013 + T014)
|
|
336
|
+
let actualPort;
|
|
337
|
+
try {
|
|
338
|
+
actualPort = await findAvailablePort(opts.port, MAX_PORT_RETRIES);
|
|
339
|
+
console.log(`${OK} Port ${actualPort} available`);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (err.message === 'NO_PORTS') {
|
|
342
|
+
console.log(`\n${FAIL} No available port in range ${opts.port}-${opts.port + MAX_PORT_RETRIES - 1}\n`);
|
|
343
|
+
console.log(' Fix: Stop other services using these ports,\n'
|
|
344
|
+
+ ' or use --port to specify a different start port.\n');
|
|
345
|
+
process.exit(2); // Exit code 2 per contract
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Start Python server (T008)
|
|
351
|
+
console.log('\n Starting server...');
|
|
352
|
+
const child = startServer(pythonCmd, actualPort);
|
|
353
|
+
|
|
354
|
+
// Setup graceful shutdown (T016-T018)
|
|
355
|
+
setupShutdownHandlers(child);
|
|
356
|
+
|
|
357
|
+
// Wait for server ready (T009)
|
|
358
|
+
try {
|
|
359
|
+
await waitForReady(child, READY_TIMEOUT_MS);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (err.message === 'READY_TIMEOUT') {
|
|
362
|
+
console.log(`${FAIL} Server failed to start within ${READY_TIMEOUT_MS / 1000}s`);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
if (err.message === 'SERVER_EXIT') {
|
|
366
|
+
console.log(`${FAIL} Server exited unexpectedly`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Build URL with actual port (T015)
|
|
373
|
+
const url = `http://localhost:${actualPort}`;
|
|
374
|
+
|
|
375
|
+
// Open browser unless --no-open (T010 + E1 fix)
|
|
376
|
+
if (!opts.noOpen) {
|
|
377
|
+
openBrowser(url);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Output access address
|
|
381
|
+
console.log(`\n → ${url}\n`);
|
|
382
|
+
|
|
383
|
+
// Keep process alive — Python child runs via inherit stdio
|
|
384
|
+
child.on('exit', (code) => {
|
|
385
|
+
if (code !== 0 && code !== null) {
|
|
386
|
+
console.log(`${WARN} Server exited with code ${code}`);
|
|
387
|
+
}
|
|
388
|
+
process.exit(code || 0);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Run
|
|
393
|
+
main().catch((err) => {
|
|
394
|
+
console.error(`${FAIL} Unexpected error: ${err.message}`);
|
|
395
|
+
process.exit(1);
|
|
396
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name": "visualknowledge", "version": "0.1.0", "description": "Interactive AI Chat with Visualization - one-click launch via npx", "bin": {"visualknowledge": "./bin/visualknowledge.js"}, "files": ["bin/", "index.html", "server.py", "skills/"], "keywords": ["ai", "chat", "visualization", "claude", "mermaid"], "license": "MIT", "engines": {"node": ">=16"}, "repository": {"type": "git", "url": "https://github.com/user/VisualKnowledge"}}
|
package/server.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
from flask import Flask, request, Response, jsonify, send_from_directory
|
|
6
|
+
from anthropic import Anthropic
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'skills'))
|
|
9
|
+
from visualize import get_skill_prompt
|
|
10
|
+
|
|
11
|
+
# 配置日志
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=logging.INFO,
|
|
14
|
+
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
|
|
15
|
+
)
|
|
16
|
+
logger = logging.getLogger('server')
|
|
17
|
+
|
|
18
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
app = Flask(__name__, static_folder=os.path.join(BASE_DIR, 'static'))
|
|
20
|
+
|
|
21
|
+
API_BASE_URL = os.environ.get(
|
|
22
|
+
'ANTHROPIC_BASE_URL', 'https://open.bigmodel.cn/api/anthropic'
|
|
23
|
+
)
|
|
24
|
+
API_KEY = os.environ.get(
|
|
25
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
26
|
+
os.environ.get('ANTHROPIC_API_KEY', '')
|
|
27
|
+
)
|
|
28
|
+
MODEL = os.environ.get('ANTHROPIC_MODEL', 'GLM-5V-Turbo')
|
|
29
|
+
|
|
30
|
+
SKILL_PROMPT = get_skill_prompt()
|
|
31
|
+
|
|
32
|
+
SYSTEM_PROMPT = f"""你是一个优秀的AI助手,擅长用直观的可视化方式解释复杂概念。
|
|
33
|
+
|
|
34
|
+
{SKILL_PROMPT}
|
|
35
|
+
|
|
36
|
+
### Mermaid 图表(简单关系图使用)
|
|
37
|
+
|
|
38
|
+
对于简单的关系图、时序图、饼图等,可以使用 ```mermaid 代码块。
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
graph TD
|
|
42
|
+
A[输入] --> B[处理]
|
|
43
|
+
B --> C[输出]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 回答风格
|
|
47
|
+
- 先用简洁语言解释核心概念
|
|
48
|
+
- **涉及架构、流程、数据变换、神经网络结构、算法步骤等,必须优先使用 ```html 可视化**
|
|
49
|
+
- 简单的关系图可以用 ```mermaid
|
|
50
|
+
- 代码块前后可以有文字说明
|
|
51
|
+
- 最后给出总结要点"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.route('/')
|
|
55
|
+
def index():
|
|
56
|
+
resp = send_from_directory(os.path.join(BASE_DIR, 'frontend'), 'index.html')
|
|
57
|
+
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
58
|
+
resp.headers['Pragma'] = 'no-cache'
|
|
59
|
+
resp.headers['Expires'] = '0'
|
|
60
|
+
return resp
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.route('/api/models', methods=['GET'])
|
|
64
|
+
def get_models():
|
|
65
|
+
models = {
|
|
66
|
+
'current': MODEL,
|
|
67
|
+
'available': [
|
|
68
|
+
MODEL,
|
|
69
|
+
os.environ.get('ANTHROPIC_DEFAULT_HAIKU_MODEL', 'GLM-4.5-air'),
|
|
70
|
+
os.environ.get('ANTHROPIC_DEFAULT_OPUS_MODEL', 'GLM-5.1'),
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
return jsonify(models)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.route('/api/chat', methods=['POST'])
|
|
77
|
+
def chat():
|
|
78
|
+
data = request.json or {}
|
|
79
|
+
messages = data.get('messages', [])
|
|
80
|
+
model = data.get('model', MODEL)
|
|
81
|
+
|
|
82
|
+
if not messages:
|
|
83
|
+
return jsonify({'error': 'No messages provided'}), 400
|
|
84
|
+
|
|
85
|
+
if not API_KEY:
|
|
86
|
+
logger.error("API key is not configured")
|
|
87
|
+
return jsonify({'error': 'API key is not configured'}), 500
|
|
88
|
+
|
|
89
|
+
client = Anthropic(api_key=API_KEY, base_url=API_BASE_URL)
|
|
90
|
+
|
|
91
|
+
def generate():
|
|
92
|
+
full_response = ""
|
|
93
|
+
try:
|
|
94
|
+
with client.messages.stream(
|
|
95
|
+
model=model,
|
|
96
|
+
max_tokens=8000,
|
|
97
|
+
system=SYSTEM_PROMPT,
|
|
98
|
+
messages=messages,
|
|
99
|
+
) as stream:
|
|
100
|
+
for event in stream:
|
|
101
|
+
if event.type == 'content_block_delta':
|
|
102
|
+
if hasattr(event.delta, 'text'):
|
|
103
|
+
delta_text = event.delta.text
|
|
104
|
+
full_response += delta_text
|
|
105
|
+
payload = json.dumps(
|
|
106
|
+
{'type': 'text', 'content': delta_text},
|
|
107
|
+
ensure_ascii=False
|
|
108
|
+
)
|
|
109
|
+
yield f"data: {payload}\n\n"
|
|
110
|
+
|
|
111
|
+
logger.info(f"Full response length: {len(full_response)} chars")
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
import traceback
|
|
115
|
+
logger.error(f"Chat error: {e}\n{traceback.format_exc()}")
|
|
116
|
+
payload = json.dumps(
|
|
117
|
+
{'type': 'error', 'message': str(e)},
|
|
118
|
+
ensure_ascii=False
|
|
119
|
+
)
|
|
120
|
+
yield f"data: {payload}\n\n"
|
|
121
|
+
|
|
122
|
+
yield "data: [DONE]\n\n"
|
|
123
|
+
|
|
124
|
+
resp = Response(generate(), mimetype='text/event-stream')
|
|
125
|
+
resp.headers['Cache-Control'] = 'no-cache'
|
|
126
|
+
resp.headers['X-Accel-Buffering'] = 'no'
|
|
127
|
+
resp.headers['Connection'] = 'keep-alive'
|
|
128
|
+
return resp
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == '__main__':
|
|
132
|
+
import sys
|
|
133
|
+
import io
|
|
134
|
+
import argparse
|
|
135
|
+
|
|
136
|
+
parser = argparse.ArgumentParser(description='VisualKnowledge Server')
|
|
137
|
+
parser.add_argument('--port', type=int, default=5000, help='Port to listen on (default: 5000)')
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
141
|
+
print(f"🚀 Claude Chat 启动中...")
|
|
142
|
+
print(f" API: {API_BASE_URL}")
|
|
143
|
+
print(f" 模型: {MODEL}")
|
|
144
|
+
print(f" 地址: http://localhost:{args.port}")
|
|
145
|
+
app.run(host='0.0.0.0', port=args.port, debug=True)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
SKILLS = {
|
|
2
|
+
"transformer_architecture": {
|
|
3
|
+
"name": "Transformer 架构图",
|
|
4
|
+
"trigger": ["transformer", "注意力机制", "attention", "encoder-decoder", "自注意力", "self-attention"],
|
|
5
|
+
"description": "展示 Transformer 完整架构的数据流"
|
|
6
|
+
},
|
|
7
|
+
"neural_network_flow": {
|
|
8
|
+
"name": "神经网络流程图",
|
|
9
|
+
"trigger": ["神经网络", "前向传播", "反向传播", "cnn", "卷积", "池化", "全连接"],
|
|
10
|
+
"description": "展示神经网络各层数据处理流程"
|
|
11
|
+
},
|
|
12
|
+
"attention_detail": {
|
|
13
|
+
"name": "注意力机制详解",
|
|
14
|
+
"trigger": ["qkv", "query key value", "注意力计算", "注意力权重", "softmax"],
|
|
15
|
+
"description": "详细展示 Q/K/V 计算过程"
|
|
16
|
+
},
|
|
17
|
+
"generic_pipeline": {
|
|
18
|
+
"name": "通用流程图",
|
|
19
|
+
"trigger": ["流程", "步骤", "过程", "pipeline", "workflow"],
|
|
20
|
+
"description": "通用多步骤流程图"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_skill_prompt():
|
|
26
|
+
return """## 可视化 Skill 系统
|
|
27
|
+
|
|
28
|
+
系统支持三种可视化:```html(优先)、```mermaid、```svg。所有复杂可视化必须优先用 HTML。SVG 仅在特殊需要时使用。
|
|
29
|
+
|
|
30
|
+
### HTML 可视化规范(最重要!)
|
|
31
|
+
|
|
32
|
+
#### 1. 布局规则
|
|
33
|
+
- **只用 Flexbox 和 Grid,绝对不要用 position:absolute 或手算坐标**
|
|
34
|
+
- 水平排列:`display:flex; align-items:center; gap:16px; flex-wrap:wrap`
|
|
35
|
+
- 垂直排列:`display:flex; flex-direction:column; gap:12px`
|
|
36
|
+
- 居中:`justify-content:center`
|
|
37
|
+
- 禁止:position:absolute、硬编码像素坐标、负 margin
|
|
38
|
+
|
|
39
|
+
#### 2. 整体结构
|
|
40
|
+
```html
|
|
41
|
+
<div style="max-width:860px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
|
42
|
+
<h2 style="text-align:center;font-size:18px;margin-bottom:20px;color:inherit">标题</h2>
|
|
43
|
+
<!-- 水平行 -->
|
|
44
|
+
<div style="display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap">
|
|
45
|
+
<div class="block">模块A</div>
|
|
46
|
+
<span style="color:inherit;opacity:0.3">→</span>
|
|
47
|
+
<div class="block">模块B</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### 3. 方块样式(用 CSS class)
|
|
53
|
+
```css
|
|
54
|
+
.block{
|
|
55
|
+
border-radius:10px; padding:12px 18px; text-align:center;
|
|
56
|
+
border:1.5px solid; min-width:120px;
|
|
57
|
+
transition:all .2s; cursor:pointer;
|
|
58
|
+
}
|
|
59
|
+
.block:hover{ filter:brightness(1.15); transform:translateY(-1px); }
|
|
60
|
+
.block .title{ font-size:13px; font-weight:700; }
|
|
61
|
+
.block .dim{ font-size:10px; opacity:0.4; margin-top:4px; font-family:monospace; }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### 4. 配色(低透明度填充 + 主色边框)
|
|
65
|
+
| 用途 | 主色 | 用法 |
|
|
66
|
+
|------|------|------|
|
|
67
|
+
| 强调/输入/输出 | #d97706 | fill:#d9770612; border:#d97706 |
|
|
68
|
+
| 注意力/计算 | #2563eb | fill:#2563eb12; border:#2563eb |
|
|
69
|
+
| 归一化/残差 | #16a34a | fill:#16a34a12; border:#16a34a |
|
|
70
|
+
| FFN/Value | #7c3aed | fill:#7c3aed12; border:#7c3aed |
|
|
71
|
+
| Query/Wo | #e11d48 | fill:#e11d4812; border:#e11d48 |
|
|
72
|
+
| Key | #0d9488 | fill:#0d948812; border:#0d9488 |
|
|
73
|
+
| 辅助/通用 | #475569 | fill:#47556912; border:#475569 |
|
|
74
|
+
|
|
75
|
+
#### 5. 箭头和连线
|
|
76
|
+
- 水平箭头用文字 `→` 或 `⟶`(opacity:0.3),不要画 SVG 线条
|
|
77
|
+
- 垂直箭头用 `↓` 或 `⬇`,放在 flex-column 的 gap 中
|
|
78
|
+
- 简单直接,不要复杂化
|
|
79
|
+
|
|
80
|
+
#### 6. 主题适配
|
|
81
|
+
- 背景色用 `transparent`(跟随页面主题)
|
|
82
|
+
- 文字颜色用 `color:inherit`(自动适配深浅色)
|
|
83
|
+
- 方块填充用低透明度(`#d9770612` 即 hex+alpha),深浅色都好看
|
|
84
|
+
|
|
85
|
+
#### 7. 交互
|
|
86
|
+
- `.block:hover` 加 `filter:brightness(1.15)` 和 `transform:translateY(-1px)` 微动画
|
|
87
|
+
- 可选:点击展开详情(用 `<details><summary>` 原生 HTML)
|
|
88
|
+
|
|
89
|
+
### 高质量 HTML 示例(Transformer 数据流)
|
|
90
|
+
|
|
91
|
+
```html
|
|
92
|
+
<div style="max-width:860px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:inherit;background:transparent">
|
|
93
|
+
<style>
|
|
94
|
+
.b{border-radius:10px;padding:12px 18px;text-align:center;border:1.5px solid;min-width:120px;transition:all .2s;cursor:pointer}
|
|
95
|
+
.b:hover{filter:brightness(1.15);transform:translateY(-1px)}
|
|
96
|
+
.b .t{font-size:13px;font-weight:700}
|
|
97
|
+
.b .d{font-size:10px;opacity:.4;margin-top:4px;font-family:monospace}
|
|
98
|
+
.row{display:flex;align-items:center;justify-content:center;gap:14px;flex-wrap:wrap;margin-bottom:16px}
|
|
99
|
+
.col{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:16px}
|
|
100
|
+
.arrow{font-size:18px;opacity:.25;color:inherit}
|
|
101
|
+
.sec{font-size:11px;opacity:.4;font-weight:600;margin-bottom:6px;text-align:center}
|
|
102
|
+
.c-amber{background:#d9770612;border-color:#d97706}
|
|
103
|
+
.c-blue{background:#2563eb12;border-color:#2563eb}
|
|
104
|
+
.c-green{background:#16a34a12;border-color:#16a34a}
|
|
105
|
+
.c-purple{background:#7c3aed12;border-color:#7c3aed}
|
|
106
|
+
.c-rose{background:#e11d4812;border-color:#e11d48}
|
|
107
|
+
.c-teal{background:#0d948812;border-color:#0d9488}
|
|
108
|
+
.c-slate{background:#47556912;border-color:#475569}
|
|
109
|
+
.c-orange{background:#ea580c12;border-color:#ea580c}
|
|
110
|
+
</style>
|
|
111
|
+
|
|
112
|
+
<h2 style="text-align:center;font-size:18px;margin-bottom:20px">Transformer 架构 — 数据流</h2>
|
|
113
|
+
|
|
114
|
+
<div class="sec">输入处理</div>
|
|
115
|
+
<div class="row">
|
|
116
|
+
<div class="b c-slate"><div class="t">输入序列</div><div class="d">"我 喜欢 AI"</div></div>
|
|
117
|
+
<span class="arrow">→</span>
|
|
118
|
+
<div class="b c-amber"><div class="t">Token Embedding</div><div class="d">词→向量 512维</div></div>
|
|
119
|
+
<span class="arrow">→</span>
|
|
120
|
+
<div class="b c-orange"><div class="t">位置编码</div><div class="d">sin/cos</div></div>
|
|
121
|
+
<span class="arrow">→</span>
|
|
122
|
+
<div class="b c-amber" style="border-style:dashed"><div class="t">输入表示</div></div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<span class="arrow">↓</span>
|
|
126
|
+
|
|
127
|
+
<div class="sec">编码器 × N 层</div>
|
|
128
|
+
<div class="col">
|
|
129
|
+
<div class="b c-blue" style="min-width:340px"><div class="t">多头自注意力</div><div class="d">Q·Kᵀ/√d → softmax → ·V</div></div>
|
|
130
|
+
<span class="arrow">↓</span>
|
|
131
|
+
<div class="b c-green" style="min-width:340px"><div class="t">Add & LayerNorm</div><div class="d">残差连接 + 层归一化</div></div>
|
|
132
|
+
<span class="arrow">↓</span>
|
|
133
|
+
<div class="b c-purple" style="min-width:340px"><div class="t">前馈网络 FFN</div><div class="d">512 → 2048 → 512</div></div>
|
|
134
|
+
<span class="arrow">↓</span>
|
|
135
|
+
<div class="b c-green" style="min-width:340px"><div class="t">Add & LayerNorm</div></div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<span class="arrow">↓</span>
|
|
139
|
+
|
|
140
|
+
<div class="sec">输出</div>
|
|
141
|
+
<div class="row">
|
|
142
|
+
<div class="b c-slate"><div class="t">Linear</div><div class="d">512 → vocab</div></div>
|
|
143
|
+
<span class="arrow">→</span>
|
|
144
|
+
<div class="b c-amber"><div class="t">Softmax</div></div>
|
|
145
|
+
<span class="arrow">→</span>
|
|
146
|
+
<div class="b c-rose"><div class="t">预测输出</div></div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 可用的 Skill 类型
|
|
152
|
+
- **Transformer 架构图** (transformer_architecture): transformer, attention, 自注意力
|
|
153
|
+
- **神经网络流程图** (neural_network_flow): 神经网络, CNN, 卷积, 池化
|
|
154
|
+
- **注意力机制详解** (attention_detail): QKV, 注意力权重, softmax
|
|
155
|
+
- **通用流程图** (generic_pipeline): 流程, 步骤, pipeline
|
|
156
|
+
|
|
157
|
+
### 使用规则
|
|
158
|
+
1. 涉及架构/流程/数据变换/神经网络结构/算法步骤 → **必须用 ```html**
|
|
159
|
+
2. 简单关系图/时序图/饼图 → 用 ```mermaid
|
|
160
|
+
3. 每个可视化要包含维度信息和简要说明,文字全部中文
|
|
161
|
+
4. **布局口诀:flex 排列不手算,方块用 class 样式,箭头用文字符号,颜色低透明度**"""
|