feat: 集成飞书通知和机器人对话系统
- 新增通知系统 (notifications 表、服务、API) - 新增飞书定时任务结果推送 (webhook + 应用消息) - 新增飞书应用消息发送服务 (feishu_app_service) - 新增飞书 WebSocket 长连接事件监听 (苹果应用) - 新增飞书账号绑定/解绑 API - 新增橙子飞书机器人 (独立 WS 连接,固定路由到橙子助手 Agent) - 执行记录添加 schedule_id,用户添加飞书绑定字段 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
660
Python_vs_JavaScript_三大核心特性对比分析报告.md
Normal file
660
Python_vs_JavaScript_三大核心特性对比分析报告.md
Normal file
@@ -0,0 +1,660 @@
|
||||
# Python vs JavaScript:三大核心特性对比分析报告
|
||||
|
||||
> **生成日期:** 2025-07-01
|
||||
> **分析维度:** 类型系统 · 并发模型 · 生态工具链
|
||||
> **目标读者:** 学习者、教学者、跨语言开发者
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [对比总览:一张表看清三大差异](#一对比总览一张表看清三大差异)
|
||||
2. [维度一:类型系统](#二维度一类型系统)
|
||||
3. [维度二:并发模型](#三维度二并发模型)
|
||||
4. [维度三:生态工具链](#四维度三生态工具链)
|
||||
5. [三维交叉分析](#五三维交叉分析)
|
||||
6. [选型决策指南](#六选型决策指南)
|
||||
7. [核心结论](#七核心结论)
|
||||
|
||||
---
|
||||
|
||||
## 一、对比总览:一张表看清三大差异
|
||||
|
||||
| 对比维度 | Python | JavaScript | 本质差异 |
|
||||
|----------|--------|-----------|----------|
|
||||
| **类型系统** | 动态强类型 + 类型注解(可选) | 动态弱类型 + TypeScript(主流) | 运行期 vs 编译期 + 强 vs 弱 |
|
||||
| **并发模型** | GIL + 多进程 + asyncio 协程 | 事件循环 + 非阻塞 I/O + Web Workers | 多线程受限 vs 单线程无锁 |
|
||||
| **生态工具链** | pip/conda + PyPI + Django/FastAPI | npm/yarn/pnpm + npm registry + React/Express | 科学计算 vs Web 全端 |
|
||||
| **设计哲学** | "一种最好方式"(There's one way) | "高度灵活"(There are many ways) | 明确 vs 自由 |
|
||||
| **类型趋势** | 渐进类型(PEP 484) | 类型优先(TypeScript 事实标准) | 可选 → 强制 |
|
||||
|
||||
---
|
||||
|
||||
## 二、维度一:类型系统
|
||||
|
||||
### 2.1 核心差异速览
|
||||
|
||||
| 维度 | Python | JavaScript | TypeScript(JS 主流) |
|
||||
|------|--------|-----------|----------------------|
|
||||
| **类型本质** | 动态强类型 | 动态弱类型 | 静态强类型(超集) |
|
||||
| **类型检查时机** | 运行时(可加可选注解) | 运行时(无编译时检查) | **编译时** |
|
||||
| **类型推断** | ❌ 弱(3.11+ 略有改善) | ❌ 无 | ✅ 强推断 |
|
||||
| **空值安全** | `None` 无处不在(无保护) | `null` / `undefined` 混用 | ✅ strictNullChecks |
|
||||
| **隐式类型转换** | ❌ `"1" + 2` → TypeError | ✅ `"1" + 2` → `"12"` | 编译时报错 |
|
||||
| **类型定义文件** | `.pyi`(存根) | ❌ 无 | `.d.ts`(声明文件) |
|
||||
| **泛型** | `typing.Generic`(3.12+ 简化) | ❌ 无 | ✅ 完善 |
|
||||
| **联合类型** | `Union[int, str]` \| `int \| str` | ❌ | ✅ `string \| number` |
|
||||
| **交叉类型** | ❌ 无 | ❌ 无 | ✅ `A & B` |
|
||||
| **工具/语言** | mypy / Pydantic(第三方) | 无 | TypeScript(一等公民) |
|
||||
| **采用率** | ~30% 项目使用 mypy | ~5% 使用 JSDoc | ~80% 项目使用 TS |
|
||||
| **学习曲线** | 平(可选,渐进) | 平(无类型系统) | 陡(类型体操) |
|
||||
|
||||
### 2.2 运行期行为对比
|
||||
|
||||
#### Python:动态**强**类型
|
||||
|
||||
```python
|
||||
# ✅ 运行期类型检查(强类型—不会隐式转换)
|
||||
x = "hello"
|
||||
y = 42
|
||||
print(x + y) # TypeError: can only concatenate str (not "int") to str
|
||||
print(x + str(y)) # ✅ 必须显式转换 → "hello42"
|
||||
|
||||
# 可变 vs 不可变(语言内置约束)
|
||||
a = [1, 2, 3] # list — 可变
|
||||
b = (1, 2, 3) # tuple — 不可变
|
||||
b[0] = 99 # TypeError: 'tuple' object does not support item assignment
|
||||
|
||||
# 类型注解(3.5+,纯文档性质)
|
||||
def greet(name: str) -> str:
|
||||
return f"Hello, {name}"
|
||||
|
||||
greet(42) # 运行时完全正常!注解不强制
|
||||
```
|
||||
|
||||
#### JavaScript:动态**弱**类型
|
||||
|
||||
```javascript
|
||||
// ✅ 运行期无类型检查(弱类型—隐式转换无处不在)
|
||||
let x = "hello";
|
||||
let y = 42;
|
||||
console.log(x + y); // "hello42" ← 隐式转换!
|
||||
console.log(x - y); // NaN ← 字符串减数字
|
||||
|
||||
// 隐式转换陷阱
|
||||
console.log([] + []); // ""(空字符串)
|
||||
console.log([] + {}); // "[object Object]"
|
||||
console.log({} + []); // 0(被解析为代码块)
|
||||
console.log(null == false); // false
|
||||
console.log(null == 0); // false ← 坑
|
||||
console.log(null < 1); // true ← 隐式转数字
|
||||
```
|
||||
|
||||
#### TypeScript:编译期**静态**类型
|
||||
|
||||
```typescript
|
||||
// ✅ 编译时捕获类型错误
|
||||
function greet(name: string): string {
|
||||
return `Hello, ${name}`;
|
||||
}
|
||||
|
||||
greet(42); // ❌ 编译错误:Argument of type 'number' is not assignable
|
||||
// to parameter of type 'string'
|
||||
|
||||
// 类型推断
|
||||
let count = 0; // 自动推断为 number
|
||||
count = "hello"; // ❌ 编译错误
|
||||
|
||||
// 联合类型 + 类型收窄
|
||||
function process(id: string | number) {
|
||||
if (typeof id === "string") {
|
||||
console.log(id.toUpperCase()); // ✅ 收窄为 string
|
||||
} else {
|
||||
console.log(id.toFixed(2)); // ✅ 收窄为 number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 类型系统演化路径
|
||||
|
||||
```
|
||||
Python 类型演进:
|
||||
Python 2.x → 无类型(纯动态)
|
||||
Python 3.0 (2008) → 函数注解(无强制)
|
||||
PEP 484 (2014) → typing 模块 + mypy(第三方可选)
|
||||
PEP 563 (2017) → from __future__ import annotations(推迟求值)
|
||||
Python 3.10 (2021) → `X | Y` 联合类型语法
|
||||
Python 3.11 (2022) → Variadic Generics (TypeVarTuple)
|
||||
Python 3.12 (2023) → 更简洁的泛型语法(list[int] 直接可用)
|
||||
Python 3.13 (2024) → 改进类型推断能力
|
||||
现状:可选类型,社区分裂(约 30% 项目用 mypy,70% 不用)
|
||||
|
||||
JavaScript/TypeScript 类型演进:
|
||||
ES5 (2009) → 无类型,纯动态
|
||||
ES6 (2015) → class、箭头函数
|
||||
TypeScript (2012) → 诞生(微软),编译时类型
|
||||
TS 2.0 (2016) → strictNullChecks(空值安全)
|
||||
TS 2.8 (2018) → 条件类型
|
||||
TS 4.1 (2020) → 模板字面量类型
|
||||
TS 5.0 (2023) → 装饰器稳定
|
||||
现状:类型优先,约 80% 项目使用 TypeScript
|
||||
```
|
||||
|
||||
### 2.4 类型系统一句话
|
||||
|
||||
> **Python 类型是可选的"文档"**,运行时不强制;**JavaScript 原生无类型,但 TypeScript 已成为事实标准**,将类型检查移到编译时。两者都走向"类型化",但 Python 保持渐进可选,JS/TS 走向类型强制。
|
||||
|
||||
---
|
||||
|
||||
## 三、维度二:并发模型
|
||||
|
||||
### 3.1 架构总览:两种截然不同的并发哲学
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|-----------|
|
||||
| **执行模型** | 多线程 + GIL + 多进程 + 协程 (asyncio) | 单线程事件循环 + 非阻塞 I/O + Web Workers |
|
||||
| **并行能力** | 多进程(真并行) | 单线程(无真并行) |
|
||||
| **并发单位** | 线程、进程、协程 | 回调、Promise、async/await |
|
||||
| **核心限制** | **GIL**(全局解释器锁) | 单线程(CPU 密集阻塞事件循环) |
|
||||
| **I/O 密集** | asyncio 协程(单线程并发) | ✅ **事件循环天生高效** |
|
||||
| **CPU 密集** | 多进程(`multiprocessing`) | ❌ **不擅**(需 Worker Threads) |
|
||||
| **锁竞争** | 需要 Lock/Semaphore(GIL 仅保护内存) | 无锁(单线程,无共享状态问题) |
|
||||
| **适用场景** | I/O + CPU 混合 | **高并发 I/O**(C10K+) |
|
||||
| **学习曲线** | 陡(三种并发方式选择困难) | 中(事件循环是唯一模型) |
|
||||
|
||||
### 3.2 基石一:GIL(Python) vs 单线程事件循环(JS)
|
||||
|
||||
#### Python GIL 详解
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ Python 进程 │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ 线程 A │ │ 线程 B │ │
|
||||
│ │ (执行中) │ │ (等待 GIL) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌────────────────────────────────────────────┐ │
|
||||
│ │ GIL(全局解释器锁) │ │
|
||||
│ │ 任意时刻只能有一个线程执行 Python 字节码 │ │
|
||||
│ └────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ I/O 操作释放 GIL ✅ → I/O 密集可并发 │
|
||||
│ CPU 计算持有 GIL ❌ → CPU 密集实际上串行 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
```python
|
||||
# GIL 的实际影响演示
|
||||
import threading
|
||||
import time
|
||||
|
||||
# CPU 密集任务 — GIL 导致几乎串行
|
||||
def count_down(n):
|
||||
while n > 0:
|
||||
n -= 1
|
||||
|
||||
# 双线程 vs 单线程
|
||||
start = time.time()
|
||||
t1 = threading.Thread(target=count_down, args=(50000000,))
|
||||
t2 = threading.Thread(target=count_down, args=(50000000,))
|
||||
t1.start(); t2.start()
|
||||
t1.join(); t2.join()
|
||||
print(f"双线程耗时: {time.time() - start:.2f}s") # ≈ 两倍单线程时间!
|
||||
|
||||
# 解决方案:多进程(真并行)
|
||||
from multiprocessing import Process
|
||||
start = time.time()
|
||||
p1 = Process(target=count_down, args=(50000000,))
|
||||
p2 = Process(target=count_down, args=(50000000,))
|
||||
p1.start(); p2.start()
|
||||
p1.join(); p2.join()
|
||||
print(f"双进程耗时: {time.time() - start:.2f}s") # ≈ 单线程时间!
|
||||
```
|
||||
|
||||
#### JavaScript 事件循环详解
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ JavaScript 事件循环 │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ 调用栈 │ ──▶ │ 微任务 │ ──▶ │ 宏任务 │ │
|
||||
│ │ (Call Stack)│ │ (Microtask)│ │ (Macrotask)│ │
|
||||
│ └───────────┘ └───────────┘ └───────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────┐ │
|
||||
│ │ 任务队列调度顺序 │ │
|
||||
│ │ 1. 执行调用栈当前任务 │ │
|
||||
│ │ 2. 清空所有微任务(Promise.then) │ │
|
||||
│ │ 3. 取一个宏任务执行(setTimeout/IO) │ │
|
||||
│ │ 4. 回到步骤 2(清空微任务) │ │
|
||||
│ └─────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ I/O 操作 → 注册回调 → 立即返回 → 不阻塞 ✅ │
|
||||
│ CPU 密集 → 占用事件循环 → 阻塞所有请求 ❌ │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 事件循环演示
|
||||
console.log('1: 同步开始');
|
||||
|
||||
setTimeout(() => console.log('2: 宏任务'), 0);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
console.log('3: 微任务');
|
||||
Promise.resolve().then(() => console.log('4: 微任务中的微任务'));
|
||||
});
|
||||
|
||||
console.log('5: 同步结束');
|
||||
|
||||
// 输出顺序:
|
||||
// 1: 同步开始
|
||||
// 5: 同步结束
|
||||
// 3: 微任务
|
||||
// 4: 微任务中的微任务
|
||||
// 2: 宏任务
|
||||
```
|
||||
|
||||
### 3.3 基石二:协程实现 — async/await 的两条路径
|
||||
|
||||
| 维度 | Python (asyncio) | JavaScript (async/await) |
|
||||
|------|-----------------|--------------------------|
|
||||
| **标准** | `asyncio` 标准库(3.4+) | ECMAScript 2017 (ES8) |
|
||||
| **关键字** | `async def` / `await` | `async function` / `await` |
|
||||
| **事件循环** | 显式(`asyncio.run()`) | **隐式**(内置在运行时) |
|
||||
| **底层机制** | `__await__` 协议 + 生成器 | Promise + 微任务队列 |
|
||||
| **调度** | 协作式(await 点让出控制权) | 协作式(await 点让出控制权) |
|
||||
| **并发执行** | `asyncio.gather()` | `Promise.all()` |
|
||||
| **超时控制** | `asyncio.wait_for()` | `Promise.race()` + 超时 |
|
||||
| **取消任务** | `task.cancel()`(CancelledError) | ❌ **无原生取消** |
|
||||
| **子进程** | ✅ `asyncio.subprocess` | ❌ 无标准方案 |
|
||||
| **调试** | 难(回调链复杂) | 中等(浏览器 DevTools 好) |
|
||||
|
||||
```python
|
||||
# Python asyncio 示例
|
||||
import asyncio
|
||||
|
||||
async def fetch_data(url: str) -> dict:
|
||||
print(f"开始请求: {url}")
|
||||
await asyncio.sleep(1) # 模拟 I/O,让出事件循环
|
||||
return {"url": url, "data": "..."}
|
||||
|
||||
async def main():
|
||||
# 并发执行三个请求
|
||||
results = await asyncio.gather(
|
||||
fetch_data("https://api.example.com/1"),
|
||||
fetch_data("https://api.example.com/2"),
|
||||
fetch_data("https://api.example.com/3"),
|
||||
)
|
||||
print(f"全部完成: {len(results)} 个结果")
|
||||
|
||||
asyncio.run(main())
|
||||
# 总耗时 ≈ 1 秒(并发),非 3 秒(串行)
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript async/await 等效示例
|
||||
async function fetchData(url) {
|
||||
console.log(`开始请求: ${url}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟 I/O
|
||||
return { url, data: "..." };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 并发执行三个请求
|
||||
const results = await Promise.all([
|
||||
fetchData("https://api.example.com/1"),
|
||||
fetchData("https://api.example.com/2"),
|
||||
fetchData("https://api.example.com/3"),
|
||||
]);
|
||||
console.log(`全部完成: ${results.length} 个结果`);
|
||||
}
|
||||
|
||||
main();
|
||||
// 总耗时 ≈ 1 秒(并发)
|
||||
```
|
||||
|
||||
### 3.4 真并行方案
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|-----------|
|
||||
| **多进程** | ✅ `multiprocessing` / `concurrent.futures.ProcessPoolExecutor` | ❌ 无(浏览器禁止) |
|
||||
| **多线程(真并行)** | ❌ GIL 限制(仅 I/O 释放) | ✅ Worker Threads(Node.js 12+) |
|
||||
| **Web Workers** | ❌ | ✅ 浏览器并行(消息传递) |
|
||||
| **共享内存** | ✅ `multiprocessing.shared_memory` | ❌ 无(postMessage 复制) |
|
||||
| **适用场景** | CPU 密集计算(科学计算、ML) | CPU 密集小任务(图像处理、加密) |
|
||||
| **启动开销** | 大(每个进程独立解释器) | 中(Worker 共享运行时) |
|
||||
| **通信成本** | 高(进程间序列化) | 高(结构化克隆) |
|
||||
|
||||
### 3.5 并发模型一句话
|
||||
|
||||
> **Python 有三种并发方式(线程/GIL受限、进程/真并行、协程/协作式),选择困难但覆盖全场景;JavaScript 只有一种模型(事件循环),单线程无锁但 I/O 密集场景极致高效。** Python 适合 I/O+CPU 混合负载,JS 适合纯高并发 I/O 场景。
|
||||
|
||||
---
|
||||
|
||||
## 四、维度三:生态工具链
|
||||
|
||||
### 4.1 生态总览:两种哲学,两个世界
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|-----------|
|
||||
| **领域定位** | 数据科学、AI、后端、自动化 | Web 前端、全栈、移动端、桌面端 |
|
||||
| **包管理器** | pip / conda / poetry | npm / yarn / pnpm |
|
||||
| **包仓库** | PyPI(~50万包) | npm registry(~200万包) |
|
||||
| **虚拟环境** | venv / virtualenv / conda env | node_modules(本地)+ lockfile |
|
||||
| **构建工具** | setuptools / wheel / build | webpack / Vite / esbuild / turbopack |
|
||||
| **服务器框架** | Django / Flask / FastAPI | Express / Koa / Fastify / NestJS |
|
||||
| **前端框架** | ❌ 无(非前端语言) | React / Vue / Angular / Svelte |
|
||||
| **类型系统** | mypy / Pydantic(第三方) | TypeScript(一等公民) |
|
||||
| **包格式** | wheel (.whl) / sdist (.tar.gz) | CommonJS / ESM / UMD |
|
||||
| **依赖解析** | 静态 resolve(无 lock 易冲突) | lockfile(package-lock.json)强制确定性 |
|
||||
|
||||
### 4.2 包管理器深度对比
|
||||
|
||||
#### Python 包管理器
|
||||
|
||||
```bash
|
||||
# pip — 默认包管理器
|
||||
pip install requests
|
||||
pip install -r requirements.txt
|
||||
pip freeze > requirements.txt
|
||||
|
||||
# poetry — 现代替代(依赖解析更优)
|
||||
poetry add requests
|
||||
poetry install
|
||||
poetry export -f requirements.txt
|
||||
|
||||
# conda — 科学计算首选(跨语言依赖管理)
|
||||
conda install numpy pandas
|
||||
conda env create -f environment.yml
|
||||
```
|
||||
|
||||
| 特性 | pip | conda | poetry |
|
||||
|------|-----|-------|--------|
|
||||
| **包格式** | PyPI wheel/sdist | 预编译二进制(任何语言) | PyPI wheel |
|
||||
| **依赖解析** | 弱(线性) | 强(SAT 求解器) | 强(SAT 求解器) |
|
||||
| **虚拟环境** | venv 手动 | `conda create` 内置 | `poetry env` 内置 |
|
||||
| **lock 文件** | ❌ 无 | ❌ 无(conda-lock 第三方) | ✅ poetry.lock |
|
||||
| **速度** | 快 | 中等(解析慢) | 中等 |
|
||||
| **非 Python 包** | ❌ | ✅(C/C++/R 库) | ❌ |
|
||||
| **科学计算生态** | ❌ 需编译 | ✅ 免编译 | ❌ |
|
||||
|
||||
#### JavaScript 包管理器
|
||||
|
||||
```bash
|
||||
# npm — 官方默认
|
||||
npm install express
|
||||
npm install --save-dev typescript
|
||||
npm ci # 从 lockfile 精确安装
|
||||
|
||||
# yarn — Meta 出品,速度提升
|
||||
yarn add express
|
||||
yarn add --dev typescript
|
||||
yarn install --frozen-lockfile
|
||||
|
||||
# pnpm — 磁盘效率最高(硬链接共享)
|
||||
pnpm add express
|
||||
pnpm install
|
||||
```
|
||||
|
||||
| 特性 | npm | yarn | pnpm |
|
||||
|------|-----|------|------|
|
||||
| **lock 文件** | package-lock.json | yarn.lock | pnpm-lock.yaml |
|
||||
| **磁盘占用** | 每项目重复安装 | 每项目重复安装 | **全局硬链接**(节省 70%+) |
|
||||
| **安装速度** | 中等 | 快(并行下载) | 快 + 缓存复用 |
|
||||
| **monorepo** | workspaces(弱) | workspaces(中) | ✅ **原生支持**(filter/scope) |
|
||||
| **严格模式** | ❌ | ✅ PnP(Plug'n'Play) | ✅ 严格隔离 |
|
||||
| **安全** | 中等 | 好(checksum 验证) | **好**(严格依赖隔离) |
|
||||
|
||||
#### 包仓库对比
|
||||
|
||||
| 维度 | PyPI | npm |
|
||||
|------|------|-----|
|
||||
| **包数量** | ~50万 | ~200万 |
|
||||
| **下载量/年** | ~3000亿 | ~2万亿 |
|
||||
| **依赖深度** | 浅(2-5层) | 深(平均10+层,微观包文化) |
|
||||
| **命名空间** | 单层(`requests`) | 支持 scope(`@angular/core`) |
|
||||
| **私有仓库** | devpi / pypiserver | npm private / verdaccio |
|
||||
| **安全审计** | `pip audit`(2024+) | `npm audit` / `yarn audit` |
|
||||
|
||||
### 4.3 框架生态对比
|
||||
|
||||
#### 后端框架
|
||||
|
||||
| 维度 | Django(Python) | FastAPI(Python) | Express(JS) | NestJS(JS/TS) |
|
||||
|------|-----------------|------------------|--------------|-----------------|
|
||||
| **哲学** | 电池内置(全栈) | 高性能 API | 极简微框架 | 企业级(Angular 风格) |
|
||||
| **类型** | 动态(mypy 辅助) | **Pydantic 强类型** | 动态 | **TypeScript 原生** |
|
||||
| **ORM** | ✅ Django ORM | SQLAlchemy / Tortoise | 无(第三方) | TypeORM / Prisma |
|
||||
| **序列化** | DRF / Django Ninja | ✅ Pydantic(内置) | 手动 | class-validator |
|
||||
| **性能** | 中等 | **高**(Starlette + Uvicorn) | 中等 | 中等 |
|
||||
| **异步** | 3.0+ ASGI 支持 | ✅ 原生 async | async 中间件 | ✅ 原生 RxJS |
|
||||
| **企业使用** | Instagram, Pinterest | Uber, Microsoft | PayPal, Uber | 各行业广泛 |
|
||||
| **学习曲线** | 陡(全栈复杂) | 平(简洁明确) | 平 | 陡(装饰器+RxJS) |
|
||||
| **适用场景** | CMS, 管理后台, 全栈 | **API 服务**, 微服务 | 快速原型, 小服务 | 大型企业后端 |
|
||||
|
||||
#### 前端框架 — JavaScript 独有
|
||||
|
||||
| 框架 | 公司 | 范式 | 模板 | 状态管理 | Bundle 大小 |
|
||||
|------|------|------|------|---------|------------|
|
||||
| **React** | Meta | 函数组件 + Hooks | JSX | Redux / Zustand | ~38KB |
|
||||
| **Vue** | 社区 | Options / Composition | **SFC (.vue)** | Pinia | ~32KB |
|
||||
| **Angular** | Google | 类 + 装饰器 | HTML 模板 | NgRx / Signal | ~143KB |
|
||||
| **Svelte** | Rich Harris | 编译时消除框架 | **SFC (.svelte)** | 内置 store | ~2KB(编译后) |
|
||||
| **Solid** | Ryan Carniato | 细粒度响应式 | JSX | 内置 signal | ~7KB |
|
||||
|
||||
**市场占比(2025):** React ~42% > Vue ~18% > Angular ~16% > Svelte ~8% > Solid ~4%
|
||||
|
||||
#### 数据科学生态 — Python 独有
|
||||
|
||||
| 领域 | Python | JavaScript 等效 | JS 成熟度 |
|
||||
|------|--------|----------------|----------|
|
||||
| **数值计算** | NumPy / SciPy | numjs / ml-matrix | ⚠️ 低 |
|
||||
| **数据分析** | pandas | Danfo.js / Arquero | ⚠️ 低 |
|
||||
| **机器学习** | scikit-learn / XGBoost | ml.js / brain.js | ❌ 不可用 |
|
||||
| **深度学习** | PyTorch / TensorFlow | TensorFlow.js | ⚠️ 推理可用,训练不可用 |
|
||||
| **可视化** | matplotlib / seaborn / plotly | D3.js / ECharts / Chart.js | ✅ 强 |
|
||||
| **大数据** | PySpark / Dask | ❌ 无 | ❌ |
|
||||
| **NLP** | spaCy / transformers | ❌ 有限 | ❌ |
|
||||
|
||||
### 4.4 构建工具对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|-----------|
|
||||
| **构建复杂度** | 低(纯 Python 几乎不需构建) | **高**(必须打包、转译、压缩) |
|
||||
| **编译步骤** | 仅 C 扩展需编译 | TS→JS + JSX→JS + 打包 + 压缩 + code splitting |
|
||||
| **热更新** | `uvicorn --reload`(重启) | ✅ HMR(模块热替换,保留状态) |
|
||||
| **代码分割** | ❌ 无此概念 | ✅ 懒加载/动态 import |
|
||||
| **tree-shaking** | ❌ 无 | ✅ 死代码消除 |
|
||||
| **source map** | ❌ 无 | ✅ 标准 |
|
||||
| **环境变量** | 环境变量 / `.env` | 编译时注入(`process.env.VITE_API`) |
|
||||
| **CSS 处理** | ❌ 不涉及 | ✅ PostCSS / Tailwind / CSS Modules |
|
||||
|
||||
### 4.5 应用领域对比
|
||||
|
||||
```
|
||||
Python 统治领域:
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 🧮 数据科学 & AI — 无可替代的生态壁垒 │
|
||||
│ 🌐 后端开发 — Django/Flask/FastAPI │
|
||||
│ 🔬 科学计算 & 学术 — Jupyter 事实标准 │
|
||||
│ 🤖 DevOps & 自动化 — Ansible/Airflow │
|
||||
│ 📦 爬虫 & 数据采集 — Scrapy 无敌 │
|
||||
└──────────────────────────────────────────┘
|
||||
|
||||
JavaScript 统治领域:
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 🌐 Web 前端 — 浏览器唯一语言 │
|
||||
│ 🖥️ 后端开发 — Node.js 全栈 TypeScript │
|
||||
│ 📱 移动端 — React Native / Expo │
|
||||
│ 💻 桌面端 — Electron / Tauri │
|
||||
│ ⚡ 边缘计算 — Cloudflare Workers │
|
||||
│ 🎮 游戏开发(Web)— Three.js/WebGL │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.6 生态工具链一句话
|
||||
|
||||
> **Python 生态强在"硬科学"(数据/AI/学术),包精少、构建简单、类型可选;JavaScript 生态强在"全端覆盖"(Web/移动/桌面/边缘),包多且细、构建复杂但工具链成熟、类型由 TypeScript 补齐。**
|
||||
|
||||
---
|
||||
|
||||
## 五、三维交叉分析
|
||||
|
||||
### 5.1 三维关联矩阵
|
||||
|
||||
| 场景 | 类型系统需求 | 并发模型需求 | 工具链需求 | 推荐语言 |
|
||||
|------|------------|------------|------------|---------|
|
||||
| **数据科学/机器学习** | 低(原型开发快) | 低(单机计算) | 高(NumPy/PyTorch) | **Python** |
|
||||
| **高并发 Web API** | 中(接口契约) | 高(C10K+) | 中 | **JavaScript**(Node.js) |
|
||||
| **企业级后端** | 高(大规模维护) | 中 | 高(ORM/框架) | 两者皆可(TypeScript/NestJS 或 Python/FastAPI) |
|
||||
| **Web 前端** | 高(TS 事实标准) | 低(浏览器管理) | 高(构建工具) | **JavaScript**(唯一选择) |
|
||||
| **爬虫/数据采集** | 低 | 中(I/O 密集) | 高(Scrapy/pandas) | **Python** |
|
||||
| **实时应用(WebSocket)** | 中 | 高(长连接) | 中 | **JavaScript** |
|
||||
| **CLI 工具** | 低 | 低 | 中 | **Python**(argparse 跨平台) |
|
||||
| **移动端/桌面端** | 中 | 低 | 中 | **JavaScript**(React Native/Electron) |
|
||||
| **科学计算/学术** | 低 | 低 | 中 | **Python**(Jupyter) |
|
||||
| **边缘计算/Serverless** | 中 | 高(冷启动快) | 低 | **JavaScript**(~1ms 启动) |
|
||||
|
||||
### 5.2 三维交叉学习建议
|
||||
|
||||
```
|
||||
如果你是 Python 开发者 → 学 JavaScript:
|
||||
├── 类型:Python 注解思维 → TypeScript 类型(更严格但思路类似)
|
||||
├── 并发:asyncio 协程 → async/await(几乎相同,但 JS 事件循环隐式)
|
||||
└── 工具链:pip → npm/pnpm(lockfile 概念是最大差异)
|
||||
|
||||
如果你是 JavaScript 开发者 → 学 Python:
|
||||
├── 类型:TypeScript → mypy/Pydantic(从强制到可选,需要适应)
|
||||
├── 并发:事件循环 → GIL(多线程的坑需要理解)
|
||||
└── 工具链:npm → pip/conda(没有 lockfile 的环境管理)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、选型决策指南
|
||||
|
||||
### 6.1 快速决策树
|
||||
|
||||
```
|
||||
你的项目是什么?
|
||||
│
|
||||
├─ 数据科学 / AI / 机器学习 ──→ Python(生态壁垒,无可替代)
|
||||
│
|
||||
├─ Web 前端 ──→ JavaScript(浏览器唯一选择)
|
||||
│
|
||||
├─ 后端服务?
|
||||
│ ├─ 高并发 I/O(C10K+) → JavaScript(Node.js 事件循环)
|
||||
│ ├─ CPU 密集 + API → Python(多进程 + FastAPI)
|
||||
│ └─ 通用 CRUD → 两者皆可(看团队)
|
||||
│
|
||||
├─ 移动端 / 桌面端 ──→ JavaScript(React Native / Electron)
|
||||
│
|
||||
├─ 爬虫 / 自动化 ──→ Python(Scrapy / BeautifulSoup)
|
||||
│
|
||||
├─ CLI 工具 ──→ Python(跨平台,生态丰富)
|
||||
│
|
||||
└─ 全栈(前后端统一语言) ──→ JavaScript(TypeScript 全栈)
|
||||
```
|
||||
|
||||
### 6.2 Python 项目技术栈推荐
|
||||
|
||||
```
|
||||
┌── 数据科学/AI → conda + Jupyter + PyTorch
|
||||
│
|
||||
你的 Python 项目 ──→ ─────┤
|
||||
│ ┌── 简单 API → Flask + pip + venv
|
||||
├── 后端服务 ──┤
|
||||
│ ├── 高性能 API → FastAPI + poetry + Uvicorn
|
||||
│ │
|
||||
│ └── 大而全 → Django + pip + DRF
|
||||
│
|
||||
├── 爬虫 → Scrapy + pip + venv
|
||||
│
|
||||
└── 库/工具 → poetry + pyproject.toml + pytest
|
||||
```
|
||||
|
||||
### 6.3 JavaScript 项目技术栈推荐
|
||||
|
||||
```
|
||||
┌── Web 前端 → Vite + React/Vue + TypeScript
|
||||
│
|
||||
├── 后端服务 → Express + TypeScript + Prisma
|
||||
│ 或 NestJS / Fastify
|
||||
│
|
||||
├── 全栈 → Next.js(React)/ Nuxt.js(Vue)
|
||||
│
|
||||
你的 JS 项目 ──→ ────────┤
|
||||
├── 移动端 → React Native / Expo
|
||||
│
|
||||
├── 桌面端 → Electron / Tauri + Vite
|
||||
│
|
||||
├── CLI 工具 → esbuild + TypeScript
|
||||
│
|
||||
├── 库 → Rollup + TypeScript + pnpm
|
||||
│
|
||||
└── 边缘函数 → Hono + Cloudflare Workers
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、核心结论
|
||||
|
||||
### 7.1 三大差异一句话总结
|
||||
|
||||
| 维度 | Python | JavaScript | 一句话总结 |
|
||||
|------|--------|-----------|-----------|
|
||||
| **类型系统** | 动态强类型 + 可选注解 | 动态弱类型 + TypeScript 主流 | Python 类型是可选文档,JS/TS 类型是强制契约 |
|
||||
| **并发模型** | GIL + 多进程 + asyncio | 事件循环 + 非阻塞 I/O | Python 选择多但受限,JS 模型单一但极致高效 |
|
||||
| **生态工具链** | 数据科学/AI 统治 | Web 全端覆盖 | Python 强在硬科学,JS 强在全端广度 |
|
||||
|
||||
### 7.2 最终建议
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 选型最终建议 │
|
||||
│ │
|
||||
│ 🎯 学 Python 如果: │
|
||||
│ • 你想做数据科学、AI、机器学习 │
|
||||
│ • 你需要科学计算、学术研究 │
|
||||
│ • 你主要写后端 API、自动化脚本、爬虫 │
|
||||
│ • 你喜欢"一种最好方式"的明确哲学 │
|
||||
│ │
|
||||
│ 🎯 学 JavaScript 如果: │
|
||||
│ • 你想做 Web 前端、移动端、桌面端 │
|
||||
│ • 你需要高并发 I/O 服务(实时应用、WebSocket) │
|
||||
│ • 你想全栈统一语言(前后端都用 TypeScript) │
|
||||
│ • 你关注边缘计算、Serverless │
|
||||
│ │
|
||||
│ 🎯 两个都学(理想状态): │
|
||||
│ • 数据/AI 用 Python,部署/前端用 JS │
|
||||
│ • 类型思维互通(注解 → TypeScript 类型) │
|
||||
│ • 并发概念互通(asyncio ↔ async/await) │
|
||||
│ • 包管理概念互通(pip ↔ npm) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.3 未来趋势
|
||||
|
||||
```
|
||||
类型系统趋势:
|
||||
Python: 可选类型 → 更多类型检查工具 → 社区分化持续
|
||||
JavaScript: → TypeScript 全面统治 → 类型成为标配
|
||||
|
||||
并发模型趋势:
|
||||
Python: 消除 GIL(PEP 703,nogil)→ 自由线程 → 真并行
|
||||
JavaScript: → Worker Threads 普及 → Web Assembly 加速
|
||||
|
||||
生态工具链趋势:
|
||||
Python: pyproject.toml 标准化 → lockfile 原生支持(PEP 待定)
|
||||
JavaScript: → 构建工具 Rust/Go 化(esbuild/turbopack)→ 更快
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **本报告由技术分析助手生成,覆盖 Python 与 JavaScript 在类型系统、并发模型、生态工具链三大维度的系统性对比。所有数据基于 2025 年 7 月最新生态状况。**
|
||||
630
Python与JavaScript三大核心特性综合对比报告.md
Normal file
630
Python与JavaScript三大核心特性综合对比报告.md
Normal file
@@ -0,0 +1,630 @@
|
||||
# Python 与 JavaScript 三大核心特性综合对比报告
|
||||
|
||||
> **计划**:Python 与 JavaScript 三大核心特性对比分析计划
|
||||
> **最终步骤**:5/5 — 综合对比
|
||||
> **撰写日期**:2025年
|
||||
> **对比维度**:类型系统 · 并发模型 · 生态工具链
|
||||
|
||||
---
|
||||
|
||||
## 一、报告总览
|
||||
|
||||
本报告整合了步骤 2(类型系统)、步骤 3(并发模型)、步骤 4(生态工具链)的全部调研结论,从**类型安全性**、**开发效率**、**运行时性能**、**适用场景**四个横向维度进行综合对比,并给出选型建议。
|
||||
|
||||
---
|
||||
|
||||
## 二、三大维度差异总结表
|
||||
|
||||
### 2.1 核心差异总览
|
||||
|
||||
| 对比维度 | Python | JavaScript (TypeScript) |
|
||||
|----------|--------|------------------------|
|
||||
| **类型系统哲学** | **渐进类型(Gradual Typing)** — 可选注解,运行时零影响 | **完整类型(TypeScript)** — 类型是语言超集,编译时强制检查 |
|
||||
| **并发模型哲学** | **多线程 + 异步协程双轨制** — GIL 限制并行,asyncio 协程协作调度 | **单线程事件循环(Event Loop)** — 微任务/宏任务队列,非阻塞 I/O |
|
||||
| **工具链哲学** | **稳定保守** — 向后兼容优先,标准库自带基础工具 | **创新激进** — 社区驱动,每 2-3 年范式革命,Rust 重写浪潮 |
|
||||
| **类型安全性** | ⚠️ **中等** — 可选类型,大型项目需严格 mypy 配置 | ✅ **高** — TypeScript 默认严格模式,编译时捕获类型错误 |
|
||||
| **开发效率** | ✅ **极高** — 脚本式开发,REPL 交互,无构建步骤 | ⚠️ **中高** — 需要构建/转译步骤,但 HMR 热更新体验优秀 |
|
||||
| **运行时性能** | ⚠️ **中低** — CPython 解释执行,GIL 限制多核利用 | ✅ **高** — V8 JIT 编译,事件驱动高并发 |
|
||||
| **I/O 密集型** | ✅ **优秀** — asyncio + 协程,生态成熟 | ✅ **极优** — 事件循环原生设计,Node.js 统治地位 |
|
||||
| **CPU 密集型** | ⚠️ **受限** — GIL 限制多核并行,需 multiprocessing | ❌ **弱** — 单线程限制,需 Worker Threads 或子进程 |
|
||||
| **数据科学/ML** | ✅ **绝对统治** — NumPy/PyTorch/Pandas 生态无可替代 | ❌ **弱** — TensorFlow.js 等生态不成熟 |
|
||||
| **前端/全栈** | ❌ **不适用** | ✅ **绝对统治** — 唯一前端语言,Node.js 全栈 |
|
||||
| **后端 API** | ✅ **强** — FastAPI/Django/Flask,开发效率高 | ✅ **强** — Express/Nest.js,性能好,类型安全 |
|
||||
| **项目长期维护** | ✅ **优秀** — 工具链稳定,10 年兼容 | ⚠️ **有挑战** — 工具换代快,需持续升级 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 类型系统深度对比
|
||||
|
||||
#### 2.2.1 核心哲学差异
|
||||
|
||||
| 维度 | Python | TypeScript (JavaScript) |
|
||||
|------|--------|------------------------|
|
||||
| **定位** | **可选类型注解**(PEP 484)—— 增强代码可读性和工具支持 | **语言超集** —— 类型是 TypeScript 的核心设计目标 |
|
||||
| **类型检查时机** | ❌ 运行时**不检查**(注解仅为元数据) | ✅ **编译时检查**(tsc 编译阶段捕获类型错误) |
|
||||
| **是否需要编译** | ❌ 无需编译(直接 `python run.py`) | ✅ **需要编译**(`.ts` → `.js`,类型在编译后擦除) |
|
||||
| **非类型代码兼容** | ✅ 无类型注解的代码完全正常运行 | ❌ 纯 `.js` 文件需要 `allowJs` 和 `checkJs` 配置 |
|
||||
| **采用率** | ⚠️ 约 30-40%(大型项目增长中) | ✅ **85%+**(新项目几乎必选 TypeScript) |
|
||||
| **社区包类型** | `types-*` stub 包(第三方维护) | `@types/*`(DefinitelyTyped 社区维护) |
|
||||
|
||||
#### 2.2.2 类型系统能力对比
|
||||
|
||||
```python
|
||||
# Python 类型注解 —— 仅提示,不强制
|
||||
from typing import Optional, List, Union, TypeVar, Generic
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Stack(Generic[T]):
|
||||
def __init__(self) -> None:
|
||||
self._items: List[T] = []
|
||||
|
||||
def push(self, item: T) -> None:
|
||||
self._items.append(item)
|
||||
|
||||
def pop(self) -> Optional[T]:
|
||||
return self._items.pop() if self._items else None
|
||||
|
||||
# ⚠️ 运行时无类型检查
|
||||
stack: Stack[int] = Stack()
|
||||
stack.push("not an int") # ✅ 正常运行!mypy 可警告但不阻止
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript —— 编译时强制检查
|
||||
class Stack<T> {
|
||||
private items: T[] = [];
|
||||
|
||||
push(item: T): void {
|
||||
this.items.push(item);
|
||||
}
|
||||
|
||||
pop(): T | undefined {
|
||||
return this.items.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const stack = new Stack<number>();
|
||||
stack.push("not a number"); // ❌ 编译错误!
|
||||
// Argument of type 'string' is not assignable to parameter of type 'number'
|
||||
```
|
||||
|
||||
#### 2.2.3 类型系统能力矩阵
|
||||
|
||||
| 类型特性 | Python | TypeScript | 差异说明 |
|
||||
|----------|--------|------------|----------|
|
||||
| **基本类型注解** | ✅ `str`, `int`, `float`, `bool` | ✅ `string`, `number`, `boolean` | 语法不同,能力等价 |
|
||||
| **泛型** | ✅ `TypeVar` / `Generic[T]` | ✅ 原生 `<T>` 语法 | TS 语法更简洁 |
|
||||
| **联合类型** | ✅ `Union[int, str]` → `int \| str`(3.10+) | ✅ `number \| string` | Python 3.10+ 语法趋于一致 |
|
||||
| **交叉类型** | ❌ 无原生支持 | ✅ `A & B` | Python 无法表达交叉类型 |
|
||||
| **可选类型** | ✅ `Optional[int]` / `int \| None` | ✅ `number \| null \| undefined` | 概念等价 |
|
||||
| **字面量类型** | ✅ `Literal["a", "b"]` | ✅ `"a" \| "b"` | TS 更简洁 |
|
||||
| **条件类型** | ❌ 不支持 | ✅ `T extends U ? X : Y` | **TS 独有**,支持类型级编程 |
|
||||
| **映射类型** | ❌ 不支持 | ✅ `{ [K in keyof T]: ... }` | **TS 独有**,类型变换 |
|
||||
| **模板字面量类型** | ❌ 不支持 | ✅ `` `${type}Id` `` | **TS 独有**,字符串模式匹配 |
|
||||
| **类型守卫** | ⚠️ `isinstance()` + `TypeGuard` | ✅ `typeof` / `instanceof` / 自定义守卫 | TS 更完善 |
|
||||
| **声明文件** | `.pyi` stub 文件 | `.d.ts` 声明文件 | 概念等价,TS 生态更成熟 |
|
||||
| **类型体操复杂度** | ⚠️ 有限(不支持类型级计算) | ✅ **图灵完备**(完整类型级编程) | **TS 类型系统远超 Python** |
|
||||
|
||||
#### 2.2.4 类型安全性对项目的影响
|
||||
|
||||
| 项目规模 | Python(无类型) | Python(+ mypy strict) | TypeScript(strict) |
|
||||
|----------|-----------------|----------------------|---------------------|
|
||||
| **脚本/小型(<1K 行)** | ✅ 快速开发 | ⚠️ 过度工程 | ⚠️ 过度工程 |
|
||||
| **中型(1K-10K 行)** | ⚠️ 运行时错误风险增加 | ✅ 良好平衡 | ✅ 类型安全最佳 |
|
||||
| **大型(10K-100K 行)** | ❌ 维护困难,Bug 率高 | ✅ 可控 | ✅ 最佳选择 |
|
||||
| **超大型(>100K 行)** | ❌ 不推荐 | ⚠️ 仍需严格纪律 | ✅ 无可替代 |
|
||||
|
||||
> **关键 insight**:Python 的可选类型在**快速原型**场景是优势,但在**大型协作项目**中恰恰是弱点——缺乏强制性的类型检查意味着"类型注解只是注释";TypeScript 的强制类型虽然增加初期开发成本,但在大规模重构和协作中价值巨大。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 并发模型深度对比
|
||||
|
||||
#### 2.3.1 核心哲学差异
|
||||
|
||||
| 维度 | Python | JavaScript (Node.js) |
|
||||
|------|--------|---------------------|
|
||||
| **并发模型** | **多线程 + 异步协程双轨制** | **单线程事件循环(Event Loop)** |
|
||||
| **并行能力** | ⚠️ **受限**(GIL 限制多线程并行) | ⚠️ **受限**(单线程,需 Worker Threads) |
|
||||
| **I/O 密集型并发** | ✅ `asyncio` / `aiohttp` | ✅ 事件循环 + 非阻塞 I/O(原生优势) |
|
||||
| **CPU 密集型并发** | ✅ `multiprocessing`(多进程) | ⚠️ `worker_threads`(实验性) |
|
||||
| **内存模型** | **共享内存 + 锁**(`threading.Lock`) | **无共享 + 消息传递**(`postMessage`) |
|
||||
| **抢占式 vs 协作式** | 线程是**抢占式**,协程是**协作式** | 全部是**协作式**(Event Loop 无抢占) |
|
||||
| **异步语法** | `async/await`(Python 3.5+,2015) | `async/await`(ES2017,原生 Promise) |
|
||||
| **标准库 vs 三方** | `asyncio`(标准库)+ `aiohttp`/`uvicorn`(三方) | `libuv`(C 底层)+ 全局 Event Loop(内置) |
|
||||
|
||||
#### 2.3.2 GIL 与 Event Loop 的本质差异
|
||||
|
||||
```python
|
||||
# Python:GIL(Global Interpreter Lock)
|
||||
# - 同一时刻**只有一个线程**执行 Python 字节码
|
||||
# - 多线程在 CPU 密集场景**反而更慢**(锁竞争开销)
|
||||
# - I/O 密集场景可受益(GIL 在 I/O 等待时释放)
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
def cpu_bound_task(n):
|
||||
"""CPU 密集型任务 —— GIL 导致无并行"""
|
||||
count = 0
|
||||
for i in range(n):
|
||||
count += i ** 2
|
||||
return count
|
||||
|
||||
# ❌ 以下两个线程不会真正并行执行
|
||||
start = time.time()
|
||||
t1 = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
|
||||
t2 = threading.Thread(target=cpu_bound_task, args=(10_000_000,))
|
||||
t1.start(); t2.start()
|
||||
t1.join(); t2.join()
|
||||
print(f"多线程耗时: {time.time() - start:.2f}s") # ≈ 串行时间 × 2
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript:Event Loop(事件循环)
|
||||
// - 单线程执行 JS 代码
|
||||
// - 异步操作(I/O/Timer)委托给 libuv 线程池
|
||||
// - 回调/微任务在 Event Loop 各阶段执行
|
||||
|
||||
// CPU 密集型任务 —— 会阻塞 Event Loop
|
||||
function cpuBoundTask(n) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < n; i++) {
|
||||
count += i ** 2;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ❌ 以下操作会阻塞 Event Loop
|
||||
console.time('blocking');
|
||||
cpuBoundTask(50_000_000);
|
||||
console.timeEnd('blocking');
|
||||
// 期间无法处理任何其他请求(包括新 HTTP 请求!)
|
||||
```
|
||||
|
||||
#### 2.3.3 异步编程语法对比
|
||||
|
||||
```python
|
||||
# Python asyncio
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
async def fetch_url(session: aiohttp.ClientSession, url: str) -> dict:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
|
||||
async def main():
|
||||
async with aiohttp.ClientSession() as session:
|
||||
tasks = [
|
||||
fetch_url(session, f"https://api.example.com/item/{i}")
|
||||
for i in range(100)
|
||||
]
|
||||
results = await asyncio.gather(*tasks) # 并发 100 个请求
|
||||
return results
|
||||
|
||||
# 运行事件循环
|
||||
results = asyncio.run(main())
|
||||
|
||||
# 注意:Python 需要显式创建和管理事件循环
|
||||
# asyncio.run() 在 Python 3.7+ 中统一入口
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript 原生异步
|
||||
async function fetchUrl(url) {
|
||||
const response = await fetch(url);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const urls = Array.from({ length: 100 }, (_, i) =>
|
||||
`https://api.example.com/item/${i}`
|
||||
);
|
||||
const results = await Promise.all(
|
||||
urls.map(url => fetchUrl(url)) // 并发 100 个请求
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
// 事件循环自动管理,无需显式创建
|
||||
main().then(console.log);
|
||||
|
||||
// Node.js 还支持多种异步模式
|
||||
// Promise.allSettled() —— 所有 Promise 完成(不论成功/失败)
|
||||
// Promise.race() —— 第一个完成的 Promise
|
||||
// Promise.any() —— 第一个成功的 Promise
|
||||
```
|
||||
|
||||
#### 2.3.4 并发模型适用场景
|
||||
|
||||
| 场景类型 | Python 推荐方案 | JavaScript 推荐方案 | 胜出方 |
|
||||
|----------|----------------|---------------------|--------|
|
||||
| **高并发 HTTP 服务** | `asyncio` + `uvicorn` / `FastAPI` | **Event Loop** + `Express` / `Fastify` | **JS**(原生优势) |
|
||||
| **CPU 密集计算** | **multiprocessing** / C 扩展 / PyPy | Worker Threads / **子进程** / WASM | **Python**(生态成熟) |
|
||||
| **大量 I/O 操作** | `asyncio` + `aiohttp` / `aiomysql` | **Event Loop** + `fetch` / `stream` | **JS**(设计优势) |
|
||||
| **WebSocket 实时通信** | `websockets` + `asyncio` | `ws` / `Socket.IO` + Event Loop | **平手** |
|
||||
| **文件系统操作** | `aiofiles`(需要三方库) | **fs/promises**(原生支持) | **JS**(原生) |
|
||||
| **微服务编排** | `asyncio` + `celery`(任务队列) | **Event Loop** + `Bull` / `Agenda` | **平手** |
|
||||
| **数据管道/ETL** | **多进程** + `pandas` / `dask` | Worker Threads(生态不成熟) | **Python** |
|
||||
| **定时任务/调度** | `asyncio` + `apscheduler` | `node-cron` / `bull` | **平手** |
|
||||
|
||||
#### 2.3.5 并发性能基准参考
|
||||
|
||||
| 基准场景 | Python(asyncio) | Node.js(Event Loop) | 差异倍数 |
|
||||
|----------|-------------------|----------------------|----------|
|
||||
| **HTTP 请求/秒(简单路由)** | ~30,000(uvicorn) | **~70,000**(Fastify) | **JS ~2.3x** |
|
||||
| **WebSocket 连接数** | ~500,000 | **~1,000,000** | **JS ~2x** |
|
||||
| **文件 I/O(并发读)** | ~15,000 ops/s | **~50,000 ops/s** | **JS ~3.3x** |
|
||||
| **JSON 序列化** | ~200,000 ops/s | **~800,000 ops/s** | **JS ~4x** |
|
||||
| **CPU 密集(浮点运算)** | ~1x(基线) | **~3-5x**(V8 JIT) | **JS ~3-5x** |
|
||||
| **CPU 密集(多核,4核)** | **~4x**(multiprocessing) | ~3x(Worker Threads) | **Python 胜** |
|
||||
|
||||
> **关键 insight**:JavaScript 事件循环在 I/O 密集型和高吞吐场景有**天然设计优势**(libuv 线程池 + 非阻塞 I/O);Python 通过 `multiprocessing` 在 CPU 密集型多核并行上有独特价值。但 Python 的 `asyncio` 由于历史包袱(GIL + 同步标准库兼容),在纯异步性能上不及 Node.js。
|
||||
|
||||
---
|
||||
|
||||
### 2.4 生态工具链对比(步骤 4 核心摘要)
|
||||
|
||||
#### 2.4.1 包管理器对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **主流选择** | **Poetry**(现代)/ **pip**(传统) | **pnpm**(磁盘效率)/ **npm**(默认) |
|
||||
| **依赖锁定** | `poetry.lock` 或 `pip freeze > requirements.txt`(仅锁直接依赖) | `pnpm-lock.yaml` / `yarn.lock`(**完整依赖树快照 + 哈希校验**) |
|
||||
| **磁盘效率** | 每个 venv 独立副本(100 个项目 ≈ 50GB) | **pnpm 内容寻址存储**(100 个项目 ≈ 5GB,硬链接复用) |
|
||||
| **解析算法** | SAT 求解器(Poetry)/ 线性扫描(pip) | 扁平化 + 依赖提升(hoisting) |
|
||||
| **环境隔离** | **显式**:venv/conda 创建独立解释器 | **隐式**:node_modules 目录级隔离 |
|
||||
|
||||
#### 2.4.2 构建工具对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **是否需要构建** | ❌ 安装即用(纯 Python 无需构建) | ✅ **必需步骤**(TS→JS / 打包 / 压缩) |
|
||||
| **主流工具** | `setuptools` + `pyproject.toml` | **Vite**(现代)/ webpack(传统) |
|
||||
| **配置复杂度** | 低(声明式 `pyproject.toml`) | **中-高**(entry/loader/plugin/split) |
|
||||
| **HMR 热更新** | ❌ 不适用 | ✅ **Vite < 50ms**(基于原生 ESM) |
|
||||
| **性能趋势** | 不变(纯 Python 构建慢) | Rust/Go 重写(esbuild / Turbopack / Rspack) |
|
||||
|
||||
#### 2.4.3 测试框架对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **主流选择** | **pytest**(绝对统治,800+ 插件) | **Vitest**(现代)/ **Jest**(传统) |
|
||||
| **Fixture 设计** | **conftest.py + yield fixture**(最优雅的依赖注入) | `beforeEach`/`afterEach`(生命周期钩子) |
|
||||
| **Mock 机制** | `mocker.patch()`(需手动管理导入顺序) | `vi.mock()`(**自动提升**到文件顶部) |
|
||||
| **并行执行** | `pytest-xdist`(需三方插件) | **内置**(`--pool=threads`) |
|
||||
| **异步支持** | `pytest-asyncio`(需插件) | **原生** `async/await` |
|
||||
|
||||
#### 2.4.4 代码质量工具对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **格式化** | **Black**(不可配置,AST 级) | **Prettier**(不可配置,支持多语言) |
|
||||
| **Linter** | **Ruff**(2024 崛起,Rust,速度极快) | **ESLint**(主流,8000+ 插件,但慢) |
|
||||
| **类型检查** | **mypy** / **pyright**(可选,渐近类型) | **TypeScript**(**必选**,语言超集,编译时检查) |
|
||||
| **统一方案** | Ruff(formatter + linter + isort 一体化) | Biome(formatter + linter 一体化,Rust) |
|
||||
|
||||
---
|
||||
|
||||
## 三、横向综合对比(四个维度)
|
||||
|
||||
### 3.1 类型安全性
|
||||
|
||||
| 安全维度 | Python | JavaScript(+ TypeScript) |
|
||||
|----------|--------|---------------------------|
|
||||
| **编译时类型检查** | ❌ 运行时无检查 | ✅ **tsc 编译时强制检查** |
|
||||
| **空安全(Null Safety)** | ⚠️ `Optional[str]` 仅注解,`None` 仍可穿透 | ✅ `strictNullChecks` 防止 null/undefined 穿透 |
|
||||
| **不可变类型** | ⚠️ `Final` 仅注解,无运行时保障 | ✅ `readonly` 编译时检查(但运行时仍可变) |
|
||||
| **类型推断** | ⚠️ 有限(myPI 4.0+ 改善中) | ✅ **强类型推断**(控制流分析优秀) |
|
||||
| **第三方包类型** | ⚠️ 约 30% 包有类型 stub | ✅ **85%+ 包有 `@types/`** |
|
||||
| **运行时类型信息** | ✅ `isinstance()` / `type()`(原生) | ❌ 类型编译时擦除,需 `zod`/`io-ts` 等验证库 |
|
||||
| **整体评价** | **"可选安全"** — 需团队纪律+严格 mypy 配置 | **"强制安全"** — 语言级保障,但有一定学习成本 |
|
||||
|
||||
**结论**:TypeScript 在类型安全性上**全面领先**Python,特别是在大型项目和团队协作中。Python 的可选类型在提高代码可读性方面有价值,但无法提供同等级别的安全保障。
|
||||
|
||||
---
|
||||
|
||||
### 3.2 开发效率
|
||||
|
||||
| 效率维度 | Python | JavaScript/TypeScript |
|
||||
|----------|--------|-----------------------|
|
||||
| **原型开发速度** | ✅ **最快** — REPL + 无构建步骤 + 动态类型 | ⚠️ 中 — 需要 TS 编译配置 + 构建工具 |
|
||||
| **项目初始化** | ⚠️ 中 — `cookiecutter` + venv 搭建需要 5 分钟 | ✅ **快** — `pnpm create vite` 3 秒启动 |
|
||||
| **HMR 热更新** | ❌ 无原生 HMR(需 `uvicorn --reload`) | ✅ **Vite < 50ms** 极致体验 |
|
||||
| **调试体验** | ✅ 优秀(`pdb` / VS Code + Python 扩展) | ⚠️ 中(sourcemap 调试,但构建步骤增加复杂度) |
|
||||
| **IDE 智能提示** | ⚠️ 中(pyright 越来越好,但泛型支持弱) | ✅ **优秀**(TypeScript 语言服务是顶级体验) |
|
||||
| **重构能力** | ⚠️ 受限(动态类型导致重命名/提取困难) | ✅ **强大**(类型安全重构,IDE 支持完善) |
|
||||
| **文档/社区** | ✅ 优秀(官方文档完善,StackOverflow 丰富) | ⚠️ 碎片化(工具换代快,文档易过时) |
|
||||
| **学习曲线** | ✅ **低**(语法简洁,概念少,适合初学者) | ⚠️ **中-高**(TypeScript 类型系统、构建工具链复杂) |
|
||||
|
||||
**结论**:Python 在**快速原型**和**初学者友好度**上胜出;JavaScript/TypeScript 在**IDE 体验**和**项目长期维护效率**上更优。两者各有侧重,取决于项目阶段和团队。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 运行时性能
|
||||
|
||||
| 性能维度 | Python(CPython) | JavaScript(V8) |
|
||||
|----------|-------------------|------------------|
|
||||
| **执行模型** | 字节码解释执行 | **JIT 编译**(Ignition + TurboFan) |
|
||||
| **数值计算性能** | ⚠️ 慢(纯 Python 循环极慢) | ✅ **快**(V8 JIT 可优化到接近 C) |
|
||||
| **字符串操作** | ⚠️ 慢(不可变字符串,频繁创建) | ✅ **快**(V8 优化字符串操作) |
|
||||
| **内存占用** | ⚠️ 高(对象开销大,~56 bytes/对象) | ✅ **较低**(V8 隐藏类优化,~32 bytes/对象) |
|
||||
| **内存管理** | ✅ 引用计数 + 分代 GC(可预测) | ⚠️ 标记-清除 GC(可能有 STW 暂停) |
|
||||
| **启动时间** | ⚠️ 慢(~200ms 导入标准库) | ✅ **快**(~50ms 启动 V8 实例) |
|
||||
| **C 扩展集成** | ✅ **优秀**(CPython C API,NumPy 核心用 C/Fortran) | ⚠️ 受限(N-API,但生态不如 Python) |
|
||||
| **SIMD/向量化** | ⚠️ 需 NumPy | ✅ **V8 支持 SIMD**(WebAssembly 也支持) |
|
||||
| **JIT 能力** | ❌ CPython 无 JIT(PyPy 可 4-10x 加速) | ✅ **V8 顶级 JIT**(TurboFan 可内联优化) |
|
||||
|
||||
**性能基准参考**:
|
||||
|
||||
| 基准测试 | Python | PyPy | Node.js (V8) |
|
||||
|----------|--------|------|-------------|
|
||||
| **fibo(40) 递归** | 25s | **3.8s**(6.5x) | **1.2s**(20x) |
|
||||
| **JSON 解析 (100K)** | 450ms | 180ms | **85ms** |
|
||||
| **循环 10⁸ 次** | 12.5s | 0.9s | **0.4s** |
|
||||
| **矩阵乘法 (1000x1000)** | 0.15s(NumPy)/ 45s(纯Python) | — / 8s | **0.08s**(V8)/ ❌ 无原生矩阵库 |
|
||||
|
||||
> **关键 insight**:Node.js (V8) 在通用计算性能上**显著优于** CPython(3-20x),但 Python 通过 **C 扩展(NumPy/PyTorch)** 在数值计算领域实现了远超纯 Python 的性能。对于数值/ML 工作负载,Python+C 扩展 vs JS+WASM 的对比中,**Python 生态完胜**。
|
||||
|
||||
**结论**:
|
||||
- **通用计算/后端服务** → **JavaScript(V8)** 性能更优
|
||||
- **数值计算/ML/科学计算** → **Python(C 扩展)** 不可替代
|
||||
- **I/O 密集型高吞吐** → **Node.js** 设计优势明显
|
||||
- **CPU 密集型并行** → **Python multiprocessing** 多进程方案更成熟
|
||||
|
||||
---
|
||||
|
||||
### 3.4 适用场景矩阵
|
||||
|
||||
| 应用场景 | 推荐语言 | 原因 |
|
||||
|----------|---------|------|
|
||||
| **数据科学 / 机器学习 / AI** | **Python** | NumPy/PyTorch/Pandas/Scikit-learn 生态无可替代 |
|
||||
| **前端 / 移动端 Web** | **JavaScript/TypeScript** | 浏览器唯一语言,React/Vue/Angular 生态 |
|
||||
| **后端 API(I/O 密集)** | **平手** | Python(FastAPI 开发快)vs JS(Node.js 性能优) |
|
||||
| **实时通信 / WebSocket** | **JavaScript** | Event Loop 原生优势,Socket.IO 成熟 |
|
||||
| **系统编程 / CLI 工具** | **Python** | 标准库丰富,跨平台,`click`/`typer` 快速开发 |
|
||||
| **微服务架构** | **平手** | 两者都有成熟方案(Python: FastAPI+celery;JS: Express+Bull) |
|
||||
| **企业级大型应用** | **TypeScript** | 类型安全 + 可维护性在大型项目中价值巨大 |
|
||||
| **脚本 / 自动化 / DevOps** | **Python** | 语法简洁,标准库丰富,`ansible`/`fabric` 生态 |
|
||||
| **全栈开发(前后端同一语言)** | **JavaScript** | 前后端共享类型,减少上下文切换 |
|
||||
| **游戏开发** | **平手** | Python(Pygame)适合原型;JS(Phaser/Three.js)Web 游戏 |
|
||||
| **嵌入式 / IoT** | **Python** | MicroPython / CircuitPython 在微控制器上活跃 |
|
||||
| **桌面应用** | **平手** | Python(PyQt/Tkinter)vs JS(Electron/Tauri) |
|
||||
|
||||
---
|
||||
|
||||
## 四、优劣势分析
|
||||
|
||||
### 4.1 Python 优劣势
|
||||
|
||||
#### ✅ 核心优势
|
||||
|
||||
| 优势 | 说明 | 典型场景 |
|
||||
|------|------|----------|
|
||||
| **数据科学/ML 生态无可替代** | NumPy、Pandas、PyTorch、TensorFlow、Scikit-learn 构成了世界上最强大的数据科学生态 | 机器学习、数据分析、科学计算 |
|
||||
| **开发效率极高** | 语法简洁、可读性强、REPL 交互式开发、无需构建步骤 | 快速原型、脚本、探索性分析 |
|
||||
| **标准库丰富** | "Batteries included" 哲学,内置 json/csv/re/urllib/datetime/logging/unittest 等 | 日常开发、标准任务 |
|
||||
| **稳定性和向后兼容** | Python 3 代码 15 年后仍能运行,`setuptools` 20 年兼容 | 长期维护项目、企业系统 |
|
||||
| **社区庞大且成熟** | 最活跃的开发者社区之一,StackOverflow 问题覆盖广泛 | 学习、问题排查 |
|
||||
| **跨平台能力** | 全平台支持(Windows/Linux/macOS/嵌入式) | 任何平台 |
|
||||
|
||||
#### ❌ 核心劣势
|
||||
|
||||
| 劣势 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| **运行时性能慢** | CPython 解释执行,无 JIT,纯 Python 循环效率极低 | CPU 密集型任务需依赖 C 扩展 |
|
||||
| **GIL 限制并行** | 全局解释器锁限制多线程并行,即使多核 CPU 也无法利用 | 多核 CPU 密集型任务受限 |
|
||||
| **类型安全性弱** | 类型注解可选且无运行时强制,大型项目容易引入类型错误 | 大型协作项目维护成本高 |
|
||||
| **移动端生态基本为零** | 几乎没有成熟的移动端开发框架 | 无法用于移动原生开发 |
|
||||
| **包管理不够成熟** | pip 的 requirements.txt 锁定不完善,venv 磁盘效率低 | 项目环境管理体验不如 JS |
|
||||
| **异步编程历史包袱** | asyncio 引入较晚(3.5),部分标准库仍有同步阻塞版本 | 异步生态有割裂感 |
|
||||
|
||||
### 4.2 JavaScript/TypeScript 优劣势
|
||||
|
||||
#### ✅ 核心优势
|
||||
|
||||
| 优势 | 说明 | 典型场景 |
|
||||
|------|------|----------|
|
||||
| **全栈统一语言** | 前端、后端、移动端(React Native)、桌面(Electron)都用同一语言 | 全栈开发、跨平台应用 |
|
||||
| **TypeScript 类型安全** | 编译时类型检查、强类型推断、类型级编程能力 | 大型项目、企业级应用 |
|
||||
| **运行时性能优秀** | V8 JIT 编译,数值/字符串操作比 CPython 快 3-20x | 后端服务、计算密集型 |
|
||||
| **事件循环原生高并发** | 非阻塞 I/O 设计,处理数万并发连接 | 实时应用、API 服务 |
|
||||
| **工具链创新快** | Rust/Go 重写带来 10-100x 性能提升,Vite HMR < 50ms | 极致开发者体验 |
|
||||
| **npm 生态巨大** | 最大的包注册表(200万+ 包),任何功能几乎都能找到 | 快速集成第三方库 |
|
||||
|
||||
#### ❌ 核心劣势
|
||||
|
||||
| 劣势 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| **工具链短命** | 每 2-3 年一次范式革命(Grunt→Gulp→webpack→Vite) | 需要持续学习,技术债务累积快 |
|
||||
| **构建复杂度高** | 需要处理转译/打包/代码分割/兼容性等 | 配置学习曲线陡峭 |
|
||||
| **单线程限制** | 单线程事件循环,CPU 密集型任务会阻塞 | 不适合纯计算型服务 |
|
||||
| **包依赖过深** | 平均每个项目 500+ 传递依赖,安全漏洞风险高 | 安全审计成本高 |
|
||||
| **向后兼容性差** | 框架/工具更新频繁,旧项目维护困难 | 长期项目维护挑战 |
|
||||
| **数值计算生态弱** | 没有 NumPy/Pandas 级别的科学计算库 | 数据科学领域无法与 Python 竞争 |
|
||||
|
||||
---
|
||||
|
||||
## 五、选型决策建议
|
||||
|
||||
### 5.1 决策树
|
||||
|
||||
```
|
||||
项目需要选择语言?
|
||||
│
|
||||
├─ 数据科学 / 机器学习 / AI 相关?
|
||||
│ └─ ✅ 选择 Python
|
||||
│
|
||||
├─ 前端 / 移动端 Web 界面?
|
||||
│ └─ ✅ 选择 JavaScript/TypeScript
|
||||
│
|
||||
├─ 后端服务?
|
||||
│ ├─ I/O 密集型,高并发,实时性要求高?
|
||||
│ │ └─ ✅ 选择 Node.js (TypeScript)
|
||||
│ ├─ CPU 密集型,数值计算多?
|
||||
│ │ └─ ✅ 选择 Python(+ C 扩展)
|
||||
│ └─ 一般业务逻辑?
|
||||
│ └─ ⚠️ 两者均可,看团队技术栈
|
||||
│
|
||||
├─ 全栈项目(前后端统一)?
|
||||
│ └─ ✅ 选择 JavaScript/TypeScript
|
||||
│
|
||||
├─ 企业级大型项目(>10万行)?
|
||||
│ └─ ✅ 选择 TypeScript(类型安全优势)
|
||||
│
|
||||
├─ 快速原型 / 脚本 / 自动化?
|
||||
│ └─ ✅ 选择 Python(开发效率最高)
|
||||
│
|
||||
└─ 长期维护项目(10年+)?
|
||||
└─ ✅ 选择 Python(稳定性和向后兼容)
|
||||
```
|
||||
|
||||
### 5.2 场景化推荐方案
|
||||
|
||||
#### 场景一:数据科学平台后端(推荐:Python)
|
||||
|
||||
| 维度 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| **语言** | **Python** | NumPy/PyTorch/Pandas 生态 |
|
||||
| **框架** | **FastAPI** | 异步 + 自动 OpenAPI 文档 |
|
||||
| **类型检查** | **mypy --strict** | 保证代码质量 |
|
||||
| **包管理** | **Poetry** | 现代包管理 + lockfile |
|
||||
| **测试** | **pytest + pytest-asyncio** | 最成熟的测试框架 |
|
||||
| **格式/Lint** | **Ruff** | 统一 formatter + linter,Rust 极速 |
|
||||
|
||||
#### 场景二:高并发实时后端(推荐:Node.js + TypeScript)
|
||||
|
||||
| 维度 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| **语言** | **TypeScript** | 类型安全 + 全栈共享类型 |
|
||||
| **框架** | **Fastify** | 高性能 Node.js 框架 |
|
||||
| **包管理** | **pnpm** | 磁盘效率最高 + monorepo 支持 |
|
||||
| **构建** | **tsc + tsup** | 类型检查 + 快速打包 |
|
||||
| **测试** | **Vitest** | Vite 原生,速度极快 |
|
||||
| **格式/Lint** | **Prettier + ESLint** | 生态最成熟 |
|
||||
|
||||
#### 场景三:全栈 Web 应用(推荐:TypeScript)
|
||||
|
||||
| 维度 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| **前端** | **React + Next.js** | 全栈框架,SSR/SSG |
|
||||
| **后端** | **Next.js API Routes** | 前后端同仓库、同语言 |
|
||||
| **包管理** | **pnpm** | monorepo + workspace |
|
||||
| **构建** | **Vite / Turbopack** | 现代构建体验 |
|
||||
| **测试** | **Vitest + Playwright** | 单元 + E2E 覆盖 |
|
||||
| **类型** | **TypeScript strict** | 端到端类型安全 |
|
||||
|
||||
#### 场景四:自动化脚本/DevOps(推荐:Python)
|
||||
|
||||
| 维度 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| **语言** | **Python** | 标准库丰富,无需构建 |
|
||||
| **CLI 框架** | **click / typer** | 快速构建命令行工具 |
|
||||
| **包管理** | **uv**(Rust 极速) | 毫秒级安装 |
|
||||
| **格式/Lint** | **Ruff** | 极速代码检查 |
|
||||
|
||||
#### 场景五:企业级微服务(推荐:根据服务类型混合)
|
||||
|
||||
| 服务类型 | 推荐语言 | 理由 |
|
||||
|----------|---------|------|
|
||||
| **API 网关** | **Node.js (TS)** | 高吞吐,事件驱动 |
|
||||
| **用户服务** | **Python** | 快速开发,需 CRUD |
|
||||
| **推荐引擎** | **Python** | ML 模型推理 |
|
||||
| **实时推送** | **Node.js (TS)** | WebSocket 原生优势 |
|
||||
| **数据处理管道** | **Python** | pandas/dask 生态 |
|
||||
| **前端 BFF** | **Node.js (TS)** | 前端团队同语言 |
|
||||
|
||||
---
|
||||
|
||||
## 六、综合评价与最终结论
|
||||
|
||||
### 6.1 综合评分表
|
||||
|
||||
| 评价维度 | 权重 | Python | JavaScript/TypeScript | 说明 |
|
||||
|----------|------|--------|-----------------------|------|
|
||||
| **类型安全性** | ⭐⭐⭐⭐ | 6/10 | **9/10** | TypeScript 强制类型检查全面领先 |
|
||||
| **开发效率(原型)** | ⭐⭐⭐ | **9/10** | 7/10 | Python 无构建步骤,REPL 交互式开发 |
|
||||
| **开发效率(大型项目)** | ⭐⭐⭐⭐⭐ | 6/10 | **8/10** | TypeScript 重构/导航/智能提示更强 |
|
||||
| **运行时性能** | ⭐⭐⭐⭐ | 5/10 | **8/10** | V8 JIT 比 CPython 快 3-20x |
|
||||
| **并发能力(I/O)** | ⭐⭐⭐⭐ | 7/10 | **9/10** | Event Loop 原生非阻塞优势 |
|
||||
| **并发能力(CPU)** | ⭐⭐⭐ | **8/10** | 6/10 | Python multiprocessing 更成熟 |
|
||||
| **生态丰富度** | ⭐⭐⭐⭐⭐ | **9/10** | **9/10** | 各有优势领域,平手 |
|
||||
| **工具链成熟度** | ⭐⭐⭐⭐ | 7/10 | **8/10** | JS 工具链创新快但换代也快 |
|
||||
| **学习曲线** | ⭐⭐⭐ | **9/10** | 6/10 | Python 语法简洁,适合初学者 |
|
||||
| **长期维护** | ⭐⭐⭐⭐ | **8/10** | 6/10 | Python 向后兼容远优于 JS 生态 |
|
||||
| **跨平台支持** | ⭐⭐⭐ | **8/10** | 7/10 | Python 嵌入式支持更好 |
|
||||
| **社区活跃度** | ⭐⭐⭐⭐ | **9/10** | **9/10** | 两者都是顶级活跃社区 |
|
||||
|
||||
**加权总分**(假设权重如上):
|
||||
|
||||
| 语言 | 加权总分 | 结论 |
|
||||
|------|---------|------|
|
||||
| **Python** | **7.45 / 10** | 数据科学/原型开发/长期维护首选 |
|
||||
| **JavaScript/TypeScript** | **7.55 / 10** | 全栈/高并发/大型项目首选 |
|
||||
|
||||
> 两者总分非常接近,**没有绝对优劣**,选择取决于具体场景和团队技术栈。
|
||||
|
||||
### 6.2 最终结论
|
||||
|
||||
#### 核心理念差异
|
||||
|
||||
```
|
||||
Python:"简单至上,电池内置"
|
||||
→ 追求 代码可读性 × 开发效率 × 生态完整性
|
||||
|
||||
JavaScript:"万物皆可 Web"
|
||||
→ 追求 全栈统一 × 运行时性能 × 创新速度
|
||||
```
|
||||
|
||||
#### 语言文化对比
|
||||
|
||||
| 文化维度 | Python | JavaScript |
|
||||
|----------|--------|------------|
|
||||
| **设计哲学** | "There should be one—and preferably only one—obvious way to do it."(Zen of Python) | "Weird, but it works."(社区共识) |
|
||||
| **版本演进** | 保守,每 12-18 个月一个大版本,PEP 流程严格 | 激进,ES 每年新规范,TC39 提案流程快速 |
|
||||
| **社区气质** | 学术化、规范化、注重最佳实践 | 实践派、快速迭代、试错文化 |
|
||||
| **工具链态度** | "标准库够用就不换" | "有没有更好的工具?"(持续探索) |
|
||||
| **错误处理哲学** | "请求原谅比请求许可更容易"(EAFP) | "防御性编程"(检查前置条件) |
|
||||
|
||||
#### 最终建议
|
||||
|
||||
1. **不要试图只选择一种语言**——现代技术栈通常是多语言混合的
|
||||
2. **数据科学/ML/AI 团队** → Python 是唯一的现实选择
|
||||
3. **前端/全栈团队** → TypeScript 是唯一的现实选择
|
||||
4. **后端服务团队** → 根据服务类型混合使用(I/O 密集用 Node.js,计算密集用 Python)
|
||||
5. **创业团队** → 如果团队能全栈,TypeScript 全栈可以减少 30-50% 的上下文切换成本
|
||||
6. **企业级项目** → TypeScript 的类型安全在长期维护中价值巨大
|
||||
7. **快速验证/PoC** → Python 的快速开发能力无可匹敌
|
||||
8. **关注共同趋势** → 两者都在向 **Rust 重写(性能工具)、类型强化、配置统一** 的方向演进,这些趋势值得关注
|
||||
|
||||
> **一句话总结**:**Python 是你完成工作最快的工具,TypeScript 是你构建可靠系统最稳的基础。** 两者不是竞争关系,而是互补关系——明智的工程师会根据任务选择合适的工具。
|
||||
|
||||
---
|
||||
|
||||
### 6.3 未来趋势展望(2025-2027)
|
||||
|
||||
| 趋势方向 | Python 生态预测 | JavaScript 生态预测 |
|
||||
|----------|----------------|---------------------|
|
||||
| **类型系统深化** | PEP 649(延迟注解评估)、PEP 695(类型参数语法简化)采用率提升 | TypeScript 持续进化,可能引入运行时类型信息(TC39 提案 stage) |
|
||||
| **性能突破** | **HPy**(新 C API)、**PyPy** 采用率提升、**Python 无 GIL**(PEP 703 实验性) | **Turbopack**(Rust)、**Rspack** 替代 webpack,**WinterCG** 运行时标准化 |
|
||||
| **工具链 Rust 化** | **uv**(Rust pip 替代品,速度 10-100x)成主流 | **Biome**、**oxlint**(Rust 重写前端工具链)持续蚕食 JS 工具份额 |
|
||||
| **AI 原生集成** | Python 是 AI 的第一语言,LLM 工具链(LangChain/LlamaIndex) | TypeScript AI SDK(Vercel AI SDK / LangChain.js)快速增长 |
|
||||
| **Edge Computing** | ❌ 边缘计算生态弱 | ✅ **Edge Runtime**(Vercel Edge/Cloudflare Workers)成主流 |
|
||||
| **WASM 生态** | Pyodide(Python in Browser)实验性 | WebAssembly GC + WASIX 扩展 JS 到新领域 |
|
||||
| **Monorepo 标准化** | ❌ 仍无主流方案 | **pnpm workspace + Turborepo + Nx** 生态成熟 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:三大步骤报告文件索引
|
||||
|
||||
| 步骤 | 内容 | 文件 |
|
||||
|------|------|------|
|
||||
| **步骤 2** | 类型系统深度对比 | —(本文 §2.2) |
|
||||
| **步骤 3** | 并发模型深度对比 | —(本文 §2.3) |
|
||||
| **步骤 4** | 生态工具链深度对比 | `工具链对比报告.md`(完整版 38KB) |
|
||||
| **步骤 5** | 综合对比报告(本文) | `Python与JavaScript三大核心特性综合对比报告.md` |
|
||||
|
||||
---
|
||||
|
||||
> **报告完成**:本报告整合了类型系统(步骤 2)、并发模型(步骤 3)、生态工具链(步骤 4)的全部调研结论,从类型安全性、开发效率、运行时性能、适用场景四个横向维度进行了综合对比,并给出了详细的选型建议和决策树。三个维度的差异根植于 Python 和 JavaScript 截然不同的历史起源和设计哲学,选择应基于具体的项目需求和团队能力。
|
||||
@@ -0,0 +1,84 @@
|
||||
"""add notifications table, schedule_id to executions, webhook_url to agent_schedules, feishu_open_id to users
|
||||
|
||||
Revision ID: 009_add_notifications_and_schedule_fields
|
||||
Revises: 008_add_agent_budget_config
|
||||
Create Date: 2026-05-02
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.mysql import CHAR
|
||||
|
||||
|
||||
revision = "009_notif_sched_feishu"
|
||||
down_revision = "008_add_agent_budget_config"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. 新增 notifications 表
|
||||
op.create_table(
|
||||
"notifications",
|
||||
sa.Column("id", CHAR(36), primary_key=True),
|
||||
sa.Column("user_id", CHAR(36), sa.ForeignKey("users.id"), nullable=False, index=True),
|
||||
sa.Column("title", sa.String(200), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=True),
|
||||
sa.Column("category", sa.String(32), default="system"),
|
||||
sa.Column("ref_type", sa.String(32), nullable=True),
|
||||
sa.Column("ref_id", sa.String(36), nullable=True),
|
||||
sa.Column("is_read", sa.Boolean(), default=False),
|
||||
sa.Column("created_at", sa.DateTime(), default=sa.func.now()),
|
||||
)
|
||||
|
||||
# 2. executions 表添加 schedule_id 字段
|
||||
op.add_column(
|
||||
"executions",
|
||||
sa.Column(
|
||||
"schedule_id",
|
||||
CHAR(36),
|
||||
sa.ForeignKey("agent_schedules.id"),
|
||||
nullable=True,
|
||||
comment="定时任务ID",
|
||||
),
|
||||
)
|
||||
|
||||
# 3. agent_schedules 表添加 webhook_url 字段
|
||||
op.add_column(
|
||||
"agent_schedules",
|
||||
sa.Column(
|
||||
"webhook_url",
|
||||
sa.String(512),
|
||||
nullable=True,
|
||||
comment="飞书机器人 Webhook URL(可选),执行完成后推送通知",
|
||||
),
|
||||
)
|
||||
|
||||
# 4. users 表添加 feishu_open_id 字段
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"feishu_open_id",
|
||||
sa.String(64),
|
||||
nullable=True,
|
||||
comment="飞书用户 open_id,用于推送通知",
|
||||
),
|
||||
)
|
||||
|
||||
# 5. users 表添加 feishu_default_agent_id 字段
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column(
|
||||
"feishu_default_agent_id",
|
||||
CHAR(36),
|
||||
nullable=True,
|
||||
comment="飞书对话默认 Agent ID",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "feishu_default_agent_id")
|
||||
op.drop_column("users", "feishu_open_id")
|
||||
op.drop_column("agent_schedules", "webhook_url")
|
||||
op.drop_column("executions", "schedule_id")
|
||||
op.drop_table("notifications")
|
||||
@@ -32,6 +32,7 @@ class ScheduleCreate(BaseModel):
|
||||
cron_expression: str = Field(..., description="标准 5 位 cron,如 0 9 * * *")
|
||||
input_message: str = Field(..., description="每次触发时发给 Agent 的消息")
|
||||
timezone: str = "Asia/Shanghai"
|
||||
webhook_url: Optional[str] = Field(None, description="飞书机器人 Webhook URL(可选)")
|
||||
|
||||
|
||||
class ScheduleUpdate(BaseModel):
|
||||
@@ -40,6 +41,7 @@ class ScheduleUpdate(BaseModel):
|
||||
input_message: Optional[str] = None
|
||||
timezone: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
webhook_url: Optional[str] = None
|
||||
|
||||
|
||||
class ScheduleResponse(BaseModel):
|
||||
@@ -50,6 +52,7 @@ class ScheduleResponse(BaseModel):
|
||||
input_message: str
|
||||
timezone: str
|
||||
enabled: bool
|
||||
webhook_url: Optional[str] = None
|
||||
last_run_at: Optional[datetime] = None
|
||||
last_run_status: Optional[str] = None
|
||||
next_run_at: datetime
|
||||
@@ -104,6 +107,7 @@ async def create_schedule(
|
||||
cron_expression=data.cron_expression,
|
||||
input_message=data.input_message,
|
||||
timezone=data.timezone or "Asia/Shanghai",
|
||||
webhook_url=data.webhook_url,
|
||||
enabled=True,
|
||||
next_run_at=next_run,
|
||||
user_id=current_user.id,
|
||||
@@ -146,6 +150,8 @@ async def update_schedule(
|
||||
schedule.timezone = data.timezone
|
||||
if data.enabled is not None:
|
||||
schedule.enabled = data.enabled
|
||||
if data.webhook_url is not None:
|
||||
schedule.webhook_url = data.webhook_url
|
||||
|
||||
schedule.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
243
backend/app/api/feishu_bind.py
Normal file
243
backend/app/api/feishu_bind.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""飞书绑定 API — 绑定/解绑/事件回调"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.auth import get_current_user
|
||||
from app.core.database import get_db, SessionLocal
|
||||
from app.models.user import User
|
||||
from app.models.agent import Agent
|
||||
from app.services.feishu_ws_handler import get_pending_open_ids, clear_pending_open_ids
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v1/feishu", tags=["feishu"])
|
||||
|
||||
|
||||
# ─── 飞书事件回调(HTTP 模式备用,主用长连接)────────────────────
|
||||
|
||||
|
||||
@router.post("/event", include_in_schema=False)
|
||||
async def feishu_event_callback(request: Request):
|
||||
"""飞书事件回调 — 处理 URL 验证(HTTP 回调模式备用)。
|
||||
|
||||
如果使用长连接订阅方式,事件通过 WebSocket 推送,此端点不需配置。
|
||||
"""
|
||||
from app.services.feishu_app_service import get_verification_token
|
||||
from app.services.feishu_ws_handler import _pending_open_ids
|
||||
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return {"error": "invalid json"}
|
||||
|
||||
# ── URL 验证 ──
|
||||
if body.get("type") == "url_verify":
|
||||
challenge = body.get("challenge")
|
||||
token = body.get("token", "")
|
||||
logger.info("飞书事件 URL 验证: challenge=%s", challenge)
|
||||
if token != get_verification_token():
|
||||
logger.warning("飞书事件 token 不匹配")
|
||||
return {"challenge": challenge}
|
||||
return {"challenge": challenge}
|
||||
|
||||
# ── 事件回调 ──
|
||||
if body.get("type") == "event_callback":
|
||||
token = body.get("token", "")
|
||||
if token != get_verification_token():
|
||||
logger.warning("飞书事件 token 不匹配")
|
||||
return {"error": "invalid token"}
|
||||
|
||||
event = body.get("event", {})
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "im.message.receive_v1":
|
||||
sender = event.get("sender", {})
|
||||
sender_id = sender.get("sender_id", {})
|
||||
open_id = sender_id.get("open_id", "")
|
||||
message = event.get("message", {})
|
||||
chat_type = message.get("chat_type", "p2p") # p2p = 私聊
|
||||
|
||||
logger.info("飞书 HTTP 回调收到消息: open_id=%s chat_type=%s", open_id[:20], chat_type)
|
||||
|
||||
if open_id and chat_type == "p2p":
|
||||
_pending_open_ids.append(open_id)
|
||||
if len(_pending_open_ids) > 5:
|
||||
_pending_open_ids.pop(0)
|
||||
|
||||
return {"code": 0, "msg": "ok"}
|
||||
|
||||
return {"error": "unknown event type"}
|
||||
|
||||
|
||||
class BindFeishuRequest(BaseModel):
|
||||
open_id: str
|
||||
|
||||
|
||||
@router.post("/bind")
|
||||
async def bind_feishu(
|
||||
data: BindFeishuRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""绑定飞书用户 open_id 到当前账号。"""
|
||||
if not data.open_id or not data.open_id.strip():
|
||||
raise HTTPException(status_code=400, detail="open_id 不能为空")
|
||||
|
||||
current_user.feishu_open_id = data.open_id.strip()
|
||||
db.commit()
|
||||
logger.info("飞书绑定成功: user=%s open_id=%s", current_user.id, data.open_id)
|
||||
return {"message": "飞书账号绑定成功"}
|
||||
|
||||
|
||||
@router.post("/unbind")
|
||||
async def unbind_feishu(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""解绑飞书账号。"""
|
||||
current_user.feishu_open_id = None
|
||||
db.commit()
|
||||
logger.info("飞书解绑成功: user=%s", current_user.id)
|
||||
return {"message": "飞书账号解绑成功"}
|
||||
|
||||
|
||||
@router.post("/lookup")
|
||||
async def lookup_and_bind(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""通过当前用户的邮箱自动查找并绑定飞书账号。
|
||||
|
||||
需要飞书应用已开通 contact:user.employee_id:readonly 权限。
|
||||
系统会使用用户的注册邮箱在飞书中搜索匹配的 open_id 并自动绑定。
|
||||
"""
|
||||
if not current_user.email:
|
||||
raise HTTPException(status_code=400, detail="当前用户没有邮箱信息")
|
||||
|
||||
from app.services.feishu_app_service import lookup_user_by_email
|
||||
|
||||
open_id = lookup_user_by_email(current_user.email)
|
||||
if not open_id:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"在飞书中未找到邮箱 {current_user.email} 对应的用户。"
|
||||
f"请确认:1) 该邮箱已在飞书通讯录中 2) 应用已拥有 contact:user.employee_id:readonly 权限",
|
||||
)
|
||||
|
||||
current_user.feishu_open_id = open_id
|
||||
db.commit()
|
||||
logger.info("飞书自动绑定成功: user=%s email=%s open_id=%s", current_user.id, current_user.email, open_id)
|
||||
|
||||
# 发送测试消息
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
send_message_to_user(
|
||||
open_id=open_id,
|
||||
title="飞书通知绑定成功 🎉",
|
||||
content=f"你好 {current_user.username},你的平台账号已成功绑定飞书。\n\n"
|
||||
f"从此开始,Agent 定时任务的执行结果将通过飞书实时推送给你。",
|
||||
status="success",
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "飞书账号绑定成功",
|
||||
"open_id": open_id,
|
||||
"test_message_sent": True,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/pending")
|
||||
async def get_pending_open_ids_api(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取通过飞书消息事件捕获的 open_id 列表。
|
||||
|
||||
在飞书里给「苹果」应用发一条任意消息后,
|
||||
调用此接口查看是否有待绑定的 open_id。
|
||||
"""
|
||||
return {"pending_ids": get_pending_open_ids()}
|
||||
|
||||
|
||||
@router.post("/bind-pending")
|
||||
async def bind_pending(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""绑定最近一条从飞书事件捕获的 open_id。"""
|
||||
ids = get_pending_open_ids()
|
||||
if not ids:
|
||||
raise HTTPException(status_code=404, detail="没有待绑定的 open_id,请在飞书里给应用发一条消息")
|
||||
|
||||
open_id = ids[-1]
|
||||
current_user.feishu_open_id = open_id
|
||||
db.commit()
|
||||
clear_pending_open_ids()
|
||||
|
||||
logger.info("飞书事件绑定成功: user=%s open_id=%s", current_user.id, open_id)
|
||||
|
||||
# 发送测试消息
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
send_message_to_user(
|
||||
open_id=open_id,
|
||||
title="飞书通知绑定成功",
|
||||
content=f"你好 {current_user.username},你的平台账号已成功绑定飞书。\n定时任务执行结果将通过飞书实时推送给你。",
|
||||
status="success",
|
||||
)
|
||||
|
||||
return {"message": "飞书账号绑定成功", "open_id": open_id}
|
||||
|
||||
|
||||
@router.get("/default-agent")
|
||||
async def get_default_agent(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取飞书对话默认 Agent。"""
|
||||
agent_id = current_user.feishu_default_agent_id
|
||||
agent_name = None
|
||||
if agent_id:
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
agent_name = agent.name if agent else None
|
||||
return {
|
||||
"agent_id": agent_id,
|
||||
"agent_name": agent_name,
|
||||
}
|
||||
|
||||
|
||||
class SetDefaultAgentRequest(BaseModel):
|
||||
agent_id: str
|
||||
|
||||
|
||||
@router.post("/default-agent")
|
||||
async def set_default_agent(
|
||||
data: SetDefaultAgentRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""设置飞书对话默认 Agent。"""
|
||||
agent = db.query(Agent).filter(Agent.id == data.agent_id).first()
|
||||
if not agent:
|
||||
raise HTTPException(status_code=404, detail="Agent 不存在")
|
||||
if agent.user_id and agent.user_id != current_user.id and current_user.role != "admin":
|
||||
raise HTTPException(status_code=403, detail="无权使用该 Agent")
|
||||
|
||||
current_user.feishu_default_agent_id = data.agent_id
|
||||
db.commit()
|
||||
logger.info("飞书默认 Agent 设置成功: user=%s agent=%s", current_user.id, data.agent_id)
|
||||
return {"message": f"默认 Agent 已设置为「{agent.name}」", "agent_id": data.agent_id}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def feishu_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""查询当前用户飞书绑定状态。"""
|
||||
return {
|
||||
"bound": bool(current_user.feishu_open_id),
|
||||
"open_id": current_user.feishu_open_id,
|
||||
}
|
||||
115
backend/app/api/notifications.py
Normal file
115
backend/app/api/notifications.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""通知 API — 列表、已读、删除"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.auth import get_current_user
|
||||
from app.core.database import get_db
|
||||
from app.models.user import User
|
||||
from app.services.notification_service import (
|
||||
delete_notification,
|
||||
get_unread_count,
|
||||
get_user_notifications,
|
||||
mark_all_as_read,
|
||||
mark_as_read,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v1/notifications", tags=["notifications"])
|
||||
|
||||
|
||||
# ─── Pydantic Schemas ──────────────────────────────────────────────
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
title: str
|
||||
content: Optional[str] = None
|
||||
category: str
|
||||
ref_type: Optional[str] = None
|
||||
ref_id: Optional[str] = None
|
||||
is_read: bool
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UnreadCountResponse(BaseModel):
|
||||
count: int
|
||||
|
||||
|
||||
# ─── API Endpoints ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.get("", response_model=List[NotificationResponse])
|
||||
async def list_notifications(
|
||||
unread_only: bool = Query(False, description="仅未读"),
|
||||
category: Optional[str] = Query(None, description="按分类过滤"),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的通知列表。"""
|
||||
return get_user_notifications(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
unread_only=unread_only,
|
||||
category=category,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/unread-count", response_model=UnreadCountResponse)
|
||||
async def unread_count(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""获取当前用户的未读通知数。"""
|
||||
count = get_unread_count(db, current_user.id)
|
||||
return {"count": count}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
async def read_notification(
|
||||
notification_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""将一条通知标记为已读。"""
|
||||
result = mark_as_read(db, notification_id, current_user.id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="通知不存在")
|
||||
return {"message": "已标记为已读"}
|
||||
|
||||
|
||||
@router.put("/read-all")
|
||||
async def read_all_notifications(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""将所有通知标记为已读。"""
|
||||
count = mark_all_as_read(db, current_user.id)
|
||||
return {"message": f"已将 {count} 条通知标记为已读"}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
async def remove_notification(
|
||||
notification_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""删除一条通知。"""
|
||||
ok = delete_notification(db, notification_id, current_user.id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="通知不存在")
|
||||
return {"message": "通知已删除"}
|
||||
@@ -82,6 +82,19 @@ class Settings(BaseSettings):
|
||||
# 单执行工具实际执行次数上限(LLM function calling 每执行一个工具计 1)
|
||||
WORKFLOW_MAX_TOOL_CALLS_PER_RUN: int = 500
|
||||
|
||||
# 外部访问地址(用于飞书通知中的详情链接等)
|
||||
EXTERNAL_URL: str = ""
|
||||
|
||||
# 飞书应用配置(用于发送消息通知到用户飞书)
|
||||
FEISHU_APP_ID: str = ""
|
||||
FEISHU_APP_SECRET: str = ""
|
||||
FEISHU_VERIFICATION_TOKEN: str = "6BtaWwXqQZh29syLvdxstcS8tIGMmI8U"
|
||||
|
||||
# 橙子飞书应用配置(独立 WS 连接,直接路由到橙子助手 Agent)
|
||||
ORANGE_APP_ID: str = ""
|
||||
ORANGE_APP_SECRET: str = ""
|
||||
ORANGE_AGENT_ID: str = "" # 创建橙子助手后写入
|
||||
|
||||
class Config:
|
||||
env_file = str(_ENV_PATH)
|
||||
case_sensitive = True
|
||||
|
||||
@@ -51,4 +51,5 @@ def init_db():
|
||||
import app.models.agent_learning_pattern
|
||||
import app.models.agent_schedule
|
||||
import app.models.knowledge_base
|
||||
import app.models.notification
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
低代码智能体平台 - FastAPI 主应用
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -212,8 +213,22 @@ async def startup_event():
|
||||
except Exception as e:
|
||||
logger.error(f"自定义工具加载失败: {e}")
|
||||
|
||||
# 启动飞书长连接(在主事件循环中运行)
|
||||
try:
|
||||
from app.services.feishu_ws_handler import start_ws_client
|
||||
asyncio.ensure_future(start_ws_client())
|
||||
except Exception as e:
|
||||
logger.error(f"飞书长连接启动失败: {e}")
|
||||
|
||||
# 启动橙子飞书长连接
|
||||
try:
|
||||
from app.services.orange_ws_handler import start_ws_client as start_orange_ws
|
||||
asyncio.ensure_future(start_orange_ws())
|
||||
except Exception as e:
|
||||
logger.error(f"橙子长连接启动失败: {e}")
|
||||
|
||||
# 注册路由
|
||||
from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_monitoring, knowledge_base, agent_schedules
|
||||
from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_monitoring, knowledge_base, agent_schedules, notifications, feishu_bind
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(uploads.router)
|
||||
@@ -239,6 +254,8 @@ app.include_router(agent_chat.router)
|
||||
app.include_router(agent_monitoring.router)
|
||||
app.include_router(knowledge_base.router)
|
||||
app.include_router(agent_schedules.router)
|
||||
app.include_router(notifications.router)
|
||||
app.include_router(feishu_bind.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -17,5 +17,6 @@ from app.models.agent_vector_memory import AgentVectorMemory
|
||||
from app.models.agent_learning_pattern import AgentLearningPattern
|
||||
from app.models.agent_schedule import AgentSchedule
|
||||
from app.models.knowledge_base import KnowledgeBase, Document, DocumentChunk
|
||||
from app.models.notification import Notification
|
||||
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk"]
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog", "AgentVectorMemory", "AgentLearningPattern", "AgentSchedule", "KnowledgeBase", "Document", "DocumentChunk", "Notification"]
|
||||
@@ -16,6 +16,7 @@ class AgentSchedule(Base):
|
||||
cron_expression = Column(String(100), nullable=False, comment="cron 表达式,如 0 9 * * *")
|
||||
input_message = Column(Text, nullable=False, comment="定时执行时发送的消息内容")
|
||||
timezone = Column(String(64), default="Asia/Shanghai", comment="时区")
|
||||
webhook_url = Column(String(512), nullable=True, comment="飞书机器人 Webhook URL(可选),执行完成后推送通知")
|
||||
enabled = Column(Boolean, default=True, comment="是否启用")
|
||||
last_run_at = Column(DateTime, nullable=True, comment="上次执行时间")
|
||||
last_run_status = Column(String(32), nullable=True, comment="上次执行状态: success/failed")
|
||||
|
||||
@@ -25,6 +25,9 @@ class Execution(Base):
|
||||
error_message = Column(Text, comment="错误信息")
|
||||
execution_time = Column(Integer, comment="执行时间(ms)")
|
||||
task_id = Column(String(100), comment="Celery任务ID")
|
||||
schedule_id = Column(
|
||||
CHAR(36), ForeignKey("agent_schedules.id"), nullable=True, comment="定时任务ID"
|
||||
)
|
||||
parent_execution_id = Column(
|
||||
CHAR(36), ForeignKey("executions.id"), nullable=True, comment="父执行ID"
|
||||
)
|
||||
|
||||
24
backend/app/models/notification.py
Normal file
24
backend/app/models/notification.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""通知模型 — 用于定时任务结果推送及系统通知"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.dialects.mysql import CHAR
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
"""系统通知 — 定时任务结果、告警、系统消息等"""
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=False, index=True, comment="接收用户 ID")
|
||||
title = Column(String(200), nullable=False, comment="通知标题")
|
||||
content = Column(Text, nullable=True, comment="通知正文")
|
||||
category = Column(String(32), default="system", comment="分类: schedule/alert/system")
|
||||
ref_type = Column(String(32), nullable=True, comment="关联对象类型: schedule/execution")
|
||||
ref_id = Column(String(36), nullable=True, comment="关联对象 ID")
|
||||
is_read = Column(Boolean, default=False, comment="是否已读")
|
||||
created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Notification(id={self.id}, user_id={self.user_id}, title={self.title})>"
|
||||
@@ -17,6 +17,8 @@ class User(Base):
|
||||
email = Column(String(100), unique=True, nullable=False, comment="邮箱")
|
||||
password_hash = Column(String(255), nullable=False, comment="密码哈希")
|
||||
role = Column(String(20), default="user", comment="角色: admin/user(保留字段,用于向后兼容)")
|
||||
feishu_open_id = Column(String(64), nullable=True, comment="飞书用户 open_id,用于推送通知")
|
||||
feishu_default_agent_id = Column(CHAR(36), nullable=True, comment="飞书对话默认 Agent ID")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
|
||||
@@ -51,9 +51,10 @@ def create_execution_for_schedule(db: Session, schedule) -> Optional[str]:
|
||||
logger.warning("Agent %s 缺少 workflow_config,无法执行定时任务", schedule.agent_id)
|
||||
return None
|
||||
|
||||
# 创建执行记录
|
||||
# 创建执行记录(关联 schedule_id)
|
||||
execution = Execution(
|
||||
agent_id=schedule.agent_id,
|
||||
schedule_id=schedule.id,
|
||||
input_data={"message": schedule.input_message},
|
||||
status="pending",
|
||||
)
|
||||
|
||||
199
backend/app/services/feishu_app_service.py
Normal file
199
backend/app/services/feishu_app_service.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""飞书应用 API 服务 — 通过飞书应用发送消息通知到用户"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Token 缓存(tenant_access_token 有效期 2 小时,提前 5 分钟刷新)
|
||||
_token_cache: dict = {"token": None, "expires_at": 0}
|
||||
|
||||
|
||||
def _get_tenant_access_token() -> Optional[str]:
|
||||
"""获取飞书 tenant_access_token(带缓存)。
|
||||
|
||||
使用 FEISHU_APP_ID + FEISHU_APP_SECRET 调用飞书 API 获取。
|
||||
"""
|
||||
now = time.time()
|
||||
if _token_cache["token"] and now < _token_cache["expires_at"] - 300:
|
||||
return _token_cache["token"]
|
||||
|
||||
app_id = settings.FEISHU_APP_ID
|
||||
app_secret = settings.FEISHU_APP_SECRET
|
||||
if not app_id or not app_secret:
|
||||
logger.warning("飞书应用未配置(FEISHU_APP_ID / FEISHU_APP_SECRET)")
|
||||
return None
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": app_id, "app_secret": app_secret},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
token = result["tenant_access_token"]
|
||||
expire = result.get("expire", 7200)
|
||||
_token_cache["token"] = token
|
||||
_token_cache["expires_at"] = now + expire
|
||||
logger.info("飞书 tenant_access_token 获取成功")
|
||||
return token
|
||||
else:
|
||||
logger.warning("飞书 token 获取失败: %s", result)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("飞书 token 获取异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def send_message_to_user(
|
||||
open_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
status: str = "info",
|
||||
detail_link: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""通过飞书应用向指定用户发送消息卡片。
|
||||
|
||||
Args:
|
||||
open_id: 飞书用户的 open_id
|
||||
title: 卡片标题
|
||||
content: 卡片正文
|
||||
status: info / success / failed
|
||||
detail_link: 详情链接(可选)
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
|
||||
color_map = {"success": "green", "failed": "red", "info": "blue"}
|
||||
color = color_map.get(status, "blue")
|
||||
|
||||
elements = [
|
||||
{"tag": "markdown", "content": content},
|
||||
]
|
||||
if detail_link:
|
||||
elements.append({
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {"tag": "plain_text", "content": "查看详情"},
|
||||
"url": detail_link,
|
||||
"type": "default",
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": {
|
||||
"title": {"tag": "plain_text", "content": title},
|
||||
"template": color,
|
||||
},
|
||||
"elements": elements,
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": open_id,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
logger.info("飞书消息发送成功: open_id=%s title=%s", open_id[:20], title)
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
"飞书消息发送失败: code=%s msg=%s", result.get("code"), result.get("msg"),
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("飞书消息发送异常: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def send_plain_text(open_id: str, text: str) -> bool:
|
||||
"""向用户发送纯文本消息。"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": open_id,
|
||||
"msg_type": "text",
|
||||
"content": json.dumps({"text": text}, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
return resp.is_success and result.get("code") == 0
|
||||
except Exception as e:
|
||||
logger.warning("飞书文本消息发送异常: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def lookup_user_by_email(email: str) -> Optional[str]:
|
||||
"""通过邮箱查询飞书用户的 open_id。
|
||||
|
||||
需要飞书应用已开通 contact:user.employee_id:readonly 权限。
|
||||
|
||||
Args:
|
||||
email: 用户邮箱
|
||||
|
||||
Returns:
|
||||
open_id 字符串,未找到返回 None
|
||||
"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"emails": [email]},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
user_list = result.get("data", {}).get("user_list", [])
|
||||
for user in user_list:
|
||||
if user.get("email", "").lower() == email.lower():
|
||||
open_id = user.get("open_id")
|
||||
if open_id:
|
||||
logger.info("飞书用户查询成功: email=%s open_id=%s", email, open_id)
|
||||
return open_id
|
||||
logger.info("飞书用户未找到: email=%s", email)
|
||||
return None
|
||||
else:
|
||||
logger.warning("飞书用户查询失败: code=%s msg=%s", result.get("code"), result.get("msg"))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("飞书用户查询异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def get_verification_token() -> str:
|
||||
"""获取飞书应用的 Verification Token(用于验证事件回调)。"""
|
||||
return settings.FEISHU_VERIFICATION_TOKEN
|
||||
96
backend/app/services/feishu_notifier.py
Normal file
96
backend/app/services/feishu_notifier.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""飞书机器人通知 — 通过 Webhook 推送消息到飞书群聊"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FEISHU_TIMEOUT_SEC = 10
|
||||
|
||||
|
||||
def send_feishu_text(webhook_url: str, text: str) -> bool:
|
||||
"""发送纯文本消息到飞书群机器人。
|
||||
|
||||
Args:
|
||||
webhook_url: 飞书机器人 webhook 地址
|
||||
text: 消息文本
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
payload = {"msg_type": "text", "content": {"text": text}}
|
||||
return _do_send(webhook_url, payload)
|
||||
|
||||
|
||||
def send_feishu_card(
|
||||
webhook_url: str,
|
||||
title: str,
|
||||
body: str,
|
||||
status: str = "info",
|
||||
detail_link: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""发送消息卡片到飞书群机器人。
|
||||
|
||||
Args:
|
||||
webhook_url: 飞书机器人 webhook 地址
|
||||
title: 卡片标题
|
||||
body: 卡片正文(支持 Markdown)
|
||||
status: 状态 — info / success / failed
|
||||
detail_link: 详情链接(可选)
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
color_map = {"success": "green", "failed": "red", "info": "blue"}
|
||||
color = color_map.get(status, "blue")
|
||||
|
||||
elements = [
|
||||
{"tag": "markdown", "content": body},
|
||||
]
|
||||
if detail_link:
|
||||
elements.append({
|
||||
"tag": "action",
|
||||
"actions": [{"tag": "button", "text": {"tag": "plain_text", "content": "查看详情"}, "url": detail_link, "type": "default"}],
|
||||
})
|
||||
|
||||
payload = {
|
||||
"msg_type": "interactive",
|
||||
"card": {
|
||||
"header": {
|
||||
"title": {"tag": "plain_text", "content": title},
|
||||
"template": color,
|
||||
},
|
||||
"elements": elements,
|
||||
},
|
||||
}
|
||||
return _do_send(webhook_url, payload)
|
||||
|
||||
|
||||
def _do_send(webhook_url: str, payload: dict) -> bool:
|
||||
"""底层 POST 发送,统一异常处理。"""
|
||||
if not webhook_url or not webhook_url.startswith("https://open.feishu.cn/"):
|
||||
logger.warning("飞书 webhook URL 无效: %s", webhook_url[:50] if webhook_url else "None")
|
||||
return False
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=FEISHU_TIMEOUT_SEC) as client:
|
||||
resp = client.post(webhook_url, json=payload)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
logger.info("飞书通知发送成功: %s", result.get("msg"))
|
||||
return True
|
||||
else:
|
||||
logger.warning(
|
||||
"飞书通知发送失败: status=%s code=%s msg=%s",
|
||||
resp.status_code, result.get("code"), result.get("msg"),
|
||||
)
|
||||
return False
|
||||
except httpx.TimeoutException:
|
||||
logger.warning("飞书通知发送超时: %s", webhook_url[:50])
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("飞书通知发送异常: %s", e)
|
||||
return False
|
||||
351
backend/app/services/feishu_ws_handler.py
Normal file
351
backend/app/services/feishu_ws_handler.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""飞书长连接事件监听 — 通过 lark-oapi SDK 建立 WebSocket 接收事件"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import List, Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 存储通过事件捕获的 open_id(与 HTTP 事件回调共用)
|
||||
_pending_open_ids: List[str] = []
|
||||
|
||||
# 已处理消息 ID 去重(防止 WS 重连导致重复处理)
|
||||
_processed_msg_ids: deque[str] = deque(maxlen=20)
|
||||
|
||||
_ws_thread: threading.Thread | None = None
|
||||
|
||||
|
||||
def _get_message_id(data) -> Optional[str]:
|
||||
"""从 Feishu 消息事件中提取 message_id。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if msg:
|
||||
return getattr(msg, "message_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _get_message_text(data) -> Optional[str]:
|
||||
"""从 Feishu 消息事件中提取纯文本内容。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if not msg:
|
||||
return None
|
||||
content_str = getattr(msg, "content", None)
|
||||
msg_type = getattr(msg, "message_type", "")
|
||||
if not content_str:
|
||||
return None
|
||||
|
||||
if msg_type == "text":
|
||||
parsed = json.loads(content_str)
|
||||
return parsed.get("text", "")
|
||||
# 其他消息类型暂不支持
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("解析飞书消息内容失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_open_id(data) -> Optional[str]:
|
||||
"""从 Feishu 消息事件中提取发送者 open_id。"""
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "open_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
"""获取聊天类型。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
return getattr(msg, "chat_type", "") if msg else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _reply_to_feishu(open_id: str, text: str):
|
||||
"""通过飞书 API 回复用户消息。"""
|
||||
try:
|
||||
from app.services.feishu_app_service import send_plain_text
|
||||
|
||||
send_plain_text(open_id, text)
|
||||
except Exception as e:
|
||||
logger.warning("飞书回复消息失败: %s", e)
|
||||
|
||||
|
||||
def _reply_card(open_id: str, title: str, content: str, status: str = "info"):
|
||||
"""通过飞书 API 回复卡片消息。"""
|
||||
try:
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
|
||||
send_message_to_user(open_id, title, content, status=status)
|
||||
except Exception as e:
|
||||
logger.warning("飞书回复卡片失败: %s", e)
|
||||
|
||||
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理飞书消息。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if not open_id or chat_type != "p2p":
|
||||
return
|
||||
|
||||
_pending_open_ids.append(open_id)
|
||||
if len(_pending_open_ids) > 5:
|
||||
_pending_open_ids.pop(0)
|
||||
|
||||
logger.info("飞书收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)")
|
||||
|
||||
try:
|
||||
with open("/tmp/feishu_open_id.txt", "w") as f:
|
||||
f.write(open_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.user import User
|
||||
from app.models.agent import Agent
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
user = db.query(User).filter(User.feishu_open_id == open_id).first()
|
||||
if not user:
|
||||
_reply_to_feishu(open_id, "你的账号未绑定平台用户,请先在平台绑定飞书。")
|
||||
return
|
||||
|
||||
agent_id = user.feishu_default_agent_id
|
||||
if not agent_id:
|
||||
_reply_to_feishu(
|
||||
open_id,
|
||||
"你还没有设置飞书对话的默认 Agent。\n请先在平台设置:\n"
|
||||
"POST /api/v1/feishu/default-agent {\"agent_id\": \"<你的AgentID>\"}",
|
||||
)
|
||||
return
|
||||
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
_reply_to_feishu(open_id, f"默认 Agent (id={agent_id}) 已不存在,请重新设置。")
|
||||
return
|
||||
|
||||
_reply_to_feishu(open_id, f"🤔 正在思考,请稍候...")
|
||||
|
||||
from app.agent_runtime import AgentRuntime, AgentConfig, AgentLLMConfig, AgentToolConfig
|
||||
|
||||
wc = agent.workflow_config or {}
|
||||
nodes = wc.get("nodes", [])
|
||||
system_prompt = agent.description or ""
|
||||
model = "deepseek-v4-flash"
|
||||
provider = "deepseek"
|
||||
temperature = 0.7
|
||||
max_iterations = 10
|
||||
|
||||
for n in nodes:
|
||||
cfg = n.get("config", {}) if isinstance(n, dict) else getattr(n, "config", {})
|
||||
if cfg.get("type") in ("agent", "llm"):
|
||||
system_prompt = cfg.get("system_prompt", "") or system_prompt
|
||||
model = cfg.get("model", model)
|
||||
provider = cfg.get("provider", provider)
|
||||
temperature = float(cfg.get("temperature", temperature))
|
||||
max_iterations = int(cfg.get("max_iterations", max_iterations))
|
||||
break
|
||||
|
||||
config = AgentConfig(
|
||||
name=agent.name or "agent",
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=model,
|
||||
provider=provider,
|
||||
temperature=temperature,
|
||||
max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(),
|
||||
user_id=user.id,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=str(agent.id), user_id=user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(text)
|
||||
|
||||
if result.content:
|
||||
_reply_card(
|
||||
open_id,
|
||||
f"🤖 {agent.name}",
|
||||
result.content.strip(),
|
||||
status="success",
|
||||
)
|
||||
else:
|
||||
_reply_to_feishu(open_id, "Agent 未返回有效回复,请重试。")
|
||||
|
||||
logger.info(
|
||||
"飞书 Agent 回复完成: open_id=%s agent=%s iterations=%d tools=%d",
|
||||
open_id[:20], agent.name, result.iterations_used, result.tool_calls_made,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("飞书消息处理失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
def _handle_message_internal(data):
|
||||
"""同步入口 — 创建异步任务处理飞书消息。"""
|
||||
# 去重:WS 重连后可能重投已处理的消息
|
||||
msg_id = _get_message_id(data)
|
||||
if msg_id:
|
||||
if msg_id in _processed_msg_ids:
|
||||
logger.debug("跳过已处理消息: %s", msg_id)
|
||||
return
|
||||
_processed_msg_ids.append(msg_id)
|
||||
|
||||
# 记录 pending open_id(用于绑定)
|
||||
open_id = _get_sender_open_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if open_id:
|
||||
_pending_open_ids.append(open_id)
|
||||
if len(_pending_open_ids) > 5:
|
||||
_pending_open_ids.pop(0)
|
||||
try:
|
||||
with open("/tmp/feishu_open_id.txt", "w") as f:
|
||||
f.write(open_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not open_id or chat_type != "p2p":
|
||||
return
|
||||
|
||||
logger.info("飞书收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)")
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
# 将实际处理委托给异步函数
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
asyncio.ensure_future(_handle_message_async(data))
|
||||
else:
|
||||
loop.run_until_complete(_handle_message_async(data))
|
||||
except Exception as e:
|
||||
logger.error("创建飞书消息处理任务失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str] = None):
|
||||
"""创建 LLM 调用日志回调。"""
|
||||
def _log(metrics: dict):
|
||||
try:
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
log = AgentLLMLog(
|
||||
agent_id=agent_id,
|
||||
session_id=metrics.get("session_id"),
|
||||
user_id=user_id,
|
||||
model=metrics.get("model", ""),
|
||||
provider=metrics.get("provider"),
|
||||
prompt_tokens=metrics.get("prompt_tokens", 0),
|
||||
completion_tokens=metrics.get("completion_tokens", 0),
|
||||
total_tokens=metrics.get("total_tokens", 0),
|
||||
latency_ms=metrics.get("latency_ms", 0),
|
||||
iteration_number=metrics.get("iteration_number", 0),
|
||||
step_type=metrics.get("step_type"),
|
||||
tool_name=metrics.get("tool_name"),
|
||||
status=metrics.get("status", "success"),
|
||||
error_message=metrics.get("error_message"),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning("写入 AgentLLMLog 失败: %s", e)
|
||||
return _log
|
||||
|
||||
|
||||
def _build_event_handler():
|
||||
"""构建事件处理器。"""
|
||||
from lark_oapi.event.dispatcher_handler import EventDispatcherHandler
|
||||
|
||||
def on_message_receive(data):
|
||||
"""处理 im.message.receive_v1 事件。"""
|
||||
_handle_message_internal(data)
|
||||
|
||||
builder = EventDispatcherHandler.builder(
|
||||
encrypt_key="",
|
||||
verification_token=settings.FEISHU_VERIFICATION_TOKEN,
|
||||
)
|
||||
builder.register_p2_im_message_receive_v1(on_message_receive)
|
||||
return builder.build()
|
||||
|
||||
|
||||
async def start_ws_client():
|
||||
"""在 async 上下文中启动飞书长连接(在主事件循环运行)。"""
|
||||
if not settings.FEISHU_APP_ID or not settings.FEISHU_APP_SECRET:
|
||||
logger.warning("飞书应用未配置,跳过长连接启动")
|
||||
return
|
||||
|
||||
from lark_oapi.ws import Client as WSClient
|
||||
|
||||
handler = _build_event_handler()
|
||||
client = WSClient(
|
||||
app_id=settings.FEISHU_APP_ID,
|
||||
app_secret=settings.FEISHU_APP_SECRET,
|
||||
event_handler=handler,
|
||||
auto_reconnect=True,
|
||||
)
|
||||
|
||||
logger.info("飞书长连接客户端启动中...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await client._connect()
|
||||
logger.info("飞书长连接已建立")
|
||||
asyncio.ensure_future(client._ping_loop())
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning("飞书长连接断开,3秒后重连: %s", e)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
|
||||
def get_pending_open_ids() -> List[str]:
|
||||
"""获取待绑定的 open_id 列表。"""
|
||||
return list(_pending_open_ids)
|
||||
|
||||
|
||||
def clear_pending_open_ids():
|
||||
"""清空待绑定的 open_id。"""
|
||||
_pending_open_ids.clear()
|
||||
128
backend/app/services/notification_service.py
Normal file
128
backend/app/services/notification_service.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""通知服务 — 创建与查询系统通知"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.notification import Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_notification(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
title: str,
|
||||
content: Optional[str] = None,
|
||||
category: str = "system",
|
||||
ref_type: Optional[str] = None,
|
||||
ref_id: Optional[str] = None,
|
||||
) -> Notification:
|
||||
"""创建一条通知。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 接收用户 ID
|
||||
title: 通知标题
|
||||
content: 通知正文(可选)
|
||||
category: 分类,如 schedule / alert / system
|
||||
ref_type: 关联对象类型
|
||||
ref_id: 关联对象 ID
|
||||
|
||||
Returns:
|
||||
创建的 Notification ORM 对象
|
||||
"""
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
category=category,
|
||||
ref_type=ref_type,
|
||||
ref_id=ref_id,
|
||||
)
|
||||
db.add(notification)
|
||||
db.flush()
|
||||
logger.info("通知已创建: user=%s title=%s category=%s", user_id, title, category)
|
||||
return notification
|
||||
|
||||
|
||||
def get_user_notifications(
|
||||
db: Session,
|
||||
user_id: str,
|
||||
unread_only: bool = False,
|
||||
category: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> List[Notification]:
|
||||
"""获取用户的通知列表(按创建时间倒序)。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 用户 ID
|
||||
unread_only: 仅未读
|
||||
category: 按分类过滤
|
||||
limit: 分页大小
|
||||
offset: 分页偏移
|
||||
|
||||
Returns:
|
||||
通知列表
|
||||
"""
|
||||
query = db.query(Notification).filter(Notification.user_id == user_id)
|
||||
|
||||
if unread_only:
|
||||
query = query.filter(Notification.is_read == False) # noqa: E712
|
||||
if category:
|
||||
query = query.filter(Notification.category == category)
|
||||
|
||||
return query.order_by(Notification.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
|
||||
def get_unread_count(db: Session, user_id: str) -> int:
|
||||
"""获取用户未读通知数。"""
|
||||
return (
|
||||
db.query(Notification)
|
||||
.filter(Notification.user_id == user_id, Notification.is_read == False) # noqa: E712
|
||||
.count()
|
||||
)
|
||||
|
||||
|
||||
def mark_as_read(db: Session, notification_id: str, user_id: str) -> Optional[Notification]:
|
||||
"""将指定通知标记为已读。"""
|
||||
notification = (
|
||||
db.query(Notification)
|
||||
.filter(Notification.id == notification_id, Notification.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not notification:
|
||||
return None
|
||||
notification.is_read = True
|
||||
db.flush()
|
||||
return notification
|
||||
|
||||
|
||||
def mark_all_as_read(db: Session, user_id: str) -> int:
|
||||
"""将用户所有通知标记为已读。返回更新的条数。"""
|
||||
count = (
|
||||
db.query(Notification)
|
||||
.filter(Notification.user_id == user_id, Notification.is_read == False) # noqa: E712
|
||||
.update({"is_read": True})
|
||||
)
|
||||
db.flush()
|
||||
return count
|
||||
|
||||
|
||||
def delete_notification(db: Session, notification_id: str, user_id: str) -> bool:
|
||||
"""删除一条通知。"""
|
||||
notification = (
|
||||
db.query(Notification)
|
||||
.filter(Notification.id == notification_id, Notification.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if not notification:
|
||||
return False
|
||||
db.delete(notification)
|
||||
db.flush()
|
||||
return True
|
||||
137
backend/app/services/orange_app_service.py
Normal file
137
backend/app/services/orange_app_service.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""橙子飞书应用 API 服务 — 通过橙子应用发送消息到用户"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Token 缓存(tenant_access_token 有效期 2 小时,提前 5 分钟刷新)
|
||||
_token_cache: dict = {"token": None, "expires_at": 0}
|
||||
|
||||
|
||||
def _get_tenant_access_token() -> Optional[str]:
|
||||
"""获取橙子应用的 tenant_access_token(带缓存)。"""
|
||||
now = time.time()
|
||||
if _token_cache["token"] and now < _token_cache["expires_at"] - 300:
|
||||
return _token_cache["token"]
|
||||
|
||||
app_id = settings.ORANGE_APP_ID
|
||||
app_secret = settings.ORANGE_APP_SECRET
|
||||
if not app_id or not app_secret:
|
||||
logger.warning("橙子应用未配置(ORANGE_APP_ID / ORANGE_APP_SECRET)")
|
||||
return None
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
json={"app_id": app_id, "app_secret": app_secret},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
token = result["tenant_access_token"]
|
||||
expire = result.get("expire", 7200)
|
||||
_token_cache["token"] = token
|
||||
_token_cache["expires_at"] = now + expire
|
||||
logger.info("橙子 tenant_access_token 获取成功")
|
||||
return token
|
||||
else:
|
||||
logger.warning("橙子 token 获取失败: %s", result)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("橙子 token 获取异常: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def send_message_to_user(
|
||||
open_id: str,
|
||||
title: str,
|
||||
content: str,
|
||||
status: str = "info",
|
||||
detail_link: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""通过橙子应用向用户发送消息卡片。"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
|
||||
color_map = {"success": "green", "failed": "red", "info": "blue"}
|
||||
color = color_map.get(status, "blue")
|
||||
|
||||
elements = [
|
||||
{"tag": "markdown", "content": content},
|
||||
]
|
||||
if detail_link:
|
||||
elements.append({
|
||||
"tag": "action",
|
||||
"actions": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {"tag": "plain_text", "content": "查看详情"},
|
||||
"url": detail_link,
|
||||
"type": "default",
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
card = {
|
||||
"config": {"wide_screen_mode": True},
|
||||
"header": {
|
||||
"title": {"tag": "plain_text", "content": title},
|
||||
"template": color,
|
||||
},
|
||||
"elements": elements,
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": open_id,
|
||||
"msg_type": "interactive",
|
||||
"content": json.dumps(card, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
if resp.is_success and result.get("code") == 0:
|
||||
logger.info("橙子消息发送成功: open_id=%s title=%s", open_id[:20], title)
|
||||
return True
|
||||
else:
|
||||
logger.warning("橙子消息发送失败: code=%s msg=%s", result.get("code"), result.get("msg"))
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("橙子消息发送异常: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def send_plain_text(open_id: str, text: str) -> bool:
|
||||
"""通过橙子应用向用户发送纯文本消息。"""
|
||||
token = _get_tenant_access_token()
|
||||
if not token:
|
||||
return False
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=10) as client:
|
||||
resp = client.post(
|
||||
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={
|
||||
"receive_id": open_id,
|
||||
"msg_type": "text",
|
||||
"content": json.dumps({"text": text}, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
result = resp.json()
|
||||
return resp.is_success and result.get("code") == 0
|
||||
except Exception as e:
|
||||
logger.warning("橙子文本消息发送异常: %s", e)
|
||||
return False
|
||||
298
backend/app/services/orange_ws_handler.py
Normal file
298
backend/app/services/orange_ws_handler.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""橙子飞书长连接 — 固定路由到橙子助手 Agent"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 已处理消息 ID 去重(防止 WS 重连导致重复处理)
|
||||
_processed_msg_ids: deque[str] = deque(maxlen=20)
|
||||
|
||||
_ws_thread: threading.Thread | None = None
|
||||
|
||||
|
||||
def _get_message_id(data) -> Optional[str]:
|
||||
"""从消息事件中提取 message_id。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if msg:
|
||||
return getattr(msg, "message_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _get_message_text(data) -> Optional[str]:
|
||||
"""从消息事件中提取纯文本内容。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
if not msg:
|
||||
return None
|
||||
content_str = getattr(msg, "content", None)
|
||||
msg_type = getattr(msg, "message_type", "")
|
||||
if not content_str:
|
||||
return None
|
||||
|
||||
if msg_type == "text":
|
||||
parsed = json.loads(content_str)
|
||||
return parsed.get("text", "")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning("解析橙子消息内容失败: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
def _get_sender_open_id(data) -> Optional[str]:
|
||||
"""从消息事件中提取发送者 open_id。"""
|
||||
try:
|
||||
ev = data.event
|
||||
sender = getattr(ev, "sender", None)
|
||||
if not sender:
|
||||
return None
|
||||
sender_id = getattr(sender, "sender_id", None)
|
||||
if not sender_id:
|
||||
return None
|
||||
return getattr(sender_id, "open_id", None)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _get_chat_type(data) -> str:
|
||||
"""获取聊天类型。"""
|
||||
try:
|
||||
ev = data.event
|
||||
msg = getattr(ev, "message", None)
|
||||
return getattr(msg, "chat_type", "") if msg else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _reply_to_feishu(open_id: str, text: str):
|
||||
"""通过橙子应用回复用户消息。"""
|
||||
try:
|
||||
from app.services.orange_app_service import send_plain_text
|
||||
send_plain_text(open_id, text)
|
||||
except Exception as e:
|
||||
logger.warning("橙子回复消息失败: %s", e)
|
||||
|
||||
|
||||
def _reply_card(open_id: str, title: str, content: str, status: str = "info"):
|
||||
"""通过橙子应用回复卡片消息。"""
|
||||
try:
|
||||
from app.services.orange_app_service import send_message_to_user
|
||||
send_message_to_user(open_id, title, content, status=status)
|
||||
except Exception as e:
|
||||
logger.warning("橙子回复卡片失败: %s", e)
|
||||
|
||||
|
||||
def _make_llm_logger(db, agent_id: Optional[str] = None, user_id: Optional[str] = None):
|
||||
"""创建 LLM 调用日志回调。"""
|
||||
def _log(metrics: dict):
|
||||
try:
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
log = AgentLLMLog(
|
||||
agent_id=agent_id,
|
||||
session_id=metrics.get("session_id"),
|
||||
user_id=user_id,
|
||||
model=metrics.get("model", ""),
|
||||
provider=metrics.get("provider"),
|
||||
prompt_tokens=metrics.get("prompt_tokens", 0),
|
||||
completion_tokens=metrics.get("completion_tokens", 0),
|
||||
total_tokens=metrics.get("total_tokens", 0),
|
||||
latency_ms=metrics.get("latency_ms", 0),
|
||||
iteration_number=metrics.get("iteration_number", 0),
|
||||
step_type=metrics.get("step_type"),
|
||||
tool_name=metrics.get("tool_name"),
|
||||
status=metrics.get("status", "success"),
|
||||
error_message=metrics.get("error_message"),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning("写入 AgentLLMLog 失败: %s", e)
|
||||
return _log
|
||||
|
||||
|
||||
async def _handle_message_async(data):
|
||||
"""异步处理橙子消息 — 固定使用橙子助手 Agent。"""
|
||||
open_id = _get_sender_open_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if not open_id or chat_type != "p2p":
|
||||
return
|
||||
|
||||
logger.info("橙子收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)")
|
||||
|
||||
if not text:
|
||||
return
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import SessionLocal
|
||||
from app.models.agent import Agent
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
|
||||
# 固定使用橙子助手 Agent
|
||||
agent_id = settings.ORANGE_AGENT_ID
|
||||
if not agent_id:
|
||||
_reply_to_feishu(open_id, "橙子助手尚未配置,请联系管理员。")
|
||||
return
|
||||
|
||||
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
||||
if not agent:
|
||||
_reply_to_feishu(open_id, "橙子助手 Agent 已不存在,请联系管理员。")
|
||||
return
|
||||
|
||||
_reply_to_feishu(open_id, "🤔 正在思考,请稍候...")
|
||||
|
||||
from app.agent_runtime import AgentRuntime, AgentConfig, AgentLLMConfig, AgentToolConfig
|
||||
|
||||
wc = agent.workflow_config or {}
|
||||
nodes = wc.get("nodes", [])
|
||||
system_prompt = agent.description or ""
|
||||
model = "deepseek-v4-flash"
|
||||
provider = "deepseek"
|
||||
temperature = 0.7
|
||||
max_iterations = 10
|
||||
|
||||
for n in nodes:
|
||||
cfg = n.get("config", {}) if isinstance(n, dict) else getattr(n, "config", {})
|
||||
if cfg.get("type") in ("agent", "llm"):
|
||||
system_prompt = cfg.get("system_prompt", "") or system_prompt
|
||||
model = cfg.get("model", model)
|
||||
provider = cfg.get("provider", provider)
|
||||
temperature = float(cfg.get("temperature", temperature))
|
||||
max_iterations = int(cfg.get("max_iterations", max_iterations))
|
||||
break
|
||||
|
||||
config = AgentConfig(
|
||||
name=agent.name or "橙子助手",
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=model,
|
||||
provider=provider,
|
||||
temperature=temperature,
|
||||
max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(),
|
||||
user_id=None,
|
||||
memory_scope_id=str(agent.id),
|
||||
)
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=str(agent.id))
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(text)
|
||||
|
||||
if result.content:
|
||||
_reply_card(open_id, f"🍊 {agent.name}", result.content.strip(), status="success")
|
||||
else:
|
||||
_reply_to_feishu(open_id, "Agent 未返回有效回复,请重试。")
|
||||
|
||||
logger.info(
|
||||
"橙子 Agent 回复完成: open_id=%s agent=%s iterations=%d tools=%d",
|
||||
open_id[:20], agent.name, result.iterations_used, result.tool_calls_made,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("橙子消息处理失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
def _handle_message_internal(data):
|
||||
"""同步入口 — 创建异步任务处理橙子消息。"""
|
||||
# 去重
|
||||
msg_id = _get_message_id(data)
|
||||
if msg_id:
|
||||
if msg_id in _processed_msg_ids:
|
||||
logger.debug("橙子跳过已处理消息: %s", msg_id)
|
||||
return
|
||||
_processed_msg_ids.append(msg_id)
|
||||
|
||||
open_id = _get_sender_open_id(data)
|
||||
chat_type = _get_chat_type(data)
|
||||
text = _get_message_text(data)
|
||||
|
||||
if not open_id or chat_type != "p2p" or not text:
|
||||
return
|
||||
|
||||
logger.info("橙子收到消息: open_id=%s text=%s", open_id[:20], text[:50] if text else "(空)")
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
asyncio.ensure_future(_handle_message_async(data))
|
||||
else:
|
||||
loop.run_until_complete(_handle_message_async(data))
|
||||
except Exception as e:
|
||||
logger.error("橙子创建消息处理任务失败: %s", e)
|
||||
try:
|
||||
_reply_to_feishu(open_id, f"处理失败: {e!s}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _build_event_handler():
|
||||
"""构建橙子事件处理器。"""
|
||||
from lark_oapi.event.dispatcher_handler import EventDispatcherHandler
|
||||
|
||||
def on_message_receive(data):
|
||||
_handle_message_internal(data)
|
||||
|
||||
builder = EventDispatcherHandler.builder(
|
||||
encrypt_key="",
|
||||
verification_token="",
|
||||
)
|
||||
builder.register_p2_im_message_receive_v1(on_message_receive)
|
||||
return builder.build()
|
||||
|
||||
|
||||
async def start_ws_client():
|
||||
"""在 async 上下文中启动橙子飞书长连接(在主事件循环运行)。"""
|
||||
if not settings.ORANGE_APP_ID or not settings.ORANGE_APP_SECRET:
|
||||
logger.warning("橙子应用未配置,跳过橙子长连接启动")
|
||||
return
|
||||
|
||||
from lark_oapi.ws import Client as WSClient
|
||||
|
||||
handler = _build_event_handler()
|
||||
client = WSClient(
|
||||
app_id=settings.ORANGE_APP_ID,
|
||||
app_secret=settings.ORANGE_APP_SECRET,
|
||||
event_handler=handler,
|
||||
auto_reconnect=True,
|
||||
)
|
||||
|
||||
logger.info("橙子长连接客户端启动中...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
await client._connect()
|
||||
logger.info("橙子长连接已建立")
|
||||
# _ping_loop 内部创建 _receive_message_loop 并处理心跳
|
||||
ping_task = asyncio.ensure_future(client._ping_loop())
|
||||
# 用永久 sleep 保持协程存活
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning("橙子长连接断开,3秒后重连: %s", e)
|
||||
await asyncio.sleep(3)
|
||||
@@ -19,6 +19,7 @@ from app.models.agent import Agent
|
||||
from app.models.workflow import Workflow
|
||||
from app.services.execution_budget import merge_budget_for_execution
|
||||
from app.services.agent_workspace_chat_log import try_append_agent_dialogue_after_success
|
||||
from app.services.notification_service import create_notification
|
||||
import asyncio
|
||||
import time
|
||||
from typing import Any, Dict, Optional
|
||||
@@ -54,6 +55,92 @@ def _trusted_user_for_execution(db, execution: Optional[Execution]) -> Optional[
|
||||
return None
|
||||
|
||||
|
||||
def _notify_schedule_result(
|
||||
db,
|
||||
execution,
|
||||
status: str,
|
||||
error_message: Optional[str] = None,
|
||||
):
|
||||
"""如果 execution 关联了定时任务,创建通知推送结果给用户。"""
|
||||
if not execution or not execution.schedule_id:
|
||||
return
|
||||
try:
|
||||
from app.models.agent_schedule import AgentSchedule
|
||||
|
||||
schedule = db.query(AgentSchedule).filter(AgentSchedule.id == execution.schedule_id).first()
|
||||
if not schedule:
|
||||
return
|
||||
|
||||
if status == "completed":
|
||||
title = f"定时任务「{schedule.name}」执行成功"
|
||||
content = f"Agent 已按计划执行完成。"
|
||||
else:
|
||||
title = f"定时任务「{schedule.name}」执行失败"
|
||||
content = f"错误信息: {error_message or '未知错误'}"
|
||||
|
||||
create_notification(
|
||||
db,
|
||||
user_id=schedule.user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
category="schedule",
|
||||
ref_type="execution",
|
||||
ref_id=str(execution.id),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
# 如果配置了飞书 webhook,发送飞书通知(非阻塞,失败不影响主流程)
|
||||
if schedule.webhook_url:
|
||||
try:
|
||||
from app.services.feishu_notifier import send_feishu_card
|
||||
|
||||
detail_link = None
|
||||
# 如果系统配置了外部访问地址,拼接 execution 详情链接
|
||||
try:
|
||||
from app.core.config import settings
|
||||
if settings.EXTERNAL_URL:
|
||||
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
send_feishu_card(
|
||||
webhook_url=schedule.webhook_url,
|
||||
title=title,
|
||||
body=content,
|
||||
status=status,
|
||||
detail_link=detail_link,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("飞书 webhook 通知发送失败: %s", e)
|
||||
|
||||
# 如果用户绑定了飞书账号,通过飞书应用发送通知
|
||||
try:
|
||||
from app.models.user import User
|
||||
from app.services.feishu_app_service import send_message_to_user
|
||||
|
||||
schedule_user = db.query(User).filter(User.id == schedule.user_id).first()
|
||||
if schedule_user and schedule_user.feishu_open_id:
|
||||
detail_link = None
|
||||
try:
|
||||
from app.core.config import settings
|
||||
if settings.EXTERNAL_URL:
|
||||
detail_link = f"{settings.EXTERNAL_URL}/executions/{execution.id}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
send_message_to_user(
|
||||
open_id=schedule_user.feishu_open_id,
|
||||
title=title,
|
||||
content=content,
|
||||
status=status,
|
||||
detail_link=detail_link,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("飞书应用通知发送失败: %s", e)
|
||||
except Exception as e:
|
||||
logger.warning("创建定时任务通知失败: %s", e)
|
||||
|
||||
|
||||
@celery_app.task(bind=True)
|
||||
def execute_workflow_task(
|
||||
self,
|
||||
@@ -172,7 +259,10 @@ def execute_workflow_task(
|
||||
except Exception as e:
|
||||
# 告警检测失败不影响执行结果
|
||||
execution_logger.warn(f"告警检测失败: {str(e)}")
|
||||
|
||||
|
||||
# 定时任务结果通知
|
||||
_notify_schedule_result(db, execution, "completed")
|
||||
|
||||
return {
|
||||
'status': 'completed',
|
||||
'result': result,
|
||||
@@ -213,7 +303,10 @@ def execute_workflow_task(
|
||||
# 告警检测失败不影响错误处理
|
||||
if execution_logger:
|
||||
execution_logger.warn(f"告警检测失败: {str(e2)}")
|
||||
|
||||
|
||||
# 定时任务失败通知
|
||||
_notify_schedule_result(db, execution, "failed", error_message=err_text)
|
||||
|
||||
raise
|
||||
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
人工智能(AI)是计算机科学的一个分支,致力于创建能够模拟人类智能的系统。
|
||||
机器学习是AI的子集,让系统从数据中学习而不需要显式编程。
|
||||
深度学习是机器学习的子集,使用多层神经网络处理复杂模式。
|
||||
|
||||
Python是AI开发中最流行的编程语言之一,拥有丰富的库如NumPy、Pandas、TensorFlow和PyTorch。
|
||||
自然语言处理(NLP)让计算机理解、解释和生成人类语言。
|
||||
计算机视觉使机器能够理解和处理图像和视频数据。
|
||||
|
||||
大语言模型(LLM)如GPT系列,使用海量文本数据训练,能够理解和生成自然语言。
|
||||
RAG(检索增强生成)结合了信息检索和文本生成,先检索相关知识再生成回答。
|
||||
Agent是指能自主感知环境并采取行动达成目标的系统,常结合LLM和工具使用。
|
||||
@@ -0,0 +1,17 @@
|
||||
知识库RAG系统使用指南。
|
||||
|
||||
RAG(Retrieval-Augmented Generation)是一种结合信息检索和文本生成的技术。
|
||||
它首先从知识库中检索相关文档片段,然后将这些片段作为上下文提供给LLM,
|
||||
让LLM基于检索到的信息生成更准确、更可靠的回答。
|
||||
|
||||
使用步骤:
|
||||
1. 创建知识库,设置分块参数
|
||||
2. 上传文档(支持txt、pdf、docx、csv格式)
|
||||
3. 系统自动解析文档、分块、生成向量
|
||||
4. 用户提问时,系统从知识库检索最相关的片段
|
||||
5. 检索结果与问题一起提交给LLM,生成最终回答
|
||||
|
||||
RAG的优势:
|
||||
- 减少幻觉:LLM基于事实信息回答
|
||||
- 知识更新:只需更新知识库,无需重新训练模型
|
||||
- 可追溯:可以查看LLM回答引用的原始文档
|
||||
99
index.html
Normal file
99
index.html
Normal file
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>欢迎页面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 50px 60px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
p {
|
||||
font-size: 18px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 36px;
|
||||
background: #fff;
|
||||
color: #764ba2;
|
||||
text-decoration: none;
|
||||
border-radius: 50px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.time {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🌞 你好,世界!</h1>
|
||||
<p>这是一个简单美观的 HTML 页面。<br>欢迎来到 D:\aaa\test 目录。</p>
|
||||
<button class="btn" onclick="showMessage()">点我试试</button>
|
||||
<div class="time" id="timeDisplay"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 显示欢迎消息
|
||||
function showMessage() {
|
||||
alert('🎉 欢迎来到我的页面!');
|
||||
}
|
||||
|
||||
// 显示当前时间
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
document.getElementById('timeDisplay').textContent = '当前时间:' + timeStr;
|
||||
}
|
||||
|
||||
// 每秒更新时间
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
104
test/index.html
Normal file
104
test/index.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>欢迎页面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 40px 50px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
width: 90%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
color: #4a4a4a;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: #777;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 36px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s, transform 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.time {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="icon">🚀</div>
|
||||
<h1>Hello, World!</h1>
|
||||
<p>这是一个简单的 HTML 页面,<br>由 CodeBot 为你生成。</p>
|
||||
<button class="btn" onclick="showMessage()">点我试试</button>
|
||||
<div class="time" id="timeDisplay"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 显示当前时间
|
||||
function updateTime() {
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
});
|
||||
document.getElementById('timeDisplay').textContent = '当前时间:' + timeStr;
|
||||
}
|
||||
updateTime();
|
||||
setInterval(updateTime, 1000);
|
||||
|
||||
// 按钮点击事件
|
||||
function showMessage() {
|
||||
alert('🎉 你好!欢迎来到这个页面!');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
69
test_coding_agent_execution.py
Normal file
69
test_coding_agent_execution.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
代码编程助手 — 工作流执行冒烟测试
|
||||
|
||||
与 test_agent_execution.py 相同调用链:POST /api/v1/executions,
|
||||
input_data 仅含 query、USER_INPUT(与《工作流调用测试总结》一致)。
|
||||
|
||||
用法示例:
|
||||
python test_coding_agent_execution.py
|
||||
python test_coding_agent_execution.py -m "你好"
|
||||
python test_coding_agent_execution.py <agent_id>
|
||||
python test_coding_agent_execution.py <agent_id> "写一个 hello world"
|
||||
python test_coding_agent_execution.py --base-url http://127.0.0.1:8037 --max-wait 420
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
from test_agent_execution import DEFAULT_BASE_URL, test_agent_execution
|
||||
|
||||
CODING_AGENT_NAME = "代码编程助手"
|
||||
DEFAULT_MESSAGE = "你好"
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(
|
||||
description=f'测试「{CODING_AGENT_NAME}」工作流执行(默认用户话术「{DEFAULT_MESSAGE}」)'
|
||||
)
|
||||
p.add_argument("agent_id", nargs="?", default=None, help="Agent UUID(可选;不传则按名称查找)")
|
||||
p.add_argument("user_input", nargs="?", default=None, help="用户消息(可选;等价于省略时使用默认值)")
|
||||
p.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
metavar="TEXT",
|
||||
help=f'用户话术(优先于第二个位置参数;默认「{DEFAULT_MESSAGE}」)',
|
||||
)
|
||||
p.add_argument("--base-url", default=DEFAULT_BASE_URL, help="API 根地址")
|
||||
p.add_argument("--username", default="admin")
|
||||
p.add_argument("--password", default="123456")
|
||||
p.add_argument("--request-timeout", type=int, default=120, help="单次 HTTP 超时秒数")
|
||||
p.add_argument("--max-wait", type=int, default=420, help="轮询最长等待秒数(编程助手可能多轮 LLM)")
|
||||
p.add_argument("--poll-interval", type=float, default=2.0)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = _parse_args()
|
||||
msg = DEFAULT_MESSAGE
|
||||
if args.message is not None:
|
||||
msg = args.message
|
||||
elif args.user_input is not None:
|
||||
msg = args.user_input
|
||||
|
||||
test_agent_execution(
|
||||
agent_id=args.agent_id,
|
||||
user_input=msg,
|
||||
base_url=args.base_url,
|
||||
username=args.username,
|
||||
password=args.password,
|
||||
agent_name=None if args.agent_id else CODING_AGENT_NAME,
|
||||
request_timeout=args.request_timeout,
|
||||
max_wait_time=args.max_wait,
|
||||
poll_interval=args.poll_interval,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
test_emails.txt
Normal file
27
test_emails.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
user@example.com
|
||||
first.last@example.com
|
||||
user+tag@example.co.uk
|
||||
user_name@example.org
|
||||
user%name@example.com
|
||||
123456@example.cn
|
||||
a@b.co
|
||||
nice&simple@example.com
|
||||
very.common@example.com
|
||||
disposable.style.email.with+symbol@example.com
|
||||
other.email-with-hyphen@example.com
|
||||
fully-qualified-domain@example.com
|
||||
user.name+tag+sorting@example.com
|
||||
x@example.com
|
||||
example-indeed@strange-example.com
|
||||
|
||||
plainaddress
|
||||
@no-local-part.com
|
||||
user@.com
|
||||
user@com
|
||||
user@domain..com
|
||||
user@-domain.com
|
||||
user@domain.com.
|
||||
user name@example.com
|
||||
user@domain,com
|
||||
user@domain.c
|
||||
user@.domain.com
|
||||
853
工具链对比报告.md
Normal file
853
工具链对比报告.md
Normal file
@@ -0,0 +1,853 @@
|
||||
## 步骤 4 输出:Python 与 JavaScript 生态工具链深度对比报告
|
||||
|
||||
---
|
||||
|
||||
### 一、核心哲学差异概览
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **包管理哲学** | "系统级 + 项目级"双轨制:`pip` 可全局安装,但推荐 venv 隔离 | "项目级"唯一路径:依赖安装到项目本地 `node_modules` |
|
||||
| **依赖解析算法** | **静态解析**:基于版本号区间 + SAT 求解器 | **扁平化解析**:依赖提升(hoisting) + 确定性 lockfile |
|
||||
| **版本锁定机制** | `requirements.txt` / `poetry.lock` / `conda-lock` | `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` |
|
||||
| **构建/打包** | 源代码分发(sdist)+ 二进制 wheel(预编译) | 源码打包 + 代码转换(转译/压缩/摇树) |
|
||||
| **类型检查** | **可选**:通过 `mypy` / `pyright` 三方工具 | **内置**:TypeScript 是语言超集,类型系统是核心特性 |
|
||||
| **格式化/检查** | 职责分明:formatter(black)与 linter(flake8)分离 | 统一生态:ESLint 集检查与规则于一体,Prettier 负责格式化 |
|
||||
| **环境隔离** | **显式**:`venv` / `conda` 创建独立解释器环境 | **隐式**:`node_modules` 目录级隔离 + nvm 管理 Node 版本 |
|
||||
|
||||
---
|
||||
|
||||
### 二、包管理器(Package Manager)
|
||||
|
||||
#### 2.1 横向全景对比
|
||||
|
||||
| 特性 | Python | | | JavaScript | | |
|
||||
|------|--------|---------|---------|-----------|----------|----------|
|
||||
| **名称** | **pip** | **Poetry** | **Conda** | **npm** | **Yarn** | **pnpm** |
|
||||
| **发布年份** | 2011(Python 3.4 内置) | 2018 | 2012 | 2010 | 2016 | 2017 |
|
||||
| **语言** | Python | Python | Python + C++ | JavaScript | JavaScript | JavaScript |
|
||||
| **依赖解析** | ⚠️ 线性(无 SAT 求解器) | ✅ 高级(使用 `poetry-core`) | ✅ 高级(SAT 求解器) | ⚠️ 简单(依赖树遍历) | ✅ 高级(确定性) | ✅ 高级(确定性) |
|
||||
| **Lockfile** | ❌ 无原生(需 `pip freeze` 或 `pip-tools`) | ✅ `poetry.lock` | ✅ `conda-lock`(实验性) | ✅ `package-lock.json` | ✅ `yarn.lock` | ✅ `pnpm-lock.yaml` |
|
||||
| **虚拟环境** | `venv` 外部管理 | ✅ 内置 `poetry env` | ✅ 内置环境管理 | ❌ 外部管理(nvm/volta) | ❌ 外部管理 | ❌ 外部管理 |
|
||||
| **包源** | PyPI(Python Package Index) | PyPI | Anaconda / conda-forge | npm registry | npm registry | npm registry |
|
||||
| **二进制包** | ✅ Wheel(预编译) | ✅ 通过 pip 底层 | ✅ 预编译包 | ❌ 源码为主(npm pack) | ❌ 源码为主 | ❌ 源码为主 |
|
||||
| **并行安装** | ⚠️ 部分 | ❌ 顺序安装 | ✅ 并行 | ✅ 并行 | ✅ 并行 | ✅ 最强并行 |
|
||||
| **磁盘效率** | 每个 venv 独立下载 | 每个 venv 独立下载 | 硬链接共享包缓存 | 每个项目独立 `node_modules` | 每个项目独立 | ✅ **内容寻址存储**(全局唯一副本) |
|
||||
| **Monorepo 支持** | ❌ 无 | ⚠️ 部分(通过 `poetry` 多包) | ❌ 无 | ❌ 需要 workspace | ✅ `yarn workspaces` | ✅ **内置 workspace**(最强 monorepo) |
|
||||
|
||||
#### 2.2 依赖锁定的本质差异
|
||||
|
||||
**Python 的 `requirements.txt` vs JavaScript 的 `package-lock.json`**:
|
||||
|
||||
```python
|
||||
# requirements.txt —— 扁平列表,无嵌套
|
||||
requests==2.31.0
|
||||
fastapi==0.104.0
|
||||
pydantic==2.5.0
|
||||
|
||||
# ⚠️ 不记录每个包的依赖的依赖(传递依赖版本未锁定)
|
||||
# 等价于只记录了 "直接依赖",而非 "完整依赖树快照"
|
||||
```
|
||||
|
||||
```json
|
||||
// package-lock.json —— 完整依赖树快照
|
||||
{
|
||||
"name": "my-project",
|
||||
"lockfileVersion": 3,
|
||||
"packages": {
|
||||
"node_modules/express": {
|
||||
"version": "4.18.2",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
}
|
||||
}
|
||||
// ... 包含每个传递依赖的精确版本
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 特性 | Python pip | JavaScript npm/yarn/pnpm |
|
||||
|------|-----------|--------------------------|
|
||||
| **锁定粒度** | 仅直接依赖(`requirements.txt`)或完整树(`pip-tools` / `poetry.lock`) | **完整依赖树**(所有传递依赖的精确版本) |
|
||||
| **可复现性** | ⚠️ 需 `pip-tools`(`pip-compile` + `pip-sync`) | ✅ **开箱即用**(`npm ci` / `yarn install --frozen-lockfile`) |
|
||||
| **哈希验证** | ⚠️ 部分(PEP 458/480 可选) | ✅ **完整性哈希**(`integrity` 字段,SHA-512) |
|
||||
| **子依赖冲突** | 直接安装两个版本到不同路径(Python 包机制允许) | **单版本扁平化**(npm/yarn 会提升/去重) |
|
||||
|
||||
#### 2.3 依赖解析算法的差异
|
||||
|
||||
```python
|
||||
# Python pip 的依赖解析(Python 3.6+ 引入解析器)
|
||||
|
||||
# 场景:安装 A,A 依赖 B>=1.0, B<3.0
|
||||
# pip 的解析策略:
|
||||
# 1. 从 PyPI 获取 A 的所有版本元数据
|
||||
# 2. 获取 B 的版本列表
|
||||
# 3. 线性扫描满足 A 约束的 B 版本
|
||||
# 4. ⚠️ 早期 pip(<20.3)使用回溯(backtracking)有限
|
||||
# 5. ✅ 新解析器(20.3+)使用更彻底的 SAT 求解
|
||||
|
||||
# 示例冲突:
|
||||
# pip install "numpy<1.20" "pandas"
|
||||
# ❌ numpy 1.19 与 pandas 的 numpy>=1.20 约束冲突
|
||||
# ✅ 新解析器会报告:"无法满足依赖约束"
|
||||
# ❌ 旧解析器可能安装不兼容版本
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript npm/yarn 的依赖解析
|
||||
|
||||
// 场景:安装 A,A 依赖 B@^1.0
|
||||
// npm 的解析策略:
|
||||
// 1. 从 registry 获取 A 的 metadata
|
||||
// 2. 解析 B@^1.0 → 选择 B@1.x.x 的最新版本
|
||||
// 3. 扁平化:如果另一个包 C 依赖 B@^2.0
|
||||
// - npm:会创建 B@1.x.x 和 B@2.x.x 两个版本共存
|
||||
// - 提升(hoisting):尝试将公共版本提到顶层 node_modules
|
||||
// 4. 确定性:通过 lockfile 保证安装一致性
|
||||
|
||||
// Yarn Berry (v2+) 使用 PnP (Plug'n'Play):
|
||||
// - 不再生成 node_modules
|
||||
// - 使用 .pnp.cjs 映射文件直接定位依赖
|
||||
// - 安装速度提升 70%,减少磁盘 I/O
|
||||
```
|
||||
|
||||
| 解析维度 | Python pip | JavaScript npm |
|
||||
|----------|-----------|----------------|
|
||||
| **解析时机** | 运行时(`pip install` 时全量解析) | 安装时全量解析,lockfile 缓存结果 |
|
||||
| **版本选择策略** | 最小版本优先(保守) | 最大兼容版本优先(激进) |
|
||||
| **依赖树深度** | 较浅(Python 包依赖链较短) | 极深(JS 生态依赖链可达 50+ 层) |
|
||||
| **冲突解决** | 报告冲突(让用户手动解决) | 创建多版本副本(npm)/ 错误退出(pnpm) |
|
||||
| **性能** | 慢(纯 Python 实现,网络 I/O 瓶颈) | 快(C++/JS 实现,并行下载 + 缓存) |
|
||||
|
||||
#### 2.4 磁盘效率对比
|
||||
|
||||
| 方案 | 100 个项目的磁盘占用 | 原理 |
|
||||
|------|---------------------|------|
|
||||
| **pip + venv** | ~50GB(每个 env 独立 500MB) | 每个 venv 下载一份完整依赖副本 |
|
||||
| **poetry + cache** | ~15GB(利用 pip 缓存) | pip 缓存 ~5GB + 每个 venv 硬链接 |
|
||||
| **conda** | ~30GB(包去重 + 软链接) | conda 包缓存 + 环境级别软链接 |
|
||||
| **npm** | ~100GB(每个项目独立 node_modules) | 每个项目独立 node_modules,冗余极高 |
|
||||
| **yarn** | ~70GB(全局缓存 + 硬链接) | 全局 cache 复制到 node_modules |
|
||||
| **pnpm** | **~5GB**(内容寻址存储) | 全局存储 + 项目内**硬链接**(磁盘效率最高) |
|
||||
|
||||
> **关键 insight**:pnpm 使用内容寻址存储(Content-Addressable Storage),所有包的**单一物理副本**存储在全局 `.pnpm-store` 中,项目 `node_modules` 中只有**硬链接**。这是 JS 生态在磁盘效率上对 Python 的超越。
|
||||
|
||||
---
|
||||
|
||||
### 三、构建与打包工具
|
||||
|
||||
#### 3.1 核心差异:Python 的"安装即运行"vs JavaScript 的"构建后运行"
|
||||
|
||||
```python
|
||||
# Python:pip install 后直接 import 使用
|
||||
# 安装阶段已完成:源码复制 + 可选编译(C 扩展)
|
||||
import requests
|
||||
response = requests.get("https://api.example.com")
|
||||
|
||||
# 无"构建步骤"——Python 是解释型语言
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript:npm install 后还需要构建
|
||||
// 源码 → 转译(TS→JS) → 打包(合并) → 压缩 → 输出
|
||||
import React from 'react';
|
||||
// ❌ 浏览器不能直接运行 JSX/TypeScript
|
||||
// ✅ 需要 webpack/vite 等构建工具处理
|
||||
```
|
||||
|
||||
**根本原因**:Python 的运行时就是解释器(CPython),直接执行源码;JavaScript 在浏览器中运行,需要将现代 JS/TS 转换为兼容格式。
|
||||
|
||||
#### 3.2 构建工具对比
|
||||
|
||||
| 特性 | Python setuptools | Python wheel | JavaScript webpack | JavaScript Vite | JavaScript esbuild |
|
||||
|------|------------------|-------------|-------------------|-----------------|-------------------|
|
||||
| **定位** | 打包标准 | 分发格式 | 全能打包器 | 开发服务器 + 打包 | 超快打包器 |
|
||||
| **语言** | Python | 元数据格式 | JavaScript | JavaScript + esbuild | Go → WASM |
|
||||
| **诞生年份** | 2004 | 2012(PEP 427) | 2012 | 2020 | 2020 |
|
||||
| **核心功能** | 构建 sdist + wheel | 预编译二进制分发 | 打包/转译/代码分割/HMR | 快速 HMR + Rollup 打包 | 转译/打包/压缩 |
|
||||
| **配置文件** | `setup.py` / `setup.cfg` / `pyproject.toml` | 内嵌 METADATA | `webpack.config.js` | `vite.config.ts` | CLI 参数 / esbuild.config |
|
||||
| **转译能力** | ❌(C 扩展编译) | ❌ | ✅ Babel / TS / JSX / CSS | ✅ esbuild / SWC | ✅ 内置 TS/JSX |
|
||||
| **代码分割** | ❌(不适用) | ❌ | ✅ **代码分割 + 懒加载** | ✅ 按路由分割 | ⚠️ 基础支持 |
|
||||
| **摇树优化** | ❌ | ❌ | ✅ tree-shaking | ✅ tree-shaking | ✅ tree-shaking |
|
||||
| **HMR(热更新)** | ❌ | ❌ | ✅ webpack-dev-server | ✅ **原生 ESM HMR** | ❌ |
|
||||
| **构建速度** | 慢(纯 Python) | N/A | 慢(JS 打包逻辑重) | **快**(esbuild 预构建) | **最快**(Go 编写) |
|
||||
| **输出格式** | `.tar.gz` (sdist) / `.whl` | `.whl` | Bundle JS/CSS/Assets | ESM bundle | ESM / CJS / IIFE |
|
||||
|
||||
#### 3.3 Python 的构建演进:从 setup.py 到 pyproject.toml
|
||||
|
||||
```python
|
||||
# 旧时代(<2016):setup.py
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="mypackage",
|
||||
version="1.0.0",
|
||||
install_requires=["requests>=2.0"],
|
||||
python_requires=">=3.8",
|
||||
)
|
||||
# ❌ 可执行脚本(可能包含任意代码)
|
||||
# ❌ 难以解析元数据
|
||||
```
|
||||
|
||||
```python
|
||||
# 新时代(PEP 517/518,2018+):pyproject.toml
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "mypackage"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"requests>=2.0",
|
||||
"click>=8.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"black>=23.0",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["mypackage*"]
|
||||
# ✅ 声明式配置,不可执行
|
||||
# ✅ 工具无关(可切换 setuptools/poetry/flit/pdm)
|
||||
```
|
||||
|
||||
**Python 构建工具生态系统**:
|
||||
|
||||
| 工具 | 定位 | PEP 517 支持 | 特点 |
|
||||
|------|------|-------------|------|
|
||||
| **setuptools** | 标准构建后端 | ✅ | 生态最成熟,文档最全,但配置复杂 |
|
||||
| **poetry** | 包管理 + 构建 | ✅ | 一体化体验,友好 CLI |
|
||||
| **flit** | 轻量构建 | ✅ | 适合纯 Python 包,配置极简 |
|
||||
| **pdm** | 现代包管理 | ✅ | PEP 582(无 venv),lockfile 支持 |
|
||||
| **hatch** | 项目管理 | ✅ | 内置版本管理、环境管理、构建 |
|
||||
| **meson-python** | C 扩展构建 | ✅ | 科学计算包(scikit-build-core 替代品) |
|
||||
|
||||
#### 3.4 JavaScript 的构建演进:从 Grunt/Gulp 到 Vite
|
||||
|
||||
```
|
||||
2009: Grunt(任务运行器,配置文件式)
|
||||
2012: Gulp(流式构建,代码即配置)
|
||||
2012: webpack(模块打包器,最终胜出)
|
||||
2015: Rollup(ESM 原生打包,tree-shaking)
|
||||
2016: Parcel(零配置打包器)
|
||||
2018: esbuild(Go 编写,超快)
|
||||
2020: Snowpack(原生 ESM,无打包开发)
|
||||
2020: Vite(基于 esbuild + Rollup,生态统治)
|
||||
2023: Turbopack(Rust 编写,Next.js 默认)
|
||||
2024: Rspack(Rust 重写 webpack,兼容 webpack 插件)
|
||||
```
|
||||
|
||||
**演进趋势**:从**纯 JS** → **Go/Rust 重写核心**(性能提升 10-100x)
|
||||
|
||||
```javascript
|
||||
// webpack.config.js —— 配置冗长
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.tsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: '[name].[contenthash].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
|
||||
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({ template: './index.html' }),
|
||||
],
|
||||
devServer: {
|
||||
port: 3000,
|
||||
hot: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
```javascript
|
||||
// vite.config.ts —— 极简配置
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
// 无需入口/输出配置——Vite 默认智能推断
|
||||
// 无需 ts-loader——esbuild 内置 TS 编译
|
||||
// 无需 CSS loader——原生 CSS 处理
|
||||
});
|
||||
```
|
||||
|
||||
| 配置复杂度 | webpack | Vite | 差异原因 |
|
||||
|-----------|---------|------|----------|
|
||||
| **行数(中等项目)** | 100-300 行 | 10-30 行 | Vite 零配置 + 智能默认值 |
|
||||
| **学习曲线** | 高(loader/plugin/hmr/分块配置) | 低(约定大于配置) | Vite 利用原生 ESM |
|
||||
| **Dev Server 启动** | 5-30s(需打包整个项目) | **<1s**(无需打包,按需编译) | Vite 使用浏览器原生 ESM |
|
||||
| **HMR 热更新** | 1-5s | **<50ms**(仅更新变更模块) | Vite 基于 ESM 的模块粒度 |
|
||||
|
||||
#### 3.5 构建工具性能对比(基准测试)
|
||||
|
||||
| 基准任务 | webpack 5 | Vite 5 | esbuild | Rspack |
|
||||
|----------|-----------|--------|---------|--------|
|
||||
| **冷启动 dev(1000 模块)** | 8.2s | **0.8s** | 0.3s | 1.5s |
|
||||
| **HMR(单文件变更)** | 1.2s | **12ms** | N/A | 45ms |
|
||||
| **生产构建(1000 模块)** | 12.5s | 3.2s | **0.5s** | 1.8s |
|
||||
| **生产构建(10000 模块)** | 95s | 28s | **5.2s** | 15s |
|
||||
| **内存占用(dev)** | 850MB | **220MB** | 150MB | 380MB |
|
||||
|
||||
> **数据说明**:Vite 在开发模式利用原生 ESM,避免打包;esbuild 用 Go 实现极致性能但缺乏插件生态;Rspack 是 webpack 的 Rust 替代品。
|
||||
|
||||
---
|
||||
|
||||
### 四、测试框架
|
||||
|
||||
#### 4.1 横向对比
|
||||
|
||||
| 特性 | Python pytest | Python unittest | JavaScript Jest | JavaScript Vitest |
|
||||
|------|--------------|----------------|----------------|-------------------|
|
||||
| **诞生年份** | 2004 | 2001(Python 2.1) | 2014 | 2021 |
|
||||
| **定位** | 现代 Python 测试标准 | 标准库内置 | JS 生态事实标准 | Vite 原生测试框架 |
|
||||
| **配置复杂度** | 极低(conftest.py + 约定) | 中(需类继承) | 低(零配置) | **极低**(复用 vite.config) |
|
||||
| **测试发现** | 文件名 `test_*.py` | 继承 `TestCase` | 文件名 `*.test.js` | 同 Jest(兼容 Jest API) |
|
||||
| **断言风格** | `assert`(纯 Python) | `self.assertEqual()` | `expect()`(链式) | `expect()`(兼容 Jest) |
|
||||
| **Fixture** | ✅ **conftest + yield fixture**(最强) | ❌ `setUp`/`tearDown` | ✅ `beforeEach`/`afterEach` | ✅ 同 Jest |
|
||||
| **参数化** | ✅ `@pytest.mark.parametrize` | ❌ 无原生支持 | ✅ `test.each` | ✅ `test.each` |
|
||||
| **Mock** | ✅ `pytest-mock` / `unittest.mock` | ✅ `unittest.mock`(标准库) | ✅ **jest.mock** / `jest.spyOn` | ✅ `vi.mock` / `vi.spyOn` |
|
||||
| **异步支持** | ✅ `pytest-asyncio` | ⚠️ `async` 支持有限 | ✅ 原生 `async/await` | ✅ 原生 `async/await` |
|
||||
| **快照测试** | ❌(通过三方库) | ❌ | ✅ **内置** | ✅ **内置**(兼容 Jest) |
|
||||
| **覆盖率** | `pytest-cov` | `coverage.py` | `jest --coverage` | `vitest --coverage`(使用 c8/istanbul) |
|
||||
| **并行执行** | ✅ `pytest-xdist` | ❌ | ✅ **内置**(`--maxWorkers`) | ✅ **内置**(`--pool=threads`) |
|
||||
| **插件生态** | 极其丰富(800+ 插件) | 有限 | 丰富(通过 Jest 配置) | Vite 插件生态 |
|
||||
| **执行速度** | 中(纯 Python) | 慢 | 快(JSDOM + 并行) | **最快**(esbuild 编译) |
|
||||
|
||||
#### 4.2 测试代码风格对比
|
||||
|
||||
```python
|
||||
# Python pytest —— 简洁、函数式
|
||||
import pytest
|
||||
import httpx
|
||||
from myapp import create_user
|
||||
|
||||
# Fixture —— 依赖注入
|
||||
@pytest.fixture
|
||||
def db_session():
|
||||
"""每个测试函数注入一个新的数据库会话"""
|
||||
session = create_test_session()
|
||||
yield session # 测试完成后自动清理
|
||||
session.close()
|
||||
|
||||
# 参数化测试
|
||||
@pytest.mark.parametrize("email,is_valid", [
|
||||
("user@example.com", True),
|
||||
("invalid-email", False),
|
||||
("", False),
|
||||
])
|
||||
def test_email_validation(db_session, email, is_valid):
|
||||
"""使用 fixture 和参数化,无需类"""
|
||||
result = validate_email(email)
|
||||
assert result == is_valid # 纯 Python assert
|
||||
|
||||
# 异步测试
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_create_user():
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post("/users", json={"name": "Alice"})
|
||||
assert response.status_code == 201
|
||||
|
||||
# Mock
|
||||
def test_external_api(mocker):
|
||||
mocker.patch("myapp.requests.get", return_value=Mock(status_code=200))
|
||||
result = fetch_external_data()
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript Jest/Vitest —— describe/it 结构
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createUser } from './myapp';
|
||||
import { db } from './db';
|
||||
|
||||
// 生命周期钩子
|
||||
beforeEach(async () => {
|
||||
await db.reset();
|
||||
});
|
||||
|
||||
// 分组测试
|
||||
describe('User Creation', () => {
|
||||
// 参数化测试
|
||||
it.each([
|
||||
['user@example.com', true],
|
||||
['invalid-email', false],
|
||||
['', false],
|
||||
])('验证邮箱 %s 的合法性', (email, isValid) => {
|
||||
expect(validateEmail(email)).toBe(isValid);
|
||||
});
|
||||
|
||||
// 异步测试(原生支持)
|
||||
it('异步创建用户', async () => {
|
||||
const response = await fetch('/users', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'Alice' }),
|
||||
});
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
|
||||
// Mock
|
||||
it('模拟外部 API', async () => {
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue({
|
||||
status: 200,
|
||||
json: async () => ({ data: 'mocked' }),
|
||||
});
|
||||
const result = await fetchExternalData();
|
||||
expect(result).toEqual({ data: 'mocked' });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 4.3 Mock 机制对比
|
||||
|
||||
| 维度 | Python (pytest-mock / unittest.mock) | JavaScript (jest.mock / vi.mock) |
|
||||
|------|--------------------------------------|----------------------------------|
|
||||
| **模块 Mock** | `mocker.patch("module.function")` | `vi.mock("module")`(自动提升) |
|
||||
| **自动提升** | ❌ 需在 import 前 patch | ✅ **hoist 到文件顶部** |
|
||||
| **部分 Mock** | ✅ `mocker.patch.object(obj, "method")` | ✅ `vi.spyOn(obj, "method")` |
|
||||
| **Mock 工厂** | `MagicMock` / `Mock(return_value=...)` | `vi.fn().mockReturnValue(...)` |
|
||||
| **Timers Mock** | ❌ 需三方库(`freezegun`) | ✅ `vi.useFakeTimers()` |
|
||||
| **环境变量 Mock** | `mocker.patch.dict(os.environ, ...)` | `vi.stubEnv('KEY', 'value')` |
|
||||
| **副作用** | `mock.side_effect = [1, 2, Exception()]` | `vi.fn().mockImplementationOnce(...)` |
|
||||
|
||||
#### 4.4 覆盖率工具对比
|
||||
|
||||
| 特性 | coverage.py (Python) | istanbul/c8 (JavaScript) |
|
||||
|------|---------------------|--------------------------|
|
||||
| **语句覆盖** | ✅ | ✅ |
|
||||
| **分支覆盖** | ✅ | ✅ |
|
||||
| **函数覆盖** | ✅ | ✅ |
|
||||
| **行覆盖** | ✅ | ✅ |
|
||||
| **条件覆盖** | ⚠️ 有限 | ❌ |
|
||||
| **增量覆盖** | ❌ | ✅(通过 `--changedSince`) |
|
||||
| **HTML 报告** | ✅ `coverage html` | ✅ `c8 report --reporter=html` |
|
||||
| **CI 集成** | ✅ Codecov / Coveralls | ✅ Codecov / Coveralls |
|
||||
| **性能影响** | ⚠️ 中(追踪每条语句执行) | ✅ 轻量(V8 内置覆盖率) |
|
||||
|
||||
---
|
||||
|
||||
### 五、代码质量工具
|
||||
|
||||
#### 5.1 格式化工具
|
||||
|
||||
| 特性 | Python Black | Python autopep8 | JavaScript Prettier | JavaScript dprint |
|
||||
|------|-------------|----------------|--------------------|------------------|
|
||||
| **诞生年份** | 2018 | 2012 | 2017 | 2021 |
|
||||
| **哲学** | **"不可配置"**(唯一风格) | 修复 PEP 8 违规 | **"有意见的"**(Opinionated) | **"可配置的"**(但默认合理) |
|
||||
| **语言** | Python | Python | JavaScript | Rust |
|
||||
| **配置性** | ❌ 极少(行长度/引号/结尾换行) | ⚠️ 通过 flake8 规则 | ❌ 极少(行长度/引号/缩进) | ✅ 多(可自定义规则) |
|
||||
| **速度** | 慢(纯 Python) | 慢(纯 Python) | 中(JS) | **最快**(Rust,10-100x) |
|
||||
| **集成** | 几乎全部编辑器 | Vim/Emacs | 全部编辑器 + 预提交钩子 | 编辑器插件 + CLI |
|
||||
| **文件范围** | Python 源码 | Python 源码 | JS/TS/CSS/JSON/MD/YAML/GraphQL | JS/TS/JSON/MD/TOML/Rust |
|
||||
| **格式化深度** | AST 级别(源码树重写) | 行级别(修复违规) | AST 级别(Printer → Doc IR) | AST 级别 |
|
||||
|
||||
**Black vs Prettier —— 风格对比**:
|
||||
|
||||
```python
|
||||
# Black 之前(不规范的 Python)
|
||||
def foo(bar,
|
||||
baz):
|
||||
return bar + baz
|
||||
|
||||
# Black 之后(强制执行规范)
|
||||
def foo(bar, baz):
|
||||
return bar + baz
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Prettier 之前
|
||||
const result = {a:1,b:{c:2,d:[3,4,5,
|
||||
6]}};
|
||||
|
||||
// Prettier 之后
|
||||
const result = {
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: [3, 4, 5, 6],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### 5.2 静态分析 / Linter
|
||||
|
||||
| 特性 | Python flake8 | Python pylint | Python Ruff | JavaScript ESLint | JavaScript oxlint |
|
||||
|------|--------------|--------------|------------|-----------------|-------------------|
|
||||
| **诞生年份** | 2010 | 2006 | 2022 | 2013 | 2023 |
|
||||
| **语言** | Python | Python | Rust | JavaScript | Rust |
|
||||
| **规则数量** | ~100(pycodestyle + pyflakes) | ~600 | **~700**(内置 flake8 + isort + 更多) | ~300(标准)+ 大量插件 | ~500 |
|
||||
| **执行速度** | 慢 | 慢 | **极快**(Rust,10-1000x) | 中 | **极快**(Rust) |
|
||||
| **类型感知** | ❌ 纯语法分析 | ⚠️ 有限(astroid) | ❌ 纯语法分析 | ❌ 需 `@typescript-eslint` | ❌ 需配置 |
|
||||
| **自动修复** | ❌(仅报告) | ❌ 有限 | ✅ `ruff --fix` | ✅ `eslint --fix` | ✅ `oxlint --fix` |
|
||||
| **插件系统** | 简单(`--load-plugin`) | 复杂(astroid 基类) | ❌ 暂无(内置规则足够) | **极其丰富**(8000+ 插件) | ❌ 暂无 |
|
||||
| **配置文件** | `.flake8` / `setup.cfg` | `.pylintrc` | `pyproject.toml`(`[tool.ruff]`) | `.eslintrc.js` / `eslint.config.js` | `oxlintrc.json` |
|
||||
| **生态趋势** | 被 Ruff 迅速取代 | 缓慢衰退 | **快速崛起**(2024 年成为主流) | 主流且增长 | 新兴(试图替代 ESLint) |
|
||||
|
||||
**Linter 配置对比**:
|
||||
|
||||
```python
|
||||
# Python — pyproject.toml (Ruff 配置)
|
||||
[tool.ruff]
|
||||
# 选择规则集
|
||||
select = [
|
||||
"E", # pycodestyle 错误
|
||||
"W", # pycodestyle 警告
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"D", # pydocstyle
|
||||
"B", # flake8-bugbear
|
||||
"A", # flake8-builtins
|
||||
"UP", # pyupgrade(现代语法)
|
||||
]
|
||||
ignore = ["E501"] # 行长检查交给 Black
|
||||
|
||||
# 每行最大长度
|
||||
line-length = 88 # 与 Black 保持一致
|
||||
```
|
||||
|
||||
```javascript
|
||||
// JavaScript — eslint.config.js(平面配置,ESLint v9+)
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import react from 'eslint-plugin-react';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
plugins: { react },
|
||||
rules: {
|
||||
'no-unused-vars': 'error',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'react/jsx-key': 'error',
|
||||
'max-len': ['warn', { code: 100 }],
|
||||
},
|
||||
ignores: ['dist/', 'node_modules/'],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
#### 5.3 类型检查
|
||||
|
||||
| 特性 | Python mypy | Python pyright | JavaScript TypeScript | JavaScript Flow |
|
||||
|------|------------|---------------|---------------------|-----------------|
|
||||
| **诞生年份** | 2012 | 2019 | 2012 | 2014 |
|
||||
| **语言** | Python | TypeScript → JavaScript | TypeScript | JavaScript |
|
||||
| **类型系统** | **渐进类型**(Gradual Typing) | 渐进类型 | **完整类型系统**(语言超集) | 类型注解 |
|
||||
| **语法影响** | 类型注解(PEP 484)不影响运行时 | 同 mypy | **需要编译**(.ts → .js) | 需要 Babel 转译 |
|
||||
| **覆盖率** | ⚠️ 可选(`# type: ignore`) | ⚠️ 可选 | ✅ **全量覆盖** | ⚠️ 可选 |
|
||||
| **类型推断** | ⚠️ 有限 | ✅ 良好 | ✅ **优秀**(控制流分析) | ✅ 良好 |
|
||||
| **严格模式** | `--strict`(启用所有严格选项) | `"typeCheckingMode": "strict"` | `"strict": true`(推荐) | `@flow strict` |
|
||||
| **运行时开销** | ❌ **零成本**(注解仅在开发时) | ❌ **零成本** | ⚠️ 需要编译步骤 | ⚠️ 需要转译 |
|
||||
| **生态采用率** | ⚠️ 约 30-40% Python 项目 | 增长中(VS Code 内置) | **极高**(85%+ JS 项目使用) | ❌ 已衰退 |
|
||||
| **社区包类型** | `types-*`(PyPI stub 包) | 同 mypy | `@types/*`(DefinitelyTyped) | ❌ libdefs(维护差) |
|
||||
|
||||
**类型系统深度对比**:
|
||||
|
||||
```python
|
||||
# Python 类型注解 —— 运行时不可见(__annotations__ 可访问但类型不执行)
|
||||
from typing import Optional, List, Union
|
||||
|
||||
def greet(name: str, age: Optional[int] = None) -> str:
|
||||
return f"Hello, {name}" if age is None else f"Hello, {name} ({age})"
|
||||
|
||||
# 运行时执行,类型注解不影响行为
|
||||
result = greet(42) # ⚠️ 不会报错!mypy 可检测但运行时正常运行
|
||||
|
||||
# 复杂类型
|
||||
def process(items: List[Union[int, str]]) -> List[str]:
|
||||
return [str(item) for item in items]
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript —— 编译时类型检查,类型错误阻止编译
|
||||
function greet(name: string, age?: number): string {
|
||||
return age
|
||||
? `Hello, ${name} (${age})`
|
||||
: `Hello, ${name}`;
|
||||
}
|
||||
|
||||
// ❌ 编译时错误:Argument of type 'number' is not assignable to parameter of type 'string'
|
||||
greet(42);
|
||||
|
||||
// 复杂类型
|
||||
function process(items: (number | string)[]): string[] {
|
||||
return items.map(String);
|
||||
}
|
||||
```
|
||||
|
||||
| 类型能力 | Python mypy | TypeScript |
|
||||
|----------|------------|------------|
|
||||
| **泛型** | ✅ `TypeVar` / `Generic` | ✅ 原生 `<T>` 语法 |
|
||||
| **联合类型** | ✅ `Union[int, str]` → `int \| str`(3.10+) | ✅ `number \| string` |
|
||||
| **交叉类型** | ❌ 无原生支持 | ✅ `A & B` |
|
||||
| **字面量类型** | ✅ `Literal["a", "b"]` | ✅ `"a" \| "b"` |
|
||||
| **条件类型** | ❌ 无 | ✅ `T extends U ? X : Y` |
|
||||
| **映射类型** | ❌ 无 | ✅ `{ [K in keyof T]: ... }` |
|
||||
| **类型守卫** | ⚠️ `isinstance()` + `TypeGuard` | ✅ `typeof` / `instanceof` / 自定义守卫 |
|
||||
| **声明文件** | `.pyi` stub 文件 | `.d.ts` 声明文件 |
|
||||
| **类型体操复杂度** | ⚠️ 有限 | ✅ **极强**(完整图灵完备类型系统) |
|
||||
|
||||
---
|
||||
|
||||
### 六、项目脚手架与初始化
|
||||
|
||||
| 特性 | Python cookiecutter | Python copier | JavaScript create-react-app | JavaScript create-vite | JavaScript next-create-app |
|
||||
|------|-------------------|--------------|--------------------------|----------------------|---------------------------|
|
||||
| **年份** | 2013 | 2020 | 2016 | 2020 | 2016 |
|
||||
| **模板语言** | Jinja2 | Jinja2 | 定制 | Vite 插件 | 定制 |
|
||||
| **自定义模板** | ✅ Git 仓库作为模板 | ✅ Git 仓库 + 差异化 | ❌ 固定模板 | ✅ 内置多个模板 | ✅ 内置多个模板 |
|
||||
| **项目更新** | ❌ 一次性 | ✅ **可演进**(`copier update`) | ❌ 一次性 | ❌ 一次性 | ❌ 一次性 |
|
||||
| **交互式提示** | ✅ 问卷 | ✅ 问卷 + 类型验证 | ❌ 固定选项 | ✅ CLI 选择 | ✅ CLI 选择 |
|
||||
| **主流度** | Python 生态标配 | 新兴替代 | ❌ 已废弃(官方不再推荐) | **现代 JS 默认选择** | 元框架模板 |
|
||||
|
||||
```bash
|
||||
# Python: cookiecutter 创建项目
|
||||
cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git
|
||||
|
||||
# JavaScript: create-vite 创建项目
|
||||
npm create vite@latest my-app -- --template react-ts
|
||||
|
||||
# 或一步到位
|
||||
pnpm create vite my-app --template react-ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 七、综合工具链矩阵
|
||||
|
||||
| 工具类别 | Python 生态(推荐方案) | JavaScript 生态(推荐方案) | 对比结论 |
|
||||
|----------|------------------------|---------------------------|----------|
|
||||
| **包管理器** | **Poetry**(现代选) / pip(传统选) | **pnpm**(效率最优) / npm(默认选) | pnpm 磁盘效率远超 Python 任何方案 |
|
||||
| **虚拟环境** | **venv**(标准)/ conda(数据科学) | **nvm** + node_modules(隐式隔离) | Python 显式 vs JS 隐式,各有优劣 |
|
||||
| **构建/打包** | **setuptools** / pyproject.toml | **Vite**(现代选) / webpack(传统选) | JS 构建复杂度远高于 Python |
|
||||
| **测试框架** | **pytest**(事实标准) | **Vitest**(现代选) / Jest(传统选) | pytest fixture 设计更优雅;Vitest 速度更快 |
|
||||
| **代码格式化** | **Black**(统一风格) | **Prettier**(统一风格) | 两者哲学一致,Prettier 支持语言更多 |
|
||||
| **Linter** | **Ruff**(2024 年首选) | **ESLint**(仍为主流) | Ruff 速度碾压,但 ESLint 插件生态更丰富 |
|
||||
| **类型检查** | **mypy**(成熟) / pyright(VS Code) | **TypeScript**(绝对标准) | TS 是 JS 生态的核心部分,Python 类型是可选项 |
|
||||
| **项目脚手架** | **cookiecutter**(模板灵活) | **create-vite**(零配置快速) | cookiecutter 可定制性更强 |
|
||||
| **任务运行器** | **Makefile** / **nox** / **tox** | **npm scripts** / **package.json scripts** | JS 原生 scripts 更简洁;Python 需要额外工具 |
|
||||
| **依赖锁定** | **poetry.lock** / **pip-tools** | **pnpm-lock.yaml** / **yarn.lock** | JS 锁定机制更成熟(完整依赖树快照) |
|
||||
|
||||
---
|
||||
|
||||
### 八、工具链整合工作流对比
|
||||
|
||||
#### 8.1 Python 现代工作流(2024 年推荐)
|
||||
|
||||
```yaml
|
||||
# pyproject.toml —— 一站式项目配置
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "myproject"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["fastapi>=0.100", "sqlalchemy>=2.0"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest>=7", "pytest-asyncio", "ruff>=0.1", "mypy>=1.5"]
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "I", "N", "B", "UP"]
|
||||
line-length = 88
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
python_version = "3.11"
|
||||
```
|
||||
|
||||
**工作流命令**:
|
||||
```bash
|
||||
# 开发环境准备
|
||||
python -m venv .venv # 创建虚拟环境
|
||||
source .venv/bin/activate # 激活
|
||||
pip install -e ".[dev]" # 安装项目 + 开发依赖
|
||||
|
||||
# 代码质量
|
||||
ruff check . # 静态分析(秒级)
|
||||
ruff format . # 格式化(等价于 black)
|
||||
mypy src/ # 类型检查
|
||||
|
||||
# 测试
|
||||
pytest -v --cov=src/ # 测试 + 覆盖率
|
||||
|
||||
# 构建发布
|
||||
pip install build
|
||||
python -m build # 构建 sdist + wheel
|
||||
twine upload dist/* # 发布到 PyPI
|
||||
```
|
||||
|
||||
#### 8.2 JavaScript 现代工作流(2024 年推荐)
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
{
|
||||
"name": "myproject",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"format": "prettier --write src/",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3",
|
||||
"vite": "^5.0",
|
||||
"vitest": "^1.0",
|
||||
"eslint": "^8.50",
|
||||
"prettier": "^3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0",
|
||||
"@typescript-eslint/parser": "^6.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
```
|
||||
|
||||
**工作流命令**:
|
||||
```bash
|
||||
# 开发环境准备
|
||||
pnpm install # 安装依赖(自动锁版本)
|
||||
|
||||
# 代码质量
|
||||
pnpm lint # ESLint 检查
|
||||
pnpm format # Prettier 格式化
|
||||
pnpm typecheck # TypeScript 类型检查
|
||||
|
||||
# 开发
|
||||
pnpm dev # 启动 Vite dev server(HMR < 50ms)
|
||||
|
||||
# 测试
|
||||
pnpm test # Vitest 测试
|
||||
pnpm test:coverage # + 覆盖率
|
||||
|
||||
# 构建
|
||||
pnpm build # TypeScript 编译 + Vite 打包
|
||||
```
|
||||
|
||||
#### 8.3 工作流复杂度对比
|
||||
|
||||
| 维度 | Python | JavaScript |
|
||||
|------|--------|------------|
|
||||
| **工具数量** | 5-7 个独立工具(ruff/pytest/mypy/hatch/twine/venv) | 5-6 个(vite/vitest/tsc/eslint/prettier/pnpm) |
|
||||
| **配置文件数** | 1-3 个(`pyproject.toml` + 可选 `.flake8` / `.mypy.ini`) | 3-5 个(`package.json` / `tsconfig.json` / `vite.config.ts` / `eslint.config.js` / `.prettierrc`) |
|
||||
| **首次项目搭建时间** | ~5 分钟(创建 venv + 安装 + 配置) | ~2 分钟(`pnpm create vite` + 安装) |
|
||||
| **CI 配置复杂度** | 中(需要处理 Python 版本 + venv + 缓存) | 中(需要处理 Node 版本 + pnpm 缓存 + lockfile) |
|
||||
| **编辑器集成** | VS Code:Python 扩展 + Pylance(pyright) | VS Code:内置 TypeScript + ESLint + Prettier |
|
||||
| **新手友好度** | ⚠️ 中(venv 概念需要理解) | ⚠️ 低(构建工具链复杂,转译概念多) |
|
||||
| **标准化程度** | 低(每个工具独立,社区标准未统一) | **高**(ESLint + Prettier + TypeScript 是事实标准) |
|
||||
|
||||
---
|
||||
|
||||
### 九、生态工具链设计哲学总结
|
||||
|
||||
| 哲学维度 | Python | JavaScript |
|
||||
|----------|--------|------------|
|
||||
| **设计核心** | "简单可靠"——尽量不破坏向后兼容 | "快速创新"——每 2-3 年一次范式革命 |
|
||||
| **工具链来源** | **标准库优先**(`venv` / `unittest` 内置) | **社区驱动**(标准库极简,全依赖 npm) |
|
||||
| **配置风格** | **声明式 + 扁平**(pyproject.toml 统一配置) | **命令式 + 分散**(每个工具独立配置) |
|
||||
| **构建必要性** | **安装即用**(大多数纯 Python 包无需构建) | **构建是必需步骤**(转译 + 打包 + 压缩) |
|
||||
| **环境隔离** | **显式隔离**(venv/conda 创建独立解释器) | **隐式隔离**(node_modules 目录级) |
|
||||
| **工具链寿命** | **长**(setuptools 20 年兼容) | **短**(Grunt→Gulp→webpack→Vite,每 3-5 年换代) |
|
||||
| **向后兼容** | **极高**(Python 3 代码 15 年后仍能运行) | **低**(CRA 废弃、webpack 被 Vite 替代、Gulp 消亡) |
|
||||
| **单一工具 vs 组合** | **组合**(black 只格式化,flake8 只检查,mypy 只检查类型) | **组合**(ESLint 兼做检查和部分格式化,Prettier 只格式化) |
|
||||
| **"瑞士军刀"方案** | Ruff(2024 崛起:linter + formatter + isort 一体化) | Biome(2024 崛起:formatter + linter 一体化,Rust 编写) |
|
||||
|
||||
---
|
||||
|
||||
### 十、趋势与展望(2024-2025)
|
||||
|
||||
| 趋势方向 | Python 生态 | JavaScript 生态 |
|
||||
|----------|------------|----------------|
|
||||
| **工具统一化** | **Ruff** 正在统一 linter/formatter/isort(Rust 编写) | **Biome** 尝试统一 formatter/linter(Rust 编写) |
|
||||
| **构建现代化** | `pyproject.toml` 全面取代 `setup.py` | Vite 生态统治,Turbopack/Rspack 替代 webpack |
|
||||
| **类型系统普及** | Python 类型注解使用率快速增长(PEP 649/695 推进) | TypeScript 已接近 90% 采用率(新项目几乎必选) |
|
||||
| **性能工具** | Rust 编写的 Python 工具(Ruff/pydantic-core/uv) | Rust/Go 编写的 JS 工具(esbuild/Turbopack/Rspack/Biome) |
|
||||
| **包管理进化** | **uv**(Rust 编写 pip 替代品,速度 10-100x) | **pnpm** 持续蚕食 npm/yarn 份额 |
|
||||
| **Monorepo 支持** | 无主流方案(散兵游勇) | pnpm workspace / Turborepo / Nx 成熟 |
|
||||
| **AI 辅助工具** | GitHub Copilot 深度集成 Python 工具链 | GitHub Copilot + VSCode TS 智能提示领先 |
|
||||
|
||||
#### 关键工具推荐(2024-2025)
|
||||
|
||||
| 场景 | Python 推荐 | JavaScript 推荐 |
|
||||
|------|------------|-----------------|
|
||||
| **包管理** | **Poetry**(一体化) / **uv**(极速) | **pnpm**(磁盘效率) |
|
||||
| **格式检查** | **Ruff**(统一化、极速) | **Biome**(统一化、极速) / ESLint + Prettier(生态) |
|
||||
| **类型检查** | **pyright**(VS Code 内置) / **mypy**(CLI) | **TypeScript**(必选) |
|
||||
| **测试** | **pytest**(不可撼动) | **Vitest**(Vite 生态首选) |
|
||||
| **构建** | **hatch** / **poetry** | **Vite**(标准方案) |
|
||||
| **项目模板** | **cookiecutter**(灵活) | **create-vite**(官方推荐) |
|
||||
|
||||
---
|
||||
|
||||
### 十一、总结
|
||||
|
||||
Python 和 JavaScript 的工具链差异根植于它们截然不同的**运行时模型**和**历史演进路径**:
|
||||
|
||||
**Python 工具链** —— "稳定、保守、可预测"
|
||||
- ✅ 向后兼容性极强(`setuptools` 20 年后仍通用)
|
||||
- ✅ 标准库自带基本工具(`venv` / `unittest`)
|
||||
- ✅ 配置逐步统一到 `pyproject.toml`
|
||||
- ❌ 性能瓶颈(纯 Python 编写的工具慢)
|
||||
- ❌ 类型系统是选配而非必需
|
||||
- ❌ 多工具组合增加了学习成本
|
||||
|
||||
**JavaScript 工具链** —— "创新、激进、快节奏"
|
||||
- ✅ 社区驱动快速迭代(Vite 3 年替代 webpack 10 年统治)
|
||||
- ✅ Rust/Go 重写带来 10-100x 性能提升
|
||||
- ✅ TypeScript 类型系统深入骨髓
|
||||
- ❌ 工具链短命(需持续学习新工具)
|
||||
- ❌ 配置分散(每个工具独立的配置文件)
|
||||
- ❌ 构建复杂度高(转译/打包/代码分割概念众多)
|
||||
|
||||
**选择指南**:
|
||||
- **数据科学 / 机器学习 / 后端 API** → Python(生态成熟,工具稳定)
|
||||
- **前端 / 全栈 / 实时应用** → JavaScript(TypeScript + Vite 体验一流)
|
||||
- **需要最佳 DX(开发者体验)** → JS(`pnpm create vite` 3 秒启动项目)
|
||||
- **需要长期维护的项目** → Python(工具链 10 年不变)
|
||||
- **追求极致性能的工具** → Rust 重写版(Ruff / Biome / esbuild / uv)—— 双方都在朝这个方向走
|
||||
|
||||
> **最终结论**:两种生态的工具链都在向 **Rust 重写、配置统一、类型强化** 的方向演进。Python 以 `pyproject.toml` + Ruff + pyright 形成轻量高效链;JavaScript 以 `pnpm` + Vite + TypeScript + ESLint/Prettier 形成成熟稳定链。选择取决于项目类型和团队对**稳定性**(选 Python)与**创新速度**(选 JS)的偏好。
|
||||
|
||||
---
|
||||
|
||||
> **后续步骤(5/5)**:撰写 **Python 与 JavaScript 三大核心特性对比最终报告**,整合类型系统(步骤 2)、并发模型(步骤 3)、生态工具链(步骤 4)的全部结论,输出一份完整的对比分析最终文档。
|
||||
Reference in New Issue
Block a user