Merge branch 'dhk-1' into 'master'
圆桌 Step2:固定协调员单例、优化调度性能、修正研讨中状态 See merge request knowledge-base-bj/deerflow-server!71
This commit is contained in:
commit
e68b726f9a
479 files changed
+66794
-3014
No files matched your search
@@ -16,7 +16,13 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat
|
||||
VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat
|
||||
VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary
|
||||
VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web
|
||||
VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||
|
||||
@@ -10,7 +10,10 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat
|
||||
VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat
|
||||
VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary
|
||||
VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web
|
||||
VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
# --- 生产打包(见 .env.production)---
|
||||
# 部署前修改 .env.production 中的地址,再执行 pnpm build
|
||||
|
||||
# VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
|
||||
# VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
||||
# VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||
# VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
|
||||
# 可选
|
||||
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
||||
# VITE_STATIC_WEBSITE_ONLY=
|
||||
|
||||
@@ -14,7 +14,10 @@ VITE_AIPI_GENERATE_CHAT_URL=https://ch1.b.uat.4.cn/jialai_b1_chat
|
||||
VITE_AIPI_TASK_CHAT_URL=https://ch1.b.uat.4.cn/qq_b1_chat
|
||||
VITE_AIPI_KNOWLEDGE_URL=https://chlib.b.uat.4.cn/knowlibrary
|
||||
VITE_AIPI_B1COH_URL=https://ch1.b.uat.4.cn/web
|
||||
VITE_AIPI_B2ACTOR_URL=htttps://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_URL=https://ty.b.uat.4.cn
|
||||
VITE_AIPI_B2ACTOR_XWT_PATH=/fromB/toXWT?isFrame=1
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_API_URL=https://ch1.b.uat.4.cn/knowes/api/iframeKnowledgeUrl
|
||||
VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
- 三个圆桌 Router(`intent / recommend / multi_agent`)**通过 httpx ASGITransport 进程内直调本机 ASGI app**,复用既有 `/api/threads`、`/api/agents`、`/api/threads/{id}/runs/stream` 路由——**不走 TCP / 不经过网络栈**。所有 client 都由 `multi_agent._make_loopback_client(request)` 工厂统一构造。
|
||||
- 历史包袱:早期实现走 `httpx.AsyncClient(trust_env=False)` + `127.0.0.1:<port>`,遇到开发机 `HTTP_PROXY` / `HTTPS_PROXY` 时仍可能把回环走代理(症状 `headers_in=42.160s`)。**改用 ASGITransport 后此问题不再存在**,相关 `trust_env=False` workaround 全部移除。
|
||||
- **席位 agent 持久注册**(磁盘 `.deer-flow/agents/` + `_sync_legacy_agents`),**协调智能体每次新建**(命名 `roundtable-coordinator-{timestamp36}`,DB 累积,需后续 GC)。
|
||||
- **`.deer-flow/` 整个目录被 git ignore**(仓库根 `.gitignore` 第 64 / 74 行),`git clone` 后内置 agent 目录在部署机上**不会存在**——任何环境都必须把 `roundtable-intent` + `roundtable-recommender` 两个**功能型 agent** 持久化进发布物,否则 `/api/intent/stream` 报 500。**互联网 demo 还需要 6 个示例研讨席位**(`roundtable-intelligence` 等),**内网部署用客户自有 agent 替代**这些席位,无需打包。详见 §11.7。
|
||||
- **席位 agent 持久注册**(磁盘 `.deer-flow/agents/` + `_sync_legacy_agents`),**协调智能体也是持久单例**(id 固定为 `roundtable-coordinator`,与 intent/recommender 同样走 `_roundtable_seed.ensure_roundtable_functional_agents()` 缺失自动落地)。2026-05 之前曾按 `roundtable-coordinator-{timestamp36}` 每会话新建并把动态席位列表写进 SOUL,导致 `.deer-flow/agents/` 累积大量壳目录;改造后 SOUL 是静态通用版本,「本次可调度席位列表」由 `multi_agent.init` 在 coordinator thread 创建后以 human 消息追加到对话历史,leader run 每轮都能看到。
|
||||
- **`.deer-flow/` 整个目录被 git ignore**(仓库根 `.gitignore` 第 64 / 74 行),`git clone` 后内置 agent 目录在部署机上**不会存在**。但功能型 agent 现在通过 `_roundtable_seed.ensure_roundtable_functional_agents()` **自动落地**(首次任何 intent/recommend/multi-agent 路由调用都会触发),因此**新部署不再需要手动准备** `roundtable-intent` / `roundtable-recommender` / `roundtable-coordinator` 三个目录——它们都是 seeder 自动写盘的。**互联网 demo 仍需要 6 个示例研讨席位**(`roundtable-intelligence` 等),**内网部署用客户自有 agent 替代**这些席位,无需打包。详见 §11.7。
|
||||
- **`init` 是 SSE 而非一次性 JSON**:用 `asyncio.as_completed` 按真实完成顺序推送 `seat_ready`,前端因此能逐个点亮席位。
|
||||
- **`init_done.thread_ids` 包含协调智能体自身**(`agent_threads[main_agent_name] = coord_thread`),前端 Step 3 列文件时需自行过滤。
|
||||
- **澄清协议统一**:Step 1 与 Step 2 leader 都借 `ask_clarification` + 终态 `clarification` 帧驱动同一套选项卡片组件;`allow_multiple` 默认 false(单选、点击即发送)。
|
||||
@@ -282,33 +282,50 @@ offline-backend-20260512/backend/
|
||||
请求体:
|
||||
{
|
||||
"agent_names": ["roundtable-intelligence", ...], // 用户选的席位 id
|
||||
"main_agent_name": "roundtable-coordinator-xxx", // 当次总控 id,前端生成
|
||||
"model": "deepseek-chat" // 可选,写入 coordinator agent
|
||||
"main_agent_name": "<可选,已废弃>", // 历史字段;2026-05 改造后后端忽略,统一用 COORDINATOR_AGENT_ID
|
||||
"model": "deepseek-chat" // 可选;不传由 lead_agent 回退到 config.yaml models[0]
|
||||
}
|
||||
```
|
||||
|
||||
**协调智能体改造说明(2026-05)**:以前 `main_agent_name` 由前端用时间戳生成
|
||||
(`roundtable-coordinator-${Date.now().toString(36)}`),后端 `_create_coordinator_direct`
|
||||
把动态拼接的席位列表 SOUL 写盘并入 DB,导致 `.deer-flow/agents/` 越用越多。改造后:
|
||||
|
||||
- **id 固定**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"`(前后端常量必须保持一致);
|
||||
- **缺失自动落地**:路由入口调 `ensure_roundtable_functional_agents()`,与 intent/recommender 共用同一套 seeder;
|
||||
- **席位列表运行时注入**:init 创建好 coordinator thread 之后,立刻通过 `_append_thread_message` 把「【圆桌系统初始化】本次可调度席位:…」作为 human 消息追加到 thread 历史最前面,leader run 每轮都能看到;
|
||||
- **`main_agent_name` 入参保留为向后兼容**:旧 client 仍可传,后端只是日志里看到不会用它创建新 agent。
|
||||
|
||||
**早期校验**(同步抛 HTTP 4xx,**不**进 SSE):
|
||||
|
||||
- `agent_names` 必须非空
|
||||
- 每个 `agent_names` 元素 + `main_agent_name` 都需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好)
|
||||
- 每个 `agent_names` 元素需通过 `_AGENT_ID_PATTERN = ^[A-Za-z0-9_-]+$`(用 `_validate_agent_id` 提前 400;上游若收到不合规 id 会抛 422,提前拦更友好)
|
||||
- 所有 id 都 `.lower()` 后比较 / 存储
|
||||
- `main_agent_name` 入参不再做校验(被忽略,统一用 `COORDINATOR_AGENT_ID` 常量)
|
||||
|
||||
**SSE 内部流程**:
|
||||
**SSE 内部流程**(2026-05 单例改造后):
|
||||
|
||||
```
|
||||
ensure_roundtable_functional_agents()
|
||||
// 路由入口:保证 .deer-flow/agents/roundtable-coordinator/ 存在
|
||||
// 缺失时由 _roundtable_seed.COORDINATOR_AGENT_SOUL 落地静态 SOUL
|
||||
|
||||
asyncio.as_completed(各席位 _prepare_sub_agent)
|
||||
每完成一个 → data: {"event":"seat_ready","agent_name","thread_id","display_name","description"}
|
||||
(按真实完成顺序,先完成先发,前端因此能逐个点亮)
|
||||
|
||||
异常时:取消所有未完成的 task,避免泄露到 AsyncClient close 之后
|
||||
|
||||
协调智能体:
|
||||
POST /api/threads → coord_thread
|
||||
POST /api/agents
|
||||
skills=["agent_orchestration"]
|
||||
soul=动态拼接(本次 init 的席位列表 + 派活规则)
|
||||
id=main_agent_name(前端生成的 roundtable-coordinator-<ts36>)
|
||||
协调智能体(单例):
|
||||
main_agent_name = COORDINATOR_AGENT_ID # 固定常量,不再每次新建 agent
|
||||
coord_thread = _create_thread_direct(...) # 仍然每次新 thread,会话独立
|
||||
agent_threads[main_agent_name] = coord_thread // ⚠️ thread_ids 含 coordinator 自身
|
||||
|
||||
# 通过 HTTP loopback append 一条 human 消息,把本次席位清单写入对话历史:
|
||||
# "【圆桌系统初始化】本次研讨可调度的席位清单如下..."
|
||||
# leader run 每轮都能从对话历史里读到,等效于旧版动态 SOUL 的作用。
|
||||
_append_thread_message(seed_client, ..., coord_thread, seat_introduction)
|
||||
|
||||
→ data: {"event":"coordinator_ready","main_agent_name","thread_id"}
|
||||
|
||||
→ data: {"event":"init_done","main_agent_name","thread_ids":{...}}
|
||||
@@ -316,28 +333,26 @@ asyncio.as_completed(各席位 _prepare_sub_agent)
|
||||
失败 → data: {"event":"error","detail":"..."}
|
||||
```
|
||||
|
||||
**协调智能体创建幂等**:若 `_create_agent` 返回 (False, "exists ...")(同名 agent 已存在)则继续往下走,不报错。注意这里的「已存在」会复用历史 agent 的 SOUL(与本次动态拼接的不同),通常只在前端用同一个时间戳跑过两遍时触发。
|
||||
**协调智能体单例 SOUL**(磁盘上唯一一份,见 `_roundtable_seed.COORDINATOR_AGENT_SOUL`):
|
||||
|
||||
**协调智能体 SOUL**(动态拼接,与席位列表一起写入):
|
||||
通用协调规则,**不含**席位列表。模型派活时从对话历史最前面的「【圆桌系统初始化】…」消息读取本次可用 agent_name。仍然强调英文 id(避免把中文显示名拼进 `agent_name`),靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。
|
||||
|
||||
**席位清单注入消息形状**(init 阶段写入 coordinator thread):
|
||||
|
||||
```
|
||||
#核心定位:
|
||||
用途:根据用户的任务目标,调用其他智能体干活,只负责协调汇总,
|
||||
当你觉得已有信息支持回答用户问题时候,停止调度,并输出最后的结果
|
||||
|
||||
#调度方法:
|
||||
通过使用 agent_orchestration 技能调用其他智能体干活,使用技能时传入
|
||||
agent_name 和需要做的活。
|
||||
⚠️ agent_name 必须严格使用下方列出的英文 id(例如 roundtable-intelligence),
|
||||
不要把括号里的中文显示名拼进 agent_name 里。
|
||||
|
||||
#可调度智能体:
|
||||
- agent_name: roundtable-intelligence(情报收集):<description>
|
||||
- agent_name: roundtable-environment(环境评估):<description>
|
||||
【圆桌系统初始化】本次研讨可调度的席位清单如下,请严格按 agent_name 通过 agent_orchestration 技能派活,不要派给不在清单内的 agent:
|
||||
- agent_name: roundtable-intelligence(情报收集):<description>
|
||||
- agent_name: roundtable-environment(环境评估):<description>
|
||||
...
|
||||
```
|
||||
|
||||
中文显示名通过 `GET /api/agents/{id}` 取 `name`、描述取 `description`。**强调英文 id** 是因为部分模型会把整段 `roundtable-x(中文)` 当 `agent_name` 塞回来,靠后端 `_strip_display_suffix` 兜底(详见 §6.4)。
|
||||
中文显示名通过 `_get_agents_batch_direct` 一次 DB 查询取 `name`、描述取 `description`。
|
||||
|
||||
**单例 agent 的几个语义保障**:
|
||||
|
||||
1. **agent 落地幂等**:`_ensure_one` 检查目录是否存在,存在就 no-op,不覆盖运维手改的 SOUL;
|
||||
2. **席位列表 append 失败不阻断**:`_append_thread_message` 包在 try/except 里,失败只记 warning,leader run 仍能跑(只是看不到席位清单,可能派活到错的 id,由 `_validate_agent_id` 在 run/stream 里报错);
|
||||
3. **`_create_coordinator_direct` 已删除**:旧函数已移除(2026-05 改造),避免后续维护者误调;如果你正在 review 旧 PR 看到这个函数,那是 pre-rebase 版本。
|
||||
|
||||
### 6.2 `/run/stream` — `agent_type = "leader"`(`_leader_run`)
|
||||
|
||||
@@ -609,13 +624,20 @@ httpx.AsyncClient(
|
||||
|
||||
**已废止的 `trust_env=False` workaround**:旧版本走 `127.0.0.1:<port>` 真实 HTTP,需要 `trust_env=False` 阻止 httpx 读 `HTTP_PROXY` 把回环也走代理(症状日志 `headers_in=42.160s`)。改 ASGITransport 后这个问题在物理上已不存在——根本没走网络栈。
|
||||
|
||||
### 9.2 协调智能体每次新建
|
||||
### 9.2 协调智能体单例(2026-05 改造)
|
||||
|
||||
`main_agent_name` 形如 `roundtable-coordinator-{timestamp36}`,会话隔离;长期会累积 agent 目录(实际仓库已有数十个 `roundtable-coordinator-mp*`),需后续 GC 策略:
|
||||
历史实现 `main_agent_name = roundtable-coordinator-{timestamp36}`,会话隔离,但长期会累积 agent 目录(仓库里曾积累几十个 `roundtable-coordinator-mp*` 壳目录)。2026-05 改造选用上方决策 §9.2 选项 C 的本质:**把 coordinator 实例化为「内置 + 静态 SOUL + 运行时注入席位列表」**,固定 id `roundtable-coordinator`,与 `roundtable-intent` / `roundtable-recommender` 同样走 `ensure_roundtable_functional_agents()` 缺失自动落地。
|
||||
|
||||
- 选项 A:定时任务清理 N 天前无活跃 thread 的 coordinator agent
|
||||
- 选项 B:每次创建前 LRU 清理(注意:跨服务实例难协调)
|
||||
- 选项 C:把 coordinator 实例化为「内置 + 动态 SOUL」复用同一个 id
|
||||
实现要点:
|
||||
|
||||
- **磁盘 SOUL 是通用版本**:不含本次席位列表,只规定协调规则、工具用法、终止条件;
|
||||
- **席位列表运行时注入**:init 创建 coordinator thread 后立刻 `_append_thread_message` 写入一条 human 消息("【圆桌系统初始化】…"),位置在对话历史最前面,leader run 每轮自然读到;
|
||||
- **`_create_coordinator_direct` 已删除**:不再有"每次创建"路径。
|
||||
|
||||
**遗留 `roundtable-coordinator-*` 目录的处理**:仓库可能仍有几十个历史时间戳壳目录。改造后**不会**再生成新的,但旧目录目前不自动清理(避免破坏可能仍存在的旧草稿引用)。运维可通过:
|
||||
|
||||
- 在 admin agents 管理页逐个 DELETE;
|
||||
- 或写一次性脚本扫描 `.deer-flow/agents/roundtable-coordinator-*` 并删除目录 + DB 记录(注意不要删除 `roundtable-coordinator` 本体)。
|
||||
|
||||
### 9.3 澄清协议统一
|
||||
|
||||
@@ -705,15 +727,22 @@ offline-backend-20260512/backend/.deer-flow/*
|
||||
|
||||
| Agent | 角色 | 是否必须打包发布 |
|
||||
|-------|------|------------------|
|
||||
| `roundtable-intent` | **功能型**:Step 1 任务理解,所有部署都用它 | ✅ **必须**(任何环境) |
|
||||
| `roundtable-recommender` | **功能型**:Step 1→2 之间从候选池里挑推荐,所有部署都用它 | ✅ **必须**(任何环境) |
|
||||
| `roundtable-intent` | **功能型**:Step 1 任务理解,所有部署都用它 | ✅ seeder 自动落地(无需手工打包) |
|
||||
| `roundtable-recommender` | **功能型**:Step 1→2 之间从候选池里挑推荐,所有部署都用它 | ✅ seeder 自动落地(无需手工打包) |
|
||||
| `roundtable-coordinator` | **功能型单例**:Step 2 总控协调,2026-05 起为单例(id 固定) | ✅ seeder 自动落地(无需手工打包) |
|
||||
| `roundtable-intelligence` | 示例研讨席位(情报收集) | ⚠️ **互联网 demo 用**,内网不需要 |
|
||||
| `roundtable-environment` | 示例研讨席位(环境评估) | ⚠️ 同上 |
|
||||
| `roundtable-solution-design` | 示例研讨席位(方案设计) | ⚠️ 同上 |
|
||||
| `roundtable-risk-review` | 示例研讨席位(风险审查) | ⚠️ 同上 |
|
||||
| `roundtable-execution-plan` | 示例研讨席位(执行规划) | ⚠️ 同上 |
|
||||
| `roundtable-summary` | 示例研讨席位(总结陈述) | ⚠️ 同上 |
|
||||
| `roundtable-coordinator-*` | 协调智能体壳,每次会话动态新建 | ❌ **不打包**(运行时生成) |
|
||||
| `roundtable-coordinator-*`(带后缀) | 2026-05 之前每次会话动态新建的临时壳 | ❌ **遗留**(仓库可能仍有几十个旧目录,新版本不再生成;运维按需清理) |
|
||||
|
||||
**关键变化(2026-05)**:三个功能型 agent(intent / recommender / coordinator)现在都由
|
||||
``_roundtable_seed.ensure_roundtable_functional_agents()`` **自动落地**——任何 intent /
|
||||
recommend / multi-agent 路由的入口都会触发,首次请求时缺什么补什么。所以**新部署的
|
||||
机器 `git clone` 后既不需要手工拷目录、也不需要走下文的 `.gitignore` 白名单方案**,
|
||||
SOUL 改动跟着 Python 代码走 git review 即可。
|
||||
|
||||
**关键区分**:
|
||||
- `roundtable-intent` / `roundtable-recommender` 是**圆桌功能本身的一部分**——没有它们,整个 Step 1 + 推荐流程跑不起来,跟客户业务无关
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。
|
||||
- **不写死模型名**:与 `/page/canvas/ai-writing` 同样的策略 —— 全局选中模型由 `selectedModel` state 维护,首次 `useModels()` 完成后初始化为 `availableModels[0].name`,顶栏「模型」下拉可切换。所有 API 调用统一传 `model: selectedModel || undefined`;后端缺省由 `lead_agent._resolve_model_name()` 回退到 `config.yaml` 的 `models[0]`。**内网部署只需在 `config.yaml` 配自家模型即可,前后端均无需改动**。
|
||||
- **席位区间硬约束**:推荐弹窗要求 **2 ≤ 选中数 ≤ 8**(`MIN_SELECTED` / `MAX_SELECTED`)。
|
||||
- **协调智能体单例(2026-05)**:`COORDINATOR_AGENT_ID = "roundtable-coordinator"` 是固定常量,不再每会话生成新 id。后端 seeder 自动落地,「本次席位列表」改由后端通过 thread 历史 human 消息注入,前端无感。`newCoordinatorName()` 仅作为兼容性壳保留。
|
||||
- **总控编排循环上限**:`MAX_CYCLES = 8`,超出后强制停止并 toast。
|
||||
- **席位派活策略**:**顺序**(不是并行)。旧实现并行触发多路 SSE 会让浏览器卡顿,现已改为顺序。
|
||||
- **调试开关**:`window.__MULTI_AGENT_INIT_TIMING__` 与 `window.__MULTI_AGENT_DEBUG__`(**不是** `__MULTI_AGENT_STREAM_DEBUG__`,文档历史版本有误)。
|
||||
@@ -341,7 +342,12 @@ React 18 StrictMode 开发环境下,effect 会 mount → cleanup → mount 两
|
||||
| `agentConfigs` | `Map<seatId, {model, weight, prompt, temp}>` Persona 覆盖;下一轮该席位的派活会用此 model |
|
||||
| `isUpperPanelCollapsed` | 是否折叠圆桌可视化 + 调控台,给对话区让出垂直空间 |
|
||||
|
||||
**协调智能体命名**:`roundtable-coordinator-${Date.now().toString(36)}`,每次 Step 2 会话新建。后端把这类壳 agent 留在 DB 中,因此 `filterRecommendCandidates` 默认过滤掉。
|
||||
**协调智能体命名(2026-05 改造)**:现在是**固定单例 id** `roundtable-coordinator`(见 `lib/roundtable-constants.ts::COORDINATOR_AGENT_ID`)。`newCoordinatorName()` 函数为兼容性保留,内部直接返回这个常量字符串。后端 `ensure_roundtable_functional_agents()` 缺失自动落地,不再每次新建。`filterRecommendCandidates` 在 `EXCLUDED_AGENT_IDS` 集合里同时排除:
|
||||
|
||||
- `roundtable-coordinator`(新版单例 id,永远过滤);
|
||||
- `roundtable-coordinator-*` 前缀(历史残留,管理员清理之前先在 UI 隐藏)。
|
||||
|
||||
> 历史草稿兼容性:用户加载 2026-05 前保存的草稿时,存储的 `coordinatorName` 仍是 `roundtable-coordinator-<ts36>` 形式。只要那个旧 agent 目录还在磁盘上(即运维没手动删过),加载后继续在 Step 2 干活就能正常工作;如果旧 agent 被清理掉了,需要让用户「重新起草」从头跑一遍 Step 1 → Step 2。
|
||||
|
||||
### 5.4 编排循环 `runOrchestration(seedMessage, ids, coord)`
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.277" y1="0.03" x2="0.725" y2="1.083" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(-128.5 -117)"><rect class="a" width="16" height="16" transform="translate(128.5 117)"/><path class="b" d="M4,0,8,5H0Z" transform="translate(140.5 128) rotate(180)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 589 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="84.499" height="70" viewBox="0 0 84.499 70"><defs><style>.a{fill:url(#a);}</style><linearGradient id="a" x1="0.043" x2="0.978" y2="1.016" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0062d9" stop-opacity="0.6"/><stop offset="1" stop-color="#0062d9" stop-opacity="0.302"/></linearGradient></defs><path class="a" d="M8855.277-1940.39l-.054-70h78v31.183l6.5,5.257-6.5,5.259v28.3Z" transform="translate(-8855.224 2010.389)"/></svg>
|
||||
|
After Width: | Height: | Size: 536 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="77" height="45" viewBox="0 0 77 45"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="none" width="100%" height="100%" viewBox="0 0 340 45"><image width="340" height="45" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAVQAAAAtCAYAAADxyJMIAAAABHNCSVQICAgIfAhkiAAAIABJREFUeF7tnd2O5MZ1x5vdPTO7WkmGhMiQDQS5NODbvEBeIgbygcRwjOQur+D1K+QiQC4CBMld8hJ+CgO+DpAIcixblvZjZnqG+f/PqcM6Vawii+zuda+0XLSaXawvFos//s85xVHX9/3mXFvXdeeq+htSb/9mB4iX+vlmfZu/PKLs1BX7/ET1fndznsn8w5X1Pkc5jvfPV5ZfPcu784zD6v683QWXMLJbknnpsHy7gXpmWP5sIYRaYHgqsC2dKG97/haQL4Xy2SH8Drqt024JI98BtXVUi/nOBM0pWLaAkX09JRx/sBDe+Vj99sjyR10jV/ijI5Xir44s78+jBcLMPwXis0H3HWz9pXoH1FPdgFLPCaF5DCjXAvLrALM/rQzKuWH36QnH7xTX9bMzw2IO2muhPAfgNwbeM4/fKa7xiet4B9RVAzq4ndb7GL3nquSrnFKXrcBcqhbngHlO4P3+Qp3oH54xcDAH7Dng5nO3FcBTwC3Blv5d29bPeNfbby5o3wF1EVBH/vtl0+tn0ti4TA2ex4JzDpD5uc8B81TQe3FhSnTRHEDmZycCwhys54Cb97sG4GNBW1O0R7sRTjSOS6/fGfO/A2pxcJsDnykcFZjlLYdmGZZj2JZUZisof4iufJHBaykUW+D38QJ1+fIth6ld3fcWwOCLTOUSyPm4zkHa4MvrVwNxK4BL4F0L3bO4DxaM7RnhuKbqd0BdEzuo+TdLSnNOZY6B2W3mgFlSki2grMFxCoinAOBtBbg3ATS14zajP3FT+/WJgPzE3bS/XnPruDJ2HsdUUwL0/6DC78tDsfyEb4Vw3q858K4B7tiN0CNI5rd4Dj+fHajC+S4zBmdbOFOGbxlQm5VnHO4SPH85HNar/Pno6kwrzTZgah05KKlO8rQSKEuQXArHGugIuBawEVot+fzw3U5E+W/w9Js67utZkrd0c7H8kq31XJmvBvClYC5BuATfHLylOWSq92P0L87v8gh44I6V7XjcctiWVG0K2caxvzzIfguA2nht8qkzDVLNXVef3eYHSYVUnbaNZ0GuOBWY5dmSw/M18lLF+C0Hp4GxdMPWoPnBCZRgK/xmwPXRswUuhSUQDHl/+2JF4KkE7KUQLvX1q4q5O3Xt8mM5aKl0nxTcDrWxyl0KU4o2wlVvtCnXQTtYV9y0lwHXbyhQF16PHJ7+Ca3qs361vMk+pTw9NKPCTOtNYanHcqU5pTJPBccZELYC7vFEQF3ByDdSZBsAaudpv+canwX4UjAvgbB1Lodui7K1sh64Ebbjm25ayVptWu67btRyBbs4+PWHg+s3AKiL4ZnOd+/3HJvuqao01VkDZ6o0O5jmaXn75cGpwNSMY1imM6MEzDklWYNaTfUh/3fQFcLh5EC8v5BF+3PEaz1+tdAt0Fjvl1NAranpOQiXoDvnYvDQ9cBNXQjpDViGLc885jPQ/koGpH4De0Wb+mM3y1/RfTOQfQuBugCgpah7C0BT1elvA70qn2brUBWcZbX5x6H4i2C2KjTHV/e22yb3WwRleSaUQFmA5HeOUYmNAOyfnsA90Aibty7bl7jY141R6yMAXYRwCb5LwVuDrsH2Wf8o1+S/8RkHyfRGmXch9JuP3JWtuQ2mAEswzwe7XCPnAexbBNRGkFq25xi7sXO9KwSQNhP+TsJzPPI+KGRqk+A0aD5DJ14EyHrV6RXmJw6qMXAT24og1LQMlgZKb2bOKsoaIClJC1t/dxpQ9nfjB0h3rcqkdEyeTuH4VJ5TwbWlran++vLH9KkKXkC5uM0AuDQ3RuDNoVsCbm1FxE2AKTsXAYslYWEaP4PflqDV+Rtv4AjYmOb9tPkqA1WyMhWGPe8isES6Cp6HH828bM7YdGkvHKiNEOWp5mrUYDq1bGnKhDcVOl6O1GENoQ1uBx+n7kdwlsx0TUvN81Z4Sr4mpTmjKE+pJGsgbJp1LlN/OG/AaWl/avm7/YrAVaGyU8GXVXevZpRvg+IdAZaK9Ius4zVV+1Wn6pRbVLLpTWugVZeBHist9zLIlgJg0UUw5R7Qfoz9rwumwPFwvVCgNoLUQ3QcSJI5N4xmhOd4gE2FlnyeCk8LEOV+Tk3PfZs1c11VZ644c2DmVzX+rgDzGFC2gHE19J4UFO5XGIEPFszxS83K87DNzud1o2mfnVMLrI8BcRW8a4DrFe3NJgJVz0lv3NRf2zvY2pn3ULS2vnYtZLW9qF7jqNYCXM0ugfVgvSCgNtw5BtDclC8Fk8wPGpcraQPe/xkBqsfSYBFTOihP65jBU9O5jddjhjwO5GqqT0EzB+bwO4By8dWdguQiOJaA2HCZzpWlb/TptrbfNQClta6j8xHQV21AngPwWviOwDs9Pn2ibhW0erNERRuVUVx/G/Ikil8By/JfqCdI6nkW0mxwP3THPstuizTQlV6O8XItPX4GwF42UOtm/Hj++nWfPgofAcpF8ikczXS3pUlT8PzAKVWrxfyc0b85D84IBZ0tGtCpq1J3rAbKWUieAYwnhdvNhUf/b08XzT8LxGeUcQ3AS8BbgG1ZnWIyB9BGmBpsU9fBWM3mboMcsgrYuNVWFKQBLl9C2zQFO145MOZKkjKvbS4XqK0wTU35eMbRB5oO0liFjpcrmQk/XpLU4U0dbonZnvk3/ah3m1RVdc48T/OlvewyeEbV2uJzPAFAVwHz0sE4c7uc/PAKEJ8EuA2uh2MhC8CmQL1KzP/EZwfAat7oLoAbYPSQ6gdXQe1FhXTpluiR4Zqpet1sch+swTVforUaqvH2L82XywNqbtaXzXk9F2/Opwvn47nWVaiOzG1gVWk9p19yFFRogOdYUdbBmV8BVaYa+U6g6pRmWbEeCcpmSP4BwDirsk9Ou7YK58zrtloW5KKZ/XVb/qPhu0LZOlWbBzrUhNNAWTxWAe2gYvX+rLsKzB9bh6yW9+o1Va5xMCNcx0GaHLAr3QGXA9Tn4bxjdD6dVbWgEpXo+N12vmGk5cevYarvkzB77ca1Dk/rh1a4AJwllRnAYcCM4FRYjtO19aqtMQvJM8BxNfyusyCV+Qzvj1ue1T9Muwy63elM9mRS3rX5PHM8ngXSM2p4NXwr0MU5jEz+AmxjACIFbb9J/bOSb/DJpko2AteWb/FvIUwBtuR7bfW5rlKu8fa8HKD+WT7t8JsQrQeVIkjjMiYFafSFxgi8+kB1K8NzDC7n74Spnh8f4Adw+mOqQKNpHiEZFWbN3E+A6mA5BuqJQNkExxyEhUuVJ81BrqGKtzrLIog3gPlkEJ4A70Lo4g/fDA+UCM4UtHrT6Jpirwptv098swrZJF8Bsj7oFVcU3AzFbAVBBLGpV3ML8IgCNrZF9Tq1YsD7WyfU65LFdef9f0oZUPn9v+FeSk16TfSRefpDoxKNNyDNeKrQX0O5GEjp+/yYMO0677MeFsN75akL3eHHTCE6Y6ZzOVAEYqq6cuU5AmqAZ0x/H3VlAaylhKnC8pmrqUEdrobjIVOOT9HuHpM4T6+dWK48qTRn1OjG1OhcvrzNNSr2VVYJz23F1gRfvwLgRbmRo6FbcDdMQLYESK5UMOUKn+jQz971LcLUvbxBuA0vNnyZuAwkv3tJoRcs2HrZG2mD/le757lMKw6QD2ylbgHNk75QoGnfw+cX2RinS7H0YAGslwPUf3AnYCAtReipRhWiCh9To+YLZZpF5DWApBsGs7A4ntCSrRIsGvk6R8ozNdVjvzL/qLRxv/Gvl6aQVcU5p1yzq5wo4dGxTYOyXA1L35oHJKGZb0vhFsr3rt4uwMqnFVratObLy1q5Up3FtDkAe9iuBK1vtwm6E0p3NWzrirZ3sC2qT7uM8q1qNsI0fVECMI3H3HlHBVtzEUTrlIEu3fw6WINr+ncJbEkWX4uVW1PKmVugtkqgAaqXA9QfuWFUv2g6jw2kClA9GANKMa9F4fk08kC1MgGgUoea8VLfhxzV7BXJzN/JP4FWU5oJCAumuh5PoVmC51i5qutgbPLPwPJoUJYgOQHGOdClF3PBr+s39CbV3ZJbob3/k6A2KJ9I6cpEngN9BborgMu/TTsAMoyI3pyp6V6HbR2yg6vg91qx1uHfDIuAjfVz5YAB1q8ioHqV+89dY32xIILV2iBgvb81XSWg9UwEsJbMovOa/P84AVGeRFSi0Yy3eU1wKkBT8JThKdec//nA+T4BzyHdgTOmlRWnh11s+2ZCiWaBp2qQ6rovqVmdXUsV35yJfT0G9ipAtsDv0BiAemIX9w1/v25sb98QkFoB6SKA72ZcCQsV8CR024FbB6WCdgTbgqItAtmrWe8q+OrawVUvUw/I6je3q+w4FSsDXKZcrQwBG5WrpipcU5eNh2tJtRYCWP3zxvlDCC2JYLVXqzm7H4cScZlTHlyK78Kb8jSfiFei5nd06pM1B3hKUwGesg94WleL5nbm2/RKU8uH7le+hzxoMwWkgqXbvKen78arC8Csmf/joa0B81hQ1gC5EIr9Q6PKfFgIW6t3h5uG+/y2jWC8Cmk+H4+XoLnDzZS3z7SGrbN2j4XxAgAvAu8pgPsyDER8kKiKVNO9DMZxOouYsk3KzKpap2a9mgZk07YNsApX3Uyt5urVg5X54uuwXrmqWyBLine truncated
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -1,24 +1,185 @@
|
||||
.sidebar-contents{
|
||||
width:98px;
|
||||
height:986px;
|
||||
background:rgba(3, 38, 80, 0.8);
|
||||
border:1px solid;
|
||||
border-color:rgba(43, 143, 254, 0.6);
|
||||
border-radius:4px;
|
||||
backdrop-filter:blur(2px);
|
||||
.sidebar-contents {
|
||||
width: 98px;
|
||||
min-height: 986px;
|
||||
background: rgba(3, 38, 80, 0.8);
|
||||
border: 1px solid;
|
||||
border-color: rgba(43, 143, 254, 0.6);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-nav{
|
||||
width:78px;
|
||||
height:70px;
|
||||
background:rgba(0, 12, 28, 0.15);
|
||||
border:1px solid;
|
||||
border-color:rgba(85, 166, 254, 0.2);
|
||||
line-height:8px;
|
||||
font-family:Noto Sans S Chinese;
|
||||
font-weight:500;
|
||||
color:rgba(255, 255, 255, 0.8);
|
||||
font-size:20px;
|
||||
padding-bottom:18px;
|
||||
text-align:center;
|
||||
.item-nav {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background: #ffffff1a;
|
||||
border-color: #fefafa4d;
|
||||
margin-top: 13px;
|
||||
font-family: Noto Sans S Chinese;
|
||||
font-weight: 550;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
background: #000c1c26;
|
||||
border: 1px solid;
|
||||
border-color: #55a6fe33;
|
||||
box-shadow: 0 0 20px #00b9ff26 inset;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-nav:hover {
|
||||
background: rgba(85, 166, 254, 0.12);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.item-nav__label {
|
||||
display: block;
|
||||
width: 3em;
|
||||
margin: 0 auto;
|
||||
padding-left: 6px;
|
||||
box-sizing: border-box;
|
||||
letter-spacing: 6px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.item-nav-content {
|
||||
width: 155px;
|
||||
min-height: 970px;
|
||||
background: rgba(3, 38, 80, 0.8);
|
||||
border: 1px solid;
|
||||
border-color: rgba(43, 143, 254, 0.6);
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(2px);
|
||||
position: absolute;
|
||||
left: 97px;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
padding: 14px 13px 18px 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.item-nav-active {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(134.25deg,
|
||||
rgba(0, 98, 217, 0.59) 2.28%,
|
||||
rgba(0, 98, 217, 0.3) 99.59%) !important;
|
||||
background-size: cover;
|
||||
border: 1px solid;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.item-nav-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 8px solid transparent;
|
||||
border-bottom: 8px solid transparent;
|
||||
border-left: 10px solid rgba(0, 98, 217, 0.45);
|
||||
pointer-events: none;
|
||||
}
|
||||
.item-nav-active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 24px;
|
||||
width: 77px;
|
||||
height: 45px;
|
||||
background: url('../assets/icon/nav-line-logo.svg') no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.sidebar-flyout-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-flyout-list--level-1,
|
||||
.sidebar-flyout-list--level-2,
|
||||
.sidebar-flyout-list--level-3 {
|
||||
margin-left: 19px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.sidebar-flyout-node {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-flyout-node--has-children.sidebar-flyout-node--expanded>.sidebar-flyout-list::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: rgba(43, 143, 254, 0.22);
|
||||
}
|
||||
|
||||
.sidebar-flyout-item {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 3px 0;
|
||||
font-family: Noto Sans S Chinese;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease, text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-flyout-item:hover,
|
||||
.sidebar-flyout-item--active {
|
||||
color: #48ddff;
|
||||
text-shadow: 0 0 8px rgba(72, 221, 255, 0.25);
|
||||
}
|
||||
|
||||
.sidebar-flyout-item--leaf {
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-flyout-list--level-1>.sidebar-flyout-node>.sidebar-flyout-item--leaf,
|
||||
.sidebar-flyout-list--level-2>.sidebar-flyout-node>.sidebar-flyout-item--leaf,
|
||||
.sidebar-flyout-list--level-3>.sidebar-flyout-node>.sidebar-flyout-item--leaf {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-flyout-arrow {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
flex: 0 0 9px;
|
||||
background: url('../assets/icon/down-logo.svg') center / contain no-repeat;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar-flyout-node--expanded>.sidebar-flyout-item>.sidebar-flyout-arrow {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.sidebar-flyout-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ElementType } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
@@ -74,6 +74,7 @@ const menuItems: MenuItem[] = [
|
||||
icon: HelpCircle,
|
||||
children: [
|
||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||
{ id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
|
||||
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
|
||||
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
||||
@@ -132,9 +133,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "COH中任务",
|
||||
icon: Folder,
|
||||
children: [
|
||||
{ id: "coh-task-3qfx", label: "3QFX", icon: ListTodo },
|
||||
{ id: "coh-task-fx", label: "任务FX", icon: ListTodo },
|
||||
{ id: "coh-task-xdfx", label: "XDFX", icon: ListTodo },
|
||||
{
|
||||
id: "coh-task-3qfx",
|
||||
label: "3QFX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task-sentiment",
|
||||
},
|
||||
{
|
||||
id: "coh-task-fx",
|
||||
label: "任务FX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||
},
|
||||
{
|
||||
id: "coh-task-xdfx",
|
||||
label: "XDFX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task/plan-task-action",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -222,9 +238,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "资源管理",
|
||||
icon: Database,
|
||||
children: [
|
||||
{ id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network },
|
||||
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
|
||||
{ id: "resource-zz", label: "ZZ资源管理", icon: FileBox },
|
||||
{
|
||||
id: "resource-network-xwt",
|
||||
label: "网络xwt资源管理",
|
||||
icon: Network,
|
||||
path: "/page/strategy/resource-management/resource-network-actor",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
icon: Library,
|
||||
path: "/page/strategy/resource-management/resource-knowledge-base",
|
||||
},
|
||||
{
|
||||
id: "resource-evaluation-index",
|
||||
label: "作战资源管理",
|
||||
icon: FileBox,
|
||||
path: "/page/strategy/resource-management/resource-evaluation-index",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -254,7 +285,10 @@ export function PageSidebarDark({
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin);
|
||||
const visibleMenuItems = useMemo(
|
||||
() => filterMenuItemsByRole(menuItems, !!isAdmin),
|
||||
[isAdmin],
|
||||
);
|
||||
const { t } = useI18n();
|
||||
const { data: unreadCount = 0 } = useUnreadCount();
|
||||
const [expandedIds, setExpandedIds] = useState<Set<string>>(defaultExpandedIds);
|
||||
@@ -275,6 +309,26 @@ export function PageSidebarDark({
|
||||
setSettingsOpen(true);
|
||||
};
|
||||
|
||||
const hasActiveChild = (item: MenuItem): boolean => {
|
||||
if (item.path && pathname === item.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.children?.some(hasActiveChild) ?? false;
|
||||
};
|
||||
|
||||
const initialActiveTopItem = visibleMenuItems.find(hasActiveChild) ?? visibleMenuItems[0];
|
||||
const [activeTopId, setActiveTopId] = useState(initialActiveTopItem?.id ?? "qa-center");
|
||||
|
||||
useEffect(() => {
|
||||
const matchedTopItem = visibleMenuItems.find(hasActiveChild);
|
||||
if (matchedTopItem) {
|
||||
setActiveTopId(matchedTopItem.id);
|
||||
}
|
||||
}, [pathname, visibleMenuItems]);
|
||||
|
||||
const activeTopItem = visibleMenuItems.find((item) => item.id === activeTopId) ?? initialActiveTopItem;
|
||||
|
||||
const toggleExpanded = (id: string) => {
|
||||
setExpandedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -287,6 +341,51 @@ export function PageSidebarDark({
|
||||
});
|
||||
};
|
||||
|
||||
const handleTopItemClick = (item: MenuItem) => {
|
||||
setActiveTopId(item.id);
|
||||
|
||||
if (!item.children?.length && item.path) {
|
||||
navigate(item.path);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (item: MenuItem) => {
|
||||
if (item.children?.length) {
|
||||
toggleExpanded(item.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.path) {
|
||||
navigate(item.path);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFlyoutMenu = (items: MenuItem[], level = 0) => (
|
||||
<div className={`sidebar-flyout-list sidebar-flyout-list--level-${level}`}>
|
||||
{items.map((item) => {
|
||||
const hasChildren = !!item.children?.length;
|
||||
const isExpanded = expandedIds.has(item.id);
|
||||
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`sidebar-flyout-node sidebar-flyout-node--level-${level} ${hasChildren ? "sidebar-flyout-node--has-children" : ""} ${isExpanded ? "sidebar-flyout-node--expanded" : ""}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`sidebar-flyout-item ${isSelected ? "sidebar-flyout-item--active" : ""} ${hasChildren ? "sidebar-flyout-item--parent" : "sidebar-flyout-item--leaf"}`}
|
||||
onClick={() => handleMenuItemClick(item)}
|
||||
>
|
||||
{hasChildren ? <span className="sidebar-flyout-arrow" aria-hidden="true" /> : null}
|
||||
<span className="sidebar-flyout-label">{item.label}</span>
|
||||
</button>
|
||||
{hasChildren && isExpanded ? renderFlyoutMenu(item.children ?? [], level + 1) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingsDialog
|
||||
@@ -301,9 +400,19 @@ export function PageSidebarDark({
|
||||
{/* TODO: 在这里编写深色主题的侧边栏 HTML 结构 */}
|
||||
<div className="sidebar-contnet">
|
||||
<div className="sidebar-contents">
|
||||
<div className="item-nav">
|
||||
问答管理
|
||||
<div className="item-nav-content">
|
||||
{activeTopItem?.children?.length ? renderFlyoutMenu(activeTopItem.children) : null}
|
||||
</div>
|
||||
{visibleMenuItems.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.id}
|
||||
className={`item-nav ${item.id === activeTopId ? "item-nav-active" : ""}`}
|
||||
onClick={() => handleTopItemClick(item)}
|
||||
>
|
||||
<span className="item-nav__label">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -73,6 +73,7 @@ const menuItems: MenuItem[] = [
|
||||
icon: HelpCircle,
|
||||
children: [
|
||||
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
||||
{ id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
||||
// 「圆桌规划」入口暂时隐藏 —— 功能仍在 /page/roundtable/planning,只是不在侧边栏出现。
|
||||
// 需要重新开放时取消注释即可。
|
||||
// {
|
||||
@@ -130,8 +131,18 @@ const menuItems: MenuItem[] = [
|
||||
label: "XK中任务",
|
||||
icon: Folder,
|
||||
children: [
|
||||
{ id: "xk-task-jc", label: "任务JC", icon: ListTodo },
|
||||
{ id: "xk-task-dispatch", label: "任务调度", icon: ListTodo },
|
||||
{
|
||||
id: "xk-task-jc",
|
||||
label: "任务JC",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/xk-task-jc",
|
||||
},
|
||||
{
|
||||
id: "xk-task-dispatch",
|
||||
label: "任务调度",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/xk-task-dispatch",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -139,9 +150,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "COH中任务",
|
||||
icon: Folder,
|
||||
children: [
|
||||
{ id: "coh-task-3qfx", label: "3QFX", icon: ListTodo },
|
||||
{ id: "coh-task-fx", label: "任务FX", icon: ListTodo },
|
||||
{ id: "coh-task-xdfx", label: "XDFX", icon: ListTodo },
|
||||
{
|
||||
id: "coh-task-3qfx",
|
||||
label: "3QFX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task-sentiment",
|
||||
},
|
||||
{
|
||||
id: "coh-task-fx",
|
||||
label: "任务FX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
||||
},
|
||||
{
|
||||
id: "coh-task-xdfx",
|
||||
label: "XDFX",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/plan-task/plan-task-action",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -149,9 +175,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "PG中任务",
|
||||
icon: Folder,
|
||||
children: [
|
||||
{ id: "pg-task-pg", label: "任务PG", icon: ListTodo },
|
||||
{ id: "pg-task-xdpg", label: "XDPG", icon: ListTodo },
|
||||
{ id: "pg-task-info-pg", label: "信息PG", icon: ListTodo },
|
||||
{
|
||||
id: "pg-task-pg",
|
||||
label: "任务PG",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/effect-task-pg",
|
||||
},
|
||||
{
|
||||
id: "pg-task-xdpg",
|
||||
label: "XDPG",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/effect-action-pg",
|
||||
},
|
||||
{
|
||||
id: "pg-task-info-pg",
|
||||
label: "信息PG",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/effect-info-pg",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -159,9 +200,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "TY中任务",
|
||||
icon: Folder,
|
||||
children: [
|
||||
{ id: "ty-task-xdty", label: "XDTY任务", icon: ListTodo },
|
||||
{ id: "ty-task-plan", label: "方案TY任务", icon: ListTodo },
|
||||
{ id: "ty-task-xxty", label: "XXTY任务", icon: ListTodo },
|
||||
{
|
||||
id: "ty-task-xdty",
|
||||
label: "XDTY任务",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/mnty-task-mechanism",
|
||||
},
|
||||
{
|
||||
id: "ty-task-plan",
|
||||
label: "方案TY任务",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/mnty-task-plan",
|
||||
},
|
||||
{
|
||||
id: "ty-task-xxty",
|
||||
label: "XXTY任务",
|
||||
icon: ListTodo,
|
||||
path: "/page/strategy/task-management/mnty-task-zsty",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -236,9 +292,24 @@ const menuItems: MenuItem[] = [
|
||||
label: "资源管理",
|
||||
icon: Database,
|
||||
children: [
|
||||
{ id: "resource-network-xwt", label: "网络xwt资源管理", icon: Network },
|
||||
{ id: "resource-knowledge-base", label: "知识库资源管理", icon: Library },
|
||||
{ id: "resource-zz", label: "ZZ资源管理", icon: FileBox },
|
||||
{
|
||||
id: "resource-network-xwt",
|
||||
label: "网络xwt资源管理",
|
||||
icon: Network,
|
||||
path: "/page/strategy/resource-management/resource-network-actor",
|
||||
},
|
||||
{
|
||||
id: "resource-knowledge-base",
|
||||
label: "知识库资源管理",
|
||||
icon: Library,
|
||||
path: "/page/strategy/resource-management/resource-knowledge-base",
|
||||
},
|
||||
{
|
||||
id: "resource-evaluation-index",
|
||||
label: "作战资源管理",
|
||||
icon: FileBox,
|
||||
path: "/page/strategy/resource-management/resource-evaluation-index",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,11 +9,26 @@ export interface ChatMessage {
|
||||
agentName?: string;
|
||||
}
|
||||
|
||||
export interface toolsUser {
|
||||
email: string;
|
||||
id: string;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
pages_limit: number;
|
||||
pages_used: string;
|
||||
yUserId: string;
|
||||
}
|
||||
|
||||
interface ChatContextType {
|
||||
messages: ChatMessage[];
|
||||
addMessage: (role: 'user' | 'assistant', content: string, type?: 'general' | 'agent' | 'compare', agentName?: string) => void;
|
||||
clearMessages: () => void;
|
||||
deleteMessage: (id: string) => void;
|
||||
startConfig: unknown;
|
||||
setStartConfig: (val: unknown) => void;
|
||||
toolsUser: toolsUser | null;
|
||||
setToolsUser: (val: toolsUser) => void;
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||
@@ -28,6 +43,9 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
});
|
||||
|
||||
const [startConfig, setStartConfig] = useState<unknown>(null);
|
||||
const [toolsUser, setToolsUser] = useState<toolsUser | null>(null);
|
||||
|
||||
const saveToStorage = useCallback((newMessages: ChatMessage[]) => {
|
||||
try {
|
||||
localStorage.setItem('qa-chat-history', JSON.stringify(newMessages));
|
||||
@@ -66,7 +84,18 @@ export function ChatProvider({ children }: { children: ReactNode }) {
|
||||
}, [saveToStorage]);
|
||||
|
||||
return (
|
||||
<ChatContext.Provider value={{ messages, addMessage, clearMessages, deleteMessage }}>
|
||||
<ChatContext.Provider
|
||||
value={{
|
||||
messages,
|
||||
addMessage,
|
||||
clearMessages,
|
||||
deleteMessage,
|
||||
startConfig,
|
||||
setStartConfig,
|
||||
toolsUser,
|
||||
setToolsUser,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
|
||||
@@ -48,16 +48,16 @@ const themes: Record<ThemeType, ThemeConfig> = {
|
||||
textSecondary: 'slate-400',
|
||||
primary: 'blue-500',
|
||||
primaryHover: 'blue-400',
|
||||
border: 'slate-700',
|
||||
accent: 'blue-600',
|
||||
border: 'blue-800',
|
||||
accent: 'cyan-400',
|
||||
success: 'emerald-500',
|
||||
warning: 'amber-500',
|
||||
error: 'rose-500',
|
||||
card: 'slate-800',
|
||||
cardHover: 'slate-700',
|
||||
input: 'slate-800',
|
||||
inputBorder: 'slate-600',
|
||||
overlay: 'slate-900/80',
|
||||
inputBorder: 'blue-800',
|
||||
overlay: 'slate-900/50',
|
||||
},
|
||||
},
|
||||
light: {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/** Strategy 模块外部 iframe / 嵌入页链接(来自 Vite 环境变量) */
|
||||
|
||||
function trimTrailingSlash(url: string): string {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function readEnv(key: string): string {
|
||||
const value = import.meta.env[key];
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
/** B2 网络 xwt 资源管理 iframe 基础地址 */
|
||||
export function getB2ActorBaseUrl(): string {
|
||||
return trimTrailingSlash(readEnv("VITE_AIPI_B2ACTOR_URL"));
|
||||
}
|
||||
|
||||
/** B2 网络 xwt 资源管理 iframe 路径 */
|
||||
export function getNetworkActorIframePath(): string {
|
||||
return readEnv("VITE_AIPI_B2ACTOR_XWT_PATH") || "/fromB/toXWT?isFrame=1";
|
||||
}
|
||||
|
||||
/** 网络 xwt 资源管理完整 iframe URL */
|
||||
export function getNetworkActorIframeUrl(): string {
|
||||
const base = getB2ActorBaseUrl();
|
||||
const path = getNetworkActorIframePath();
|
||||
if (!base) return "";
|
||||
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
/** 知识库 iframe 地址换取 API */
|
||||
export function getKnowledgeIframeApiUrl(): string {
|
||||
return readEnv("VITE_AIPI_KNOWLEDGE_IFRAME_API_URL");
|
||||
}
|
||||
|
||||
/** 知识库 iframe 皮肤参数 */
|
||||
export function getKnowledgeIframeSkin(): string {
|
||||
return readEnv("VITE_AIPI_KNOWLEDGE_IFRAME_SKIN") || "red";
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import "./styles/globals.css";
|
||||
import "tdesign-react/es/style/index.css";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
import React from "react";
|
||||
|
||||
@@ -38,6 +38,8 @@ const ENABLE_PAUSE_RESUME_UI = false;
|
||||
import { AIWritingForm } from './AIWritingForm';
|
||||
import { AIWritingTimeline } from './AIWritingTimeline';
|
||||
import { InterventionCard } from './InterventionCard';
|
||||
import { SkillStepsCard } from './SkillStepsCard';
|
||||
import { SkillSelectStreamingView } from './SkillSelectStreamingView';
|
||||
import { WritingSkeletonCard } from './WritingSkeletonCard';
|
||||
import { DraftStreamingView } from './DraftStreamingView';
|
||||
import { IntentResultCard } from './IntentResultCard';
|
||||
@@ -411,6 +413,8 @@ export function AIWritingPanel({
|
||||
reviewResult={state.reviewResult}
|
||||
draftTitle={state.currentDraftTitle}
|
||||
revisionCount={state.revisionCount}
|
||||
skillSearchSteps={state.skillSearchSteps}
|
||||
skillSearchInProgress={state.status === 'researching' && state.skillSearchSteps.length > 0}
|
||||
onSubmit={submitIntervention}
|
||||
disabled={false}
|
||||
/>
|
||||
@@ -444,10 +448,27 @@ export function AIWritingPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 技能选择 LLM 流式打字(含 <think> 思考分离),select_skills 完成后自动清空 */}
|
||||
{state.status === 'researching' && (state.skillSelectStreamingText || state.skillSelectThinkingText) && (
|
||||
<SkillSelectStreamingView
|
||||
text={state.skillSelectStreamingText}
|
||||
thinkingText={state.skillSelectThinkingText}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 研究中 + 已有技能步骤:实时流式展示技能检索过程,避免 skeleton 卡片掩盖步骤 */}
|
||||
{state.status === 'researching' && state.skillSearchSteps.length > 0 && (
|
||||
<div className="px-4 pb-2">
|
||||
<SkillStepsCard steps={state.skillSearchSteps} inProgress />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSkeleton &&
|
||||
state.status !== 'writing_draft' &&
|
||||
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
||||
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) &&
|
||||
/* 技能检索已经在流式展示步骤 / 选择打字机,不需要再叠加 skeleton */
|
||||
!(state.status === 'researching' && (state.skillSearchSteps.length > 0 || state.skillSelectStreamingText || state.skillSelectThinkingText)) &&
|
||||
/* 意图解析流式过程中(LLM 还在吐 JSON)不要显示 skeleton */
|
||||
!state.intentStreamingText && !state.intentThinkingText && (
|
||||
<div className="px-4 pb-4">
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef, useState } from 'react';
|
||||
import {
|
||||
PauseCircle, CheckCircle2, RotateCcw, Search,
|
||||
RefreshCw, PenLine, ArrowRight, ShieldAlert, ClipboardEdit, SendHorizonal,
|
||||
Trash2, BookOpen,
|
||||
Trash2, BookOpen, Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -10,11 +10,13 @@ import type {
|
||||
AIMaterial, AIOutline, ReviewResult, ReviewIssue,
|
||||
ProgressEvent, UserInterventionPayload,
|
||||
BlockedSection, SectionDecisionKind,
|
||||
SkillSearchStep,
|
||||
} from '../../types/ai-writing';
|
||||
import { MaterialsCard } from './MaterialsCard';
|
||||
import { MaterialsToolbar } from './MaterialsToolbar';
|
||||
import { OutlineCard } from './OutlineCard';
|
||||
import { ReviewCard } from './ReviewCard';
|
||||
import { SkillStepsCard } from './SkillStepsCard';
|
||||
|
||||
interface InterventionCardProps {
|
||||
pauseEvent: ProgressEvent;
|
||||
@@ -23,18 +25,29 @@ interface InterventionCardProps {
|
||||
reviewResult: ReviewResult | null;
|
||||
draftTitle: string;
|
||||
revisionCount: number;
|
||||
skillSearchSteps?: SkillSearchStep[];
|
||||
/** 是否正在检索中(researching/awaiting 切换之间,前端用于流式展示步骤进行中) */
|
||||
skillSearchInProgress?: boolean;
|
||||
onSubmit: (payload: UserInterventionPayload) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function InterventionCard({
|
||||
pauseEvent, materials, outline, reviewResult,
|
||||
draftTitle, revisionCount, onSubmit, disabled,
|
||||
draftTitle, revisionCount, skillSearchSteps, skillSearchInProgress, onSubmit, disabled,
|
||||
}: InterventionCardProps) {
|
||||
const pausePoint = pauseEvent.pausePoint;
|
||||
|
||||
if (pausePoint === 'material_confirm') {
|
||||
return <MaterialConfirmCard materials={materials} onSubmit={onSubmit} disabled={disabled} />;
|
||||
return (
|
||||
<MaterialConfirmCard
|
||||
materials={materials}
|
||||
skillSearchSteps={skillSearchSteps ?? []}
|
||||
skillSearchInProgress={skillSearchInProgress}
|
||||
onSubmit={onSubmit}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (pausePoint === 'outline_confirm') {
|
||||
@@ -106,30 +119,40 @@ function formatSelectedIssues(issues: ReviewIssue[], selectedIndices: Set<number
|
||||
|
||||
interface MaterialConfirmCardProps {
|
||||
materials: AIMaterial[];
|
||||
skillSearchSteps: SkillSearchStep[];
|
||||
skillSearchInProgress?: boolean;
|
||||
onSubmit: (payload: UserInterventionPayload) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function MaterialConfirmCard({ materials, onSubmit, disabled }: MaterialConfirmCardProps) {
|
||||
function MaterialConfirmCard({ materials, skillSearchSteps, skillSearchInProgress, onSubmit, disabled }: MaterialConfirmCardProps) {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(
|
||||
() => new Set(materials.map((m) => m.id)),
|
||||
);
|
||||
const [extraKeywords, setExtraKeywords] = useState<string[]>([]);
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function handleConfirm() {
|
||||
setSubmitting(true);
|
||||
await onSubmit({ action: 'confirm', approvedMaterialIds: [...selectedIds], extraKeywords });
|
||||
await onSubmit({ action: 'confirm', approvedMaterialIds: [...selectedIds], userQuery: userQuery.trim() || undefined });
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
async function handleReSearch() {
|
||||
setSubmitting(true);
|
||||
await onSubmit({ action: 're_search', approvedMaterialIds: [], extraKeywords });
|
||||
await onSubmit({ action: 're_search', approvedMaterialIds: [], userQuery: userQuery.trim() || undefined });
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
const hasExtra = extraKeywords.length > 0;
|
||||
async function handleEnableSkillSearch() {
|
||||
setSubmitting(true);
|
||||
await onSubmit({ action: 'enable_skill_search', userQuery: userQuery.trim() || undefined });
|
||||
setSubmitting(false);
|
||||
}
|
||||
|
||||
const hasQuery = userQuery.trim().length > 0;
|
||||
// 当前素材是否来自技能检索:根据素材 sourceType 推断(覆盖式替换后全是 skill)
|
||||
const isSkillResult = materials.length > 0 && materials.every((m) => m.sourceType === 'skill');
|
||||
|
||||
// 空数据态:未检索到任何素材,提示用户重新搜索或在无素材情况下继续
|
||||
if (materials.length === 0) {
|
||||
@@ -140,33 +163,45 @@ function MaterialConfirmCard({ materials, onSubmit, disabled }: MaterialConfirmC
|
||||
<div>
|
||||
<p className="text-xs font-medium">未检索到任何素材</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
素材收集专家本次没有检索到相关数据。你可以补充关键词重新搜索,或在无素材的情况下直接继续写作(文章将仅依据写作意图生成)。
|
||||
素材收集专家本次没有检索到相关数据。你可以补充关键词重新搜索、启用技能检索,或在无素材的情况下直接继续写作。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(skillSearchSteps.length > 0 || skillSearchInProgress) && (
|
||||
<SkillStepsCard steps={skillSearchSteps} inProgress={skillSearchInProgress} />
|
||||
)}
|
||||
|
||||
<MaterialsToolbar
|
||||
materials={materials}
|
||||
selectedIds={selectedIds}
|
||||
onSelectAll={() => setSelectedIds(new Set())}
|
||||
onDeselectAll={() => setSelectedIds(new Set())}
|
||||
extraKeywords={extraKeywords}
|
||||
onAddKeyword={(kw) => setExtraKeywords((prev) => [...prev, kw])}
|
||||
onRemoveKeyword={(kw) => setExtraKeywords((prev) => prev.filter((k) => k !== kw))}
|
||||
userQuery={userQuery}
|
||||
onUserQueryChange={setUserQuery}
|
||||
onSubmitQuery={handleReSearch}
|
||||
disabled={disabled || submitting}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<Button
|
||||
type="button" size="sm" className="flex-1 h-8 text-xs gap-1.5"
|
||||
type="button" size="sm" className="flex-1 min-w-[140px] h-8 text-xs gap-1.5"
|
||||
onClick={handleReSearch} disabled={disabled || submitting}
|
||||
>
|
||||
{hasExtra
|
||||
? <><Search className="size-3.5" />追加搜索</>
|
||||
{hasQuery
|
||||
? <><Search className="size-3.5" />按输入重新搜索</>
|
||||
: <><RotateCcw className="size-3.5" />重新搜索</>}
|
||||
</Button>
|
||||
<Button
|
||||
type="button" variant="outline" size="sm"
|
||||
className="h-8 text-xs gap-1.5 shrink-0 border-violet-200 text-violet-700 hover:bg-violet-50 dark:border-violet-800 dark:text-violet-300 dark:hover:bg-violet-950/30"
|
||||
onClick={handleEnableSkillSearch} disabled={disabled || submitting}
|
||||
>
|
||||
<Sparkles className="size-3.5" />技能检索
|
||||
</Button>
|
||||
<Button
|
||||
type="button" variant="outline" size="sm" className="h-8 text-xs gap-1.5 shrink-0"
|
||||
onClick={handleConfirm} disabled={disabled || submitting}
|
||||
onClick={() => onSubmit({ action: 'confirm', approvedMaterialIds: [] })} disabled={disabled || submitting}
|
||||
>
|
||||
<ArrowRight className="size-3.5" />无数据继续写作
|
||||
</Button>
|
||||
@@ -182,11 +217,14 @@ function MaterialConfirmCard({ materials, onSubmit, disabled }: MaterialConfirmC
|
||||
<div>
|
||||
<p className="text-xs font-medium">需要您确认素材,才能继续</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
已搜集到 {materials.length} 条素材(默认全选)。您可以取消勾选不相关的素材,或补充关键词追加搜索。
|
||||
已搜集到 {materials.length} 条{isSkillResult ? '技能素材' : '素材'}(默认全选)。
|
||||
您可以取消勾选不相关的素材,或补充关键词追加搜索{isSkillResult ? '' : ',也可以启用技能检索从已启用的技能中获取素材'}。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{skillSearchSteps.length > 0 && <SkillStepsCard steps={skillSearchSteps} />}
|
||||
|
||||
<MaterialsCard materials={materials} selectedIds={selectedIds} onToggle={(id) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -200,24 +238,34 @@ function MaterialConfirmCard({ materials, onSubmit, disabled }: MaterialConfirmC
|
||||
selectedIds={selectedIds}
|
||||
onSelectAll={() => setSelectedIds(new Set(materials.map((m) => m.id)))}
|
||||
onDeselectAll={() => setSelectedIds(new Set())}
|
||||
extraKeywords={extraKeywords}
|
||||
onAddKeyword={(kw) => setExtraKeywords((prev) => [...prev, kw])}
|
||||
onRemoveKeyword={(kw) => setExtraKeywords((prev) => prev.filter((k) => k !== kw))}
|
||||
userQuery={userQuery}
|
||||
onUserQueryChange={setUserQuery}
|
||||
onSubmitQuery={handleReSearch}
|
||||
disabled={disabled || submitting}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
<Button
|
||||
type="button" size="sm" className="flex-1 h-8 text-xs gap-1.5"
|
||||
type="button" size="sm" className="flex-1 min-w-[180px] h-8 text-xs gap-1.5"
|
||||
onClick={handleConfirm} disabled={disabled || submitting || selectedIds.size === 0}
|
||||
>
|
||||
<CheckCircle2 className="size-3.5" />确认素材,开始规划大纲
|
||||
</Button>
|
||||
{!isSkillResult && (
|
||||
<Button
|
||||
type="button" variant="outline" size="sm"
|
||||
className="h-8 text-xs gap-1.5 shrink-0 border-violet-200 text-violet-700 hover:bg-violet-50 dark:border-violet-800 dark:text-violet-300 dark:hover:bg-violet-950/30"
|
||||
onClick={handleEnableSkillSearch} disabled={disabled || submitting}
|
||||
>
|
||||
<Sparkles className="size-3.5" />技能检索
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button" variant="outline" size="sm" className="h-8 text-xs gap-1.5 shrink-0"
|
||||
onClick={handleReSearch} disabled={disabled || submitting}
|
||||
>
|
||||
{hasExtra
|
||||
? <><Search className="size-3.5" />追加搜索</>
|
||||
{hasQuery
|
||||
? <><Search className="size-3.5" />按输入重新搜索</>
|
||||
: <><RotateCcw className="size-3.5" />重新搜索</>}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronRight, ExternalLink } from 'lucide-react';
|
||||
import { ChevronRight, ExternalLink, Sparkles } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { buildIntranetSourceUrl } from '@/core/config';
|
||||
import type { AIMaterial } from '../../types/ai-writing';
|
||||
@@ -26,12 +26,14 @@ function relevanceLabel(score: number): string {
|
||||
export function MaterialItem({ material, checked, onToggle }: MaterialItemProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const pct = Math.round(material.relevanceScore * 100);
|
||||
const isSkill = material.sourceType === 'skill';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border text-xs transition-colors',
|
||||
checked ? 'border-border bg-background' : 'border-border/50 bg-muted/30',
|
||||
isSkill && checked && 'border-violet-300/70 dark:border-violet-800/60',
|
||||
)}
|
||||
>
|
||||
{/* 头部行 */}
|
||||
@@ -50,6 +52,12 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="flex flex-1 items-start gap-1.5 text-left"
|
||||
>
|
||||
{isSkill && (
|
||||
<span className="mt-0.5 inline-flex items-center gap-0.5 px-1 py-0.5 rounded text-[10px] font-medium bg-violet-100 text-violet-700 dark:bg-violet-900/60 dark:text-violet-200 shrink-0">
|
||||
<Sparkles className="size-2.5" />
|
||||
技能
|
||||
</span>
|
||||
)}
|
||||
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
|
||||
{material.title}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useRef } from 'react';
|
||||
import type { AIMaterial } from '../../types/ai-writing';
|
||||
|
||||
interface MaterialsToolbarProps {
|
||||
@@ -8,9 +6,13 @@ interface MaterialsToolbarProps {
|
||||
selectedIds: Set<string>;
|
||||
onSelectAll: () => void;
|
||||
onDeselectAll: () => void;
|
||||
extraKeywords: string[];
|
||||
onAddKeyword: (kw: string) => void;
|
||||
onRemoveKeyword: (kw: string) => void;
|
||||
/** 单输入框文本:可以是技能名、检索来源、补充检索意图 */
|
||||
userQuery: string;
|
||||
onUserQueryChange: (text: string) => void;
|
||||
/** 回车键提交(默认重新搜索;详细按钮交给父组件渲染) */
|
||||
onSubmitQuery?: () => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function MaterialsToolbar({
|
||||
@@ -18,18 +20,27 @@ export function MaterialsToolbar({
|
||||
selectedIds,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
extraKeywords,
|
||||
onAddKeyword,
|
||||
onRemoveKeyword,
|
||||
userQuery,
|
||||
onUserQueryChange,
|
||||
onSubmitQuery,
|
||||
placeholder,
|
||||
disabled,
|
||||
}: MaterialsToolbarProps) {
|
||||
const [kwInput, setKwInput] = useState('');
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const allSelected = materials.length > 0 && selectedIds.size === materials.length;
|
||||
|
||||
function handleAddKeyword() {
|
||||
const kw = kwInput.trim();
|
||||
if (!kw || extraKeywords.includes(kw)) return;
|
||||
onAddKeyword(kw);
|
||||
setKwInput('');
|
||||
function handleInput(e: React.ChangeEvent<HTMLTextAreaElement>) {
|
||||
onUserQueryChange(e.target.value);
|
||||
const el = e.target;
|
||||
el.style.height = 'auto';
|
||||
el.style.height = `${Math.min(el.scrollHeight, 96)}px`;
|
||||
}
|
||||
|
||||
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && onSubmitQuery) {
|
||||
e.preventDefault();
|
||||
onSubmitQuery();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -51,50 +62,21 @@ export function MaterialsToolbar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 补充关键词 */}
|
||||
{/* 单输入框 */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs text-muted-foreground">补充搜索关键词(可选)</label>
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={kwInput}
|
||||
onChange={(e) => setKwInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddKeyword()}
|
||||
placeholder="输入关键词后按 Enter 或点击 +"
|
||||
className="flex-1 h-7 rounded-md border border-input bg-background px-2.5 py-1 text-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2"
|
||||
onClick={handleAddKeyword}
|
||||
disabled={!kwInput.trim()}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 已添加关键词 Tag 列表 */}
|
||||
{extraKeywords.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{extraKeywords.map((kw) => (
|
||||
<span
|
||||
key={kw}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary/10 text-primary px-2 py-0.5 text-xs"
|
||||
>
|
||||
{kw}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveKeyword(kw)}
|
||||
className="hover:text-destructive leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<label className="text-xs text-muted-foreground">
|
||||
补充检索(可选)— 写下你想检索的内容,然后点"重新搜索"或"技能检索"
|
||||
</label>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
value={userQuery}
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder ?? '例如:想查看各大平台销量 / 2025 年财报 / 行业头部公司近期动态…'}
|
||||
disabled={disabled}
|
||||
className="w-full resize-none rounded-md border border-input bg-background px-2.5 py-1.5 text-xs leading-5 placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50 min-h-[28px] max-h-24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Sparkles, ChevronDown, ChevronRight, Brain, Zap } from 'lucide-react';
|
||||
|
||||
interface SkillSelectStreamingViewProps {
|
||||
text: string;
|
||||
thinkingText?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从流式 JSON 文本里增量提取已经识别出的技能名。
|
||||
*
|
||||
* LLM 输出形如 `{"selected_skills": ["skill-a", "skill-b", "skill-c"], "reason": "..."}`,
|
||||
* 我们不等整段 JSON 解析完,逐字符扫描"已闭合"的双引号字符串:
|
||||
* - 找到 `"selected_skills"`(或末尾不完整时回退兜底)后取后续内容
|
||||
* - 在该位置之后的 `[ ... ]` 内逐个匹配 `"..."`
|
||||
* - 只取**已闭合**的字符串(结尾必须是 `"`),确保不把正在打字的半截名字露出来
|
||||
*/
|
||||
function extractStreamingSkillNames(text: string): string[] {
|
||||
if (!text) return [];
|
||||
// 定位 selected_skills 的值起点(容错:允许中英文引号 / 不同空白)
|
||||
const keyMatch = text.match(/["']?selected_skills["']?\s*:\s*\[/);
|
||||
if (!keyMatch) return [];
|
||||
const start = keyMatch.index! + keyMatch[0].length;
|
||||
// 截取数组段(找到对应的 ],或者文本结尾)
|
||||
const rest = text.slice(start);
|
||||
const endIdx = rest.indexOf(']');
|
||||
const arrBody = endIdx >= 0 ? rest.slice(0, endIdx) : rest;
|
||||
// 只提取已经"闭合"的字符串:[^"\\]*(?:\\.[^"\\]*)* 处理转义引号
|
||||
const names: string[] = [];
|
||||
const re = /"((?:[^"\\]|\\.)*)"/g;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(arrBody)) !== null) {
|
||||
const name = m[1].trim();
|
||||
if (name) names.push(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* 技能选择 LLM 流式打字机视图。
|
||||
*
|
||||
* - 思考过程(<think></think> 内)用 amber 折叠面板(与 OutlineStreamingView 风格一致)
|
||||
* - 正文 JSON 用 violet 主题(与 SkillStepsCard 同色系,强调"技能"语义)
|
||||
* - 两边都自动滚动到底;正文带闪烁光标,思考过程在正文未开始时也有光标
|
||||
*/
|
||||
export function SkillSelectStreamingView({ text, thinkingText }: SkillSelectStreamingViewProps) {
|
||||
const preRef = useRef<HTMLPreElement>(null);
|
||||
const thinkRef = useRef<HTMLPreElement>(null);
|
||||
const [thinkOpen, setThinkOpen] = useState(true);
|
||||
|
||||
// 实时从流式 JSON 中增量解析出已识别的技能名
|
||||
const streamingSkills = useMemo(() => extractStreamingSkillNames(text), [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (thinkOpen && thinkRef.current) thinkRef.current.scrollTop = thinkRef.current.scrollHeight;
|
||||
}, [thinkingText, thinkOpen]);
|
||||
|
||||
if (!text && !thinkingText) return null;
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-2 space-y-2">
|
||||
{thinkingText && (
|
||||
<div className="rounded-xl border border-amber-200 dark:border-amber-900 bg-amber-50/50 dark:bg-amber-950/20 overflow-hidden shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setThinkOpen((v) => !v)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 border-b border-amber-200/60 dark:border-amber-900/60 bg-amber-100/40 dark:bg-amber-900/30 hover:bg-amber-100/70 dark:hover:bg-amber-900/50 transition-colors"
|
||||
>
|
||||
<Brain className="size-3.5 text-amber-500 shrink-0" />
|
||||
<span className="text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||
思考过程
|
||||
</span>
|
||||
<span className="ml-auto text-amber-400">
|
||||
{thinkOpen
|
||||
? <ChevronDown className="size-3.5" />
|
||||
: <ChevronRight className="size-3.5" />}
|
||||
</span>
|
||||
</button>
|
||||
{thinkOpen && (
|
||||
<pre
|
||||
ref={thinkRef}
|
||||
className="text-[11px] font-mono leading-relaxed p-3 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-40 overflow-y-auto italic"
|
||||
>
|
||||
{thinkingText}
|
||||
{!text && (
|
||||
<span className="inline-block w-0.5 h-3.5 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
||||
)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{text && (
|
||||
<div className="rounded-xl border border-violet-200 dark:border-violet-900 bg-violet-50/50 dark:bg-violet-950/20 overflow-hidden shadow-sm">
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-violet-200/60 dark:border-violet-900/60 bg-violet-100/40 dark:bg-violet-900/30">
|
||||
<Sparkles className="size-3.5 text-violet-500 shrink-0" />
|
||||
<span className="text-xs font-medium text-violet-600 dark:text-violet-400">
|
||||
正在筛选相关技能
|
||||
{streamingSkills.length > 0 && (
|
||||
<span className="ml-1 text-violet-500/80">
|
||||
· 已识别 {streamingSkills.length} 个
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="ml-auto inline-flex gap-1">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="size-1 rounded-full bg-violet-400 animate-bounce"
|
||||
style={{ animationDelay: `${i * 150}ms` }}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 已识别的技能名:每个新出现的 badge 会 fade-in,给用户即时反馈 */}
|
||||
{streamingSkills.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 px-3 py-2 border-b border-violet-200/40 dark:border-violet-900/40 bg-violet-50/40 dark:bg-violet-950/10">
|
||||
{streamingSkills.map((name, idx) => (
|
||||
<span
|
||||
key={`${name}-${idx}`}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-mono bg-violet-100 dark:bg-violet-900/60 text-violet-700 dark:text-violet-200 animate-[skill-fade-in_0.25s_ease-out]"
|
||||
style={{ animationFillMode: 'both' }}
|
||||
>
|
||||
<Zap className="size-2.5 shrink-0" />
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<pre
|
||||
ref={preRef}
|
||||
className="text-[11px] font-mono leading-relaxed p-3 text-muted-foreground whitespace-pre-wrap max-h-52 overflow-y-auto"
|
||||
>
|
||||
{text}
|
||||
<span className="inline-block w-0.5 h-3.5 bg-violet-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
@keyframes skill-fade-in {
|
||||
from { opacity: 0; transform: translateY(-2px) scale(0.95); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronRight, Sparkles, ListChecks, Zap, Loader2, CheckCircle2, CircleStop } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { SkillSearchStep } from '../../types/ai-writing';
|
||||
import { CollapseTransition } from './CollapseTransition';
|
||||
|
||||
interface SkillStepsCardProps {
|
||||
steps: SkillSearchStep[];
|
||||
/** 是否仍在进行中(researching 状态下显示 spinner,让用户看到流式进展) */
|
||||
inProgress?: boolean;
|
||||
}
|
||||
|
||||
const STEP_ICON: Record<SkillSearchStep['type'], typeof Sparkles> = {
|
||||
list_skills: ListChecks,
|
||||
select_skills: Sparkles,
|
||||
invoke_skill: Zap,
|
||||
skill_result: CheckCircle2,
|
||||
stop_early: CircleStop,
|
||||
};
|
||||
|
||||
const STEP_LABEL: Record<SkillSearchStep['type'], string> = {
|
||||
list_skills: '列出可用技能',
|
||||
select_skills: '筛选相关技能',
|
||||
invoke_skill: '调用技能',
|
||||
skill_result: '检索结果',
|
||||
stop_early: '提前停止',
|
||||
};
|
||||
|
||||
export function SkillStepsCard({ steps, inProgress }: SkillStepsCardProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
if (steps.length === 0 && !inProgress) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-violet-200 bg-violet-50/40 dark:border-violet-900/40 dark:bg-violet-950/20 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-xs font-medium hover:bg-violet-100/50 dark:hover:bg-violet-900/30 transition-colors text-left"
|
||||
>
|
||||
{inProgress
|
||||
? <Loader2 className="size-3.5 text-violet-500 shrink-0 animate-spin" />
|
||||
: <Sparkles className="size-3.5 text-violet-500 shrink-0" />}
|
||||
<span className="flex-1">
|
||||
技能检索过程
|
||||
<span className="ml-1 text-muted-foreground font-normal">
|
||||
(共 {steps.length} 步{inProgress ? ',进行中…' : ''})
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-3 text-muted-foreground transition-transform duration-200',
|
||||
expanded && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<CollapseTransition open={expanded}>
|
||||
<ol className="px-3 pb-2.5 pt-1 space-y-1.5 border-t border-violet-200/60 dark:border-violet-900/30">
|
||||
{steps.map((step, idx) => {
|
||||
const Icon = STEP_ICON[step.type] ?? Sparkles;
|
||||
const label = STEP_LABEL[step.type] ?? step.type;
|
||||
const isStopEarly = step.type === 'stop_early';
|
||||
const isResult = step.type === 'skill_result';
|
||||
return (
|
||||
<li key={idx} className="flex items-start gap-2 text-[11px]">
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 mt-0.5 inline-flex items-center justify-center size-4 rounded-full text-[10px] font-medium',
|
||||
isStopEarly
|
||||
? 'bg-amber-100 dark:bg-amber-900/60 text-amber-600 dark:text-amber-300'
|
||||
: isResult
|
||||
? 'bg-emerald-100 dark:bg-emerald-900/60 text-emerald-600 dark:text-emerald-300'
|
||||
: 'bg-violet-100 dark:bg-violet-900/60 text-violet-600 dark:text-violet-300',
|
||||
)}
|
||||
>
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 font-medium',
|
||||
isStopEarly
|
||||
? 'text-amber-700 dark:text-amber-300'
|
||||
: isResult
|
||||
? 'text-emerald-700 dark:text-emerald-300'
|
||||
: 'text-violet-700 dark:text-violet-300',
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3 shrink-0" />
|
||||
<span>{label}</span>
|
||||
{step.skillName && (
|
||||
<span className="font-mono text-violet-900 dark:text-violet-100">
|
||||
· {step.skillName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-0.5">{step.message}</div>
|
||||
{step.skillNames && step.skillNames.length > 0 && (
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{step.skillNames.map((name) => (
|
||||
<span
|
||||
key={name}
|
||||
className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-mono bg-violet-100 dark:bg-violet-900/60 text-violet-700 dark:text-violet-200"
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.reason && (
|
||||
<div className="mt-0.5 text-[10px] text-violet-600/80 dark:text-violet-400/80 italic">
|
||||
原因:{step.reason}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{inProgress && (
|
||||
<li className="flex items-center gap-2 text-[11px] text-muted-foreground italic">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
更多步骤生成中…
|
||||
</li>
|
||||
)}
|
||||
</ol>
|
||||
</CollapseTransition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import type {
|
||||
ProgressEventType,
|
||||
ReviewIssue,
|
||||
ReviewResult,
|
||||
SkillSearchStep,
|
||||
StreamingReviewItem,
|
||||
UserInterventionPayload,
|
||||
WritingStatus,
|
||||
@@ -71,6 +72,9 @@ const INITIAL_STATE: AIWritingState = {
|
||||
intentThinkingText: '',
|
||||
parsedIntent: null,
|
||||
researchProgress: '',
|
||||
skillSearchSteps: [],
|
||||
skillSelectStreamingText: '',
|
||||
skillSelectThinkingText: '',
|
||||
streamingReviewItems: [],
|
||||
revisionSummary: null,
|
||||
pendingRevisionContext: null,
|
||||
@@ -105,7 +109,7 @@ function reducer(prev: AIWritingState, action: Action): AIWritingState {
|
||||
...prev,
|
||||
currentPause: null,
|
||||
status: ns,
|
||||
...(ns === 'researching' ? { researchProgress: '' } : {}),
|
||||
...(ns === 'researching' ? { researchProgress: '', skillSearchSteps: [], skillSelectStreamingText: '', skillSelectThinkingText: '' } : {}),
|
||||
...(ns === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}),
|
||||
...(ns === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}),
|
||||
...(ns === 'reviewing' ? { streamingReviewItems: [] } : {}),
|
||||
@@ -191,12 +195,36 @@ function handleEvent(prev: AIWritingState, event: ProgressEvent): AIWritingState
|
||||
case 'resumed':
|
||||
return { ...base, currentPause: null };
|
||||
|
||||
case 'materials_ready':
|
||||
case 'skill_search_step': {
|
||||
const step = event.skillStep;
|
||||
if (!step) return base;
|
||||
// select_skills 步骤到达 = LLM 选择完成,清空流式打字缓冲
|
||||
const clearStreaming = step.type === 'select_skills'
|
||||
? { skillSelectStreamingText: '', skillSelectThinkingText: '' }
|
||||
: {};
|
||||
return { ...base, skillSearchSteps: [...prev.skillSearchSteps, step], ...clearStreaming };
|
||||
}
|
||||
|
||||
case 'skill_select_chunk': {
|
||||
const text = (event as unknown as { text: string }).text ?? '';
|
||||
const isThinking = (event as unknown as { is_thinking: boolean }).is_thinking ?? false;
|
||||
return {
|
||||
...prev,
|
||||
skillSelectStreamingText: isThinking ? prev.skillSelectStreamingText : prev.skillSelectStreamingText + text,
|
||||
skillSelectThinkingText: isThinking ? prev.skillSelectThinkingText + text : prev.skillSelectThinkingText,
|
||||
};
|
||||
}
|
||||
|
||||
case 'materials_ready': {
|
||||
// 后端把 pkg.skill_search_steps 一并塞回来时,覆盖前端累积(保证刷新页面回放也对)
|
||||
const incomingSteps = (event as unknown as { skillSearchSteps?: SkillSearchStep[] }).skillSearchSteps;
|
||||
return {
|
||||
...base,
|
||||
materials: (event.materials ?? []) as AIMaterial[],
|
||||
researchProgress: '',
|
||||
...(incomingSteps ? { skillSearchSteps: incomingSteps } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
case 'outline_chunk': {
|
||||
const text = (event as unknown as { text: string }).text ?? '';
|
||||
@@ -337,6 +365,7 @@ function handleEvent(prev: AIWritingState, event: ProgressEvent): AIWritingState
|
||||
// ── snake_case ↔ camelCase 归一化(custom 通道事件 + interrupt payload) ──────
|
||||
|
||||
function normalizeMaterial(m: Record<string, unknown>): AIMaterial {
|
||||
const extra = (m.extra_metadata ?? m.extraMetadata) as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: String(m.id ?? ''),
|
||||
title: String(m.title ?? ''),
|
||||
@@ -344,9 +373,21 @@ function normalizeMaterial(m: Record<string, unknown>): AIMaterial {
|
||||
source: String(m.source ?? ''),
|
||||
publishedAt: String(m.published_at ?? m.publishedAt ?? ''),
|
||||
relevanceScore: Number(m.relevance_score ?? m.relevanceScore ?? 0),
|
||||
sourceType: (m.source_type ?? m.sourceType ?? 'internet') as 'internet' | 'intranet',
|
||||
sourceType: (m.source_type ?? m.sourceType ?? 'internet') as AIMaterial['sourceType'],
|
||||
url: m.url as string | undefined,
|
||||
recUuid: (m.recUuid ?? m.rec_uuid) as string | undefined,
|
||||
extraMetadata: extra,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSkillStep(s: Record<string, unknown>): SkillSearchStep {
|
||||
return {
|
||||
type: (s.type as SkillSearchStep['type']) ?? 'invoke_skill',
|
||||
message: String(s.message ?? ''),
|
||||
skillNames: (s.skill_names ?? s.skillNames) as string[] | undefined,
|
||||
skillName: (s.skill_name ?? s.skillName) as string | undefined,
|
||||
reason: s.reason as string | undefined,
|
||||
matchedCount: (s.matched_count ?? s.matchedCount) as number | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -445,6 +486,7 @@ function customToProgressEvent(raw: unknown, sessionId: string): ProgressEvent |
|
||||
'intent_chunk', 'intent_ready',
|
||||
'research_progress', 'outline_chunk', 'draft_chunk',
|
||||
'review_chunk', 'review_item_done',
|
||||
'skill_search_step', 'skill_select_chunk',
|
||||
];
|
||||
if (!allowed.includes(type as ProgressEventType)) return null;
|
||||
|
||||
@@ -459,6 +501,10 @@ function customToProgressEvent(raw: unknown, sessionId: string): ProgressEvent |
|
||||
if (Array.isArray(merged.issues)) {
|
||||
merged.reviewItemIssues = (merged.issues as Record<string, unknown>[]).map(normalizeReviewIssue);
|
||||
}
|
||||
// skill_search_step:把字段折成 skillStep 子对象,方便 reducer / UI 取用
|
||||
if (type === 'skill_search_step') {
|
||||
merged.skillStep = normalizeSkillStep(o);
|
||||
}
|
||||
return merged as unknown as ProgressEvent;
|
||||
}
|
||||
|
||||
@@ -478,6 +524,9 @@ function nodeUpdatesToEvents(nodeOutput: unknown, sessionId: string): ProgressEv
|
||||
// ``material_package`` 出现 → materials_ready(节点 progress_events 里不带)
|
||||
if (out.material_package && typeof out.material_package === 'object') {
|
||||
const pkg = out.material_package as Record<string, unknown>;
|
||||
const skillSteps = Array.isArray(pkg.skill_search_steps ?? pkg.skillSearchSteps)
|
||||
? ((pkg.skill_search_steps ?? pkg.skillSearchSteps) as Record<string, unknown>[]).map(normalizeSkillStep)
|
||||
: undefined;
|
||||
events.push({
|
||||
type: 'materials_ready',
|
||||
sessionId,
|
||||
@@ -485,7 +534,8 @@ function nodeUpdatesToEvents(nodeOutput: unknown, sessionId: string): ProgressEv
|
||||
materials: Array.isArray(pkg.materials) ? (pkg.materials as Record<string, unknown>[]).map(normalizeMaterial) : [],
|
||||
keywords: (pkg.keywords ?? []) as string[],
|
||||
materialSummary: (pkg.summary as string) ?? '',
|
||||
});
|
||||
...(skillSteps ? { skillSearchSteps: skillSteps } as unknown as Partial<ProgressEvent> : {}),
|
||||
} as ProgressEvent);
|
||||
}
|
||||
|
||||
// ``current_outline`` 出现且 progress_events 里没 outline_ready → 合成一条
|
||||
@@ -733,7 +783,9 @@ export function useAIWriting() {
|
||||
// 乐观更新到下一阶段,跟老 hook 行为对齐
|
||||
let nextStatus: WritingStatus = state.status;
|
||||
if (pausePoint === 'material_confirm') {
|
||||
nextStatus = payload.action === 're_search' ? 'researching' : 'writing_outline';
|
||||
// re_search / enable_skill_search 都回到 researching;confirm 才进大纲
|
||||
nextStatus = (payload.action === 're_search' || payload.action === 'enable_skill_search')
|
||||
? 'researching' : 'writing_outline';
|
||||
} else if (pausePoint === 'outline_confirm') {
|
||||
nextStatus = payload.action === 're_outline' ? 'writing_outline' : 'writing_draft';
|
||||
} else if (pausePoint === 'draft_confirm') {
|
||||
@@ -754,6 +806,7 @@ export function useAIWriting() {
|
||||
};
|
||||
if (payload.approvedMaterialIds !== undefined) resumePayload.approvedMaterialIds = payload.approvedMaterialIds;
|
||||
if (payload.extraKeywords !== undefined) resumePayload.extraKeywords = payload.extraKeywords;
|
||||
if (payload.userQuery !== undefined) resumePayload.userQuery = payload.userQuery;
|
||||
if (payload.editedOutline !== undefined) resumePayload.editedOutline = payload.editedOutline;
|
||||
if (payload.outlineFeedback !== undefined) resumePayload.outlineFeedback = payload.outlineFeedback;
|
||||
if (payload.userRevisionNotes !== undefined) resumePayload.userRevisionNotes = payload.userRevisionNotes;
|
||||
|
||||
@@ -7,9 +7,26 @@ export interface AIMaterial {
|
||||
source: string;
|
||||
publishedAt: string;
|
||||
relevanceScore: number;
|
||||
sourceType: 'internet' | 'intranet';
|
||||
sourceType: 'internet' | 'intranet' | 'skill';
|
||||
url?: string;
|
||||
recUuid?: string;
|
||||
// skill 类素材的元信息(skill_name / container_path / category / skill_full_content 等)
|
||||
extraMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// 技能检索过程的单步记录(前端时间线展示"使用了 X 技能")
|
||||
// list_skills:列出全部 enabled 技能
|
||||
// select_skills:LLM 按相关度排序的候选
|
||||
// invoke_skill:开始调用某个技能去检索
|
||||
// skill_result:某个技能检索返回了 N 条素材
|
||||
// stop_early:累计素材达到上限,停止后续技能
|
||||
export interface SkillSearchStep {
|
||||
type: 'list_skills' | 'select_skills' | 'invoke_skill' | 'skill_result' | 'stop_early';
|
||||
message: string;
|
||||
skillNames?: string[];
|
||||
skillName?: string;
|
||||
reason?: string;
|
||||
matchedCount?: number;
|
||||
}
|
||||
|
||||
// ── 大纲 ─────────────────────────────────────────────────────────────────────
|
||||
@@ -139,6 +156,8 @@ export type ProgressEventType =
|
||||
| 'await_user'
|
||||
| 'resumed'
|
||||
| 'notice'
|
||||
| 'skill_search_step'
|
||||
| 'skill_select_chunk'
|
||||
| 'done'
|
||||
| 'error';
|
||||
|
||||
@@ -173,6 +192,8 @@ export interface ProgressEvent {
|
||||
pauseContext?: Record<string, unknown>;
|
||||
// section_help:素材不足、待用户决策的章节
|
||||
blockedSections?: BlockedSection[];
|
||||
// skill_search_step:技能检索步骤事件(list_skills / select_skills / invoke_skill)
|
||||
skillStep?: SkillSearchStep;
|
||||
}
|
||||
|
||||
// ── 用户干预提交体 ────────────────────────────────────────────────────────────
|
||||
@@ -180,7 +201,10 @@ export interface ProgressEvent {
|
||||
export interface UserInterventionPayload {
|
||||
action: string;
|
||||
approvedMaterialIds?: string[];
|
||||
/** @deprecated 旧版多关键词追加。新版统一用 userQuery 单输入框 */
|
||||
extraKeywords?: string[];
|
||||
/** PAUSE-1 素材确认:用户单输入框写的自然语言(技能名/来源/检索意图都行)。re_search / enable_skill_search 共用。 */
|
||||
userQuery?: string;
|
||||
editedOutline?: AIOutline;
|
||||
outlineFeedback?: string;
|
||||
userRevisionNotes?: string;
|
||||
@@ -291,6 +315,11 @@ export interface AIWritingState {
|
||||
parsedIntent: ParsedIntent | null;
|
||||
// 素材收集实时进度(最新一条提示文案)
|
||||
researchProgress: string;
|
||||
// 技能检索步骤(按发生顺序累积,pause_material 卡片里渲染时间线)
|
||||
skillSearchSteps: SkillSearchStep[];
|
||||
// 技能选择 LLM 流式打字内容(正文 / 思考分开),select_skills 完成后清空
|
||||
skillSelectStreamingText: string;
|
||||
skillSelectThinkingText: string;
|
||||
// 流式审核(逐条到达)
|
||||
streamingReviewItems: StreamingReviewItem[];
|
||||
// 修改摘要
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getStoredAuth } from "@/core/auth";
|
||||
import { RouteBaseProvider } from "@/core/route-base";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
|
||||
import GeneralQA from "@/strategy-components/components/qa/GeneralQA";
|
||||
import GeneralQA from "@/strategy-components/components/chat-qa/GeneralQA";
|
||||
import { ChatRuntime } from "./WorkspaceRoutes";
|
||||
import AgentsPage from "./AgentsPage";
|
||||
import ChatsPage from "./ChatsPage";
|
||||
@@ -30,6 +30,15 @@ import LightAppNoteListPage from "./LightAppNoteListPage";
|
||||
import LightAppNoteQAPage from "./LightAppNoteQAPage";
|
||||
import TaskXkFrom from "@/strategy-components/components/task-components/TaskXkFrom";
|
||||
import TaskManagement from "@/strategy-components/components/task-components/TaskManagement";
|
||||
import TaskPgFrom from "@/strategy-components/components/task-components/TaskPgFrom";
|
||||
import FanganTyPage from "@/strategy-components/components/task-components/FanganTy";
|
||||
import XingdongTyPage from "@/strategy-components/components/task-components/XingdongTy";
|
||||
import XXTyPage from "@/strategy-components/components/task-components/XXTy";
|
||||
import SentimentAnalyzes from "@/strategy-components/components/task-components/SentimentAnalyzes";
|
||||
import TaskAnalyzes from "@/strategy-components/components/task-components/TaskAnalyzes";
|
||||
import { ActionAnalyzes } from "@/strategy-components/components/task-components/ActionAnalyzes";
|
||||
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
|
||||
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
|
||||
|
||||
export default function StrategyRoutes() {
|
||||
return (
|
||||
@@ -41,26 +50,91 @@ export default function StrategyRoutes() {
|
||||
|
||||
{/* 问答中心 */}
|
||||
<Route path="qa/general" element={<GeneralQA />} />
|
||||
<Route
|
||||
path="qa/expert-qa"
|
||||
element={
|
||||
<GeneralQA
|
||||
chatHeadTitle="专家问答"
|
||||
chatDesign="支持单轮RAG、多轮RAG、深度检索及结果对比"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 任务管理 */}
|
||||
<Route path="task-management" element={<TaskManagement />}>
|
||||
<Route index element={<Navigate to="xk-task-jc" replace />} />
|
||||
{/* XK任务 */}
|
||||
<Route
|
||||
path="xk-task-jc"
|
||||
element={<TaskXkFrom gopath="#/pageIndie/pageHome/pageSwrw" />}
|
||||
/>
|
||||
<Route
|
||||
path="xk-task-dispatch"
|
||||
element={<TaskXkFrom gopath="#/pageIndie/pageJczl" />}
|
||||
/>
|
||||
{/* pg中任务 */}
|
||||
<Route
|
||||
path="effect-task-pg"
|
||||
element={<TaskPgFrom gopath="task" />}
|
||||
/>
|
||||
<Route
|
||||
path="effect-action-pg"
|
||||
element={<TaskPgFrom gopath="xd" />}
|
||||
/>
|
||||
<Route
|
||||
path="effect-info-pg"
|
||||
element={<TaskPgFrom gopath="info" />}
|
||||
/>
|
||||
{/* COH中任务 */}
|
||||
<Route
|
||||
path="plan-task-sentiment"
|
||||
element={
|
||||
<TaskXkFrom
|
||||
gopath={'#/pageIndie/pageHome/pageSwrw'}
|
||||
<SentimentAnalyzes
|
||||
title="3QFX"
|
||||
category="规划任务"
|
||||
subCategory="规划任务"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="xk-task-dispatch"
|
||||
path="plan-task/plan-task-fx"
|
||||
element={
|
||||
<TaskXkFrom
|
||||
gopath={'#/pageIndie/pageJczl'}
|
||||
<TaskAnalyzes
|
||||
title="任务FX"
|
||||
category="规划任务"
|
||||
subCategory="规划任务"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="plan-task/plan-task-action"
|
||||
element={
|
||||
<ActionAnalyzes
|
||||
title="xdfx"
|
||||
category="规划任务"
|
||||
subCategory="规划任务"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* MNTY任务 */}
|
||||
<Route path="mnty-task-mechanism" element={<XingdongTyPage />} />
|
||||
<Route path="mnty-task-plan" element={<FanganTyPage />} />
|
||||
<Route path="mnty-task-zsty" element={<XXTyPage />} />
|
||||
</Route>
|
||||
{/* 资源管理 */}
|
||||
<Route
|
||||
path="resource-management/resource-network-actor"
|
||||
element={<NetworkActorResourceManagement />}
|
||||
/>
|
||||
<Route
|
||||
path="resource-management/resource-knowledge-base"
|
||||
element={<KnowledgeBaseResourceManagement />}
|
||||
/>
|
||||
<Route
|
||||
path="resource-management/resource-evaluation-index"
|
||||
element={
|
||||
<TaskXkFrom gopath="#/pageIndie/pageZyjc/pageZyzl" />
|
||||
}
|
||||
/>
|
||||
{/* 轻应用 */}
|
||||
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
||||
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
||||
|
||||
@@ -68,13 +68,20 @@ export interface RecommendCandidate {
|
||||
* Agent ids that must never enter the candidate pool. Centralized here so
|
||||
* every caller (recommend dialog, future tools) applies the same rule.
|
||||
*
|
||||
* - `roundtable-intent` — Step 1 clarification specialist, no role in research.
|
||||
* - `roundtable-recommender`— the recommender itself, can't recommend itself.
|
||||
* - `roundtable-coordinator-*` — disposable Step 2 coordinator shells (DB
|
||||
* accumulates one per session); meaningless as roundtable participants.
|
||||
* - `roundtable-intent` — Step 1 clarification specialist, no role in research.
|
||||
* - `roundtable-recommender` — the recommender itself, can't recommend itself.
|
||||
* - `roundtable-coordinator` — Step 2 coordinator singleton (2026-05 改造后固定 id,
|
||||
* ensure 自动落地;它是"协调者",不能反过来作为席位被协调)。
|
||||
* - `roundtable-coordinator-*` 前缀 — 历史残留:2026-05 之前每次 init 会创建
|
||||
* `roundtable-coordinator-<ts36>` 形式的临时 agent,DB 仍可能保留这些遗留记录。
|
||||
* 保留前缀过滤可以在管理员清理之前先把它们隐藏起来,避免出现在推荐弹窗候选池里。
|
||||
*/
|
||||
const EXCLUDED_AGENT_ID_PREFIX = "roundtable-coordinator-";
|
||||
const EXCLUDED_AGENT_IDS = new Set<string>(["roundtable-intent", "roundtable-recommender"]);
|
||||
const EXCLUDED_AGENT_IDS = new Set<string>([
|
||||
"roundtable-intent",
|
||||
"roundtable-recommender",
|
||||
"roundtable-coordinator",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Drop excluded ids from a raw `GET /api/agents` list and project each entry
|
||||
|
||||
@@ -36,7 +36,9 @@ import {
|
||||
type RecommendCandidate,
|
||||
type RecommendStreamPhase,
|
||||
} from "../api/recommend";
|
||||
import { useAutoScroll } from "../hooks/useAutoScroll";
|
||||
import { splitInlineReasoning } from "../lib/reasoning";
|
||||
import { ScrollToBottomButton } from "./ScrollToBottomButton";
|
||||
|
||||
/**
|
||||
* Picks-selection dialog that sits between Step 1 (intent) and Step 2
|
||||
@@ -453,6 +455,7 @@ export function RecommendAgentsDialog({
|
||||
state.phase === "loading_agents" || state.phase === "recommending"
|
||||
}
|
||||
phase={state.phase}
|
||||
scrollDeps={[state.rationale, state.steps, state.streamPhase]}
|
||||
>
|
||||
<RecommenderOutput state={state} />
|
||||
</CollapsibleModelOutput>
|
||||
@@ -593,16 +596,22 @@ const MODEL_OUTPUT_AUTO_CLOSE_DELAY_MS = 1000;
|
||||
function CollapsibleModelOutput({
|
||||
isLoading,
|
||||
phase,
|
||||
scrollDeps,
|
||||
children,
|
||||
}: {
|
||||
/** True 时面板保持展开 + header 显示 spinner;涵盖加载候选 + 流式生成。 */
|
||||
isLoading: boolean;
|
||||
phase: DialogPhase;
|
||||
/** 内容变化时触发自动滚到底(与 Step1/Step2 对话区同一套 useAutoScroll)。 */
|
||||
scrollDeps: React.DependencyList;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState<boolean>(true);
|
||||
const userTouchedRef = useRef<boolean>(false);
|
||||
const hasAutoClosedRef = useRef<boolean>(false);
|
||||
const modelOutputScrollRef = useRef<HTMLDivElement>(null);
|
||||
const { atBottom: modelOutputAtBottom, scrollToBottom: scrollModelOutputToBottom } =
|
||||
useAutoScroll(modelOutputScrollRef, scrollDeps);
|
||||
|
||||
// 加载中 → 保持展开(用户没手动动过的前提下)。
|
||||
// 加载完成 → 延迟自动收起一次(仅一次),让出焦点给下方推荐卡片。
|
||||
@@ -677,8 +686,16 @@ function CollapsibleModelOutput({
|
||||
/>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-3 pb-3 min-h-0 flex-1 overflow-y-auto custom-scrollbar">
|
||||
{children}
|
||||
<div className="relative min-h-0 flex-1 flex flex-col">
|
||||
<div
|
||||
ref={modelOutputScrollRef}
|
||||
className="px-3 pb-3 min-h-0 flex-1 overflow-y-auto custom-scrollbar"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!modelOutputAtBottom && (
|
||||
<ScrollToBottomButton onClick={scrollModelOutputToBottom} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -751,7 +768,10 @@ function RecommenderOutput({ state }: { state: InternalState }) {
|
||||
{hasReasoning && (
|
||||
<ReasoningBlock
|
||||
streaming={isStreaming}
|
||||
contentClassName="max-h-48 overflow-y-auto custom-scrollbar"
|
||||
contentClassName={cn(
|
||||
"overflow-y-auto custom-scrollbar",
|
||||
isStreaming ? "max-h-none" : "max-h-48",
|
||||
)}
|
||||
>
|
||||
{reasoning!}
|
||||
</ReasoningBlock>
|
||||
@@ -762,7 +782,12 @@ function RecommenderOutput({ state }: { state: InternalState }) {
|
||||
)}
|
||||
|
||||
{hasRationale && (
|
||||
<div className="max-h-40 overflow-y-auto custom-scrollbar rounded-md border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700 leading-relaxed">
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-y-auto custom-scrollbar rounded-md border border-slate-200 bg-white px-3 py-2.5 text-sm text-slate-700 leading-relaxed",
|
||||
isStreaming ? "max-h-none" : "max-h-40",
|
||||
)}
|
||||
>
|
||||
<MarkdownContent>{cleanedRationale}</MarkdownContent>
|
||||
{isStreaming && <StreamingCursor />}
|
||||
</div>
|
||||
|
||||
@@ -833,16 +833,19 @@ export function useStep2Orchestration(
|
||||
|
||||
const markSpeakerStreaming = useCallback(
|
||||
(speakerId: number) => {
|
||||
setActiveSpeakerSet((prev) => {
|
||||
if (prev.has(speakerId)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.add(speakerId);
|
||||
return next;
|
||||
});
|
||||
// 编排循环已经改成**顺序**派 special(详见 §5.4 / 文件顶部注释),
|
||||
// 任意时刻只有一个子智能体在跑。因此这里既要把当前发言席位置为"研讨中",
|
||||
// 又要把上一轮 / 历史残留下来的"研讨中"统一清成"在线",保证右侧"参与角色"
|
||||
// 面板任何时刻**只有一个** sub agent 显示研讨中,与对话区实际在 stream 的
|
||||
// 那一个一致。activeSpeakerSet 沿用 Set 形态(虽然实际只放 1 个)以兼容外部
|
||||
// 消费方,真正的"高亮排他"靠 roles 的 status reset。
|
||||
setActiveSpeakerSet(() => new Set([speakerId]));
|
||||
setRoles((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === speakerId ? { ...item, status: "研讨中" } : item,
|
||||
),
|
||||
prev.map((item) => {
|
||||
if (item.id === speakerId) return { ...item, status: "研讨中" };
|
||||
if (item.status === "研讨中") return { ...item, status: "在线" };
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setRoles],
|
||||
@@ -912,7 +915,14 @@ export function useStep2Orchestration(
|
||||
safetyCounter < MAX_CYCLES
|
||||
) {
|
||||
safetyCounter += 1;
|
||||
highlightSpeaker(2);
|
||||
// Leader(协调智能体)正在派活,**不应该**让任何 sub agent 显示"研讨中"——
|
||||
// 协调者本身不在右侧"参与角色"面板里(COMMANDER_SEAT_ID=1 是人类主持人,
|
||||
// sub agent 从 id=2 起)。历史代码这里写的是 `highlightSpeaker(2)`,会
|
||||
// 错把第一个 sub agent 误标为研讨中,叠加 special 派活时另一个 sub 也被
|
||||
// markSpeakerStreaming,就出现"两个 sub 同时研讨中"的视觉 bug。
|
||||
// 改成 null 后:leader 跑的阶段所有 sub 都是"在线",special 跑哪个就只
|
||||
// 高亮哪个。activeSpeakerId 同步置空,圆桌可视化也不会高亮错的位置。
|
||||
highlightSpeaker(null);
|
||||
|
||||
// 总控也可能直接 write_file —— 之前只接了 special 流,导致总控写文件时沙箱不自动打开。
|
||||
// 这里把 leader thread id 绑到气泡 + artifact handler 上,与 special 同样的行为。
|
||||
|
||||
@@ -130,8 +130,27 @@ export function nowTime(): string {
|
||||
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆桌总控协调智能体的**固定单例 id**。后端 `_roundtable_seed.COORDINATOR_AGENT_ID`
|
||||
* 与本常量必须保持一致(2026-05 改造前是 `roundtable-coordinator-<ts36>` 形式,
|
||||
* 每次会话生成新 id 导致 `.deer-flow/agents/` 累积大量壳目录)。
|
||||
*
|
||||
* 改造后:
|
||||
* - 后端 ensure 机制保证磁盘上始终存在 `.deer-flow/agents/roundtable-coordinator/`;
|
||||
* - 本次会话的「可调度席位列表」由后端 init 通过 human 消息注入 coordinator
|
||||
* thread 历史,不再写入 SOUL,所以单例 SOUL 长期稳定;
|
||||
* - 前端 `streamMultiAgent({ coordinatorName })` 等位置不需要任何改动,因为
|
||||
* `coordinatorName` 仍然是一个**字符串**,只是值不再每次变化。
|
||||
*/
|
||||
export const COORDINATOR_AGENT_ID = "roundtable-coordinator";
|
||||
|
||||
/**
|
||||
* 兼容性保留:历史上叫 `newCoordinatorName()`,意为"生成新的一次性 coordinator id"。
|
||||
* 改造为单例后实际只返回常量;命名不再准确,但留下避免触发跨文件级联改名。
|
||||
* 新代码请直接使用 {@link COORDINATOR_AGENT_ID}。
|
||||
*/
|
||||
export function newCoordinatorName(): string {
|
||||
return `roundtable-coordinator-${Date.now().toString(36)}`;
|
||||
return COORDINATOR_AGENT_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import service from "@/utils/request";
|
||||
export const requestCrudService = {
|
||||
get: async (endpoint, params = {}) => {
|
||||
try {
|
||||
const response = await service.get(endpoint, { params });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`GET request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
post: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.post(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`POST request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
put: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.put(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`PUT request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
update: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.patch(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`patch request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
delete: async (endpoint) => {
|
||||
try {
|
||||
const response = await service.delete(endpoint);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`DELETE request failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { environment } from '@/environments/environment';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
|
||||
export interface GetOneParam {
|
||||
fields?: string[]; // 指定返回的字段 id,name
|
||||
join?: string[]; // 指定关联表,如 ['user||id,name', 'relation||id']
|
||||
cache?: 0 | 1; // 0直接DB,1缓存,默认1缓存
|
||||
}
|
||||
export interface GetAllParam {
|
||||
fields?: string[] | string; // 指定返回的字段: 多个字段如 'id', 'name'
|
||||
filter?: string[]; // 指定过滤条件,如 ['name||$eq||batman', 'id||$in||1,2,3', 'field||$isnull', 'relation.id||$eq||1']
|
||||
or?: string[]; // 同filter
|
||||
join?: string[]; // 指定关联表,如 ['user||id,name', 'relation||id']
|
||||
cache?: 0 | 1; // 0直接DB,1缓存,默认1缓存
|
||||
page?: number; // 页码(会被转换成offset)
|
||||
offset?: number; // 偏移量
|
||||
limit?: number; // 每页数量
|
||||
sort?: string[]; // 排序,如 '['name,ASC', 'id,DESC]'
|
||||
s?: string;
|
||||
joinTaskAction?: boolean; //是否连行动表查询
|
||||
joinTaskActionBoci?: boolean; //策略分析列表使用
|
||||
}
|
||||
|
||||
export interface GetAllResponse {
|
||||
data: any[];
|
||||
page: number;
|
||||
pageCount: number;
|
||||
count: number;
|
||||
total: number;
|
||||
errMsg?: string;
|
||||
}
|
||||
let deepCopy = (str: any) => {
|
||||
return JSON.parse(JSON.stringify(str));
|
||||
}
|
||||
export class RequestCrudService {
|
||||
|
||||
/**
|
||||
* 获取单个对象
|
||||
* @param url 地址
|
||||
* @param id ID
|
||||
* @param params 参数
|
||||
* @returns 单个对象
|
||||
*/
|
||||
async getOne(url: string, id: number, params?: GetOneParam): Promise<any> {
|
||||
const getUrl = `${url}/${id}`;
|
||||
const res = await requestCrudService.get(getUrl, params);
|
||||
if (res) return res;
|
||||
// this.notification.create('error', getUrl, `${res.data}`);
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* 获取多个对象
|
||||
* @param url 地址
|
||||
* @param params 参数
|
||||
* @returns 多个对象数组
|
||||
*/
|
||||
async getAll(url: string, params: GetAllParam | null = null): Promise<GetAllResponse> {
|
||||
const formattedParams = deepCopy(params || {});
|
||||
// 分页默认值
|
||||
if (!params || !params.limit) formattedParams.limit = 999;
|
||||
if (!params || !params.page) formattedParams.page = 1;
|
||||
// 计算offset
|
||||
if (params && formattedParams.page) {
|
||||
formattedParams.offset = (formattedParams.page - 1) * formattedParams.limit!;
|
||||
delete formattedParams.page;
|
||||
}
|
||||
const res = await requestCrudService.get(url, formattedParams);
|
||||
if (res.status === 200) return res.data;
|
||||
// this.notification.create('error', url, `${res.data}`);
|
||||
return {
|
||||
count: 0,
|
||||
data: [],
|
||||
page: 1,
|
||||
pageCount: 1,
|
||||
total: 0
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 创建单个对象
|
||||
* @param url 地址
|
||||
* @param data 单个对象
|
||||
* @returns 被创建的单个对象
|
||||
*/
|
||||
async createOne(url: string, data: any) {
|
||||
const res = await requestCrudService.post(url, data);
|
||||
if (res.status === 201) return res.data;
|
||||
// this.notification.create('error', url, `${res.data}`);
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* 创建多个对象
|
||||
* @param url 地址
|
||||
* @param data 多个对象
|
||||
* @returns 被创建的多个对象
|
||||
*/
|
||||
async createMulti(url: string, data: any[]) {
|
||||
const res = await requestCrudService.post(url, {
|
||||
bulk: data
|
||||
});
|
||||
if (res.status === 201) return res.data;
|
||||
// this.notification.create('error', url, `${res.data}`);
|
||||
return [];
|
||||
}
|
||||
/**
|
||||
* 更新单个对象
|
||||
* @param url 地址
|
||||
* @param id ID
|
||||
* @param data 单个对象部分键值对
|
||||
* @returns 被修改的单个对象
|
||||
*/
|
||||
async updateOne(url: string, id: number, data: any) {
|
||||
const res = await requestCrudService.update(`${url}/${id}`, data);
|
||||
if (res.status === 200) return res.data;
|
||||
// this.notification.create('error', url, `${res.data}`);
|
||||
return {};
|
||||
}
|
||||
/**
|
||||
* 删除单个对象
|
||||
* @param url 地址
|
||||
* @param id ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
async deleteOne(url: string, id: number) {
|
||||
const res = await requestCrudService.delete(`${url}/${id}`);
|
||||
if (res.status === 200) return res.data;
|
||||
// this.notification.create('error', url, `${res.data}`);
|
||||
return 'error';
|
||||
}
|
||||
// get 请求
|
||||
async simpleGet(url: string, params?: any): Promise<any> {
|
||||
const res = await requestCrudService.get(url, params);
|
||||
if (res.status === 200) return res.data;
|
||||
return res.data;
|
||||
}
|
||||
// post 请求
|
||||
async simplePost(url: string, data: any) {
|
||||
const res = await requestCrudService.post(url, data);
|
||||
if (res.status === 200) return res.data;
|
||||
return res.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import toolsService from "@/utils/tools-request";
|
||||
const service = toolsService;
|
||||
export const toolsCrudService = {
|
||||
get: async (endpoint, params = {}) => {
|
||||
try {
|
||||
const response = await service.get(endpoint, { params });
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`GET request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
post: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.post(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`POST request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
put: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.put(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`PUT request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
patch: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.patch(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`PATCH request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
update: async (endpoint, data = {}) => {
|
||||
try {
|
||||
const response = await service.patch(endpoint, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`patch request failed: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
delete: async (endpoint) => {
|
||||
try {
|
||||
const response = await service.delete(endpoint);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw new Error(`DELETE request failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-190
@@ -88,6 +88,7 @@ export const createThinkingTrace = (mode: QAMode): QAThinkingTrace => {
|
||||
title: '理解问题并拆解检索方向', // 本轮主题:理解问题并确定检索方向
|
||||
thoughtMarkdown: '用户询问 **RAG 技术工作原理**。需要先明确核心链路:\n\n- 问题理解与向量化\n- 知识库召回\n- 上下文增强\n- 大模型生成\n\n第一轮优先检索基础定义和主流程。', // 模型思考过程,展示分析步骤
|
||||
query: 'RAG 检索增强生成 工作原理 基础流程', // 发送给检索系统的查询词
|
||||
queryShow: true,
|
||||
retrievals: [ // 第一轮检索返回的资料列表
|
||||
{
|
||||
id: 'r1-1',
|
||||
@@ -114,6 +115,7 @@ export const createThinkingTrace = (mode: QAMode): QAThinkingTrace => {
|
||||
thoughtMarkdown: '基础链路已经明确,继续检索 **多轮 RAG** 和 **深度检索** 的差异:\n\n1. 是否需要 Query 改写\n2. 是否需要对子问题分别召回\n3. 是否需要结果重排与证据融合',
|
||||
query: '多轮 RAG Query 改写 子问题检索 结果重排', // 本轮发送给检索系统的查询词
|
||||
// 本轮检索返回的资料列表
|
||||
queryShow: true,
|
||||
retrievals: [
|
||||
{
|
||||
id: 'r2-1',
|
||||
@@ -144,6 +146,7 @@ export const createThinkingTrace = (mode: QAMode): QAThinkingTrace => {
|
||||
title: '综合证据并形成回答结构', // 本轮的主题:整合前面检索到的所有证据,组织成完整的回答
|
||||
thoughtMarkdown: '当前证据已覆盖定义、流程和优化机制。最后需要组织成清晰结构:\n\n- 一句话定义\n- 分阶段流程\n- 技术价值\n- 适用场景与注意事项',
|
||||
query: 'RAG 技术价值 幻觉 时效性 可追溯性 应用场景', // 本轮查询:关于 RAG 价值和应用场景的信息
|
||||
queryShow: true,
|
||||
retrievals: [
|
||||
{
|
||||
id: 'r3-1',
|
||||
@@ -181,9 +184,11 @@ export const createMultiRoundStreamTrace = (): QAThinkingTrace => ({
|
||||
title: '正在准备检索',
|
||||
thoughtMarkdown: '',
|
||||
query: '',
|
||||
queryShow: true,
|
||||
queryLoading: true,
|
||||
retrievals: [],
|
||||
}],
|
||||
thinkingComplete: false,
|
||||
finalAnswerMarkdown: '',
|
||||
});
|
||||
|
||||
@@ -196,7 +201,7 @@ export const createMultiRoundStreamTrace = (): QAThinkingTrace => ({
|
||||
export const createComparePane = (mode: QAMode): ComparePane => ({
|
||||
mode,
|
||||
loading: true,
|
||||
singleRagTrace: mode === 'single-round'
|
||||
singleRagTrace: (mode === 'single-round' || mode === 'single-chat')
|
||||
? {
|
||||
title: '单轮 RAG 思考过程',
|
||||
thinkingMarkdown: '',
|
||||
@@ -204,7 +209,7 @@ export const createComparePane = (mode: QAMode): ComparePane => ({
|
||||
thinkingComplete: false,
|
||||
}
|
||||
: undefined,
|
||||
thinkingTrace: mode === 'single-round' ? undefined : createMultiRoundStreamTrace(),
|
||||
thinkingTrace: (mode === 'single-round' || mode === 'single-chat') ? undefined : createMultiRoundStreamTrace(),
|
||||
sources: [],
|
||||
});
|
||||
|
||||
@@ -215,88 +220,6 @@ export const createComparePane = (mode: QAMode): ComparePane => ({
|
||||
* 拆分时完整保留原始 mock 数据,确保首屏体验不变。
|
||||
*/
|
||||
export const createMockSingleRagMessages = (): Message[] => ([
|
||||
{
|
||||
id: 'mock-user-code-preview',
|
||||
role: 'user',
|
||||
content: '帮我看一下 Markdown 代码块的展示效果',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'mock-assistant-code-preview',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
singleRagTrace: {
|
||||
title: '代码块渲染预览',
|
||||
thinkingMarkdown: '这条模拟数据用于检查 MarkdownRenderer 的代码块样式:多行代码应该展示为带语言标签和复制按钮的彩色代码块,短单行代码例如 `const ok = true` 应该保持行内展示。',
|
||||
answerMarkdown: [
|
||||
'下面是几段模拟代码,用来预览不同语言的渲染效果。',
|
||||
'',
|
||||
'短单行代码:`const count = 1`,这一段应该显示为行内代码,而不是大代码块。',
|
||||
'',
|
||||
'### TSX 示例',
|
||||
'',
|
||||
'```tsx',
|
||||
'type UserCardProps = {',
|
||||
' name: string;',
|
||||
' role: string;',
|
||||
'};',
|
||||
'',
|
||||
'export function UserCard({ name, role }: UserCardProps) {',
|
||||
' return (',
|
||||
' <section className="rounded-lg border p-4">',
|
||||
' <h3>{name}</h3>',
|
||||
' <p>{role}</p>',
|
||||
' </section>',
|
||||
' );',
|
||||
'}',
|
||||
'```',
|
||||
'',
|
||||
'### Vue 示例',
|
||||
'',
|
||||
'```vue',
|
||||
'<script setup lang="ts">',
|
||||
'import { ref } from "vue";',
|
||||
'',
|
||||
'const keyword = ref("strategy");',
|
||||
'</script>',
|
||||
'',
|
||||
'<template>',
|
||||
' <button @click="keyword = keyword.toUpperCase()">',
|
||||
' {{ keyword }}',
|
||||
' </button>',
|
||||
'</template>',
|
||||
'```',
|
||||
'',
|
||||
'### Java 示例',
|
||||
'',
|
||||
'```java',
|
||||
'public class StrategyScore {',
|
||||
' public static int normalize(int value) {',
|
||||
' return Math.max(0, Math.min(100, value));',
|
||||
' }',
|
||||
'',
|
||||
' public static void main(String[] args) {',
|
||||
' System.out.println(normalize(128));',
|
||||
' }',
|
||||
'}',
|
||||
'```',
|
||||
'',
|
||||
'引用按钮也顺便保留测试:[[1]]',
|
||||
].join('\n'),
|
||||
thinkingComplete: true,
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
title: 'MarkdownRenderer mock preview',
|
||||
type: '本地模拟数据',
|
||||
snippet: '用于预览 tsx、vue、java 代码块高亮,以及短单行代码的行内展示。',
|
||||
source: 'GeneralQA.mock.ts',
|
||||
time: '2026-04-30',
|
||||
content: '这是一条本地模拟引用数据,用于测试 [[1]] 引用按钮点击后的详情展示。',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-user-1',
|
||||
role: 'user',
|
||||
@@ -382,111 +305,6 @@ export const createMockSingleRagMessages = (): Message[] => ([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-user-multi-1',
|
||||
role: 'user',
|
||||
content: '我想了解多轮检索的工作机制,以及它在大规模企业知识库中的实际表现如何?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'mock-assistant-multi-1',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
thinkingTrace: createThinkingTrace('multi-round'),
|
||||
sources: [
|
||||
{
|
||||
title: '多轮RAG检索白皮书',
|
||||
type: '知识库',
|
||||
snippet: '多轮检索通过逐轮改写查询、逐步聚焦,能在复杂问题上比单轮检索提升30%以上的召回覆盖。',
|
||||
source: '内部架构组',
|
||||
time: '2026-04-15',
|
||||
content: '多轮检索的核心在于每轮检索后根据已召回内容改写查询,逐步缩小语义范围,最终实现精准覆盖。',
|
||||
},
|
||||
{
|
||||
title: '企业知识库检索时延对比',
|
||||
type: '技术报告',
|
||||
snippet: '两轮检索在10万级文档库中平均增加40%延迟,但答案完整度提升显著。',
|
||||
source: 'NLP团队',
|
||||
time: '2026-03-22',
|
||||
content: '实测数据:单轮检索平均时延1.2s,两轮检索平均时延1.7s,三轮检索平均时延2.3s。建议根据SLA选择合适轮次。',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'mock-user-multi-2',
|
||||
role: 'user',
|
||||
content: '企业知识问答系统的多轮检索需要做哪些工程化优化?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
{
|
||||
id: 'mock-assistant-multi-2',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
thinkingTrace: {
|
||||
title: '多轮检索工程化优化思考过程',
|
||||
rounds: [
|
||||
{
|
||||
id: 'round-opt-1',
|
||||
title: '分析工程化优化方向',
|
||||
thoughtMarkdown: '用户关心的是工程层面的优化,需要从以下几个维度回答:\n\n1. 查询改写策略的缓存与复用\n2. 检索结果去重与增量更新\n3. 并发检索与超时控制\n4. 检索质量的在线评估机制',
|
||||
query: '多轮检索 工程化优化 查询改写 缓存 去重 并发',
|
||||
retrievals: [
|
||||
{
|
||||
id: 'ro1-1',
|
||||
title: '查询改写缓存策略实践',
|
||||
content: '对于相似查询,可通过向量相似度匹配复用上一轮的改写结果,减少调用大模型的次数和延迟。',
|
||||
source: '知识库 / 查询优化实践指南.md',
|
||||
},
|
||||
{
|
||||
id: 'ro1-2',
|
||||
title: '检索结果去重方案',
|
||||
content: '多轮检索容易出现重复文档,需基于文档ID或内容哈希进行去重,并在每轮检索时排除已召回文档。',
|
||||
source: '知识库 / 检索去重技术方案.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'round-opt-2',
|
||||
title: '检索性能与质量保障',
|
||||
thoughtMarkdown: '第二轮聚焦性能优化:\n\n1. 并发检索多个知识库以降低延迟\n2. 设置合理的检索超时防止长尾请求\n3. 引入在线相关性评分作为质量护栏',
|
||||
query: '并发检索 超时控制 在线评估 知识库评分',
|
||||
retrievals: [
|
||||
{
|
||||
id: 'ro2-1',
|
||||
title: '知识库并发检索架构设计',
|
||||
content: '采用异步并发查询多个索引,配合全局超时(建议3-5s),保证用户体验的同时最大化召回。',
|
||||
source: '架构库 / 并发检索设计文档.docx',
|
||||
},
|
||||
{
|
||||
id: 'ro2-2',
|
||||
title: '检索质量在线评估方法',
|
||||
content: '通过小模型对每轮召回结果做相关性打分,低于阈值时自动触发查询改写或切换检索策略。',
|
||||
source: '评测库 / 在线质量评估方案.md',
|
||||
},
|
||||
{
|
||||
id: 'ro2-3',
|
||||
title: '多轮检索时延优化总结',
|
||||
content: '工程实践中建议两轮为上限,超过时需评估能否合并为单轮。首批结果可先展示,第二轮做增量补充。',
|
||||
source: '性能库 / 时延优化白皮书.pdf',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
finalAnswerMarkdown: '多轮检索的工程化优化主要围绕四个方向:\n\n### 1. 查询改写优化\n- 缓存相似查询的改写结果,通过向量相似度匹配复用\n- 降低大模型调用频次,减少时延和成本\n\n### 2. 结果去重与增量更新\n- 基于文档ID或内容哈希去重\n- 每轮检索时排除已召回文档,避免重复返回\n- 增量模式:先返回首批结果,后续轮次做补充\n\n### 3. 并发与超时控制\n- 异步并发查询多个知识库索引\n- 设置全局超时(建议3-5秒),避免长尾请求\n- 首批结果到达后立即展示,后续增量追加\n\n### 4. 在线质量评估\n- 用小模型对召回结果做相关性打分\n- 低于阈值时自动触发查询改写或切换策略\n- 持续监控命中率和引用准确率\n\n> 建议:实际工程中两轮检索已能覆盖大多数复杂场景,超过两轮应重新评估是否可以通过更好的查询改写来合并。',
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
title: '多轮检索工程化最佳实践',
|
||||
type: '技术方案',
|
||||
snippet: '两轮检索配合增量更新,可在延迟和完整度之间取得最优平衡。',
|
||||
source: '工程团队',
|
||||
time: '2026-05-01',
|
||||
content: '总结了过去一年的多轮检索优化经验,涵盖查询改写缓存、结果去重、并发检索和在线评估四大方向。',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -506,9 +324,10 @@ export const mapDocsToSources = (docs: KnowledgeBaseChunk['docs']): SourceItem[]
|
||||
const normalizedDocs = Array.isArray(docs) ? docs : docs ? [docs] : [];
|
||||
|
||||
return normalizedDocs.map((item, idx) => {
|
||||
console.log(item,'检索资料')
|
||||
if (typeof item === 'string') {
|
||||
return {
|
||||
title: `参考资料 ${idx + 1}`,
|
||||
title: '',
|
||||
type: '知识库',
|
||||
snippet: item,
|
||||
source: '',
|
||||
@@ -529,6 +348,34 @@ export const mapDocsToSources = (docs: KnowledgeBaseChunk['docs']): SourceItem[]
|
||||
});
|
||||
};
|
||||
|
||||
export const chatDataJSON = (chatJSON: string) => {
|
||||
let chatJSONText = chatJSON.replace(/}{"/g, '},{"');
|
||||
chatJSONText = '[' + chatJSONText + ']';
|
||||
// console.log('解析chat',chatJSONText);
|
||||
let chatData = [];
|
||||
let chatTitle: any = '';
|
||||
const docsList: any[] = [];
|
||||
try {
|
||||
chatData = JSON.parse(chatJSONText);
|
||||
chatData.forEach((item: any) => {
|
||||
if (item['answer']) {
|
||||
chatTitle += item['answer'];
|
||||
} else if (item['docs']) {
|
||||
docsList.push(item);
|
||||
}
|
||||
});
|
||||
return {
|
||||
chatTitle,
|
||||
docsList,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
chatTitle,
|
||||
docsList,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从流式文本中提取完整 JSON 对象。
|
||||
*
|
||||
@@ -582,3 +429,22 @@ export const extractJsonObjects = (raw: string): { objects: string[]; rest: stri
|
||||
|
||||
return { objects, rest: '' };
|
||||
};
|
||||
|
||||
// 处理平台数据
|
||||
export const platformTypeChange = (platform: string) => {
|
||||
let platformType: string = '';
|
||||
if (platform.includes('fb')) {
|
||||
platformType = 'facebook';
|
||||
} else if (platform.includes('x')) {
|
||||
platformType = 'x';
|
||||
} else if (platform.includes('in')) {
|
||||
platformType = 'instagram';
|
||||
} else if (platform.includes('re')) {
|
||||
platformType = 'reddit';
|
||||
} else if (platform.includes('th')) {
|
||||
platformType = 'threads';
|
||||
} else {
|
||||
platformType = 'facebook';
|
||||
}
|
||||
return platformType;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+27
-1
@@ -1,6 +1,7 @@
|
||||
import type { QASingleRagTrace, QAThinkingTrace } from './QACommon';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
|
||||
export type QAMode = 'single-round' | 'multi-round' | 'deep-search';
|
||||
export type QAMode = 'single-chat' | 'single-round' | 'multi-round' | 'deep-search' | 'hx-search' | 'news-report';
|
||||
export type ViewMode = 'single' | 'compare';
|
||||
export type KnowledgeScope = 'all' | 'specific-kb' | 'specific-domain';
|
||||
export type AnswerStyle = 'concise' | 'detailed';
|
||||
@@ -13,6 +14,8 @@ export interface SourceItem {
|
||||
source?: string;
|
||||
time?: string;
|
||||
url?: string;
|
||||
keywords?: string;
|
||||
taskDirection?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
@@ -30,7 +33,14 @@ export interface ToolCallData {
|
||||
tool_name: string;
|
||||
tool_input: {
|
||||
text?: string;
|
||||
query?: string;
|
||||
q?: string;
|
||||
keyword?: string;
|
||||
platform?: string;
|
||||
user_fig?: string;
|
||||
result?: string;
|
||||
s?: string;
|
||||
e?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +66,8 @@ export interface ComparePane {
|
||||
}
|
||||
|
||||
export interface CompareResult {
|
||||
id: string | number;
|
||||
compareUserInput: string;
|
||||
left: ComparePane;
|
||||
right: ComparePane;
|
||||
differences?: {
|
||||
@@ -72,3 +84,17 @@ export interface SearchProcessState {
|
||||
hits: number;
|
||||
queryRewrite: string;
|
||||
}
|
||||
|
||||
export interface stateItem {
|
||||
compareIndex: number;
|
||||
compareId: string;
|
||||
messageList: Message[],
|
||||
compareResult: CompareResult[];
|
||||
}
|
||||
|
||||
export interface qaModeItem {
|
||||
value: QAMode;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
desc: string;
|
||||
}
|
||||
+33
-20
@@ -1,5 +1,6 @@
|
||||
import { BookOpen, Brain, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
|
||||
import type { AnswerStyle, CompareLayout, KnowledgeScope, QAMode, ViewMode } from './GeneralQA.types';
|
||||
import React from 'react';
|
||||
import { BookOpen, Bot, Brain, ChevronLeft, ChevronRight, Sparkles, User2Icon } from 'lucide-react';
|
||||
import type { AnswerStyle, CompareLayout, KnowledgeScope, QAMode, qaModeItem, ViewMode } from './GeneralQA.types';
|
||||
|
||||
/**
|
||||
* 左侧配置栏 Props。
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
autoSummarizeDiff: boolean; // 对比模式下是否自动总结左右差异
|
||||
recommendBetter: boolean; // 对比模式下是否推荐更好的回答方案
|
||||
isCollapsed: boolean; // 左侧配置面板是否折叠
|
||||
qaModeOptions?: qaModeItem[];
|
||||
setQaMode: (mode: QAMode) => void; // 设置单一模式的问答模式
|
||||
setCompareLeftMode: (mode: QAMode) => void; // 设置对比模式左侧的模式
|
||||
setCompareRightMode: (mode: QAMode) => void; // 设置对比模式右侧的模式
|
||||
@@ -39,10 +41,13 @@ type Props = {
|
||||
*
|
||||
* 每个选项同时提供展示文案、图标和简短说明,便于配置栏用统一结构渲染卡片。
|
||||
*/
|
||||
const qaModeOptions = [
|
||||
const qaModeList: qaModeItem[] = [
|
||||
{ value: 'single-chat' as const, label: '快速', icon: Bot, desc: '快速直接' },
|
||||
{ value: 'single-round' as const, label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
|
||||
{ value: 'multi-round' as const, label: '多轮RAG', icon: Brain, desc: '深入理解' },
|
||||
{ value: 'deep-search' as const, label: '深度检索', icon: BookOpen, desc: '全面分析' },
|
||||
{ value: 'deep-search' as const, label: '任务检索', icon: BookOpen, desc: '检索相关任务' },
|
||||
{ value: 'hx-search' as const, label: '画像检索', icon: User2Icon, desc: '检索相关账号' },
|
||||
{ value: 'news-report' as const, label: '新闻报告生成', icon: User2Icon, desc: '检索相关新闻' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -77,6 +82,7 @@ export default function GeneralQAConfigSidebar({
|
||||
autoSummarizeDiff,
|
||||
recommendBetter,
|
||||
isCollapsed,
|
||||
qaModeOptions = qaModeList,
|
||||
setQaMode,
|
||||
setCompareLeftMode,
|
||||
setCompareRightMode,
|
||||
@@ -88,7 +94,7 @@ export default function GeneralQAConfigSidebar({
|
||||
setAutoSummarizeDiff,
|
||||
setRecommendBetter,
|
||||
setIsCollapsed,
|
||||
}: Props): JSX.Element {
|
||||
}: Props): React.JSX.Element {
|
||||
/**
|
||||
* 自定义勾选图标。
|
||||
*
|
||||
@@ -130,7 +136,9 @@ export default function GeneralQAConfigSidebar({
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>问答模式</h3>
|
||||
<div className="space-y-2">
|
||||
{qaModeOptions.map((mode) => (
|
||||
{qaModeOptions.map((mode) => {
|
||||
const Icon = mode.icon;
|
||||
return (
|
||||
<button
|
||||
key={mode.value}
|
||||
onClick={() => setQaMode(mode.value)}
|
||||
@@ -144,13 +152,14 @@ export default function GeneralQAConfigSidebar({
|
||||
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<mode.icon className="w-5 h-5" />
|
||||
<Icon className="w-5 h-5" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{mode.label}</div>
|
||||
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{mode.desc}</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -160,18 +169,22 @@ export default function GeneralQAConfigSidebar({
|
||||
<div className="mb-4">
|
||||
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>模式A</label>
|
||||
<select value={compareLeftMode} onChange={(e) => setCompareLeftMode(e.target.value as QAMode)} className={`w-full px-3 py-2 rounded-lg text-sm ${isDark ? 'bg-slate-700/50 border border-slate-600 text-white' : 'bg-white border border-gray-200 text-gray-700'}`}>
|
||||
<option value="single-round">单轮RAG</option>
|
||||
<option value="multi-round">多轮RAG</option>
|
||||
<option value="deep-search">深度检索</option>
|
||||
{
|
||||
qaModeOptions.filter(item => item.value !== compareRightMode).map(item => (
|
||||
<option value={item.value} key={item.value}>{item.label}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div className={`text-center my-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}><span className="text-lg">VS</span></div>
|
||||
<div className="mb-4">
|
||||
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>模式B</label>
|
||||
<select value={compareRightMode} onChange={(e) => setCompareRightMode(e.target.value as QAMode)} className={`w-full px-3 py-2 rounded-lg text-sm ${isDark ? 'bg-slate-700/50 border border-slate-600 text-white' : 'bg-white border border-gray-200 text-gray-700'}`}>
|
||||
<option value="single-round">单轮RAG</option>
|
||||
<option value="multi-round">多轮RAG</option>
|
||||
<option value="deep-search">深度检索</option>
|
||||
{
|
||||
qaModeOptions.filter(item => item.value !== compareLeftMode).map(item => (
|
||||
<option value={item.value} key={item.value}>{item.label}</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +202,7 @@ export default function GeneralQAConfigSidebar({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
{/* <div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>知识范围</h3>
|
||||
<div className="space-y-2">
|
||||
{knowledgeScopeOptions.map((option) => (
|
||||
@@ -202,9 +215,9 @@ export default function GeneralQAConfigSidebar({
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="mb-6">
|
||||
{/* <div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>回答偏好</h3>
|
||||
<div className="space-y-3">
|
||||
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'}`}>
|
||||
@@ -223,9 +236,9 @@ export default function GeneralQAConfigSidebar({
|
||||
<span className="text-sm">展示检索过程</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
{viewMode === 'compare' && (
|
||||
{/* {viewMode === 'compare' && (
|
||||
<div className="mb-6">
|
||||
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>对比设置</h3>
|
||||
<div className="space-y-3">
|
||||
@@ -241,7 +254,7 @@ export default function GeneralQAConfigSidebar({
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { History } from 'lucide-react';
|
||||
import type { ViewMode } from './GeneralQA.types';
|
||||
|
||||
/**
|
||||
* 顶部栏 Props。
|
||||
*
|
||||
* `setViewMode` 由父组件传入,父组件会在生成中切换时做确认拦截;
|
||||
* Header 本身不直接感知 loading,保持纯展示和事件转发。
|
||||
*/
|
||||
type Props = {
|
||||
isDark: boolean; // 当前是否为暗黑主题,用于样式适配
|
||||
viewMode: ViewMode; // 当前视图模式:'single' 单一模式 或 'compare' 对比模式
|
||||
chatHeadTitle?: string;
|
||||
chatDesign?: string;
|
||||
compareShow?: boolean;
|
||||
setViewMode: (mode: ViewMode) => void; // 设置视图模式回调函数
|
||||
onNavigate?: (page: string) => void; // 页面导航回调函数
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用问答页面顶部栏。
|
||||
*
|
||||
* 该组件只负责页面标题、模式切换和历史记录入口,所有业务状态仍由父组件维护。
|
||||
* 这样拆分后,顶部导航的样式和交互不会和中间问答流处理逻辑混在一起,后续如果要新增
|
||||
* "保存模板""导出记录"等入口,也可以只改这个文件。
|
||||
*/
|
||||
export default function GeneralQAHeader({ isDark, viewMode, chatHeadTitle = '通用问答', chatDesign = '支持单轮RAG、多轮RAG、深度检索及结果对比', compareShow = true, setViewMode, onNavigate }: Props): React.JSX.Element {
|
||||
return (
|
||||
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{chatHeadTitle}
|
||||
</h1>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{chatDesign}
|
||||
</span>
|
||||
</div>
|
||||
{
|
||||
compareShow && <div className="flex items-center gap-2">
|
||||
<div className={`flex rounded-lg p-1 ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
|
||||
<button
|
||||
onClick={() => setViewMode('single')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${viewMode === 'single'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
单一模式
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('compare')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${viewMode === 'compare'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
对比模式
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{onNavigate && (
|
||||
<button
|
||||
onClick={() => onNavigate('qa-records')}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
历史记录
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,933 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Dialog, Textarea } from 'tdesign-react';
|
||||
import {
|
||||
Bookmark,
|
||||
BrainCircuit,
|
||||
Bot,
|
||||
ChevronDown,
|
||||
FileSearch,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Send,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Square,
|
||||
Copy,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Upload,
|
||||
UserRound,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import MarkdownRenderer from '@/strategy-components/MarkdownRenderer';
|
||||
import { SourceItem } from './GeneralQA.types';
|
||||
import '../qa/qa-animations.css';
|
||||
|
||||
/**
|
||||
* 单条检索结果。
|
||||
*
|
||||
* 多轮检索过程中每一轮可能返回多条资料,这里只保留 UI 展示所需的标题、摘要和来源。
|
||||
*/
|
||||
export interface QARetrievalItem {
|
||||
id?: string;
|
||||
title: string;
|
||||
content: string;
|
||||
keywords?: string;
|
||||
taskDirection?: string;
|
||||
type?: string;
|
||||
time?: string;
|
||||
source?: string;
|
||||
recUuid?: string;
|
||||
platform?: string;
|
||||
fans?: number;
|
||||
comment_count?: number;
|
||||
message_id?: string;
|
||||
hot_num?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多轮检索中的单轮思考节点。
|
||||
*
|
||||
* `queryLoading` 控制该轮检索结果是否仍显示 loading;retrievals 到达后再渲染资料列表。
|
||||
*/
|
||||
export interface QAThinkingRound {
|
||||
id: string; // 唯一标识符,用于列表渲染时的 key
|
||||
title: string; // 该轮检索的标题,如"第1轮检索"或"背景资料收集"
|
||||
thoughtMarkdown: string; // 模型思考过程的 Markdown 内容,展示思考步骤和推理逻辑
|
||||
queryTitle?: string; // 检索内容
|
||||
query: string; // 该轮发送给检索系统的查询语句
|
||||
queryLoading?: boolean; // 该轮检索是否仍在进行中,true 时显示加载动画
|
||||
textidContent?: string; // 新闻详细内容
|
||||
retrievals: QARetrievalItem[]; // 该轮返回的检索结果列表
|
||||
queryShow: boolean; // 检索列表为空时是否显示
|
||||
}
|
||||
|
||||
/**
|
||||
* 多轮/深度检索的完整思考链。
|
||||
*
|
||||
* rounds 展示检索过程,finalAnswerMarkdown 展示最终回答;两者分开便于流式过程中逐步更新。
|
||||
*/
|
||||
export interface QAThinkingTrace {
|
||||
title?: string; // 整体思考链的标题
|
||||
rounds: QAThinkingRound[]; // 多轮检索过程中的所有轮次,每轮包含思考和检索结果
|
||||
finalAnswerMarkdown: string; // 基于多轮检索结果生成的最终回答内容
|
||||
thinkingComplete?: boolean; // 思考过程是否已完成,true 时思考区域可折叠
|
||||
}
|
||||
|
||||
/**
|
||||
* 单轮 RAG 的回答结构。
|
||||
*
|
||||
* 后端可能把 `<think>...</think>` 和答案混在同一个 answer 流中,解析后分别放到 thinkingMarkdown 与 answerMarkdown。
|
||||
*/
|
||||
export interface QASingleRagTrace {
|
||||
title?: string; // 思考过程的标题,如"单轮 RAG 思考过程"
|
||||
thinkingMarkdown: string; // 模型在生成回答前的思考过程内容,使用 Markdown 格式
|
||||
answerMarkdown: string; // 最终生成的回答内容,使用 Markdown 格式
|
||||
thinkingComplete?: boolean; // 思考过程是否已完成,true 时思考区域可折叠
|
||||
}
|
||||
|
||||
export interface QACommonMessage {
|
||||
id: string; // 消息唯一标识符
|
||||
role: 'user' | 'assistant'; // 消息发送者角色:user 为用户消息,assistant 为 AI 回复
|
||||
content: string; // 消息的纯文本内容(当使用 thinkingTrace 或 singleRagTrace 时为空)
|
||||
timestamp?: Date; // 消息发送时间
|
||||
thinkingTrace?: QAThinkingTrace; // 多轮/深度检索模式的思考链,包含多轮检索过程和最终回答
|
||||
sources?: { // 消息引用的参考资料列表
|
||||
title: string; // 参考资料标题
|
||||
type: string; // 资料类型,如"文档"、"网页"等
|
||||
snippet: string; // 资料摘要内容
|
||||
source?: string; // 来源名称或 URL
|
||||
time?: string; // 发布时间
|
||||
url?: string; // 资料链接
|
||||
content?: string; // 完整内容(可能用于详情弹窗)
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface QACommonMessage {
|
||||
singleRagTrace?: QASingleRagTrace; // 单轮 RAG 模式的思考过程和回答结构
|
||||
}
|
||||
|
||||
interface QAAvatarProps {
|
||||
role: 'user' | 'assistant';
|
||||
isActive?: boolean;
|
||||
label?: string;
|
||||
variant?: 'default' | 'blue' | 'purple';
|
||||
}
|
||||
|
||||
const getAssistantAvatarClass = (variant: QAAvatarProps['variant'], isDark: boolean) => {
|
||||
switch (variant) {
|
||||
case 'blue':
|
||||
return isDark
|
||||
? 'bg-gradient-to-br from-blue-400 via-blue-600 to-cyan-700 text-white'
|
||||
: 'bg-gradient-to-br from-blue-300 via-blue-500 to-cyan-500 text-white';
|
||||
case 'purple':
|
||||
return isDark
|
||||
? 'bg-gradient-to-br from-purple-400 via-violet-600 to-fuchsia-700 text-white'
|
||||
: 'bg-gradient-to-br from-purple-300 via-violet-500 to-fuchsia-500 text-white';
|
||||
default:
|
||||
return isDark
|
||||
? 'bg-gradient-to-br from-cyan-500 via-blue-600 to-violet-600 text-white'
|
||||
: 'bg-gradient-to-br from-red-400 via-rose-500 to-orange-400 text-white';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 统一头像组件。
|
||||
*
|
||||
* 单一模式使用默认助手色;对比模式可通过 variant 指定蓝/紫色系,让头像与左右面板标签保持一致。
|
||||
* `isActive` 会触发呼吸和星光动画,用来提示当前助手正在生成。
|
||||
*/
|
||||
export const QAAvatar: React.FC<QAAvatarProps> = ({ role, isActive = false, label, variant = 'default' }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const Icon = role === 'assistant' ? Bot : UserRound;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative w-9 h-9 rounded-2xl flex items-center justify-center flex-shrink-0 overflow-hidden shadow-sm ${role === 'assistant'
|
||||
? getAssistantAvatarClass(variant, isDark)
|
||||
: isDark
|
||||
? 'bg-gradient-to-br from-slate-500 via-slate-700 to-slate-900 text-white'
|
||||
: 'bg-gradient-to-br from-sky-400 via-indigo-500 to-violet-500 text-white'
|
||||
} ${isActive ? 'qa-avatar-active' : ''}`}
|
||||
title={label}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 opacity-0 qa-avatar-shine" />
|
||||
<Icon className={`w-5 h-5 relative z-10 ${isActive ? 'qa-avatar-icon-active' : ''}`} />
|
||||
{role === 'assistant' && isActive && (
|
||||
<Sparkles className="absolute -right-0.5 -top-0.5 w-3.5 h-3.5 text-yellow-200 qa-avatar-spark" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TypingIndicatorProps {
|
||||
label?: string;
|
||||
assistantLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手预连接/生成中的占位提示。
|
||||
*
|
||||
* 用于请求已经发出但还没有生成可展示消息时,给用户明确的等待反馈。
|
||||
*/
|
||||
export const TypingIndicator: React.FC<TypingIndicatorProps> = ({ label = '正在思考并生成回复', assistantLabel = 'AI 助手' }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className="flex gap-3">
|
||||
<QAAvatar role="assistant" isActive label={assistantLabel} />
|
||||
<div className={`max-w-[70%] rounded-2xl px-4 py-3 shadow-sm border ${isDark ? 'bg-slate-700/60 border-slate-600/50 text-slate-200' : 'bg-white border-red-100 text-gray-700'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className={`w-4 h-4 animate-spin ${isDark ? 'text-blue-300' : 'text-red-500'}`} />
|
||||
<span
|
||||
className={`text-sm font-medium qa-generating-marquee ${isDark ? 'qa-generating-marquee-dark' : 'qa-generating-marquee-light'}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{/* <div className="flex gap-1">
|
||||
{[0, 1, 2].map((idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={`w-1.5 h-1.5 rounded-full qa-typing-dot ${isDark ? 'bg-blue-300' : 'bg-red-400'}`}
|
||||
style={{
|
||||
animationDelay: `${idx * 160}ms`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatMessageProps<T extends QACommonMessage> {
|
||||
message: T;
|
||||
showSources?: boolean;
|
||||
assistantLabel?: string;
|
||||
assistantActive?: boolean;
|
||||
onCitationClick?: (message: T, citationIndex: number) => void;
|
||||
onCopyAssistant?: (message: T) => void;
|
||||
onDeleteAssistant?: (message: T) => void;
|
||||
onEditAssistant?: (message: T) => void;
|
||||
onResendAssistant?: (message: T) => void;
|
||||
renderAssistantExtra?: (message: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天消息渲染器。
|
||||
*
|
||||
* 根据消息类型自动选择普通文本、单轮 RAG 回答或多轮检索思考链。
|
||||
* sources 区域独立折叠展示,避免引用资料过长时挤占主回答阅读空间。
|
||||
*/
|
||||
export function ChatMessage<T extends QACommonMessage>({
|
||||
message,
|
||||
showSources = true,
|
||||
assistantLabel = 'AI 助手',
|
||||
assistantActive = false,
|
||||
onCitationClick,
|
||||
onCopyAssistant,
|
||||
onDeleteAssistant,
|
||||
onEditAssistant,
|
||||
onResendAssistant,
|
||||
renderAssistantExtra,
|
||||
}: ChatMessageProps<T>) {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const isUser = message.role === 'user';
|
||||
const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace);
|
||||
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
|
||||
const [visible, setVisible] = useState(false); // 打开详情对话框
|
||||
const [listData, setListData] = useState<SourceItem>({
|
||||
title: '',
|
||||
type: '',
|
||||
time: '',
|
||||
snippet: '',
|
||||
source: '',
|
||||
content: '',
|
||||
});
|
||||
const showDialog = (item: SourceItem) => {
|
||||
console.log('item', item)
|
||||
if (item.url) {
|
||||
window.open(`${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${item.url}`);
|
||||
} else {
|
||||
setListData(item);
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
|
||||
{!isUser && <QAAvatar role="assistant" isActive={assistantActive} label={assistantLabel} />}
|
||||
<div className={`${hasTrace ? 'max-w-[85%] flex-1' : 'max-w-[72%]'} rounded-2xl px-4 py-3 shadow-sm ${isUser
|
||||
? isDark
|
||||
? 'bg-blue-600/70 text-white'
|
||||
: 'bg-gradient-to-r from-red-500 to-rose-500 text-white'
|
||||
: message.singleRagTrace || message.thinkingTrace
|
||||
? isDark
|
||||
? 'bg-slate-800/40 text-slate-100 border border-slate-600/40'
|
||||
: 'bg-white text-gray-800 border border-transparent shadow-[0px_3px_12px_rgba(0, 0, 0, 0.12)] px-0 py-0'
|
||||
: isDark
|
||||
? 'bg-slate-700/60 text-slate-100 border border-slate-600/40'
|
||||
: 'bg-white text-gray-800 border border-gray-100'
|
||||
}`}>
|
||||
{message.singleRagTrace && !isUser ? (
|
||||
<SingleRagAnswer
|
||||
trace={message.singleRagTrace}
|
||||
onCitationClick={(citationIndex) => onCitationClick?.(message, citationIndex)}
|
||||
/>
|
||||
) : message.thinkingTrace && !isUser ? (
|
||||
<RetrievalThinkingTrace
|
||||
trace={message.thinkingTrace}
|
||||
onCitationClick={(citationIndex) => onCitationClick?.(message, citationIndex)}
|
||||
/>
|
||||
) : (
|
||||
<p className="whitespace-pre-wrap leading-relaxed">{message.content}</p>
|
||||
)}
|
||||
{!isUser && (onCopyAssistant || onDeleteAssistant || onEditAssistant || onResendAssistant) && (
|
||||
<div className={`mt-2 flex items-center justify-end gap-1.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{onCopyAssistant && (
|
||||
<button type="button" onClick={() => onCopyAssistant(message)} className={`p-1.5 rounded-md transition-colors ${isDark ? 'hover:bg-slate-700 hover:text-slate-100' : 'hover:bg-gray-100 hover:text-gray-700'}`} title="复制回答">
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{onDeleteAssistant && (
|
||||
<button type="button" onClick={() => onDeleteAssistant(message)} className={`p-1.5 rounded-md transition-colors ${isDark ? 'hover:bg-slate-700 hover:text-rose-300' : 'hover:bg-red-50 hover:text-red-600'}`} title="删除问答">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{onEditAssistant && (
|
||||
<button type="button" onClick={() => onEditAssistant(message)} className={`p-1.5 rounded-md transition-colors ${isDark ? 'hover:bg-slate-700 hover:text-blue-300' : 'hover:bg-blue-50 hover:text-blue-600'}`} title="编辑回答和思考过程">
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{onResendAssistant && (
|
||||
<button type="button" onClick={() => onResendAssistant(message)} className={`p-1.5 rounded-md transition-colors ${isDark ? 'hover:bg-slate-700 hover:text-emerald-300' : 'hover:bg-emerald-50 hover:text-emerald-600'}`} title="重新发送">
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isUser && <QAAvatar role="user" label="用户" />}
|
||||
</div>
|
||||
|
||||
|
||||
{!isUser && message.sources && message.sources.length > 0 && showSources && (
|
||||
<CollapsibleSection
|
||||
defaultOpen={false}
|
||||
className={`ml-12 ${hasTrace ? 'max-w-[85%]' : 'max-w-[72%]'} ${isDark ? 'bg-slate-800/50 border-slate-700/50' : 'bg-gray-50 border-gray-200'}`}
|
||||
title={(
|
||||
<div className="flex items-center gap-2">
|
||||
<BrainCircuit className={`w-3.5 h-3.5 ${isDark ? 'text-blue-300' : 'text-red-500'}`} />
|
||||
<span className={`text-xs font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>引用来源</span>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-500'}`}>({message.sources.length})</span>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div className="space-y-2 pt-[10px]">
|
||||
{message.sources.map((source, idx) => (
|
||||
<div key={idx} className={`text-xs p-2 rounded-[4px] border shadow-sm transition-all duration-200 ease-out cursor-pointer hover:-translate-y-0.5 hover:scale-[1.01] ${isDark ? 'bg-slate-700/50 border-slate-600/50 shadow-black/10 hover:bg-slate-700/80 hover:border-blue-400/40 hover:shadow-lg hover:shadow-blue-950/20' : 'bg-white border-gray-100 shadow-gray-200/60 hover:border-red-200 hover:shadow-md hover:shadow-red-100/70'}`} onClick={() => showDialog(source)}>
|
||||
<div className="flex items-center gap-2 mb-1 min-w-0">
|
||||
<BrainCircuit className={`w-3 h-3 shrink-0 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
|
||||
<span className={`font-medium truncate min-w-0 flex-1 ${isDark ? 'text-slate-300' : 'text-gray-700'}`} title={source.title}>{source.title}</span>
|
||||
<span className={`shrink-0 px-1.5 py-0.5 rounded text-xs ${isDark ? 'bg-slate-600 text-slate-300' : 'bg-gray-200 text-gray-500'}`}>
|
||||
{source.type}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`line-clamp-3 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{source.snippet}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
header="提示"
|
||||
footer={null}
|
||||
>
|
||||
<div>
|
||||
<div className="pr-9">
|
||||
<h3 className="text-base font-semibold">{listData.title || '引用内容'}</h3>
|
||||
<div className={`mt-2 text-xs space-y-1 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<div>来源:{listData.source || '未知来源'}</div>
|
||||
<div>时间:{listData.time || '未知'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{listData.url && (
|
||||
<div className="mt-4">
|
||||
<a
|
||||
href={`${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${listData.url}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm transition-all ${isDark
|
||||
? 'border-slate-600 text-slate-200 hover:bg-slate-800'
|
||||
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
title="打开原始链接"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
查看原文链接
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className={`mt-4 rounded-xl p-3 text-sm leading-relaxed whitespace-pre-wrap ${isDark ? 'bg-slate-800/70 text-slate-200' : 'bg-gray-50 text-gray-700'
|
||||
}`}>
|
||||
{listData.content || '暂无引用内容'}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{!isUser && renderAssistantExtra?.(message)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用折叠面板。
|
||||
*
|
||||
* 思考过程、检索结果、引用来源都复用该组件,保证展开/收起动画和深浅色主题一致。
|
||||
*/
|
||||
const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = true,
|
||||
className = '',
|
||||
headerClassName = '',
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
// 当 defaultOpen 改变时,更新 open 状态
|
||||
useEffect(() => {
|
||||
setOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
return (
|
||||
<div className={`rounded-2xl border overflow-hidden ${isDark ? 'border-slate-700/60 bg-slate-900/30' : 'border-red-100 bg-white'} ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
className={`w-full flex items-center justify-between gap-3 px-4 py-3 text-left transition-colors ${isDark ? 'hover:bg-slate-800/70' : 'hover:bg-red-50/60'
|
||||
} ${headerClassName}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">{title}</div>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 flex-shrink-0 transition-transform duration-300 ease-out ${open ? 'rotate-0' : '-rotate-90'
|
||||
} ${isDark ? 'text-slate-400' : 'text-gray-500'}`}
|
||||
/>
|
||||
</button>
|
||||
<div className={`grid transition-[grid-template-rows,opacity] duration-300 ease-out ${open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}`}>
|
||||
<div className="overflow-hidden">
|
||||
<div className={`px-4 pb-4 transition-transform duration-300 ease-out ${open ? 'translate-y-0' : '-translate-y-2'} ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SingleRagAnswerProps {
|
||||
trace: QASingleRagTrace;
|
||||
onCitationClick?: (citationIndex: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单轮 RAG 回答展示。
|
||||
*
|
||||
* 思考内容和最终回答分成两个区域:思考区可折叠,回答区始终展示。
|
||||
* 这样既保留模型推理过程,也不会影响用户优先阅读最终答案。
|
||||
*/
|
||||
export const SingleRagAnswer: React.FC<SingleRagAnswerProps> = ({ trace, onCitationClick }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-5xl space-y-4 overflow-hidden">
|
||||
<CollapsibleSection
|
||||
defaultOpen={!trace.thinkingComplete}
|
||||
className={isDark ? 'bg-slate-800/60 shadow-lg shadow-black/10' : 'bg-white shadow-sm'}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-9 h-9 rounded-2xl flex items-center justify-center ${isDark ? 'bg-violet-500/15 text-violet-300' : 'bg-orange-50 text-orange-500'
|
||||
}`}>
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-sm font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>{trace.title || '思考过程'}</div>
|
||||
{trace.thinkingComplete ? (
|
||||
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
思考已完成,可展开查看
|
||||
</div>
|
||||
) : (
|
||||
<div className={`text-xs qa-generating-marquee ${isDark ? 'qa-generating-marquee-dark text-slate-300' : 'qa-generating-marquee-light text-gray-600'}`}>
|
||||
正在接收 <think> 流式思考内容
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{trace.thinkingMarkdown ? (
|
||||
<MarkdownRenderer
|
||||
content={trace.thinkingMarkdown}
|
||||
className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}
|
||||
onCitationClick={onCitationClick}
|
||||
/>
|
||||
) : (
|
||||
<div className={`text-sm flex items-center gap-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
等待模型输出思考内容...
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
|
||||
<div className={`rounded-2xl border p-4 ${isDark ? 'bg-slate-700/60 border-slate-600/40' : 'bg-white border-gray-100'}`}>
|
||||
{trace.answerMarkdown ? (
|
||||
<MarkdownRenderer
|
||||
content={trace.answerMarkdown}
|
||||
className={`text-sm ${isDark ? 'text-slate-200' : 'text-gray-700'}`}
|
||||
onCitationClick={onCitationClick}
|
||||
/>
|
||||
) : (
|
||||
<div className={`text-sm flex items-center gap-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
{trace.thinkingComplete ? '正在流式生成回答...' : '思考结束后开始生成回答...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RetrievalListProps {
|
||||
items: QARetrievalItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索结果列表。
|
||||
*
|
||||
* 每条资料默认折叠,首条默认展开,便于用户快速看到最相关资料,同时保留完整资料明细。
|
||||
*/
|
||||
const RetrievalList: React.FC<RetrievalListProps> = ({ items }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const [visible, setVisible] = useState(false); // 打开详情对话框
|
||||
const [listData, setListData] = useState<QARetrievalItem>({
|
||||
title: '',
|
||||
content: '',
|
||||
});
|
||||
const knowledgeUrl = import.meta.env.VITE_AIPI_KNOWLEDGE_URL;
|
||||
const b1SearchUrl = import.meta.env.VITE_AIPI_B1COH_URL;
|
||||
const b2SearchUrl = import.meta.env.VITE_AIPI_B2ACTOR_URL;
|
||||
const showDialog = (item: QARetrievalItem) => {
|
||||
console.log('item', item)
|
||||
if (item.type === 'more') {
|
||||
if (item.recUuid) {
|
||||
window.open(`${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${item.recUuid}`);
|
||||
} else {
|
||||
setListData(item);
|
||||
setVisible(true);
|
||||
}
|
||||
} else if (item.type === 'task') {
|
||||
window.open(`${b1SearchUrl}/#/task/item-aicoh/step-info-search?id=${item.id}`)
|
||||
} else if (item.type === 'hx') {
|
||||
window.open(`${b2SearchUrl}/fromB/toDetail?user_id=${item.id}&platform=${item.platform}`);
|
||||
} else {
|
||||
setListData(item);
|
||||
setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{items.map((item, index) => (
|
||||
<div key={index}>
|
||||
{
|
||||
item.type !== 'user_behavior' ? <CollapsibleSection
|
||||
key={index}
|
||||
defaultOpen={index === 0}
|
||||
className={isDark ? 'bg-slate-800/40' : 'bg-gray-50/70'}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-6 h-6 rounded-lg flex items-center justify-center text-xs font-semibold ${isDark ? 'bg-blue-500/20 text-blue-300' : 'bg-red-100 text-red-600'
|
||||
}`}>{index + 1}</span>
|
||||
<div className="min-w-0">
|
||||
<div className={`text-sm font-medium truncate ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>{item.title}</div>
|
||||
{
|
||||
item.type === 'more' && <div className={`text-xs truncate ${isDark ? 'text-slate-500' : 'text-gray-500'}`}>时间:{item.time || '-'}</div>
|
||||
}
|
||||
{
|
||||
item.type === 'task' && <div className={`text-xs truncate ${isDark ? 'text-slate-500' : 'text-gray-500'}`}>关键词:{item.keywords || '-'}</div>
|
||||
}
|
||||
{
|
||||
item.type === 'hx' && <div className={`text-xs truncate ${isDark ? 'text-slate-500' : 'text-gray-500'}`}><span>平台:{item.platform || '-'}</span><span className="ml-[10px]">粉丝数:{item.fans || '-'}</span></div>
|
||||
}
|
||||
{
|
||||
(item.type === 'taiwan_news' || item.type === 'world_news' || item.type === 'messaage_comment' || item.type === 'america_news') && <div className={`text-xs truncate ${isDark ? 'text-slate-500' : 'text-gray-500'}`}><span>平台:{item.platform || '-'}</span>{item.comment_count && <span className="ml-[10px]">评论数:{item.comment_count || '-'}</span>}</div>
|
||||
}
|
||||
{
|
||||
item.type === 'get_time_data' && <div className={`text-xs truncate ${isDark ? 'text-slate-500' : 'text-gray-500'}`}><span>时间:{item.time || '-'}</span><span className="ml-[10px]">热度:{item.hot_num || '-'}</span></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p className={`text-sm leading-relaxed cursor-progress line-clamp-3 hover:underline ${isDark ? 'text-slate-300' : 'text-gray-600'}`} onClick={() => showDialog(item)}>{item.content}</p>
|
||||
</CollapsibleSection>
|
||||
: <div className="rounded-[5px] border border-[#f5f5f5] p-[10px]" key={index + 10}>
|
||||
<span className="font-bold mr-[10px]">{index + 1}.</span>
|
||||
<span>{item.title}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onClose={() => setVisible(false)}
|
||||
header="提示"
|
||||
footer={null}
|
||||
>
|
||||
<div>
|
||||
<div className="pr-9">
|
||||
<h3 className="text-base font-semibold">{listData.title}</h3>
|
||||
<div className={`mt-2 text-xs space-y-1 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{listData.time && <span>时间:{listData.time || '未知'}</span>}
|
||||
{listData.platform && <span className="ml-[10px]">平台:{listData.platform || '未知'}</span>}
|
||||
{listData.comment_count && <span className="ml-[10px]">评论数:{listData.comment_count || '未知'}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mt-4 rounded-xl p-3 text-sm leading-relaxed whitespace-pre-wrap ${isDark ? 'bg-slate-800/70 text-slate-200' : 'bg-gray-50 text-gray-700'
|
||||
}`}>
|
||||
{listData.content || '暂无引用内容'}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SearchQueryBadgeProps {
|
||||
query: string;
|
||||
loading?: boolean;
|
||||
queryTitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索词徽标。
|
||||
*
|
||||
* 多轮检索每一轮都会产生一个 query;loading 状态下展示旋转图标,表示该 query 正在召回资料。
|
||||
*/
|
||||
const SearchQueryBadge: React.FC<SearchQueryBadgeProps> = ({ query, loading = false, queryTitle = '检索词' }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className={`inline-flex max-w-full items-center gap-2 rounded-full px-3 py-1.5 border ${isDark ? 'bg-blue-500/10 border-blue-400/20 text-blue-200' : 'bg-red-50 border-red-100 text-red-600'
|
||||
}`}>
|
||||
{loading ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Search className="w-3.5 h-3.5" />}
|
||||
<span className="text-xs flex-shrink-0">{queryTitle}</span>
|
||||
<span className={`text-xs font-medium truncate ${loading && 'qa-generating-marquee'} ${loading && (isDark ? 'qa-generating-marquee-dark' : 'qa-generating-marquee-light')}`}>
|
||||
{query}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface RetrievalThinkingTraceProps {
|
||||
trace: QAThinkingTrace;
|
||||
onCitationClick?: (citationIndex: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多轮/深度检索思考链展示。
|
||||
*
|
||||
* 组件按轮次渲染思考内容、检索词和检索结果,底部展示最终回答。
|
||||
* 它只消费已经整理好的 trace 数据,不关心数据来自真实 SSE 还是本地模拟。
|
||||
*/
|
||||
export const RetrievalThinkingTrace: React.FC<RetrievalThinkingTraceProps> = ({ trace, onCitationClick }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className="w-full min-w-0 max-w-5xl space-y-4 overflow-hidden">
|
||||
<CollapsibleSection
|
||||
defaultOpen={!trace.thinkingComplete}
|
||||
className={isDark ? 'bg-slate-800/60 shadow-lg shadow-black/10' : 'bg-white shadow-sm'}
|
||||
title={
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-9 h-9 rounded-2xl flex items-center justify-center ${isDark ? 'bg-cyan-500/15 text-cyan-300' : 'bg-red-50 text-red-500'
|
||||
}`}>
|
||||
<BrainCircuit className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-sm font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
{trace.title || '思考过程'}
|
||||
</div>
|
||||
<div className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
共 {trace.rounds.length} 轮推理与检索,可展开查看细节
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3 pt-1">
|
||||
{trace.rounds.map((round, index) => (
|
||||
<div key={index} className={`relative pl-5 ${index < trace.rounds.length - 1 ? 'pb-2' : ''}`}>
|
||||
{index < trace.rounds.length - 1 && (
|
||||
<div className={`absolute left-[7px] top-8 bottom-0 w-px ${isDark ? 'bg-slate-700' : 'bg-red-100'}`} />
|
||||
)}
|
||||
<div className={`absolute left-0 top-4 w-3.5 h-3.5 rounded-full border-2 ${isDark ? 'bg-slate-900 border-cyan-400' : 'bg-white border-red-400'
|
||||
}`} />
|
||||
<CollapsibleSection
|
||||
defaultOpen
|
||||
className={isDark ? 'bg-slate-900/40' : 'bg-red-50/30'}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${isDark ? 'bg-cyan-500/15 text-cyan-300' : 'bg-red-100 text-red-600'
|
||||
}`}>第 {index + 1} 轮</span>
|
||||
<span className={`text-sm font-medium ${round.queryLoading && 'qa-generating-marquee'} ${round.queryLoading && (isDark ? 'text-slate-100 qa-generating-marquee-dark' : 'text-gray-900 qa-generating-marquee-light')}`}>{round.title}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<CollapsibleSection
|
||||
defaultOpen
|
||||
className={isDark ? 'bg-slate-800/50' : 'bg-white/80'}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className={`w-4 h-4 ${isDark ? 'text-violet-300' : 'text-orange-500'}`} />
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-slate-100' : 'text-gray-800'}`}>思考内容</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MarkdownRenderer
|
||||
content={round.thoughtMarkdown}
|
||||
className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}
|
||||
onCitationClick={onCitationClick}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
{
|
||||
round.queryShow &&
|
||||
<div className="flex items-center">
|
||||
<SearchQueryBadge query={round.query} queryTitle={round.queryTitle} loading={round.queryLoading} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
round.queryShow &&
|
||||
<CollapsibleSection
|
||||
defaultOpen={!round.queryLoading}
|
||||
className={isDark ? 'bg-slate-800/50' : 'bg-white/80'}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<FileSearch className={`w-4 h-4 ${isDark ? 'text-blue-300' : 'text-red-500'}`} />
|
||||
<span className={`text-sm font-medium ${isDark ? 'text-slate-100' : 'text-gray-800'}`}>检索内容</span>
|
||||
<span className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{round.retrievals.length} 条</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{round.queryLoading ? (
|
||||
<div className={`rounded-xl px-3 py-4 text-sm flex items-center gap-2 ${isDark ? 'bg-slate-900/60 text-slate-400' : 'bg-gray-50 text-gray-500'}`}>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
正在召回相关内容...
|
||||
</div>
|
||||
) : (
|
||||
<RetrievalList items={round.retrievals} />
|
||||
)}
|
||||
</CollapsibleSection>
|
||||
}
|
||||
{
|
||||
round.textidContent &&
|
||||
<div className="mt-[10px]">
|
||||
{round.textidContent}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<div className={`rounded-2xl border p-4 ${isDark ? 'bg-slate-700/60 border-slate-600/40' : 'bg-white border-gray-100'}`}>
|
||||
<div className={`text-sm font-semibold mb-3 ${isDark ? 'text-white' : 'text-gray-900'}`}>最终回答</div>
|
||||
{
|
||||
!trace.finalAnswerMarkdown && <div className={`rounded-xl px-3 py-4 text-sm flex items-center gap-2 ${isDark ? 'bg-slate-900/60 text-slate-400' : 'bg-gray-50 text-gray-500'}`}>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
正在生成思考过程与检索内容...
|
||||
</div>
|
||||
}
|
||||
<MarkdownRenderer
|
||||
content={trace.finalAnswerMarkdown}
|
||||
className={`text-sm ${isDark ? 'text-slate-200' : 'text-gray-700'}`}
|
||||
onCitationClick={onCitationClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmptyChatStateProps {
|
||||
icon?: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 空状态展示。
|
||||
*
|
||||
* 当当前模式没有任何消息时使用,提示用户可以开始输入问题。
|
||||
*/
|
||||
export const EmptyChatState: React.FC<EmptyChatStateProps> = ({ icon, title, description }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
return (
|
||||
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
|
||||
<div className={`w-20 h-20 rounded-3xl mb-5 flex items-center justify-center ${isDark ? 'bg-slate-700/40' : 'bg-gradient-to-br from-red-50 to-orange-50'
|
||||
}`}>
|
||||
{icon ?? <Bot className="w-11 h-11 opacity-60" />}
|
||||
</div>
|
||||
<p className="text-lg font-medium">{title}</p>
|
||||
<p className="text-sm mt-2">{description}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface QAComposerProps {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
isLoading: boolean;
|
||||
canSubmit?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
onStop: () => void;
|
||||
onKeyDown?: (val: string) => void;
|
||||
onUpload?: () => void;
|
||||
onReset?: () => void;
|
||||
onSave?: () => void;
|
||||
onCollect?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
showUpload?: boolean;
|
||||
showReset?: boolean;
|
||||
showSave?: boolean;
|
||||
showCollect?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 问答输入区。
|
||||
*
|
||||
* 负责输入、发送/停止、附件、重置、保存、收藏和模型参数设置入口。
|
||||
* 具体业务动作通过 props 回调交给父组件处理,保证输入区本身不依赖某个问答模式。
|
||||
*/
|
||||
export const QAComposer: React.FC<QAComposerProps> = ({
|
||||
value,
|
||||
placeholder,
|
||||
isLoading,
|
||||
canSubmit,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onStop,
|
||||
onKeyDown,
|
||||
onUpload,
|
||||
onReset,
|
||||
onSave,
|
||||
onCollect,
|
||||
onOpenSettings,
|
||||
showUpload = true,
|
||||
showReset = false,
|
||||
showSave = true,
|
||||
showCollect = true,
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const submitEnabled = canSubmit ?? Boolean(value.trim());
|
||||
const iconButtonClass = `p-2 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'}`;
|
||||
const handleKeyDown = (val: string, context: { e: React.KeyboardEvent }) => {
|
||||
if (context?.e?.key === 'Enter') {
|
||||
context.e.preventDefault();
|
||||
onKeyDown?.(val);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className={`flex items-end gap-3 rounded-2xl p-3 shadow-inner ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
|
||||
<Textarea
|
||||
value={value}
|
||||
onChange={(nextValue) => onChange(String(nextValue))}
|
||||
onKeydown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
autosize={{ minRows: 1, maxRows: 15 }}
|
||||
disabled={isLoading}
|
||||
className={`flex-1 !bg-transparent !border-0 !shadow-none qa-composer-textarea [&]:!border-0 [&]:!shadow-none [&]:!outline-none [&]:focus:!border-0 [&]:focus:!shadow-none [&]:focus:!outline-none [&_.t-textarea]:!border-0 [&_.t-textarea]:!shadow-none [&_.t-textarea]:!outline-none [&_.t-textarea:hover]:!border-0 [&_.t-textarea:hover]:!shadow-none [&_.t-textarea--focused]:!border-0 [&_.t-textarea--focused]:!shadow-none [&_.t-is-focused]:!border-0 [&_.t-is-focused]:!shadow-none [&_.t-textarea__inner]:!bg-transparent [&_.t-textarea__inner]:!border-0 [&_.t-textarea__inner]:!shadow-none [&_.t-textarea__inner]:!outline-none [&_.t-textarea__inner]:!resize-none [&_.t-textarea__inner]:!min-h-[40px] [&_.t-textarea__inner]:!p-0 [&_.t-textarea__inner:hover]:!border-0 [&_.t-textarea__inner:hover]:!shadow-none [&_.t-textarea__inner:focus]:!border-0 [&_.t-textarea__inner:focus]:!shadow-none [&_.t-textarea__inner:focus]:!outline-none [&_.t-textarea__inner]:disabled:!cursor-not-allowed ${isDark
|
||||
? '[&_.t-textarea__inner]:!text-white [&_.t-textarea__inner]:placeholder:!text-slate-500'
|
||||
: '[&_.t-textarea__inner]:!text-gray-800 [&_.t-textarea__inner]:placeholder:!text-gray-400'
|
||||
}`}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* {showUpload && (
|
||||
<button onClick={onUpload} className={iconButtonClass} title="上传附件" type="button">
|
||||
<Upload className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{showReset && (
|
||||
<button onClick={onReset} className={iconButtonClass} title="重新提问" type="button">
|
||||
<RotateCcw className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{showSave && (
|
||||
<button onClick={onSave} className={iconButtonClass} title="保存记录" type="button">
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
{showCollect && (
|
||||
<button onClick={onCollect} className={iconButtonClass} title="加入收藏" type="button">
|
||||
<Bookmark className="w-5 h-5" />
|
||||
</button>
|
||||
)} */}
|
||||
{onOpenSettings && (
|
||||
<button onClick={onOpenSettings} className={iconButtonClass} title="模型参数设置" type="button">
|
||||
<Settings className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={isLoading ? onStop : onSubmit}
|
||||
disabled={!isLoading && !submitEnabled}
|
||||
className={`px-4 py-2 rounded-xl font-medium transition-all flex items-center gap-2 shadow-sm ${isLoading
|
||||
? isDark
|
||||
? 'bg-rose-600 text-white qa-stop-breathing'
|
||||
: 'bg-red-600 text-white qa-stop-breathing'
|
||||
: submitEnabled
|
||||
? isDark
|
||||
? 'bg-blue-600 hover:bg-blue-700 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
: isDark
|
||||
? 'bg-slate-600 text-slate-400 cursor-not-allowed'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
type="button"
|
||||
>
|
||||
{isLoading ? <Square className="w-4 h-4 fill-current" /> : <Send className="w-4 h-4" />}
|
||||
{isLoading ? '停止' : '发送'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,80 +0,0 @@
|
||||
import { History } from 'lucide-react';
|
||||
import type { ViewMode } from './GeneralQA.types';
|
||||
|
||||
/**
|
||||
* 顶部栏 Props。
|
||||
*
|
||||
* `setViewMode` 由父组件传入,父组件会在生成中切换时做确认拦截;
|
||||
* Header 本身不直接感知 loading,保持纯展示和事件转发。
|
||||
*/
|
||||
type Props = {
|
||||
isDark: boolean; // 当前是否为暗黑主题,用于样式适配
|
||||
viewMode: ViewMode; // 当前视图模式:'single' 单一模式 或 'compare' 对比模式
|
||||
setViewMode: (mode: ViewMode) => void; // 设置视图模式回调函数
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用问答页面顶部栏。
|
||||
*
|
||||
* 该组件只负责页面标题、模式切换和历史记录入口,所有业务状态仍由父组件维护。
|
||||
* 这样拆分后,顶部导航的样式和交互不会和中间问答流处理逻辑混在一起,后续如果要新增
|
||||
* “保存模板”“导出记录”等入口,也可以只改这个文件。
|
||||
*/
|
||||
export default function GeneralQAHeader({ isDark, viewMode, setViewMode }: Props): JSX.Element {
|
||||
return (
|
||||
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
|
||||
通用问答
|
||||
</h1>
|
||||
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
支持单轮RAG、多轮RAG、深度检索及结果对比
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`flex rounded-lg p-1 ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
|
||||
<button
|
||||
onClick={() => setViewMode('single')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'single'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
单一模式
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('compare')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
|
||||
viewMode === 'compare'
|
||||
? isDark
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'text-slate-400 hover:text-white'
|
||||
: 'text-gray-600 hover:text-red-600'
|
||||
}`}
|
||||
>
|
||||
对比模式
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
isDark
|
||||
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<History className="w-4 h-4" />
|
||||
历史记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import MarkdownRenderer from '../../MarkdownRenderer';
|
||||
import './qa-animations.css';
|
||||
|
||||
/**
|
||||
* 单条检索结果。
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* QA 系列共享动画与样式
|
||||
*
|
||||
* 这些类被 QACommon.tsx(以及通过它的 GeneralQA / AgentQA / CollaborativeQA)使用。
|
||||
* 全部样式从 web-code/theme.css、web-code/App.css 和 web-code/index.css 迁移而来,
|
||||
* 仅保留 QA 系列实际使用的部分。
|
||||
*/
|
||||
|
||||
/* ── 头像扫光(生成中沿头像横扫的高光条) ───────────────────────────── */
|
||||
@keyframes qa-avatar-shine {
|
||||
0% {
|
||||
transform: translateX(-120%) skewX(-18deg);
|
||||
opacity: 0;
|
||||
}
|
||||
45% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(120%) skewX(-18deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 头像整体微旋转(generating 状态) ─────────────────────────────── */
|
||||
@keyframes qa-avatar-orbit {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(4deg) scale(1.06);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 头像图标脉冲 ────────────────────────────────────────────────── */
|
||||
@keyframes qa-avatar-icon-pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.12);
|
||||
opacity: 0.82;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 右上角 Sparkles 闪烁 ───────────────────────────────────────── */
|
||||
@keyframes qa-spark-twinkle {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.85) rotate(0deg);
|
||||
opacity: 0.65;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15) rotate(18deg);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 停止按钮呼吸 ─────────────────────────────────────────────── */
|
||||
@keyframes qa-stop-breathing {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.45);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── “正在生成” 跑马灯(文字渐变流动) ─────────────────────────── */
|
||||
@keyframes qa-generating-marquee {
|
||||
0% {
|
||||
background-position: 180% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: -80% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 输入中三个圆点跳动 ───────────────────────────────────────── */
|
||||
@keyframes qa-typing-dot {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.35;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-5px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 类绑定 ───────────────────────────────────────────────────── */
|
||||
.qa-avatar-active {
|
||||
animation: qa-avatar-orbit 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-avatar-icon-active {
|
||||
animation: qa-avatar-icon-pulse 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-avatar-active .qa-avatar-shine {
|
||||
animation: qa-avatar-shine 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-avatar-spark {
|
||||
animation: qa-spark-twinkle 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-stop-breathing {
|
||||
animation: qa-stop-breathing 1.25s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-typing-dot {
|
||||
animation: qa-typing-dot 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.qa-generating-marquee {
|
||||
display: inline-block;
|
||||
background-size: 220% 100%;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: qa-generating-marquee 1.45s linear infinite;
|
||||
}
|
||||
|
||||
.qa-generating-marquee-light {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
#4b5563 0%,
|
||||
#4b5563 34%,
|
||||
#ef4444 48%,
|
||||
#fb923c 55%,
|
||||
#4b5563 68%,
|
||||
#4b5563 100%
|
||||
);
|
||||
}
|
||||
|
||||
.qa-generating-marquee-dark {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
#cbd5e1 0%,
|
||||
#cbd5e1 34%,
|
||||
#67e8f9 48%,
|
||||
#a78bfa 55%,
|
||||
#cbd5e1 68%,
|
||||
#cbd5e1 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* ── 兜底:清掉 TDesign textarea 的边框/阴影/背景 ─────────────── */
|
||||
.qa-composer-textarea,
|
||||
.qa-composer-textarea .t-textarea,
|
||||
.qa-composer-textarea .t-textarea__inner {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.qa-composer-textarea .t-textarea__inner {
|
||||
resize: none !important;
|
||||
min-height: 40px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
getKnowledgeIframeApiUrl,
|
||||
getKnowledgeIframeSkin,
|
||||
} from "@/core/strategy/external-links";
|
||||
import { requestCrudService } from "@/service/request-axios-service";
|
||||
import "@/strategy-components/components/task-components/css/strategy-data.css";
|
||||
|
||||
const service = requestCrudService;
|
||||
|
||||
const KnowledgeBaseResourceManagement: React.FC = () => {
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const initIframeUrl = async () => {
|
||||
const apiUrl = getKnowledgeIframeApiUrl();
|
||||
if (!apiUrl) return;
|
||||
|
||||
const jsonStr = localStorage.getItem("userInfo");
|
||||
if (!jsonStr) return;
|
||||
|
||||
try {
|
||||
const userInfo = JSON.parse(jsonStr);
|
||||
const token = userInfo.accessToken;
|
||||
if (!token) return;
|
||||
|
||||
const res = (await service.post(apiUrl, {
|
||||
token,
|
||||
skin: getKnowledgeIframeSkin(),
|
||||
})) as { iframeUrl?: string };
|
||||
if (res?.iframeUrl) {
|
||||
setIframeUrl(res.iframeUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load knowledge iframe url:", error);
|
||||
}
|
||||
};
|
||||
|
||||
void initIframeUrl();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="strategy-data iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe" title="知识库资源管理" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeBaseResourceManagement;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { getNetworkActorIframeUrl } from "@/core/strategy/external-links";
|
||||
import "@/strategy-components/components/task-components/css/strategy-data.css";
|
||||
|
||||
const NetworkActorResourceManagement: React.FC = () => {
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setIframeUrl(getNetworkActorIframeUrl());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="strategy-data iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe" title="网络xwt资源管理" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkActorResourceManagement;
|
||||
@@ -0,0 +1,207 @@
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { actionColumnItem, ActionListItem, TaskListItem, taskStatusList } from './TaskManagement';
|
||||
import { Bot, ChevronRight, Download, Edit, Eye, FileText, Layers, MoreHorizontal, Play, Search, User, Users, Workflow } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Col, Empty, Loading, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { ActionColumn } from './ActionColumn';
|
||||
|
||||
interface ActionAnalyzesProps {
|
||||
TaskType?: string;
|
||||
title: string;
|
||||
category: string;
|
||||
subCategory: string;
|
||||
}
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
export const ActionAnalyzes: React.FC<ActionAnalyzesProps> = ({ title, category, subCategory}) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
// 主题颜色变量
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-100';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-white';
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [taskList, setTaskList] = useState<TaskListItem[]>([]); // 任务列表
|
||||
const [taskId, setTaskId] = useState<number>(0); // 任务
|
||||
const [actionColumnList, setActionColumnList] = useState<actionColumnItem[]>([]);
|
||||
const [taskLoading, setTaskLoading] = useState<boolean>(false);
|
||||
const [actionLoading, setActionLoading] = useState<boolean>(false);
|
||||
// 当前页码,默认为第 1 页
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
// 每页显示数量,默认为 10 条
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
const [total, setTotal] = useState<number>(0); // 查询任务总数
|
||||
const state = useRef({
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await taskListSearch()
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (value: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
setPageNum(value);
|
||||
state.current.pageIndex = value;
|
||||
await taskListSearch();
|
||||
}
|
||||
|
||||
// 任务查询
|
||||
const taskListSearch = async () => {
|
||||
const param = {
|
||||
keyword: '',
|
||||
pageNum: state.current.pageIndex,
|
||||
pageSize: state.current.pageSize
|
||||
}
|
||||
setTaskLoading(true);
|
||||
const res = await service.post(`${zncmUrl}/action/cop-action-tasks`, param);
|
||||
setTaskLoading(false);
|
||||
console.log('任务搜索', res);
|
||||
const result = res.data;
|
||||
result.forEach((item: TaskListItem) => {
|
||||
item.progress = item.endCount / item.sumAll || 0; // 获取已完成和未完成的占比
|
||||
item.taskStatusData = taskStatusList[item.taskStatus] // 根据任务状态码获取相关信息
|
||||
})
|
||||
setTaskList(result || []); // 设置任务列表页
|
||||
const totalRes = await service.post(`${zncmUrl}/action/cop-action-tasks-count`, param); // 查询任务总数
|
||||
console.log('totalRes', totalRes)
|
||||
setTotal(totalRes.data || 0); // 设置总数
|
||||
if (result.length > 0) {
|
||||
setTaskId(result[0].id);
|
||||
await actionListSearch(result[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
// 行动查询
|
||||
const actionListSearch = async (taskId: number) => {
|
||||
const param = {
|
||||
taskId: taskId,
|
||||
}
|
||||
setActionLoading(true);
|
||||
const res = await service.get(`${zncmUrl}/action/cop-task-actions`, param);
|
||||
console.log('行动查询', res);
|
||||
setActionLoading(false);
|
||||
const result = res.data;
|
||||
setActionColumnList([
|
||||
{ columnName: '待分析', icon: '待CEH.svg', actionList: result.collect || [] },
|
||||
{ columnName: 'CEH中', icon: '进行中.svg', actionList: result.collect1 || [] },
|
||||
{ columnName: 'CEH完成', icon: '完成.svg', actionList: result.collect2 || [] },
|
||||
{ columnName: '已终止', icon: '已终止.svg', actionList: result.endDate || [] },
|
||||
]);
|
||||
}
|
||||
|
||||
const taskDataChange = async (taskId: number) => {
|
||||
setTaskId(taskId);
|
||||
await actionListSearch(taskId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
taskListSearch();
|
||||
}, [])
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<Row className="h-full flex flex-col">
|
||||
<Col span={2} className="h-full">
|
||||
<div className={`h-[calc(100vh-100px)] p-[10px] ${isDark ? 'bg-[rgba(3,38,80,0.6)]' : 'bg-white'} shadow-[2px_0px_6px_rgba(0,0,0,0.12)]`} >
|
||||
{/* <div className="flex justify-between">
|
||||
<span>筛选</span>
|
||||
<span className="flex items-center">
|
||||
<span>下发时间排序</span>
|
||||
</span>
|
||||
</div> */}
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${textMuted}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索任务名称"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
className={`w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${inputBg} ${cardBorder} ${textPrimary} placeholder:${textMuted}`}
|
||||
/>
|
||||
</div>
|
||||
{/* 任务列表 */}
|
||||
<Loading loading={taskLoading}>
|
||||
<div className="h-[calc(100vh-200px)] overflow-y-auto pr-[10px]">
|
||||
{
|
||||
taskList.map((item: TaskListItem, index: number) => (
|
||||
<div key={index} className={`${isDark ? 'bg-[#0062d91a] border-[#55a6fe1a] shadow-[0_3px_4px_#8b8b8b0f] dark-theme' : 'bg-white border-[#f5f5f5] shadow-[2px_0px_6px_rgba(0,0,0,0.08)] task-item-active'} p-[10px] mt-[10px] mb-[10px] rounded-[4px] border ${taskId === item.id && 'task-active'} cursor-pointer`} onClick={() => taskDataChange(item.id)}>
|
||||
<div className="text-[var(--text-color)] text-[17px] mb-[5px] font-[500]">{item.taskName}</div>
|
||||
<div className="text-[#7f7f7f] mb-[5px]">{format(new Date(item.taskStartDate), 'yyyy-MM-dd HH:mm:ss')}</div>
|
||||
<div className="flex items-center mb-[8px]">
|
||||
<img src={`/icon/${item?.taskStatusData?.icon}`} alt="" />
|
||||
<span className="ml-[5px] leading-[16px]">{item?.taskStatusData?.label}</span>
|
||||
</div>
|
||||
<div className="pb-[10px] pt-[10px]">
|
||||
<div className="border-b border-[#e8e8e8]"></div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="h-[24px] item-active pl-[10px] pr-[10px] task-tab-color leading-[24px] rounded-[50px] bg-[var(--bg-color)]">
|
||||
完成{item.endCount}/{item.sumAll}
|
||||
</div>
|
||||
<div className="pl-[20px] flex-1">
|
||||
<Progress percentage={item.progress} label={false} color={item?.taskStatusData?.color} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Loading>
|
||||
{/* 分页 */}
|
||||
<div>
|
||||
<Pagination
|
||||
total={total}
|
||||
size="small"
|
||||
theme="simple"
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
showPageSize={false}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col span={10} className="h-full">
|
||||
<div className="h-full flex flex-col p-[0_10px]">
|
||||
<Loading loading={actionLoading}>
|
||||
<div className="flex grid grid-cols-4 gap-[10px] pl-[10px]">
|
||||
{
|
||||
actionColumnList.map((item: actionColumnItem, index: number) => (
|
||||
<ActionColumn key={index} icon={item.icon} columnIndex={index} columnName={item.columnName} actionCount={item.actionList.length} actionList={item.actionList} title={title} category={category} subCategory={subCategory} />
|
||||
))
|
||||
}
|
||||
{
|
||||
actionColumnList.length === 0 && <Empty/>
|
||||
}
|
||||
</div>
|
||||
</Loading>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { ActionListItem, TaskListItem, taskStatusList } from './TaskManagement';
|
||||
import { Bot, ChevronRight, Download, Edit, Eye, FileText, Layers, MoreHorizontal, Play, Search, User, Users, Workflow } from 'lucide-react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Col, Empty, Pagination, Progress, Row } from 'tdesign-react';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
|
||||
interface ActionColumnProps {
|
||||
TaskType?: string;
|
||||
title: string;
|
||||
category: string;
|
||||
subCategory: string;
|
||||
icon: string;
|
||||
columnIndex: number;
|
||||
columnName: string;
|
||||
actionCount: number;
|
||||
actionList: ActionListItem[];
|
||||
}
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
export const ActionColumn: React.FC<ActionColumnProps> = ({ title, category, subCategory, icon, columnName, columnIndex, actionCount, actionList }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
// 主题颜色变量
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-100';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-white';
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
// 当前页码,默认为第 1 页
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
// 每页显示数量,默认为 10 条
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
const [total, setTotal] = useState<number>(0); // 查询任务总数
|
||||
const state = useRef({
|
||||
pageIndex: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (value: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
setPageNum(value);
|
||||
state.current.pageIndex = value;
|
||||
}
|
||||
|
||||
const actionRouterChange = (item: ActionListItem) => {
|
||||
if (item.actionStatus !== 0) {
|
||||
window.open(`https://ch1.b.uat.4.cn/web/#/action/item-aiceh/step-analysis-three?id=${item.id}`);
|
||||
} else {
|
||||
window.open(`https://ch1.b.uat.4.cn/web/#/action/item-aiceh/step-action-topic?id=${item.id}&isPassApply=false`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`h-[calc(100vh-100px)] p-[10px] flex-1 ${isDark? 'dark-action-bg' : `bg-white action-bg${columnIndex} shadow-[2px_0px_6px_rgba(0,0,0,0.12)]`} rounded-[4px]`}>
|
||||
<div className="mb-[10px]">
|
||||
<div className="flex items-center mb-[20px]">
|
||||
<img src={`/icon/${icon}`} className="w-[24px] mr-[10px]" alt="" />
|
||||
<span className="text-[#222222] text-[18px] font-bold mr-[20px]">{columnName}</span>
|
||||
<span className="text-[#222222] text-[18px] font-bold mr-[20px]">{actionCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[calc(100vh-200px)] overflow-y-auto pr-[10px]">
|
||||
{
|
||||
actionList.map((item: ActionListItem, index: number) => (
|
||||
<div key={index} className={`${isDark? 'dark-action-item' : 'bg-white border border-[#f5f5f5] shadow-[0_1px_16px_#0000000f] action-item'} rounded-md px-4 py-2.5 mb-[10px] hover:border-[var(--card-border)] cursor-pointer`} onClick={() => actionRouterChange(item)}>
|
||||
<div className="line-clamp-4 overflow-hidden mb-[10px]">
|
||||
{
|
||||
item.actionName
|
||||
}
|
||||
</div>
|
||||
<div className="flex items-center mb-[10px]">
|
||||
<span className={`${isDark? 'bg-[#0062d94d] text-[#aaceff]' :'bg-[#f5f5f5]'} leading-[16px] p-[5px_10px] rounded-[2px]`}>
|
||||
{item.level === 1 ? '主要行动' : '次要行动'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className={`${isDark? 'bg-[#0062d94d] text-[#aaceff]' :'bg-[#f5f5f5]'} leading-[16px] p-[5px_10px] rounded-[2px]`}>
|
||||
{item.deptName}
|
||||
</span>
|
||||
<span className="border border-[var(--color-main)] text-[var(--color-main)] cursor-pointer leading-[16px] ml-[10px] p-[5px_10px] rounded-[2px]">
|
||||
{
|
||||
item.actionStatus === 0 ? <span>XD签收</span> : <span>开始CEH</span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
actionList.length === 0 && <Empty image={
|
||||
<img
|
||||
src="/icon/empty-list.png"
|
||||
alt="empty"
|
||||
className="w-[135px]"
|
||||
/>
|
||||
} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "./css/iframe.css";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
const TYURL = import.meta.env.VITE_AIPI_B2ACTOR_URL || "";
|
||||
|
||||
const FanganTyPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
useEffect(() => {
|
||||
initIframeUrl();
|
||||
}, []);
|
||||
const initIframeUrl = () => {
|
||||
const url = `${TYURL}/fromB/toPage?defaultPage=iframeFaty`;
|
||||
console.log({ iframeUrl: url });
|
||||
setIframeUrl(url);
|
||||
};
|
||||
return (
|
||||
<div className="iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FanganTyPage;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { ChevronRight, FileText } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { Pagination } from 'tdesign-react';
|
||||
|
||||
interface KanbanColumnProps {
|
||||
title: string;
|
||||
count: number;
|
||||
color: string;
|
||||
tasks: TaskListItem[];
|
||||
taskType: string;
|
||||
TaskDataType?: string; // 区分来源类型
|
||||
pageIndexSearch: (taskType: string, pageIndex: number) => void;
|
||||
handleStartAnalysis: (task: TaskListItem) => void
|
||||
}
|
||||
|
||||
export const KanbanColumn: React.FC<KanbanColumnProps> = ({ title, count, color, tasks,taskType, TaskDataType = 'task' , pageIndexSearch, handleStartAnalysis }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
// 当前页码,默认为第 1 页
|
||||
const [pageNum, setPageNum] = useState<number>(1);
|
||||
// 每页显示数量,默认为 10 条
|
||||
const [pageSize, setPageSize] = useState<number>(10); // 每页显示 10 条
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (value: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
console.log('taskType', taskType)
|
||||
setPageNum(value);
|
||||
await pageIndexSearch(taskType, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每页显示数量变化时的回调函数
|
||||
* 当用户切换每页显示条数时触发,更新每页数量并重新获取数据
|
||||
* @param value - 新的每页显示数量
|
||||
*/
|
||||
const pageSizeChange = async (value: number) => {
|
||||
// 更新 ref 中存储的每页显示数量
|
||||
// 更新每页显示数量状态,触发组件重新渲染
|
||||
setPageSize(value);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col min-h-0 flex-1">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${color}`} />
|
||||
<h2 className={`font-semibold text-sm ${textPrimary}`}>{title}</h2>
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${isDark ? 'bg-slate-700/50 text-slate-400' : 'bg-red-50 text-red-500'
|
||||
}`}>{count}</span>
|
||||
</div>
|
||||
{/* <button className={`p-1 rounded transition-colors ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
|
||||
}`}>
|
||||
<ChevronRight className={`w-4 h-4 ${textMuted}`} />
|
||||
</button> */}
|
||||
<div>
|
||||
<Pagination
|
||||
total={count}
|
||||
size="small"
|
||||
theme="simple"
|
||||
current={pageNum}
|
||||
pageSize={pageSize}
|
||||
maxPageBtn={3}
|
||||
showPageSize={false}
|
||||
totalContent={false}
|
||||
onCurrentChange={(page) => pageIndexChange(Number(page))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto space-y-3 pr-1 scrollbar-thin">
|
||||
{tasks.length === 0 ? (
|
||||
<div className={`flex flex-col items-center justify-center py-8 ${textMuted}`}>
|
||||
<FileText className="w-8 h-8 mb-2 opacity-50" />
|
||||
<p className="text-sm">暂无任务</p>
|
||||
</div>
|
||||
) : (
|
||||
tasks.map(task => <TaskCard key={task.id} task={task} TaskType={TaskDataType} handleStartAnalysis={handleStartAnalysis}/>)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+391
@@ -0,0 +1,391 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Eye,
|
||||
Play,
|
||||
Pause,
|
||||
Clock,
|
||||
User,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
FileText,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
MoreHorizontal,
|
||||
Edit,
|
||||
Download,
|
||||
Bot,
|
||||
Users,
|
||||
Workflow,
|
||||
X,
|
||||
ArrowRight,
|
||||
Target,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
// 导入请求服务,用于与后端 API 交互
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
category: string;
|
||||
subCategory: string;
|
||||
}
|
||||
|
||||
interface stateItem {
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
task: TaskListItem,
|
||||
}
|
||||
|
||||
type TaskType = 'single' | 'consultation' | 'scene' | 'multi-agent' | 'task-search';
|
||||
type TaskStatus = 'draft' | 'pending' | 'running' | 'paused' | 'blocked' | 'completed';
|
||||
type ColumnType = 'unanalyzed' | 'analyzing' | 'completed';
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory}) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const router = useNavigate();
|
||||
// 主题颜色变量
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-100';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-white';
|
||||
const { setStartConfig } = useChatContext();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [modeFilter, setModeFilter] = useState<string>('all');
|
||||
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
||||
const [selectedTaskForAnalysis, setSelectedTaskForAnalysis] = useState<TaskListItem | null>(null);
|
||||
const [unanalyzedTotal, setUnanalyzedTotal] = useState<number>(0); // 未分析
|
||||
const [runningTotal, setRunningTotal] = useState<number>(0); // 分析中
|
||||
const [completedTotal, setCompletedTotal] = useState<number>(0); // 已完成
|
||||
const [unanalyzedList, setUnanalyzedList] = useState<TaskListItem[]>([]); // 未分析
|
||||
const [runningList, setRunningList] = useState<TaskListItem[]>([]); // 分析中
|
||||
const [completedList, setCompletedList] = useState<TaskListItem[]>([]); // 已完成
|
||||
const [unanalyzedLoading, setUnanalyzedLoading] = useState<boolean>(false); // 未分析
|
||||
const [runningLoading, setRunningLoading] = useState<boolean>(false); // 分析中
|
||||
const [completedLoading, setCompletedLoading] = useState<boolean>(false); // 已完成
|
||||
const [presets] = useState<DateRangePickerProps['presets']>({
|
||||
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
|
||||
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
|
||||
今天: [dayjs().toDate(), dayjs().toDate()],
|
||||
});
|
||||
const [range, setRange] = useState<DateRangeValue>([]);
|
||||
const state = useRef<stateItem>({
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
task: {} as TaskListItem,
|
||||
})
|
||||
|
||||
// 时间日期选择器查询
|
||||
const setRangeChange = async (val) => {
|
||||
setRange(val);
|
||||
console.log('时间日期', val);
|
||||
state.current.startTime = '';
|
||||
state.current.endTime = '';
|
||||
if (val.length > 0) {
|
||||
state.current.startTime = val[0];
|
||||
state.current.endTime = val[1];
|
||||
}
|
||||
|
||||
await taskDataChange();
|
||||
}
|
||||
|
||||
// 处理开始分析按钮点击
|
||||
const handleStartAnalysis = (task: TaskListItem) => {
|
||||
setSelectedTaskForAnalysis(task);
|
||||
state.current.task = task;
|
||||
setShowAnalysisModal(true);
|
||||
};
|
||||
|
||||
// 处理分析模式选择
|
||||
const handleSelectAnalysisMode = (mode: TaskType) => {
|
||||
setShowAnalysisModal(false);
|
||||
console.log('mode', mode);
|
||||
if (mode === 'consultation') {
|
||||
console.log('state.current.task', state.current.task)
|
||||
const sceneItem = JSON.parse(state.current.task.scene);
|
||||
if (sceneItem.taskJson) {
|
||||
sceneItem.taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...sceneItem.taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: state.current.task.overview
|
||||
});
|
||||
router('/qa-center/mope-chat')
|
||||
}
|
||||
console.log('sceneItem', sceneItem);
|
||||
} else if (mode === 'task-search') {
|
||||
window.open(`https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-info-search?id=${state.current.task.id}`)
|
||||
}
|
||||
};
|
||||
|
||||
// 处理卡片点击
|
||||
const taskListSearch = async (taskType: string, pageNum: number = 1, pageSize: number = 10) => {
|
||||
let taskStatus: number[] = []
|
||||
if (taskType === 'unanalyzed') { // 未分析
|
||||
taskStatus = [0, 10];
|
||||
setUnanalyzedLoading(true);
|
||||
} else if (taskType === 'running') { // 分析中
|
||||
taskStatus = [20];
|
||||
setRunningLoading(true);
|
||||
} else if (taskType === 'completed') { // 已完成
|
||||
taskStatus = [25, 30, 40, 60, 70, 80, 90, 100, 110];
|
||||
setCompletedLoading(true);
|
||||
}
|
||||
const param = {
|
||||
content: searchQuery,
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
taskStatus: taskStatus,
|
||||
startTime: state.current.startTime,
|
||||
endTime: state.current.endTime,
|
||||
taskDirection: ''
|
||||
}
|
||||
const res = await service.post(`${zncmUrl}/three/cop-three-tasks`, param);
|
||||
console.log('任务列表', res);
|
||||
const result = res.data;
|
||||
result.records.forEach((item: TaskListItem) => {
|
||||
item.column = taskType;
|
||||
if (!item.taskLevel) item.taskLevel = 1;
|
||||
})
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedLoading(false);
|
||||
setUnanalyzedTotal(result.total);
|
||||
setUnanalyzedList(result.records);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningLoading(false);
|
||||
setRunningTotal(result.total);
|
||||
setRunningList(result.records);
|
||||
} else if (taskType === 'completed') {
|
||||
setCompletedLoading(false);
|
||||
setCompletedTotal(result.total);
|
||||
setCompletedList(result.records);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (type: string, pageIndex: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
await taskListSearch(type, pageIndex);
|
||||
}
|
||||
|
||||
const taskDataChange = async (pageNum: number = 1, pageSize: number = 10) => {
|
||||
taskListSearch('unanalyzed', pageNum, pageSize);
|
||||
taskListSearch('running', pageNum, pageSize);
|
||||
taskListSearch('completed', pageNum, pageSize);
|
||||
}
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await taskDataChange();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
taskDataChange();
|
||||
}, []);
|
||||
|
||||
// 渲染看板列
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`rounded-xl p-4 flex-shrink-0 mb-4 ${cardBg} border ${cardBorder}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'}`}>
|
||||
{category}
|
||||
</span>
|
||||
<span className={`text-xs ${textMuted}`}>/</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-red-100 text-red-600'}`}>
|
||||
{subCategory}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className={`text-xl font-bold ${textPrimary}`}>任务中心</h1>
|
||||
</div>
|
||||
{/* <button
|
||||
onClick={() => onNavigate('task-create')}
|
||||
className={`flex items-center gap-2 px-4 py-2 text-white rounded-lg text-sm font-medium transition-colors ${isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500'
|
||||
: 'bg-red-600 hover:bg-red-500 shadow-md'
|
||||
}`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新建任务
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${textMuted}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索任务名称"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
className={`w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${inputBg} ${cardBorder} ${textPrimary} placeholder:${textMuted}`}
|
||||
/>
|
||||
</div>
|
||||
{/* <select
|
||||
value={modeFilter}
|
||||
onChange={(e) => setModeFilter(e.target.value)}
|
||||
className={`px-4 py-2 border rounded-lg text-sm focus:outline-none cursor-pointer ${inputBg} ${cardBorder} ${textSecondary}`}
|
||||
>
|
||||
<option value="all">全部模式</option>
|
||||
<option value="single">单智能体</option>
|
||||
<option value="multi-agent">多智能体协同</option>
|
||||
<option value="consultation">多智能体会商</option>
|
||||
<option value="scene">场景执行</option>
|
||||
</select> */}
|
||||
{/* <button className={`flex items-center gap-2 px-4 py-2 border rounded-lg text-sm transition-colors ${inputBg} ${cardBorder} ${textSecondary}`}>
|
||||
<Clock className="w-4 h-4" />
|
||||
时间范围
|
||||
</button> */}
|
||||
<DateRangePicker value={range} presets={presets} onChange={(val) => setRangeChange(val)} enableTimePicker={true} clearable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览栏 */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-4 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => { setModeFilter('all'); }}
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${textPrimary}`}>{unanalyzedTotal + runningTotal + completedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>全部任务</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${textPrimary}`}>{unanalyzedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>未分析</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${isDark ? 'text-blue-400' : 'text-red-600'}`}>{runningTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>分析中</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${isDark ? 'text-green-400' : 'text-green-600'}`}>{completedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>已完成</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 看板区 */}
|
||||
<div className="flex-1 grid grid-cols-3 gap-4 min-h-0 overflow-hidden">
|
||||
<Loading loading={unanalyzedLoading}>
|
||||
<KanbanColumn title="未分析" taskType={"unanalyzed"} TaskDataType={'sentiment'} count={unanalyzedTotal} color={isDark ? 'bg-yellow-400' : 'bg-yellow-400'} tasks={unanalyzedList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis} />
|
||||
</Loading>
|
||||
<Loading loading={runningLoading}>
|
||||
<KanbanColumn title="分析中" taskType="running" TaskDataType={'sentiment'} count={runningTotal} color={isDark ? 'bg-blue-400' : 'bg-red-400'} tasks={runningList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis} />
|
||||
</Loading>
|
||||
<Loading loading={completedLoading}>
|
||||
<KanbanColumn title="已完成" taskType="completed" TaskDataType={'sentiment'} count={completedTotal} color={isDark ? 'bg-green-400' : 'bg-green-400'} tasks={completedList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis} />
|
||||
</Loading>
|
||||
</div>
|
||||
|
||||
{/* 选择分析模式弹窗 */}
|
||||
{showAnalysisModal && selectedTaskForAnalysis && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowAnalysisModal(false)}>
|
||||
<div
|
||||
className={`w-full max-w-lg rounded-2xl p-6 ${cardBg} border ${cardBorder} shadow-xl`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className={`text-xl font-bold ${textPrimary}`}>选择分析模式</h2>
|
||||
<p className={`text-sm ${textMuted} mt-1`}>为任务「{selectedTaskForAnalysis.taskName}」选择分析方式</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAnalysisModal(false)}
|
||||
className={`p-2 rounded-lg hover:bg-slate-700/50 ${textSecondary}`}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
|
||||
<button
|
||||
onClick={() => handleSelectAnalysisMode('task-search')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-green-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-green-500/20' : 'bg-green-100'
|
||||
}`}>
|
||||
<Layers className={`w-6 h-6 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>查看任务详情</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>进入b1系统查看任务详情</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
||||
{/* 多智能体会商 */}
|
||||
<button
|
||||
onClick={() => handleSelectAnalysisMode('consultation')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-orange-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-orange-500/20' : 'bg-red-100'
|
||||
}`}>
|
||||
<Users className={`w-6 h-6 ${isDark ? 'text-orange-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>多智能体会商</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>多个AI智能体从不同视角开展讨论并形成共识</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SentimentAnalyzes;
|
||||
@@ -0,0 +1,648 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Eye,
|
||||
Play,
|
||||
Pause,
|
||||
Clock,
|
||||
User,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
FileText,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
MoreHorizontal,
|
||||
Edit,
|
||||
Download,
|
||||
Bot,
|
||||
Users,
|
||||
Workflow,
|
||||
X,
|
||||
ArrowRight,
|
||||
Target,
|
||||
Layers,
|
||||
} from 'lucide-react';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
// 导入请求服务,用于与后端 API 交互
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import dayjs from 'dayjs';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
category: string;
|
||||
subCategory: string;
|
||||
}
|
||||
|
||||
type TaskType = 'single' | 'consultation' | 'scene' | 'multi-agent' | 'task-search';
|
||||
type TaskStatus = 'draft' | 'pending' | 'running' | 'paused' | 'blocked' | 'completed';
|
||||
type ColumnType = 'unanalyzed' | 'analyzing' | 'completed';
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
name: string;
|
||||
type: TaskType;
|
||||
status: TaskStatus;
|
||||
agent?: string;
|
||||
scene?: string;
|
||||
participants?: string[];
|
||||
currentPhase?: string;
|
||||
currentRound?: number;
|
||||
summary?: string;
|
||||
creator: string;
|
||||
createTime: string;
|
||||
updateTime: string;
|
||||
column: ColumnType;
|
||||
}
|
||||
|
||||
interface stateItem {
|
||||
startTime: string,
|
||||
endTime: string,
|
||||
task: TaskListItem,
|
||||
}
|
||||
|
||||
const service = requestCrudService;
|
||||
const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory}) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const router = useNavigate();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
// 主题颜色变量
|
||||
const cardBg = isDark ? 'bg-slate-800/30' : 'bg-white';
|
||||
const cardBorder = isDark ? 'border-blue-800/30' : 'border-red-100';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
const inputBg = isDark ? 'bg-slate-900/50' : 'bg-white';
|
||||
|
||||
// 生成模拟数据
|
||||
const [tasks] = useState<Task[]>([
|
||||
// 未分析列
|
||||
{
|
||||
id: '2026-0320-001',
|
||||
name: '端午节品牌传播方案',
|
||||
type: 'scene',
|
||||
scene: '宣传策略生成',
|
||||
status: 'draft',
|
||||
column: 'unanalyzed',
|
||||
summary: '针对端午节品牌活动制定完整传播策略方案',
|
||||
creator: '李策划',
|
||||
createTime: '2026-03-20 10:00',
|
||||
updateTime: '2026-03-20 10:00',
|
||||
},
|
||||
{
|
||||
id: '2026-0320-002',
|
||||
name: '新品上市用户调研',
|
||||
type: 'single',
|
||||
agent: 'AudienceAgent',
|
||||
status: 'pending',
|
||||
column: 'unanalyzed',
|
||||
summary: '对新品目标用户进行画像分析',
|
||||
creator: '王运营',
|
||||
createTime: '2026-03-20 09:30',
|
||||
updateTime: '2026-03-20 09:30',
|
||||
},
|
||||
// 分析中列
|
||||
{
|
||||
id: '2026-0319-001',
|
||||
name: '五四青年节宣传活动策略',
|
||||
type: 'scene',
|
||||
scene: '宣传策略生成',
|
||||
status: 'running',
|
||||
currentPhase: '行为分析',
|
||||
column: 'analyzing',
|
||||
summary: '正在按场景模板执行目标、受众、行为、叙事等分析...',
|
||||
creator: '张策略',
|
||||
createTime: '2026-03-19 10:30',
|
||||
updateTime: '2026-03-20 09:45',
|
||||
},
|
||||
{
|
||||
id: '2026-0319-002',
|
||||
name: '青年传播方向会商分析',
|
||||
type: 'consultation',
|
||||
participants: ['GoalAgent', 'AudienceAgent', 'NarrativeAgent'],
|
||||
status: 'running',
|
||||
currentRound: 2,
|
||||
column: 'analyzing',
|
||||
summary: '围绕宣传方向、叙事方式和受众切入点开展多视角会商...',
|
||||
creator: '王策划',
|
||||
createTime: '2026-03-19 14:00',
|
||||
updateTime: '2026-03-20 09:00',
|
||||
},
|
||||
{
|
||||
id: '2026-0318-001',
|
||||
name: '年度品牌升级方案推演',
|
||||
type: 'scene',
|
||||
scene: '宣传方案推演',
|
||||
status: 'paused',
|
||||
currentPhase: '叙事分析',
|
||||
column: 'analyzing',
|
||||
summary: '任务已暂停,等待人工介入确认叙事方向',
|
||||
creator: '王策划',
|
||||
createTime: '2026-03-18 14:00',
|
||||
updateTime: '2026-03-20 08:30',
|
||||
},
|
||||
// 已完成列
|
||||
{
|
||||
id: '2026-0318-002',
|
||||
name: '核心受众画像分析',
|
||||
type: 'single',
|
||||
agent: 'AudienceAgent',
|
||||
status: 'completed',
|
||||
column: 'completed',
|
||||
summary: '已完成目标受众识别、关切提炼和阻力分析',
|
||||
creator: '赵运营',
|
||||
createTime: '2026-03-18 09:00',
|
||||
updateTime: '2026-03-18 16:30',
|
||||
},
|
||||
{
|
||||
id: '2026-0319-003',
|
||||
name: '五四青年节宣传策略',
|
||||
type: 'scene',
|
||||
scene: '宣传策略生成',
|
||||
status: 'completed',
|
||||
column: 'completed',
|
||||
summary: '已完成宣传策略方案生成',
|
||||
creator: '张策略',
|
||||
createTime: '2026-03-19 10:00',
|
||||
updateTime: '2026-03-19 18:00',
|
||||
},
|
||||
{
|
||||
id: '2026-0319-004',
|
||||
name: '青年品牌传播路径会商',
|
||||
type: 'consultation',
|
||||
participants: ['GoalAgent', 'NarrativeAgent', 'RiskAgent'],
|
||||
status: 'completed',
|
||||
column: 'completed',
|
||||
summary: '已完成多智能体会商,形成结论与分歧报告',
|
||||
creator: '王策划',
|
||||
createTime: '2026-03-19 14:30',
|
||||
updateTime: '2026-03-19 17:00',
|
||||
},
|
||||
]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [modeFilter, setModeFilter] = useState<string>('all');
|
||||
const [showAnalysisModal, setShowAnalysisModal] = useState(false);
|
||||
const [selectedTaskForAnalysis, setSelectedTaskForAnalysis] = useState<TaskListItem | null>(null);
|
||||
const [unanalyzedTotal, setUnanalyzedTotal] = useState<number>(0); // 未分析
|
||||
const [runningTotal, setRunningTotal] = useState<number>(0); // 分析中
|
||||
const [completedTotal, setCompletedTotal] = useState<number>(0); // 已完成
|
||||
const [unanalyzedList, setUnanalyzedList] = useState<TaskListItem[]>([]); // 未分析
|
||||
const [runningList, setRunningList] = useState<TaskListItem[]>([]); // 分析中
|
||||
const [completedList, setCompletedList] = useState<TaskListItem[]>([]); // 已完成
|
||||
const [unanalyzedLoading, setUnanalyzedLoading] = useState<boolean>(false); // 未分析
|
||||
const [runningLoading, setRunningLoading] = useState<boolean>(false); // 分析中
|
||||
const [completedLoading, setCompletedLoading] = useState<boolean>(false); // 已完成
|
||||
const [presets] = useState<DateRangePickerProps['presets']>({
|
||||
最近7天: [dayjs().subtract(6, 'day').toDate(), dayjs().toDate()],
|
||||
最近3天: [dayjs().subtract(2, 'day').toDate(), dayjs().toDate()],
|
||||
今天: [dayjs().toDate(), dayjs().toDate()],
|
||||
});
|
||||
const [range, setRange] = useState<DateRangeValue>([]);
|
||||
const state = useRef<stateItem>({
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
task: {} as TaskListItem,
|
||||
})
|
||||
|
||||
// 时间日期选择器查询
|
||||
const setRangeChange = async (val) => {
|
||||
setRange(val);
|
||||
console.log('时间日期', val);
|
||||
state.current.startTime = '';
|
||||
state.current.endTime = '';
|
||||
if (val.length > 0) {
|
||||
state.current.startTime = val[0];
|
||||
state.current.endTime = val[1];
|
||||
}
|
||||
|
||||
await taskDataChange();
|
||||
}
|
||||
|
||||
// 获取任务类型配置
|
||||
const getTypeConfig = (type: TaskType) => {
|
||||
if (isDark) {
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return { label: '单智能体', color: 'bg-cyan-500/20 text-cyan-400', icon: Bot };
|
||||
case 'scene':
|
||||
return { label: '场景执行', color: 'bg-purple-500/20 text-purple-400', icon: Workflow };
|
||||
case 'consultation':
|
||||
return { label: '多智能体会商', color: 'bg-orange-500/20 text-orange-400', icon: Users };
|
||||
case 'multi-agent':
|
||||
return { label: '多智能体协同', color: 'bg-green-500/20 text-green-400', icon: Layers };
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return { label: '单智能体', color: 'bg-blue-100 text-blue-600', icon: Bot };
|
||||
case 'scene':
|
||||
return { label: '场景执行', color: 'bg-purple-100 text-purple-600', icon: Workflow };
|
||||
case 'consultation':
|
||||
return { label: '多智能体会商', color: 'bg-orange-100 text-orange-600', icon: Users };
|
||||
case 'multi-agent':
|
||||
return { label: '多智能体协同', color: 'bg-green-100 text-green-600', icon: Layers };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态配置
|
||||
const getStatusConfig = (status: TaskStatus, column: ColumnType) => {
|
||||
if (column === 'unanalyzed') {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return { label: '草稿', color: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-100 text-gray-600' };
|
||||
case 'pending':
|
||||
return { label: '待启动', color: isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-gray-100 text-gray-600' };
|
||||
default:
|
||||
return { label: '待配置', color: isDark ? 'bg-yellow-500/20 text-yellow-400' : 'bg-yellow-100 text-yellow-600' };
|
||||
}
|
||||
} else if (column === 'analyzing') {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return { label: '运行中', color: isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600' };
|
||||
case 'paused':
|
||||
return { label: '已暂停', color: isDark ? 'bg-yellow-500/20 text-yellow-400' : 'bg-yellow-100 text-yellow-600' };
|
||||
case 'blocked':
|
||||
return { label: '阻塞中', color: isDark ? 'bg-red-500/20 text-red-400' : 'bg-orange-100 text-orange-600' };
|
||||
default:
|
||||
return { label: '待处理', color: isDark ? 'bg-purple-500/20 text-purple-400' : 'bg-purple-100 text-purple-600' };
|
||||
}
|
||||
} else {
|
||||
return { label: '已完成', color: isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600' };
|
||||
}
|
||||
};
|
||||
|
||||
// 筛选任务
|
||||
const filteredTasks = tasks.filter(task => {
|
||||
const matchesSearch = task.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.id.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesMode = modeFilter === 'all' || task.type === modeFilter;
|
||||
return matchesSearch && matchesMode;
|
||||
});
|
||||
|
||||
const unanalyzedTasks = filteredTasks.filter(t => t.column === 'unanalyzed');
|
||||
const analyzingTasks = filteredTasks.filter(t => t.column === 'analyzing');
|
||||
const completedTasks = filteredTasks.filter(t => t.column === 'completed');
|
||||
|
||||
// 统计数据
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
unanalyzed: unanalyzedTasks.length,
|
||||
analyzing: analyzingTasks.length,
|
||||
completed: completedTasks.length,
|
||||
};
|
||||
|
||||
// 处理开始分析按钮点击
|
||||
const handleStartAnalysis = (task: TaskListItem) => {
|
||||
setSelectedTaskForAnalysis(task);
|
||||
state.current.task = task;
|
||||
setShowAnalysisModal(true);
|
||||
};
|
||||
|
||||
// 处理分析模式选择
|
||||
const handleSelectAnalysisMode = (mode: TaskType) => {
|
||||
setShowAnalysisModal(false);
|
||||
console.log('mode', mode);
|
||||
if (mode === 'consultation') {
|
||||
console.log('state.current.task', state.current.task)
|
||||
const sceneItem = JSON.parse(state.current.task.scene);
|
||||
if (sceneItem.taskJson) {
|
||||
sceneItem.taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...sceneItem.taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: {diverge_rounds: 3},
|
||||
task_context: state.current.task.overview
|
||||
});
|
||||
router('/qa-center/mope-chat')
|
||||
}
|
||||
console.log('sceneItem', sceneItem);
|
||||
} else if (mode === 'task-search') {
|
||||
window.open(`https://ch1.b.uat.4.cn/web/#/task/item-aicoh/step-info-search?id=${state.current.task.id}`)
|
||||
}
|
||||
};
|
||||
|
||||
const taskListSearch = async (taskType: string, pageNum: number = 1, pageSize: number = 10) => {
|
||||
let taskStatus: number[] = []
|
||||
if (taskType === 'unanalyzed') { // 未分析
|
||||
taskStatus = [0, 10];
|
||||
setUnanalyzedLoading(true);
|
||||
} else if (taskType === 'running') { // 分析中
|
||||
taskStatus = [20];
|
||||
setRunningLoading(true);
|
||||
} else if (taskType === 'completed') { // 已完成
|
||||
taskStatus = [25, 30, 40, 60, 70, 80, 90, 100, 110];
|
||||
setCompletedLoading(true);
|
||||
}
|
||||
const param = {
|
||||
content: searchQuery,
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
taskStatus: taskStatus,
|
||||
startTime: state.current.startTime,
|
||||
endTime: state.current.endTime,
|
||||
taskDirection: ''
|
||||
}
|
||||
const res = await service.post(`${zncmUrl}/task/cop-tasks`, param);
|
||||
console.log('任务列表', res);
|
||||
const result = res.data;
|
||||
result.records.forEach((item: TaskListItem) => {
|
||||
item.column = taskType;
|
||||
if (!item.taskLevel) item.taskLevel = 1;
|
||||
})
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedLoading(false);
|
||||
setUnanalyzedTotal(result.total);
|
||||
setUnanalyzedList(result.records);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningLoading(false);
|
||||
setRunningTotal(result.total);
|
||||
setRunningList(result.records);
|
||||
} else if (taskType === 'completed') {
|
||||
setCompletedLoading(false);
|
||||
setCompletedTotal(result.total);
|
||||
setCompletedList(result.records);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码变化时的回调函数
|
||||
* 当用户切换页码时触发,更新页码并重新获取数据
|
||||
* @param value - 新的页码值
|
||||
*/
|
||||
const pageIndexChange = async (type: string, pageIndex: number) => {
|
||||
// 更新 ref 中存储的页码值
|
||||
// 更新页码状态,触发组件重新渲染
|
||||
await taskListSearch(type, pageIndex);
|
||||
}
|
||||
|
||||
const taskDataChange = async (pageNum: number = 1, pageSize: number = 10) => {
|
||||
taskListSearch('unanalyzed', pageNum, pageSize);
|
||||
taskListSearch('running', pageNum, pageSize);
|
||||
taskListSearch('completed', pageNum, pageSize);
|
||||
}
|
||||
|
||||
// 回车处理
|
||||
const handleKeyDown = async (e) => {
|
||||
if ((e.metaKey || e.ctrlKey || e.shiftKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
await taskDataChange();
|
||||
} else if (e.key === "/") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
taskDataChange();
|
||||
}, []);
|
||||
|
||||
// 渲染看板列
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* 顶部工具栏 */}
|
||||
<div className={`rounded-xl p-4 flex-shrink-0 mb-4 ${cardBg} border ${cardBorder}`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600'}`}>
|
||||
{category}
|
||||
</span>
|
||||
<span className={`text-xs ${textMuted}`}>/</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-red-100 text-red-600'}`}>
|
||||
{subCategory}
|
||||
</span>
|
||||
</div>
|
||||
<h1 className={`text-xl font-bold ${textPrimary}`}>任务中心</h1>
|
||||
</div>
|
||||
<button
|
||||
className={`flex items-center gap-2 px-4 py-2 text-white rounded-lg text-sm font-medium transition-colors ${isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500'
|
||||
: 'bg-red-600 hover:bg-red-500 shadow-md'
|
||||
}`}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
新建任务
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${textMuted}`} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索任务名称"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e)}
|
||||
className={`w-full pl-10 pr-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 transition-colors ${inputBg} ${cardBorder} ${textPrimary} placeholder:${textMuted}`}
|
||||
/>
|
||||
</div>
|
||||
{/* <select
|
||||
value={modeFilter}
|
||||
onChange={(e) => setModeFilter(e.target.value)}
|
||||
className={`px-4 py-2 border rounded-lg text-sm focus:outline-none cursor-pointer ${inputBg} ${cardBorder} ${textSecondary}`}
|
||||
>
|
||||
<option value="all">全部模式</option>
|
||||
<option value="single">单智能体</option>
|
||||
<option value="multi-agent">多智能体协同</option>
|
||||
<option value="consultation">多智能体会商</option>
|
||||
<option value="scene">场景执行</option>
|
||||
</select> */}
|
||||
{/* <button className={`flex items-center gap-2 px-4 py-2 border rounded-lg text-sm transition-colors ${inputBg} ${cardBorder} ${textSecondary}`}>
|
||||
<Clock className="w-4 h-4" />
|
||||
时间范围
|
||||
</button> */}
|
||||
<DateRangePicker value={range} presets={presets} onChange={(val) => setRangeChange(val)} enableTimePicker={true} clearable />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计概览栏 */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-4 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => { setModeFilter('all'); }}
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${textPrimary}`}>{unanalyzedTotal + runningTotal + completedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>全部任务</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${textPrimary}`}>{unanalyzedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>未分析</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${isDark ? 'text-blue-400' : 'text-red-600'}`}>{runningTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>分析中</p>
|
||||
</button>
|
||||
<button
|
||||
className={`p-4 rounded-xl border transition-all ${cardBg} ${cardBorder} hover:border-red-400`}
|
||||
>
|
||||
<p className={`text-2xl font-bold ${isDark ? 'text-green-400' : 'text-green-600'}`}>{completedTotal}</p>
|
||||
<p className={`text-sm ${textSecondary}`}>已完成</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 看板区 */}
|
||||
<div className="flex-1 grid grid-cols-3 gap-4 min-h-0 overflow-hidden">
|
||||
<Loading loading={unanalyzedLoading}>
|
||||
<KanbanColumn title="未分析" taskType={"unanalyzed"} count={unanalyzedTotal} color={isDark ? 'bg-yellow-400' : 'bg-yellow-400'} tasks={unanalyzedList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis}/>
|
||||
</Loading>
|
||||
<Loading loading={runningLoading}>
|
||||
<KanbanColumn title="分析中" taskType="running" count={runningTotal} color={isDark ? 'bg-blue-400' : 'bg-red-400'} tasks={runningList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis}/>
|
||||
</Loading>
|
||||
<Loading loading={completedLoading}>
|
||||
<KanbanColumn title="已完成" taskType="completed" count={completedTotal} color={isDark ? 'bg-green-400' : 'bg-green-400'} tasks={completedList} pageIndexSearch={pageIndexChange} handleStartAnalysis={handleStartAnalysis}/>
|
||||
</Loading>
|
||||
</div>
|
||||
|
||||
{/* 选择分析模式弹窗 */}
|
||||
{showAnalysisModal && selectedTaskForAnalysis && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowAnalysisModal(false)}>
|
||||
<div
|
||||
className={`w-full max-w-lg rounded-2xl p-6 ${cardBg} border ${cardBorder} shadow-xl`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className={`text-xl font-bold ${textPrimary}`}>选择分析模式</h2>
|
||||
<p className={`text-sm ${textMuted} mt-1`}>为任务「{selectedTaskForAnalysis.taskName}」选择分析方式</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowAnalysisModal(false)}
|
||||
className={`p-2 rounded-lg hover:bg-slate-700/50 ${textSecondary}`}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* 单智能体分析 */}
|
||||
{/* <button
|
||||
onClick={() => handleSelectAnalysisMode('single')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-cyan-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-cyan-500/20' : 'bg-red-100'
|
||||
}`}>
|
||||
<Bot className={`w-6 h-6 ${isDark ? 'text-cyan-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>单智能体分析</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>选择一个AI智能体完成单一维度的专项分析</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button> */}
|
||||
|
||||
{/* 多智能体协同分析 */}
|
||||
{/* <button
|
||||
onClick={() => handleSelectAnalysisMode('multi-agent')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-green-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-green-500/20' : 'bg-green-100'
|
||||
}`}>
|
||||
<Layers className={`w-6 h-6 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>多智能体协同分析</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>多个AI智能体并行分析,输出各自的判断和结论</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button> */}
|
||||
<button
|
||||
onClick={() => handleSelectAnalysisMode('task-search')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-green-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-green-500/20' : 'bg-green-100'
|
||||
}`}>
|
||||
<Layers className={`w-6 h-6 ${isDark ? 'text-green-400' : 'text-green-600'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>查看任务详情</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>进入b1系统查看任务详情</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 多智能体会商 */}
|
||||
<button
|
||||
onClick={() => handleSelectAnalysisMode('consultation')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-orange-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-orange-500/20' : 'bg-red-100'
|
||||
}`}>
|
||||
<Users className={`w-6 h-6 ${isDark ? 'text-orange-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>多智能体会商</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>多个AI智能体从不同视角开展讨论并形成共识</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* 场景执行 */}
|
||||
{/* <button
|
||||
onClick={() => handleSelectAnalysisMode('scene')}
|
||||
className={`w-full p-4 rounded-xl border transition-all duration-200 text-left hover:shadow-md ${isDark
|
||||
? 'bg-slate-700/50 border-blue-800/30 hover:border-purple-500/50'
|
||||
: 'bg-white border-red-100 hover:border-red-300 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${isDark ? 'bg-purple-500/20' : 'bg-red-100'
|
||||
}`}>
|
||||
<Workflow className={`w-6 h-6 ${isDark ? 'text-purple-400' : 'text-red-500'}`} />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold ${textPrimary}`}>场景执行</h3>
|
||||
<p className={`text-sm ${textSecondary}`}>按照预设场景模板自动执行多阶段策略分析</p>
|
||||
</div>
|
||||
<ArrowRight className={`w-5 h-5 ${textMuted}`} />
|
||||
</div>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskAnalyzes;
|
||||
@@ -0,0 +1,303 @@
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { Bot, ChevronRight, Download, Edit, Eye, FileText, Layers, MoreHorizontal, Play, User, Users, Workflow } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
|
||||
interface TaskCardProps {
|
||||
TaskType?: string;
|
||||
task: TaskListItem;
|
||||
handleStartAnalysis: (task: TaskListItem) => void;
|
||||
}
|
||||
|
||||
export const TaskCard: React.FC<TaskCardProps> = ({ task, TaskType = 'task', handleStartAnalysis }) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const isDark = theme === 'dark';
|
||||
const textPrimary = isDark ? 'text-white' : 'text-gray-900';
|
||||
const textMuted = isDark ? 'text-slate-500' : 'text-gray-400';
|
||||
const textSecondary = isDark ? 'text-slate-400' : 'text-gray-600';
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleStartChange = (task: TaskListItem) => {
|
||||
// if (TaskType === 'task') {
|
||||
// handleStartAnalysis(task);
|
||||
// } else {
|
||||
// sceneDataChange(task);
|
||||
// }
|
||||
handleStartAnalysis(task);
|
||||
}
|
||||
|
||||
const sceneDataChange = (task: TaskListItem) => {
|
||||
if (!task.scene) return;
|
||||
const sceneItem = JSON.parse(task.scene);
|
||||
if (sceneItem.taskJson) {
|
||||
sceneItem.taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...sceneItem.taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: task.overview
|
||||
});
|
||||
navigate('/qa-center/mope-chat')
|
||||
}
|
||||
console.log('sceneItem', sceneItem);
|
||||
}
|
||||
// 处理卡片点击
|
||||
const handleTaskClick = (task: TaskListItem) => {
|
||||
if (TaskType === 'sentiment') {
|
||||
console.log('点击了', task)
|
||||
sceneDataChange(task);
|
||||
// window.open(`http://ch1.b.uat.4.cn/taskcop/#/situation-overview/action/sentiment?taskId=${task.id}`)
|
||||
} else {
|
||||
if (task.column === 'completed') {
|
||||
// if (task.type === 'consultation') {
|
||||
// onNavigate('task-consult-result');
|
||||
// } else {
|
||||
// onNavigate('strategy-result');
|
||||
// }
|
||||
navigate('/strategy-result')
|
||||
} else if (task.column === 'running') {
|
||||
// navigate('/task-management/task-consult')
|
||||
sceneDataChange(task);
|
||||
// window.open(`http://ch1.b.uat.4.cn/mopeweb/#/mope?taskOverview=${task.overview}`)
|
||||
// if (task.type === 'scene') {
|
||||
// onNavigate('task-scene');
|
||||
// } else if (task.type === 'consultation') {
|
||||
// onNavigate('task-consult');
|
||||
// } else {
|
||||
// onNavigate('task-single');
|
||||
// }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取任务类型配置
|
||||
const getTypeConfig = (type: string) => {
|
||||
if (isDark) {
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return { label: '单智能体', color: 'bg-cyan-500/20 text-cyan-400', icon: Bot };
|
||||
case 'scene':
|
||||
return { label: '场景执行', color: 'bg-purple-500/20 text-purple-400', icon: Workflow };
|
||||
case 'consultation':
|
||||
return { label: '多智能体会商', color: 'bg-orange-500/20 text-orange-400', icon: Users };
|
||||
case 'multi-agent':
|
||||
return { label: '多智能体协同', color: 'bg-green-500/20 text-green-400', icon: Layers };
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return { label: '单智能体', color: 'bg-blue-100 text-blue-600', icon: Bot };
|
||||
case 'scene':
|
||||
return { label: '场景执行', color: 'bg-purple-100 text-purple-600', icon: Workflow };
|
||||
case 'consultation':
|
||||
return { label: '多智能体会商', color: 'bg-orange-100 text-orange-600', icon: Users };
|
||||
case 'multi-agent':
|
||||
return { label: '多智能体协同', color: 'bg-green-100 text-green-600', icon: Layers };
|
||||
}
|
||||
}
|
||||
};
|
||||
const typeConfig = getTypeConfig('single');
|
||||
// 获取任务等级
|
||||
const getLevel = (type: number) => {
|
||||
const fallback = { label: '一般', color: isDark ? 'bg-cyan-500/20 text-cyan-400' : 'bg-blue-100 text-blue-600', icon: Bot };
|
||||
if (isDark) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return { label: '一般', color: 'bg-cyan-500/20 text-cyan-400', icon: Bot };
|
||||
case 2:
|
||||
return { label: '紧急', color: 'bg-purple-500/20 text-purple-400', icon: Workflow };
|
||||
case 3:
|
||||
return { label: '加急', color: 'bg-orange-500/20 text-orange-400', icon: Users };
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return { label: '一般', color: 'bg-blue-100 text-blue-600', icon: Bot };
|
||||
case 2:
|
||||
return { label: '紧急', color: 'bg-purple-100 text-purple-600', icon: Workflow };
|
||||
case 3:
|
||||
return { label: '加急', color: 'bg-orange-100 text-orange-600', icon: Users };
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const levelConfig = getLevel(task.taskLevel);
|
||||
const LevelIcon = levelConfig.icon;
|
||||
// 获取状态配置
|
||||
const getStatusConfig = (column: string) => {
|
||||
if (column === 'unanalyzed') {
|
||||
return { label: '待分析', color: isDark ? 'bg-yellow-500/20 text-yellow-400' : 'bg-yellow-100 text-yellow-600' };
|
||||
} else if (column === 'running') {
|
||||
return { label: '运行中', color: isDark ? 'bg-blue-500/20 text-blue-400' : 'bg-red-100 text-red-600' };
|
||||
} else {
|
||||
return { label: '已完成', color: isDark ? 'bg-green-500/20 text-green-400' : 'bg-green-100 text-green-600' };
|
||||
}
|
||||
};
|
||||
const statusConfig = getStatusConfig(task.column);
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl p-4 hover:shadow-md transition-all duration-200 cursor-pointer group theme-card border ${isDark
|
||||
? 'bg-slate-800/40 backdrop-blur-xl border-blue-800/30 hover:border-blue-500/50'
|
||||
: 'bg-white border-red-100 hover:shadow-md shadow-red-100'
|
||||
}`}
|
||||
onClick={() => handleTaskClick(task)}
|
||||
>
|
||||
{/* 卡片头部:模式标签 + 状态标签 */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium ${levelConfig.color}`}>
|
||||
{LevelIcon && <LevelIcon className="w-3 h-3" />}
|
||||
{levelConfig.label}
|
||||
</span>
|
||||
<span></span>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${statusConfig.color}`}>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 任务名称 */}
|
||||
<h3 className={`font-semibold text-sm mb-1 line-clamp-2 group-hover:text-red-500 transition-colors ${textPrimary}`}>
|
||||
{task.taskName}
|
||||
</h3>
|
||||
|
||||
{/* 任务编号 */}
|
||||
<p className={`text-xs mb-2 ${textMuted}`}>TASK-{task.id}</p>
|
||||
|
||||
{/* 任务摘要 */}
|
||||
<p className={`text-xs mb-3 line-clamp-2 ${textSecondary}`}>{task.overview}</p>
|
||||
|
||||
{/* 补充信息区 */}
|
||||
{/* <div className="space-y-1 mb-3">
|
||||
{task.type === 'single' && task.agent && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Bot className={`w-3 h-3 ${isDark ? 'text-cyan-400' : 'text-red-500'}`} />
|
||||
<span className={textSecondary}>执行智能体:</span>
|
||||
<span className={isDark ? 'text-cyan-400' : 'text-red-600'}>{task.agent}</span>
|
||||
</div>
|
||||
)}
|
||||
{task.type === 'scene' && task.scene && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Workflow className={`w-3 h-3 ${isDark ? 'text-purple-400' : 'text-red-500'}`} />
|
||||
<span className={textSecondary}>关联场景:</span>
|
||||
<span className={isDark ? 'text-purple-400' : 'text-red-600'}>{task.scene}</span>
|
||||
</div>
|
||||
)}
|
||||
{task.type === 'consultation' && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Users className={`w-3 h-3 ${isDark ? 'text-orange-400' : 'text-red-500'}`} />
|
||||
<span className={textSecondary}>参与智能体:</span>
|
||||
<span className={isDark ? 'text-orange-400' : 'text-red-600'}>{task.participants?.length || 0}个</span>
|
||||
</div>
|
||||
{task.currentRound && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Clock className={`w-3 h-3 ${textMuted}`} />
|
||||
<span className={textSecondary}>当前轮次:</span>
|
||||
<span className={textPrimary}>第{task.currentRound}轮</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{task.column === 'analyzing' && task.currentPhase && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Target className={`w-3 h-3 ${textMuted}`} />
|
||||
<span className={textSecondary}>当前阶段:</span>
|
||||
<span className={textPrimary}>{task.currentPhase}</span>
|
||||
</div>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{/* 时间信息 */}
|
||||
<div className={`flex items-center justify-between text-xs pt-3 border-t ${isDark ? 'border-blue-800/30 text-slate-500' : 'border-red-100 text-gray-400'
|
||||
}`}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<User className="w-3 h-3" />
|
||||
<span>{task.taskDirection}</span>
|
||||
</div>
|
||||
<span>{format(new Date(task.updateTime), 'yyyy-MM-dd HH:mm:ss')}</span>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区 */}
|
||||
<div className={`flex items-center gap-2 mt-3 pt-3 border-t ${isDark ? 'border-blue-800/30' : 'border-red-100'
|
||||
}`}>
|
||||
{task.column === 'unanalyzed' && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleStartChange(task); }}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-white rounded-lg text-xs font-medium transition-colors ${isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500'
|
||||
: 'bg-red-600 hover:bg-red-500 shadow-md'
|
||||
}`}
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
开始分析
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={(e) => { e.stopPropagation(); onNavigate('task-create'); }}
|
||||
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
|
||||
}`}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-red-400'}`} />
|
||||
</button> */}
|
||||
</>
|
||||
)}
|
||||
{task.column === 'running' && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleStartChange(task); }}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-white rounded-lg text-xs font-medium transition-colors ${isDark
|
||||
? 'bg-blue-600 hover:bg-blue-500'
|
||||
: 'bg-red-600 hover:bg-red-500 shadow-md'
|
||||
}`}
|
||||
>
|
||||
<Play className="w-3 h-3" />
|
||||
开始分析
|
||||
</button>
|
||||
{/* <button
|
||||
onClick={(e) => { e.stopPropagation(); handleStartChange(task); }}
|
||||
className={`px-2 py-1.5 rounded-lg text-xs transition-colors ${isDark
|
||||
? 'hover:bg-slate-700/50 border border-blue-800/30 text-slate-300'
|
||||
: 'hover:bg-red-50 border border-red-200 text-red-600'
|
||||
}`}
|
||||
>
|
||||
查看进展
|
||||
</button> */}
|
||||
</>
|
||||
)}
|
||||
{task.column === 'completed' && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleStartChange(task); }}
|
||||
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-white rounded-lg text-xs font-medium transition-colors ${isDark
|
||||
? 'bg-green-600 hover:bg-green-500'
|
||||
: 'bg-green-600 hover:bg-green-500 shadow-md'
|
||||
}`}
|
||||
>
|
||||
<Eye className="w-3 h-3" />
|
||||
开始分析
|
||||
</button>
|
||||
{/* <button className={`px-2 py-1.5 rounded-lg text-xs transition-colors ${isDark
|
||||
? 'hover:bg-slate-700/50 border border-blue-800/30 text-slate-300'
|
||||
: 'hover:bg-red-50 border border-red-200 text-red-600'
|
||||
}`}>
|
||||
<Download className="w-4 h-4" />
|
||||
</button> */}
|
||||
</>
|
||||
)}
|
||||
{/* <button className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-700/50' : 'hover:bg-red-50'
|
||||
}`} title="更多">
|
||||
<MoreHorizontal className={`w-4 h-4 ${isDark ? 'text-slate-400' : 'text-red-400'}`} />
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import './css/strategy-data.css';
|
||||
export interface SceneInfo {
|
||||
applicableTask: string;
|
||||
createTime: string;
|
||||
@@ -159,7 +160,7 @@ export const taskStatusList = {
|
||||
};
|
||||
export default function TaskManagement() {
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="strategy-data h-full flex flex-col">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Outlet, useNavigate } from 'react-router-dom';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
const pgLinkUrl = import.meta.env.VITE_AIPI_PGLINKTO_URL;
|
||||
interface TaskPgFromProps {
|
||||
gopath: string
|
||||
}
|
||||
const TaskPgFrom: React.FC<TaskPgFromProps> = ({gopath}) => {
|
||||
const [pgUrl, setPgUrl] = useState(pgLinkUrl);
|
||||
useEffect(() => {
|
||||
if(userInfo) {
|
||||
const userData = JSON.parse(userInfo);
|
||||
setPgUrl(`${pgUrl}?access_token=${userData.accessToken}&goPath=${gopath}`);
|
||||
}
|
||||
}, [gopath]);
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<iframe className="h-[calc(100vh-92px)]" src={pgUrl}></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default TaskPgFrom
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "./css/iframe.css";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
const TYURL = import.meta.env.VITE_AIPI_B2ACTOR_URL || "";
|
||||
|
||||
const XXTyPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
useEffect(() => {
|
||||
initIframeUrl();
|
||||
}, []);
|
||||
const initIframeUrl = () => {
|
||||
const url = `${TYURL}/fromB/toPage?defaultPage=iframeSysystem`;
|
||||
console.log({ iframeUrl: url });
|
||||
setIframeUrl(url);
|
||||
};
|
||||
return (
|
||||
<div className="iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default XXTyPage;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import "./css/iframe.css";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
|
||||
const TYURL = import.meta.env.VITE_AIPI_B2ACTOR_URL || "";
|
||||
|
||||
const XingdongTyPage: React.FC = () => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [iframeUrl, setIframeUrl] = useState("");
|
||||
useEffect(() => {
|
||||
initIframeUrl();
|
||||
}, []);
|
||||
const initIframeUrl = () => {
|
||||
const url = `${TYURL}/fromB/toPage?defaultPage=iframeXdty`;
|
||||
console.log({ iframeUrl: url });
|
||||
setIframeUrl(url);
|
||||
};
|
||||
return (
|
||||
<div className="iframe-container">
|
||||
<iframe src={iframeUrl} className="full-size-iframe"></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default XingdongTyPage;
|
||||
@@ -0,0 +1,10 @@
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px); /* 可根据需求调整 */
|
||||
}
|
||||
|
||||
.full-size-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Strategy task components styles migrated from web-code/index.css
|
||||
* Used by TaskAnalyzes, SentimentAnalyzes, ActionAnalyzes and related children.
|
||||
*/
|
||||
|
||||
/* ── CSS variables (light) ─────────────────────────────────────────────── */
|
||||
.strategy-data {
|
||||
--color-main: #cf1322;
|
||||
--td-brand-color: #ce2d39;
|
||||
--bg-color: #fef6f6;
|
||||
--card-border: #fa9196;
|
||||
--text-color: #222222;
|
||||
|
||||
--card-bg: 0 0% 100%;
|
||||
--card-hover: 0 86% 97%;
|
||||
--border-color: 0 76% 89%;
|
||||
--input-bg: 0 0% 100%;
|
||||
--input-border: 0 76% 89%;
|
||||
}
|
||||
|
||||
/* ── CSS variables (dark) ────────────────────────────────────────────── */
|
||||
.dark .strategy-data {
|
||||
--color-main: #0062d9;
|
||||
--td-brand-color: #ce2d39;
|
||||
--bg-color: #0062d966;
|
||||
--card-border: #fa9196;
|
||||
--text-color: #ffffff;
|
||||
|
||||
--card-bg: 217.2 32.6% 17.5%;
|
||||
--card-hover: 215 20.2% 25%;
|
||||
--border-color: 217.2 32.6% 17.5%;
|
||||
--input-bg: 217.2 32.6% 17.5%;
|
||||
--input-border: 217.2 32.6% 17.5%;
|
||||
}
|
||||
|
||||
/* ── Theme card (TaskCard, KanbanColumn) ─────────────────────────────── */
|
||||
.strategy-data .theme-card {
|
||||
background-color: hsl(var(--card-bg));
|
||||
}
|
||||
|
||||
.strategy-data .theme-card-hover:hover {
|
||||
background-color: hsl(var(--card-hover));
|
||||
}
|
||||
|
||||
.strategy-data .theme-transition {
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease,
|
||||
border-color 0.3s ease,
|
||||
box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Scrollbar (KanbanColumn) ────────────────────────────────────────── */
|
||||
.strategy-data .scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #ddd #f1f1f1;
|
||||
}
|
||||
|
||||
.strategy-data .scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.strategy-data .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: #ddd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.strategy-data .scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.strategy-data .scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.dark .strategy-data .scrollbar-thin {
|
||||
scrollbar-color: rgba(148, 163, 184, 0.45) transparent;
|
||||
}
|
||||
|
||||
.dark .strategy-data .scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.45);
|
||||
}
|
||||
|
||||
.dark .strategy-data .scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(148, 163, 184, 0.65);
|
||||
}
|
||||
|
||||
.dark .strategy-data .scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ── ActionAnalyzes task sidebar ─────────────────────────────────────── */
|
||||
.strategy-data .task-item-active:hover {
|
||||
border: 1px solid var(--color-main);
|
||||
}
|
||||
|
||||
.strategy-data .task-item-active:hover .item-active {
|
||||
background: var(--color-main);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.strategy-data .dark-theme:hover {
|
||||
background: #0062d966;
|
||||
color: #ffffff;
|
||||
border-color: #0062d9 !important;
|
||||
box-shadow: 0 3px 4px #8b8b8b0f;
|
||||
}
|
||||
|
||||
.strategy-data .action-item:hover {
|
||||
border-color: var(--card-border);
|
||||
}
|
||||
|
||||
.strategy-data .dark-theme.task-active,
|
||||
.strategy-data .dark-theme .task-active {
|
||||
border-color: #0062d9 !important;
|
||||
}
|
||||
|
||||
.strategy-data .task-active {
|
||||
background-color: var(--bg-color);
|
||||
border: 1px solid var(--color-main);
|
||||
}
|
||||
|
||||
.strategy-data .task-active .item-active {
|
||||
background: var(--color-main);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.strategy-data .task-tab-color {
|
||||
color: var(--color-main);
|
||||
}
|
||||
|
||||
.dark .strategy-data .dark-theme .task-tab-color {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.dark .strategy-data .dark-theme .t-progress__bar {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* ── ActionColumn kanban backgrounds ─────────────────────────────────── */
|
||||
.strategy-data .action-bg0 {
|
||||
background: linear-gradient(180deg, #f2f9ff, #fff 17.89% 100%);
|
||||
}
|
||||
|
||||
.strategy-data .action-bg1 {
|
||||
background: linear-gradient(180deg, #fffaed, #fff 17.89% 100%);
|
||||
}
|
||||
|
||||
.strategy-data .action-bg2 {
|
||||
background: linear-gradient(180deg, #f2fff9, #fff 17.89% 100%);
|
||||
}
|
||||
|
||||
.strategy-data .action-bg3 {
|
||||
background: linear-gradient(180deg, #f5f5f5, #fff 17.89% 100%);
|
||||
}
|
||||
|
||||
.strategy-data .dark-action-bg {
|
||||
background: rgba(3, 38, 80, 0.6) !important;
|
||||
border: 1px solid rgba(43, 143, 254, 0.6) !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 1px 16px rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.strategy-data .dark-action-item {
|
||||
background: #0062d91a;
|
||||
border: 1px solid #2b8ffe99;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.strategy-data .dark-action-item:hover {
|
||||
background: #0062d966;
|
||||
border-color: #0062d9;
|
||||
}
|
||||
|
||||
/* ── TDesign overrides inside task pages ─────────────────────────────── */
|
||||
.strategy-data .t-loading,
|
||||
.strategy-data .t-loading__parent {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.strategy-data .t-loading--full {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.strategy-data .t-loading__overlay {
|
||||
background: rgb(255 255 255 / 60%);
|
||||
}
|
||||
|
||||
.dark .strategy-data .t-loading__overlay {
|
||||
background: rgb(15 23 42 / 45%);
|
||||
}
|
||||
|
||||
.strategy-data .t-pagination {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.strategy-data .t-date-range-picker,
|
||||
.strategy-data .t-date-picker,
|
||||
.strategy-data .t-input {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.strategy-data .t-input__wrap,
|
||||
.strategy-data .t-range-input {
|
||||
background: var(--td-bg-color-container, #fff);
|
||||
}
|
||||
|
||||
.dark .strategy-data .t-input__wrap,
|
||||
.dark .strategy-data .t-range-input {
|
||||
background: var(--td-bg-color-container, #1e293b);
|
||||
color: var(--td-text-color-primary, #e2e8f0);
|
||||
}
|
||||
|
||||
.strategy-data .t-row,
|
||||
.strategy-data .t-col {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Popups are portaled to body — keep panels above app chrome */
|
||||
.t-popup,
|
||||
.t-date-picker__panel-container,
|
||||
.t-date-range-picker__panel-container {
|
||||
z-index: 6500;
|
||||
}
|
||||
|
||||
/* ── iframe embed pages (resource management, TY tasks, etc.) ─────────── */
|
||||
.strategy-data .iframe-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.strategy-data .full-size-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import axios from 'axios'
|
||||
// axios.interceptors
|
||||
const service = axios.create({
|
||||
// baseURL: '', // url = base url + request url
|
||||
// baseURL: '/api',
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 500000000*1000 // request timeout
|
||||
})
|
||||
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
// if(!sessionStorage.getItem('userName')){
|
||||
// // window.location = '/#/lagchain-login'
|
||||
// }
|
||||
if (res.code !== 0) {
|
||||
return res
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if(!error.response) return Promise.reject(error)
|
||||
if (error.response.status == 401) {
|
||||
// window.location = sessionStorage.getItem('mhUrl');
|
||||
} else {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// let token: any = localStorage.getItem('token');
|
||||
config.headers['Authorization'] = `Bearer admin`;
|
||||
// if (token) {
|
||||
// token = JSON.parse(token);
|
||||
// // 在请求头中添加 token
|
||||
// config.headers['Authorization'] = `Bearer ${token.token}`;
|
||||
// }
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
@@ -0,0 +1,46 @@
|
||||
import axios from 'axios'
|
||||
// axios.interceptors
|
||||
const toolsService = axios.create({
|
||||
// baseURL: '', // url = base url + request url
|
||||
// baseURL: '/api',
|
||||
// withCredentials: true, // send cookies when cross-domain requests
|
||||
timeout: 500000000*1000 // request timeout
|
||||
})
|
||||
|
||||
toolsService.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
// if(!sessionStorage.getItem('userName')){
|
||||
// // window.location = '/#/lagchain-login'
|
||||
// }
|
||||
if (res.code !== 0) {
|
||||
return res
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if(!error.response) return Promise.reject(error)
|
||||
if (error.response.status == 401) {
|
||||
// window.location = sessionStorage.getItem('mhUrl');
|
||||
} else {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
toolsService.interceptors.request.use(
|
||||
config => {
|
||||
const token: any = localStorage.getItem('toolToken');
|
||||
if (token) {
|
||||
// 在请求头中添加 token
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default toolsService
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"name": "deerflow-server",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
|
||||
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
agent-base
|
||||
==========
|
||||
### Turn a function into an [`http.Agent`][http.Agent] instance
|
||||
[](https://github.com/TooTallNate/node-agent-base/actions?workflow=Node+CI)
|
||||
|
||||
This module provides an `http.Agent` generator. That is, you pass it an async
|
||||
callback function, and it returns a new `http.Agent` instance that will invoke the
|
||||
given callback function when sending outbound HTTP requests.
|
||||
|
||||
#### Some subclasses:
|
||||
|
||||
Here's some more interesting uses of `agent-base`.
|
||||
Send a pull request to list yours!
|
||||
|
||||
* [`http-proxy-agent`][http-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTP endpoints
|
||||
* [`https-proxy-agent`][https-proxy-agent]: An HTTP(s) proxy `http.Agent` implementation for HTTPS endpoints
|
||||
* [`pac-proxy-agent`][pac-proxy-agent]: A PAC file proxy `http.Agent` implementation for HTTP and HTTPS
|
||||
* [`socks-proxy-agent`][socks-proxy-agent]: A SOCKS proxy `http.Agent` implementation for HTTP and HTTPS
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install with `npm`:
|
||||
|
||||
``` bash
|
||||
$ npm install agent-base
|
||||
```
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Here's a minimal example that creates a new `net.Socket` connection to the server
|
||||
for every HTTP request (i.e. the equivalent of `agent: false` option):
|
||||
|
||||
```js
|
||||
var net = require('net');
|
||||
var tls = require('tls');
|
||||
var url = require('url');
|
||||
var http = require('http');
|
||||
var agent = require('agent-base');
|
||||
|
||||
var endpoint = 'http://nodejs.org/api/';
|
||||
var parsed = url.parse(endpoint);
|
||||
|
||||
// This is the important part!
|
||||
parsed.agent = agent(function (req, opts) {
|
||||
var socket;
|
||||
// `secureEndpoint` is true when using the https module
|
||||
if (opts.secureEndpoint) {
|
||||
socket = tls.connect(opts);
|
||||
} else {
|
||||
socket = net.connect(opts);
|
||||
}
|
||||
return socket;
|
||||
});
|
||||
|
||||
// Everything else works just like normal...
|
||||
http.get(parsed, function (res) {
|
||||
console.log('"response" event!', res.headers);
|
||||
res.pipe(process.stdout);
|
||||
});
|
||||
```
|
||||
|
||||
Returning a Promise or using an `async` function is also supported:
|
||||
|
||||
```js
|
||||
agent(async function (req, opts) {
|
||||
await sleep(1000);
|
||||
// etc…
|
||||
});
|
||||
```
|
||||
|
||||
Return another `http.Agent` instance to "pass through" the responsibility
|
||||
for that HTTP request to that agent:
|
||||
|
||||
```js
|
||||
agent(function (req, opts) {
|
||||
return opts.secureEndpoint ? https.globalAgent : http.globalAgent;
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
## Agent(Function callback[, Object options]) → [http.Agent][]
|
||||
|
||||
Creates a base `http.Agent` that will execute the callback function `callback`
|
||||
for every HTTP request that it is used as the `agent` for. The callback function
|
||||
is responsible for creating a `stream.Duplex` instance of some kind that will be
|
||||
used as the underlying socket in the HTTP request.
|
||||
|
||||
The `options` object accepts the following properties:
|
||||
|
||||
* `timeout` - Number - Timeout for the `callback()` function in milliseconds. Defaults to Infinity (optional).
|
||||
|
||||
The callback function should have the following signature:
|
||||
|
||||
### callback(http.ClientRequest req, Object options, Function cb) → undefined
|
||||
|
||||
The ClientRequest `req` can be accessed to read request headers and
|
||||
and the path, etc. The `options` object contains the options passed
|
||||
to the `http.request()`/`https.request()` function call, and is formatted
|
||||
to be directly passed to `net.connect()`/`tls.connect()`, or however
|
||||
else you want a Socket to be created. Pass the created socket to
|
||||
the callback function `cb` once created, and the HTTP request will
|
||||
continue to proceed.
|
||||
|
||||
If the `https` module is used to invoke the HTTP request, then the
|
||||
`secureEndpoint` property on `options` _will be set to `true`_.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Nathan Rajlich <nathan@tootallnate.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
[http-proxy-agent]: https://github.com/TooTallNate/node-http-proxy-agent
|
||||
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
|
||||
[pac-proxy-agent]: https://github.com/TooTallNate/node-pac-proxy-agent
|
||||
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
|
||||
[http.Agent]: https://nodejs.org/api/http.html#http_class_http_agent
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/// <reference types="node" />
|
||||
import net from 'net';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { Duplex } from 'stream';
|
||||
import { EventEmitter } from 'events';
|
||||
declare function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
|
||||
declare function createAgent(callback: createAgent.AgentCallback, opts?: createAgent.AgentOptions): createAgent.Agent;
|
||||
declare namespace createAgent {
|
||||
interface ClientRequest extends http.ClientRequest {
|
||||
_last?: boolean;
|
||||
_hadError?: boolean;
|
||||
method: string;
|
||||
}
|
||||
interface AgentRequestOptions {
|
||||
host?: string;
|
||||
path?: string;
|
||||
port: number;
|
||||
}
|
||||
interface HttpRequestOptions extends AgentRequestOptions, Omit<http.RequestOptions, keyof AgentRequestOptions> {
|
||||
secureEndpoint: false;
|
||||
}
|
||||
interface HttpsRequestOptions extends AgentRequestOptions, Omit<https.RequestOptions, keyof AgentRequestOptions> {
|
||||
secureEndpoint: true;
|
||||
}
|
||||
type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
|
||||
type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
|
||||
type AgentCallbackReturn = Duplex | AgentLike;
|
||||
type AgentCallbackCallback = (err?: Error | null, socket?: createAgent.AgentCallbackReturn) => void;
|
||||
type AgentCallbackPromise = (req: createAgent.ClientRequest, opts: createAgent.RequestOptions) => createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
|
||||
type AgentCallback = typeof Agent.prototype.callback;
|
||||
type AgentOptions = {
|
||||
timeout?: number;
|
||||
};
|
||||
/**
|
||||
* Base `http.Agent` implementation.
|
||||
* No pooling/keep-alive is implemented by default.
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @api public
|
||||
*/
|
||||
class Agent extends EventEmitter {
|
||||
timeout: number | null;
|
||||
maxFreeSockets: number;
|
||||
maxTotalSockets: number;
|
||||
maxSockets: number;
|
||||
sockets: {
|
||||
[key: string]: net.Socket[];
|
||||
};
|
||||
freeSockets: {
|
||||
[key: string]: net.Socket[];
|
||||
};
|
||||
requests: {
|
||||
[key: string]: http.IncomingMessage[];
|
||||
};
|
||||
options: https.AgentOptions;
|
||||
private promisifiedCallback?;
|
||||
private explicitDefaultPort?;
|
||||
private explicitProtocol?;
|
||||
constructor(callback?: createAgent.AgentCallback | createAgent.AgentOptions, _opts?: createAgent.AgentOptions);
|
||||
get defaultPort(): number;
|
||||
set defaultPort(v: number);
|
||||
get protocol(): string;
|
||||
set protocol(v: string);
|
||||
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions, fn: createAgent.AgentCallbackCallback): void;
|
||||
callback(req: createAgent.ClientRequest, opts: createAgent.RequestOptions): createAgent.AgentCallbackReturn | Promise<createAgent.AgentCallbackReturn>;
|
||||
/**
|
||||
* Called by node-core's "_http_client.js" module when creating
|
||||
* a new HTTP request with this Agent instance.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
addRequest(req: ClientRequest, _opts: RequestOptions): void;
|
||||
freeSocket(socket: net.Socket, opts: AgentOptions): void;
|
||||
destroy(): void;
|
||||
}
|
||||
}
|
||||
export = createAgent;
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
const events_1 = require("events");
|
||||
const debug_1 = __importDefault(require("debug"));
|
||||
const promisify_1 = __importDefault(require("./promisify"));
|
||||
const debug = debug_1.default('agent-base');
|
||||
function isAgent(v) {
|
||||
return Boolean(v) && typeof v.addRequest === 'function';
|
||||
}
|
||||
function isSecureEndpoint() {
|
||||
const { stack } = new Error();
|
||||
if (typeof stack !== 'string')
|
||||
return false;
|
||||
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
|
||||
}
|
||||
function createAgent(callback, opts) {
|
||||
return new createAgent.Agent(callback, opts);
|
||||
}
|
||||
(function (createAgent) {
|
||||
/**
|
||||
* Base `http.Agent` implementation.
|
||||
* No pooling/keep-alive is implemented by default.
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @api public
|
||||
*/
|
||||
class Agent extends events_1.EventEmitter {
|
||||
constructor(callback, _opts) {
|
||||
super();
|
||||
let opts = _opts;
|
||||
if (typeof callback === 'function') {
|
||||
this.callback = callback;
|
||||
}
|
||||
else if (callback) {
|
||||
opts = callback;
|
||||
}
|
||||
// Timeout for the socket to be returned from the callback
|
||||
this.timeout = null;
|
||||
if (opts && typeof opts.timeout === 'number') {
|
||||
this.timeout = opts.timeout;
|
||||
}
|
||||
// These aren't actually used by `agent-base`, but are required
|
||||
// for the TypeScript definition files in `@types/node` :/
|
||||
this.maxFreeSockets = 1;
|
||||
this.maxSockets = 1;
|
||||
this.maxTotalSockets = Infinity;
|
||||
this.sockets = {};
|
||||
this.freeSockets = {};
|
||||
this.requests = {};
|
||||
this.options = {};
|
||||
}
|
||||
get defaultPort() {
|
||||
if (typeof this.explicitDefaultPort === 'number') {
|
||||
return this.explicitDefaultPort;
|
||||
}
|
||||
return isSecureEndpoint() ? 443 : 80;
|
||||
}
|
||||
set defaultPort(v) {
|
||||
this.explicitDefaultPort = v;
|
||||
}
|
||||
get protocol() {
|
||||
if (typeof this.explicitProtocol === 'string') {
|
||||
return this.explicitProtocol;
|
||||
}
|
||||
return isSecureEndpoint() ? 'https:' : 'http:';
|
||||
}
|
||||
set protocol(v) {
|
||||
this.explicitProtocol = v;
|
||||
}
|
||||
callback(req, opts, fn) {
|
||||
throw new Error('"agent-base" has no default implementation, you must subclass and override `callback()`');
|
||||
}
|
||||
/**
|
||||
* Called by node-core's "_http_client.js" module when creating
|
||||
* a new HTTP request with this Agent instance.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
addRequest(req, _opts) {
|
||||
const opts = Object.assign({}, _opts);
|
||||
if (typeof opts.secureEndpoint !== 'boolean') {
|
||||
opts.secureEndpoint = isSecureEndpoint();
|
||||
}
|
||||
if (opts.host == null) {
|
||||
opts.host = 'localhost';
|
||||
}
|
||||
if (opts.port == null) {
|
||||
opts.port = opts.secureEndpoint ? 443 : 80;
|
||||
}
|
||||
if (opts.protocol == null) {
|
||||
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
|
||||
}
|
||||
if (opts.host && opts.path) {
|
||||
// If both a `host` and `path` are specified then it's most
|
||||
// likely the result of a `url.parse()` call... we need to
|
||||
// remove the `path` portion so that `net.connect()` doesn't
|
||||
// attempt to open that as a unix socket file.
|
||||
delete opts.path;
|
||||
}
|
||||
delete opts.agent;
|
||||
delete opts.hostname;
|
||||
delete opts._defaultAgent;
|
||||
delete opts.defaultPort;
|
||||
delete opts.createConnection;
|
||||
// Hint to use "Connection: close"
|
||||
// XXX: non-documented `http` module API :(
|
||||
req._last = true;
|
||||
req.shouldKeepAlive = false;
|
||||
let timedOut = false;
|
||||
let timeoutId = null;
|
||||
const timeoutMs = opts.timeout || this.timeout;
|
||||
const onerror = (err) => {
|
||||
if (req._hadError)
|
||||
return;
|
||||
req.emit('error', err);
|
||||
// For Safety. Some additional errors might fire later on
|
||||
// and we need to make sure we don't double-fire the error event.
|
||||
req._hadError = true;
|
||||
};
|
||||
const ontimeout = () => {
|
||||
timeoutId = null;
|
||||
timedOut = true;
|
||||
const err = new Error(`A "socket" was not created for HTTP request before ${timeoutMs}ms`);
|
||||
err.code = 'ETIMEOUT';
|
||||
onerror(err);
|
||||
};
|
||||
const callbackError = (err) => {
|
||||
if (timedOut)
|
||||
return;
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
onerror(err);
|
||||
};
|
||||
const onsocket = (socket) => {
|
||||
if (timedOut)
|
||||
return;
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
if (isAgent(socket)) {
|
||||
// `socket` is actually an `http.Agent` instance, so
|
||||
// relinquish responsibility for this `req` to the Agent
|
||||
// from here on
|
||||
debug('Callback returned another Agent instance %o', socket.constructor.name);
|
||||
socket.addRequest(req, opts);
|
||||
return;
|
||||
}
|
||||
if (socket) {
|
||||
socket.once('free', () => {
|
||||
this.freeSocket(socket, opts);
|
||||
});
|
||||
req.onSocket(socket);
|
||||
return;
|
||||
}
|
||||
const err = new Error(`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``);
|
||||
onerror(err);
|
||||
};
|
||||
if (typeof this.callback !== 'function') {
|
||||
onerror(new Error('`callback` is not defined'));
|
||||
return;
|
||||
}
|
||||
if (!this.promisifiedCallback) {
|
||||
if (this.callback.length >= 3) {
|
||||
debug('Converting legacy callback function to promise');
|
||||
this.promisifiedCallback = promisify_1.default(this.callback);
|
||||
}
|
||||
else {
|
||||
this.promisifiedCallback = this.callback;
|
||||
}
|
||||
}
|
||||
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
||||
timeoutId = setTimeout(ontimeout, timeoutMs);
|
||||
}
|
||||
if ('port' in opts && typeof opts.port !== 'number') {
|
||||
opts.port = Number(opts.port);
|
||||
}
|
||||
try {
|
||||
debug('Resolving socket for %o request: %o', opts.protocol, `${req.method} ${req.path}`);
|
||||
Promise.resolve(this.promisifiedCallback(req, opts)).then(onsocket, callbackError);
|
||||
}
|
||||
catch (err) {
|
||||
Promise.reject(err).catch(callbackError);
|
||||
}
|
||||
}
|
||||
freeSocket(socket, opts) {
|
||||
debug('Freeing socket %o %o', socket.constructor.name, opts);
|
||||
socket.destroy();
|
||||
}
|
||||
destroy() {
|
||||
debug('Destroying agent %o', this.constructor.name);
|
||||
}
|
||||
}
|
||||
createAgent.Agent = Agent;
|
||||
// So that `instanceof` works correctly
|
||||
createAgent.prototype = createAgent.Agent.prototype;
|
||||
})(createAgent || (createAgent = {}));
|
||||
module.exports = createAgent;
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;AAIA,mCAAsC;AACtC,kDAAgC;AAChC,4DAAoC;AAEpC,MAAM,KAAK,GAAG,eAAW,CAAC,YAAY,CAAC,CAAC;AAExC,SAAS,OAAO,CAAC,CAAM;IACtB,OAAO,OAAO,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,CAAC;AACzD,CAAC;AAED,SAAS,gBAAgB;IACxB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,KAAK,EAAE,CAAC;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAK,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACxG,CAAC;AAOD,SAAS,WAAW,CACnB,QAA+D,EAC/D,IAA+B;IAE/B,OAAO,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,WAAU,WAAW;IAmDpB;;;;;;OAMG;IACH,MAAa,KAAM,SAAQ,qBAAY;QAmBtC,YACC,QAA+D,EAC/D,KAAgC;YAEhC,KAAK,EAAE,CAAC;YAER,IAAI,IAAI,GAAG,KAAK,CAAC;YACjB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE;gBACnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;aACzB;iBAAM,IAAI,QAAQ,EAAE;gBACpB,IAAI,GAAG,QAAQ,CAAC;aAChB;YAED,0DAA0D;YAC1D,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACpB,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE;gBAC7C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;aAC5B;YAED,+DAA+D;YAC/D,0DAA0D;YAC1D,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;YACxB,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;YAChC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YAClB,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;QACnB,CAAC;QAED,IAAI,WAAW;YACd,IAAI,OAAO,IAAI,CAAC,mBAAmB,KAAK,QAAQ,EAAE;gBACjD,OAAO,IAAI,CAAC,mBAAmB,CAAC;aAChC;YACD,OAAO,gBAAgB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACtC,CAAC;QAED,IAAI,WAAW,CAAC,CAAS;YACxB,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC;QAC9B,CAAC;QAED,IAAI,QAAQ;YACX,IAAI,OAAO,IAAI,CAAC,gBAAgB,KAAK,QAAQ,EAAE;gBAC9C,OAAO,IAAI,CAAC,gBAAgB,CAAC;aAC7B;YACD,OAAO,gBAAgB,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;QAChD,CAAC;QAED,IAAI,QAAQ,CAAC,CAAS;YACrB,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC3B,CAAC;QAaD,QAAQ,CACP,GAA8B,EAC9B,IAA8B,EAC9B,EAAsC;YAKtC,MAAM,IAAI,KAAK,CACd,yFAAyF,CACzF,CAAC;QACH,CAAC;QAED;;;;;WAKG;QACH,UAAU,CAAC,GAAkB,EAAE,KAAqB;YACnD,MAAM,IAAI,qBAAwB,KAAK,CAAE,CAAC;YAE1C,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE;gBAC7C,IAAI,CAAC,cAAc,GAAG,gBAAgB,EAAE,CAAC;aACzC;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE;gBACtB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;aACxB;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,EAAE;gBACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3C;YAED,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE;gBAC1B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;aACzD;YAED,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;gBAC3B,2DAA2D;gBAC3D,0DAA0D;gBAC1D,4DAA4D;gBAC5D,8CAA8C;gBAC9C,OAAO,IAAI,CAAC,IAAI,CAAC;aACjB;YAED,OAAO,IAAI,CAAC,KAAK,CAAC;YAClB,OAAO,IAAI,CAAC,QAAQ,CAAC;YACrB,OAAO,IAAI,CAAC,aAAa,CAAC;YAC1B,OAAO,IAAI,CAAC,WAAW,CAAC;YACxB,OAAO,IAAI,CAAC,gBAAgB,CAAC;YAE7B,kCAAkC;YAClC,2CAA2C;YAC3C,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC;YACjB,GAAG,CAAC,eAAe,GAAG,KAAK,CAAC;YAE5B,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,IAAI,SAAS,GAAyC,IAAI,CAAC;YAC3D,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC;YAE/C,MAAM,OAAO,GAAG,CAAC,GAA0B,EAAE,EAAE;gBAC9C,IAAI,GAAG,CAAC,SAAS;oBAAE,OAAO;gBAC1B,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;gBACvB,yDAAyD;gBACzD,iEAAiE;gBACjE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,CAAC,CAAC;YAEF,MAAM,SAAS,GAAG,GAAG,EAAE;gBACtB,SAAS,GAAG,IAAI,CAAC;gBACjB,QAAQ,GAAG,IAAI,CAAC;gBAChB,MAAM,GAAG,GAA0B,IAAI,KAAK,CAC3C,sDAAsD,SAAS,IAAI,CACnE,CAAC;gBACF,GAAG,CAAC,IAAI,GAAG,UAAU,CAAC;gBACtB,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,MAAM,aAAa,GAAG,CAAC,GAA0B,EAAE,EAAE;gBACpD,IAAI,QAAQ;oBAAE,OAAO;gBACrB,IAAI,SAAS,KAAK,IAAI,EAAE;oBACvB,YAAY,CAAC,SAAS,CAAC,CAAC;oBACxB,SAAS,GAAG,IAAI,CAAC;iBACjB;gBACD,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,MAAM,QAAQ,GAAG,CAAC,MAA2B,EAAE,EAAE;gBAChD,IAAI,QAAQ;oBAAE,OAAO;gBACrB,IAAI,SAAS,IAAI,IAAI,EAAE;oBACtB,YAAY,CAAC,SAAS,CAAC,CAAC;oBACxB,SAAS,GAAG,IAAI,CAAC;iBACjB;gBAED,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE;oBACpB,oDAAoD;oBACpD,wDAAwD;oBACxD,eAAe;oBACf,KAAK,CACJ,6CAA6C,EAC7C,MAAM,CAAC,WAAW,CAAC,IAAI,CACvB,CAAC;oBACD,MAA4B,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;oBACpD,OAAO;iBACP;gBAED,IAAI,MAAM,EAAE;oBACX,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;wBACxB,IAAI,CAAC,UAAU,CAAC,MAAoB,EAAE,IAAI,CAAC,CAAC;oBAC7C,CAAC,CAAC,CAAC;oBACH,GAAG,CAAC,QAAQ,CAAC,MAAoB,CAAC,CAAC;oBACnC,OAAO;iBACP;gBAED,MAAM,GAAG,GAAG,IAAI,KAAK,CACpB,qDAAqD,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,CAC/E,CAAC;gBACF,OAAO,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC;YAEF,IAAI,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE;gBACxC,OAAO,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC;gBAChD,OAAO;aACP;YAED,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE;gBAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,EAAE;oBAC9B,KAAK,CAAC,gDAAgD,CAAC,CAAC;oBACxD,IAAI,CAAC,mBAAmB,GAAG,mBAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;iBACpD;qBAAM;oBACN,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,QAAQ,CAAC;iBACzC;aACD;YAED,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,SAAS,GAAG,CAAC,EAAE;gBACnD,SAAS,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;aAC7C;YAED,IAAI,MAAM,IAAI,IAAI,IAAI,OALine truncated
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { ClientRequest, RequestOptions, AgentCallbackCallback, AgentCallbackPromise } from './index';
|
||||
declare type LegacyCallback = (req: ClientRequest, opts: RequestOptions, fn: AgentCallbackCallback) => void;
|
||||
export default function promisify(fn: LegacyCallback): AgentCallbackPromise;
|
||||
export {};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function promisify(fn) {
|
||||
return function (req, opts) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn.call(this, req, opts, (err, rtn) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(rtn);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
exports.default = promisify;
|
||||
//# sourceMappingURL=promisify.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"promisify.js","sourceRoot":"","sources":["../../src/promisify.ts"],"names":[],"mappings":";;AAeA,SAAwB,SAAS,CAAC,EAAkB;IACnD,OAAO,UAAsB,GAAkB,EAAE,IAAoB;QACpE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACtC,EAAE,CAAC,IAAI,CACN,IAAI,EACJ,GAAG,EACH,IAAI,EACJ,CAAC,GAA6B,EAAE,GAAyB,EAAE,EAAE;gBAC5D,IAAI,GAAG,EAAE;oBACR,MAAM,CAAC,GAAG,CAAC,CAAC;iBACZ;qBAAM;oBACN,OAAO,CAAC,GAAG,CAAC,CAAC;iBACb;YACF,CAAC,CACD,CAAC;QACH,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC;AACH,CAAC;AAjBD,4BAiBC"}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "agent-base",
|
||||
"version": "6.0.2",
|
||||
"description": "Turn a function into an `http.Agent` instance",
|
||||
"main": "dist/src/index",
|
||||
"typings": "dist/src/index",
|
||||
"files": [
|
||||
"dist/src",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "tsc",
|
||||
"postbuild": "cpy --parents src test '!**/*.ts' dist",
|
||||
"test": "mocha --reporter spec dist/test/*.js",
|
||||
"test-lint": "eslint src --ext .js,.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/TooTallNate/node-agent-base.git"
|
||||
},
|
||||
"keywords": [
|
||||
"http",
|
||||
"agent",
|
||||
"base",
|
||||
"barebones",
|
||||
"https"
|
||||
],
|
||||
"author": "Nathan Rajlich <nathan@tootallnate.net> (http://n8.io/)",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/TooTallNate/node-agent-base/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "4",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@types/node": "^14.0.20",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/ws": "^6.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "1.6.0",
|
||||
"@typescript-eslint/parser": "1.1.0",
|
||||
"async-listen": "^1.2.0",
|
||||
"cpy-cli": "^2.0.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-airbnb": "17.1.0",
|
||||
"eslint-config-prettier": "4.1.0",
|
||||
"eslint-import-resolver-typescript": "1.1.1",
|
||||
"eslint-plugin-import": "2.16.0",
|
||||
"eslint-plugin-jsx-a11y": "6.2.1",
|
||||
"eslint-plugin-react": "7.12.4",
|
||||
"mocha": "^6.2.0",
|
||||
"rimraf": "^3.0.0",
|
||||
"semver": "^7.1.2",
|
||||
"typescript": "^3.5.3",
|
||||
"ws": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
import net from 'net';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import { Duplex } from 'stream';
|
||||
import { EventEmitter } from 'events';
|
||||
import createDebug from 'debug';
|
||||
import promisify from './promisify';
|
||||
|
||||
const debug = createDebug('agent-base');
|
||||
|
||||
function isAgent(v: any): v is createAgent.AgentLike {
|
||||
return Boolean(v) && typeof v.addRequest === 'function';
|
||||
}
|
||||
|
||||
function isSecureEndpoint(): boolean {
|
||||
const { stack } = new Error();
|
||||
if (typeof stack !== 'string') return false;
|
||||
return stack.split('\n').some(l => l.indexOf('(https.js:') !== -1 || l.indexOf('node:https:') !== -1);
|
||||
}
|
||||
|
||||
function createAgent(opts?: createAgent.AgentOptions): createAgent.Agent;
|
||||
function createAgent(
|
||||
callback: createAgent.AgentCallback,
|
||||
opts?: createAgent.AgentOptions
|
||||
): createAgent.Agent;
|
||||
function createAgent(
|
||||
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
|
||||
opts?: createAgent.AgentOptions
|
||||
) {
|
||||
return new createAgent.Agent(callback, opts);
|
||||
}
|
||||
|
||||
namespace createAgent {
|
||||
export interface ClientRequest extends http.ClientRequest {
|
||||
_last?: boolean;
|
||||
_hadError?: boolean;
|
||||
method: string;
|
||||
}
|
||||
|
||||
export interface AgentRequestOptions {
|
||||
host?: string;
|
||||
path?: string;
|
||||
// `port` on `http.RequestOptions` can be a string or undefined,
|
||||
// but `net.TcpNetConnectOpts` expects only a number
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface HttpRequestOptions
|
||||
extends AgentRequestOptions,
|
||||
Omit<http.RequestOptions, keyof AgentRequestOptions> {
|
||||
secureEndpoint: false;
|
||||
}
|
||||
|
||||
export interface HttpsRequestOptions
|
||||
extends AgentRequestOptions,
|
||||
Omit<https.RequestOptions, keyof AgentRequestOptions> {
|
||||
secureEndpoint: true;
|
||||
}
|
||||
|
||||
export type RequestOptions = HttpRequestOptions | HttpsRequestOptions;
|
||||
|
||||
export type AgentLike = Pick<createAgent.Agent, 'addRequest'> | http.Agent;
|
||||
|
||||
export type AgentCallbackReturn = Duplex | AgentLike;
|
||||
|
||||
export type AgentCallbackCallback = (
|
||||
err?: Error | null,
|
||||
socket?: createAgent.AgentCallbackReturn
|
||||
) => void;
|
||||
|
||||
export type AgentCallbackPromise = (
|
||||
req: createAgent.ClientRequest,
|
||||
opts: createAgent.RequestOptions
|
||||
) =>
|
||||
| createAgent.AgentCallbackReturn
|
||||
| Promise<createAgent.AgentCallbackReturn>;
|
||||
|
||||
export type AgentCallback = typeof Agent.prototype.callback;
|
||||
|
||||
export type AgentOptions = {
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base `http.Agent` implementation.
|
||||
* No pooling/keep-alive is implemented by default.
|
||||
*
|
||||
* @param {Function} callback
|
||||
* @api public
|
||||
*/
|
||||
export class Agent extends EventEmitter {
|
||||
public timeout: number | null;
|
||||
public maxFreeSockets: number;
|
||||
public maxTotalSockets: number;
|
||||
public maxSockets: number;
|
||||
public sockets: {
|
||||
[key: string]: net.Socket[];
|
||||
};
|
||||
public freeSockets: {
|
||||
[key: string]: net.Socket[];
|
||||
};
|
||||
public requests: {
|
||||
[key: string]: http.IncomingMessage[];
|
||||
};
|
||||
public options: https.AgentOptions;
|
||||
private promisifiedCallback?: createAgent.AgentCallbackPromise;
|
||||
private explicitDefaultPort?: number;
|
||||
private explicitProtocol?: string;
|
||||
|
||||
constructor(
|
||||
callback?: createAgent.AgentCallback | createAgent.AgentOptions,
|
||||
_opts?: createAgent.AgentOptions
|
||||
) {
|
||||
super();
|
||||
|
||||
let opts = _opts;
|
||||
if (typeof callback === 'function') {
|
||||
this.callback = callback;
|
||||
} else if (callback) {
|
||||
opts = callback;
|
||||
}
|
||||
|
||||
// Timeout for the socket to be returned from the callback
|
||||
this.timeout = null;
|
||||
if (opts && typeof opts.timeout === 'number') {
|
||||
this.timeout = opts.timeout;
|
||||
}
|
||||
|
||||
// These aren't actually used by `agent-base`, but are required
|
||||
// for the TypeScript definition files in `@types/node` :/
|
||||
this.maxFreeSockets = 1;
|
||||
this.maxSockets = 1;
|
||||
this.maxTotalSockets = Infinity;
|
||||
this.sockets = {};
|
||||
this.freeSockets = {};
|
||||
this.requests = {};
|
||||
this.options = {};
|
||||
}
|
||||
|
||||
get defaultPort(): number {
|
||||
if (typeof this.explicitDefaultPort === 'number') {
|
||||
return this.explicitDefaultPort;
|
||||
}
|
||||
return isSecureEndpoint() ? 443 : 80;
|
||||
}
|
||||
|
||||
set defaultPort(v: number) {
|
||||
this.explicitDefaultPort = v;
|
||||
}
|
||||
|
||||
get protocol(): string {
|
||||
if (typeof this.explicitProtocol === 'string') {
|
||||
return this.explicitProtocol;
|
||||
}
|
||||
return isSecureEndpoint() ? 'https:' : 'http:';
|
||||
}
|
||||
|
||||
set protocol(v: string) {
|
||||
this.explicitProtocol = v;
|
||||
}
|
||||
|
||||
callback(
|
||||
req: createAgent.ClientRequest,
|
||||
opts: createAgent.RequestOptions,
|
||||
fn: createAgent.AgentCallbackCallback
|
||||
): void;
|
||||
callback(
|
||||
req: createAgent.ClientRequest,
|
||||
opts: createAgent.RequestOptions
|
||||
):
|
||||
| createAgent.AgentCallbackReturn
|
||||
| Promise<createAgent.AgentCallbackReturn>;
|
||||
callback(
|
||||
req: createAgent.ClientRequest,
|
||||
opts: createAgent.AgentOptions,
|
||||
fn?: createAgent.AgentCallbackCallback
|
||||
):
|
||||
| createAgent.AgentCallbackReturn
|
||||
| Promise<createAgent.AgentCallbackReturn>
|
||||
| void {
|
||||
throw new Error(
|
||||
'"agent-base" has no default implementation, you must subclass and override `callback()`'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by node-core's "_http_client.js" module when creating
|
||||
* a new HTTP request with this Agent instance.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
addRequest(req: ClientRequest, _opts: RequestOptions): void {
|
||||
const opts: RequestOptions = { ..._opts };
|
||||
|
||||
if (typeof opts.secureEndpoint !== 'boolean') {
|
||||
opts.secureEndpoint = isSecureEndpoint();
|
||||
}
|
||||
|
||||
if (opts.host == null) {
|
||||
opts.host = 'localhost';
|
||||
}
|
||||
|
||||
if (opts.port == null) {
|
||||
opts.port = opts.secureEndpoint ? 443 : 80;
|
||||
}
|
||||
|
||||
if (opts.protocol == null) {
|
||||
opts.protocol = opts.secureEndpoint ? 'https:' : 'http:';
|
||||
}
|
||||
|
||||
if (opts.host && opts.path) {
|
||||
// If both a `host` and `path` are specified then it's most
|
||||
// likely the result of a `url.parse()` call... we need to
|
||||
// remove the `path` portion so that `net.connect()` doesn't
|
||||
// attempt to open that as a unix socket file.
|
||||
delete opts.path;
|
||||
}
|
||||
|
||||
delete opts.agent;
|
||||
delete opts.hostname;
|
||||
delete opts._defaultAgent;
|
||||
delete opts.defaultPort;
|
||||
delete opts.createConnection;
|
||||
|
||||
// Hint to use "Connection: close"
|
||||
// XXX: non-documented `http` module API :(
|
||||
req._last = true;
|
||||
req.shouldKeepAlive = false;
|
||||
|
||||
let timedOut = false;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
const timeoutMs = opts.timeout || this.timeout;
|
||||
|
||||
const onerror = (err: NodeJS.ErrnoException) => {
|
||||
if (req._hadError) return;
|
||||
req.emit('error', err);
|
||||
// For Safety. Some additional errors might fire later on
|
||||
// and we need to make sure we don't double-fire the error event.
|
||||
req._hadError = true;
|
||||
};
|
||||
|
||||
const ontimeout = () => {
|
||||
timeoutId = null;
|
||||
timedOut = true;
|
||||
const err: NodeJS.ErrnoException = new Error(
|
||||
`A "socket" was not created for HTTP request before ${timeoutMs}ms`
|
||||
);
|
||||
err.code = 'ETIMEOUT';
|
||||
onerror(err);
|
||||
};
|
||||
|
||||
const callbackError = (err: NodeJS.ErrnoException) => {
|
||||
if (timedOut) return;
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
onerror(err);
|
||||
};
|
||||
|
||||
const onsocket = (socket: AgentCallbackReturn) => {
|
||||
if (timedOut) return;
|
||||
if (timeoutId != null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
if (isAgent(socket)) {
|
||||
// `socket` is actually an `http.Agent` instance, so
|
||||
// relinquish responsibility for this `req` to the Agent
|
||||
// from here on
|
||||
debug(
|
||||
'Callback returned another Agent instance %o',
|
||||
socket.constructor.name
|
||||
);
|
||||
(socket as createAgent.Agent).addRequest(req, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket) {
|
||||
socket.once('free', () => {
|
||||
this.freeSocket(socket as net.Socket, opts);
|
||||
});
|
||||
req.onSocket(socket as net.Socket);
|
||||
return;
|
||||
}
|
||||
|
||||
const err = new Error(
|
||||
`no Duplex stream was returned to agent-base for \`${req.method} ${req.path}\``
|
||||
);
|
||||
onerror(err);
|
||||
};
|
||||
|
||||
if (typeof this.callback !== 'function') {
|
||||
onerror(new Error('`callback` is not defined'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.promisifiedCallback) {
|
||||
if (this.callback.length >= 3) {
|
||||
debug('Converting legacy callback function to promise');
|
||||
this.promisifiedCallback = promisify(this.callback);
|
||||
} else {
|
||||
this.promisifiedCallback = this.callback;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
||||
timeoutId = setTimeout(ontimeout, timeoutMs);
|
||||
}
|
||||
|
||||
if ('port' in opts && typeof opts.port !== 'number') {
|
||||
opts.port = Number(opts.port);
|
||||
}
|
||||
|
||||
try {
|
||||
debug(
|
||||
'Resolving socket for %o request: %o',
|
||||
opts.protocol,
|
||||
`${req.method} ${req.path}`
|
||||
);
|
||||
Promise.resolve(this.promisifiedCallback(req, opts)).then(
|
||||
onsocket,
|
||||
callbackError
|
||||
);
|
||||
} catch (err) {
|
||||
Promise.reject(err).catch(callbackError);
|
||||
}
|
||||
}
|
||||
|
||||
freeSocket(socket: net.Socket, opts: AgentOptions) {
|
||||
debug('Freeing socket %o %o', socket.constructor.name, opts);
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
debug('Destroying agent %o', this.constructor.name);
|
||||
}
|
||||
}
|
||||
|
||||
// So that `instanceof` works correctly
|
||||
createAgent.prototype = createAgent.Agent.prototype;
|
||||
}
|
||||
|
||||
export = createAgent;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Agent,
|
||||
ClientRequest,
|
||||
RequestOptions,
|
||||
AgentCallbackCallback,
|
||||
AgentCallbackPromise,
|
||||
AgentCallbackReturn
|
||||
} from './index';
|
||||
|
||||
type LegacyCallback = (
|
||||
req: ClientRequest,
|
||||
opts: RequestOptions,
|
||||
fn: AgentCallbackCallback
|
||||
) => void;
|
||||
|
||||
export default function promisify(fn: LegacyCallback): AgentCallbackPromise {
|
||||
return function(this: Agent, req: ClientRequest, opts: RequestOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn.call(
|
||||
this,
|
||||
req,
|
||||
opts,
|
||||
(err: Error | null | undefined, rtn?: AgentCallbackReturn) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rtn);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Alex Indigo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
# asynckit [](https://www.npmjs.com/package/asynckit)
|
||||
|
||||
Minimal async jobs utility library, with streams support.
|
||||
|
||||
[](https://travis-ci.org/alexindigo/asynckit)
|
||||
[](https://travis-ci.org/alexindigo/asynckit)
|
||||
[](https://ci.appveyor.com/project/alexindigo/asynckit)
|
||||
|
||||
[](https://coveralls.io/github/alexindigo/asynckit?branch=master)
|
||||
[](https://david-dm.org/alexindigo/asynckit)
|
||||
[](https://www.bithound.io/github/alexindigo/asynckit)
|
||||
|
||||
<!-- [](https://www.npmjs.com/package/reamde) -->
|
||||
|
||||
AsyncKit provides harness for `parallel` and `serial` iterators over list of items represented by arrays or objects.
|
||||
Optionally it accepts abort function (should be synchronously return by iterator for each item), and terminates left over jobs upon an error event. For specific iteration order built-in (`ascending` and `descending`) and custom sort helpers also supported, via `asynckit.serialOrdered` method.
|
||||
|
||||
It ensures async operations to keep behavior more stable and prevent `Maximum call stack size exceeded` errors, from sync iterators.
|
||||
|
||||
| compression | size |
|
||||
| :----------------- | -------: |
|
||||
| asynckit.js | 12.34 kB |
|
||||
| asynckit.min.js | 4.11 kB |
|
||||
| asynckit.min.js.gz | 1.47 kB |
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
$ npm install --save asynckit
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Parallel Jobs
|
||||
|
||||
Runs iterator over provided array in parallel. Stores output in the `result` array,
|
||||
on the matching positions. In unlikely event of an error from one of the jobs,
|
||||
will terminate rest of the active jobs (if abort function is provided)
|
||||
and return error along with salvaged data to the main callback function.
|
||||
|
||||
#### Input Array
|
||||
|
||||
```javascript
|
||||
var parallel = require('asynckit').parallel
|
||||
, assert = require('assert')
|
||||
;
|
||||
|
||||
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
|
||||
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
|
||||
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
|
||||
, target = []
|
||||
;
|
||||
|
||||
parallel(source, asyncJob, function(err, result)
|
||||
{
|
||||
assert.deepEqual(result, expectedResult);
|
||||
assert.deepEqual(target, expectedTarget);
|
||||
});
|
||||
|
||||
// async job accepts one element from the array
|
||||
// and a callback function
|
||||
function asyncJob(item, cb)
|
||||
{
|
||||
// different delays (in ms) per item
|
||||
var delay = item * 25;
|
||||
|
||||
// pretend different jobs take different time to finish
|
||||
// and not in consequential order
|
||||
var timeoutId = setTimeout(function() {
|
||||
target.push(item);
|
||||
cb(null, item * 2);
|
||||
}, delay);
|
||||
|
||||
// allow to cancel "leftover" jobs upon error
|
||||
// return function, invoking of which will abort this job
|
||||
return clearTimeout.bind(null, timeoutId);
|
||||
}
|
||||
```
|
||||
|
||||
More examples could be found in [test/test-parallel-array.js](test/test-parallel-array.js).
|
||||
|
||||
#### Input Object
|
||||
|
||||
Also it supports named jobs, listed via object.
|
||||
|
||||
```javascript
|
||||
var parallel = require('asynckit/parallel')
|
||||
, assert = require('assert')
|
||||
;
|
||||
|
||||
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
|
||||
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
|
||||
, expectedTarget = [ 1, 1, 2, 4, 8, 16, 32, 64 ]
|
||||
, expectedKeys = [ 'first', 'one', 'two', 'four', 'eight', 'sixteen', 'thirtyTwo', 'sixtyFour' ]
|
||||
, target = []
|
||||
, keys = []
|
||||
;
|
||||
|
||||
parallel(source, asyncJob, function(err, result)
|
||||
{
|
||||
assert.deepEqual(result, expectedResult);
|
||||
assert.deepEqual(target, expectedTarget);
|
||||
assert.deepEqual(keys, expectedKeys);
|
||||
});
|
||||
|
||||
// supports full value, key, callback (shortcut) interface
|
||||
function asyncJob(item, key, cb)
|
||||
{
|
||||
// different delays (in ms) per item
|
||||
var delay = item * 25;
|
||||
|
||||
// pretend different jobs take different time to finish
|
||||
// and not in consequential order
|
||||
var timeoutId = setTimeout(function() {
|
||||
keys.push(key);
|
||||
target.push(item);
|
||||
cb(null, item * 2);
|
||||
}, delay);
|
||||
|
||||
// allow to cancel "leftover" jobs upon error
|
||||
// return function, invoking of which will abort this job
|
||||
return clearTimeout.bind(null, timeoutId);
|
||||
}
|
||||
```
|
||||
|
||||
More examples could be found in [test/test-parallel-object.js](test/test-parallel-object.js).
|
||||
|
||||
### Serial Jobs
|
||||
|
||||
Runs iterator over provided array sequentially. Stores output in the `result` array,
|
||||
on the matching positions. In unlikely event of an error from one of the jobs,
|
||||
will not proceed to the rest of the items in the list
|
||||
and return error along with salvaged data to the main callback function.
|
||||
|
||||
#### Input Array
|
||||
|
||||
```javascript
|
||||
var serial = require('asynckit/serial')
|
||||
, assert = require('assert')
|
||||
;
|
||||
|
||||
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
|
||||
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
|
||||
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
|
||||
, target = []
|
||||
;
|
||||
|
||||
serial(source, asyncJob, function(err, result)
|
||||
{
|
||||
assert.deepEqual(result, expectedResult);
|
||||
assert.deepEqual(target, expectedTarget);
|
||||
});
|
||||
|
||||
// extended interface (item, key, callback)
|
||||
// also supported for arrays
|
||||
function asyncJob(item, key, cb)
|
||||
{
|
||||
target.push(key);
|
||||
|
||||
// it will be automatically made async
|
||||
// even it iterator "returns" in the same event loop
|
||||
cb(null, item * 2);
|
||||
}
|
||||
```
|
||||
|
||||
More examples could be found in [test/test-serial-array.js](test/test-serial-array.js).
|
||||
|
||||
#### Input Object
|
||||
|
||||
Also it supports named jobs, listed via object.
|
||||
|
||||
```javascript
|
||||
var serial = require('asynckit').serial
|
||||
, assert = require('assert')
|
||||
;
|
||||
|
||||
var source = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
|
||||
, expectedResult = [ 2, 2, 8, 32, 128, 64, 16, 4 ]
|
||||
, expectedTarget = [ 0, 1, 2, 3, 4, 5, 6, 7 ]
|
||||
, target = []
|
||||
;
|
||||
|
||||
var source = { first: 1, one: 1, four: 4, sixteen: 16, sixtyFour: 64, thirtyTwo: 32, eight: 8, two: 2 }
|
||||
, expectedResult = { first: 2, one: 2, four: 8, sixteen: 32, sixtyFour: 128, thirtyTwo: 64, eight: 16, two: 4 }
|
||||
, expectedTarget = [ 1, 1, 4, 16, 64, 32, 8, 2 ]
|
||||
, target = []
|
||||
;
|
||||
|
||||
|
||||
serial(source, asyncJob, function(err, result)
|
||||
{
|
||||
assert.deepEqual(result, expectedResult);
|
||||
assert.deepEqual(target, expectedTarget);
|
||||
});
|
||||
|
||||
// shortcut interface (item, callback)
|
||||
// works for object as well as for the arrays
|
||||
function asyncJob(item, cb)
|
||||
{
|
||||
target.push(item);
|
||||
|
||||
// it will be automatically made async
|
||||
// even it iterator "returns" in the same event loop
|
||||
cb(null, item * 2);
|
||||
}
|
||||
```
|
||||
|
||||
More examples could be found in [test/test-serial-object.js](test/test-serial-object.js).
|
||||
|
||||
_Note: Since _object_ is an _unordered_ collection of properties,
|
||||
it may produce unexpected results with sequential iterations.
|
||||
Whenever order of the jobs' execution is important please use `serialOrdered` method._
|
||||
|
||||
### Ordered Serial Iterations
|
||||
|
||||
TBD
|
||||
|
||||
For example [compare-property](compare-property) package.
|
||||
|
||||
### Streaming interface
|
||||
|
||||
TBD
|
||||
|
||||
## Want to Know More?
|
||||
|
||||
More examples can be found in [test folder](test/).
|
||||
|
||||
Or open an [issue](https://github.com/alexindigo/asynckit/issues) with questions and/or suggestions.
|
||||
|
||||
## License
|
||||
|
||||
AsyncKit is licensed under the MIT license.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/* eslint no-console: "off" */
|
||||
|
||||
var asynckit = require('./')
|
||||
, async = require('async')
|
||||
, assert = require('assert')
|
||||
, expected = 0
|
||||
;
|
||||
|
||||
var Benchmark = require('benchmark');
|
||||
var suite = new Benchmark.Suite;
|
||||
|
||||
var source = [];
|
||||
for (var z = 1; z < 100; z++)
|
||||
{
|
||||
source.push(z);
|
||||
expected += z;
|
||||
}
|
||||
|
||||
suite
|
||||
// add tests
|
||||
|
||||
.add('async.map', function(deferred)
|
||||
{
|
||||
var total = 0;
|
||||
|
||||
async.map(source,
|
||||
function(i, cb)
|
||||
{
|
||||
setImmediate(function()
|
||||
{
|
||||
total += i;
|
||||
cb(null, total);
|
||||
});
|
||||
},
|
||||
function(err, result)
|
||||
{
|
||||
assert.ifError(err);
|
||||
assert.equal(result[result.length - 1], expected);
|
||||
deferred.resolve();
|
||||
});
|
||||
}, {'defer': true})
|
||||
|
||||
|
||||
.add('asynckit.parallel', function(deferred)
|
||||
{
|
||||
var total = 0;
|
||||
|
||||
asynckit.parallel(source,
|
||||
function(i, cb)
|
||||
{
|
||||
setImmediate(function()
|
||||
{
|
||||
total += i;
|
||||
cb(null, total);
|
||||
});
|
||||
},
|
||||
function(err, result)
|
||||
{
|
||||
assert.ifError(err);
|
||||
assert.equal(result[result.length - 1], expected);
|
||||
deferred.resolve();
|
||||
});
|
||||
}, {'defer': true})
|
||||
|
||||
|
||||
// add listeners
|
||||
.on('cycle', function(ev)
|
||||
{
|
||||
console.log(String(ev.target));
|
||||
})
|
||||
.on('complete', function()
|
||||
{
|
||||
console.log('Fastest is ' + this.filter('fastest').map('name'));
|
||||
})
|
||||
// run async
|
||||
.run({ 'async': true });
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
module.exports =
|
||||
{
|
||||
parallel : require('./parallel.js'),
|
||||
serial : require('./serial.js'),
|
||||
serialOrdered : require('./serialOrdered.js')
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
// API
|
||||
module.exports = abort;
|
||||
|
||||
/**
|
||||
* Aborts leftover active jobs
|
||||
*
|
||||
* @param {object} state - current state object
|
||||
*/
|
||||
function abort(state)
|
||||
{
|
||||
Object.keys(state.jobs).forEach(clean.bind(state));
|
||||
|
||||
// reset leftover jobs
|
||||
state.jobs = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up leftover job by invoking abort function for the provided job id
|
||||
*
|
||||
* @this state
|
||||
* @param {string|number} key - job id to abort
|
||||
*/
|
||||
function clean(key)
|
||||
{
|
||||
if (typeof this.jobs[key] == 'function')
|
||||
{
|
||||
this.jobs[key]();
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
var defer = require('./defer.js');
|
||||
|
||||
// API
|
||||
module.exports = async;
|
||||
|
||||
/**
|
||||
* Runs provided callback asynchronously
|
||||
* even if callback itself is not
|
||||
*
|
||||
* @param {function} callback - callback to invoke
|
||||
* @returns {function} - augmented callback
|
||||
*/
|
||||
function async(callback)
|
||||
{
|
||||
var isAsync = false;
|
||||
|
||||
// check if async happened
|
||||
defer(function() { isAsync = true; });
|
||||
|
||||
return function async_callback(err, result)
|
||||
{
|
||||
if (isAsync)
|
||||
{
|
||||
callback(err, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
defer(function nextTick_callback()
|
||||
{
|
||||
callback(err, result);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
module.exports = defer;
|
||||
|
||||
/**
|
||||
* Runs provided function on next iteration of the event loop
|
||||
*
|
||||
* @param {function} fn - function to run
|
||||
*/
|
||||
function defer(fn)
|
||||
{
|
||||
var nextTick = typeof setImmediate == 'function'
|
||||
? setImmediate
|
||||
: (
|
||||
typeof process == 'object' && typeof process.nextTick == 'function'
|
||||
? process.nextTick
|
||||
: null
|
||||
);
|
||||
|
||||
if (nextTick)
|
||||
{
|
||||
nextTick(fn);
|
||||
}
|
||||
else
|
||||
{
|
||||
setTimeout(fn, 0);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
var async = require('./async.js')
|
||||
, abort = require('./abort.js')
|
||||
;
|
||||
|
||||
// API
|
||||
module.exports = iterate;
|
||||
|
||||
/**
|
||||
* Iterates over each job object
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {object} state - current job status
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
*/
|
||||
function iterate(list, iterator, state, callback)
|
||||
{
|
||||
// store current index
|
||||
var key = state['keyedList'] ? state['keyedList'][state.index] : state.index;
|
||||
|
||||
state.jobs[key] = runJob(iterator, key, list[key], function(error, output)
|
||||
{
|
||||
// don't repeat yourself
|
||||
// skip secondary callbacks
|
||||
if (!(key in state.jobs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// clean up jobs
|
||||
delete state.jobs[key];
|
||||
|
||||
if (error)
|
||||
{
|
||||
// don't process rest of the results
|
||||
// stop still active jobs
|
||||
// and reset the list
|
||||
abort(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.results[key] = output;
|
||||
}
|
||||
|
||||
// return salvaged results
|
||||
callback(error, state.results);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs iterator over provided job element
|
||||
*
|
||||
* @param {function} iterator - iterator to invoke
|
||||
* @param {string|number} key - key/index of the element in the list of jobs
|
||||
* @param {mixed} item - job description
|
||||
* @param {function} callback - invoked after iterator is done with the job
|
||||
* @returns {function|mixed} - job abort function or something else
|
||||
*/
|
||||
function runJob(iterator, key, item, callback)
|
||||
{
|
||||
var aborter;
|
||||
|
||||
// allow shortcut if iterator expects only two arguments
|
||||
if (iterator.length == 2)
|
||||
{
|
||||
aborter = iterator(item, async(callback));
|
||||
}
|
||||
// otherwise go with full three arguments
|
||||
else
|
||||
{
|
||||
aborter = iterator(item, key, async(callback));
|
||||
}
|
||||
|
||||
return aborter;
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
var streamify = require('./streamify.js')
|
||||
, defer = require('./defer.js')
|
||||
;
|
||||
|
||||
// API
|
||||
module.exports = ReadableAsyncKit;
|
||||
|
||||
/**
|
||||
* Base constructor for all streams
|
||||
* used to hold properties/methods
|
||||
*/
|
||||
function ReadableAsyncKit()
|
||||
{
|
||||
ReadableAsyncKit.super_.apply(this, arguments);
|
||||
|
||||
// list of active jobs
|
||||
this.jobs = {};
|
||||
|
||||
// add stream methods
|
||||
this.destroy = destroy;
|
||||
this._start = _start;
|
||||
this._read = _read;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys readable stream,
|
||||
* by aborting outstanding jobs
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function destroy()
|
||||
{
|
||||
if (this.destroyed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.destroyed = true;
|
||||
|
||||
if (typeof this.terminator == 'function')
|
||||
{
|
||||
this.terminator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts provided jobs in async manner
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function _start()
|
||||
{
|
||||
// first argument – runner function
|
||||
var runner = arguments[0]
|
||||
// take away first argument
|
||||
, args = Array.prototype.slice.call(arguments, 1)
|
||||
// second argument - input data
|
||||
, input = args[0]
|
||||
// last argument - result callback
|
||||
, endCb = streamify.callback.call(this, args[args.length - 1])
|
||||
;
|
||||
|
||||
args[args.length - 1] = endCb;
|
||||
// third argument - iterator
|
||||
args[1] = streamify.iterator.call(this, args[1]);
|
||||
|
||||
// allow time for proper setup
|
||||
defer(function()
|
||||
{
|
||||
if (!this.destroyed)
|
||||
{
|
||||
this.terminator = runner.apply(null, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
endCb(null, Array.isArray(input) ? [] : {});
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implement _read to comply with Readable streams
|
||||
* Doesn't really make sense for flowing object mode
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function _read()
|
||||
{
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
var parallel = require('../parallel.js');
|
||||
|
||||
// API
|
||||
module.exports = ReadableParallel;
|
||||
|
||||
/**
|
||||
* Streaming wrapper to `asynckit.parallel`
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {stream.Readable#}
|
||||
*/
|
||||
function ReadableParallel(list, iterator, callback)
|
||||
{
|
||||
if (!(this instanceof ReadableParallel))
|
||||
{
|
||||
return new ReadableParallel(list, iterator, callback);
|
||||
}
|
||||
|
||||
// turn on object mode
|
||||
ReadableParallel.super_.call(this, {objectMode: true});
|
||||
|
||||
this._start(parallel, list, iterator, callback);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
var serial = require('../serial.js');
|
||||
|
||||
// API
|
||||
module.exports = ReadableSerial;
|
||||
|
||||
/**
|
||||
* Streaming wrapper to `asynckit.serial`
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {stream.Readable#}
|
||||
*/
|
||||
function ReadableSerial(list, iterator, callback)
|
||||
{
|
||||
if (!(this instanceof ReadableSerial))
|
||||
{
|
||||
return new ReadableSerial(list, iterator, callback);
|
||||
}
|
||||
|
||||
// turn on object mode
|
||||
ReadableSerial.super_.call(this, {objectMode: true});
|
||||
|
||||
this._start(serial, list, iterator, callback);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
var serialOrdered = require('../serialOrdered.js');
|
||||
|
||||
// API
|
||||
module.exports = ReadableSerialOrdered;
|
||||
// expose sort helpers
|
||||
module.exports.ascending = serialOrdered.ascending;
|
||||
module.exports.descending = serialOrdered.descending;
|
||||
|
||||
/**
|
||||
* Streaming wrapper to `asynckit.serialOrdered`
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} sortMethod - custom sort function
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {stream.Readable#}
|
||||
*/
|
||||
function ReadableSerialOrdered(list, iterator, sortMethod, callback)
|
||||
{
|
||||
if (!(this instanceof ReadableSerialOrdered))
|
||||
{
|
||||
return new ReadableSerialOrdered(list, iterator, sortMethod, callback);
|
||||
}
|
||||
|
||||
// turn on object mode
|
||||
ReadableSerialOrdered.super_.call(this, {objectMode: true});
|
||||
|
||||
this._start(serialOrdered, list, iterator, sortMethod, callback);
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// API
|
||||
module.exports = state;
|
||||
|
||||
/**
|
||||
* Creates initial state object
|
||||
* for iteration over list
|
||||
*
|
||||
* @param {array|object} list - list to iterate over
|
||||
* @param {function|null} sortMethod - function to use for keys sort,
|
||||
* or `null` to keep them as is
|
||||
* @returns {object} - initial state object
|
||||
*/
|
||||
function state(list, sortMethod)
|
||||
{
|
||||
var isNamedList = !Array.isArray(list)
|
||||
, initState =
|
||||
{
|
||||
index : 0,
|
||||
keyedList: isNamedList || sortMethod ? Object.keys(list) : null,
|
||||
jobs : {},
|
||||
results : isNamedList ? {} : [],
|
||||
size : isNamedList ? Object.keys(list).length : list.length
|
||||
}
|
||||
;
|
||||
|
||||
if (sortMethod)
|
||||
{
|
||||
// sort array keys based on it's values
|
||||
// sort object's keys just on own merit
|
||||
initState.keyedList.sort(isNamedList ? sortMethod : function(a, b)
|
||||
{
|
||||
return sortMethod(list[a], list[b]);
|
||||
});
|
||||
}
|
||||
|
||||
return initState;
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
var async = require('./async.js');
|
||||
|
||||
// API
|
||||
module.exports = {
|
||||
iterator: wrapIterator,
|
||||
callback: wrapCallback
|
||||
};
|
||||
|
||||
/**
|
||||
* Wraps iterators with long signature
|
||||
*
|
||||
* @this ReadableAsyncKit#
|
||||
* @param {function} iterator - function to wrap
|
||||
* @returns {function} - wrapped function
|
||||
*/
|
||||
function wrapIterator(iterator)
|
||||
{
|
||||
var stream = this;
|
||||
|
||||
return function(item, key, cb)
|
||||
{
|
||||
var aborter
|
||||
, wrappedCb = async(wrapIteratorCallback.call(stream, cb, key))
|
||||
;
|
||||
|
||||
stream.jobs[key] = wrappedCb;
|
||||
|
||||
// it's either shortcut (item, cb)
|
||||
if (iterator.length == 2)
|
||||
{
|
||||
aborter = iterator(item, wrappedCb);
|
||||
}
|
||||
// or long format (item, key, cb)
|
||||
else
|
||||
{
|
||||
aborter = iterator(item, key, wrappedCb);
|
||||
}
|
||||
|
||||
return aborter;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps provided callback function
|
||||
* allowing to execute snitch function before
|
||||
* real callback
|
||||
*
|
||||
* @this ReadableAsyncKit#
|
||||
* @param {function} callback - function to wrap
|
||||
* @returns {function} - wrapped function
|
||||
*/
|
||||
function wrapCallback(callback)
|
||||
{
|
||||
var stream = this;
|
||||
|
||||
var wrapped = function(error, result)
|
||||
{
|
||||
return finisher.call(stream, error, result, callback);
|
||||
};
|
||||
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps provided iterator callback function
|
||||
* makes sure snitch only called once,
|
||||
* but passes secondary calls to the original callback
|
||||
*
|
||||
* @this ReadableAsyncKit#
|
||||
* @param {function} callback - callback to wrap
|
||||
* @param {number|string} key - iteration key
|
||||
* @returns {function} wrapped callback
|
||||
*/
|
||||
function wrapIteratorCallback(callback, key)
|
||||
{
|
||||
var stream = this;
|
||||
|
||||
return function(error, output)
|
||||
{
|
||||
// don't repeat yourself
|
||||
if (!(key in stream.jobs))
|
||||
{
|
||||
callback(error, output);
|
||||
return;
|
||||
}
|
||||
|
||||
// clean up jobs
|
||||
delete stream.jobs[key];
|
||||
|
||||
return streamer.call(stream, error, {key: key, value: output}, callback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream wrapper for iterator callback
|
||||
*
|
||||
* @this ReadableAsyncKit#
|
||||
* @param {mixed} error - error response
|
||||
* @param {mixed} output - iterator output
|
||||
* @param {function} callback - callback that expects iterator results
|
||||
*/
|
||||
function streamer(error, output, callback)
|
||||
{
|
||||
if (error && !this.error)
|
||||
{
|
||||
this.error = error;
|
||||
this.pause();
|
||||
this.emit('error', error);
|
||||
// send back value only, as expected
|
||||
callback(error, output && output.value);
|
||||
return;
|
||||
}
|
||||
|
||||
// stream stuff
|
||||
this.push(output);
|
||||
|
||||
// back to original track
|
||||
// send back value only, as expected
|
||||
callback(error, output && output.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream wrapper for finishing callback
|
||||
*
|
||||
* @this ReadableAsyncKit#
|
||||
* @param {mixed} error - error response
|
||||
* @param {mixed} output - iterator output
|
||||
* @param {function} callback - callback that expects final results
|
||||
*/
|
||||
function finisher(error, output, callback)
|
||||
{
|
||||
// signal end of the stream
|
||||
// only for successfully finished streams
|
||||
if (!error)
|
||||
{
|
||||
this.push(null);
|
||||
}
|
||||
|
||||
// back to original track
|
||||
callback(error, output);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
var abort = require('./abort.js')
|
||||
, async = require('./async.js')
|
||||
;
|
||||
|
||||
// API
|
||||
module.exports = terminator;
|
||||
|
||||
/**
|
||||
* Terminates jobs in the attached state context
|
||||
*
|
||||
* @this AsyncKitState#
|
||||
* @param {function} callback - final callback to invoke after termination
|
||||
*/
|
||||
function terminator(callback)
|
||||
{
|
||||
if (!Object.keys(this.jobs).length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// fast forward iteration index
|
||||
this.index = this.size;
|
||||
|
||||
// abort jobs
|
||||
abort(this);
|
||||
|
||||
// send back results we have so far
|
||||
async(callback)(null, this.results);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "asynckit",
|
||||
"version": "0.4.0",
|
||||
"description": "Minimal async jobs utility library, with streams support",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf coverage",
|
||||
"lint": "eslint *.js lib/*.js test/*.js",
|
||||
"test": "istanbul cover --reporter=json tape -- 'test/test-*.js' | tap-spec",
|
||||
"win-test": "tape test/test-*.js",
|
||||
"browser": "browserify -t browserify-istanbul test/lib/browserify_adjustment.js test/test-*.js | obake --coverage | tap-spec",
|
||||
"report": "istanbul report",
|
||||
"size": "browserify index.js | size-table asynckit",
|
||||
"debug": "tape test/test-*.js"
|
||||
},
|
||||
"pre-commit": [
|
||||
"clean",
|
||||
"lint",
|
||||
"test",
|
||||
"browser",
|
||||
"report",
|
||||
"size"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/alexindigo/asynckit.git"
|
||||
},
|
||||
"keywords": [
|
||||
"async",
|
||||
"jobs",
|
||||
"parallel",
|
||||
"serial",
|
||||
"iterator",
|
||||
"array",
|
||||
"object",
|
||||
"stream",
|
||||
"destroy",
|
||||
"terminate",
|
||||
"abort"
|
||||
],
|
||||
"author": "Alex Indigo <iam@alexindigo.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/alexindigo/asynckit/issues"
|
||||
},
|
||||
"homepage": "https://github.com/alexindigo/asynckit#readme",
|
||||
"devDependencies": {
|
||||
"browserify": "^13.0.0",
|
||||
"browserify-istanbul": "^2.0.0",
|
||||
"coveralls": "^2.11.9",
|
||||
"eslint": "^2.9.0",
|
||||
"istanbul": "^0.4.3",
|
||||
"obake": "^0.1.2",
|
||||
"phantomjs-prebuilt": "^2.1.7",
|
||||
"pre-commit": "^1.1.3",
|
||||
"reamde": "^1.1.0",
|
||||
"rimraf": "^2.5.2",
|
||||
"size-table": "^0.2.0",
|
||||
"tap-spec": "^4.1.1",
|
||||
"tape": "^4.5.1"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
var iterate = require('./lib/iterate.js')
|
||||
, initState = require('./lib/state.js')
|
||||
, terminator = require('./lib/terminator.js')
|
||||
;
|
||||
|
||||
// Public API
|
||||
module.exports = parallel;
|
||||
|
||||
/**
|
||||
* Runs iterator over provided array elements in parallel
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {function} - jobs terminator
|
||||
*/
|
||||
function parallel(list, iterator, callback)
|
||||
{
|
||||
var state = initState(list);
|
||||
|
||||
while (state.index < (state['keyedList'] || list).length)
|
||||
{
|
||||
iterate(list, iterator, state, function(error, result)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
callback(error, result);
|
||||
return;
|
||||
}
|
||||
|
||||
// looks like it's the last one
|
||||
if (Object.keys(state.jobs).length === 0)
|
||||
{
|
||||
callback(null, state.results);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
state.index++;
|
||||
}
|
||||
|
||||
return terminator.bind(state, callback);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
var serialOrdered = require('./serialOrdered.js');
|
||||
|
||||
// Public API
|
||||
module.exports = serial;
|
||||
|
||||
/**
|
||||
* Runs iterator over provided array elements in series
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {function} - jobs terminator
|
||||
*/
|
||||
function serial(list, iterator, callback)
|
||||
{
|
||||
return serialOrdered(list, iterator, null, callback);
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
var iterate = require('./lib/iterate.js')
|
||||
, initState = require('./lib/state.js')
|
||||
, terminator = require('./lib/terminator.js')
|
||||
;
|
||||
|
||||
// Public API
|
||||
module.exports = serialOrdered;
|
||||
// sorting helpers
|
||||
module.exports.ascending = ascending;
|
||||
module.exports.descending = descending;
|
||||
|
||||
/**
|
||||
* Runs iterator over provided sorted array elements in series
|
||||
*
|
||||
* @param {array|object} list - array or object (named list) to iterate over
|
||||
* @param {function} iterator - iterator to run
|
||||
* @param {function} sortMethod - custom sort function
|
||||
* @param {function} callback - invoked when all elements processed
|
||||
* @returns {function} - jobs terminator
|
||||
*/
|
||||
function serialOrdered(list, iterator, sortMethod, callback)
|
||||
{
|
||||
var state = initState(list, sortMethod);
|
||||
|
||||
iterate(list, iterator, state, function iteratorHandler(error, result)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
callback(error, result);
|
||||
return;
|
||||
}
|
||||
|
||||
state.index++;
|
||||
|
||||
// are we there yet?
|
||||
if (state.index < (state['keyedList'] || list).length)
|
||||
{
|
||||
iterate(list, iterator, state, iteratorHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
// done here
|
||||
callback(null, state.results);
|
||||
});
|
||||
|
||||
return terminator.bind(state, callback);
|
||||
}
|
||||
|
||||
/*
|
||||
* -- Sort methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* sort helper to sort array elements in ascending order
|
||||
*
|
||||
* @param {mixed} a - an item to compare
|
||||
* @param {mixed} b - an item to compare
|
||||
* @returns {number} - comparison result
|
||||
*/
|
||||
function ascending(a, b)
|
||||
{
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort helper to sort array elements in descending order
|
||||
*
|
||||
* @param {mixed} a - an item to compare
|
||||
* @param {mixed} b - an item to compare
|
||||
* @returns {number} - comparison result
|
||||
*/
|
||||
function descending(a, b)
|
||||
{
|
||||
return -1 * ascending(a, b);
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
var inherits = require('util').inherits
|
||||
, Readable = require('stream').Readable
|
||||
, ReadableAsyncKit = require('./lib/readable_asynckit.js')
|
||||
, ReadableParallel = require('./lib/readable_parallel.js')
|
||||
, ReadableSerial = require('./lib/readable_serial.js')
|
||||
, ReadableSerialOrdered = require('./lib/readable_serial_ordered.js')
|
||||
;
|
||||
|
||||
// API
|
||||
module.exports =
|
||||
{
|
||||
parallel : ReadableParallel,
|
||||
serial : ReadableSerial,
|
||||
serialOrdered : ReadableSerialOrdered,
|
||||
};
|
||||
|
||||
inherits(ReadableAsyncKit, Readable);
|
||||
|
||||
inherits(ReadableParallel, ReadableAsyncKit);
|
||||
inherits(ReadableSerial, ReadableAsyncKit);
|
||||
inherits(ReadableSerialOrdered, ReadableAsyncKit);
|
||||
+1747
File diff suppressed because it is too large.
Load diff
+7
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) 2014-present Matt Zabriskie & Collaborators
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+877
@@ -0,0 +1,877 @@
|
||||
# Axios Migration Guide
|
||||
|
||||
> **Migrating from Axios 0.x to 1.x**
|
||||
>
|
||||
> This guide helps developers upgrade from Axios 0.x to 1.x by documenting breaking changes, providing migration strategies, and offering solutions to common upgrade challenges.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Error Handling Migration](#error-handling-migration)
|
||||
- [API Changes](#api-changes)
|
||||
- [Configuration Changes](#configuration-changes)
|
||||
- [Migration Strategies](#migration-strategies)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Resources](#resources)
|
||||
|
||||
## Overview
|
||||
|
||||
Axios 1.x introduced several breaking changes to improve consistency, security, and developer experience. While these changes provide better error handling and more predictable behavior, they require code updates when migrating from 0.x versions.
|
||||
|
||||
### Key Changes Summary
|
||||
|
||||
| Area | 0.x Behavior | 1.x Behavior | Impact |
|
||||
|------|--------------|--------------|--------|
|
||||
| Error Handling | Selective throwing | Consistent throwing | High |
|
||||
| JSON Parsing | Lenient | Strict | Medium |
|
||||
| Browser Support | IE11+ | Modern browsers | Low-Medium |
|
||||
| TypeScript | Partial | Full support | Low |
|
||||
|
||||
### Migration Complexity
|
||||
|
||||
- **Simple applications**: 1-2 hours
|
||||
- **Medium applications**: 1-2 days
|
||||
- **Large applications with complex error handling**: 3-5 days
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### 1. Error Handling Changes
|
||||
|
||||
**The most significant change in Axios 1.x is how errors are handled.**
|
||||
|
||||
#### 0.x Behavior
|
||||
```javascript
|
||||
// Axios 0.x - Some HTTP error codes didn't throw
|
||||
axios.get('/api/data')
|
||||
.then(response => {
|
||||
// Response interceptor could handle all errors
|
||||
console.log('Success:', response.data);
|
||||
});
|
||||
|
||||
// Response interceptor handled everything
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
handleError(error);
|
||||
// Error was "handled" and didn't propagate
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.x Behavior
|
||||
```javascript
|
||||
// Axios 1.x - All HTTP errors throw consistently
|
||||
axios.get('/api/data')
|
||||
.then(response => {
|
||||
console.log('Success:', response.data);
|
||||
})
|
||||
.catch(error => {
|
||||
// Must handle errors at call site or they propagate
|
||||
console.error('Request failed:', error);
|
||||
});
|
||||
|
||||
// Response interceptor must re-throw or return rejected promise
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
handleError(error);
|
||||
// Must explicitly handle propagation
|
||||
return Promise.reject(error); // or throw error;
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Impact
|
||||
- **Response interceptors** can no longer "swallow" errors silently
|
||||
- **Every API call** must handle errors explicitly or they become unhandled promise rejections
|
||||
- **Centralized error handling** requires new patterns
|
||||
|
||||
### 2. JSON Parsing Changes
|
||||
|
||||
#### 0.x Behavior
|
||||
```javascript
|
||||
// Axios 0.x - Lenient JSON parsing
|
||||
// Would attempt to parse even invalid JSON
|
||||
response.data; // Might contain partial data or fallbacks
|
||||
```
|
||||
|
||||
#### 1.x Behavior
|
||||
```javascript
|
||||
// Axios 1.x - Strict JSON parsing
|
||||
// Throws clear errors for invalid JSON
|
||||
try {
|
||||
const data = response.data;
|
||||
} catch (error) {
|
||||
// Handle JSON parsing errors explicitly
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Request/Response Transform Changes
|
||||
|
||||
#### 0.x Behavior
|
||||
```javascript
|
||||
// Implicit transformations with some edge cases
|
||||
transformRequest: [function (data) {
|
||||
// Less predictable behavior
|
||||
return data;
|
||||
}]
|
||||
```
|
||||
|
||||
#### 1.x Behavior
|
||||
```javascript
|
||||
// More consistent transformation pipeline
|
||||
transformRequest: [function (data, headers) {
|
||||
// Headers parameter always available
|
||||
// More predictable behavior
|
||||
return data;
|
||||
}]
|
||||
```
|
||||
|
||||
### 4. Browser Support Changes
|
||||
|
||||
- **0.x**: Supported IE11 and older browsers
|
||||
- **1.x**: Requires modern browsers with Promise support
|
||||
- **Polyfills**: May be needed for older browser support
|
||||
|
||||
## Error Handling Migration
|
||||
|
||||
The error handling changes are the most complex part of migrating to Axios 1.x. Here are proven strategies:
|
||||
|
||||
### Strategy 1: Centralized Error Handling with Error Boundary
|
||||
|
||||
```javascript
|
||||
// Create a centralized error handler
|
||||
class ApiErrorHandler {
|
||||
constructor() {
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
setupInterceptors() {
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Centralized error processing
|
||||
this.processError(error);
|
||||
|
||||
// Return a resolved promise with error info for handled errors
|
||||
if (this.isHandledError(error)) {
|
||||
return Promise.resolve({
|
||||
data: null,
|
||||
error: this.normalizeError(error),
|
||||
handled: true
|
||||
});
|
||||
}
|
||||
|
||||
// Re-throw unhandled errors
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
processError(error) {
|
||||
// Log errors
|
||||
console.error('API Error:', error);
|
||||
|
||||
// Show user notifications
|
||||
if (error.response?.status === 401) {
|
||||
this.handleAuthError();
|
||||
} else if (error.response?.status >= 500) {
|
||||
this.showErrorNotification('Server error occurred');
|
||||
}
|
||||
}
|
||||
|
||||
isHandledError(error) {
|
||||
// Define which errors are "handled" centrally
|
||||
const handledStatuses = [401, 403, 404, 422, 500, 502, 503];
|
||||
return handledStatuses.includes(error.response?.status);
|
||||
}
|
||||
|
||||
normalizeError(error) {
|
||||
return {
|
||||
status: error.response?.status,
|
||||
message: error.response?.data?.message || error.message,
|
||||
code: error.response?.data?.code || error.code
|
||||
};
|
||||
}
|
||||
|
||||
handleAuthError() {
|
||||
// Redirect to login, clear tokens, etc.
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
showErrorNotification(message) {
|
||||
// Show user-friendly error message
|
||||
console.error(message); // Replace with your notification system
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize globally
|
||||
const errorHandler = new ApiErrorHandler();
|
||||
|
||||
// Usage in components/services
|
||||
async function fetchUserData(userId) {
|
||||
try {
|
||||
const response = await axios.get(`/api/users/${userId}`);
|
||||
|
||||
// Check if error was handled centrally
|
||||
if (response.handled) {
|
||||
return { data: null, error: response.error };
|
||||
}
|
||||
|
||||
return { data: response.data, error: null };
|
||||
} catch (error) {
|
||||
// Unhandled errors still need local handling
|
||||
return { data: null, error: { message: 'Unexpected error occurred' } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 2: Wrapper Function Pattern
|
||||
|
||||
```javascript
|
||||
// Create a wrapper that provides 0.x-like behavior
|
||||
function createApiWrapper() {
|
||||
const api = axios.create();
|
||||
|
||||
// Add response interceptor for centralized handling
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Handle common errors centrally
|
||||
if (error.response?.status === 401) {
|
||||
// Handle auth errors
|
||||
handleAuthError();
|
||||
}
|
||||
|
||||
if (error.response?.status >= 500) {
|
||||
// Handle server errors
|
||||
showServerErrorNotification();
|
||||
}
|
||||
|
||||
// Always reject to maintain error propagation
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Wrapper function that mimics 0.x behavior
|
||||
function safeRequest(requestConfig, options = {}) {
|
||||
return api(requestConfig)
|
||||
.then(response => response)
|
||||
.catch(error => {
|
||||
if (options.suppressErrors) {
|
||||
// Return error info instead of throwing
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
status: error.response?.status,
|
||||
message: error.response?.data?.message || error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return { safeRequest, axios: api };
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { safeRequest } = createApiWrapper();
|
||||
|
||||
// For calls where you want centralized error handling
|
||||
const result = await safeRequest(
|
||||
{ method: 'get', url: '/api/data' },
|
||||
{ suppressErrors: true }
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
// Handle error case
|
||||
console.log('Request failed:', result.error.message);
|
||||
} else {
|
||||
// Handle success case
|
||||
console.log('Data:', result.data);
|
||||
}
|
||||
```
|
||||
|
||||
### Strategy 3: Global Error Handler with Custom Events
|
||||
|
||||
```javascript
|
||||
// Set up global error handling with events
|
||||
class GlobalErrorHandler extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
setupInterceptors() {
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Emit custom event for global handling
|
||||
this.dispatchEvent(new CustomEvent('apiError', {
|
||||
detail: { error, timestamp: new Date() }
|
||||
}));
|
||||
|
||||
// Always reject to maintain proper error flow
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const globalErrorHandler = new GlobalErrorHandler();
|
||||
|
||||
// Set up global listeners
|
||||
globalErrorHandler.addEventListener('apiError', (event) => {
|
||||
const { error } = event.detail;
|
||||
|
||||
// Centralized error logic
|
||||
if (error.response?.status === 401) {
|
||||
handleAuthError();
|
||||
}
|
||||
|
||||
if (error.response?.status >= 500) {
|
||||
showErrorNotification('Server error occurred');
|
||||
}
|
||||
});
|
||||
|
||||
// Usage remains clean
|
||||
async function apiCall() {
|
||||
try {
|
||||
const response = await axios.get('/api/data');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// Error was already handled globally
|
||||
// Just handle component-specific logic
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Changes
|
||||
|
||||
### Request Configuration
|
||||
|
||||
#### 0.x to 1.x Changes
|
||||
```javascript
|
||||
// 0.x - Some properties had different defaults
|
||||
const config = {
|
||||
timeout: 0, // No timeout by default
|
||||
maxContentLength: -1, // No limit
|
||||
};
|
||||
|
||||
// 1.x - More secure defaults
|
||||
const config = {
|
||||
timeout: 0, // Still no timeout, but easier to configure
|
||||
maxContentLength: 2000, // Default limit for security
|
||||
maxBodyLength: 2000, // New property
|
||||
};
|
||||
```
|
||||
|
||||
### Response Object
|
||||
|
||||
The response object structure remains largely the same, but error responses are more consistent:
|
||||
|
||||
```javascript
|
||||
// Both 0.x and 1.x
|
||||
response = {
|
||||
data: {}, // Response body
|
||||
status: 200, // HTTP status
|
||||
statusText: 'OK', // HTTP status message
|
||||
headers: {}, // Response headers
|
||||
config: {}, // Request config
|
||||
request: {} // Request object
|
||||
};
|
||||
|
||||
// Error responses are more consistent in 1.x
|
||||
error.response = {
|
||||
data: {}, // Error response body
|
||||
status: 404, // HTTP error status
|
||||
statusText: 'Not Found',
|
||||
headers: {},
|
||||
config: {},
|
||||
request: {}
|
||||
};
|
||||
```
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
### Default Configuration Updates
|
||||
|
||||
```javascript
|
||||
// 0.x defaults
|
||||
axios.defaults.timeout = 0; // No timeout
|
||||
axios.defaults.maxContentLength = -1; // No limit
|
||||
|
||||
// 1.x defaults (more secure)
|
||||
axios.defaults.timeout = 0; // Still no timeout
|
||||
axios.defaults.maxContentLength = 2000; // 2MB limit
|
||||
axios.defaults.maxBodyLength = 2000; // 2MB limit
|
||||
```
|
||||
|
||||
### Instance Configuration
|
||||
|
||||
```javascript
|
||||
// 0.x - Instance creation
|
||||
const api = axios.create({
|
||||
baseURL: 'https://api.example.com',
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
// 1.x - Same API, but more options available
|
||||
const api = axios.create({
|
||||
baseURL: 'https://api.example.com',
|
||||
timeout: 1000,
|
||||
maxBodyLength: Infinity, // Override default if needed
|
||||
maxContentLength: Infinity,
|
||||
});
|
||||
```
|
||||
|
||||
## Migration Strategies
|
||||
|
||||
### Step-by-Step Migration Process
|
||||
|
||||
#### Phase 1: Preparation
|
||||
1. **Audit Current Error Handling**
|
||||
```bash
|
||||
# Find all axios usage
|
||||
grep -r "axios\." src/
|
||||
grep -r "\.catch" src/
|
||||
grep -r "interceptors" src/
|
||||
```
|
||||
|
||||
2. **Identify Patterns**
|
||||
- Response interceptors that handle errors
|
||||
- Components that rely on centralized error handling
|
||||
- Authentication and retry logic
|
||||
|
||||
3. **Create Test Cases**
|
||||
```javascript
|
||||
// Test current error handling behavior
|
||||
describe('Error Handling Migration', () => {
|
||||
it('should handle 401 errors consistently', async () => {
|
||||
// Test authentication error flows
|
||||
});
|
||||
|
||||
it('should handle 500 errors with user feedback', async () => {
|
||||
// Test server error handling
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Phase 2: Implementation
|
||||
1. **Update Dependencies**
|
||||
```bash
|
||||
npm update axios
|
||||
```
|
||||
|
||||
2. **Implement New Error Handling**
|
||||
- Choose one of the strategies above
|
||||
- Update response interceptors
|
||||
- Add error handling to API calls
|
||||
|
||||
3. **Update Authentication Logic**
|
||||
```javascript
|
||||
// 0.x pattern
|
||||
axios.interceptors.response.use(null, error => {
|
||||
if (error.response?.status === 401) {
|
||||
logout();
|
||||
// Error was "handled"
|
||||
}
|
||||
});
|
||||
|
||||
// 1.x pattern
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
if (error.response?.status === 401) {
|
||||
logout();
|
||||
}
|
||||
return Promise.reject(error); // Always propagate
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Phase 3: Testing and Validation
|
||||
1. **Test Error Scenarios**
|
||||
- Network failures
|
||||
- HTTP error codes (401, 403, 404, 500, etc.)
|
||||
- Timeout errors
|
||||
- JSON parsing errors
|
||||
|
||||
2. **Validate User Experience**
|
||||
- Error messages are shown appropriately
|
||||
- Authentication redirects work
|
||||
- Loading states are handled correctly
|
||||
|
||||
### Gradual Migration Approach
|
||||
|
||||
For large applications, consider gradual migration:
|
||||
|
||||
```javascript
|
||||
// Create a compatibility layer
|
||||
const axiosCompat = {
|
||||
// Use new axios instance for new code
|
||||
v1: axios.create({
|
||||
// 1.x configuration
|
||||
}),
|
||||
|
||||
// Wrapper for legacy code
|
||||
legacy: createLegacyWrapper(axios.create({
|
||||
// Configuration that mimics 0.x behavior
|
||||
}))
|
||||
};
|
||||
|
||||
function createLegacyWrapper(axiosInstance) {
|
||||
// Add interceptors that provide 0.x-like behavior
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
// Handle errors in 0.x style for legacy code
|
||||
handleLegacyError(error);
|
||||
// Don't propagate certain errors
|
||||
if (shouldSuppressError(error)) {
|
||||
return Promise.resolve({ data: null, error: true });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return axiosInstance;
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authentication Interceptors
|
||||
|
||||
#### Updated Authentication Pattern
|
||||
```javascript
|
||||
// Token refresh interceptor for 1.x
|
||||
let isRefreshing = false;
|
||||
let refreshSubscribers = [];
|
||||
|
||||
function subscribeTokenRefresh(cb) {
|
||||
refreshSubscribers.push(cb);
|
||||
}
|
||||
|
||||
function onTokenRefreshed(token) {
|
||||
refreshSubscribers.forEach(cb => cb(token));
|
||||
refreshSubscribers = [];
|
||||
}
|
||||
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
if (isRefreshing) {
|
||||
// Wait for token refresh
|
||||
return new Promise(resolve => {
|
||||
subscribeTokenRefresh(token => {
|
||||
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||
resolve(axios(originalRequest));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
originalRequest._retry = true;
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const newToken = await refreshToken();
|
||||
onTokenRefreshed(newToken);
|
||||
isRefreshing = false;
|
||||
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
||||
return axios(originalRequest);
|
||||
} catch (refreshError) {
|
||||
isRefreshing = false;
|
||||
logout();
|
||||
return Promise.reject(refreshError);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Retry Logic
|
||||
|
||||
```javascript
|
||||
// Retry interceptor for 1.x
|
||||
function createRetryInterceptor(maxRetries = 3, retryDelay = 1000) {
|
||||
return axios.interceptors.response.use(
|
||||
response => response,
|
||||
async error => {
|
||||
const config = error.config;
|
||||
|
||||
if (!config || !config.retry) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
config.__retryCount = config.__retryCount || 0;
|
||||
|
||||
if (config.__retryCount >= maxRetries) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
config.__retryCount += 1;
|
||||
|
||||
// Exponential backoff
|
||||
const delay = retryDelay * Math.pow(2, config.__retryCount - 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
return axios(config);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
const api = axios.create();
|
||||
createRetryInterceptor(3, 1000);
|
||||
|
||||
// Make request with retry
|
||||
api.get('/api/data', { retry: true });
|
||||
```
|
||||
|
||||
### Loading State Management
|
||||
|
||||
```javascript
|
||||
// Loading interceptor for 1.x
|
||||
class LoadingManager {
|
||||
constructor() {
|
||||
this.requests = new Set();
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
setupInterceptors() {
|
||||
axios.interceptors.request.use(config => {
|
||||
this.requests.add(config);
|
||||
this.updateLoadingState();
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
this.requests.delete(response.config);
|
||||
this.updateLoadingState();
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
this.requests.delete(error.config);
|
||||
this.updateLoadingState();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateLoadingState() {
|
||||
const isLoading = this.requests.size > 0;
|
||||
// Update your loading UI
|
||||
document.body.classList.toggle('loading', isLoading);
|
||||
}
|
||||
}
|
||||
|
||||
const loadingManager = new LoadingManager();
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Migration Issues
|
||||
|
||||
#### Issue 1: Unhandled Promise Rejections
|
||||
|
||||
**Problem:**
|
||||
```javascript
|
||||
// This pattern worked in 0.x but causes unhandled rejections in 1.x
|
||||
axios.get('/api/data'); // No .catch() handler
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```javascript
|
||||
// Always handle promises
|
||||
axios.get('/api/data')
|
||||
.catch(error => {
|
||||
// Handle error appropriately
|
||||
console.error('Request failed:', error.message);
|
||||
});
|
||||
|
||||
// Or use async/await with try/catch
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await axios.get('/api/data');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Issue 2: Response Interceptors Not "Handling" Errors
|
||||
|
||||
**Problem:**
|
||||
```javascript
|
||||
// 0.x style - interceptor "handled" errors
|
||||
axios.interceptors.response.use(null, error => {
|
||||
showErrorMessage(error.message);
|
||||
// Error was considered "handled"
|
||||
});
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```javascript
|
||||
// 1.x style - explicitly control error propagation
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
showErrorMessage(error.message);
|
||||
|
||||
// Choose whether to propagate the error
|
||||
if (shouldPropagateError(error)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Return success-like response for "handled" errors
|
||||
return Promise.resolve({
|
||||
data: null,
|
||||
handled: true,
|
||||
error: normalizeError(error)
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Issue 3: JSON Parsing Errors
|
||||
|
||||
**Problem:**
|
||||
```javascript
|
||||
// 1.x is stricter about JSON parsing
|
||||
// This might throw where 0.x was lenient
|
||||
const data = response.data;
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```javascript
|
||||
// Add response transformer for better error handling
|
||||
axios.defaults.transformResponse = [
|
||||
function (data) {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
// Handle JSON parsing errors gracefully
|
||||
console.warn('Invalid JSON response:', data);
|
||||
return { error: 'Invalid JSON', rawData: data };
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
#### Issue 4: TypeScript Errors After Upgrade
|
||||
|
||||
**Problem:**
|
||||
```typescript
|
||||
// TypeScript errors after upgrade
|
||||
const response = await axios.get('/api/data');
|
||||
// Property 'someProperty' does not exist on type 'any'
|
||||
```
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Define proper interfaces
|
||||
interface ApiResponse {
|
||||
data: any;
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const response = await axios.get<ApiResponse>('/api/data');
|
||||
// Now properly typed
|
||||
console.log(response.data.data);
|
||||
```
|
||||
|
||||
### Debug Migration Issues
|
||||
|
||||
#### Enable Debug Logging
|
||||
```javascript
|
||||
// Add request/response logging
|
||||
axios.interceptors.request.use(config => {
|
||||
console.log('Request:', config);
|
||||
return config;
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
console.log('Response:', response);
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
console.log('Error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
#### Compare Behavior
|
||||
```javascript
|
||||
// Create side-by-side comparison during migration
|
||||
const axios0x = require('axios-0x'); // Keep old version for testing
|
||||
const axios1x = require('axios');
|
||||
|
||||
async function compareRequests(config) {
|
||||
try {
|
||||
const [result0x, result1x] = await Promise.allSettled([
|
||||
axios0x(config),
|
||||
axios1x(config)
|
||||
]);
|
||||
|
||||
console.log('0.x result:', result0x);
|
||||
console.log('1.x result:', result1x);
|
||||
} catch (error) {
|
||||
console.log('Comparison error:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### Official Documentation
|
||||
- [Axios 1.x Documentation](https://axios-http.com/)
|
||||
- [Axios GitHub Repository](https://github.com/axios/axios)
|
||||
- [Axios Changelog](https://github.com/axios/axios/blob/main/CHANGELOG.md)
|
||||
|
||||
### Migration Tools
|
||||
- [Axios Migration Codemod](https://github.com/axios/axios-migration-codemod) *(if available)*
|
||||
- [ESLint Rules for Axios 1.x](https://github.com/axios/eslint-plugin-axios) *(if available)*
|
||||
|
||||
### Community Resources
|
||||
- [Stack Overflow - Axios Migration Questions](https://stackoverflow.com/questions/tagged/axios+migration)
|
||||
- [GitHub Discussions](https://github.com/axios/axios/discussions)
|
||||
- [Axios Discord Community](https://discord.gg/axios) *(if available)*
|
||||
|
||||
### Related Issues
|
||||
- [Error Handling Changes Discussion](https://github.com/axios/axios/issues/7208)
|
||||
- [Migration Guide Request](https://github.com/axios/axios/issues/xxxx) *(link to related issues)*
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
If you encounter issues during migration that aren't covered in this guide:
|
||||
|
||||
1. **Search existing issues** in the [Axios GitHub repository](https://github.com/axios/axios/issues)
|
||||
2. **Ask questions** in [GitHub Discussions](https://github.com/axios/axios/discussions)
|
||||
3. **Contribute improvements** to this migration guide
|
||||
|
||||
---
|
||||
|
||||
*This migration guide is maintained by the community. If you find errors or have suggestions, please [open an issue](https://github.com/axios/axios/issues) or submit a pull request.*
|
||||
+2426
File diff suppressed because it is too large.
Load diff
+4892
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"axios.js","sources":["../lib/helpers/bind.js","../lib/utils.js","../lib/helpers/parseHeaders.js","../lib/helpers/sanitizeHeaderValue.js","../lib/core/AxiosHeaders.js","../lib/core/AxiosError.js","../lib/helpers/null.js","../lib/helpers/toFormData.js","../lib/helpers/AxiosURLSearchParams.js","../lib/helpers/buildURL.js","../lib/core/InterceptorManager.js","../lib/defaults/transitional.js","../lib/platform/browser/classes/URLSearchParams.js","../lib/platform/browser/classes/FormData.js","../lib/platform/browser/classes/Blob.js","../lib/platform/browser/index.js","../lib/platform/common/utils.js","../lib/platform/index.js","../lib/helpers/toURLEncodedForm.js","../lib/helpers/formDataToJSON.js","../lib/defaults/index.js","../lib/core/transformData.js","../lib/cancel/isCancel.js","../lib/cancel/CanceledError.js","../lib/core/settle.js","../lib/helpers/parseProtocol.js","../lib/helpers/speedometer.js","../lib/helpers/throttle.js","../lib/helpers/progressEventReducer.js","../lib/helpers/isURLSameOrigin.js","../lib/helpers/cookies.js","../lib/helpers/isAbsoluteURL.js","../lib/helpers/combineURLs.js","../lib/core/buildFullPath.js","../lib/core/mergeConfig.js","../lib/helpers/resolveConfig.js","../lib/adapters/xhr.js","../lib/helpers/composeSignals.js","../lib/helpers/trackStream.js","../lib/helpers/estimateDataURLDecodedBytes.js","../lib/env/data.js","../lib/adapters/fetch.js","../lib/adapters/adapters.js","../lib/core/dispatchRequest.js","../lib/helpers/validator.js","../lib/core/Axios.js","../lib/cancel/CancelToken.js","../lib/helpers/spread.js","../lib/helpers/isAxiosError.js","../lib/helpers/HttpStatusCode.js","../lib/axios.js"],"sourcesContent":["'use strict';\n\n/**\n * Create a bound version of a function with a specified `this` context\n *\n * @param {Function} fn - The function to bind\n * @param {*} thisArg - The value to be passed as the `this` parameter\n * @returns {Function} A new function that will call the original function with the specified `this` context\n */\nexport default function bind(fn, thisArg) {\n return function wrap() {\n return fn.apply(thisArg, arguments);\n };\n}\n","'use strict';\n\nimport bind from './helpers/bind.js';\n\n// utils is a library of generic helper functions non-specific to axios\n\nconst { toString } = Object.prototype;\nconst { getPrototypeOf } = Object;\nconst { iterator, toStringTag } = Symbol;\n\nconst kindOf = ((cache) => (thing) => {\n const str = toString.call(thing);\n return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());\n})(Object.create(null));\n\nconst kindOfTest = (type) => {\n type = type.toLowerCase();\n return (thing) => kindOf(thing) === type;\n};\n\nconst typeOfTest = (type) => (thing) => typeof thing === type;\n\n/**\n * Determine if a value is a non-null object\n *\n * @param {Object} val The value to test\n *\n * @returns {boolean} True if value is an Array, otherwise false\n */\nconst { isArray } = Array;\n\n/**\n * Determine if a value is undefined\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if the value is undefined, otherwise false\n */\nconst isUndefined = typeOfTest('undefined');\n\n/**\n * Determine if a value is a Buffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Buffer, otherwise false\n */\nfunction isBuffer(val) {\n return (\n val !== null &&\n !isUndefined(val) &&\n val.constructor !== null &&\n !isUndefined(val.constructor) &&\n isFunction(val.constructor.isBuffer) &&\n val.constructor.isBuffer(val)\n );\n}\n\n/**\n * Determine if a value is an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is an ArrayBuffer, otherwise false\n */\nconst isArrayBuffer = kindOfTest('ArrayBuffer');\n\n/**\n * Determine if a value is a view on an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false\n */\nfunction isArrayBufferView(val) {\n let result;\n if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {\n result = ArrayBuffer.isView(val);\n } else {\n result = val && val.buffer && isArrayBuffer(val.buffer);\n }\n return result;\n}\n\n/**\n * Determine if a value is a String\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a String, otherwise false\n */\nconst isString = typeOfTest('string');\n\n/**\n * Determine if a value is a Function\n *\n * @param {*} val The value to test\n * @returns {boolean} True if value is a Function, otherwise false\n */\nconst isFunction = typeOfTest('function');\n\n/**\n * Determine if a value is a Number\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Number, otherwise false\n */\nconst isNumber = typeOfTest('number');\n\n/**\n * Determine if a value is an Object\n *\n * @param {*} thing The value to test\n *\n * @returns {boolean} True if value is an Object, othLine truncated
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/*! Axios v1.16.1 Copyright (c) 2026 Matt Zabriskie and contributors */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).axios=t()}(this,function(){"use strict";function e(e,t){this.v=e,this.k=t}function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function n(t){var n={},r=!1;function o(n,o){return r=!0,o=new Promise(function(e){e(t[n](o))}),{done:!1,value:new e(o,1)}}return n["undefined"!=typeof Symbol&&Symbol.iterator||"@@iterator"]=function(){return this},n.next=function(e){return r?(r=!1,e):o("next",e)},"function"==typeof t.throw&&(n.throw=function(e){if(r)throw r=!1,e;return o("throw",e)}),"function"==typeof t.return&&(n.return=function(e){return r?(r=!1,e):o("return",e)}),n}function r(e){var t,n,r,i=2;for("undefined"!=typeof Symbol&&(n=Symbol.asyncIterator,r=Symbol.iterator);i--;){if(n&&null!=(t=e[n]))return t.call(e);if(r&&null!=(t=e[r]))return new o(t.call(e));n="@@asyncIterator",r="@@iterator"}throw new TypeError("Object is not async iterable")}function o(e){function t(e){if(Object(e)!==e)return Promise.reject(new TypeError(e+" is not an object."));var t=e.done;return Promise.resolve(e.value).then(function(e){return{value:e,done:t}})}return o=function(e){this.s=e,this.n=e.next},o.prototype={s:null,n:null,next:function(){return t(this.n.apply(this.s,arguments))},return:function(e){var n=this.s.return;return void 0===n?Promise.resolve({value:e,done:!0}):t(n.apply(this.s,arguments))},throw:function(e){var n=this.s.return;return void 0===n?Promise.reject(e):t(n.apply(this.s,arguments))}},new o(e)}function i(e,t,n,r,o,i,a){try{var u=e[i](a),s=u.value}catch(e){return void n(e)}u.done?t(s):Promise.resolve(s).then(r,o)}function a(e){return function(){var t=this,n=arguments;return new Promise(function(r,o){var a=e.apply(t,n);function u(e){i(a,r,o,u,s,"next",e)}function s(e){i(a,r,o,u,s,"throw",e)}u(void 0)})}}function u(t){return new e(t,0)}function s(e,t,n){return t=p(t),function(e,t){if(t&&("object"==typeof t||"function"==typeof t))return t;if(void 0!==t)throw new TypeError("Derived constructors may only return object or undefined");return function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e)}(e,y()?Reflect.construct(t,n||[],p(e).constructor):t.apply(e,n))}function c(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function f(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,S(r.key),r)}}function l(e,t,n){return t&&f(e.prototype,t),n&&f(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function d(e,t,n){return(t=S(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function p(e){return p=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},p(e)}function h(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&O(e,t)}function y(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch(e){}return(y=function(){return!!e})()}function v(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function b(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?v(Object(n),!0).forEach(function(t){d(e,t,n[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):v(Object(n)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))})}return e}function m(){
|
||||
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
||||
var e,t,n="function"==typeof Symbol?Symbol:{},r=n.iterator||"@@iterator",o=n.toStringTag||"@@toStringTag";function i(n,r,o,i){var s=r&&r.prototype instanceof u?r:u,c=Object.create(s.prototype);return g(c,"_invoke",function(n,r,o){var i,u,s,c=0,f=o||[],l=!1,d={p:0,n:0,v:e,a:p,f:p.bind(e,4),d:function(t,n){return i=t,u=0,s=e,d.n=n,a}};function p(n,r){for(u=n,s=r,t=0;!l&&c&&!o&&t<f.length;t++){var o,i=f[t],p=d.p,h=i[2];n>3?(o=h===r)&&(s=i[(u=i[4])?5:(u=3,3)],i[4]=i[5]=e):i[0]<=p&&((o=n<2&&p<i[1])?(u=0,d.v=r,d.n=i[1]):p<h&&(o=n<3||i[0]>r||r>h)&&(i[4]=n,i[5]=r,d.n=h,u=0))}if(o||n>1)return a;throw l=!0,r}return function(o,f,h){if(c>1)throw TypeError("Generator is already running");for(l&&1===f&&p(f,h),u=f,s=h;(t=u<2?e:s)||!l;){i||(u?u<3?(u>1&&(d.n=-1),p(u,s)):d.n=s:d.v=s);try{if(c=2,i){if(u||(o="next"),t=i[o]){if(!(t=t.call(i,s)))throw TypeError("iterator result is not an object");if(!t.done)return t;s=t.value,u<2&&(u=0)}else 1===u&&(t=i.return)&&t.call(i),u<2&&(s=TypeError("The iterator does not provide a '"+o+"' method"),u=1);i=e}else if((t=(l=d.n<0)?s:n.call(r,d))!==a)break}catch(t){i=e,u=1,s=t}finally{c=1}}return{value:t,done:l}}}(n,o,i),!0),c}var a={};function u(){}function s(){}function c(){}t=Object.getPrototypeOf;var f=[][r]?t(t([][r]())):(g(t={},r,function(){return this}),t),l=c.prototype=u.prototype=Object.create(f);function d(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,c):(e.__proto__=c,g(e,o,"GeneratorFunction")),e.prototype=Object.create(l),e}return s.prototype=c,g(l,"constructor",c),g(c,"constructor",s),s.displayName="GeneratorFunction",g(c,o,"GeneratorFunction"),g(l),g(l,o,"Generator"),g(l,r,function(){return this}),g(l,"toString",function(){return"[object Generator]"}),(m=function(){return{w:i,m:d}})()}function g(e,t,n,r){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}g=function(e,t,n,r){function i(t,n){g(e,t,function(e){return this._invoke(t,n,e)})}t?o?o(e,t,{value:n,enumerable:!r,configurable:!r,writable:!r}):e[t]=n:(i("next",0),i("throw",1),i("return",2))},g(e,t,n,r)}function w(e){if(null!=e){var t=e["function"==typeof Symbol&&Symbol.iterator||"@@iterator"],n=0;if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length))return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}}}throw new TypeError(typeof e+" is not iterable")}function O(e,t){return O=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},O(e,t)}function E(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var n=null==e?null:"undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=n){var r,o,i,a,u=[],s=!0,c=!1;try{if(i=(n=n.call(e)).next,0===t){if(Object(n)!==n)return;s=!1}else for(;!(s=(r=i.call(n)).done)&&(u.push(r.value),u.length!==t);s=!0);}catch(e){c=!0,o=e}finally{try{if(!s&&null!=n.return&&(a=n.return(),Object(a)!==a))return}finally{if(c)throw o}}return u}}(e,t)||A(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function R(e){return function(e){if(Array.isArray(e))return t(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||A(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function S(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t);if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}function _(e){return _="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},_(e)}function A(e,n){if(e){if("string"==typeof e)return t(e,n);var r={}.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(e,n):void 0}}function T(e){return function(){return new j(e.apply(this,arguments))}}function j(t){var n,r;function o(n,r){try{var a=t[n](r),u=a.value,s=u instanceof e;Promise.resolve(s?u.v:u).then(function(e){if(s){var r="return"===n?"return":"next";if(!u.k||e.done)return o(r,e);e=t[r](e).value}i(a.done?"return":"normal",e)},function(e){o("throw",e)})}catch(e){i("throw",e)}}function i(e,t){switch(e){case"return":n.resolve({value:t,done:!0});break;case"throw":n.reject(t);break;default:n.resolve({value:t,done:!1})}(n=n.next)?o(n.key,n.arg):r=null}this._invoke=function(e,t){return new Promise(function(i,a){var u={key:e,arg:t,resolve:i,reject:a,next:null};r?r=r.next=u:(n=r=u,o(e,t))})},"function"!=typeof t.reLine truncated
|
||||
//# sourceMappingURL=axios.min.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"axios.min.js","sources":["../lib/helpers/bind.js","../lib/utils.js","../lib/helpers/parseHeaders.js","../lib/helpers/sanitizeHeaderValue.js","../lib/core/AxiosHeaders.js","../lib/core/AxiosError.js","../lib/helpers/toFormData.js","../lib/helpers/AxiosURLSearchParams.js","../lib/helpers/buildURL.js","../lib/core/InterceptorManager.js","../lib/defaults/transitional.js","../lib/platform/browser/index.js","../lib/platform/browser/classes/URLSearchParams.js","../lib/platform/browser/classes/FormData.js","../lib/platform/browser/classes/Blob.js","../lib/platform/common/utils.js","../lib/platform/index.js","../lib/helpers/formDataToJSON.js","../lib/defaults/index.js","../lib/helpers/toURLEncodedForm.js","../lib/core/transformData.js","../lib/cancel/isCancel.js","../lib/cancel/CanceledError.js","../lib/core/settle.js","../lib/helpers/progressEventReducer.js","../lib/helpers/speedometer.js","../lib/helpers/throttle.js","../lib/helpers/isURLSameOrigin.js","../lib/helpers/cookies.js","../lib/core/buildFullPath.js","../lib/helpers/isAbsoluteURL.js","../lib/helpers/combineURLs.js","../lib/core/mergeConfig.js","../lib/helpers/resolveConfig.js","../lib/adapters/xhr.js","../lib/helpers/parseProtocol.js","../lib/helpers/composeSignals.js","../lib/helpers/trackStream.js","../lib/helpers/estimateDataURLDecodedBytes.js","../lib/env/data.js","../lib/adapters/fetch.js","../lib/adapters/adapters.js","../lib/helpers/null.js","../lib/core/dispatchRequest.js","../lib/helpers/validator.js","../lib/core/Axios.js","../lib/cancel/CancelToken.js","../lib/helpers/HttpStatusCode.js","../lib/axios.js","../lib/helpers/spread.js","../lib/helpers/isAxiosError.js"],"sourcesContent":["'use strict';\n\n/**\n * Create a bound version of a function with a specified `this` context\n *\n * @param {Function} fn - The function to bind\n * @param {*} thisArg - The value to be passed as the `this` parameter\n * @returns {Function} A new function that will call the original function with the specified `this` context\n */\nexport default function bind(fn, thisArg) {\n return function wrap() {\n return fn.apply(thisArg, arguments);\n };\n}\n","'use strict';\n\nimport bind from './helpers/bind.js';\n\n// utils is a library of generic helper functions non-specific to axios\n\nconst { toString } = Object.prototype;\nconst { getPrototypeOf } = Object;\nconst { iterator, toStringTag } = Symbol;\n\nconst kindOf = ((cache) => (thing) => {\n const str = toString.call(thing);\n return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());\n})(Object.create(null));\n\nconst kindOfTest = (type) => {\n type = type.toLowerCase();\n return (thing) => kindOf(thing) === type;\n};\n\nconst typeOfTest = (type) => (thing) => typeof thing === type;\n\n/**\n * Determine if a value is a non-null object\n *\n * @param {Object} val The value to test\n *\n * @returns {boolean} True if value is an Array, otherwise false\n */\nconst { isArray } = Array;\n\n/**\n * Determine if a value is undefined\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if the value is undefined, otherwise false\n */\nconst isUndefined = typeOfTest('undefined');\n\n/**\n * Determine if a value is a Buffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Buffer, otherwise false\n */\nfunction isBuffer(val) {\n return (\n val !== null &&\n !isUndefined(val) &&\n val.constructor !== null &&\n !isUndefined(val.constructor) &&\n isFunction(val.constructor.isBuffer) &&\n val.constructor.isBuffer(val)\n );\n}\n\n/**\n * Determine if a value is an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is an ArrayBuffer, otherwise false\n */\nconst isArrayBuffer = kindOfTest('ArrayBuffer');\n\n/**\n * Determine if a value is a view on an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false\n */\nfunction isArrayBufferView(val) {\n let result;\n if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {\n result = ArrayBuffer.isView(val);\n } else {\n result = val && val.buffer && isArrayBuffer(val.buffer);\n }\n return result;\n}\n\n/**\n * Determine if a value is a String\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a String, otherwise false\n */\nconst isString = typeOfTest('string');\n\n/**\n * Determine if a value is a Function\n *\n * @param {*} val The value to test\n * @returns {boolean} True if value is a Function, otherwise false\n */\nconst isFunction = typeOfTest('function');\n\n/**\n * Determine if a value is a Number\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Number, otherwise false\n */\nconst isNumber = typeOfTest('number');\n\n/**\n * Determine if a value is an Object\n *\n * @param {*} thing The value to test\n *\n * @returns {boolean} True if value is an Object,Line truncated
|
||||
+4733
File diff suppressed because it is too large.
Load diff
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"axios.cjs","sources":["../../lib/helpers/bind.js","../../lib/utils.js","../../lib/helpers/parseHeaders.js","../../lib/helpers/sanitizeHeaderValue.js","../../lib/core/AxiosHeaders.js","../../lib/core/AxiosError.js","../../lib/helpers/null.js","../../lib/helpers/toFormData.js","../../lib/helpers/AxiosURLSearchParams.js","../../lib/helpers/buildURL.js","../../lib/core/InterceptorManager.js","../../lib/defaults/transitional.js","../../lib/platform/browser/classes/URLSearchParams.js","../../lib/platform/browser/classes/FormData.js","../../lib/platform/browser/classes/Blob.js","../../lib/platform/browser/index.js","../../lib/platform/common/utils.js","../../lib/platform/index.js","../../lib/helpers/toURLEncodedForm.js","../../lib/helpers/formDataToJSON.js","../../lib/defaults/index.js","../../lib/core/transformData.js","../../lib/cancel/isCancel.js","../../lib/cancel/CanceledError.js","../../lib/core/settle.js","../../lib/helpers/parseProtocol.js","../../lib/helpers/speedometer.js","../../lib/helpers/throttle.js","../../lib/helpers/progressEventReducer.js","../../lib/helpers/isURLSameOrigin.js","../../lib/helpers/cookies.js","../../lib/helpers/isAbsoluteURL.js","../../lib/helpers/combineURLs.js","../../lib/core/buildFullPath.js","../../lib/core/mergeConfig.js","../../lib/helpers/resolveConfig.js","../../lib/adapters/xhr.js","../../lib/helpers/composeSignals.js","../../lib/helpers/trackStream.js","../../lib/helpers/estimateDataURLDecodedBytes.js","../../lib/env/data.js","../../lib/adapters/fetch.js","../../lib/adapters/adapters.js","../../lib/core/dispatchRequest.js","../../lib/helpers/validator.js","../../lib/core/Axios.js","../../lib/cancel/CancelToken.js","../../lib/helpers/spread.js","../../lib/helpers/isAxiosError.js","../../lib/helpers/HttpStatusCode.js","../../lib/axios.js"],"sourcesContent":["'use strict';\n\n/**\n * Create a bound version of a function with a specified `this` context\n *\n * @param {Function} fn - The function to bind\n * @param {*} thisArg - The value to be passed as the `this` parameter\n * @returns {Function} A new function that will call the original function with the specified `this` context\n */\nexport default function bind(fn, thisArg) {\n return function wrap() {\n return fn.apply(thisArg, arguments);\n };\n}\n","'use strict';\n\nimport bind from './helpers/bind.js';\n\n// utils is a library of generic helper functions non-specific to axios\n\nconst { toString } = Object.prototype;\nconst { getPrototypeOf } = Object;\nconst { iterator, toStringTag } = Symbol;\n\nconst kindOf = ((cache) => (thing) => {\n const str = toString.call(thing);\n return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());\n})(Object.create(null));\n\nconst kindOfTest = (type) => {\n type = type.toLowerCase();\n return (thing) => kindOf(thing) === type;\n};\n\nconst typeOfTest = (type) => (thing) => typeof thing === type;\n\n/**\n * Determine if a value is a non-null object\n *\n * @param {Object} val The value to test\n *\n * @returns {boolean} True if value is an Array, otherwise false\n */\nconst { isArray } = Array;\n\n/**\n * Determine if a value is undefined\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if the value is undefined, otherwise false\n */\nconst isUndefined = typeOfTest('undefined');\n\n/**\n * Determine if a value is a Buffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Buffer, otherwise false\n */\nfunction isBuffer(val) {\n return (\n val !== null &&\n !isUndefined(val) &&\n val.constructor !== null &&\n !isUndefined(val.constructor) &&\n isFunction(val.constructor.isBuffer) &&\n val.constructor.isBuffer(val)\n );\n}\n\n/**\n * Determine if a value is an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is an ArrayBuffer, otherwise false\n */\nconst isArrayBuffer = kindOfTest('ArrayBuffer');\n\n/**\n * Determine if a value is a view on an ArrayBuffer\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false\n */\nfunction isArrayBufferView(val) {\n let result;\n if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {\n result = ArrayBuffer.isView(val);\n } else {\n result = val && val.buffer && isArrayBuffer(val.buffer);\n }\n return result;\n}\n\n/**\n * Determine if a value is a String\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a String, otherwise false\n */\nconst isString = typeOfTest('string');\n\n/**\n * Determine if a value is a Function\n *\n * @param {*} val The value to test\n * @returns {boolean} True if value is a Function, otherwise false\n */\nconst isFunction = typeOfTest('function');\n\n/**\n * Determine if a value is a Number\n *\n * @param {*} val The value to test\n *\n * @returns {boolean} True if value is a Number, otherwise false\n */\nconst isNumber = typeOfTest('nuLine truncated
|
||||
Loaded 100 of 479 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user