feat: 初始化 sundynix-agentix 分层式 AI Agent 平台脚手架
5 层 + 1 条 NATS 零拷贝消息总线的 monorepo(Monolith First → Microservices Morph B)。 纵向主干(任务流 + Token 流回流)已真实跑通,横向各层能力为带注释的桩。 已贯通(real code): - sundynix-shared: 共享契约 + JetStream/core NATS 真实收发(bus) + 内嵌 NATS(devnats) + e2e 测试 - sundynix-gateway: Gin 接入 + DSL 解析组装 + NATS Publish + SSE 流式输出 - sundynix-dispatcher: NATS 消费 + Eino Orchestrator 流式回流 + 熔断器 + LLM Pool 占位流式 - 链路: HTTP POST → DSL → sundynix.tasks.* → Dispatcher → Token 经 sundynix.streams.<id> 回流 → SSE - 基础设施: docker-compose(nats/postgres/redis/neo4j/milvus) + Makefile(make demo/e2e) 待填(桩): - Eino 图编排 compose.NewGraph、LLM Pool 接 vLLM/Ollama - Gateway store 换真实 pgx/redis - sundynix-mcp-go: Bleve+Milvus+Neo4j 混合检索 / UniOffice / 外部 API - sundynix-mcp-py: gVisor 沙箱 / MinerU(PaddleOCR) / Docker 解释器 - sundynix-desktop: React Flow 画布 → DSL 导出 → SSE 展示
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Go
|
||||||
|
*.exe
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# Wails
|
||||||
|
build/bin/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# Data
|
||||||
|
data/
|
||||||
|
|
||||||
|
# 演示产物
|
||||||
|
.bin/
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GoImports">
|
||||||
|
<option name="excludedPackages">
|
||||||
|
<array>
|
||||||
|
<option value="golang.org/x/net/context" />
|
||||||
|
</array>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/sundynix_agentix.iml" filepath="$PROJECT_DIR$/.idea/sundynix_agentix.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.PHONY: infra infra-down devnats demo e2e gateway dispatcher mcp-go mcp-py desktop tidy
|
||||||
|
|
||||||
|
infra: ## 启动基础设施 (NATS / Postgres / Redis / Milvus / Neo4j)
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
infra-down:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
devnats: ## 启动内嵌 JetStream NATS(无 Docker 本地联调)
|
||||||
|
cd sundynix-shared && go run ./cmd/devnats
|
||||||
|
|
||||||
|
demo: ## 一键演示 Gateway→NATS→Dispatcher 任务流(无需 Docker)
|
||||||
|
bash scripts/demo.sh
|
||||||
|
|
||||||
|
e2e: ## 跑共享 bus 的端到端测试(内嵌 NATS)
|
||||||
|
cd sundynix-shared && go test ./bus/ -run 'TestTaskRoundTrip|TestTokenStreamRoundTrip' -v
|
||||||
|
|
||||||
|
gateway:
|
||||||
|
cd sundynix-gateway && go run ./cmd/server
|
||||||
|
|
||||||
|
dispatcher:
|
||||||
|
cd sundynix-dispatcher && go run ./cmd/dispatcher
|
||||||
|
|
||||||
|
mcp-go:
|
||||||
|
cd sundynix-mcp-go && go run ./cmd/server
|
||||||
|
|
||||||
|
mcp-py:
|
||||||
|
cd sundynix-mcp-py && python -m sundynix_mcp_py.main
|
||||||
|
|
||||||
|
desktop:
|
||||||
|
cd sundynix-desktop && wails dev
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
cd sundynix-gateway && go mod tidy
|
||||||
|
cd sundynix-dispatcher && go mod tidy
|
||||||
|
cd sundynix-mcp-go && go mod tidy
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# sundynix-agentix
|
||||||
|
|
||||||
|
分层式 AI Agent 平台 — **Monolith First → Microservices (Morph B)**。
|
||||||
|
架构总览见 [architecture.md](architecture.md) / [architecture.png](architecture.png)。
|
||||||
|
|
||||||
|
## 仓库结构(Monorepo)
|
||||||
|
|
||||||
|
| 目录 | 层 | 语言 / 技术栈 | 职责 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [`sundynix-desktop/`](sundynix-desktop) | 1 · Client | Wails + React 19 + TS + shadcn/ui | 桌面端、React Flow Agent 编排、导出 JSON DSL、LLM Wiki 面板 |
|
||||||
|
| [`sundynix-gateway/`](sundynix-gateway) | 2 · Gateway | Go · Gin | 统一接入、DSL 解析组装、计费、Guardrail、PgSQL + Redis |
|
||||||
|
| [`deploy/nats/`](deploy/nats) | 3 · Message Bus | NATS (Go) | 零拷贝骨干网:Queue + Stream |
|
||||||
|
| [`sundynix-dispatcher/`](sundynix-dispatcher) | 4 · Dispatcher | Go · Eino | 图编排、LLM Pool 调度、自动化评测、熔断降级 |
|
||||||
|
| [`sundynix-mcp-go/`](sundynix-mcp-go) | 5a · MCP Tools (I/O) | Go | MCP 网关、Wiki 混合检索(Bleve/Milvus/Neo4j)、UniOffice、外部 API |
|
||||||
|
| [`sundynix-mcp-py/`](sundynix-mcp-py) | 5b · MCP Tools (算法) | Python | MCP 网关、安全沙箱(gVisor/KataVM)、MinerU(PaddleOCR)、Docker 解释器 |
|
||||||
|
| [`sundynix-shared/`](sundynix-shared) | 共享契约 | Go | Task 数据契约、NATS subject 约定、JetStream 收发逻辑(bus)、内嵌 NATS(devnats) |
|
||||||
|
|
||||||
|
## 核心数据流
|
||||||
|
|
||||||
|
1. `Gateway` 解析 DSL → **Publish** `sundynix.tasks.*` (NATS Queue)
|
||||||
|
2. `Dispatcher` 订阅任务 → `Eino` 图编排 → 调用 `LLM Pool`
|
||||||
|
3. 经 NATS 调用第 5 层 `MCP Tools`
|
||||||
|
4. 结果以零拷贝 Token Stream 经 `sundynix.streams.task_id` 回流 → SSE/WS 推给 `Client`
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 无 Docker — 一键验证任务流(推荐先跑这个)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make demo # 内嵌NATS + Gateway + Dispatcher,提交一个 DSL 任务,看 Dispatcher 消费到
|
||||||
|
make e2e # 仅跑共享 bus 的端到端测试(go test,内嵌 NATS)
|
||||||
|
```
|
||||||
|
|
||||||
|
`make demo` 实测输出:
|
||||||
|
```
|
||||||
|
Gateway: POST /api/v1/tasks → task_xxx → published (seq=1)
|
||||||
|
Dispatcher: [eino] task_xxx received → streaming tokens...
|
||||||
|
SSE 客户端: event:token data:已 event:token data:编 ... event:done ← 流式闭环打通
|
||||||
|
```
|
||||||
|
完整链路:HTTP POST → DSL 解析 → NATS 任务队列 → Dispatcher 消费 → LLM 流式推理
|
||||||
|
→ Token 经 `sundynix.streams.<id>` 回流 → Gateway SSE → 客户端逐 token 收到。
|
||||||
|
|
||||||
|
### 完整环境(Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make infra # 启动 NATS / Postgres / Redis / Milvus / Neo4j (docker-compose)
|
||||||
|
make devnats # 或:无 Docker 时单独起内嵌 JetStream NATS
|
||||||
|
make gateway # 运行 Gateway
|
||||||
|
make dispatcher # 运行 Dispatcher
|
||||||
|
make mcp-go # 运行 Go MCP 工具服务
|
||||||
|
make mcp-py # 运行 Python MCP 工具服务
|
||||||
|
make desktop # 开发模式运行桌面端 (wails dev)
|
||||||
|
```
|
||||||
|
|
||||||
|
> Go 多模块用 `go.work` 工作区串联;`sundynix-shared` 通过各服务 go.mod 的 `replace` 指向本地。
|
||||||
|
|
||||||
|
## NATS Subject 约定
|
||||||
|
|
||||||
|
| Subject | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `sundynix.tasks.*` | Queue | 分布式任务队列 |
|
||||||
|
| `sundynix.streams.<task_id>` | Stream | 零拷贝 Token 字节管道 |
|
||||||
|
| `sundynix.tools.go.*` | Queue | Go MCP 工具调用 |
|
||||||
|
| `sundynix.tools.py.*` | Queue | Python MCP 工具调用 |
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>sundynix-agentix · 系统架构图</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--client:#f6d9b0; --client-bg:#fdf2e3; --client-br:#e0a85a;
|
||||||
|
--gateway:#f8c9c9; --gateway-stroke:#d96b6b;
|
||||||
|
--nats:#cfe8c6; --nats-bg:#e7f4e1; --nats-br:#86c06a;
|
||||||
|
--dispatcher:#dcc7ef; --dispatcher-bg:#efe5f8; --dispatcher-br:#a984cf;
|
||||||
|
--gotools:#bcdcef; --gotools-bg:#dcecf7; --gotools-br:#5fa9d4;
|
||||||
|
--pytools:#f6e6a8; --pytools-bg:#fbf3cf; --pytools-br:#d8bf52;
|
||||||
|
--ginserver:#bfe3c4;
|
||||||
|
--box:#eef1f4; --box-br:#c4ccd4;
|
||||||
|
--ink:#1c2b3a; --muted:#5a6b7b;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;}
|
||||||
|
body{
|
||||||
|
margin:0; padding:32px;
|
||||||
|
font-family:"PingFang SC","Microsoft YaHei","Segoe UI",system-ui,sans-serif;
|
||||||
|
background:#f3f5f7; color:var(--ink);
|
||||||
|
}
|
||||||
|
h1{font-size:22px; margin:0 0 4px;}
|
||||||
|
.sub{color:var(--muted); margin:0 0 24px; font-size:13px;}
|
||||||
|
|
||||||
|
.grid{
|
||||||
|
display:grid;
|
||||||
|
grid-template-columns: 1.05fr 1.05fr 1.15fr;
|
||||||
|
gap:22px;
|
||||||
|
align-items:start;
|
||||||
|
max-width:1980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- generic layer card ---- */
|
||||||
|
.layer{
|
||||||
|
border-radius:14px; padding:14px;
|
||||||
|
border:2px solid transparent;
|
||||||
|
}
|
||||||
|
.layer > .ltitle{
|
||||||
|
font-weight:800; font-size:15px; margin:0 0 12px; letter-spacing:.2px;
|
||||||
|
}
|
||||||
|
.layer > .ltitle small{display:block; font-weight:600; font-size:11px; color:var(--muted); margin-top:2px;}
|
||||||
|
|
||||||
|
.panel{
|
||||||
|
background:rgba(255,255,255,.65);
|
||||||
|
border-radius:11px; padding:12px; margin-bottom:12px;
|
||||||
|
}
|
||||||
|
.panel:last-child{margin-bottom:0;}
|
||||||
|
.panel-title{font-weight:700; font-size:13px; margin:0 0 10px;}
|
||||||
|
.panel-title small{font-weight:500; color:var(--muted);}
|
||||||
|
|
||||||
|
.box{
|
||||||
|
background:var(--box); border:1px solid var(--box-br);
|
||||||
|
border-radius:8px; padding:9px 11px; margin-bottom:8px;
|
||||||
|
font-size:12.5px; line-height:1.35; text-align:center;
|
||||||
|
}
|
||||||
|
.box:last-child{margin-bottom:0;}
|
||||||
|
.box b{display:block; font-weight:700;}
|
||||||
|
.box span{display:block; color:var(--muted); font-size:11px; margin-top:2px;}
|
||||||
|
.box.hl{background:#fff;}
|
||||||
|
|
||||||
|
.row2{display:grid; grid-template-columns:1fr 1fr; gap:8px;}
|
||||||
|
.row2 .box{margin-bottom:0;}
|
||||||
|
|
||||||
|
/* ---- layer colors ---- */
|
||||||
|
.client { background:var(--client-bg); border-color:var(--client-br); }
|
||||||
|
.gateway { background:#e6eef6; border-color:var(--gotools-br); }
|
||||||
|
.gateway .panel{background:#cfe0ef;}
|
||||||
|
.nats { background:var(--nats-bg); border-color:var(--nats-br); }
|
||||||
|
.dispatcher{ background:var(--dispatcher-bg); border-color:var(--dispatcher-br); }
|
||||||
|
.toolswrap { background:var(--nats-bg); border-color:var(--nats-br); padding:14px; border-radius:14px; border:2px solid var(--nats-br);}
|
||||||
|
.mcp-go { background:#d6e7f3; border:2px solid var(--gotools-br); border-radius:12px; padding:12px; margin-bottom:14px;}
|
||||||
|
.mcp-py { background:var(--pytools-bg); border:2px solid var(--pytools-br); border-radius:12px; padding:12px;}
|
||||||
|
|
||||||
|
.colA{display:flex; flex-direction:column; gap:0;}
|
||||||
|
.colB{display:flex; flex-direction:column; gap:18px;}
|
||||||
|
|
||||||
|
/* connector labels */
|
||||||
|
.conn{
|
||||||
|
text-align:center; font-size:11px; color:#2a3a49; font-weight:700;
|
||||||
|
margin:6px 0; line-height:1.4;
|
||||||
|
}
|
||||||
|
.conn .arr{font-size:15px; color:#33485c;}
|
||||||
|
.conn small{display:block; font-weight:500; color:var(--muted);}
|
||||||
|
|
||||||
|
.midbar{
|
||||||
|
display:flex; align-items:center; justify-content:center; gap:8px;
|
||||||
|
font-weight:800; font-size:12px; color:#2a3a49; text-align:center;
|
||||||
|
background:#fff; border:1px dashed #9fb0bf; border-radius:8px;
|
||||||
|
padding:8px 10px; margin:14px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llmpool{
|
||||||
|
background:#e9edf0; border:1px solid #c4ccd4; border-radius:50px/40px;
|
||||||
|
padding:14px 16px; text-align:center; font-size:12.5px;
|
||||||
|
}
|
||||||
|
.llmpool b{display:block;}
|
||||||
|
.einolink{text-align:center; font-size:11px; font-weight:700; color:#6a4fa0; margin:8px 0;}
|
||||||
|
|
||||||
|
/* legend */
|
||||||
|
.legend{
|
||||||
|
margin-top:26px; display:flex; flex-wrap:wrap; gap:10px 26px;
|
||||||
|
align-items:center; background:#fff; border:1px solid var(--box-br);
|
||||||
|
border-radius:12px; padding:14px 18px; max-width:1980px;
|
||||||
|
}
|
||||||
|
.legend b{width:100%; font-size:13px; margin-bottom:2px;}
|
||||||
|
.lg{display:flex; align-items:center; gap:8px; font-size:12.5px;}
|
||||||
|
.sw{width:20px; height:14px; border-radius:4px; border:1px solid rgba(0,0,0,.15);}
|
||||||
|
.sw.client{background:var(--client);} .sw.gateway{background:var(--gateway);}
|
||||||
|
.sw.nats{background:var(--nats);} .sw.dispatcher{background:var(--dispatcher);}
|
||||||
|
.sw.gotools{background:var(--gotools);} .sw.pytools{background:var(--pytools);}
|
||||||
|
.sw.ginserver{background:var(--ginserver);}
|
||||||
|
|
||||||
|
.flow-note{
|
||||||
|
max-width:1980px; margin-top:22px; background:#fff; border:1px solid var(--box-br);
|
||||||
|
border-radius:12px; padding:16px 20px; font-size:13px; line-height:1.7;
|
||||||
|
}
|
||||||
|
.flow-note b{color:#2a3a49;}
|
||||||
|
.tag{display:inline-block; background:#eef1f4; border:1px solid var(--box-br);
|
||||||
|
border-radius:5px; padding:1px 7px; font-size:11.5px; font-family:ui-monospace,monospace;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>sundynix-agentix · 系统架构图</h1>
|
||||||
|
<p class="sub">分层式 AI Agent 平台 — Client / Gateway / NATS 总线 / Dispatcher / MCP Tools(Monolith First → Microservices Morph B)</p>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
|
||||||
|
<!-- ===================== 列 1:左纵列 (Client → Gateway → NATS → Dispatcher) ===================== -->
|
||||||
|
<div class="colA">
|
||||||
|
|
||||||
|
<!-- 1. CLIENT -->
|
||||||
|
<div class="layer client">
|
||||||
|
<div class="ltitle">1. CLIENT LAYER <small>Edge Interaction</small></div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">sundynix-desktop <small>(User Device App)</small></div>
|
||||||
|
<div class="box hl"><b>UI Representation Layer</b><span>React 19 + TypeScript</span></div>
|
||||||
|
<div class="box"><b>shadcn/ui + Tailwind CSS</b><span>Styles / Components</span></div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box"><b>React Flow Canvas</b><span>Agent Orchestration</span></div>
|
||||||
|
<div class="box"><b>JSON DSL export</b><span> </span></div>
|
||||||
|
</div>
|
||||||
|
<div class="box"><b>LLM Wiki Management Panel</b></div>
|
||||||
|
<div class="box hl" style="background:var(--ginserver);"><b>Wails Local Go Runtime</b></div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="box hl" style="background:var(--gotools);"><b>Strong Binding</b><span>TS / Go</span></div>
|
||||||
|
<div class="box hl" style="background:var(--gotools);"><b>Local File System I/O</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- connector client <-> gateway -->
|
||||||
|
<div class="conn">
|
||||||
|
<span class="arr">↕</span> 4. PHYSICAL COMMUNICATION<br>
|
||||||
|
<small>HTTPS / WebSocket / SSE · <span class="tag">sundynix.streams.task_id</span> Token Stream</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. GATEWAY -->
|
||||||
|
<div class="layer gateway">
|
||||||
|
<div class="ltitle">2. BUSINESS GATEWAY LAYER</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">sundynix-gateway <small>(Gin 微服务 · 集成 Monolith First)</small></div>
|
||||||
|
<div class="box hl"><b>Gin 微服务 / 统一接入层</b></div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box">🗄️ <b>MainDB</b><span>PgSQL: Users, Billing, DSL</span></div>
|
||||||
|
<div class="box">🧱 <b>CacheDB</b><span>Session / Rate Limit</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box">🧱 <b>CacheDB</b><span>Redis: Session, Rate Limit</span></div>
|
||||||
|
<div class="box"><b>商业化与计费模块</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="box"><b>Harness</b><span>Input / Output Guardrail</span></div>
|
||||||
|
<div class="box"><b>Task DSL Parser & Assembly</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- connector gateway <-> nats -->
|
||||||
|
<div class="conn">
|
||||||
|
<span class="arr">↓</span> 1. Publish Task DSL | 4. Subscribe Stream <span class="arr">↑</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. NATS -->
|
||||||
|
<div class="layer nats">
|
||||||
|
<div class="ltitle">3. MESSAGE BUS — NATS 零拷贝骨干网</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title" style="text-align:center;">NATS Server (Go) 🗄️</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="box"><b>NATS Queue</b><span>分布式任务队列<br><span class="tag">sundynix.tasks.*</span></span></div>
|
||||||
|
<div class="box"><b>NATS Stream</b><span>零拷贝字节管道<br><span class="tag">sundynix.streams.*</span></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- connector nats <-> dispatcher -->
|
||||||
|
<div class="conn"><span class="arr">↕</span> 4. Subscribe Stream</div>
|
||||||
|
|
||||||
|
<!-- 4. DISPATCHER -->
|
||||||
|
<div class="layer dispatcher">
|
||||||
|
<div class="ltitle">4. AI AGENT DISPATCHER LAYER</div>
|
||||||
|
<div class="llmpool" style="margin-bottom:10px;">
|
||||||
|
<b>LLM Pool</b><span style="color:var(--muted); font-size:11px;">vLLM / Ollama 集群</span>
|
||||||
|
</div>
|
||||||
|
<div class="einolink">↑ EinoCore · Eino 回调机制 ↓</div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">sundynix-dispatcher <small>(Go / Eino 集群)</small></div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box hl"><b>Eino 图编排引擎</b></div>
|
||||||
|
<div class="box hl"><b>nats.go client</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="box"><b>Harness: LLM 自动化评测</b></div>
|
||||||
|
<div class="box"><b>Harness: 熔断降级中心</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== 列 2:留白做视觉间隔 / 物理通信说明 ===================== -->
|
||||||
|
<div class="colA" style="justify-content:flex-start;">
|
||||||
|
<div class="midbar">⟷ 4. PHYSICAL COMMUNICATION ⟷</div>
|
||||||
|
<div class="flow-note" style="margin-top:0;">
|
||||||
|
<b>核心数据流(编号对应连线)</b><br>
|
||||||
|
<b>1.</b> Gateway 解析 DSL 后 <b>Publish Task DSL</b> → NATS Queue(<span class="tag">sundynix.tasks.*</span>)<br>
|
||||||
|
<b>2.</b> Dispatcher 通过 <span class="tag">nats.go</span> 订阅任务,Eino 图编排引擎驱动 LLM Pool 推理<br>
|
||||||
|
<b>3.</b> Dispatcher / Gateway 经 NATS 调用第 5 层 MCP 工具(Go I/O 型 + Python 算法型)<br>
|
||||||
|
<b>4.</b> 推理结果以 <b>零拷贝 Token Stream</b> 经 <span class="tag">sundynix.streams.task_id</span> 回流 Gateway → 经 SSE/WS 推送给 Client
|
||||||
|
</div>
|
||||||
|
<div class="midbar" style="margin-top:18px;">⟶ 4. Subscribe Stream ⟶ MCP Tools</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== 列 3:MCP Tools 层 ===================== -->
|
||||||
|
<div class="colB">
|
||||||
|
<div class="toolswrap">
|
||||||
|
<div style="font-weight:800; font-size:15px; margin-bottom:4px;">5. MICROSERVICE MCP TOOLS LAYER</div>
|
||||||
|
<div style="font-size:11px; color:var(--muted); margin-bottom:12px;">Standalone microservices · Morph B</div>
|
||||||
|
|
||||||
|
<!-- mcp_go -->
|
||||||
|
<div class="mcp-go">
|
||||||
|
<div class="panel-title">sundynix-mcp-go <small>(Go 微服务 · I/O 型)</small></div>
|
||||||
|
<div class="box hl"><b>MCP Protocol Gateway</b></div>
|
||||||
|
<div class="box hl"><b>LLM Wiki 搜索引擎</b><span>Hybrid Search: Bleve · Milvus Go SDK · Neo4j Go Driver</span></div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box">🧊 <b>Milvus</b><span>Vector DB</span></div>
|
||||||
|
<div class="box">🔎 <b>Bleve</b><span>Go Search Index</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="row2" style="margin-bottom:8px;">
|
||||||
|
<div class="box">🕸️ <b>Neo4j</b><span>Knowledge Graph</span></div>
|
||||||
|
<div class="box">🕸️ <b>Neo4j</b><span>Graph</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="row2">
|
||||||
|
<div class="box">📄 <b>UniOffice</b><span>Word/Doc Rendering</span></div>
|
||||||
|
<div class="box"><b>External APIs</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- mcp_py -->
|
||||||
|
<div class="mcp-py">
|
||||||
|
<div class="panel-title">sundynix-mcp-py <small>(Python 微服务 · 算法型)</small></div>
|
||||||
|
<div class="box hl"><b>MCP Protocol Gateway</b></div>
|
||||||
|
<div class="box"><b>Harness: Secure Code Sandbox</b><span>gVisor / KataVM · Static Code Guard</span></div>
|
||||||
|
<div class="box"><b>MinerU</b><span>Multimodal Parser: PaddleOCR</span></div>
|
||||||
|
<div class="box"><b>Docker 隔离沙箱</b><span>Code Interpreter</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== 图例 ===================== -->
|
||||||
|
<div class="legend">
|
||||||
|
<b>Legend</b>
|
||||||
|
<div class="lg"><span class="sw client"></span>Client</div>
|
||||||
|
<div class="lg"><span class="sw gateway"></span>Gateway</div>
|
||||||
|
<div class="lg"><span class="sw nats"></span>NATS</div>
|
||||||
|
<div class="lg"><span class="sw dispatcher"></span>Dispatcher</div>
|
||||||
|
<div class="lg"><span class="sw gotools"></span>Go Tools</div>
|
||||||
|
<div class="lg"><span class="sw pytools"></span>Python Tools</div>
|
||||||
|
<div class="lg"><span class="sw ginserver"></span>GinServer</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
# sundynix-agentix · 系统架构图
|
||||||
|
|
||||||
|
分层式 AI Agent 平台 — **Monolith First → Microservices (Morph B)** 演进。
|
||||||
|
共 **5 层 + 1 条 NATS 零拷贝消息总线**。
|
||||||
|
|
||||||
|
> 下方 Mermaid 图在 GitHub / VS Code(装 Mermaid 插件) / Typora / Obsidian 中可直接渲染。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
|
||||||
|
%% ===================== 1. CLIENT =====================
|
||||||
|
subgraph CLIENT["1 · CLIENT LAYER (Edge Interaction)"]
|
||||||
|
direction TB
|
||||||
|
subgraph DESKTOP["sundynix-desktop (User Device App)"]
|
||||||
|
UI["UI Representation Layer<br/>React 19 + TypeScript"]
|
||||||
|
SHAD["shadcn/ui + Tailwind CSS<br/>Styles / Components"]
|
||||||
|
RF["React Flow Canvas<br/>Agent Orchestration"]
|
||||||
|
DSL["JSON DSL export"]
|
||||||
|
WIKI["LLM Wiki Management Panel"]
|
||||||
|
WAILS["Wails Local Go Runtime"]
|
||||||
|
BIND["Strong Binding TS/Go"]
|
||||||
|
FS["Local File System I/O"]
|
||||||
|
UI --> SHAD --> RF --> DSL
|
||||||
|
RF --> WIKI --> WAILS
|
||||||
|
WAILS --> BIND
|
||||||
|
WAILS --> FS
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ===================== 2. GATEWAY =====================
|
||||||
|
subgraph GATEWAY["2 · BUSINESS GATEWAY LAYER"]
|
||||||
|
direction TB
|
||||||
|
subgraph GW["sundynix-gateway (Gin 微服务 · Monolith First)"]
|
||||||
|
GIN["Gin 微服务 / 统一接入层"]
|
||||||
|
MAINDB[("MainDB<br/>PgSQL: Users, Billing, DSL")]
|
||||||
|
CACHE1[("CacheDB<br/>Session / Rate Limit")]
|
||||||
|
CACHE2[("CacheDB<br/>Redis: Session, Rate Limit")]
|
||||||
|
BILL["商业化与计费模块"]
|
||||||
|
GUARD["Harness: Input/Output Guardrail"]
|
||||||
|
PARSER["Task DSL Parser & Assembly"]
|
||||||
|
GIN --> MAINDB
|
||||||
|
GIN --> CACHE1
|
||||||
|
GIN --> CACHE2
|
||||||
|
GIN --> BILL
|
||||||
|
GIN --> GUARD
|
||||||
|
GIN --> PARSER
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ===================== 3. NATS =====================
|
||||||
|
subgraph NATS["3 · MESSAGE BUS — NATS 零拷贝骨干网"]
|
||||||
|
direction LR
|
||||||
|
QUEUE["NATS Queue<br/>分布式任务队列<br/>sundynix.tasks.*"]
|
||||||
|
STREAM["NATS Stream<br/>零拷贝字节管道<br/>sundynix.streams.*"]
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ===================== 4. DISPATCHER =====================
|
||||||
|
subgraph DISPATCHER["4 · AI AGENT DISPATCHER LAYER"]
|
||||||
|
direction TB
|
||||||
|
LLMPOOL[("LLM Pool<br/>vLLM / Ollama 集群")]
|
||||||
|
subgraph DISP["sundynix-dispatcher (Go / Eino 集群)"]
|
||||||
|
EINO["Eino 图编排引擎"]
|
||||||
|
NATSGO["nats.go client"]
|
||||||
|
EVAL["Harness: LLM 自动化评测"]
|
||||||
|
FUSE["Harness: 熔断降级中心"]
|
||||||
|
EINO <--> NATSGO
|
||||||
|
end
|
||||||
|
LLMPOOL <-->|"EinoCore · Eino 回调机制"| EINO
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ===================== 5. MCP TOOLS =====================
|
||||||
|
subgraph TOOLS["5 · MICROSERVICE MCP TOOLS LAYER (Morph B)"]
|
||||||
|
direction TB
|
||||||
|
subgraph MCPGO["sundynix-mcp-go (Go 微服务 · I/O 型)"]
|
||||||
|
GGW["MCP Protocol Gateway"]
|
||||||
|
SEARCH["LLM Wiki 搜索引擎<br/>Hybrid: Bleve · Milvus Go SDK · Neo4j Go Driver"]
|
||||||
|
MILVUS[("Milvus · Vector DB")]
|
||||||
|
BLEVE["Bleve · Go Search Index"]
|
||||||
|
NEO1[("Neo4j · Knowledge Graph")]
|
||||||
|
NEO2[("Neo4j · Graph")]
|
||||||
|
UNI["UniOffice · Word/Doc Rendering"]
|
||||||
|
EXT["External APIs"]
|
||||||
|
GGW --> SEARCH --> MILVUS & BLEVE & NEO1 & NEO2
|
||||||
|
GGW --> UNI
|
||||||
|
GGW --> EXT
|
||||||
|
end
|
||||||
|
subgraph MCPPY["sundynix-mcp-py (Python 微服务 · 算法型)"]
|
||||||
|
PGW["MCP Protocol Gateway"]
|
||||||
|
SANDBOX["Harness: Secure Code Sandbox<br/>gVisor / KataVM · Static Code Guard"]
|
||||||
|
MINERU["MinerU · Multimodal Parser (PaddleOCR)"]
|
||||||
|
DOCKER["Docker 隔离沙箱 · Code Interpreter"]
|
||||||
|
PGW --> SANDBOX --> MINERU --> DOCKER
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%% ===================== 跨层连线 =====================
|
||||||
|
DESKTOP <==>|"4 · PHYSICAL COMMUNICATION<br/>HTTPS / WebSocket / SSE<br/>sundynix.streams.task_id (Token Stream)"| GIN
|
||||||
|
PARSER ==>|"1 · Publish Task DSL"| QUEUE
|
||||||
|
STREAM ==>|"4 · Subscribe Stream"| GIN
|
||||||
|
QUEUE <==>|"4 · Subscribe Stream"| NATSGO
|
||||||
|
STREAM <==> NATSGO
|
||||||
|
NATS ==> MCPGO
|
||||||
|
NATS ==> MCPPY
|
||||||
|
|
||||||
|
%% ===================== 配色 =====================
|
||||||
|
classDef client fill:#fdf2e3,stroke:#e0a85a,color:#1c2b3a;
|
||||||
|
classDef gateway fill:#dcecf7,stroke:#5fa9d4,color:#1c2b3a;
|
||||||
|
classDef nats fill:#e7f4e1,stroke:#86c06a,color:#1c2b3a;
|
||||||
|
classDef dispatcher fill:#efe5f8,stroke:#a984cf,color:#1c2b3a;
|
||||||
|
classDef gotools fill:#d6e7f3,stroke:#5fa9d4,color:#1c2b3a;
|
||||||
|
classDef pytools fill:#fbf3cf,stroke:#d8bf52,color:#1c2b3a;
|
||||||
|
|
||||||
|
class CLIENT,DESKTOP,UI,SHAD,RF,DSL,WIKI,WAILS,BIND,FS client;
|
||||||
|
class GATEWAY,GW,GIN,MAINDB,CACHE1,CACHE2,BILL,GUARD,PARSER gateway;
|
||||||
|
class NATS,QUEUE,STREAM nats;
|
||||||
|
class DISPATCHER,DISP,LLMPOOL,EINO,NATSGO,EVAL,FUSE dispatcher;
|
||||||
|
class TOOLS,MCPGO,GGW,SEARCH,MILVUS,BLEVE,NEO1,NEO2,UNI,EXT gotools;
|
||||||
|
class MCPPY,PGW,SANDBOX,MINERU,DOCKER pytools;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 分层说明
|
||||||
|
|
||||||
|
| 层 | 组件 | 技术栈 / 职责 | 配色 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1. Client** | `sundynix-desktop` | React 19 + TS、shadcn/ui + Tailwind、React Flow 画布(Agent 编排 → 导出 JSON DSL)、LLM Wiki 面板、Wails 本地 Go 运行时、TS/Go 强绑定、本地文件 I/O | 🟧 橙 |
|
||||||
|
| **2. Gateway** | `sundynix-gateway` | Gin 统一接入层;MainDB(PgSQL: Users/Billing/DSL) + CacheDB(Redis: Session/Rate Limit);商业化计费;输入/输出 Guardrail;**Task DSL 解析与组装** | 🟦 蓝 |
|
||||||
|
| **3. 消息总线** | `NATS Server (Go)` | 零拷贝骨干网:Queue(`sundynix.tasks.*` 分布式任务队列) + Stream(`sundynix.streams.*` 零拷贝字节管道) | 🟩 绿 |
|
||||||
|
| **4. Dispatcher** | `sundynix-dispatcher` | Go/Eino 集群:Eino 图编排引擎 + nats.go client,经 EinoCore/回调驱动 LLM Pool(vLLM/Ollama);LLM 自动化评测、熔断降级中心 | 🟪 紫 |
|
||||||
|
| **5a. MCP Go** | `sundynix-mcp-go` | Go I/O 型:MCP 协议网关、Wiki 混合检索(Bleve+Milvus+Neo4j)、UniOffice 文档渲染、外部 API | 🟦 蓝 |
|
||||||
|
| **5b. MCP Py** | `sundynix-mcp-py` | Python 算法型:MCP 协议网关、安全代码沙箱(gVisor/KataVM)、MinerU 多模态解析(PaddleOCR)、Docker 代码解释器 | 🟨 黄 |
|
||||||
|
|
||||||
|
## 核心数据流(图中编号)
|
||||||
|
|
||||||
|
1. **Publish Task DSL** — Gateway 解析 DSL 后发布任务到 NATS Queue (`sundynix.tasks.*`)
|
||||||
|
2. **订阅与编排** — Dispatcher 经 `nats.go` 订阅任务,Eino 图编排引擎驱动 LLM Pool 推理
|
||||||
|
3. **调用 MCP 工具** — Dispatcher / Gateway 经 NATS 调用第 5 层 Go(I/O 型) + Python(算法型) 工具
|
||||||
|
4. **Token Stream 回流** — 推理结果以零拷贝流经 `sundynix.streams.task_id` 回 Gateway → 经 SSE/WebSocket 推送给 Client
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 674 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
# NATS 零拷贝骨干网 — JetStream 开启
|
||||||
|
port: 4222
|
||||||
|
http_port: 8222 # 监控端点
|
||||||
|
|
||||||
|
jetstream {
|
||||||
|
store_dir: "/data/jetstream"
|
||||||
|
max_memory_store: 1GB
|
||||||
|
max_file_store: 10GB
|
||||||
|
}
|
||||||
|
|
||||||
|
# Subject 命名空间约定(仅文档,实际 stream 由各服务声明式创建)
|
||||||
|
# sundynix.tasks.* 分布式任务队列 (Queue)
|
||||||
|
# sundynix.streams.<task_id> 零拷贝 Token 字节管道 (Stream)
|
||||||
|
# sundynix.tools.go.* Go MCP 工具调用
|
||||||
|
# sundynix.tools.py.* Python MCP 工具调用
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
# 基础设施 — NATS 零拷贝骨干网 + 业务存储 + 向量/图数据库
|
||||||
|
services:
|
||||||
|
nats:
|
||||||
|
image: nats:2-alpine
|
||||||
|
command: ["-c", "/etc/nats/nats-server.conf"]
|
||||||
|
ports: ["4222:4222", "8222:8222"]
|
||||||
|
volumes:
|
||||||
|
- ./deploy/nats/nats-server.conf:/etc/nats/nats-server.conf:ro
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: sundynix
|
||||||
|
POSTGRES_PASSWORD: sundynix
|
||||||
|
POSTGRES_DB: sundynix
|
||||||
|
ports: ["5432:5432"]
|
||||||
|
volumes: ["pg_data:/var/lib/postgresql/data"]
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports: ["6379:6379"]
|
||||||
|
|
||||||
|
# --- Milvus 向量数据库 (standalone 需 etcd + minio) ---
|
||||||
|
milvus-etcd:
|
||||||
|
image: quay.io/coreos/etcd:v3.5.14
|
||||||
|
environment:
|
||||||
|
ETCD_AUTO_COMPACTION_MODE: revision
|
||||||
|
ETCD_AUTO_COMPACTION_RETENTION: "1000"
|
||||||
|
ETCD_QUOTA_BACKEND_BYTES: "4294967296"
|
||||||
|
ETCD_SNAPSHOT_COUNT: "50000"
|
||||||
|
command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
|
||||||
|
volumes: ["milvus_etcd:/etcd"]
|
||||||
|
|
||||||
|
milvus-minio:
|
||||||
|
image: minio/minio:RELEASE.2023-03-20T20-16-18Z
|
||||||
|
environment:
|
||||||
|
MINIO_ACCESS_KEY: minioadmin
|
||||||
|
MINIO_SECRET_KEY: minioadmin
|
||||||
|
command: minio server /minio_data
|
||||||
|
volumes: ["milvus_minio:/minio_data"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
milvus:
|
||||||
|
image: milvusdb/milvus:v2.4.13
|
||||||
|
command: ["milvus", "run", "standalone"]
|
||||||
|
environment:
|
||||||
|
ETCD_ENDPOINTS: milvus-etcd:2379
|
||||||
|
MINIO_ADDRESS: milvus-minio:9000
|
||||||
|
ports: ["19530:19530", "9091:9091"] # 19530=gRPC, 9091=metrics/health
|
||||||
|
volumes: ["milvus_data:/var/lib/milvus"]
|
||||||
|
depends_on: [milvus-etcd, milvus-minio]
|
||||||
|
|
||||||
|
neo4j:
|
||||||
|
image: neo4j:5-community
|
||||||
|
environment:
|
||||||
|
NEO4J_AUTH: neo4j/sundynix
|
||||||
|
ports: ["7474:7474", "7687:7687"]
|
||||||
|
volumes: ["neo4j_data:/data"]
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pg_data:
|
||||||
|
milvus_etcd:
|
||||||
|
milvus_minio:
|
||||||
|
milvus_data:
|
||||||
|
neo4j_data:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
go 1.23
|
||||||
|
|
||||||
|
use (
|
||||||
|
./sundynix-shared
|
||||||
|
./sundynix-gateway
|
||||||
|
./sundynix-dispatcher
|
||||||
|
./sundynix-mcp-go
|
||||||
|
)
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
|
||||||
|
github.com/RoaringBitmap/roaring v1.9.3/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||||
|
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
|
||||||
|
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
|
github.com/blevesearch/bleve/v2 v2.4.2 h1:NooYP1mb3c0StkiY9/xviiq2LGSaE8BQBCc/pirMx0U=
|
||||||
|
github.com/blevesearch/bleve_index_api v1.1.10 h1:PDLFhVjrjQWr6jCuU7TwlmByQVCSEURADHdCqVS9+g0=
|
||||||
|
github.com/blevesearch/bleve_index_api v1.1.10/go.mod h1:PbcwjIcRmjhGbkS/lJCpfgVSMROV6TRubGGAODaK1W8=
|
||||||
|
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||||
|
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||||
|
github.com/blevesearch/go-faiss v1.0.20 h1:AIkdTQFWuZ5LQmKQSebgMR4RynGNw8ZseJXaan5kvtI=
|
||||||
|
github.com/blevesearch/go-faiss v1.0.20/go.mod h1:jrxHrbl42X/RnDPI+wBoZU8joxxuRwedrxqswQ3xfU8=
|
||||||
|
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:kDy+zgJFJJoJYBvdfBSiZYBbdsUL0XcjHYWezpQBGPA=
|
||||||
|
github.com/blevesearch/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:9eJDeqxJ3E7WnLebQUlPD7ZjSce7AnDb9vjGmMCbD0A=
|
||||||
|
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||||
|
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||||
|
github.com/blevesearch/goleveldb v1.0.1 h1:iAtV2Cu5s0GD1lwUiekkFHe2gTMCCNVj2foPclDLIFI=
|
||||||
|
github.com/blevesearch/goleveldb v1.0.1/go.mod h1:WrU8ltZbIp0wAoig/MHbrPCXSOLpe79nz5lv5nqfYrQ=
|
||||||
|
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
|
||||||
|
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
|
||||||
|
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||||
|
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||||
|
github.com/blevesearch/scorch_segment_api/v2 v2.2.15 h1:prV17iU/o+A8FiZi9MXmqbagd8I0bCqM7OKUYPbnb5Y=
|
||||||
|
github.com/blevesearch/scorch_segment_api/v2 v2.2.15/go.mod h1:db0cmP03bPNadXrCDuVkKLV6ywFSiRgPFT1YVrestBc=
|
||||||
|
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||||
|
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||||
|
github.com/blevesearch/snowball v0.6.1 h1:cDYjn/NCH+wwt2UdehaLpr2e4BwLIjN4V/TdLsL+B5A=
|
||||||
|
github.com/blevesearch/snowball v0.6.1/go.mod h1:ZF0IBg5vgpeoUhnMza2v0A/z8m1cWPlwhke08LpNusg=
|
||||||
|
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||||
|
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||||
|
github.com/blevesearch/stempel v0.2.0 h1:CYzVPaScODMvgE9o+kf6D4RJ/VRomyi9uHF+PtB+Afc=
|
||||||
|
github.com/blevesearch/stempel v0.2.0/go.mod h1:wjeTHqQv+nQdbPuJ/YcvOjTInA2EIc6Ks1FoSUzSLvc=
|
||||||
|
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
|
||||||
|
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
|
||||||
|
github.com/blevesearch/vellum v1.0.10 h1:HGPJDT2bTva12hrHepVT3rOyIKFFF4t7Gf6yMxyMIPI=
|
||||||
|
github.com/blevesearch/vellum v1.0.10/go.mod h1:ul1oT0FhSMDIExNjIxHqJoGpVrBpKCdgDQNxfqgJt7k=
|
||||||
|
github.com/blevesearch/zapx/v11 v11.3.10 h1:hvjgj9tZ9DeIqBCxKhi70TtSZYMdcFn7gDb71Xo/fvk=
|
||||||
|
github.com/blevesearch/zapx/v11 v11.3.10/go.mod h1:0+gW+FaE48fNxoVtMY5ugtNHHof/PxCqh7CnhYdnMzQ=
|
||||||
|
github.com/blevesearch/zapx/v12 v12.3.10 h1:yHfj3vXLSYmmsBleJFROXuO08mS3L1qDCdDK81jDl8s=
|
||||||
|
github.com/blevesearch/zapx/v12 v12.3.10/go.mod h1:0yeZg6JhaGxITlsS5co73aqPtM04+ycnI6D1v0mhbCs=
|
||||||
|
github.com/blevesearch/zapx/v13 v13.3.10 h1:0KY9tuxg06rXxOZHg3DwPJBjniSlqEgVpxIqMGahDE8=
|
||||||
|
github.com/blevesearch/zapx/v13 v13.3.10/go.mod h1:w2wjSDQ/WBVeEIvP0fvMJZAzDwqwIEzVPnCPrz93yAk=
|
||||||
|
github.com/blevesearch/zapx/v14 v14.3.10 h1:SG6xlsL+W6YjhX5N3aEiL/2tcWh3DO75Bnz77pSwwKU=
|
||||||
|
github.com/blevesearch/zapx/v14 v14.3.10/go.mod h1:qqyuR0u230jN1yMmE4FIAuCxmahRQEOehF78m6oTgns=
|
||||||
|
github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wyhnpDHHQ=
|
||||||
|
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
|
||||||
|
github.com/blevesearch/zapx/v16 v16.1.5 h1:b0sMcarqNFxuXvjoXsF8WtwVahnxyhEvBSRJi/AUHjU=
|
||||||
|
github.com/blevesearch/zapx/v16 v16.1.5/go.mod h1:J4mSF39w1QELc11EWRSBFkPeZuO7r/NPKkHzDCoiaI8=
|
||||||
|
github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cockroachdb/errors v1.9.1 h1:yFVvsI0VxmRShfawbt/laCIDy/mtTqqnvoNgiy5bEV8=
|
||||||
|
github.com/cockroachdb/errors v1.9.1/go.mod h1:2sxOtL2WIc096WSZqZ5h8fa17rdDq9HZOZLBCor4mBk=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f h1:6jduT9Hfc0njg5jJ1DdKCFPdMBrp/mdZfCpa5h+WM74=
|
||||||
|
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||||
|
github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ=
|
||||||
|
github.com/cockroachdb/redact v1.1.3/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||||
|
github.com/couchbase/ghistogram v0.1.0 h1:b95QcQTCzjTUocDXp/uMgSNQi8oj1tGwnJ4bODWZnps=
|
||||||
|
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
|
||||||
|
github.com/couchbase/moss v0.2.0 h1:VCYrMzFwEryyhRSeI+/b3tRBSeTpi/8gn5Kf6dxqn+o=
|
||||||
|
github.com/couchbase/moss v0.2.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||||
|
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||||
|
github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk=
|
||||||
|
github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TBdRL1M71JZW2c=
|
||||||
|
github.com/go-faker/faker/v4 v4.1.0 h1:ffuWmpDrducIUOO0QSKSF5Q2dxAht+dhsT9FvVHhPEI=
|
||||||
|
github.com/go-faker/faker/v4 v4.1.0/go.mod h1:uuNc0PSRxF8nMgjGrrrU4Nw5cF30Jc6Kd0/FUTTYbhg=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
|
||||||
|
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||||
|
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
|
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/knz/go-libedit v1.10.1 h1:0pHpWtx9vcvC0xGZqEQlQdfSQs7WRlAjuPvk3fOZDCo=
|
||||||
|
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||||
|
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.3 h1:KUSaWVePVlHMIluAXf2qmNffI1CMlGFLLiP+4iy9014=
|
||||||
|
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.3/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek=
|
||||||
|
github.com/milvus-io/milvus-sdk-go/v2 v2.4.1 h1:KhqjmaJE4mSxj1a88XtkGaqgH4duGiHs1sjnvSXkwE0=
|
||||||
|
github.com/milvus-io/milvus-sdk-go/v2 v2.4.1/go.mod h1:7SJxshlnVhNLksS73tLPtHYY9DiX7lyL43Rv41HCPCw=
|
||||||
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
github.com/neo4j/neo4j-go-driver/v5 v5.24.0 h1:7MAFoB7L6f9heQUo/tJ5EnrrpVzm9ZBHgH8ew03h6Eo=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
|
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||||
|
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||||
|
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||||
|
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
|
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||||
|
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||||
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
|
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||||
|
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||||
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29 h1:DJUvgAPiJWeMBiT+RzBVcJGQN7bAEWS5UEoMshES9xs=
|
||||||
|
google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||||
|
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
|
||||||
|
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||||
|
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f h1:rqzndB2lIQGivcXdTuY3Y9NBvr70X+y77woofSRluec=
|
||||||
|
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f/go.mod h1:gxndsbNG1n4TZcHGgsYEfVGnTxqfEdfiDv6/DADXX9o=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
|
||||||
|
nullprogram.com/x/optparse v1.0.0 h1:xGFgVi5ZaWOnYdac2foDT3vg0ZZC9ErXFV57mr4OHrI=
|
||||||
|
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 无 Docker 的最小任务流演示:devnats(内嵌NATS) + gateway + dispatcher。
|
||||||
|
# 提交一个 DSL 任务,验证 Gateway → NATS → Dispatcher 全链路。
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
mkdir -p .bin
|
||||||
|
echo "== 编译 =="
|
||||||
|
( cd sundynix-shared && go build -o ../.bin/devnats ./cmd/devnats )
|
||||||
|
( cd sundynix-gateway && go build -o ../.bin/gateway ./cmd/server )
|
||||||
|
( cd sundynix-dispatcher && go build -o ../.bin/dispatcher ./cmd/dispatcher )
|
||||||
|
|
||||||
|
cleanup() { kill "${GW_PID:-}" "${DISP_PID:-}" "${NATS_PID:-}" 2>/dev/null || true; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# 若 :4222 已有 NATS(docker compose 的容器),直接复用;否则起内嵌 devnats。
|
||||||
|
if nc -z 127.0.0.1 4222 2>/dev/null; then
|
||||||
|
echo "== 检测到已运行的 NATS(:4222),复用之 =="
|
||||||
|
else
|
||||||
|
echo "== 启动内嵌 devnats =="
|
||||||
|
.bin/devnats > .bin/devnats.log 2>&1 & NATS_PID=$!
|
||||||
|
for _ in $(seq 1 30); do nc -z 127.0.0.1 4222 2>/dev/null && break || sleep 0.2; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== 启动 dispatcher / gateway =="
|
||||||
|
.bin/dispatcher > .bin/dispatcher.log 2>&1 & DISP_PID=$!
|
||||||
|
.bin/gateway > .bin/gateway.log 2>&1 & GW_PID=$!
|
||||||
|
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
curl -s -o /dev/null http://127.0.0.1:8080/api/v1/billing && break || sleep 0.3
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "== 提交 DSL 任务 =="
|
||||||
|
RESP=$(curl -s -X POST http://127.0.0.1:8080/api/v1/tasks \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"nodes":[{"id":"n1","type":"agent","data":{"prompt":"hello"}}],"edges":[]}')
|
||||||
|
echo "$RESP"
|
||||||
|
TASK_ID=$(echo "$RESP" | sed -n 's/.*"task_id":"\([^"]*\)".*/\1/p')
|
||||||
|
|
||||||
|
echo "== 订阅 SSE Token 流 (Gateway ← NATS ← Dispatcher) =="
|
||||||
|
# 客户端在 TTFT(700ms) 内连上即可收全部 token;--max-time 超时(exit 28) 属正常,不让 set -e 中断
|
||||||
|
curl -sN --max-time 10 "http://127.0.0.1:8080/api/v1/tasks/$TASK_ID/stream" || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "== dispatcher 日志 =="
|
||||||
|
cat .bin/dispatcher.log
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// App 通过 Wails 的 TS/Go 强绑定暴露给前端,承载本地文件 I/O 等能力。
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp() *App { return &App{} }
|
||||||
|
|
||||||
|
// SubmitDSL 接收 React Flow 导出的 JSON DSL,转发到 Gateway。
|
||||||
|
func (a *App) SubmitDSL(dsl string) (string, error) {
|
||||||
|
// TODO: HTTP POST 到 sundynix-gateway /api/v1/tasks
|
||||||
|
return "task_placeholder", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLocalFile 本地文件系统 I/O(Local File System I/O)。
|
||||||
|
func (a *App) ReadLocalFile(path string) (string, error) {
|
||||||
|
// TODO: os.ReadFile,受权限白名单约束
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>sundynix-agentix</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "sundynix-desktop-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"@xyflow/react": "^12.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vite": "^5.4.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"postcss": "^8.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { AgentCanvas } from "./canvas/AgentCanvas";
|
||||||
|
import { WikiPanel } from "./wiki/WikiPanel";
|
||||||
|
|
||||||
|
// UI Representation Layer —— 顶层布局:左侧编排画布 + 右侧 Wiki 面板。
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-screen">
|
||||||
|
<main className="flex-1 border-r">
|
||||||
|
<AgentCanvas />
|
||||||
|
</main>
|
||||||
|
<aside className="w-96 overflow-auto">
|
||||||
|
<WikiPanel />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
Background,
|
||||||
|
Controls,
|
||||||
|
addEdge,
|
||||||
|
useNodesState,
|
||||||
|
useEdgesState,
|
||||||
|
type Connection,
|
||||||
|
} from "@xyflow/react";
|
||||||
|
import "@xyflow/react/dist/style.css";
|
||||||
|
|
||||||
|
import { exportDsl } from "../lib/dsl";
|
||||||
|
|
||||||
|
// React Flow Canvas —— Agent 编排,可导出 JSON DSL 提交到 Gateway。
|
||||||
|
export function AgentCanvas() {
|
||||||
|
const [nodes, , onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
|
|
||||||
|
const onConnect = useCallback(
|
||||||
|
(c: Connection) => setEdges((eds) => addEdge(c, eds)),
|
||||||
|
[setEdges],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onExport = useCallback(() => {
|
||||||
|
const dsl = exportDsl(nodes, edges); // → JSON DSL export
|
||||||
|
// TODO: 经 Wails 强绑定调用 App.SubmitDSL(dsl)
|
||||||
|
console.log(dsl);
|
||||||
|
}, [nodes, edges]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<button onClick={onExport} className="absolute z-10 m-2 rounded border px-3 py-1">
|
||||||
|
导出 DSL
|
||||||
|
</button>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Edge, Node } from "@xyflow/react";
|
||||||
|
|
||||||
|
// Task DSL —— React Flow 画布的可序列化表示,提交给 Gateway 解析组装。
|
||||||
|
export interface TaskDsl {
|
||||||
|
version: "1";
|
||||||
|
nodes: Array<{ id: string; type?: string; data: unknown }>;
|
||||||
|
edges: Array<{ source: string; target: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// exportDsl 把画布的节点/连线导出为 JSON DSL。
|
||||||
|
export function exportDsl(nodes: Node[], edges: Edge[]): TaskDsl {
|
||||||
|
return {
|
||||||
|
version: "1",
|
||||||
|
nodes: nodes.map((n) => ({ id: n.id, type: n.type, data: n.data })),
|
||||||
|
edges: edges.map((e) => ({ source: e.source, target: e.target })),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// LLM Wiki Management Panel —— 管理知识库条目,触发第 5 层混合检索。
|
||||||
|
export function WikiPanel() {
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="mb-2 text-lg font-semibold">LLM Wiki</h2>
|
||||||
|
<input
|
||||||
|
className="mb-3 w-full rounded border px-2 py-1"
|
||||||
|
placeholder="搜索 Wiki(Hybrid: Bleve + Qdrant + Neo4j)"
|
||||||
|
/>
|
||||||
|
{/* TODO: 检索结果列表 / 条目编辑 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "@/*": ["./src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: { "@": path.resolve(__dirname, "./src") },
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/sundynix/sundynix-desktop
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v2 v2.9.2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Command sundynix-desktop —— 第 1 层客户端,Wails 本地 Go 运行时入口。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := NewApp()
|
||||||
|
_ = wails.Run(&options.App{
|
||||||
|
Title: "sundynix-agentix",
|
||||||
|
Width: 1280,
|
||||||
|
Height: 800,
|
||||||
|
// Bind: TS/Go 强绑定 —— 把 App 的方法暴露给前端
|
||||||
|
Bind: []any{app},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "sundynix_desktop",
|
||||||
|
"outputfilename": "sundynix_desktop",
|
||||||
|
"frontend:install": "npm install",
|
||||||
|
"frontend:build": "npm run build",
|
||||||
|
"frontend:dev:watcher": "npm run dev",
|
||||||
|
"frontend:dev:serverUrl": "auto",
|
||||||
|
"author": {
|
||||||
|
"name": "sundynix"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Command dispatcher 启动 sundynix-dispatcher —— 第 4 层 AI Agent 调度集群。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-dispatcher/internal/eino"
|
||||||
|
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||||||
|
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||||||
|
dnats "github.com/sundynix/sundynix-dispatcher/internal/nats"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
||||||
|
|
||||||
|
pool := llm.NewPool() // LLM Pool: vLLM / Ollama 集群
|
||||||
|
breaker := harness.NewCircuitBreaker() // Harness: 熔断降级中心
|
||||||
|
|
||||||
|
sub := dnats.MustConnect(natsURL)
|
||||||
|
defer sub.Close()
|
||||||
|
|
||||||
|
// sub 同时作为 Token 回流出口(TokenSink)。
|
||||||
|
orch := eino.NewOrchestrator(pool, breaker, sub)
|
||||||
|
|
||||||
|
// 监听退出信号,优雅停止消费。
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
log.Println("[dispatcher] consuming sundynix.tasks.* (Ctrl-C to quit)")
|
||||||
|
if err := sub.ConsumeTasks(ctx, orch.Handle); err != nil && err != context.Canceled {
|
||||||
|
log.Fatalf("[dispatcher] exit: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
nats:
|
||||||
|
url: "nats://localhost:4222"
|
||||||
|
task_subject: "sundynix.tasks.*"
|
||||||
|
queue_group: "dispatchers"
|
||||||
|
stream_prefix: "sundynix.streams"
|
||||||
|
|
||||||
|
llm_pool:
|
||||||
|
backends:
|
||||||
|
- name: "vllm-0"
|
||||||
|
base_url: "http://localhost:8000/v1"
|
||||||
|
type: "vllm"
|
||||||
|
- name: "ollama-0"
|
||||||
|
base_url: "http://localhost:11434"
|
||||||
|
type: "ollama"
|
||||||
|
|
||||||
|
circuit_breaker:
|
||||||
|
error_threshold: 0.5
|
||||||
|
open_timeout: "30s"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module github.com/sundynix/sundynix-dispatcher
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require github.com/sundynix/sundynix-shared v0.0.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/nats-io/nats.go v1.37.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/sundynix/sundynix-shared => ../sundynix-shared
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||||
|
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20 h1:CXDTYNHeBiAKBTAIP2gjpgbWap2GhATnTLgP8etyvEI=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20/go.mod h1:hgcPnoUtMfxz1qVOvLZGurVypQ+Cg6GXVXjG53iHk+M=
|
||||||
|
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||||
|
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||||
|
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||||
|
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// Package eino 封装基于 CloudWeGo Eino 的 Agent 图编排引擎。
|
||||||
|
package eino
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-dispatcher/internal/harness"
|
||||||
|
"github.com/sundynix/sundynix-dispatcher/internal/llm"
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenSink 是 Token 流回流出口(由 NATS bus 实现)。
|
||||||
|
type TokenSink interface {
|
||||||
|
PublishToken(taskID string, token []byte) error
|
||||||
|
CompleteStream(taskID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orchestrator 将 DSL 图编译为 Eino Graph 并驱动执行。
|
||||||
|
type Orchestrator struct {
|
||||||
|
pool *llm.Pool
|
||||||
|
breaker *harness.CircuitBreaker
|
||||||
|
sink TokenSink
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrchestrator(pool *llm.Pool, breaker *harness.CircuitBreaker, sink TokenSink) *Orchestrator {
|
||||||
|
return &Orchestrator{pool: pool, breaker: breaker, sink: sink}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle 消费一个任务:编译图 → 流式推理 → 经 sink 把 Token 回流到 sundynix.streams.<id>。
|
||||||
|
func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
|
||||||
|
if !o.breaker.Allow() {
|
||||||
|
log.Printf("[eino] circuit open, drop task %s", t.ID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Printf("[eino] task %s received (graph=%d bytes), streaming tokens...", t.ID, len(t.Graph))
|
||||||
|
|
||||||
|
// TODO: compose.NewGraph(...) 编译 DSL;此处 prompt 占位为图原文。
|
||||||
|
// 工具节点经 NATS 调用第 5 层 MCP(sundynix.tools.go.* / sundynix.tools.py.*)。
|
||||||
|
prompt := string(t.Graph)
|
||||||
|
|
||||||
|
n := 0
|
||||||
|
err := o.pool.Stream(ctx, prompt, func(tok []byte) {
|
||||||
|
if perr := o.sink.PublishToken(t.ID, tok); perr != nil {
|
||||||
|
log.Printf("[eino] publish token failed: %v", perr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[eino] task %s stream error: %v", t.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cerr := o.sink.CompleteStream(t.ID); cerr != nil {
|
||||||
|
log.Printf("[eino] complete stream failed: %v", cerr)
|
||||||
|
}
|
||||||
|
log.Printf("[eino] task %s done, %d tokens streamed", t.ID, n)
|
||||||
|
o.breaker.Report(err == nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package harness
|
||||||
|
|
||||||
|
// CircuitBreaker 实现熔断降级中心:后端异常时熔断并切换降级策略。
|
||||||
|
type CircuitBreaker struct{ /* state, counters */ }
|
||||||
|
|
||||||
|
func NewCircuitBreaker() *CircuitBreaker { return &CircuitBreaker{} }
|
||||||
|
|
||||||
|
// Allow 判定当前是否放行请求。
|
||||||
|
func (c *CircuitBreaker) Allow() bool { return true } // TODO: half-open / open 状态机
|
||||||
|
|
||||||
|
// Report 上报一次调用结果以驱动状态机。
|
||||||
|
func (c *CircuitBreaker) Report(success bool) {} // TODO
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Package harness 提供 LLM 自动化评测与熔断降级能力。
|
||||||
|
package harness
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Evaluator 实现 LLM 自动化评测(质量打分 / 回归对比)。
|
||||||
|
type Evaluator struct{}
|
||||||
|
|
||||||
|
func NewEvaluator() *Evaluator { return &Evaluator{} }
|
||||||
|
|
||||||
|
// Score 对一次推理输出打分。
|
||||||
|
func (e *Evaluator) Score(ctx context.Context, input, output string) (float64, error) {
|
||||||
|
// TODO: LLM-as-judge / 规则评测
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Package llm 抽象 LLM Pool(vLLM / Ollama 集群)的负载均衡与流式推理。
|
||||||
|
package llm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pool 维护后端 LLM 实例列表与路由策略。
|
||||||
|
type Pool struct{ /* backends []Backend */ }
|
||||||
|
|
||||||
|
func NewPool() *Pool { return &Pool{} }
|
||||||
|
|
||||||
|
// 占位参数:模拟真实后端的 TTFT(首 token 延迟) 与逐 token 间隔。
|
||||||
|
const (
|
||||||
|
timeToFirstToken = 700 * time.Millisecond
|
||||||
|
interTokenDelay = 60 * time.Millisecond
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stream 选择一个后端进行流式推理,逐 Token 回调 onToken。
|
||||||
|
// 当前为占位实现:把对 prompt 的确定性回复按 token 流式返回,
|
||||||
|
// 真实接入 vLLM/Ollama 时替换为后端 streaming API 即可(回调签名不变)。
|
||||||
|
func (p *Pool) Stream(ctx context.Context, prompt string, onToken func([]byte)) error {
|
||||||
|
// TODO: 选路 (least-load / 模型亲和) → 调 vLLM/Ollama streaming API
|
||||||
|
reply := buildReply(prompt)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(timeToFirstToken): // 模拟 TTFT
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tok := range tokenize(reply) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
onToken([]byte(tok))
|
||||||
|
time.Sleep(interTokenDelay)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildReply 占位:真实实现应由 DSL 编排出的对话上下文驱动后端生成。
|
||||||
|
func buildReply(prompt string) string {
|
||||||
|
p := strings.TrimSpace(prompt)
|
||||||
|
if len(p) > 40 {
|
||||||
|
p = p[:40] + "…"
|
||||||
|
}
|
||||||
|
return "已编排执行该 Agent 图,输入摘要: " + p
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenize 占位分词:按 rune 切,保证多字节中文也能逐字流式。
|
||||||
|
func tokenize(s string) []string {
|
||||||
|
out := make([]string, 0, len(s))
|
||||||
|
for _, r := range s {
|
||||||
|
out = append(out, string(r))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// Package nats 是调度器对共享 bus 的薄封装(消费任务 / 回写 Token)。
|
||||||
|
package nats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskHandler 处理单个任务。
|
||||||
|
type TaskHandler func(ctx context.Context, t *contract.Task) error
|
||||||
|
|
||||||
|
// Subscriber 包装共享 bus,向调度器暴露消费能力。
|
||||||
|
type Subscriber struct {
|
||||||
|
inner *sharedbus.Bus
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustConnect 接入 NATS 并确保任务流存在(消费者声明在 Consume 时完成)。
|
||||||
|
func MustConnect(url string) *Subscriber {
|
||||||
|
inner, err := sharedbus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[dispatcher/nats] connect: %v", err)
|
||||||
|
}
|
||||||
|
if err := inner.EnsureTaskStream(context.Background()); err != nil {
|
||||||
|
log.Fatalf("[dispatcher/nats] ensure stream: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("[dispatcher/nats] connected %s", url)
|
||||||
|
return &Subscriber{inner: inner}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeTasks 从 sundynix.tasks.* 持续消费任务(队列组负载均衡),阻塞至 ctx 取消。
|
||||||
|
func (s *Subscriber) ConsumeTasks(ctx context.Context, h TaskHandler) error {
|
||||||
|
stop, err := s.inner.ConsumeTasks(ctx, func(c context.Context, t *contract.Task) error {
|
||||||
|
return h(c, t)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stop()
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishToken / CompleteStream 让 Subscriber 满足 eino.TokenSink,
|
||||||
|
// 把推理 Token 回流到 sundynix.streams.<taskID>。
|
||||||
|
func (s *Subscriber) PublishToken(taskID string, token []byte) error {
|
||||||
|
return s.inner.PublishToken(taskID, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) CompleteStream(taskID string) error {
|
||||||
|
return s.inner.CompleteStream(taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) Close() { s.inner.Close() }
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Command server 启动 sundynix-gateway —— 第 2 层业务网关 / 统一接入层。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/router"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
natsURL := envOr("NATS_URL", "nats://localhost:4222")
|
||||||
|
|
||||||
|
db := store.MustOpenPostgres() // MainDB: Users / Billing / DSL
|
||||||
|
cache := store.MustOpenRedis() // CacheDB: Session / Rate Limit
|
||||||
|
bus := nats.MustConnect(natsURL) // 接入 NATS 零拷贝骨干网 + 声明任务流
|
||||||
|
defer bus.Close()
|
||||||
|
|
||||||
|
r := router.New(db, cache, bus)
|
||||||
|
addr := envOr("GATEWAY_ADDR", ":8080")
|
||||||
|
log.Printf("[gateway] listening on %s", addr)
|
||||||
|
if err := r.Run(addr); err != nil {
|
||||||
|
log.Fatalf("[gateway] exit: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func envOr(key, def string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
server:
|
||||||
|
addr: ":8080"
|
||||||
|
|
||||||
|
postgres: # MainDB
|
||||||
|
dsn: "postgres://sundynix:sundynix@localhost:5432/sundynix?sslmode=disable"
|
||||||
|
|
||||||
|
redis: # CacheDB
|
||||||
|
addr: "localhost:6379"
|
||||||
|
|
||||||
|
nats:
|
||||||
|
url: "nats://localhost:4222"
|
||||||
|
task_subject: "sundynix.tasks"
|
||||||
|
stream_prefix: "sundynix.streams"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
module github.com/sundynix/sundynix-gateway
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/sundynix/sundynix-shared v0.0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/sundynix/sundynix-shared => ../sundynix-shared
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/nats-io/nats.go v1.37.0 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.27.0 // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
golang.org/x/text v0.18.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
|
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||||
|
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20 h1:CXDTYNHeBiAKBTAIP2gjpgbWap2GhATnTLgP8etyvEI=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20/go.mod h1:hgcPnoUtMfxz1qVOvLZGurVypQ+Cg6GXVXjG53iHk+M=
|
||||||
|
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||||
|
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||||
|
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||||
|
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
|
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// Package dsl 负责把客户端导出的 JSON DSL 解析并组装为可调度的 Task。
|
||||||
|
package dsl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseAndAssemble 校验 DSL 结构并生成共享契约中的 Task。
|
||||||
|
func ParseAndAssemble(raw json.RawMessage) (*contract.Task, error) {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return nil, errors.New("empty dsl")
|
||||||
|
}
|
||||||
|
// 轻量结构校验:至少要能解析为对象。
|
||||||
|
var probe map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||||
|
return nil, errors.New("invalid dsl json: " + err.Error())
|
||||||
|
}
|
||||||
|
// TODO: 节点拓扑校验 / 节点-工具映射
|
||||||
|
return &contract.Task{
|
||||||
|
ID: newID(),
|
||||||
|
Graph: raw,
|
||||||
|
Meta: map[string]any{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newID() string {
|
||||||
|
var b [8]byte
|
||||||
|
_, _ = rand.Read(b[:])
|
||||||
|
return "task_" + hex.EncodeToString(b[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Package handler 实现网关的 HTTP 处理器。
|
||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/dsl"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
db *store.Postgres
|
||||||
|
cache *store.Redis
|
||||||
|
bus *nats.Bus
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus) *Handler {
|
||||||
|
return &Handler{db: db, cache: cache, bus: bus}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTask: 解析客户端导出的 JSON DSL,组装为 Task,Publish 到 sundynix.tasks.*。
|
||||||
|
func (h *Handler) SubmitTask(c *gin.Context) {
|
||||||
|
var raw json.RawMessage
|
||||||
|
if err := c.ShouldBindJSON(&raw); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task, err := dsl.ParseAndAssemble(raw) // Task DSL Parser & Assembly
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.bus.PublishTask(c.Request.Context(), task); err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusAccepted, gin.H{"task_id": task.ID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamTask: 订阅 sundynix.streams.<task_id>,以 SSE 把零拷贝 Token Stream 推给客户端。
|
||||||
|
func (h *Handler) StreamTask(c *gin.Context) {
|
||||||
|
taskID := c.Param("id")
|
||||||
|
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||||
|
c.Writer.Header().Set("Connection", "keep-alive")
|
||||||
|
|
||||||
|
tokens := make(chan []byte, 256)
|
||||||
|
done := make(chan struct{})
|
||||||
|
unsub, err := h.bus.SubscribeTokens(taskID,
|
||||||
|
func(tok []byte) {
|
||||||
|
select {
|
||||||
|
case tokens <- tok:
|
||||||
|
default: // 背压保护:客户端过慢则丢弃,避免阻塞 NATS 回调
|
||||||
|
}
|
||||||
|
},
|
||||||
|
func() { close(done) },
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() { _ = unsub() }()
|
||||||
|
|
||||||
|
// gin 的流式写:返回 false 即结束响应。
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
select {
|
||||||
|
case tok := <-tokens:
|
||||||
|
c.SSEvent("token", string(tok))
|
||||||
|
return true
|
||||||
|
case <-done:
|
||||||
|
c.SSEvent("done", taskID)
|
||||||
|
return false
|
||||||
|
case <-c.Request.Context().Done():
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) Billing(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{"status": "ok"}) // TODO: 商业化与计费模块
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Package middleware 提供 Guardrail 与限流等接入层中间件。
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Guardrail 实现 Harness 的输入/输出护栏(敏感词、注入、配额校验等)。
|
||||||
|
func Guardrail() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// TODO: 输入护栏校验
|
||||||
|
c.Next()
|
||||||
|
// TODO: 输出护栏校验
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimit 基于 Redis 的会话级限流。
|
||||||
|
func RateLimit(cache *store.Redis) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// TODO: 令牌桶 / 滑动窗口
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Package nats 是网关对共享 bus 的薄封装(发布任务 / 订阅 Token 回流)。
|
||||||
|
package nats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
sharedbus "github.com/sundynix/sundynix-shared/bus"
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bus 包装共享 bus,向网关其余代码暴露发布能力。
|
||||||
|
type Bus struct {
|
||||||
|
inner *sharedbus.Bus
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustConnect 接入 NATS 并确保任务流存在。
|
||||||
|
func MustConnect(url string) *Bus {
|
||||||
|
inner, err := sharedbus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[nats] connect: %v", err)
|
||||||
|
}
|
||||||
|
if err := inner.EnsureTaskStream(context.Background()); err != nil {
|
||||||
|
log.Fatalf("[nats] ensure stream: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("[nats] connected %s, task stream ready", url)
|
||||||
|
return &Bus{inner: inner}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishTask 把组装后的 Task 发布到 sundynix.tasks.<id>。
|
||||||
|
func (b *Bus) PublishTask(ctx context.Context, t *contract.Task) error {
|
||||||
|
seq, err := b.inner.PublishTask(ctx, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("[nats] published task %s (seq=%d)", t.ID, seq)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeTokens 订阅 sundynix.streams.<taskID> 的 Token 回流,
|
||||||
|
// 每个 Token 触发 onToken,流结束触发 onDone,返回 unsub。
|
||||||
|
func (b *Bus) SubscribeTokens(taskID string, onToken func([]byte), onDone func()) (func() error, error) {
|
||||||
|
return b.inner.SubscribeTokens(taskID, onToken, onDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bus) Close() { b.inner.Close() }
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// Package router 装配 Gin 统一接入层的路由与中间件。
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/handler"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/middleware"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||||
|
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New 构建带有 Guardrail / 限流中间件的 Gin 引擎。
|
||||||
|
func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus) *gin.Engine {
|
||||||
|
r := gin.Default()
|
||||||
|
r.Use(middleware.RateLimit(cache))
|
||||||
|
r.Use(middleware.Guardrail()) // Harness: Input/Output Guardrail
|
||||||
|
|
||||||
|
h := handler.New(db, cache, bus)
|
||||||
|
api := r.Group("/api/v1")
|
||||||
|
{
|
||||||
|
api.POST("/tasks", h.SubmitTask) // 1. 解析 DSL 并 Publish 到 NATS
|
||||||
|
api.GET("/tasks/:id/stream", h.StreamTask) // 4. SSE/WS 回流 Token Stream
|
||||||
|
api.GET("/billing", h.Billing)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Package store 封装 MainDB(PgSQL) 与 CacheDB(Redis) 的访问。
|
||||||
|
package store
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
// Postgres 持有 MainDB 连接池(Users / Billing / DSL)。
|
||||||
|
type Postgres struct{ /* *pgxpool.Pool */ }
|
||||||
|
|
||||||
|
// MustOpenPostgres 建立 Postgres 连接,失败即退出。
|
||||||
|
func MustOpenPostgres() *Postgres {
|
||||||
|
// TODO: pgxpool.New(ctx, dsn)
|
||||||
|
log.Println("[store] postgres connected (stub)")
|
||||||
|
return &Postgres{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
// Redis 持有 CacheDB 连接(Session / Rate Limit)。
|
||||||
|
type Redis struct{ /* *redis.Client */ }
|
||||||
|
|
||||||
|
// MustOpenRedis 建立 Redis 连接,失败即退出。
|
||||||
|
func MustOpenRedis() *Redis {
|
||||||
|
// TODO: redis.NewClient(opts)
|
||||||
|
log.Println("[store] redis connected (stub)")
|
||||||
|
return &Redis{}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Command server 启动 sundynix-mcp-go —— 第 5 层 Go I/O 型 MCP 工具微服务。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-mcp-go/internal/mcp"
|
||||||
|
"github.com/sundynix/sundynix-mcp-go/internal/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
engine := search.NewHybrid() // LLM Wiki 混合检索:Bleve + Milvus + Neo4j
|
||||||
|
gw := mcp.NewGateway(engine)
|
||||||
|
|
||||||
|
log.Println("[mcp_go] serving MCP over sundynix.tools.go.*")
|
||||||
|
if err := gw.Serve(); err != nil {
|
||||||
|
log.Fatalf("[mcp_go] exit: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
nats:
|
||||||
|
url: "nats://localhost:4222"
|
||||||
|
tool_subject: "sundynix.tools.go.*"
|
||||||
|
|
||||||
|
bleve:
|
||||||
|
index_path: "./data/wiki.bleve"
|
||||||
|
|
||||||
|
milvus:
|
||||||
|
addr: "localhost:19530"
|
||||||
|
collection: "wiki"
|
||||||
|
|
||||||
|
neo4j:
|
||||||
|
uri: "bolt://localhost:7687"
|
||||||
|
user: "neo4j"
|
||||||
|
password: "sundynix"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module github.com/sundynix/sundynix-mcp-go
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/blevesearch/bleve/v2 v2.4.2
|
||||||
|
github.com/milvus-io/milvus-sdk-go/v2 v2.4.1
|
||||||
|
github.com/neo4j/neo4j-go-driver/v5 v5.24.0
|
||||||
|
github.com/nats-io/nats.go v1.37.0
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
github.com/blevesearch/bleve/v2 v2.4.2/go.mod h1:ATNKj7Yl2oJv/lGuF4kx39bST2dveX6w0th2FFYLkc8=
|
||||||
|
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||||
|
github.com/neo4j/neo4j-go-driver/v5 v5.24.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||||
|
github.com/qdrant/go-client v1.11.0/go.mod h1:j+OVRsJIZhOSRK2toPl8tTBOhwr4AxXCz9RACzv0JB4=
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// Package external 封装对第三方外部 API 的统一调用(鉴权、重试、限流)。
|
||||||
|
package external
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Client 是外部 API 的统一出口。
|
||||||
|
type Client struct{}
|
||||||
|
|
||||||
|
func NewClient() *Client { return &Client{} }
|
||||||
|
|
||||||
|
// Call 调用某个外部 API。
|
||||||
|
func (c *Client) Call(ctx context.Context, name string, args map[string]any) (any, error) {
|
||||||
|
// TODO: 路由到具体 provider,统一鉴权/重试/限流/审计
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Package mcp 实现 MCP 协议网关,把工具注册到 NATS 并响应调用。
|
||||||
|
package mcp
|
||||||
|
|
||||||
|
import "github.com/sundynix/sundynix-mcp-go/internal/search"
|
||||||
|
|
||||||
|
// Gateway 暴露 MCP 协议端点(stdio / HTTP / NATS)。
|
||||||
|
type Gateway struct {
|
||||||
|
search *search.Hybrid
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGateway(s *search.Hybrid) *Gateway { return &Gateway{search: s} }
|
||||||
|
|
||||||
|
// Serve 监听 sundynix.tools.go.* 并按 MCP 协议分发工具调用。
|
||||||
|
func (g *Gateway) Serve() error {
|
||||||
|
// TODO: 注册工具清单 (wiki_search / render_doc / call_external_api ...)
|
||||||
|
// 订阅 NATS,按 MCP JSON-RPC 解析并路由
|
||||||
|
select {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Package office 基于 UniOffice 提供 Word/文档渲染能力。
|
||||||
|
package office
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Renderer 把结构化数据渲染为 docx/xlsx 等文档。
|
||||||
|
type Renderer struct{}
|
||||||
|
|
||||||
|
func NewRenderer() *Renderer { return &Renderer{} }
|
||||||
|
|
||||||
|
// RenderDocx 生成 Word 文档并返回字节流。
|
||||||
|
func (r *Renderer) RenderDocx(ctx context.Context, payload map[string]any) ([]byte, error) {
|
||||||
|
// TODO: 使用 unioffice/document 构建并序列化
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Package search 实现 LLM Wiki 混合检索引擎。
|
||||||
|
// Hybrid Search = Bleve(全文/BM25) + Milvus(向量) + Neo4j(知识图谱) 融合排序。
|
||||||
|
package search
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Hybrid 聚合三路检索后端并做 RRF/加权融合。
|
||||||
|
type Hybrid struct {
|
||||||
|
// bleve *bleve.Index // Go 全文检索
|
||||||
|
// milvus client.Client // Vector DB (Milvus Go SDK)
|
||||||
|
// neo4j neo4j.DriverWithContext // Knowledge Graph
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHybrid() *Hybrid {
|
||||||
|
// TODO: 打开 bleve 索引;连接 Milvus;连接 Neo4j
|
||||||
|
return &Hybrid{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result 是融合后的检索结果。
|
||||||
|
type Result struct {
|
||||||
|
ID string
|
||||||
|
Score float64
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query 并行查询三路后端并融合排序。
|
||||||
|
func (h *Hybrid) Query(ctx context.Context, q string, topK int) ([]Result, error) {
|
||||||
|
// TODO: 并发 bleve.Search + milvus.Search + neo4j Cypher,做 RRF 融合
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
nats:
|
||||||
|
url: "nats://localhost:4222"
|
||||||
|
tool_subject: "sundynix.tools.py.*"
|
||||||
|
|
||||||
|
sandbox:
|
||||||
|
runtime: "gvisor" # gvisor | kata
|
||||||
|
mem_limit: "512m"
|
||||||
|
timeout: "30s"
|
||||||
|
|
||||||
|
interpreter:
|
||||||
|
image: "python:3.11-slim"
|
||||||
|
network_disabled: true
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "sundynix-mcp-py"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "sundynix-agentix · 第 5 层 Python 算法型 MCP 工具微服务"
|
||||||
|
requires-python = ">=3.11"
|
||||||
|
dependencies = [
|
||||||
|
"mcp>=1.2.0", # MCP 协议
|
||||||
|
"nats-py>=2.7.0", # 接入 NATS 骨干网
|
||||||
|
"docker>=7.1.0", # Docker 隔离沙箱 / Code Interpreter
|
||||||
|
# "magic-pdf", # MinerU 多模态解析 (PaddleOCR),按需安装
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/sundynix_mcp_py"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""sundynix_mcp_py — 第 5 层 Python 算法型 MCP 工具微服务。"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Docker 隔离沙箱:Code Interpreter 执行不可信代码并采集产物。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class CodeInterpreter:
|
||||||
|
"""Docker 隔离沙箱 · Code Interpreter。"""
|
||||||
|
|
||||||
|
async def execute(self, code: str, *, image: str = "python:3.11-slim") -> dict:
|
||||||
|
"""在一次性 Docker 容器中执行代码,返回 stdout/stderr/artifacts。"""
|
||||||
|
# TODO: docker.from_env().containers.run(..., network_disabled=True, mem_limit=...)
|
||||||
|
return {"stdout": "", "stderr": "", "artifacts": []}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""入口:启动 MCP 协议网关,把算法型工具注册到 NATS。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .mcp_gateway import McpGateway
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
log = logging.getLogger("mcp_py")
|
||||||
|
|
||||||
|
|
||||||
|
async def _run() -> None:
|
||||||
|
gateway = McpGateway()
|
||||||
|
await gateway.register_tools() # secure_sandbox / parse_document / run_code
|
||||||
|
log.info("[mcp_py] serving MCP over sundynix.tools.py.*")
|
||||||
|
await gateway.serve()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
asyncio.run(_run())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""MCP 协议网关:注册算法型工具并经 NATS 分发调用。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .interpreter import CodeInterpreter
|
||||||
|
from .mineru import MultimodalParser
|
||||||
|
from .sandbox import SecureSandbox
|
||||||
|
|
||||||
|
|
||||||
|
class McpGateway:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.sandbox = SecureSandbox() # gVisor / KataVM + Static Code Guard
|
||||||
|
self.parser = MultimodalParser() # MinerU / PaddleOCR
|
||||||
|
self.interpreter = CodeInterpreter() # Docker 隔离沙箱
|
||||||
|
|
||||||
|
async def register_tools(self) -> None:
|
||||||
|
# TODO: 向 MCP server 注册工具 schema
|
||||||
|
...
|
||||||
|
|
||||||
|
async def serve(self) -> None:
|
||||||
|
# TODO: 连接 NATS,订阅 sundynix.tools.py.*,按 MCP JSON-RPC 路由
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
await asyncio.Event().wait()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""MinerU 多模态解析:PDF/图片 → 结构化文本(PaddleOCR)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class MultimodalParser:
|
||||||
|
"""MinerU · Multimodal Parser (PaddleOCR)。"""
|
||||||
|
|
||||||
|
async def parse(self, file_path: str) -> dict:
|
||||||
|
"""解析文档,返回结构化内容(标题/段落/表格/公式)。"""
|
||||||
|
# TODO: 调 magic-pdf / PaddleOCR 流水线
|
||||||
|
return {"path": file_path, "blocks": []}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
"""安全代码沙箱:gVisor / KataVM 强隔离 + 静态代码守卫。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class SecureSandbox:
|
||||||
|
"""Harness: Secure Code Sandbox。"""
|
||||||
|
|
||||||
|
def static_guard(self, code: str) -> bool:
|
||||||
|
"""静态代码守卫:危险调用/导入检测,返回是否放行。"""
|
||||||
|
# TODO: AST 扫描,拦截 os/subprocess/网络等高危调用
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def run(self, code: str, *, runtime: str = "gvisor") -> str:
|
||||||
|
"""在 gVisor/KataVM 隔离环境中执行代码。"""
|
||||||
|
if not self.static_guard(code):
|
||||||
|
raise PermissionError("static code guard rejected")
|
||||||
|
# TODO: 调度到 gVisor(runsc) / Kata 容器执行并回收
|
||||||
|
return ""
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Package bus 封装 NATS JetStream 的连接、流声明、任务发布与消费。
|
||||||
|
// Gateway 与 Dispatcher 共用这套真实收发逻辑,e2e 测试也直接覆盖它。
|
||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bus 持有 NATS 连接与 JetStream 上下文。
|
||||||
|
type Bus struct {
|
||||||
|
nc *nats.Conn
|
||||||
|
js jetstream.JetStream
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 接入 NATS 骨干网并初始化 JetStream,使用默认重试参数。
|
||||||
|
func Connect(url string) (*Bus, error) {
|
||||||
|
return ConnectWithRetry(url, 30, time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectWithRetry 在 NATS 暂不可用时按固定间隔重试,容忍服务先于 NATS 启动。
|
||||||
|
func ConnectWithRetry(url string, attempts int, interval time.Duration) (*Bus, error) {
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < attempts; i++ {
|
||||||
|
nc, err := nats.Connect(url,
|
||||||
|
nats.Timeout(5*time.Second),
|
||||||
|
nats.RetryOnFailedConnect(true),
|
||||||
|
nats.MaxReconnects(-1),
|
||||||
|
nats.ReconnectWait(interval),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
time.Sleep(interval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// RetryOnFailedConnect 下 Connect 可能立即返回但尚未连上,等待真正建立。
|
||||||
|
if nc.Status() != nats.CONNECTED {
|
||||||
|
if !waitConnected(nc, 5*time.Second) {
|
||||||
|
lastErr = fmt.Errorf("nats not connected within timeout")
|
||||||
|
nc.Close()
|
||||||
|
time.Sleep(interval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
js, err := jetstream.New(nc)
|
||||||
|
if err != nil {
|
||||||
|
nc.Close()
|
||||||
|
return nil, fmt.Errorf("jetstream init: %w", err)
|
||||||
|
}
|
||||||
|
return &Bus{nc: nc, js: js}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("nats connect after %d attempts: %w", attempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitConnected(nc *nats.Conn, d time.Duration) bool {
|
||||||
|
deadline := time.Now().Add(d)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if nc.Status() == nats.CONNECTED {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return nc.Status() == nats.CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭底层连接。
|
||||||
|
func (b *Bus) Close() {
|
||||||
|
if b.nc != nil {
|
||||||
|
b.nc.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureTaskStream 幂等地创建/更新任务流,捕获 sundynix.tasks.>。
|
||||||
|
func (b *Bus) EnsureTaskStream(ctx context.Context) error {
|
||||||
|
_, err := b.js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{
|
||||||
|
Name: contract.StreamTasks,
|
||||||
|
Subjects: []string{contract.SubjectTasksAll},
|
||||||
|
Storage: jetstream.FileStorage,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishTask 把任务发布到 sundynix.tasks.<id>,返回序列号。
|
||||||
|
func (b *Bus) PublishTask(ctx context.Context, t *contract.Task) (uint64, error) {
|
||||||
|
data, err := t.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
ack, err := b.js.Publish(ctx, contract.TaskSubject(t.ID), data)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("publish task: %w", err)
|
||||||
|
}
|
||||||
|
return ack.Sequence, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Token 流回流(core NATS 零拷贝字节管道)----
|
||||||
|
|
||||||
|
// PublishToken 把一个推理 Token 以 core NATS 写到 sundynix.streams.<taskID>。
|
||||||
|
func (b *Bus) PublishToken(taskID string, token []byte) error {
|
||||||
|
return b.nc.Publish(contract.StreamSubject(taskID), token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteStream 发送 Token 流结束信号(空体 + 结束头)。
|
||||||
|
func (b *Bus) CompleteStream(taskID string) error {
|
||||||
|
msg := nats.NewMsg(contract.StreamSubject(taskID))
|
||||||
|
msg.Header.Set(contract.HeaderStreamEnd, "1")
|
||||||
|
return b.nc.PublishMsg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeTokens 订阅某 task 的 Token 流。每个 Token 触发 onToken;
|
||||||
|
// 收到结束信号后触发 onDone。返回的 unsub 用于退订。
|
||||||
|
// 注意:core NATS 无持久化,订阅须在 Token 产生前建立(SSE 客户端先连)。
|
||||||
|
func (b *Bus) SubscribeTokens(taskID string, onToken func([]byte), onDone func()) (unsub func() error, err error) {
|
||||||
|
sub, err := b.nc.Subscribe(contract.StreamSubject(taskID), func(m *nats.Msg) {
|
||||||
|
if m.Header.Get(contract.HeaderStreamEnd) == "1" {
|
||||||
|
onDone()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 拷贝,避免 nats 复用底层 buffer。
|
||||||
|
tok := make([]byte, len(m.Data))
|
||||||
|
copy(tok, m.Data)
|
||||||
|
onToken(tok)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("subscribe tokens: %w", err)
|
||||||
|
}
|
||||||
|
return sub.Unsubscribe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskHandler 处理一个消费到的任务。
|
||||||
|
type TaskHandler func(ctx context.Context, t *contract.Task) error
|
||||||
|
|
||||||
|
// ConsumeTasks 在持久消费者上消费任务,队列组内负载均衡。
|
||||||
|
// 返回的 stop 函数用于优雅停止消费。
|
||||||
|
func (b *Bus) ConsumeTasks(ctx context.Context, h TaskHandler) (stop func(), err error) {
|
||||||
|
cons, err := b.js.CreateOrUpdateConsumer(ctx, contract.StreamTasks, jetstream.ConsumerConfig{
|
||||||
|
Durable: contract.ConsumerDurable,
|
||||||
|
AckPolicy: jetstream.AckExplicitPolicy,
|
||||||
|
FilterSubject: contract.SubjectTasksAll,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create consumer: %w", err)
|
||||||
|
}
|
||||||
|
cc, err := cons.Consume(func(msg jetstream.Msg) {
|
||||||
|
t, err := contract.Unmarshal(msg.Data())
|
||||||
|
if err != nil {
|
||||||
|
_ = msg.Term() // 脏数据,丢弃不重投
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h(ctx, t); err != nil {
|
||||||
|
_ = msg.NakWithDelay(time.Second) // 处理失败,延迟重投
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = msg.Ack()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("consume: %w", err)
|
||||||
|
}
|
||||||
|
return cc.Stop, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package bus_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
natsserver "github.com/nats-io/nats-server/v2/server"
|
||||||
|
natstest "github.com/nats-io/nats-server/v2/test"
|
||||||
|
|
||||||
|
"github.com/sundynix/sundynix-shared/bus"
|
||||||
|
"github.com/sundynix/sundynix-shared/contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
// startEmbeddedNATS 启动一个内嵌、开启 JetStream 的 NATS 服务器,免 Docker。
|
||||||
|
func startEmbeddedNATS(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
opts := natstest.DefaultTestOptions
|
||||||
|
opts.Port = -1 // 随机端口
|
||||||
|
opts.JetStream = true
|
||||||
|
opts.StoreDir = t.TempDir()
|
||||||
|
srv := natstest.RunServer(&opts)
|
||||||
|
if !srv.ReadyForConnections(5 * time.Second) {
|
||||||
|
t.Fatal("embedded NATS not ready")
|
||||||
|
}
|
||||||
|
t.Cleanup(srv.Shutdown)
|
||||||
|
_ = natsserver.Server{} // 触发包引用
|
||||||
|
return srv.ClientURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTaskRoundTrip 模拟 Gateway 发布 → NATS → Dispatcher 消费 的完整任务流。
|
||||||
|
func TestTaskRoundTrip(t *testing.T) {
|
||||||
|
url := startEmbeddedNATS(t)
|
||||||
|
|
||||||
|
// --- Gateway 侧:连接并声明任务流 ---
|
||||||
|
gw, err := bus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gateway connect: %v", err)
|
||||||
|
}
|
||||||
|
defer gw.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := gw.EnsureTaskStream(ctx); err != nil {
|
||||||
|
t.Fatalf("ensure stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dispatcher 侧:连接并开始消费 ---
|
||||||
|
dp, err := bus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatcher connect: %v", err)
|
||||||
|
}
|
||||||
|
defer dp.Close()
|
||||||
|
|
||||||
|
got := make(chan *contract.Task, 1)
|
||||||
|
stop, err := dp.ConsumeTasks(ctx, func(_ context.Context, task *contract.Task) error {
|
||||||
|
got <- task
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("consume: %v", err)
|
||||||
|
}
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
// --- Gateway 发布一个任务 ---
|
||||||
|
want := &contract.Task{
|
||||||
|
ID: "task_demo_001",
|
||||||
|
Graph: json.RawMessage(`{"nodes":[{"id":"n1","type":"agent"}],"edges":[]}`),
|
||||||
|
Meta: map[string]any{"user": "wt"},
|
||||||
|
}
|
||||||
|
seq, err := gw.PublishTask(ctx, want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("publish: %v", err)
|
||||||
|
}
|
||||||
|
if seq == 0 {
|
||||||
|
t.Fatal("expected non-zero stream sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 断言 Dispatcher 收到同一个任务 ---
|
||||||
|
select {
|
||||||
|
case task := <-got:
|
||||||
|
if task.ID != want.ID {
|
||||||
|
t.Fatalf("task id = %q, want %q", task.ID, want.ID)
|
||||||
|
}
|
||||||
|
if task.Meta["user"] != "wt" {
|
||||||
|
t.Fatalf("task meta lost: %+v", task.Meta)
|
||||||
|
}
|
||||||
|
t.Logf("✓ 任务流打通:Gateway publish (seq=%d) → NATS → Dispatcher consume,task_id=%s", seq, task.ID)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timeout: dispatcher 未收到任务")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenStreamRoundTrip 模拟 Dispatcher 回流 Token → Gateway 订阅 的流式闭环。
|
||||||
|
func TestTokenStreamRoundTrip(t *testing.T) {
|
||||||
|
url := startEmbeddedNATS(t)
|
||||||
|
|
||||||
|
// Gateway 侧:先订阅(core NATS 无持久化,须先连)。
|
||||||
|
gw, err := bus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("gateway connect: %v", err)
|
||||||
|
}
|
||||||
|
defer gw.Close()
|
||||||
|
|
||||||
|
const taskID = "task_stream_001"
|
||||||
|
var got []string
|
||||||
|
done := make(chan struct{})
|
||||||
|
unsub, err := gw.SubscribeTokens(taskID,
|
||||||
|
func(tok []byte) { got = append(got, string(tok)) },
|
||||||
|
func() { close(done) },
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subscribe tokens: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = unsub() }()
|
||||||
|
|
||||||
|
// Dispatcher 侧:逐 Token 回流后发结束信号。
|
||||||
|
dp, err := bus.Connect(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dispatcher connect: %v", err)
|
||||||
|
}
|
||||||
|
defer dp.Close()
|
||||||
|
|
||||||
|
want := []string{"Hello", " ", "Agent", "!"}
|
||||||
|
for _, tok := range want {
|
||||||
|
if err := dp.PublishToken(taskID, []byte(tok)); err != nil {
|
||||||
|
t.Fatalf("publish token: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := dp.CompleteStream(taskID); err != nil {
|
||||||
|
t.Fatalf("complete stream: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
joined := ""
|
||||||
|
for _, s := range got {
|
||||||
|
joined += s
|
||||||
|
}
|
||||||
|
if joined != "Hello Agent!" {
|
||||||
|
t.Fatalf("token stream = %q, want %q", joined, "Hello Agent!")
|
||||||
|
}
|
||||||
|
t.Logf("✓ Token 流闭环:Dispatcher 回流 %d 个 token → Gateway 拼回 %q", len(got), joined)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timeout: 未收到流结束信号")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Command devnats 启动一个内嵌、开启 JetStream 的本地 NATS 服务器,
|
||||||
|
// 用于无 Docker 环境下的本地联调(生产环境用 deploy/nats 的真实集群)。
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats-server/v2/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
storeDir, err := os.MkdirTemp("", "sundynix-jetstream-")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[devnats] tempdir: %v", err)
|
||||||
|
}
|
||||||
|
opts := &server.Options{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 4222,
|
||||||
|
JetStream: true,
|
||||||
|
StoreDir: storeDir,
|
||||||
|
}
|
||||||
|
ns, err := server.NewServer(opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[devnats] new server: %v", err)
|
||||||
|
}
|
||||||
|
go ns.Start()
|
||||||
|
if !ns.ReadyForConnections(5e9) {
|
||||||
|
log.Fatal("[devnats] not ready")
|
||||||
|
}
|
||||||
|
log.Printf("[devnats] JetStream NATS ready on %s (store=%s)", ns.ClientURL(), storeDir)
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sig
|
||||||
|
log.Println("[devnats] shutting down")
|
||||||
|
shutdownQuietly(ns)
|
||||||
|
_ = os.RemoveAll(storeDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdownQuietly 容忍内嵌 server 退出时偶发的 "close of nil channel" panic。
|
||||||
|
func shutdownQuietly(ns *server.Server) {
|
||||||
|
defer func() { _ = recover() }()
|
||||||
|
ns.Shutdown()
|
||||||
|
ns.WaitForShutdown()
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Package contract 是 Gateway / Dispatcher / MCP 之间的共享契约:
|
||||||
|
// Task 数据结构与 NATS subject 命名约定。
|
||||||
|
package contract
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// NATS subject / stream 约定(与 README、各服务 config 保持一致)。
|
||||||
|
const (
|
||||||
|
StreamTasks = "SUNDYNIX_TASKS" // JetStream stream 名
|
||||||
|
SubjectTasks = "sundynix.tasks" // 任务发布主题前缀;实际为 sundynix.tasks.<id>
|
||||||
|
SubjectTasksAll = "sundynix.tasks.>" // stream 捕获的通配
|
||||||
|
SubjectStream = "sundynix.streams" // Token 回流前缀;实际 sundynix.streams.<id>
|
||||||
|
ConsumerDurable = "dispatchers" // Dispatcher 持久消费者(队列组负载均衡)
|
||||||
|
|
||||||
|
// HeaderStreamEnd 是 Token 流的结束信号(core NATS 消息头)。
|
||||||
|
// 置为 "1" 的消息体为空,表示该 task 的 Token 流结束。
|
||||||
|
HeaderStreamEnd = "X-Stream-End"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Task 是 DSL 解析组装后的可调度任务,在 NATS 上以 JSON 传输。
|
||||||
|
type Task struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Graph json.RawMessage `json:"graph"` // React Flow 导出的 Agent 编排图
|
||||||
|
Meta map[string]any `json:"meta,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskSubject 返回某任务的发布主题。
|
||||||
|
func TaskSubject(id string) string { return SubjectTasks + "." + id }
|
||||||
|
|
||||||
|
// StreamSubject 返回某任务的 Token 回流主题。
|
||||||
|
func StreamSubject(id string) string { return SubjectStream + "." + id }
|
||||||
|
|
||||||
|
// Marshal / Unmarshal 便捷方法。
|
||||||
|
func (t *Task) Marshal() ([]byte, error) { return json.Marshal(t) }
|
||||||
|
func Unmarshal(b []byte) (*Task, error) {
|
||||||
|
var t Task
|
||||||
|
if err := json.Unmarshal(b, &t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module github.com/sundynix/sundynix-shared
|
||||||
|
|
||||||
|
go 1.23
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20
|
||||||
|
github.com/nats-io/nats.go v1.37.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/minio/highwayhash v1.0.3 // indirect
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8 // indirect
|
||||||
|
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||||
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
golang.org/x/time v0.6.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
|
||||||
|
github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE=
|
||||||
|
github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20 h1:CXDTYNHeBiAKBTAIP2gjpgbWap2GhATnTLgP8etyvEI=
|
||||||
|
github.com/nats-io/nats-server/v2 v2.10.20/go.mod h1:hgcPnoUtMfxz1qVOvLZGurVypQ+Cg6GXVXjG53iHk+M=
|
||||||
|
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||||
|
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||||
|
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||||
|
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||||
|
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||||
|
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||||
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
|
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||||
|
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
Reference in New Issue
Block a user