Compare commits

...

10 Commits

Author SHA1 Message Date
whlaoding
2ecf4e903a Fix orchestrator logic and workspace push for planning confirmation and artifact handling 2026-03-16 13:17:23 +08:00
whlaoding
9fccb0a473 Update env file with LLM_MODEL qwen3.5-plus 2026-03-16 01:56:40 +08:00
whlaoding
38d6875ab8 Fix truncation issues in piplan, SQLite storage, and history compression; add PIPlanMaxChars configuration 2026-03-15 00:32:14 +08:00
whlaoding
ea88e1dc18 fix(sse): correct UTF-8 handling and chunking in SSE streaming\n\n- Updated splitContentIntoSegments to handle runes instead of bytes\n- Fixed buildWebUIStreamForwarder to send delta chunks instead of cumulative content\n- Ensures proper handling of multi-byte characters in SSE streams 2026-03-14 01:41:51 +08:00
whlaoding
60195f00a0 Add Docker support: Dockerfile, docker-compose.yaml, and README updates 2026-03-13 23:11:46 +08:00
33c357a1de feat: optimize WebUI stream output and sanitize user-facing answers 2026-03-13 13:14:37 +08:00
8dc5354fa4 feat: implement streaming chat, skill routing, and SAFe PI planning tools
- Add /api/chat/stream endpoint with Server-Sent Events (SSE) for real-time message streaming
  * Implement StreamEvent types (thought, tool_call, tool_result, final, error)
  * Add StreamEventCallback mechanism for event propagation
  * Create StreamChatHandler in webui/bot with proper HTTP headers and flushing

- Implement LLM-based skill router for intelligent capability selection
  * Add optional routerLLM client for semantic routing
  * Implement routeSkillsWithLLM() to match user intent to available skills
  * Add matchSkillsByName() for fuzzy skill matching
  * Update buildUnifiedSystemPrompt() to use routed skills

- Add streaming support to ReAct pipeline
  * Implement runUnifiedReActStream() for streaming thought/action/observation
  * Emit StreamEvent at each ReAct step
  * Support callback error handling in streaming mode

- Integrate three new DevOps tools
  * tools/filedoc: Extract document content from file_id via OpenAI
  * tools/giteaticket: Create Gitea issues from PI plan items with SAFe metadata
  * tools/piplan: Publish PI planning blueprints with dependency tracking

- Add SAFe PI Planning skill
  * Implement PM/SA/RTE (iron triangle) workflow
  * Support for Feature, Enabler, and Dependency definition
  * Automatic task decomposition and Gitea integration

- Create frontend integration documentation
  * Complete SSE protocol specification
  * TypeScript fetch + ReadableStream example
  * LLM-ready refactoring template for other projects

- Simplify file handling
  * Remove legacy file context structures and dual-mode processing
  * Consolidate file operations into UploadAndCacheFiles()
  * Remove FilePromptMode configuration and related complexity

- Update configuration
  * Add Router model support (LLM_ROUTER_MODEL)
  * Add Gitea configuration (BaseURL, Token, Owner, Repo)
  * WebSearch and additional tool infrastructure

Tests: All 22 test packages passing, 8/8 webui tests including 3 new stream tests
2026-03-11 17:58:19 +08:00
0e1a800646 Migrate LLM client to OpenAI SDK and implement WebUI-specific fileID handling 2026-03-10 17:54:50 +08:00
49f6297631 feat: add webui http channel for chat and file upload 2026-03-10 10:23:53 +08:00
bd41f48971 feat: support file-aware model switch and update tech docs 2026-03-09 17:38:13 +08:00
46 changed files with 5882 additions and 8554 deletions

3
.vscode/launch.json vendored
View File

@@ -6,7 +6,6 @@
"type": "go",
"request": "launch",
"mode": "auto",
"preLaunchTask": "Kill Stale LaodingBot Debug Processes",
"program": "${workspaceFolder}/cmd/bot",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/configs/env"
@@ -16,7 +15,6 @@
"type": "go",
"request": "launch",
"mode": "auto",
"preLaunchTask": "Kill Stale LaodingBot Debug Processes",
"program": "${workspaceFolder}/cmd/bot",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/configs/env",
@@ -29,7 +27,6 @@
"type": "go",
"request": "launch",
"mode": "auto",
"preLaunchTask": "Kill Stale LaodingBot Debug Processes",
"program": "${workspaceFolder}/cmd/bot",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/configs/env",

11
.vscode/tasks.json vendored
View File

@@ -1,11 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Kill Stale LaodingBot Debug Processes",
"type": "shell",
"command": "pkill -f '/LaodingBot/cmd/bot/__debug_bin|dlv.*LaodingBot/cmd/bot|dlv dap' || true",
"problemMatcher": []
}
]
}

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
FROM golang:1.23-alpine AS builder
WORKDIR /src
# Pull modules first for better layer cache.
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/laodingbot ./cmd/bot
FROM alpine:3.20
WORKDIR /app
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /out/laodingbot /app/laodingbot
# Keep default runtime assets in image; can be overridden by bind mounts.
COPY bot_context /app/bot_context
COPY skills /app/skills
COPY workspace/agent_runtime /app/workspace/agent_runtime
ENV MESSAGE_CHANNEL=webui
ENV WEBUI_LISTEN_ADDR=:8090
ENV AGENT_WORKSPACE_DIR=/app/workspace/agent_runtime
# Host can bind mount these directories for persistent/external runtime data.
VOLUME ["/app/workspace/agent_runtime"]
EXPOSE 8090
CMD ["/app/laodingbot"]

View File

@@ -15,6 +15,7 @@ Now supports mutually exclusive message channels:
- `telegram` (long polling)
- `feishu` (official SDK websocket long connection)
- `webui` (HTTP + SSE, default `:8090`)
## Quick Start
@@ -22,9 +23,11 @@ Now supports mutually exclusive message channels:
- The app auto-loads `configs/env` (or `.env`) if present.
- You can also set `CONFIG_ENV_FILE=/path/to/env`.
- Process environment variables override file values.
2. Choose exactly one channel with `MESSAGE_CHANNEL=telegram|feishu`.
2. Choose exactly one channel with `MESSAGE_CHANNEL=telegram|feishu|webui`.
- If `telegram`: set `TELEGRAM_BOT_TOKEN`, keep `FEISHU_*` empty.
- If `feishu`: set `FEISHU_APP_ID` and `FEISHU_APP_SECRET`, keep `TELEGRAM_BOT_TOKEN` empty.
- If `webui`: set `WEBUI_LISTEN_ADDR` (default `:8090`).
- Optional for `webui`: set `WEBUI_EXPOSE_REASONING=true` to expose `thought/tool_call/tool_result` events to frontend.
3. Set log level with `LOG_LEVEL=debug|info|warn|error`.
- To inspect full skill/tool execution content and detailed ReAct step traces, use `LOG_LEVEL=debug`.
4. Configure knowledge and reasoning:
@@ -65,6 +68,83 @@ go run ./cmd/bot
- Skills directory default path: `skills/`
- Skill format uses subdirectories: `skills/<skill_name>/skill.md`
## Docker Build And Run
### Required Files And Folders
Before building/running with Docker, ensure these exist:
- `Dockerfile`
- `docker-compose.yaml`
- `workspace/agent_runtime/configs/env`
- `workspace/agent_runtime/bot_context/soul.md`
- `workspace/agent_runtime/skills/`
- `workspace/agent_runtime/data/`
- `workspace/agent_runtime/workspace/`
Quick bootstrap command:
```bash
mkdir -p workspace/agent_runtime/{configs,bot_context,skills,data,workspace}
cp -n configs/env.sample workspace/agent_runtime/configs/env
cp -n bot_context/soul.md workspace/agent_runtime/bot_context/soul.md
cp -rn skills/* workspace/agent_runtime/skills/
```
Minimum env config in `workspace/agent_runtime/configs/env`:
```env
MESSAGE_CHANNEL=webui
WEBUI_LISTEN_ADDR=:8090
WEBUI_EXPOSE_REASONING=false
LLM_API_KEY=your_api_key
```
`docker-compose.yaml` already mounts `./workspace/agent_runtime` into the container, so host-side data is persistent.
### Build Docker Image
```bash
docker build -t laodingbot:latest .
```
### Run With Docker (Single Container)
```bash
docker run --rm -d \
--name laodingbot \
-p 8090:8090 \
--env-file ./workspace/agent_runtime/configs/env \
-e MESSAGE_CHANNEL=webui \
-e WEBUI_LISTEN_ADDR=:8090 \
-e WEBUI_EXPOSE_REASONING=false \
-e AGENT_WORKSPACE_DIR=/app/workspace/agent_runtime \
-v "$(pwd)/workspace/agent_runtime:/app/workspace/agent_runtime" \
laodingbot:latest
```
### Run With Docker Compose
Build and start:
```bash
docker compose up -d --build
```
View logs:
```bash
docker compose logs -f laodingbot
```
Stop and remove:
```bash
docker compose down
```
After startup, open WebUI at `http://localhost:8090`.
## Security Notes
- `shell` only allows commands listed in `ALLOWED_COMMANDS`.

BIN
bot Executable file

Binary file not shown.

View File

@@ -19,6 +19,7 @@ import (
"laodingbot/internal/tools"
"laodingbot/internal/transport/feishu"
"laodingbot/internal/transport/telegram"
"laodingbot/internal/transport/webui"
)
// main 是程序的入口点。它负责初始化环境、加载配置、注册工具并启动消息通道。
@@ -125,9 +126,19 @@ func main() {
// 实例化 LLM 客户端
llmClient := llm.NewOpenAICompatibleClient(cfg.LLM, appLogger.WithComponent("llm"))
// 实例化路由 LLM 客户端(如果配置了独立的路由模型)
var routerLLMClient llm.Client
if cfg.LLM.RouterModel != "" {
routerCfg := cfg.LLM
routerCfg.Model = cfg.LLM.RouterModel
routerLLMClient = llm.NewOpenAICompatibleClient(routerCfg, appLogger.WithComponent("llm.router"))
appLogger.Infof("skill router enabled, model=%s", cfg.LLM.RouterModel)
}
// 创建编排器,整合 LLM、记忆系统、知识技能库与各种工具
engine := agent.NewOrchestrator(
llmClient,
routerLLMClient,
store,
toolRegistry,
soul,
@@ -197,7 +208,91 @@ func runMessageChannel(ctx context.Context, cfg config.Config, engine *agent.Orc
}
return engine.HandleMessage(ctx, msg.ChatID, msg.UserID, msg.Text)
})
case "webui":
wb, err := webui.NewBot(cfg.WebUI, lg.WithComponent("transport.webui"))
if err != nil {
return fmt.Errorf("init webui bot failed: %w", err)
}
lg.Infof("starting webui transport listen_addr=%s", cfg.WebUI.ListenAddr)
return wb.Run(
ctx,
func(ctx context.Context, msg webui.IncomingMessage) (string, error) {
return engine.HandleMessage(ctx, msg.ChatID, msg.UserID, msg.Text)
},
func(ctx context.Context, msg webui.IncomingMessage, callback webui.StreamEventCallback) (string, error) {
return engine.HandleMessageStream(ctx, msg.ChatID, msg.UserID, msg.Text, buildWebUIStreamForwarder(callback, cfg.WebUI.ExposeReasoning))
},
func(ctx context.Context, chatID, userID string, files []llm.InputFile) ([]string, error) {
return engine.UploadAndCacheFiles(ctx, chatID, userID, files)
},
func(ctx context.Context, chatID string, limit int) ([]memory.Message, error) {
return engine.GetHistory(chatID, limit)
},
)
default:
return fmt.Errorf("unsupported message channel: %s", cfg.MessageChannel)
}
}
func buildWebUIStreamForwarder(callback webui.StreamEventCallback, exposeReasoning bool) agent.StreamEventCallback {
const finalChunkRunes = 12
const finalChunkInterval = 25 * time.Millisecond
return func(event agent.StreamEvent) error {
if callback == nil {
return nil
}
switch event.Type {
case agent.StreamEventTypeThought, agent.StreamEventTypeToolCall, agent.StreamEventTypeToolResult:
if !exposeReasoning {
return nil
}
return callback(webui.StreamEvent{
Type: webui.StreamEventType(event.Type),
Content: event.Content,
Step: event.Step,
ToolName: event.ToolName,
})
case agent.StreamEventTypeFinal:
runes := []rune(event.Content)
if len(runes) == 0 {
return callback(webui.StreamEvent{Type: webui.StreamEventTypeFinal, Content: "", Step: event.Step})
}
start := 0
for start < len(runes) {
end := start + finalChunkRunes
if end > len(runes) {
end = len(runes)
}
if err := callback(webui.StreamEvent{
Type: webui.StreamEventTypeFinal,
Content: string(runes[start:end]),
Step: event.Step,
}); err != nil {
return err
}
start = end
if start < len(runes) {
time.Sleep(finalChunkInterval)
}
}
return nil
case agent.StreamEventTypeError:
return callback(webui.StreamEvent{
Type: webui.StreamEventTypeError,
Content: event.Content,
Step: event.Step,
})
case agent.StreamEventTypeWorkspaceStart, agent.StreamEventTypeWorkspaceDelta, agent.StreamEventTypeWorkspaceEnd:
return callback(webui.StreamEvent{
Type: webui.StreamEventType(event.Type),
Content: event.Content,
Step: event.Step,
ToolName: event.ToolName,
WorkspaceTitle: event.WorkspaceTitle,
})
default:
return nil
}
}
}

43
cmd/bot/main_test.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"testing"
"laodingbot/internal/agent"
"laodingbot/internal/transport/webui"
)
func TestBuildWebUIStreamForwarderFiltersTraceEvents(t *testing.T) {
var got []webui.StreamEvent
forwarder := buildWebUIStreamForwarder(func(event webui.StreamEvent) error {
got = append(got, event)
return nil
})
events := []agent.StreamEvent{
{Type: agent.StreamEventTypeThought, Content: "thinking", Step: 1},
{Type: agent.StreamEventTypeToolCall, Content: "pwd", Step: 1, ToolName: "shell"},
{Type: agent.StreamEventTypeToolResult, Content: "C:/Project", Step: 1, ToolName: "shell"},
{Type: agent.StreamEventTypeFinal, Content: "done", Step: 2},
{Type: agent.StreamEventTypeError, Content: "boom", Step: 3},
}
for _, event := range events {
if err := forwarder(event); err != nil {
t.Fatalf("forwarder returned error: %v", err)
}
}
if len(got) != 2 {
t.Fatalf("expected 2 forwarded events, got %d", len(got))
}
if got[0].Type != webui.StreamEventTypeFinal || got[0].Content != "done" {
t.Fatalf("unexpected final event: %+v", got[0])
}
if got[0].Step != 0 || got[0].ToolName != "" {
t.Fatalf("expected final event without trace fields, got %+v", got[0])
}
if got[1].Type != webui.StreamEventTypeError || got[1].Content != "boom" {
t.Fatalf("unexpected error event: %+v", got[1])
}
}

View File

@@ -6,6 +6,7 @@ SKILLS_DIR=./skills
REACT_MAX_STEPS=4
TOOL_CALL_TIMEOUT_SEC=15
TOOL_OUTPUT_MAX_CHARS=4000
PI_PLAN_MAX_CHARS=40000
ENABLE_CAPABILITY_GAP=true
AUTO_SKILL_DIR=./skills
GAP_DRAFT_TRIGGER_COUNT=3
@@ -17,10 +18,25 @@ TELEGRAM_POLL_TIMEOUT_SECONDS=30
FEISHU_APP_ID=
FEISHU_APP_SECRET=
FEISHU_VERIFY_TOKEN=
FEISHU_LISTEN_ADDR=:8080
FEISHU_EVENT_PATH=/feishu/events
WEBUI_LISTEN_ADDR=:8090
WEBUI_MAX_UPLOAD_MB=20
WEBUI_EXPOSE_REASONING=false
LLM_BASE_URL=https://api.openai.com/v1
LLM_API_KEY=
LLM_MODEL=gpt-4o-mini
LLM_FILE_MODEL=gpt-4o-mini
LLM_ROUTER_MODEL=
WEB_SEARCH_ENGINE=duckduckgo
WEB_SEARCH_API_KEY=
GITEA_BASE_URL=
GITEA_TOKEN=
GITEA_OWNER=
GITEA_REPO=
SQLITE_PATH=./data/laodingbot.db
ALLOWED_DIRS=./workspace,./data,./skills

34
deploy.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Configuration
REGISTRY_URL="dcr-by1jwyxk44.71826370.xyz"
IMAGE_NAME="laodingbot"
TAG=$(date +%Y%m%d%H%M%S)
FULL_IMAGE_NAME="${REGISTRY_URL}/${IMAGE_NAME}:${TAG}"
LATEST_IMAGE_NAME="${REGISTRY_URL}/${IMAGE_NAME}:latest"
echo "Step 1: Building Docker image..."
docker build -t ${IMAGE_NAME}:latest .
if [ $? -ne 0 ]; then
echo "Error: Docker build failed."
exit 1
fi
echo "Step 2: Tagging image..."
docker tag ${IMAGE_NAME}:latest ${FULL_IMAGE_NAME}
docker tag ${IMAGE_NAME}:latest ${LATEST_IMAGE_NAME}
echo "Step 3: Pushing image to ${REGISTRY_URL}..."
# Note: You might need to run 'docker login ${REGISTRY_URL}' once before running this script
docker push ${FULL_IMAGE_NAME}
docker push ${LATEST_IMAGE_NAME}
if [ $? -ne 0 ]; then
echo "Error: Docker push failed. Make sure you are logged in to the registry."
exit 1
fi
echo "Successfully built and pushed:"
echo " - ${FULL_IMAGE_NAME}"
echo " - ${LATEST_IMAGE_NAME}"

View File

@@ -0,0 +1,140 @@
# Vibe Coding Design Docs: Workspace/Artifact Split-Screen Pattern
## 1. Context & Objective
The goal is to implement a UI/UX pattern similar to **Claude Artifacts** or **Gemini Deep Research**. When a specific complex task is triggered (e.g., "Project Planning Skill"), the single-column chat interface should smoothly transition into a split-screen layout:
- **Left Panel (35%)**: Conversational context, CoT (Chain of Thought) traces, tool calls, and user input.
- **Right Panel (65%)**: A dedicated "Workspace" or "Artifact" rendering area to display long-form content (Markdown, code, diagrams) generated by the Agent's skills.
Crucially, this system must support **Reflexion/Iterative generation**. The user can comment on the generated artifact in the left panel, and the agent should update the artifact in the right panel based on the feedback.
---
## 2. Frontend Implementation Guide (React + Vite + Tailwind)
### 2.1 State Management (State & Types)
Extend the existing frontend state to track the workspace status and content.
```typescript
// 1. Extend the StreamEvent type to support UI control and artifact streaming
type StreamEvent = {
type:
| "thought"
| "tool_call"
| "tool_result"
| "message" // Standard chat message
| "error"
| "workspace_start" // Trigger right panel open
| "workspace_delta" // Streaming text for the right panel
| "workspace_end"; // Streaming completed
content: string;
step?: number;
tool_name?: string;
workspace_title?: string; // Optional title for the artifact
};
// 2. Add Workspace State (Can be added to useReducer or a separate useState)
type WorkspaceState = {
isOpen: boolean;
title: string;
content: string;
isGenerating: boolean;
};
```
### 2.2 SSE Parsing Logic
Modify the `onEvent` handler inside `streamChat` to intercept `workspace_*` events.
- When `workspace_start` arrives: Set `workspace.isOpen = true`, clear previous content, set `isGenerating = true`.
- When `workspace_delta` arrives: Append text to `workspace.content`. Do **not** append this text to the left-panel chat history to avoid redundancy.
- When `workspace_end` arrives: Set `isGenerating = false`.
### 2.3 Layout & UI Re-architecture
Refactor the root `<div>` of `PlanningAgent.tsx` to handle dynamic flex layouts. Use Tailwind's transition utilities for smooth scaling.
```tsx
<div className="flex h-full w-full overflow-hidden bg-surface">
{/* Left Panel: Chat & Controls */}
<div
className={`flex flex-col h-full transition-all duration-300 ease-in-out ${
workspace.isOpen ? 'w-[35%] border-r border-border min-w-[350px]' : 'w-full max-w-5xl mx-auto'
}`}
>
{/* Existing Message List & Input Area */}
</div>
{/* Right Panel: Workspace / Deep Research Output */}
{workspace.isOpen && (
<div className="w-[65%] h-full flex flex-col bg-surface-muted transition-opacity duration-300 animate-fade-in">
{/* Header */}
<div className="h-14 border-b border-border flex items-center px-6 justify-between bg-white">
<h3 className="font-semibold text-txt flex items-center gap-2">
<IconDocument /> {workspace.title || 'Project Planning Document'}
</h3>
{workspace.isGenerating && (
<span className="text-sm text-magenta animate-pulse flex items-center gap-1">
<Spinner /> Generating...
</span>
)}
</div>
{/* Markdown Content Area */}
<div className="flex-1 overflow-y-auto p-8 prose prose-slate max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{workspace.content}
</ReactMarkdown>
</div>
</div>
)}
</div>
```
---
## 3. Backend Implementation Guide (Agent / ReAct Loop)
The backend agent requires structural changes to understand the "Artifact" concept, emit correct SSE events, and maintain the artifact in its memory for iterative edits.
### 3.1 Tool / Skill Definition
When defining the `Project Planning Skill` for the LLM, clearly state its output behavior so the LLM knows *when* to use it.
- **Tool Description**: `use_planning_workspace`: "Invoke this tool to generate, structure, or update a major project planning document. The output will be rendered in a dedicated UI workspace."
### 3.2 Context Injection (Memory for Reflexion)
To allow the user to say *"extend the testing phase to 2 weeks"*, the LLM **must know what is currently in the right panel**.
- **Before sending the prompt to the LLM**, query the database/session for the current Artifact state.
- **Prompt Assembly**:
```text
[System Prompt / ReAct Instructions]
...
[Current Workspace Artifact (if exists)]
<workspace>
# Project Plan
1. Dev Phase: 1 week
2. Testing Phase: 1 week
</workspace>
[Chat History]
User: extend the testing phase to 2 weeks.
```
### 3.3 Streaming Control (Hijacking the Stream)
Within the ReAct execution loop, when the Agent decides to execute the `Project Planning Skill`:
1. The Backend normally streams `thought` or `tool_call` events.
2. Upon entering the specific Skill execution, the backend emits `{"type": "workspace_start", "workspace_title": "Update: Project Plan"}`.
3. As the LLM (or a sub-agent) generates the Markdown schema, the backend maps these tokens to `workspace_delta` events and flushes them to the frontend.
4. (CRITICAL) Do **not** send these tokens as `message` or `final` chat events. The chat bubble should only say something like: *"I have updated the project plan in the workspace area."*
5. Save the final generated Markdown text into the session memory as the `Current Artifact` for future context injection.
---
## 4. Work Flow Summary (For LLM context generation)
1. `User` sends prompt: "Plan the new feature".
2. `Agent` thinks (`type: thought`), decides to use `Project Planning Skill` (`type: tool_call`).
3. `Agent` emits `{"type": "workspace_start"}`.
4. `Frontend` expands right panel (65% width).
5. `Agent` streams `{"type": "workspace_delta", "content": "..."}`.
6. `Frontend` live-renders Markdown in the right panel.
7. `Agent` finishes, saves artifact to backend session.
8. `User` reads right panel, types in left panel: "Change point 2".
9. `Agent` receives Left Panel history + Right Panel Artifact Content.
10. `Agent` updates document, streaming new `workspace_delta`. Frontend live-updates the right panel.

View File

@@ -0,0 +1,197 @@
# WebUI `/api/chat/stream` 前端对接说明
本文档用于指导前端项目接入 LaodingBot 的流式聊天接口,并可直接作为提示词输入给 LLM批量改造其他前端代码。
## 1. 接口总览
- 方法: `POST`
- 路径: `/api/chat/stream`
- 请求头: `Content-Type: application/json`
- 响应类型: `text/event-stream`
- 协议: SSE (Server-Sent Events)
说明: 该接口为单次请求、多次推送。后端会持续推送 `data: <json>\n\n` 格式的事件。
## 2. 请求体
```json
{
"text": "请帮我分析当前目录",
"session_id": "sess_abc",
"user_id": "user_001"
}
```
字段说明:
- `text` (string, required): 用户输入文本,去除空白后不能为空。
- `session_id` (string, optional): 会话 ID不传时后端自动生成。
- `user_id` (string, optional): 用户 ID不传时后端自动生成。
兼容字段:
- `sessionId` 等价于 `session_id`
- `userId` 等价于 `user_id`
## 3. SSE 事件格式
每条 SSE 消息只包含 `data` 字段,内容是 JSON:
```text
data: {"type":"thought","content":"我先判断是否需要调用工具","step":1}
data: {"type":"tool_call","content":"{\"input\":\"pwd\"}","step":1,"tool_name":"shell"}
data: {"type":"tool_result","content":"C:/Project/MyProject","step":1,"tool_name":"shell"}
data: {"type":"final","content":"当前目录是 C:/Project/MyProject","step":2}
```
事件字段:
- `type` (string): 事件类型
- `content` (string): 事件文本内容
- `step` (number, optional): ReAct 步骤编号
- `tool_name` (string, optional): 工具名
事件类型:
- `thought`: LLM 思考片段(可选透传)
- `tool_call`: 工具调用请求(可选透传)
- `tool_result`: 工具执行结果(可选透传)
- `final`: 最终回答
- `error`: 错误信息
说明:
- 默认情况下(`WEBUI_EXPOSE_REASONING=false`WebUI 只向前端返回 `final``error`
- 当设置 `WEBUI_EXPOSE_REASONING=true`WebUI 会额外透传 `thought``tool_call``tool_result` 事件。
- 无论是否透传推理事件,`final` 都会分段累计推送,以便前端实现打字机效果。
## 4. 连接生命周期
- 正常结束: 收到 `type=final` 后结束渲染,连接可由浏览器自然关闭。
- 异常结束: 收到 `type=error`,前端应显示错误并结束当前轮次。
- 网络中断: 前端应允许用户重试,并保留已收到的事件记录。
## 5. 前端渲染建议
推荐前端仅处理两类结果:
- 答案区: 显示最后一个 `final`
- 错误区: 显示 `error`
如果开启了 `WEBUI_EXPOSE_REASONING=true`,建议额外提供“思考面板”:
- 思考区: 渲染 `thought`
- 工具区: 渲染 `tool_call` / `tool_result`
建议状态机:
- `idle`: 初始状态
- `streaming`: 请求中且持续接收事件
- `done`: 收到 `final`
- `failed`: 收到 `error` 或请求异常
## 6. TypeScript 对接示例 (fetch + ReadableStream)
```ts
type StreamEventType = 'thought' | 'tool_call' | 'tool_result' | 'final' | 'error';
interface StreamEvent {
type: StreamEventType;
content: string;
step?: number;
tool_name?: string;
}
export async function streamChat(
payload: { text: string; session_id?: string; user_id?: string },
onEvent: (event: StreamEvent) => void,
signal?: AbortSignal,
): Promise<void> {
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal,
});
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}`);
}
if (!resp.body) {
throw new Error('ReadableStream is not available');
}
const reader = resp.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE message delimiter: blank line
let idx = buffer.indexOf('\n\n');
while (idx >= 0) {
const chunk = buffer.slice(0, idx).trim();
buffer = buffer.slice(idx + 2);
for (const line of chunk.split('\n')) {
const text = line.trim();
if (!text.startsWith('data:')) continue;
const raw = text.slice(5).trim();
if (!raw) continue;
const event = JSON.parse(raw) as StreamEvent;
onEvent(event);
}
idx = buffer.indexOf('\n\n');
}
}
}
```
## 7. 给 LLM 的改造任务提示词模板
将下面模板发给 LLM可用于自动改造其他前端项目:
```text
你要改造一个前端项目的聊天页面,把非流式接口 `/api/chat` 改为流式接口 `/api/chat/stream`。
后端协议约束:
1) 请求方法 POSTContent-Type=application/json
2) 请求体: { text, session_id?, user_id? }
3) 响应是 SSE 文本流,事件格式为 `data: <json>\n\n`
4) JSON 结构:
- type: thought | tool_call | tool_result | final | error
- content: string
- step?: number
- tool_name?: string
5) 收到 final 视为本轮完成;收到 error 视为失败
6) 默认不要假设前端一定会收到 thought/tool_call/tool_result仅当 `WEBUI_EXPOSE_REASONING=true` 才会透传
你的改造要求:
1) 保留现有 UI 风格和组件结构,不做无关重构
2) 新增流式读取逻辑,支持中途取消 (AbortController)
3) 将事件按 step 渲染到消息区
4) 兼容旧会话字段命名 (session_id / sessionId, user_id / userId)
5) 增加错误态与重试按钮
6) 不破坏原有上传、历史消息和输入框行为
7) 输出改动文件列表 + 每个文件的关键变更说明
请直接给出可运行代码补丁。
```
## 8. 调试清单
- 检查响应头是否为 `text/event-stream`
- 检查每条事件是否以 `data:` 开头并以空行结尾
- 确认 `final``error` 都能正确结束当前轮次
- 验证弱网下不会丢失已收到的事件
- 验证用户快速连续提问时,旧流可被取消

View File

@@ -0,0 +1,305 @@
# LaodingBot WebUI 开发技术与需求说明
> 版本: v1.0
> 目标读者: 下一个执行 WebUI 开发任务的 LLM / 前端工程师
> 依据: 当前后端已落地代码(`internal/transport/webui/bot.go`、`cmd/bot/main.go`、`internal/agent/orchestrator.go`
---
## 1. 文档目标与范围
本说明用于指导“仅 WebUI 页面层/前端层”的开发,确保严格对接当前 LaodingBot 已开放的 WebUI 后端能力。
本期范围:
- 支持纯文本对话
- 支持独立文档上传(上传后立即返回 `file_id`
- 支持“先上传文档,再发送文本提问”的两阶段交互
非本期范围:
- 不改动后端接口语义
- 不引入新的后端路由
- 不实现复杂权限系统(当前后端无鉴权)
- 不实现流式输出(当前为一次性响应)
---
## 2. 当前后端能力总览
### 2.1 消息通道
后端已支持 `MESSAGE_CHANNEL=webui`,入口在 `cmd/bot/main.go`
当通道为 `webui` 时:
- 启动 HTTP 服务
- 暴露两个接口:
- `POST /api/upload`
- `POST /api/chat`
### 2.2 运行配置
关键环境变量(`configs/env.sample`:
- `MESSAGE_CHANNEL=webui`
- `WEBUI_LISTEN_ADDR=:8090`
- `WEBUI_MAX_UPLOAD_MB=20`
- `LLM_*` 相关配置(含文件模型切换)
### 2.3 文件上下文机制(非常关键)
当前后端使用 `chat_id + user_id` 作为会话键缓存 `file_id`:
- 上传接口会把文件上传到 LLM并把 `file_id` 缓存到该键
- 后续 `chat` 接口只要带同一组 `session_id`/`user_id`,就会自动消费缓存文件上下文
结论:
- 前端必须稳定维护并复用 `session_id``user_id`
- 两者变化会导致上传文件上下文丢失(从后端视角是新会话)
---
## 3. 后端 API 契约(以代码为准)
## 3.1 `POST /api/upload`
用途:
- 独立上传文档
- 后端立即调用 LLM 文件上传并返回 `file_id`
- 同时缓存到当前会话,供下一次聊天使用
请求:
- Method: `POST`
- Content-Type: `multipart/form-data`
- Form 字段:
- `file` (required)
- `session_id` (optional建议前端始终传)
- `user_id` (optional建议前端始终传)
响应 `200`:
```json
{
"file_id": "file-xxx",
"file_ids": ["file-xxx"],
"file_name": "report.pdf",
"mime_type": "application/pdf",
"size_bytes": 12345,
"session_id": "sess_xxx",
"user_id": "user_xxx"
}
```
错误响应:
- `400`:
- `{"error":"file is required"}`
- `{"error":"invalid multipart form"}`
- `{"error":"invalid file name"}`
- `{"error":"empty file"}`
- `{"error":"read file failed"}`
- `413`:
- `{"error":"file too large"}`
- `500`:
- `{"error":"upload failed"}`
- `{"error":"upload succeeded but file_id is empty"}`
---
## 3.2 `POST /api/chat`
用途:
- 发送纯文本消息
- 若该 `session_id + user_id` 下已有上传文件,会自动携带文件上下文进行问答
请求:
- Method: `POST`
- Content-Type: `application/json`
- Body:
```json
{
"text": "请总结我上传的文档",
"session_id": "sess_xxx",
"user_id": "user_xxx"
}
```
响应 `200`:
```json
{
"reply": "...",
"session_id": "sess_xxx",
"user_id": "user_xxx"
}
```
错误响应:
- `400`:
- `{"error":"content-type must be application/json"}`
- `{"error":"invalid json body"}`
- `{"error":"text is required"}`
- `405`:
- `{"error":"method not allowed"}`
- `500`:
- `{"error":"chat failed"}`
---
## 4. WebUI 功能需求(给前端实现)
### 4.1 页面最小能力
必须实现:
1. 聊天消息列表
2. 文本输入框 + 发送按钮
3. 文件选择器 + 上传按钮
4. 当前会话标识展示(`session_id``user_id`
5. 上传结果区(展示 `file_id`、文件名、大小、时间)
6. 错误提示区(接口失败、文件超限、网络错误)
### 4.2 会话策略
必须:
- 首次进入页面时生成并持久化 `session_id``user_id`(例如 LocalStorage
- 每次请求都携带这两个字段
- 提供“重置会话”操作(清空本地 session/user 并重新生成)
建议 ID 规则:
- `session_id`: `sess_<uuid>`
- `user_id`: `user_<uuid>`
### 4.3 文件上传交互
必须:
- 文件选择后显示基本信息(名称、大小、类型)
- 点击上传后调用 `/api/upload`
- 上传成功后将返回的 `file_id` 加入“已上传文件列表”
- 上传失败时给出明确错误提示
建议:
- 前端先做大小校验(默认 20MB可配置
- 上传中禁用重复点击
### 4.4 聊天交互
必须:
- 发送前校验 `text` 非空
- 调用 `/api/chat`
- 成功后追加机器人消息
- 失败后展示错误并保留用户输入
建议:
- 支持回车发送Shift+Enter 换行)
- 增加“请求中”状态,避免并发多发
---
## 5. 前端状态模型(建议)
```ts
type UploadedFile = {
fileId: string;
fileName: string;
mimeType: string;
sizeBytes: number;
uploadedAt: number;
};
type ChatMessage = {
id: string;
role: "user" | "assistant" | "system";
content: string;
createdAt: number;
status?: "sending" | "sent" | "failed";
};
type AppState = {
sessionId: string;
userId: string;
messages: ChatMessage[];
uploadedFiles: UploadedFile[];
uploading: boolean;
chatting: boolean;
lastError?: string;
};
```
状态约束:
- `uploading=true` 时不应再次触发上传
- `chatting=true` 时可选禁用发送(或允许排队)
- 任何失败都要更新 `lastError`
---
## 6. 接口调用示例
### 6.1 上传
```bash
curl -X POST http://localhost:8090/api/upload \
-F "session_id=sess_demo" \
-F "user_id=user_demo" \
-F "file=@./demo.pdf"
```
### 6.2 聊天
```bash
curl -X POST http://localhost:8090/api/chat \
-H "Content-Type: application/json" \
-d '{"text":"这份文档主要讲了什么?","session_id":"sess_demo","user_id":"user_demo"}'
```
---
## 7. 开发任务拆解(给下一个 LLM
1. 初始化 WebUI 工程(技术栈自选,建议 React + TypeScript
2. 实现全局状态管理(至少管理 session/user/messages/uploads
3. 实现 API Client 层:
- `uploadFile(formData)`
- `sendChat(payload)`
- 统一错误解析
4. 实现页面组件:
- `ChatPanel`
- `Composer`
- `UploadPanel`
- `SessionBadge`
5. 实现本地持久化:
- 启动时恢复 `session_id``user_id`
- 重置会话功能
6. 完成联调与自测。
---
## 8. 验收标准
必须全部满足:
1. 纯文本聊天可用(无文件也能正常回复)。
2. 文档上传可用,返回并展示 `file_id`
3. 上传后同会话提问能基于文档回答。
4. 刷新页面后会话 ID 不丢失(可继续提问)。
5. 文件超限、空消息、网络失败均有可见错误反馈。
6. 同一会话多文件上传后,后续问答仍可使用文件上下文。
---
## 9. 已知限制与后续建议
当前后端限制(前端需知晓):
- 无鉴权机制
- 无流式输出
- 上传 MIME 未做白名单限制
- 无会话文件列表接口(前端只能依赖本地记录)
后续建议(非本期必做):
1. 新增后端 `GET /api/session/files` 以便页面恢复历史上传列表。
2. 增加鉴权中间件Bearer Token/JWT
3. 增加流式聊天接口SSE/WebSocket
4. 增加上传文件类型白名单和病毒扫描。
---
## 10. 对下一个 LLM 的执行指令模板
可直接复制给下一个 LLM:
```text
请基于 `doc/WebUI开发技术与需求说明.md` 实现 WebUI 前端,严格对接现有后端接口:
- POST /api/upload
- POST /api/chat
必须满足:
1) 纯文本聊天
2) 独立文档上传
3) 上传后同会话提问可利用文档上下文
4) session_id/user_id 持久化
5) 完整错误处理和可视化反馈
不允许修改后端接口语义;若需新增接口,请先输出变更提案,不直接改。
```

View File

@@ -1,73 +1,80 @@
# LaodingBot 技术说明文档2026-02-28 最新实现
# LaodingBot 技术说明文档2026-03-09
> 本文档基于当前代码状态,描述真实可运行架构能力边界。
> 本文档基于当前代码状态(含本次“文档问答 + 模型切换”改造)整理,描述真实可运行架构能力边界与配置方式
---
## 1. 项目定位
LaodingBot 当前已从“单进程工具调用 MVP”演进为:
- **父进程 Agent 编排**(技能路由 + ReAct + 记忆)
- **子进程 ToolHost 执行**JSON-RPC
- **workspace 隔离运行空间**(配置与工具权限收敛)
- **能力缺口闭环**(落库、聚类、自动生成技能热加载)
LaodingBot 当前架构为:
- 父进程 Agent 编排(技能路由 + 统一 ReAct + 记忆)
- 子进程 ToolHost 执行JSON-RPC
- runtime workspace 隔离(配置、数据、技能、工具权限收敛)
- 能力缺口闭环(落库、聚类、自动生成技能热加载)
- 文档问答链路(飞书文件下载 -> 上传 LLM -> 缓存 file_id -> 下一轮文本问答使用)
核心目标:让 Agent 在安全边界内持续补全能力,而不是仅做静态问答
核心目标:在安全边界内持续扩展能力,并兼容文本问答与文档长上下文问答的混合场景
---
## 2. 目录与模块
## 2. 关键模块
- `cmd/bot/main.go`应用入口、workspace 引导、toolhost 启动、通道分发
- `internal/config/config.go`配置加载、workspace 路径解析、安全策略归一化
- `internal/runtimews/bootstrap.go`运行时 workspace 准备与种子目录复制
- `internal/agent/orchestrator.go`:主编排器(技能匹配、ReAct、能力缺口闭环
- `internal/toolhost/*`:工具子进程协议、服务端、客户端、远程工具适配
- `internal/tools/filetool/filetool.go`:文件工具(`read/list/write`
- `internal/tools/shelltool/shelltool.go`:命令工具(白名单 + 超时 + 输出限制)
- `internal/memory/store_sqlite.go`:消息与能力缺口存储、聚类查询
- `internal/knowledge/loader.go`skill/soul 加载
- `internal/knowledge/drafts.go`:能力缺口驱动的 skill 自动生成
- `cmd/bot/main.go`应用入口、workspace 引导、toolhost 启动、消息通道分发
- `internal/config/config.go`配置加载、workspace 解析、安全策略归一化、LLM 模型策略
- `internal/runtimews/bootstrap.go`runtime workspace 初始化与种子复制
- `internal/agent/orchestrator.go`:主编排(路由、ReAct、文件上下文、能力缺口)
- `internal/llm/client.go`OpenAI 兼容客户端(聊天、文件上传、文件注入模式)
- `internal/transport/feishu/bot.go`:飞书事件接入、文件下载与本地落盘
- `internal/toolhost/*`:工具子进程协议、客户端/服务端、远程工具适配
- `internal/memory/store_sqlite.go`:消息与能力缺口存储
- `internal/knowledge/*`soul/skills 加载与技能草稿生成
---
## 3. 启动链路(当前)
## 3. 启动链路
`main()` 执行顺序:
1. 建立可取消上下文(SIGINT/SIGTERM
1. 创建 SIGINT/SIGTERM 可取消上下文
2. 调用 `runtimews.PrepareFromEnv()`
- 解析 `AGENT_WORKSPACE_DIR`(默认 `./workspace/agent_runtime`
- `configs/data/skills/bot_context` 种子复制到 runtime workspace缺失才复制
- 设定 `CONFIG_ENV_FILE=<workspace>/configs/env`
3. 调用 `config.Load()`,优先读取 workspace env。
4. `--toolhost` 模式进入子进程服务。
5. 正常父进程初始化日志、SQLite、ToolHost Client、知识、Orchestrator。
6. 根据 `MESSAGE_CHANNEL` 启动 Telegram 或 Feishu transport
- 复制 `configs/data/skills/bot_context` 种子到 runtime workspace缺失才复制
- 注入 `CONFIG_ENV_FILE=<workspace>/configs/env`
3. 调用 `config.Load()`优先读取 env。
4. `--toolhost` 模式进入子进程工具服务。
5. 父进程初始化日志、SQLite、ToolHost Client、知识、Orchestrator。
6. `MESSAGE_CHANNEL` 启动 Telegram 或 Feishu。
---
## 4. 配置加载与优先级关键变更)
## 4. 配置优先级关键配置
`config.Load()` 的 env 读取优先级:
`config.Load()` 的 env 优先级:
1. `CONFIG_ENV_FILE`(强覆盖)
2. `<workspace>/configs/env``<workspace>/.env`(强覆盖)
3. 根目录 `configs/env``.env`(仅兜底,不覆盖已有值
2. `<workspace>/configs/env``<workspace>/.env`(强覆盖)
3. 根目录 `configs/env``.env`(仅兜底)
这保证 VS Code Debug 场景下,**workspace 配置优先于根目录配置**。
### 关键配置
- `REACT_MAX_STEPS`:必须来自 env无代码默认值
- `AGENT_WORKSPACE_DIR`agent 运行空间根目录
关键配置:
- `REACT_MAX_STEPS`必须配置1~8
- `AGENT_WORKSPACE_DIR`:运行空间根目录
- `ALLOWED_DIRS` / `ALLOWED_COMMANDS` / `WORK_DIR`:工具安全边界
- `AUTO_SKILL_DIR`:自动生成 skill 的目标目录(默认 workspace/skills
- `GAP_DRAFT_TRIGGER_COUNT` / `GAP_CLUSTER_LOOKBACK_HOURS`:缺口聚类触发参数
- `AUTO_SKILL_DIR`:自动生成 skill 目录
- `GAP_DRAFT_TRIGGER_COUNT` / `GAP_CLUSTER_LOOKBACK_HOURS`:缺口聚类参数
### 新增(文档问答)
- `LLM_MODEL`:常规文本问答模型(路由 + ReAct 默认模型)
- `LLM_FILE_MODEL`:携带 `file_id` 时使用的模型(未设置时回退 `LLM_MODEL`
- `LLM_FILE_PROMPT_MODE`:文件注入模式
- `user_content_file_parts`(默认):`messages.user.content=[{type:text},{type:file,file_id}]`
- `system_fileid_uri`:按 provider 要求注入 `system: fileid://<id>`
说明:`LLM_FILE_PROMPT_MODE=system_fileid_uri` 可对齐 Qwen/DashScope 兼容模式中常见的 `fileid://` 用法。
---
## 5. workspace 隔离策略
当前实现中Agent 与工具默认都在 workspace 内高权限运行:
- 相对路径统一`AGENT_WORKSPACE_DIR` 解析
当前实现中Agent 与工具默认都在 workspace 内运行:
- 相对路径按 `AGENT_WORKSPACE_DIR` 解析
- `ALLOWED_DIRS` 强制补齐:
- workspace 根
- `workspace/skills`
@@ -75,122 +82,147 @@ LaodingBot 当前已从“单进程工具调用 MVP”演进为
- `workspace/workspace`
- `ALLOWED_COMMANDS` 自动补齐:`go``curl``curl.exe`
`filetool` 对相对路径优先 workspace 根解析,避免写到代码仓库根目录
`filetool` 对相对路径优先解析到 workspace 根,避免写到仓库根。
---
## 6. ToolHost 子进程架构
当前工具调用已迁移到 JSON-RPC 子进程:
- 协议方法`ping``tool.list``tool.call`
- 父进程 `Client` 能力:
工具调用通过 JSON-RPC 子进程完成
- 协议:`ping``tool.list``tool.call`
- 父进程客户端能力:
- 调用超时
- 心跳检测
- 失败重启与重试
- 并发限制(信号量)
- 子进程 stdout 仅承载协议数据(避免日志污染 RPC
- 子进程 stdout 仅输出协议内容,避免日志污染
果:工具崩溃不会直接拖垮 Agent 主编排逻辑
果:工具崩溃不会直接拖垮 Agent 主编排。
---
## 7. ReAct 与技能路由
## 7. 文本问答主流程(统一 ReAct
`Orchestrator` 流程:
1. 保存用户消息到 SQLite
2. 读取最近对话并压缩
3. LLM 进行技能路由(最多命中 2 个
4. 若无技能命中:尝试回退到 `创建skill` 技能
5. 进入 ReAct 多轮决策(`action/final`
6. 工具调用观察写入 scratchpad
`Orchestrator.HandleMessage*()` 流程:
1. 保存用户消息
2. 加载最近消息并压缩
3. 执行能力路由Router
4. 进入统一 ReAct 循环
5. 按决策调用工具并将 Observation 写入 scratchpad
6. 直到 `is_final_answer=true`
7. 保存 assistant 回复
工具错误会结构化为:
- `ERROR_CODE=...; TOOL=...; REASON=...`
注:当前循环有固定安全上限 20 步(代码内硬上限)。
---
## 8. 能力缺口闭环(已落地
## 8. 文档问答链路(飞书
当出现“不会做”信号(如无 skill、解析失败、工具失败
1. 写入 `capability_gaps`
2. 进行意图归一化聚类(按 `intent_key + reason`
3. 高频达到阈值后自动生成 skill 文件
4. 自动调用 `ReloadSkills()` 热加载
### 8.1 接收与下载
`internal/transport/feishu/bot.go``msg_type=file` 时:
1. 从事件中解析 `file_key``file_name`
2. 调用飞书 `message resource` 下载二进制
3. 校验大小(默认上限 20MB
4. 保存到本地 `files/` 目录
5. 组装 `IncomingMessage{FileBytes, FileMime, FilePath}` 交给主流程
可通过消息命令查看与控制:
- `/capability_gaps`:输出当前高频缺口清单
- `/reload_skills`:手动热加载 skills
### 8.2 上传与缓存 file_id
`cmd/bot/main.go` 在 Feishu 文件消息分支:
- 调用 `engine.HandleMessageWithFiles(..., text="", files=[...])`
- Orchestrator 识别为 `isFileOnly`
- 上传文件到 LLM`UploadFile(..., purpose=file-extract)`
- 缓存 `pendingFiles[chat_id::user_id]`
- 回复“文件上传完成,等待下一次提问”
### 8.3 下一轮文本提问使用 file_id
当用户随后发送文本:
- Orchestrator 取出 `pendingFiles`
- 构建 `fileCtx.FileIDs`
- 在 ReAct 内通过 `generateWithOptionalFiles()` 调用 LLM
- 回答成功后清空该用户待消费文件缓存
该行为与需求一致:文件上传与提问分离,且 `file_id` 在下一轮问答自动注入。
---
## 9. 自动生成 skill 的当前行为
## 9. LLM 文件能力实现细节
自动生成由 `internal/knowledge/drafts.go` 执行
- 目标目录:`AUTO_SKILL_DIR`(默认 workspace/skills
- 命名:`auto_<intent_key>/skill.md`
- 仅在文件不存在时创建,避免重复覆盖
- 模板内包含:触发背景、执行流程、工具建议、测试建议
`internal/llm/client.go` 当前能力
并额外提供基础引导技能:
- `skills/skill_builder/skill.md`
### 9.1 文件上传
- 接口:`POST /files`
- multipart 字段:`purpose` + `file`
- 目的值尝试顺序:调用方指定 -> `file-extract` -> `batch`
- 兼容返回:`id``data.id`
### 9.2 模型切换策略
- 无文件:使用 `LLM_MODEL`
- 有文件:优先使用 `LLM_FILE_MODEL`
### 9.3 文件注入策略
- `user_content_file_parts``user.content` 使用 text/file parts
- `system_fileid_uri`:将每个 `file_id` 注入为一条 `system: fileid://<id>` 消息
这使同一套 ReAct 编排可适配不同 provider 的文件上下文协议差异。
---
## 10. file/shell 工具现状
## 10. 为什么不会破坏 ReAct / tools / skills
### file tool
支持:
- `read <path>`
- `list <path>`
- `write <path>\n<content>`
本次改造仅发生在 LLM I/O 层,不改变编排核心:
- ReAct 决策协议JSON 输出格式)不变
- ToolHost、工具注册与调用链路不变
- 技能加载、路由与能力缺口闭环不变
- 仅在 `GenerateWithFiles()` 分支切换模型与消息格式
特性:
- 白名单路径检查
- 目录误读防护:`read` 目录返回 `PATH_IS_DIRECTORY`
- 输出长度限制
### shell tool
特性:
- 命令白名单(首 token
- 超时中断
- 固定工作目录
- 输出截断
- Windows 不可执行命令友好报错
因此:文本问答仍走原有路径,文档问答只在“带 file_id 的 LLM 调用”处差异化。
---
## 11. 数据存储
SQLite 表:
1. `messages`:对话消息
1. `messages`用户与 assistant 对话
2. `capability_gaps`:能力缺口事件
提供查询:
- 最近消息
- 最近缺口事件
- 高频缺口聚类(含计数与最近出现时间)
支持查询:最近消息、最近缺口、高频缺口聚类。
---
## 12. 与最初文档相比的变化
## 12. 与文档相比的更新点
当前代码已经完成并替代旧文档中的以下“待实现项”
- ToolHost 子进程隔离(已实现)
- 能力缺口闭环(已实现)
- 自动 skill 生成与热加载(已实现)
- workspace 配置优先与运行空间隔离(已实现)
仍属于持续演进项:
- 新工具代码自动注册与生效的全自动化流水线
- 更细粒度权限域(按 skill/tool 分级)
- 更强的自动化验收e2e + 故障注入)
已补充并对齐代码现状
- 飞书文件事件下载与本地保存流程
- 文件上传到 LLM 并缓存 `file_id` 的两阶段问答流程
- 双模型配置:`LLM_MODEL` + `LLM_FILE_MODEL`
- 文件注入模式:`LLM_FILE_PROMPT_MODE`
- “不影响 ReAct/tools/skills”的边界说明
---
## 13. 下一步建议
## 13. 推荐配置Qwen Long 场景)
1.`toolhost client/server` 增加专项故障单测(心跳失败、子进程崩溃、并发压力)。
2. 增加“自动生成 tool 后自动接线注册”的流水线模块。
3. 为 skill 自动生成增加结构门禁frontmatter/章节完整性校验)。
4. 引入操作审计视图,串联 trace_id 与 capability_gap。
示例:
```env
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_API_KEY=<your_key>
LLM_MODEL=qwen-plus
LLM_FILE_MODEL=qwen-long
LLM_FILE_PROMPT_MODE=system_fileid_uri
```
解释:
- 普通对话用 `qwen-plus`
- 文档问答自动切到 `qwen-long`
- 文件上下文注入采用 `fileid://` 形式
---
## 14. 后续建议
1.`internal/llm/client.go` 增加表驱动单测,覆盖两种 `LLM_FILE_PROMPT_MODE`
2. 在能力路由中加入“文档问答意图”标记,优化带文件时的提示词压缩策略。
3. 为 pending file_id 增加 TTL 清理,避免长期未消费堆积。
4. 增加 e2e 用例:飞书文件消息 -> 下一轮提问 -> 产出稳定答案。

18
docker-compose.yaml Normal file
View File

@@ -0,0 +1,18 @@
services:
laodingbot:
build:
context: .
dockerfile: Dockerfile
image: laodingbot:latest
container_name: laodingbot
restart: unless-stopped
ports:
- "8090:8090"
env_file:
- ./workspace/agent_runtime/configs/env
environment:
MESSAGE_CHANNEL: webui
WEBUI_LISTEN_ADDR: ":8090"
AGENT_WORKSPACE_DIR: /app/workspace/agent_runtime
volumes:
- ./workspace/agent_runtime:/app/workspace/agent_runtime

File diff suppressed because it is too large Load Diff

View File

@@ -1,250 +0,0 @@
Sub-Goal 5: Users experience is
enhanced by the provision of Use Cases: 3 Requirements: 8
value-added services
Figure 2 Structure of this document.
1.6 Product Scope and Perspective
The EIP Projects urban platform is an open common architecture which serves for city data
collection, management and distribution. An urban platform is intended to support the widespread
exploitation of city data by humans and machines in the urban environment. Figure 3 illustrates a
holistic high level overview of the urban platform the EIP project intends to deliver.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 6
The reference architecture for urban platforms should:
• Cater for interoperability between urban infrastructures
• Enable replicability of the solutions/platforms city to city
• Scale without technical constraints and excessive cost increase
• Provide open APIs and SDKs
• Enable Real Time capabilities
• Support implementation of functional and technical capabilities
Figure 3. High level overview of the urban platform (currently approved EC DG CNECT).
The current urban platform market is nascent. Many software vendors offer such a platform, though
many requirements and expectations of the stakeholders of city data are not (fully) addressed. As
a result, current urban platforms are often more costly to design and maintain, less reusable and
often not interoperable platform-to-platform, and susceptible to information fragmentation and
overload.
The urban platform which the EIP initiative intends to design takes a step beyond the platforms
currently on the market by ensuring the requirements are fully founded on a co-created and
common set of representative city needs, from which it solicits suitable industry input, and an open
and managed collaboration between industry, cities and communities, and others, in order to take
into account their needs and concerns. To do this, it is necessary to take a technology agnostic
approach to design an open and common reference architecture for urban platforms. This platform
must ensure data is collected and sustained in accordance with well-stablished standards,
managed in a robust manner so that it can handle high level supply and demand of data, and
distributed across different value chains, systems and stakeholders. The ability to handle high level
of city data supply and demand while being user secure and accessible enough for city-wide
exploitation of data is one of many keys to the success of urban platforms. This is central to the
design and implementation of urban platforms.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 7
1.7 The Urban Platform Development Stack
The requirements specification of this document is based on the Urban Platform Development
Stack illustrated in Figure 4. The stack is composed by five domains (represented as layers in the
stack) necessary to fully implement an urban platform software suite as shown in Table 1. Each
domain comprehends a set of requirements necessary to the design of a common and open urban
data service platform. The elicited requirements are used to define a technical architecture which is
simple enough to be comprehensible at least at a high level of abstraction. The platform should be
conceptually decomposable into its major subsystems, the platforms functionality reused by many
services and external applications should be identifiable, and interactions between the platform
and services, data providers and data consumers should be well defined and explicit.
The first layer of the stack “Societal needs” concerns to outcomes we strive for within a portfolio of
city service domains. An urban platform should recognise societal needs and wants as the starting
point for city data service offering. Ultimately, an urban platform aims to provide tailor made and
compelling engaging services for the users. The Services and Business models layers concerns
with delivering data services which carefully targets the needs and expectations of the different
users of the urban platform, and explore use cases and commercial models where data is used to
deliver different forms of value. The city data layer concerns with the mechanisms necessary to
transform urban platforms into a foundation for widespread exploitation of data, including handling
data architectural features, data usability, semantics and quality aspects. The urban platform layer
concerns to the technology foundation to configure, share, and interpret exponentially increasing
volumes city data and services. Finally, the Infrastructure layer concerns with the base level
connectivity that supports the platform to be scalable and reliable in the long run.
STACK OUTPUT
Requirements to deliver new digital services that will address the
Societal Needs societal needs of cities in a positive manner that relates to political
narratives.
Services & Requirements to new profitable business models and the development
Business Models of an increase range of new and engaging services in the smart cities.
City Data Requirements to provide all city data stakeholders ready access and
delivery of all city data that unpins the decision making process in smart
cities.
Urban Platform Requirements to put in place applications together to build a foundation
for the widespread exploitation of data.
Infrastructure Requirements to deliver the backbone infrastructure that will be used to
capture the opportunities of digital technology and data to enable
transformation.
Figure 4. Urban Platform Development Stack.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 8
Table 1. Urban Platform Development Stack
Layer Rationale
Societal Needs - Accessible services and data necessary to solve social problems
and drive innovation;
- Parameters that influence users experience while interacting with
services (e.g. usability, feeling of security and trust);
Services and Business - Tailor-made data services which careful targets the needs of users
Models and businesses;
- New potential and cost-effective beneficial services that could be
rolled out across cities of different sizes;
- Use cases where data is used to deliver different forms of value.
City Data - Data architectural features (e.g. volume, variety, temporal factors
and sensitivity);
- Data licensing, policies and regulations to exploit data to full effect;
- Minimum metadata requirements;
- Data usability and reusability aspects of humans and machines.
Urban Platform - Holistic and interoperable solutions;
- Integrated approaches which ensures that services fit together and
that synergies can be exploited;
- Data management mechanisms to ensure data integrity and
compliance with data protection regulations
- Extension capabilities to accommodate additional functionality at
later stage at a fair and transparent cost.
1.8 User Classes, Characteristics, and User Access
The users of the Urban Platform include end-users, such as the general Public, public and private
organisations; data providers; service providers; and the platform providers who will be
working with the providers of city data and services, and managing the content, defining policies
and regulations of the platform. A crucial feature of an urban platform is the provision of the
various access levels required by the different types of users. Particular uses need different
access levels to some data than the general public. Data publishers will require access to the
Urban Platform in order to ingest, administer, manage, preserve and access their resources. This
will require multiple levels of access to city data and its respective metadata. Table 2 provides a
description of each class of users.
Table 2. Actors
User Class Rationale
Platform - Maintains the ecosystem of data, services and users;
Provider - Defines standards, licenses and regulations and provides terms and conditions
for platform usage and the commercial exploitation of data and services;
- Decides who are allowed to join the value network of data and services
providers;
City Data - Publishes open and proprietary data into the platform;
Publisher - Manages and maintain resources in the platform accordingly to terms and
conditions.
Data - Deploys open and commercial data services into the platform (e.g. data
Services visualisation, data cleansing, data integration tools);
Provider - Manages and maintain resources in the platform accordingly to terms and
conditions.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 9
City Data - Consumes open and proprietary data provided in the platform;
Consumer - Uses open and commercial data services provided in the platform;
- Provides feedback on data and services provision;
1.8.1 End-User Access
City data consumers will need to access and use the city data residing in the Urban Platform. End-
users will be able to search metadata and full text within datasets (when available), and obtain city
data in open formats readily available to both humans and machines such as CSV, XML, JSON.
Some end-users may require different access rights to city data. The 2 major end-user groups that
have been identified are:
• Open data users, including both national and international users (humans and machines).
Open access to some city data may be restricted by licensing terms (e.g. commercial data),
embargo periods, copyright, etc.
• Private data users, which need to use the Urban Platform to obtain commercial city data. Data
access is available via data subscriptions or when purchase requirements and licenses are
waived by the data provider.
1.8.2 City Data Publisher Access
A broad data provider level access is needed for stakeholders (humans and machines) working
with the urban platform and their respective data in it. Basically, data publishers will carry out the
following activities:
• Data publication access, available to publishers adding new data and metadata, checking the
quality of datasets, manipulating data, performing format conversions, defining data-access
level, tariff for consumption when applicable, and licences.
• Data maintenance access, for publishers reviewing or editing appropriate data and metadata in
the urban platform. Data publishers can view data and add to or edit metadata without
changing the data itself. They should be provided with access to feedback from users to
investigate problem in their resources (e.g. missing data, inconsistent metadata), and statistical
information about how their resources are used by users.
1.8.3 Data Services Provider Access
This is the second most restrictive access level providing rights to deploy services in the platform.
Basically, data service providers will carry out the following activities:
• Data services deployment access, available to service providers adding new mechanisms or
integrating new applications, testing and validating integration, defining data-access level and
tariff for service usage.
• Data services maintenance access, for services providers reviewing, extending or editing
applications in the urban platform. Data services providers can view their services deployed
and add to or edit access level and tariff without having to deploy the services again. They
should be provided with access to feedback from users to investigate problem in their services
(e.g. bugs, scalability issues), and statistical information about how their services are used by
users.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 10
1.8.4 Platform Provider Access
This is the most restrictive access level providing ultimate rights to the system and is required for
its management, development, and assigning appropriate rights to data and services providers.
Policies and regulations, license agreements are also defined by the provider of the urban
platform. Platform providers should be provided with the means to follow up on civic engagement
(e.g. feedback, request for city data) and on the provision of city data and services.
1.9 User Documentation
• City Data Consumers: Provide license terms and conditions associated to consuming
data and services provided in the platform, documentation of APIs and guide to
discover city data in the platform.
• City Data Providers: Provide data publication documentation describing the minimum
metadata requirements, formats accepted, step-by-step guide to publish accurate city
data.
• Data Service Providers: Disclosure technical and architecture blueprint details in
order share and outsource expertise, and partnerships, and integrate supporting
partners solutions into the platform itself.
1.10 Design and Implementation Constraints
1.10.1 Design Constraints
• Lack of standards agreement for metadata representation.

View File

@@ -1,600 +0,0 @@
• City data found in existing data catalogues may require special consideration concerning the
type of formats and datasets that must be stored within the platform.
• Requirements mismatch due to increased number of stakeholders involved in the design
1.10.2 Implementation Constraints
• Evaluation and testing of software options is expected to occur prior to selection and
implementation of a production urban platform.
• Budget costs are unknown until evaluation of software options is completed.
1.11 Assumptions, Alignment with other Action Clusters and Policies
1.11.1 Assumptions
The assumptions in Table 3 have been identified by the Demand Side Engagement Stream as
relevant to this Requirements Specification.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 11
Table 3. Assumptions
# ASSUMPTION
1 The providers of city data and services will be responsible to maintain their resources in the platform.
2 All city data must meet the minimum metadata requirements and use the standards adopted by the platform.
3 The platform shall consider open Source as an optional commercial model, with open standards as a principle
4 The system design and architecture should minimize fragmentation of city data in the urban platform.
5 To the extent possible, automation should be used for the extraction of descriptive and technical metadata.
6 The platform must be designed in a way it accommodates additional functionality at later stage at a fair and transparent cost.
The platform must be a modular based architecture which relies on stable and well-defined open interfaces to ensure
7
interoperability between the platform, services and the applications provided by service providers.
The platform will offer open and well-documented APIs and clear service descriptions and contracts that is offered for reuse by
8 another party to foster open innovation in the city, which means that developers and interested individuals openly utilize the
resources provided.
9 Adopt open and published European and International standards where possible.
The platform must be flexible enough to accommodate different local, National and International data protection, licensing and
10
commercialization regulations.
11 Platform providers will monitor emerging technologies in order to maintain and improve the architecture.
12 Platform providers will monitor emerging information standards, including metadata standards and data interface standards.
13 Platform providers will monitor new commercial models for city data exploitation
1.11.2 Alignment with Citizens Focus Action Cluster
This specification document is aligned with the principles defined in the Citizen Focus5 Action
Cluster of the European Innovation Partnership on Smart Cities and Communities. Citizen Focus is
about “working together with citizens to realize public interests at the intersection of ICT, mobility
and energy in an urban environment”. We recognize citizens as owners of and participants in
the creation and delivery of city data and digital services, and we specify requirements to deliver
new digital services that will address the societal needs of cities in a positive manner that relates to
political narratives. Societal needs and wants are considered the starting point for city data service
offering by the urban platform. The requirements in this document were elicited considering
• Human behaviour and needs as important as technology;
• The services and data that solve social problems and drive innovation;
• The mechanisms that make data and services more accessible to users;
• The factors that influences users experience while interacting with services provided (e.g.
usability, feeling of security and trust);
5
https://eu-smartcities.eu/node/1333
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 12
1.11.3 Policies to be developed
The following policies have been identified by the Demand Side Engagement Stream as relevant to
this Requirements Specification.
• Data formatting and Metadata Schemes: Urban platforms will require more expansive, robust
and useful data encoding and conversion that what is available in existing data catalogues.
Data preservation policies should be developed to allow data to be stored in formats that can
be migrated, associated with metadata and ontologies to become both humans and machines
readable and understandable, ongoing monitoring for data obsolescence, and migrating data to
systems environments as needed to ensure their continued availability. Current Metadata
schemes (e.g. open data, sensory data ontologies) should be reviewed to see if it meets
current needs for city data management. It is possible that the needs of the urban platform
designed here may require new or additional schemas.
• Data commercialisation: The commercial exploitation of city data and their funding models
are unexplored concepts that we are committed to address. There is an urgent need to define
license agreements and fair commercial and subscription models to allow interoperable open
and proprietary data to co-exist in the platform.
• Data publication and services deployment: Policy development will be needed regarding
ingesting data and deploying data services into the platform, including which users/machines
will be authorized to submit data for publication, the minimum requirements for data submitted
by open and proprietary data publishers, and the removal of resources and services from the
platform.
2. Urban Platform Value Proposition, Use Cases and
Functional Requirements
2.1 From Value Proposition to Platform Specifications
This document uses goal-oriented modelling for eliciting, elaborating, structuring, specifying,
documenting, and modifying requirements. Goals represent the objectives which the urban
platform should achieve through cooperation of actors in the intended system and in the
environment. They capture, at different levels of abstraction, the various objectives the urban
platform under design should achieve. Through goals modelling we consider how the value
proposition and intended solutions connects across the stack, how the urban platform meets city
goals, why the system and its functionality are needed, and how the stakeholders interests may be
addressed.
In our specification, we present the overall goal (the value proposition) that the urban platform
should aim to achieve in order to be considered as a viable final product, and a set of sub-goals
(intended solutions) it should maintain in the long run so that the overall goal can be unceasingly
achieved. By using this approach, the low-level technical requirements can be traced back to high-
level strategic objectives of the urban platform. The formal notations used in this document are:
Achieve [“Name of Overall Goal”] and Maintain [“Name of Sub-Goal”]. The requirements of the
Urban Platform are noted for each of the sub-goals, and are presented as a series of statements
regarding the capabilities needed in to achieve the overall goal of the Urban Platform.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 13
2.2 Overall Goal: “City data is exploited to its full potential”
An urban platform is an organization of people and systems, which has accepted the responsibility
to preserve city data and make it available for all the stakeholders of smart cities. Ultimately, an
urban platform is a foundation for the full exploitation of city data. Hence, the major goal an Urban
Platform must achieve is “city data is exploited to its full potential”. To achieve this high level goal,
the urban platform must maintain in the long run the five sub-goals illustrated in Figure 3.
Each one of the defined sub-goals co-enables the achievement of the specified high-level (overall)
goal of the urban platform. The sub-goals include the ingestion of city data, metadata generation,
data management, data storage, access, preservation, and administration, provision of engaging
services in the smart cities. These sub-goals are discussed in details in the following sections.
Achieve [City data is exploited How we achieve the platform overall goal?
Why do we need the sub-goals?
to its full effect]
Goal
Maintain [User s experience is
Maintain [City data is provided
enhanced by the provision of
in a harmonised way]
S ub-Goal 1 S ub-Goal 5 value-added services]
co-ena bles
Maintain [Resources are managed in Maintain [City data is offered
a safe and intelligent manner] in an accessible manner]
S ub-Goal 2 S ub-Goal 4
Maintain [City data is orchestrated
in a market place]
S ub-Goal 3
Figure 3. Platform High-Level Goal and its respective sub-goals.
2.2.1 Urban Platform Boundary
The use case diagram illustrated in Figure 4 identifies the boundaries between the actors (either
automated or human) and the urban platform. We have arrived at the urban platform boundary by
inspecting each business use case and determining, in conjunction with the stakeholders needs,
which part of the business use case should be implemented and what part should be done by an
outsourced product (e.g. Billing System) using the framework 4.
This task is technology agnostic and takes into account the abilities of the users/actors, the
constraints, the goals of the urban platform. Table 4 maps out the use cases with their respective
sub-goals and actors.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 14
Manage Services
S ub-Goal 5
Data Service
Publish City Data
Provider
S ub-Goal 1
<<include>> Deploy Data Services
rules
S ub-Goal 5
Authenticate in the
Manage Resources <<include>>
Platform Platform
S ub-Goal 1 Provider Database
<<include>> Manage Infrastructure System
S ub-Goal 2
Utlise Data Services
City Data <<include>>
Publisher S ub-Goal 5
Store City Data Management
<<include>>
S ub-Goal 2
Systems Services
System
<<include>>
City Data
Consumer
QoS Monitoring
Transmit Data
System
Register in the Platform S ub-Goal 2
<<include>>
rules
<<extend>>
rules
Platform
Provider
rules
Commercialise Data
Consume City Data
Services City Data
S ub-Goal 3 S ub-Goal 4
Consumer
<<extend>>
Commercialise
City Data Discover City Data
S ub-Goal 3
S ub-Goal 4
Billing
Management
System
Authenticate in the Register in the
Platform Platform
Authenticate Authenticate
Register Consumers Register Publishers
Consumers Publishers
Figure 4. Simplistic overview of the use cases identified in the early stages of the platform design.
Table 4. Use Cases Mapping with Sub-Goals and Actors
Sub-Goals Use Cases ID Specialised Use Cases Actors
HIGH-LEVEL GOAL: City data is exploited to its
Publish City Data UC1 User publishes city data via data APIs
City Data Publisher
1. City data is Register as a publisher
UC2 User manually uploads datasets
provided in a in the Platform
harmonised way
full effect
User manages resources
Manage Resources UC3 City Data Publisher
User tracks resources usage
Store City Data UC4
2. City data is
managed in a safe Transmit Data UC5 - Management Systems
and intelligent
manner
Manage Infrastructure UC6
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 15
City Data Publisher
Set commercial city data
Platform Provider
Subscribe to proprietary data City Data Consumer
Commercialise City
UC7
Data
Manage commercial data City Data Publisher
3. City data is Manage data subscription City Data Consumer
orchestrated in a
Data Services Provider
market place Set commercial data services
Platform Provider
Subscribe to commercial services Data Services Consumer
Commercialise Data
UC8
Services
Manage commercial services Data Services Provider
Manage services subscription Data Services Consumer
UC9
Register in the Platform City Data Consumer
-
4. City data is Discover City Data City Data Consumer
UC10
offered in an
User consumes city data via data
accessible manner
APIs
Consume City Data UC11 City Data Consumer
User downloads datasets
5. Users Data Services Provider
Deploy Data Services UC12
experience is Platform Provider
enhanced by the
Manage Services UC13 - Data Services Provider
provision of value-
added services
Utilise Data Services UC14 City Data Consumer
2.3 SUB-GOAL 1: City data is collected in an intelligent manner
Description: The urban platform enables the owners of city data to easily publish both historic and
data streams in the platform, as well as their associated metadata.
Rationale: This sub-goal is maintained by the services and functions to accept the publication of
city data from data providers (of both open and proprietary data) and prepare the contents for
storage and management within the urban platform. Functions include receiving data, performing
quality assurance on data, verifying data formatting and document standards, associating meta-
data information, and coordinating updates to databases and resources management.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
How we achieve the platform overall goal?
Maintain [City data is provided in
a harmonised manner]
S ub-Goal 1
What actions can maintain the sub-goal?
Achieve [Register Publisher] Achieve [Publish City Data] Achieve [Manage Resources]
UC1 UC2 UC3
Figure 5. Sub-Goal 1 “City data is collected in an intelligent manner” refinement.
Drivers: Ensure data is published in an easy and uniform way.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 16
Actions: For Sub-Goal 1 to be maintained in the long-run it requires the efficient realisation of use
cases: “Publish City Data” and “Manage Resources”, as shown in Figure 5.
2.3.1 Use Case: Register Publisher
ID: UC1
Refines: SUB-GOAL 1: City data is collected in an intelligent manner
Pre-condition: Data Publisher is not logged in the system
Actors: Data Publishers
Rationale: Data Publishers can register in the platform and request approval to submit city data.
They provide valid registration details (to be defined) and wait for registration confirmation.
Platform Providers may authorise or not data publishers to offer both open and proprietary city data
in the platform. Data submission agreement is a formal agreement between the Data Provider and
the Urban Platform defining the terms of the content, standards, metadata creation, and license
agreement. The Urban Platform will proactively work with Data Providers to agree on the content,
quality and format of city data. Agreements between Platform and Data Providers may be
renegotiated on a periodic or ad-hoc basis.
Refines into requirements: FREQ.1 to FREQ.5.
Use Case Basic Stimulus and Responses
1. The platform prompts the user for a username and password or
register new account.
2. The user selects registration option.
3. The platform prompts user for publisher registration information (e.g.
username, password, organisation)
4. The user enters in their information.
UC1. Register 5. Platform verifies information and creates account.
Publisher o If non-valid information, platform shows error message and
returns to step 1.
6. Platform provider is requested to approve the account
o Platform acknowledges registration has been successful
o If non approved, platform shows error message and returns to
step 1.
7. End of registration
2.3.2 Use Case: Publish City Data
ID: UC2
Refines: SUB-GOAL 1 - City data is collected in an intelligent manner
Pre-condition: User is authenticated in the platform
Actors: City data publisher
Rationale: The Publish City Data function provides the appropriate mechanisms to receive city data from
authorized data providers. Data may be manually uploaded or submitted via APIs. In general, data providers
with whom the Urban Platform negotiates submission agreements are the providers of proprietary city data
(those producing published material, i.e. publishers) and open data, and they can be either humans or
machines. The providers of the Urban Platform will provide data providers with specifications on the content,
quality and format of data, and publication terms and conditions.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 17
The Publish City Data function may represent a legal transfer of custody for the data in the urban platform,
and may require that special access controls be placed on the contents. This function provides a
confirmation of receipt of data publication to the Producer, which may include a request to resubmit data in
the case of errors resulting from the submission.
Once data has arrived, it must undergo several reviews, including virus checking, format compliance,
metadata minimum requirement agreement, quality and anticipated content and data formatting. The
platform must include the ability to record all actions and decisions made concerning the publication of city
data. The reasons for publication failure (e.g. missing metadata information, non-valid dataset) will be
provided back to the city data publisher. In some cases, the provider can then resubmit corrected data and
metadata information, while in other instances data publication refusal criteria should prevent the publisher
from submitting the same dataset at a later time period (e.g. in cases of suspicious datasets copyrights
violation, viruses). When data is successfully submitted (either via APIs or manual upload), it will be
processed/prepared for storage into the platforms database.
Specialised Use Cases: The Use Case Publish City Data data is distinguished into two
specialised Use Cases: “User publishes city data via data APIs (UC2.1)” and “User manually
uploads datasets (UC2.2)”.
Subordinated Use Cases: “Store City Data (UC4)”
Refines into requirements: FREQ 6 to FREQ.26.
Specialised Use Cases Basic Stimulus and Responses
1. Platform provides user with an interface for static data
publication
2. User selects datasets to be uploaded
3. User provides metadata associated with the data (license,
provenance, ownership, semantics) in accordance with defined
standards
UC2.1. User manually 4. User requests data publication
uploads datasets 5. Platform quickly process users request for data publication
6. Platform validates data submitted
o If valid data, platform acknowledges data publication has
been successful
o If non-valid data, platform shows error message and returns
to step 1.
7. End of data publication
1. Platform provides user with an interface for real-time data
publication
2. User input data API information
3. User provides metadata associated with the data (license,
provenance, ownership, semantics) in accordance with defined
standards
UC2.2. User publishes 4. User confirm information and request data publication
city data via data APIs 5. Platform quickly process users request for data publication
6. Platform validates data submitted
o If valid data, platform acknowledges data publication has
been successful
o If non-valid data, platform shows error message and returns
to step 1
End of data publication
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 18
2.3.3 Use Case: Manage Resources
ID: UC3
Refines: GOAL 1: City data is collected in an intelligent manner
Pre-condition: User successfully authenticates in the platform
Actors: City data publisher
Rationale: Manage resources provides the services and functions for updating, maintaining and
accessing both data and metadata, as well as tracking the usage of resources by users. Ideally
the owners of the resources should be the only authorised user to manage resources, and other
authorised users can track the usage of the resources in the platform. The platform must provide a
database update response indicating the status of the update, avoid update errors to be
propagated in the platform, and should keep an audit trail of all actions to enable rollback. Data
usage tracking includes performing queries on the data management data to generate result sets,
and producing reports from these result sets.
Specialised Use Cases: The Use Case Manage Resources data is distinguished into two
specialised Use Cases: “User manages resources (UC3.1)” and “User tracks resources usage
(UC3.2)”.
Subordinated Use Cases: “Transmit Data (UC5)”
Refines into requirements: FREQ.27 to FREQ.25.
Specialised Use Cases Basic Interactions and Responses
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to edit or delete data
3. If edit, user revise metadata associated with the data (license,
provenance, ownership, access-control, semantics);
UC3.1. User manages 4. If delete, user selects dataset(s) to be removed
resources 5. User confirms action
6. Platform quickly process users request
7. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
8. End of resources management
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to visualise usage information of a dataset
UC3.2. User tracks 3. Platform quickly process users request for data usage
resources usage information
4. Platform provides user with statistical information about data
usage and data users anonymised information
5. End of data usage tracking.
2.3.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
Societal Needs,
FREQ.1 UC1 Allow data publishers to register to submit data for publication Must
Platform
Tracks data publication agreements between Data and Platform Business Needs,
FREQ.2 UC1 Must
Providers Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 19
Store terms of agreements, and use them to monitor/review/process
FREQ.3 UC1 Must City Data, Platform
data submissions.
Able to add and edit terms of agreement, based on access of level of Business Needs,
FREQ.4 UC1 Must
user. Platform
FREQ.5 UC1 Data publications are managed and monitored Must City Data, Platform
Allow authenticated users from across different organisations to City Data, Platform,
FREQ.6 UC2 Must
publish city data Business Needs
Provide authorization mechanisms for users and sensors to publish
FREQ.7 UC2 Must City Data, Platform
city data
City Data, Platform,
FREQ.9 UC2 Provide mechanisms for static data publication Must
Business Needs
City Data, Platform,
FREQ.10 UC2 Provide mechanisms for real-time data publication Must
Business Needs
FREQ.11 UC2 Enable the publication of metadata Must City Data, Platform
FREQ.12 UC2 Maintain temporal information about the data Must City Data, Platform
FREQ.13 UC2 Support sensory data collection Must City Data, Platform
FREQ.14 UC2 Accepts content in numerous file types/formats Must City Data, Platform
Prompts a request for resubmission to the data provider if an error of

View File

@@ -1,600 +0,0 @@
harmonised way
full effect
User manages resources
Manage Resources UC3 City Data Publisher
User tracks resources usage
Store City Data UC4
2. City data is
managed in a safe Transmit Data UC5 - Management Systems
and intelligent
manner
Manage Infrastructure UC6
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 15
City Data Publisher
Set commercial city data
Platform Provider
Subscribe to proprietary data City Data Consumer
Commercialise City
UC7
Data
Manage commercial data City Data Publisher
3. City data is Manage data subscription City Data Consumer
orchestrated in a
Data Services Provider
market place Set commercial data services
Platform Provider
Subscribe to commercial services Data Services Consumer
Commercialise Data
UC8
Services
Manage commercial services Data Services Provider
Manage services subscription Data Services Consumer
UC9
Register in the Platform City Data Consumer
-
4. City data is Discover City Data City Data Consumer
UC10
offered in an
User consumes city data via data
accessible manner
APIs
Consume City Data UC11 City Data Consumer
User downloads datasets
5. Users Data Services Provider
Deploy Data Services UC12
experience is Platform Provider
enhanced by the
Manage Services UC13 - Data Services Provider
provision of value-
added services
Utilise Data Services UC14 City Data Consumer
2.3 SUB-GOAL 1: City data is collected in an intelligent manner
Description: The urban platform enables the owners of city data to easily publish both historic and
data streams in the platform, as well as their associated metadata.
Rationale: This sub-goal is maintained by the services and functions to accept the publication of
city data from data providers (of both open and proprietary data) and prepare the contents for
storage and management within the urban platform. Functions include receiving data, performing
quality assurance on data, verifying data formatting and document standards, associating meta-
data information, and coordinating updates to databases and resources management.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
How we achieve the platform overall goal?
Maintain [City data is provided in
a harmonised manner]
S ub-Goal 1
What actions can maintain the sub-goal?
Achieve [Register Publisher] Achieve [Publish City Data] Achieve [Manage Resources]
UC1 UC2 UC3
Figure 5. Sub-Goal 1 “City data is collected in an intelligent manner” refinement.
Drivers: Ensure data is published in an easy and uniform way.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 16
Actions: For Sub-Goal 1 to be maintained in the long-run it requires the efficient realisation of use
cases: “Publish City Data” and “Manage Resources”, as shown in Figure 5.
2.3.1 Use Case: Register Publisher
ID: UC1
Refines: SUB-GOAL 1: City data is collected in an intelligent manner
Pre-condition: Data Publisher is not logged in the system
Actors: Data Publishers
Rationale: Data Publishers can register in the platform and request approval to submit city data.
They provide valid registration details (to be defined) and wait for registration confirmation.
Platform Providers may authorise or not data publishers to offer both open and proprietary city data
in the platform. Data submission agreement is a formal agreement between the Data Provider and
the Urban Platform defining the terms of the content, standards, metadata creation, and license
agreement. The Urban Platform will proactively work with Data Providers to agree on the content,
quality and format of city data. Agreements between Platform and Data Providers may be
renegotiated on a periodic or ad-hoc basis.
Refines into requirements: FREQ.1 to FREQ.5.
Use Case Basic Stimulus and Responses
1. The platform prompts the user for a username and password or
register new account.
2. The user selects registration option.
3. The platform prompts user for publisher registration information (e.g.
username, password, organisation)
4. The user enters in their information.
UC1. Register 5. Platform verifies information and creates account.
Publisher o If non-valid information, platform shows error message and
returns to step 1.
6. Platform provider is requested to approve the account
o Platform acknowledges registration has been successful
o If non approved, platform shows error message and returns to
step 1.
7. End of registration
2.3.2 Use Case: Publish City Data
ID: UC2
Refines: SUB-GOAL 1 - City data is collected in an intelligent manner
Pre-condition: User is authenticated in the platform
Actors: City data publisher
Rationale: The Publish City Data function provides the appropriate mechanisms to receive city data from
authorized data providers. Data may be manually uploaded or submitted via APIs. In general, data providers
with whom the Urban Platform negotiates submission agreements are the providers of proprietary city data
(those producing published material, i.e. publishers) and open data, and they can be either humans or
machines. The providers of the Urban Platform will provide data providers with specifications on the content,
quality and format of data, and publication terms and conditions.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 17
The Publish City Data function may represent a legal transfer of custody for the data in the urban platform,
and may require that special access controls be placed on the contents. This function provides a
confirmation of receipt of data publication to the Producer, which may include a request to resubmit data in
the case of errors resulting from the submission.
Once data has arrived, it must undergo several reviews, including virus checking, format compliance,
metadata minimum requirement agreement, quality and anticipated content and data formatting. The
platform must include the ability to record all actions and decisions made concerning the publication of city
data. The reasons for publication failure (e.g. missing metadata information, non-valid dataset) will be
provided back to the city data publisher. In some cases, the provider can then resubmit corrected data and
metadata information, while in other instances data publication refusal criteria should prevent the publisher
from submitting the same dataset at a later time period (e.g. in cases of suspicious datasets copyrights
violation, viruses). When data is successfully submitted (either via APIs or manual upload), it will be
processed/prepared for storage into the platforms database.
Specialised Use Cases: The Use Case Publish City Data data is distinguished into two
specialised Use Cases: “User publishes city data via data APIs (UC2.1)” and “User manually
uploads datasets (UC2.2)”.
Subordinated Use Cases: “Store City Data (UC4)”
Refines into requirements: FREQ 6 to FREQ.26.
Specialised Use Cases Basic Stimulus and Responses
1. Platform provides user with an interface for static data
publication
2. User selects datasets to be uploaded
3. User provides metadata associated with the data (license,
provenance, ownership, semantics) in accordance with defined
standards
UC2.1. User manually 4. User requests data publication
uploads datasets 5. Platform quickly process users request for data publication
6. Platform validates data submitted
o If valid data, platform acknowledges data publication has
been successful
o If non-valid data, platform shows error message and returns
to step 1.
7. End of data publication
1. Platform provides user with an interface for real-time data
publication
2. User input data API information
3. User provides metadata associated with the data (license,
provenance, ownership, semantics) in accordance with defined
standards
UC2.2. User publishes 4. User confirm information and request data publication
city data via data APIs 5. Platform quickly process users request for data publication
6. Platform validates data submitted
o If valid data, platform acknowledges data publication has
been successful
o If non-valid data, platform shows error message and returns
to step 1
End of data publication
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 18
2.3.3 Use Case: Manage Resources
ID: UC3
Refines: GOAL 1: City data is collected in an intelligent manner
Pre-condition: User successfully authenticates in the platform
Actors: City data publisher
Rationale: Manage resources provides the services and functions for updating, maintaining and
accessing both data and metadata, as well as tracking the usage of resources by users. Ideally
the owners of the resources should be the only authorised user to manage resources, and other
authorised users can track the usage of the resources in the platform. The platform must provide a
database update response indicating the status of the update, avoid update errors to be
propagated in the platform, and should keep an audit trail of all actions to enable rollback. Data
usage tracking includes performing queries on the data management data to generate result sets,
and producing reports from these result sets.
Specialised Use Cases: The Use Case Manage Resources data is distinguished into two
specialised Use Cases: “User manages resources (UC3.1)” and “User tracks resources usage
(UC3.2)”.
Subordinated Use Cases: “Transmit Data (UC5)”
Refines into requirements: FREQ.27 to FREQ.25.
Specialised Use Cases Basic Interactions and Responses
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to edit or delete data
3. If edit, user revise metadata associated with the data (license,
provenance, ownership, access-control, semantics);
UC3.1. User manages 4. If delete, user selects dataset(s) to be removed
resources 5. User confirms action
6. Platform quickly process users request
7. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
8. End of resources management
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to visualise usage information of a dataset
UC3.2. User tracks 3. Platform quickly process users request for data usage
resources usage information
4. Platform provides user with statistical information about data
usage and data users anonymised information
5. End of data usage tracking.
2.3.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
Societal Needs,
FREQ.1 UC1 Allow data publishers to register to submit data for publication Must
Platform
Tracks data publication agreements between Data and Platform Business Needs,
FREQ.2 UC1 Must
Providers Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 19
Store terms of agreements, and use them to monitor/review/process
FREQ.3 UC1 Must City Data, Platform
data submissions.
Able to add and edit terms of agreement, based on access of level of Business Needs,
FREQ.4 UC1 Must
user. Platform
FREQ.5 UC1 Data publications are managed and monitored Must City Data, Platform
Allow authenticated users from across different organisations to City Data, Platform,
FREQ.6 UC2 Must
publish city data Business Needs
Provide authorization mechanisms for users and sensors to publish
FREQ.7 UC2 Must City Data, Platform
city data
City Data, Platform,
FREQ.9 UC2 Provide mechanisms for static data publication Must
Business Needs
City Data, Platform,
FREQ.10 UC2 Provide mechanisms for real-time data publication Must
Business Needs
FREQ.11 UC2 Enable the publication of metadata Must City Data, Platform
FREQ.12 UC2 Maintain temporal information about the data Must City Data, Platform
FREQ.13 UC2 Support sensory data collection Must City Data, Platform
FREQ.14 UC2 Accepts content in numerous file types/formats Must City Data, Platform
Prompts a request for resubmission to the data provider if an error of
FREQ.15 UC2 Must City Data, Platform
data transmission or receipt occurs
FREQ.16 UC2 Enable the semantic description of connected devices Must City Data, Platform
FREQ.17 UC2 Gather data from authenticated and authorized devices Must City Data, Platform
FREQ.18 UC2 Validates automatically the successful transfer of the data Must City Data, Platform
FREQ.19 UC2 Performs virus checking on data Must City Data, Platform
Verifies the validity of the submission based on submitter, expected
FREQ.20 UC2 Must City Data, Platform
format, data quality, and completeness
Platform should have built-in checks on the incoming metadata. Data
FREQ.21 UC2 not containing the minimally defined set of attributes should be Must City Data, Platform
returned to the publisher for metadata enhancement.
System should have a user-friendly method of mapping non-standard
FREQ.22 UC2 Should City Data, Platform
metadata elements into approved standard elements.
Once ingested, metadata should be stored in a single common format.
FREQ.23 UC2 This format should be one that ensures against data loss, and allows a Must City Data, Platform
variety of access/distribution options
Data in the repository shall have sufficient technical metadata to
FREQ.24 UC2 assure functionality (e.g. viewing and display) to ensure accessibility Must City Data, Platform
and reusability.
Allows publishers to display and perform manual/visual quality control Business Needs, City
FREQ.25 UC2 Could
assurance via a user-friendly GUI Data, Platform
Business Needs, City
FREQ.26 UC2 Any errors shall prompt a request for resubmission of data Should
Data, Platform
FREQ.27 UC3 Enable data providers to manage their resources Must Business Needs
A minimal set of identifying information/metadata concerning data Business Needs,
FREQ.28 UC3 Must
publication submission must be recorded Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 20
Stores and tracks versions of data. Links /connections between
FREQ.29 UC3 Must City Data, Platform
versions are created and maintained
Give service and data providers access to anonymized data of the Business Needs
FREQ.30 UC3 Should
subscribers of their data or services
City Data, Platform,
FREQ.31 UC3 Enable data providers to maintain and repair data and metadata Should
Business Needs
Tracks data publication agreements between Data and Platform Business Needs,
FREQ.32 UC3 Must
Providers Platform
Store terms of agreements, and use them to monitor/review/process
FREQ.33 UC3 Must City Data, Platform
data submissions.
Able to add and edit terms of agreement, based on access of level of Business Needs,
FREQ.34 UC3 Must
user. Platform
FREQ.35 UC3 Submission volumes and schedules are managed and monitored Must City Data, Platform
2.4 SUB-GOAL 2: City data is managed in a safe and intelligent manner
Rationale: The urban platform enables users to publish, consume and commercialise data, as well
as deploy and manage services all in a secure and privacy protected manner.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
Maintain [Resources are managed in a
safe and intelligent manner]
S ub-Goal 1
Achieve [Store Data] Achieve [Transmit Data] Achieve [Manage Infrastructure]
UC4 UC5 UC6
Figure 6. Sub-Goal 2 “City data is managed in a safe and intelligent manner” refinement.
Drivers: Ensure data is secured and the identity of users are preserved
Actions: For Sub-Goal 2 to be maintained in the long-run it requires the efficient realisation of use
cases: “Store City Data” and “Retrieve and Transmit City Data”, as shown in Figure 6.
2.4.1 Use Case: Store City Data
ID: UC4
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: Data is successfully published in the platform
Actors: Urban Platform
Rationale: When data is successfully submitted (either via APIs or manual upload), it is
processed/prepared for storage into the platforms database. This procedure will include the
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 21
generation of unique identifiers to the database, enrichment with ontologies (when applicable),
encrypted (when applicable), signed with digital certificates (when applicable) to ensure that the
data conforms to the platform data formatting, standards, security and regulation. Data may be
converted to accepted formats, as needed (e.g. graph model). A primary goal of the conversion of
content for the platform is the preservation of the content. Priority will be given to preserving the
data accordingly to the policies defined in section (2.6.2). Access-control levels and license models
are associated to data which is subject to restrictions relating to access and conditions of use.
Refines into requirements: FREQ 28 FREQ 39.
Use Case Basic Interactions and Responses
1. Platform mechanisms converts submitted data into a standard format
2. Security enforcement (e.g. anonymisation, cryptography) is placed on
sensitive information.
UC4. Store City 3. Platform associates with datasets the access-control definitions set by
Data owner of resources.
4. Data is enriched with semantics and is associated with other datasets
5. Platform stores data in a scalable and secure database.
6. End of data storage.
2.4.2 Use Case: Transmit Data
ID: UC5
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: Data is successfully published in the platform
Actors: Urban Platform
Rationale: The platform accepts data retrieval request, validates users rights to access the data,
retrieves city data from data storage, and moves a copy of the data to the relevant platform
component for further processing. If special processing is required, the retrieval function accesses
data in staging storage and applies the requested processes. The types of operations, which may
be carried out, include sub-sampling in temporal or spatial dimensions, conversions between
different data types or output formats, and other specialized processing (e.g., data visualisation).
Once it is finalised data will be sent to the appropriate delivery channels (e.g. APIs, GUI). It also
encompasses function to verify corruption during any internal data transfer. This function requires
that all hardware and software within the platform provide notification of potential errors and that
these errors are routed to standard error logs that are checked by the Platform Provider.
Refines into requirements: FREQ 40 to 54.
2.4.3 Use Case: Manage Infrastructure
ID: UC6
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: The platform is available
Actors: Urban Platform
Rationale: Manage infrastructure provides the services and functions for the overall operation of
the urban platform. Administration functions include monitoring quality of service agreements,
auditing data publication to ensure that they meet archive standards, and maintaining configuration
management of system hardware and software. In overall, it provides system engineering
functions to monitor and improve platform operations, and to inventory, report on, and
migrate/update the contents of the platforms databases.
Refines into requirements: FREQ 55 to 62.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 22
Use Case Basic Interactions and Responses
1. Platform keeps monitoring services at run-time to ensure operation
and integrity of city data
o If system failure, the platform activates mechanisms for recovery
UC6. Manage based on pre-defined rules
Infrastructure o Platform logs issue and issue alert messages to platform providers
2. Platform logs operation capabilities (e.g. performance, mean of time
failure, issues, etc.)
2.4.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
A minimal set of identifying information/metadata concerning data Business Needs,
FREQ.28 UC4 Must
publication submission must be recorded. Platform
Stores and tracks versions of data. Links /connections between
FREQ.29 UC4 Must City Data, Platform
versions are created and maintained.
FREQ.30 UC4 Converts data to accepted file formats Must City Data, Platform
Keep sensitive information secured and accessible only to authorized
FREQ.31 UC4 Must City Data, Platform
users
FREQ.32 UC4 Keep users personal information protected Should City Data, Platform
FREQ.33 UC4 Keep city data and meta-data secured Must Platform Needs
FREQ.34 UC4 Enable privacy preserving mechanisms associated to data Must Platform Needs
FREQ.35 UC4 Model data in accordance with defined standards Must City Data, Platform
FREQ.36 UC4 Support the use of ontologies and semantic modelling of city data Could City Data
FREQ.37 UC4 Support database-level provenance annotation Should City Data
FREQ.38 UC4 Support data-level provenance annotation Should City Data
FREQ.39 UC4 Enable data to be encrypted Should Platform Needs
System must have the ability to search and display metadata, preferably
City Data, Platform
FREQ.40 UC5 in a user-conformable, human readable display as well as in its native Must
Needs
format for machine harvesting and manipulation.
Controls access to data in the repository based on multiple permission
City Data, Platform
FREQ.41 UC5 levels. These permission levels determine the create/edit/read/delete Should
Needs
privileges granted users.
Access rights and conditions of use will be held for each data and its City Data, Platform
FREQ.42 UC5 Should
related metadata. Needs
Access rights and conditions can be inherited from a parent data to any City Data, Platform
FREQ.43 UC5 Could
data designated as a child data (derived information). Needs
Access rights and conditions of use will be machine readable and City Data, Platform
FREQ.44 UC5 Should
actionable. Needs
Access mechanisms must be sufficiently granular to allow the Business Needs, City
FREQ.45 UC5 identification of individual users, in order to maintain audit logs of actions Should
Data, Platform
performed by users.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 23
Maintains the integrity of the database which contains both metadata
FREQ.46 UC5 and system information. Must Platform Needs
Provides internal validation such as referential integrity of the contents of City Data, Platform
FREQ.47 UC5 the database. Must
Needs
Creates and maintains schema definitions required to support data
FREQ.48 UC5 management functions. Must Platform Needs
Monitors and ensures that data and metadata are not corrupted during
FREQ.49 UC5 transfers. Must Platform Needs
Provides statistically acceptable assurance that no components of the
FREQ.50 UC5 data are corrupted during any internal data transfer. Must Platform Needs
Performs routine and special data integrity checking for each dataset
FREQ.51 UC5 and generates error reports. Must Platform Needs
Provides disaster recovery capabilities including data backup, off-site
FREQ.52 UC5 data storage, data recovery, etc. Must Platform Needs
Refresh/replace data without service interruption, and update
FREQ.53 UC5 corresponding metadata as appropriate. Must Platform Needs
Ensure that any associated unique identifiers of the updated data are not
FREQ.54 UC5 altered. Must Platform Needs
Audits submissions to ensure that they meet archive/repository
FREQ.55 UC6 standards. Must Platform Needs
Maintains configuration management of the system hardware and
FREQ.56 UC6 software. Must Platform Needs
Has capability to inventory, report on and migrate the contents of the
FREQ.57 UC6 repository. Must Platform Needs
FREQ.58 UC6 Ensures data integrity for version upgrades and format migration. Must Platform Needs
FREQ.59 UC6 Monitors functionality of the entire repository. Must Platform Needs
FREQ.60 UC6 Maintains integrity of system configuration. Must Platform Needs
Audits system operations, performance and usage.
FREQ.61 UC6 Must Platform Needs
Provides platform performance information and database holdings
FREQ.62 UC6 inventory reports Must Platform Needs
2.5 SUB-GOAL 3: City data is orchestrated in a marketplace
Rationale: The urban platform enables users to consume and publish data in a secure and privacy
protected manner.
Drivers: Ensure data is secured and the identity of users are preserved
Actions: For Sub-Goal 3 to be maintained in the long-run it requires the efficient realisation of use
cases: “Commercialise City Data” and “Commercialise Data Services”, as shown in Figure 7.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 24

View File

@@ -1,700 +0,0 @@
city data via data APIs 5. Platform quickly process users request for data publication
6. Platform validates data submitted
o If valid data, platform acknowledges data publication has
been successful
o If non-valid data, platform shows error message and returns
to step 1
End of data publication
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 18
2.3.3 Use Case: Manage Resources
ID: UC3
Refines: GOAL 1: City data is collected in an intelligent manner
Pre-condition: User successfully authenticates in the platform
Actors: City data publisher
Rationale: Manage resources provides the services and functions for updating, maintaining and
accessing both data and metadata, as well as tracking the usage of resources by users. Ideally
the owners of the resources should be the only authorised user to manage resources, and other
authorised users can track the usage of the resources in the platform. The platform must provide a
database update response indicating the status of the update, avoid update errors to be
propagated in the platform, and should keep an audit trail of all actions to enable rollback. Data
usage tracking includes performing queries on the data management data to generate result sets,
and producing reports from these result sets.
Specialised Use Cases: The Use Case Manage Resources data is distinguished into two
specialised Use Cases: “User manages resources (UC3.1)” and “User tracks resources usage
(UC3.2)”.
Subordinated Use Cases: “Transmit Data (UC5)”
Refines into requirements: FREQ.27 to FREQ.25.
Specialised Use Cases Basic Interactions and Responses
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to edit or delete data
3. If edit, user revise metadata associated with the data (license,
provenance, ownership, access-control, semantics);
UC3.1. User manages 4. If delete, user selects dataset(s) to be removed
resources 5. User confirms action
6. Platform quickly process users request
7. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
8. End of resources management
1. Platform provides user with an interface for resources
management (e.g. data and metadata, data usage)
2. User chooses to visualise usage information of a dataset
UC3.2. User tracks 3. Platform quickly process users request for data usage
resources usage information
4. Platform provides user with statistical information about data
usage and data users anonymised information
5. End of data usage tracking.
2.3.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
Societal Needs,
FREQ.1 UC1 Allow data publishers to register to submit data for publication Must
Platform
Tracks data publication agreements between Data and Platform Business Needs,
FREQ.2 UC1 Must
Providers Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 19
Store terms of agreements, and use them to monitor/review/process
FREQ.3 UC1 Must City Data, Platform
data submissions.
Able to add and edit terms of agreement, based on access of level of Business Needs,
FREQ.4 UC1 Must
user. Platform
FREQ.5 UC1 Data publications are managed and monitored Must City Data, Platform
Allow authenticated users from across different organisations to City Data, Platform,
FREQ.6 UC2 Must
publish city data Business Needs
Provide authorization mechanisms for users and sensors to publish
FREQ.7 UC2 Must City Data, Platform
city data
City Data, Platform,
FREQ.9 UC2 Provide mechanisms for static data publication Must
Business Needs
City Data, Platform,
FREQ.10 UC2 Provide mechanisms for real-time data publication Must
Business Needs
FREQ.11 UC2 Enable the publication of metadata Must City Data, Platform
FREQ.12 UC2 Maintain temporal information about the data Must City Data, Platform
FREQ.13 UC2 Support sensory data collection Must City Data, Platform
FREQ.14 UC2 Accepts content in numerous file types/formats Must City Data, Platform
Prompts a request for resubmission to the data provider if an error of
FREQ.15 UC2 Must City Data, Platform
data transmission or receipt occurs
FREQ.16 UC2 Enable the semantic description of connected devices Must City Data, Platform
FREQ.17 UC2 Gather data from authenticated and authorized devices Must City Data, Platform
FREQ.18 UC2 Validates automatically the successful transfer of the data Must City Data, Platform
FREQ.19 UC2 Performs virus checking on data Must City Data, Platform
Verifies the validity of the submission based on submitter, expected
FREQ.20 UC2 Must City Data, Platform
format, data quality, and completeness
Platform should have built-in checks on the incoming metadata. Data
FREQ.21 UC2 not containing the minimally defined set of attributes should be Must City Data, Platform
returned to the publisher for metadata enhancement.
System should have a user-friendly method of mapping non-standard
FREQ.22 UC2 Should City Data, Platform
metadata elements into approved standard elements.
Once ingested, metadata should be stored in a single common format.
FREQ.23 UC2 This format should be one that ensures against data loss, and allows a Must City Data, Platform
variety of access/distribution options
Data in the repository shall have sufficient technical metadata to
FREQ.24 UC2 assure functionality (e.g. viewing and display) to ensure accessibility Must City Data, Platform
and reusability.
Allows publishers to display and perform manual/visual quality control Business Needs, City
FREQ.25 UC2 Could
assurance via a user-friendly GUI Data, Platform
Business Needs, City
FREQ.26 UC2 Any errors shall prompt a request for resubmission of data Should
Data, Platform
FREQ.27 UC3 Enable data providers to manage their resources Must Business Needs
A minimal set of identifying information/metadata concerning data Business Needs,
FREQ.28 UC3 Must
publication submission must be recorded Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 20
Stores and tracks versions of data. Links /connections between
FREQ.29 UC3 Must City Data, Platform
versions are created and maintained
Give service and data providers access to anonymized data of the Business Needs
FREQ.30 UC3 Should
subscribers of their data or services
City Data, Platform,
FREQ.31 UC3 Enable data providers to maintain and repair data and metadata Should
Business Needs
Tracks data publication agreements between Data and Platform Business Needs,
FREQ.32 UC3 Must
Providers Platform
Store terms of agreements, and use them to monitor/review/process
FREQ.33 UC3 Must City Data, Platform
data submissions.
Able to add and edit terms of agreement, based on access of level of Business Needs,
FREQ.34 UC3 Must
user. Platform
FREQ.35 UC3 Submission volumes and schedules are managed and monitored Must City Data, Platform
2.4 SUB-GOAL 2: City data is managed in a safe and intelligent manner
Rationale: The urban platform enables users to publish, consume and commercialise data, as well
as deploy and manage services all in a secure and privacy protected manner.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
Maintain [Resources are managed in a
safe and intelligent manner]
S ub-Goal 1
Achieve [Store Data] Achieve [Transmit Data] Achieve [Manage Infrastructure]
UC4 UC5 UC6
Figure 6. Sub-Goal 2 “City data is managed in a safe and intelligent manner” refinement.
Drivers: Ensure data is secured and the identity of users are preserved
Actions: For Sub-Goal 2 to be maintained in the long-run it requires the efficient realisation of use
cases: “Store City Data” and “Retrieve and Transmit City Data”, as shown in Figure 6.
2.4.1 Use Case: Store City Data
ID: UC4
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: Data is successfully published in the platform
Actors: Urban Platform
Rationale: When data is successfully submitted (either via APIs or manual upload), it is
processed/prepared for storage into the platforms database. This procedure will include the
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 21
generation of unique identifiers to the database, enrichment with ontologies (when applicable),
encrypted (when applicable), signed with digital certificates (when applicable) to ensure that the
data conforms to the platform data formatting, standards, security and regulation. Data may be
converted to accepted formats, as needed (e.g. graph model). A primary goal of the conversion of
content for the platform is the preservation of the content. Priority will be given to preserving the
data accordingly to the policies defined in section (2.6.2). Access-control levels and license models
are associated to data which is subject to restrictions relating to access and conditions of use.
Refines into requirements: FREQ 28 FREQ 39.
Use Case Basic Interactions and Responses
1. Platform mechanisms converts submitted data into a standard format
2. Security enforcement (e.g. anonymisation, cryptography) is placed on
sensitive information.
UC4. Store City 3. Platform associates with datasets the access-control definitions set by
Data owner of resources.
4. Data is enriched with semantics and is associated with other datasets
5. Platform stores data in a scalable and secure database.
6. End of data storage.
2.4.2 Use Case: Transmit Data
ID: UC5
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: Data is successfully published in the platform
Actors: Urban Platform
Rationale: The platform accepts data retrieval request, validates users rights to access the data,
retrieves city data from data storage, and moves a copy of the data to the relevant platform
component for further processing. If special processing is required, the retrieval function accesses
data in staging storage and applies the requested processes. The types of operations, which may
be carried out, include sub-sampling in temporal or spatial dimensions, conversions between
different data types or output formats, and other specialized processing (e.g., data visualisation).
Once it is finalised data will be sent to the appropriate delivery channels (e.g. APIs, GUI). It also
encompasses function to verify corruption during any internal data transfer. This function requires
that all hardware and software within the platform provide notification of potential errors and that
these errors are routed to standard error logs that are checked by the Platform Provider.
Refines into requirements: FREQ 40 to 54.
2.4.3 Use Case: Manage Infrastructure
ID: UC6
Refines: SUB-GOAL 2: City data is managed in a safe and intelligent manner
Pre-condition: The platform is available
Actors: Urban Platform
Rationale: Manage infrastructure provides the services and functions for the overall operation of
the urban platform. Administration functions include monitoring quality of service agreements,
auditing data publication to ensure that they meet archive standards, and maintaining configuration
management of system hardware and software. In overall, it provides system engineering
functions to monitor and improve platform operations, and to inventory, report on, and
migrate/update the contents of the platforms databases.
Refines into requirements: FREQ 55 to 62.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 22
Use Case Basic Interactions and Responses
1. Platform keeps monitoring services at run-time to ensure operation
and integrity of city data
o If system failure, the platform activates mechanisms for recovery
UC6. Manage based on pre-defined rules
Infrastructure o Platform logs issue and issue alert messages to platform providers
2. Platform logs operation capabilities (e.g. performance, mean of time
failure, issues, etc.)
2.4.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
A minimal set of identifying information/metadata concerning data Business Needs,
FREQ.28 UC4 Must
publication submission must be recorded. Platform
Stores and tracks versions of data. Links /connections between
FREQ.29 UC4 Must City Data, Platform
versions are created and maintained.
FREQ.30 UC4 Converts data to accepted file formats Must City Data, Platform
Keep sensitive information secured and accessible only to authorized
FREQ.31 UC4 Must City Data, Platform
users
FREQ.32 UC4 Keep users personal information protected Should City Data, Platform
FREQ.33 UC4 Keep city data and meta-data secured Must Platform Needs
FREQ.34 UC4 Enable privacy preserving mechanisms associated to data Must Platform Needs
FREQ.35 UC4 Model data in accordance with defined standards Must City Data, Platform
FREQ.36 UC4 Support the use of ontologies and semantic modelling of city data Could City Data
FREQ.37 UC4 Support database-level provenance annotation Should City Data
FREQ.38 UC4 Support data-level provenance annotation Should City Data
FREQ.39 UC4 Enable data to be encrypted Should Platform Needs
System must have the ability to search and display metadata, preferably
City Data, Platform
FREQ.40 UC5 in a user-conformable, human readable display as well as in its native Must
Needs
format for machine harvesting and manipulation.
Controls access to data in the repository based on multiple permission
City Data, Platform
FREQ.41 UC5 levels. These permission levels determine the create/edit/read/delete Should
Needs
privileges granted users.
Access rights and conditions of use will be held for each data and its City Data, Platform
FREQ.42 UC5 Should
related metadata. Needs
Access rights and conditions can be inherited from a parent data to any City Data, Platform
FREQ.43 UC5 Could
data designated as a child data (derived information). Needs
Access rights and conditions of use will be machine readable and City Data, Platform
FREQ.44 UC5 Should
actionable. Needs
Access mechanisms must be sufficiently granular to allow the Business Needs, City
FREQ.45 UC5 identification of individual users, in order to maintain audit logs of actions Should
Data, Platform
performed by users.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 23
Maintains the integrity of the database which contains both metadata
FREQ.46 UC5 and system information. Must Platform Needs
Provides internal validation such as referential integrity of the contents of City Data, Platform
FREQ.47 UC5 the database. Must
Needs
Creates and maintains schema definitions required to support data
FREQ.48 UC5 management functions. Must Platform Needs
Monitors and ensures that data and metadata are not corrupted during
FREQ.49 UC5 transfers. Must Platform Needs
Provides statistically acceptable assurance that no components of the
FREQ.50 UC5 data are corrupted during any internal data transfer. Must Platform Needs
Performs routine and special data integrity checking for each dataset
FREQ.51 UC5 and generates error reports. Must Platform Needs
Provides disaster recovery capabilities including data backup, off-site
FREQ.52 UC5 data storage, data recovery, etc. Must Platform Needs
Refresh/replace data without service interruption, and update
FREQ.53 UC5 corresponding metadata as appropriate. Must Platform Needs
Ensure that any associated unique identifiers of the updated data are not
FREQ.54 UC5 altered. Must Platform Needs
Audits submissions to ensure that they meet archive/repository
FREQ.55 UC6 standards. Must Platform Needs
Maintains configuration management of the system hardware and
FREQ.56 UC6 software. Must Platform Needs
Has capability to inventory, report on and migrate the contents of the
FREQ.57 UC6 repository. Must Platform Needs
FREQ.58 UC6 Ensures data integrity for version upgrades and format migration. Must Platform Needs
FREQ.59 UC6 Monitors functionality of the entire repository. Must Platform Needs
FREQ.60 UC6 Maintains integrity of system configuration. Must Platform Needs
Audits system operations, performance and usage.
FREQ.61 UC6 Must Platform Needs
Provides platform performance information and database holdings
FREQ.62 UC6 inventory reports Must Platform Needs
2.5 SUB-GOAL 3: City data is orchestrated in a marketplace
Rationale: The urban platform enables users to consume and publish data in a secure and privacy
protected manner.
Drivers: Ensure data is secured and the identity of users are preserved
Actions: For Sub-Goal 3 to be maintained in the long-run it requires the efficient realisation of use
cases: “Commercialise City Data” and “Commercialise Data Services”, as shown in Figure 7.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 24
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
Maintain [City data is orchestrated in
a marketplace]
S ub-Goal 3
Achieve [Commercialise Data
Achieve [Commercialise City Data]
Services]
UC7 UC8
Figure 7. Sub-Goal 3 “City data is orchestrated in a marketplace” refinement.
2.5.1 Use Case: Commercialise City Data
ID: UC7
Refines: SUB-GOAL 3: City data is orchestrated in a marketplace
Pre-condition: Data is successfully published in the platform, both publisher and consumers of
city data are authenticated in the platform, and there are billing capabilities available.
Actors: Data Publishers, Data Consumers
Rationale: The providers of city data can commercialise city data based on the policies and
financial models defined in the platform. After publishing their data, publishers can define which
data can be available as open data and which data should be available with the payment of a
subscription fee. Once publishers define which data is to be commercially exploited, the platform
will associate the data with their respective financial models and let it ready for subscription. City
data consumer chooses which data to purchase and is redirected to a billing interface where the
subscription payment is taken. The platform must provide an update response indicating the status
of the payment. If successful, data is ready available to be consumed by humans and machines,
otherwise the user can re-try the payment or cancel transaction.
Commercialise city data also involves the function of managing commercial data. It provides
services and functions for updating, maintaining and accessing both data and its respective
commercial transactions. Furthermore, it enables data providers to track the usage of commercial
data by users. Ideally the owners of the data should be the only authorised user to manage
resources, and other authorised users can track the usage of the data in the platform. Data usage
tracking includes performing queries on the data management data to generate result sets, and
producing reports from these result sets. Data consumers are provided with functions which enable
them to manage their subscriptions and financial transactions on the platform. These functions
include updating, maintaining and accessing financial transactions. For all these functions and
services, the platform must provide a database update response indicating the status of the
update, avoid update errors to be propagated in the platform, and should keep an audit trail of all
actions to enable rollback.
Specialised Use Cases: The Use Case Commercialise City Data is distinguished into four
specialised Use Cases: “Commercialise Data (UC7.1)”, “Consume Commercial City Data (UC7.2)”,
“Manage Commercial Data (UC7.3)” and “Manage Data Subscription (UC7.4)”.
Refines into requirements: FREQ 63 to 68.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 25
Use Case Basic Interactions and Responses
1. User selects the data to be commercialised
2. User selects the commercial model for data consumption
UC7.1. 3. Platform validates selection
Commercialise 4. Platform associates data to subscription model
Data 5. Platform releases data for commercial exploitation in the marketplace
6. End of data commercialisation set up.
1. User selects the data to be subscribed to
2. User request subscription to data
UC7.2. 3. Platform validates selection
Consume 4. Platform redirects user to billing system
Proprietary 5. Billing system deals with user request
Data o If successful, user is redirected to a GUI where data is ready to use
o If unsuccessful, user can try payment again or cancel request
2. End of data subscription.
1. Platform provides user with an interface for commercial data
management
2. User chooses to edit or delete commercial data
o If edit, user revise commercial models, licenses, access-control,
semantics;
o If delete, user selects dataset(s) to be removed (following policies
UC7.3. Manage defined in the platform for data removal)
commercial 3. User confirms action
Data 4. Platform promptly process users request
5. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
6. End of resources management
1. Platform provides user with an interface for data subscription
management
2. User chooses to edit or cancel data subscription
o If edit, user revise payment and subscription timeframe;
o If cancel, user selects dataset(s) to have subscription cancelled
UC7.4. Manage (following policies defined in the platform for data subscription)
data 3. User confirms action
subscription 4. Platform promptly process users request
5. Platform confirms execution of request
o If valid request, platform updates acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
6. End of data subscription management
2.5.2 Use Case: Commercialise Data Services
ID: UC8
Refines: SUB-GOAL 3: City data is orchestrated in a marketplace
Pre-condition: Data is successfully published in the platform, both publisher and consumers of
city data are authenticated in the platform, and there are billing capabilities available.
Actors: Data Service Providers, Data Publishers and Consumers
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 26
Rationale: After deploying data services in the platform, the providers of data services can choose
to commercialise services based on the policies and financial models defined in the platform. Once
data service providers define which service(s) is (are) to be commercially exploited, the platform
will associate the services with their respective financial models and let available in the platform
applications module ready for use. User (either publisher or consumer) chooses which data
services to use, and in case a charged service is selected the platform redirects the user to a
billing interface where the payment is taken. The platform must provide an update response
indicating the status of the payment. If successful, service is ready available to be used, otherwise
the user can re-try the payment or cancel transaction. Note that data service owners should be
able to waive the payment of tariff to certain users categories.
Data services providers are offered with functions to manage their commercial services. The
platform provides functions for updating, maintaining and accessing both service and its respective
commercial transactions. Furthermore, it enables data services providers to track the usage of
services by users. Services usage tracking includes performing queries on the platform to
generate result sets, and producing reports from these result sets. The consumers of data services
are provided with functions which enable them to manage their subscriptions and financial
transactions. These functions include updating, maintaining and accessing financial transactions.
For all these functions and services, the platform must provide a database update response
indicating the status of the update, avoid update errors to be propagated in the platform, and
should keep an audit trail of all actions to enable rollback.
Specialised Use Cases: The Use Case Commercialise Data Services is distinguished into four
specialised Use Cases: “Commercialise Data Services (UC8.1)”, “Consume Data Services
(UC8.2)”, “Manage Commercial Services (UC8.3)” and “Manage Services Subscription (UC8.4)”.
Refines into requirements: FREQ 68 to 73.
Use Case Basic Interactions and Responses
1. User selects the data services to be commercialised
2. User selects the commercial model for data service usage based on the
category of users
UC8.1. 3. Platform validates selection
Commercialise 4. Platform associates data services to subscription model
Data Services 5. Platform enables data service to be commercially exploited in the
marketplace
6. End of services commercialisation set up.
1. User selects the data service to be subscribed to
2. User request subscription to service
3. Platform validates selection
UC8.2. 4. Platform validates users category its respective commercial models
Consume Data 4. If applicable, platform redirects user to billing system
Services 5. Billing system deals with users request
o If successful, user is redirected to a GUI where data is ready to use
o If unsuccessful, user can try payment again or cancel request
6. End of data subscription.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 27
1. Platform provides user with an interface for commercial data
management
2. User chooses to edit or delete commercial data
o If edit, user revise commercial models, licenses, access-control,
semantics;
o If delete, user selects dataset(s) to be removed (following policies
UC8.3. Manage defined in the platform for data removal)
commercial 3. User confirms action
services 4. Platform promptly process users request
5. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
6. End of resources management
1. Platform provides user with an interface for data subscription
management
2. User chooses to edit or cancel data subscription
o If edit, user revise payment and subscription timeframe;
o If cancel, user selects dataset(s) to have subscription cancelled
UC8.4. Manage (following policies defined in the platform for data subscription)
services 3. User confirms action
subscription 4. Platform promptly process users request
5. Platform confirms execution of request
o If valid request, platform updates acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
6. End of data subscription management
2.5.3 Functional Requirements
Req. ID UC. ID Description Priority Domain
City Data, Platform,
FREQ.63 UC7 Support the commercialization of city data Should Business Needs
City Data, Platform,
FREQ.64 UC7 Enable users to subscribe to city data through the payment of a tariff Should Business Needs
City Data, Platform,
FREQ.65 UC7 Enable users to manage their data subscriptions Should Business
Provide platform providers mechanisms to define the terms and
FREQ.66 UC7 Must Platform Needs
conditions for platform data usage
Enable data providers to manage the subscription models of their City Data, Platform,
FREQ.67 UC7 Should
data Business
UC7/ City Data, Platform,
FREQ.68 Utilise secure and reliable billing and payment management systems Must
UC8 Business
City Data, Platform,
FREQ.69 UC8 Support the commercialization of data services Should Business Needs
Enable data providers to manage the commercial models of their City Data, Platform,
FREQ.70 UC8 Should
services Business Needs
Provide service providers mechanisms to define the terms and City Data, Platform,
FREQ.71 UC8 Should
conditions of platform services Business Needs
Allow users to pay a tariff for using certain advanced services (e.g. City Data, Platform,
FREQ.72 UC8 Should
Data manipulation, enrichment) Business Needs
City Data, Platform,
FREQ.73 UC8 Enable users to manage their data services subscriptions Should Business Needs
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 28
2.6 SUB-GOAL 4: City data is offered in an accessible manner
Rationale: The urban platform provides city data in both human and machine (e.g. sensors,
actuators, systems) readable and understandable formats.
Drivers: Ensure data understandability and machine-to-machine data transaction.
Actions: For Sub-Goal 3 to be maintained in the long-run it requires the efficient realisation of use
cases: “Register Consumer”, “Discover City Data”, and “Consume City Data” as shown in Figure 8.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
Maintain [City data is offered in an
accessible manner]
S ub-Goal 1
Achieve [Register Consumer] Achieve [Discover City Data] Achieve [Consume City Data]
UC9 UC10 UC11
Figure 8. Sub-Goal 4 “City data is offered in an accessible manner” refinement.
2.6.1 Use Case: Register Data Consumer
ID: UC9
Refines: SUB-GOAL 4: City data is offered in an accessible manner
Pre-condition: User is not logged in the platform
Actors: Data Consumers
Rationale: Data Consumers can register in the platform and request approval to consume city data
via GUI or APIs. They provide valid registration details (to be defined) and wait for platform to
confirm their registration. Users must accept the terms and conditions of platform usage and define
how their personal data can be used by the Platform Owner. Users can manage and alter their
registration information at any time they want to.
Refines into requirements: FREQ 64.
Use Case Basic Stimulus and Responses
1. The platform prompts the user for a username and password or register
new account.
2. The user selects registration option.
3. The platform prompts user for data consumer registration information
UC1. Register 4. The user enters in their information.
Consumer 5. Platform verifies information and creates account.
o If non-valid information, platform shows error message and returns to
step 1.
6. Platform acknowledges registration has been successful
7. End of registration
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 29
2.6.2 Use Case: Discover City Data
ID: UC10
Refines: SUB-GOAL 4: City data is offered in an accessible manner
Pre-condition: User has access to either platform GUI or API
Actors: Data Consumers
Rationale: Data Consumers can register in the platform and request approval to consume city data
via GUI or APIs. They provide valid registration details (to be defined) and wait for platform to
confirm their registration.
Refines into requirements: FREQ 64.
Use Case Basic Stimulus and Responses
1. Users access specialised data query end-points (e.g. SPARQL)
2. Users provides information for pre-defined parameters for search
3. Users request data search
4. Platform quickly process users request for data

View File

@@ -1,604 +0,0 @@
Consume City o If authentication is successful, users are provided with requested
Data via APIs data streams
3. Users are provided with requested data via APIs
2.6.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
Allow users to register to use services and consume proprietary city Societal Needs,
FREQ.74 UC9 Should
data and open data (optional) Platform
Keep sensitive information secured and accessible only to Societal Needs,
FREQ.75 UC9 Should
authorized users Platform
Societal Needs,
FREQ.76 UC9 Provide authentication mechanisms for users Must Platform
Societal Needs,
FREQ.77 UC9 Keep users personal information protected Must Platform
Allow users to control which data they are willing to provide and how Societal Needs,
FREQ.78 UC9 Must
their data should be used Platform
Societal Needs,
FREQ.79 UC10 Allow users to format data in any supported data formats Must
Platform
The query request may require data to be sourced from different Societal Needs,
FREQ.80 UC10 Must
storage locations Platform
Allows query requests against all metadata used to manage the Societal Needs,
FREQ.81 UC10 repository. Should Platform
Provide users information about the legal aspects of the data Societal Needs,
FREQ.82 UC11 Must
(license, ownership) City Data
Societal Needs,
FREQ.83 UC11 Keeps an audit trail of all actions. Must City Data
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 31
2.7 SUB-GOAL 5: Users experience is enhanced by the provision of
value-added services
Rationale: The urban platform enables users to consume and publish data in a secure and privacy
protected manner
Drivers: Ensure data is secured and the identity of users are preserved
Actions: For Sub-Goal 5 to be maintained in the long-run it requires the efficient realisation of use
cases: “Deploy Data Services”, “Manage Services”, and “Utilise Data Services” as shown in Figure
8.
Achieve [City data is exploited
to its full effect]
Goal
co-ena bles
Maintain [User s experience is enhanced by
the provision of value-added services]
S ub-Goal 1
Achieve [Deploy Data Services] Achieve [Manage Services] Achieve [Utilise Data Services]
UC12 UC13 UC14
Figure 9. Sub-Goal 5 “Users experience is enhanced by the provision of value-added services” refinement
2.7.1 Deploy Data Services
ID: UC12
Refines: SUB-GOAL 5: Users experience is enhanced by the provision of value-added services
Pre-condition: Data Services Providers are provided with credentials, are authorised to deploy
their services in the platform, and have access to technical specifications of the platform interfaces.
Actors: Data Services Providers
Rationale: Data Services can register in the platform and request approval to deploy services in
the platform. They provide valid registration details (to be defined) and wait for registration
confirmation. Platform Providers may authorise or not data services providers to offer both open
and proprietary services in the platform. Data services providers must formally agree with service
deployment agreement with the Urban Platform. This agreement defines terms of the content,
policies, regulations, license agreement. The Urban Platform will proactively work with Service
Providers to agree on the technical specifications of interfaces and platform openness level.
Agreements between Platform and Data Service Providers may be renegotiated on a periodic or
ad-hoc basis.
Refines into requirements: FREQ.1 to FREQ.5.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 32
Use Case Basic Stimulus and Responses
1. User authenticates in the platform
2. User is provided access to platform interfaces for service deployment
3. User deploy services
4. Platform checks compatibility and any technical issues arising from the
UC12. Deploy new service
Services 5. If approved the service is ready to be used and managed in the
platform
o If deployment is not approved, platform shows error message and
returns to step 1.
6. End of deployment
2.7.2 Use Case: Manage Services
ID: UC13
Refines: GOAL 5: Users experience is enhanced by the provision of value-added services
Pre-condition: User successfully authenticates in the platform
Actors: Data Services Providers
Rationale: Manage services provides the functions for updating, maintaining and accessing
services as well as tracking their usage by users. Ideally the owners of the services should be the
only authorised user to manage them, and other authorised users can track the usage of the
services in the platform. In case of updates the platform must log in the database update details
and send to service providers a response indicating the status of the update. The platform should
also ensure update errors are not propagated nor affect the health of other services provided in the
platform, and should keep an audit trail of all actions to enable rollback. Data services usage
tracking includes performing queries on the data management data to generate result sets, and
producing reports from these result sets. All users information provided to service providers must
follow regulations of data protection and the users defined rules for their data use.
Specialised Use Cases: The Use Case Manage Services is distinguished into two specialised
Use Cases: “User manages services (UC13.1)” and “User tracks service usage (UC13.2)”.
Subordinated Use Cases: “Transmit Data (UC5)”
Refines into requirements: FREQ.27 to FREQ.25.
Specialised Use Cases Basic Interactions and Responses
1. Platform provides user with an interface for services management
2. User chooses to edit or delete services
3. If edit, user revise service information (access-control,
commercial models, parameters) and deployment;
If delete, user selects services to be removed / disabled
UC13.1. User manages 4. User confirms action
services 5. Platform quickly process users request
6. Platform confirms execution of request
o If valid request, platform acknowledges request has been
processed successfully
o If non-valid request, platform returns to step 1.
7. End of services management
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 33
1. Platform provides user with an interface for services management
2. User chooses to visualise usage information of a service
3. Platform quickly process users request for data usage
UC13.2. User tracks information
services usage 4. Platform provides user with statistical information about services
usage and data users anonymised information
5. End of data services tracking.
2.7.3 Use Case: Utilise Data Services
ID: UC2
Refines: SUB-GOAL 1 - City data is collected in an intelligent manner
Pre-condition: User is authenticated in the platform
Actors: City data publisher
Rationale: Data Consumers can register in the platform and request approval to consume city data via GUI
or APIs. They provide valid registration details (to be defined) and wait for platform to confirm their
registration.
Subordinated Use Cases: “Commercialise Data Services (UC8)”
Refines into requirements: FREQ 6 to FREQ.26.
Specialised Use Cases Basic Interactions and Responses
1. Users / Machines select data service to be utilised
2. Users / Machines are redirected to authentication mechanism in
UC2.1. User utilises data case of registration is needed for the particular service
services o If authentication is successful, users are provided with
requested data streams
3. Users are provided with requested service either via API or GUI
2.7.4 Functional Requirements
Req. ID UC. ID Description Priority Domain
Provide stable and well-defined interfaces to ensure interoperability
Societal Needs,
FREQ.84 UC12 between the platform, services and the applications provided by Should Platform
services providers
Ensure the interfaces of the architecture are open to reduce entry Societal Needs,
FREQ.85 UC12 Should
barriers and integration issues Platform
Provide multi-purposed and network intelligent interfaces to Societal Needs,
FREQ.86 UC12 Must
providers and consumers of services Platform
Provide service providers mechanisms to define the terms and Societal Needs,
FREQ.87 UC12 Must
conditions of platform services deployment Platform
Business Needs,
FREQ.88 UC13 Provide statistical information of users feedback on service usage Must Platform
Allow users to use services to manipulate city data (e.g. Create Societal Needs,
FREQ.89 UC10 Must
mash ups, integrate) Platform
Allow users to provide feedback on usability, and quality of data Societal Needs,
FREQ.90 UC14 Must
and services provided by the platform Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 34
3. Non-functional Requirements
3.1 Run-time Quality Requirements
3.1.1 Scalability Requirements
Rationale: The ability of the system to execute its task within its expected performance profile and
to handle on-demand increased processing volumes of data and service requests
Drivers: Provide ready access to all data that underpins decision making processes in smart cities,
and accommodate users and data usage patterns
Refines into requirements: NFREQ.1, NFREQ.2, NFREQ.3, NFREQ.4
Relevance: Urban platforms have ambitious performance requirements. Such platforms must cope
with users demand for data and services, capture real-time data that will be catalysed by a myriad
of sensors. The demand for urban platform is very likely to significantly increase over time. It is
very difficult to have clear performance characteristics due to the ubiquity, heterogeneity high
connectivity of devices and end users.
SCALABILITY MEASURES
Actions Capture the performance requirements
Create service level agreements
Predict scalability using software simulation
Analyse the performance of the platform overtime
Conduct practical testing
Strategy Prioritize service and data requests
Distribute processing over time
Scale up or scale out as necessary
Reuse resources and results Partition and parallelize
Constantly monitor Quality of Service at runtime
3.1.2 Availability and Reliability Requirements
Rationale: The ability of the system to be fully or partly operational as and when required and to
effectively handle failures that could affect system availability
Drivers: Build a reliable foundation for “on demand” exploitation of data
Refines into requirements: NFREQ.5, NFREQ.6, NFREQ.7, NFREQ.8
Relevance: Any system that has complex or extended availability requirements, complex recovery
processes, or a high profile (e.g., is visible to the public)
AVAILABILITY AND RELIABILITY MEASURES
Actions Capture the availability requirements Produce the availability schedule
Estimate platform availability Estimate functional availability Assess against the
requirements Rework the architecture
Strategy Adopt fault-tolerant hardware
Use reliable infrastructure database
Log transactions
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 35
Develop adaptive software to cope and recover from faults
Design and test for failure
Deploy load balancing
Identify suitable backup and disaster recovery solution
3.1.3 Trust Requirements
Rationale: A quality related to the users belief in the reliability, integrity and ability of the functional
behaviour of the platform
Drivers: Gain understanding of what influences users experience while interacting with services
provided
Requirements: NFREQ.16, NFREQ.17
Relevance: Relevant to the systems that share and collect information that may raise public
concern. In some cases, trust has to do with the reliability of data and their providers, whereas in
other cases trust can be associated with the security and privacy of the technology that was
deployed. Trust affects the reputation of the platform besides its dissemination and maturity on the
market.
TRUST MEASURES
Capture trust requirements
Perform risk analysis so measures can be implemented
Actions Explore the vulnerability aspects of city data
Check whether extensibility requirements impact on trust
Define a trust model
Adopt trust model
Deploy monitoring capabilities and assess its impact on scalability
Manage the data in a way that ensures its compliance with data
Strategy protection regulations
Implement tampering and data misuse detection
Make use of Cryptography when necessary
Allow Federation of trust between platforms
3.1.4 Security Requirements
Rationale: Ability of the system to enforce the intended confidentiality, trust, integrity and service
and data access policies, and to detect and recover from failure in these security mechanisms.
Drivers: Manage the data and services in a way that ensures its integrity, and compliance with
data protection regulations
Relevance: Relevant to the systems that share and collect information that may raise public
concern. Urban platforms may become a valuable target for attackers which can potentially leave
huge swathes of information exposed. It could potentially undermine trust in the government and
damage its reputation.
Refines into requirements: NFREQ.9 to NFREQ.15
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 36
SECURITY MEASURES
Elicit the security requirements
Cross-check security requirements impact on services offered by different
providers
Verify security impact on service composition
Conduct risk analysis and impacts of security breach
Use scalable and efficient user authentication components
Use authentication and authorization components to secure interfacing with
Actions external services
Address aspects of data collection, storage and distribution
Address aspects of service and communication security among devices
Identify security requirements of the physical infrastructure
Balance and prioritize scalability (performance) and security requirements
Evaluate trade-offs between privacy considerations and preventing abuse. (While
anonymous access guarantees privacy of users, traceability of users due to abuse
of the service is not possible.)
Toughen users functional components
Authenticate subjects
Define and enforce access policies
Secure communication infrastructures
Secure interfaces with external systems and services
Strategy
Secure databases
Check data integrity for critical services
User digital certificates and encryption where necessary
Secure monetary transactions
Secure users personal information
3.1.5 Privacy Requirements
Rationale: Ability of the system to ensure that the collection and transmission of personal data is
minimized and handled in accordance with users expectation and regulations.
Drivers: Protect the vulnerability aspects volunteered citizens data
Requirements: NFREQ.18, NFREQ.19, NFREQ.20, NFREQ.21, NFREQ.22
Relevance: Ensuring users privacy is protected positively influences users experience,
acceptance and continuous use of the platform. Besides other factors, the reputation of the
platform depends on how well users information is secure and preserved.
PRIVACY MEASURES
Capture trust requirements
Perform risk analysis so measures can be implemented
Actions Explore the vulnerability aspects of city data
Check whether extensibility requirements impact on trust
Define a trust model
Allow users to interact with the platform anonymously in given
circumstances
Manage users identification securely
Strategy Anonymize data whenever necessary to comply with data regulations
Cryptograph personal identifiers during data transmission when that is
necessary
Avoid unauthorized access to implicit information (e.g. location)
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 37
Verify the impact of security, trust and scalability requirements trade-offs
on privacy
Allow the user to control how personal information is used
Empower user to control the data disclosure mechanism
3.2 Non Run-time Quality Requirements
3.2.1 Evolvability Requirements
Rationale: The ability of the platform to withstand and easily adapt when new requirements and
changes is introduced.
Drivers: Ensure the platform is able to accommodate additional functionality and emerging
technologies at later stage at a fair and transparent cost
Refines into requirements: NFREQ.23
Relevance: Important for longer- lived and more widely used systems. Urban platforms are
expected to be highly evolvable in order to accommodate future emerging technologies and avoid
interoperability issues.
EVOLVABILITY MEASURES
Actions Characterize and assess the evolution needs
Consider the evolution trade-offs
Balance and negotiate potential conflicting requirements emerging from
the evolution capability.
Strategy Adopt standards and open interfaces
Design loose-coupled components
Preserve the platform resilience
3.2.2 Extensibility Requirements
Rationale: The flexibility of the system to allow services and functionality to be extended and
augmented by service providers in order to increase value of services to both platform providers
and end-users. The extension of the platform services is determined by the Platform Openness
strategy.
Drivers: Stakeholders can extend the services provided by the urban platform, so that
partnerships can be built to deliver holistic and interoperable solutions. Identify integrated
approaches to design and service delivery which ensures that services fit together and that
synergies can be exploited.
Refines into requirements: NFREQ.24, NFREQ.25
Relevance: Important for longer- lived and more widely used systems. Urban platforms are
expected to be highly evolvable in order to accommodate future emerging technologies and avoid
interoperability issues.
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 38
EXTENSIBILITY
Actions Characterize and assess the extensibility needs
Trace the impacts and cost of extensions
Negotiate potential conflicting requirements emerging from adding extensions
in the platform
Strategy Develop a modular architecture with standard interfaces that reduces entry
barriers due to increased transparency and integration
Share technical information about interfaces will help service providers in
targeting opportunities around the platform
3.3 List of Non-Functional Requirements
ID # Description Concern Priority Domain
Platform,
NFREQ.1 Support different service level agreements (SLA) Scalability Should
Infrastructure
Platform,
NFREQ.2 Process services and events on a set of distributed nodes Scalability Should
Infrastructure
Platform,
NFREQ.3 Continuously monitor Quality of Service at runtime Scalability Should
Infrastructure
Platform,
NFREQ.4 Balance its load at runtime Scalability Should
Infrastructure
Platform,
NFREQ.5 Provide high availability Availability Should
Infrastructure
Platform,
NFREQ.6 Guarantee infrastructure availability Availability Should
Infrastructure
Platform,
NFREQ.7 Ensure network availability Availability Should
Infrastructure
Platform,
NFREQ.8 Be able to perform self-healing Availability Should
Infrastructure
City Data,
NFREQ.9 Expose data and services to authorized users Security Must
Platform
City Data,
NFREQ.10 Ensure services are always accessible to entitled users Security Must
Platform
NFREQ.11 Ensure Data Freshness Security Must Platform
NFREQ.12 Support access control mechanisms Security Must Platform
Platform,
NFREQ.13 Have security mechanisms to protect data transmission Security Should
Infrastructure
Platform,
NFREQ.14 Make it difficult to spy on communicated messages Security Should
Infrastructure
Platform,
NFREQ.15 Be able to perform to detect threats at runtime Security Should
Infrastructure
Provide trusted and secure communication and information Platform,
NFREQ.16 Trust Should
management Infrastructure
NFREQ.17 The platform infrastructure and services shall be trustable Trust Should Infrastructure
Societal Needs,
NFREQ.18 Allow users to use free services anonymously Privacy Should
Platform
Societal Needs,
NFREQ.19 Allow people to use free services anonymously Privacy Should
Platform
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 39
Allow users to control which data they are willing to provide Societal Needs,
NFREQ.20 Privacy Should
and how their data should be used Platform
NFREQ.21 Keep users access-control rights/ policies secured. Privacy Should Platform
Provide privacy protection for users interacting with the Societal Needs,
NFREQ.22 Privacy Should
platform Platform
Platform,
NFREQ.23 Provide communication confidentiality Privacy Should
Infrastructure
Societal Needs,
NFREQ.24 Be extensible for future technologies. Evolvability Must City Data, Platform,
Infrastructure
Platform,
NFREQ.26 Provide standard interfaces for service providers Extensibility Should
Business Needs
NFREQ.26 Be able to provide services in an interoperable manner Extensibility Should Platform
4. Other Requirements
4.1 Minimal Descriptive Metadata Required from Data Provider
Minimal Descriptive Metadata Required from Data Provider
Attribute Required Definition Notes Examples
Database Name Y A name given to the resource
Author Name of a person or body associated with
Y
the creation of the resource
Maintainer Name of the entity responsible for making
Y
the resource available
Date Created Y Date the dataset was created
Date Modified Y Date the dataset was modified
Last Revision Y Date the dataset was last revised
Update Frequency Y Frequency of data maintenance
Mode of Release Y Open/Proprietary
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 40
4.2 Urban Platform Supported Data Formats
MIME type Description Extensions Level
application/json JavaScript Object Notation json supported
application/xml eXtensible markup language file xml supported
text/csv Comma separated values file csv supported
5. Conclusion & Forward Plans
This document represents the first set of requirements specification for urban platform. Future
activities will collaboratively assess, resolve requirements conflicts, prioritize, and validate the
requirements of the urban platform. Ultimately, this document will become a complete final
requirements specification document to guide and speed up the development open platform for
cities. Table below shows the milestones, deliverables and engagement activities completed and
yet to be completed by the Demand Side Engagement Stream.
Milestones, deliverables and engagement activity Forecast date Status
Stakeholders invitation Late Completed
September/2015
Online Workshop 1: Project Scope and Outcomes 20/11/2015 Completed
Online meeting held with city members to communicate goals,
expected outcomes, time commitment and required actions.
Start of Collaborative Requirements Engineering Process 20/11/2015 Completed
Participants reviewed the first draft of urban platform requirements,
provided comments, and suggested new requirements and changes.
Online Workshop 2: Review and Refine Requirements 08/12/2015 Completed
Participants had a conference call to review the requirements
specification document and provide their comments.
Deliverable: Requirements Specification Document v2.1 shared 05/01/2016 Completed
across the Working Streams for consultation
Online Workshop 3: Review Requirements Specification 12/01/2016 Completed
Document and discussion of Letter of Intent
Participants had a conference call to review the requirements
specification document and provide their comments, and discussed
the Letter of Intent to be signed by Mayor/Equiv of EU cities to
commit to application of common U.P. approach.
Urban Platforms Workshop in Brussels 19/01/2016 Completed
Participants provided feedback on the requirements specification
document and recommended changes in the document
Requirements Specification for Urban Platforms (EIP_SCC Initiative) Page 41
Deliverable: Updated Requirements Specification Document v2.2 29/01/2016 Completed
shared with Industry and Standardisation Working Streams
Deliverable: Online release of the Requirements Specification Early
Document and open calls for wider consultation on the Requirements February/2016
Online Workshop 4: Engagement with Industry Side Early
Capture industry feedback on the Requirements Specification February/2016
Document to assess its suitability to drive the development of the first
draft reference architecture.
Online Workshop 5: Review Requirements Specification Mid
Document v2.2 and discuss requirement prioritization and February/2016
balancing
Collectively determine which candidate requirements of urban
platforms should be prioritized as high importance to all the cities
(use of Value Oriented Prioritization Method). Requirements are also
prioritized to minimize risk during development of urban platforms so
that the most important requirements are made explicit and
considered in the reference architecture.
Capture of case studies from cities intended to support requirements Mid March/2016
validation (City of Porto), and learning and confidence building
amongst cities.

File diff suppressed because it is too large Load Diff

7
go.mod
View File

@@ -4,6 +4,7 @@ go 1.23
require (
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/openai/openai-go v1.12.0
modernc.org/sqlite v1.34.5
)
@@ -15,7 +16,11 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.22.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
golang.org/x/sys v0.29.0 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect

16
go.sum
View File

@@ -16,8 +16,20 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -38,8 +50,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

File diff suppressed because it is too large Load Diff

View File

@@ -46,3 +46,79 @@ func TestFormatRuntimeContextForPromptIncludesGOOS(t *testing.T) {
t.Fatalf("expected runtime context contains GOOS=%s, got: %s", runtime.GOOS, doc)
}
}
func TestMatchSkillsByNameExact(t *testing.T) {
all := []knowledge.Skill{
{Name: "SAFe PI Planning", Content: "PI规划技能"},
{Name: "文件系统查询专家", Content: "文件查询"},
{Name: "代码生成", Content: "代码生成技能"},
}
matched := matchSkillsByName(all, []string{"SAFe PI Planning"})
if len(matched) != 1 {
t.Fatalf("expected 1 match, got %d", len(matched))
}
if matched[0].Name != "SAFe PI Planning" {
t.Fatalf("expected SAFe PI Planning, got %s", matched[0].Name)
}
}
func TestMatchSkillsByNameFuzzy(t *testing.T) {
all := []knowledge.Skill{
{Name: "SAFe PI Planning", Content: "PI规划技能"},
{Name: "文件系统查询专家", Content: "文件查询"},
}
matched := matchSkillsByName(all, []string{"pi planning", "文件"})
if len(matched) != 2 {
t.Fatalf("expected 2 matches, got %d", len(matched))
}
}
func TestMatchSkillsByNameNoMatch(t *testing.T) {
all := []knowledge.Skill{
{Name: "文件系统查询专家", Content: "文件查询"},
}
matched := matchSkillsByName(all, []string{"不存在的技能"})
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
}
func TestMatchSkillsByNameEmpty(t *testing.T) {
matched := matchSkillsByName(nil, []string{"any"})
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
matched = matchSkillsByName([]knowledge.Skill{{Name: "test"}}, nil)
if len(matched) != 0 {
t.Fatalf("expected 0 matches, got %d", len(matched))
}
}
func TestSanitizeUserFacingAnswerExtractsFinalAnswer(t *testing.T) {
raw := "Thought: 先分析用户问题\nObservation: 已经有足够信息\nFinal Answer: 这是给用户的结果"
got := sanitizeUserFacingAnswer(raw)
if got != "这是给用户的结果" {
t.Fatalf("expected final answer only, got %q", got)
}
}
func TestSanitizeUserFacingAnswerDropsTraceLines(t *testing.T) {
raw := strings.Join([]string{
"Step 1 Thought: 检查上下文",
"Action: shell",
"Observation: ok",
"请执行以下变更。",
}, "\n")
got := sanitizeUserFacingAnswer(raw)
if got != "请执行以下变更。" {
t.Fatalf("expected user-facing text only, got %q", got)
}
}
func TestSanitizeUserFacingAnswerKeepsNormalAnswer(t *testing.T) {
raw := "1. 先打开配置文件\n2. 修改端口后重启服务"
got := sanitizeUserFacingAnswer(raw)
if got != raw {
t.Fatalf("expected answer unchanged, got %q", got)
}
}

View File

@@ -18,6 +18,7 @@ type Config struct {
ReactMaxSteps int
ToolCallTimeoutSec int
ToolOutputMaxChars int
PIPlanMaxChars int // PI 规划工具专用输出上限,独立于 TOOL_OUTPUT_MAX_CHARS
EnableCapabilityGap bool
AutoSkillDir string
GapDraftTriggerCount int
@@ -25,9 +26,11 @@ type Config struct {
Telegram TelegramConfig
Feishu FeishuConfig
WebUI WebUIConfig
LLM LLMConfig
Security SecurityConfig
WebSearch WebSearchConfig
Gitea GiteaConfig
SQLitePath string
}
@@ -45,10 +48,18 @@ type FeishuConfig struct {
EventPath string
}
type WebUIConfig struct {
ListenAddr string
MaxUploadBytes int64
ExposeReasoning bool
}
type LLMConfig struct {
BaseURL string
APIKey string
Model string
FileModel string
RouterModel string // 轻量路由模型,用于技能意图路由;为空则仅用关键词匹配
}
type SecurityConfig struct {
@@ -62,6 +73,13 @@ type WebSearchConfig struct {
APIKey string
}
type GiteaConfig struct {
BaseURL string // Gitea 实例地址
Token string // Personal Access Token
Owner string // 仓库所有者
Repo string // 仓库名称
}
func Load() (Config, error) {
agentWorkspaceDir := resolveAgentWorkspaceDir()
if err := preloadEnvFiles(); err != nil {
@@ -78,6 +96,7 @@ func Load() (Config, error) {
ReactMaxSteps: intFromEnv("REACT_MAX_STEPS", 0),
ToolCallTimeoutSec: intFromEnv("TOOL_CALL_TIMEOUT_SEC", 15),
ToolOutputMaxChars: intFromEnv("TOOL_OUTPUT_MAX_CHARS", 4000),
PIPlanMaxChars: intFromEnv("PI_PLAN_MAX_CHARS", 40000),
EnableCapabilityGap: boolFromEnv("ENABLE_CAPABILITY_GAP", true),
AutoSkillDir: defaultIfEmpty(os.Getenv("AUTO_SKILL_DIR"), filepath.Join(agentWorkspaceDir, "skills")),
GapDraftTriggerCount: intFromEnv("GAP_DRAFT_TRIGGER_COUNT", 3),
@@ -93,10 +112,17 @@ func Load() (Config, error) {
ListenAddr: defaultIfEmpty(os.Getenv("FEISHU_LISTEN_ADDR"), ":8080"),
EventPath: defaultIfEmpty(os.Getenv("FEISHU_EVENT_PATH"), "/feishu/events"),
},
WebUI: WebUIConfig{
ListenAddr: defaultIfEmpty(os.Getenv("WEBUI_LISTEN_ADDR"), ":8090"),
MaxUploadBytes: int64(intFromEnv("WEBUI_MAX_UPLOAD_MB", 20)) * 1024 * 1024,
ExposeReasoning: boolFromEnv("WEBUI_EXPOSE_REASONING", false),
},
LLM: LLMConfig{
BaseURL: strings.TrimRight(defaultIfEmpty(os.Getenv("LLM_BASE_URL"), "https://api.openai.com/v1"), "/"),
APIKey: strings.TrimSpace(os.Getenv("LLM_API_KEY")),
Model: defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini"),
FileModel: defaultIfEmpty(os.Getenv("LLM_FILE_MODEL"), defaultIfEmpty(os.Getenv("LLM_MODEL"), "gpt-4o-mini")),
RouterModel: strings.TrimSpace(os.Getenv("LLM_ROUTER_MODEL")),
},
SQLitePath: defaultIfEmpty(os.Getenv("SQLITE_PATH"), filepath.Join(defaultDataDir, "laodingbot.db")),
WebSearch: WebSearchConfig{
@@ -108,12 +134,18 @@ func Load() (Config, error) {
AllowedCommands: splitCSV(defaultIfEmpty(os.Getenv("ALLOWED_COMMANDS"), "pwd,ls,cat,echo,grep,find,head,tail,go")),
WorkDir: defaultIfEmpty(os.Getenv("WORK_DIR"), defaultWorkSubdir),
},
Gitea: GiteaConfig{
BaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("GITEA_BASE_URL")), "/"),
Token: strings.TrimSpace(os.Getenv("GITEA_TOKEN")),
Owner: strings.TrimSpace(os.Getenv("GITEA_OWNER")),
Repo: strings.TrimSpace(os.Getenv("GITEA_REPO")),
},
}
cfg.MessageChannel = strings.ToLower(strings.TrimSpace(cfg.MessageChannel))
cfg.LogLevel = strings.ToLower(strings.TrimSpace(cfg.LogLevel))
if cfg.MessageChannel != "telegram" && cfg.MessageChannel != "feishu" {
return Config{}, fmt.Errorf("MESSAGE_CHANNEL must be telegram or feishu")
if cfg.MessageChannel != "telegram" && cfg.MessageChannel != "feishu" && cfg.MessageChannel != "webui" {
return Config{}, fmt.Errorf("MESSAGE_CHANNEL must be telegram, feishu, or webui")
}
if cfg.LogLevel != "debug" && cfg.LogLevel != "info" && cfg.LogLevel != "warn" && cfg.LogLevel != "error" {
return Config{}, fmt.Errorf("LOG_LEVEL must be debug, info, warn, or error")
@@ -127,12 +159,18 @@ func Load() (Config, error) {
if cfg.ToolOutputMaxChars < 256 || cfg.ToolOutputMaxChars > 200000 {
return Config{}, fmt.Errorf("TOOL_OUTPUT_MAX_CHARS must be between 256 and 200000")
}
if cfg.PIPlanMaxChars < 1000 || cfg.PIPlanMaxChars > 500000 {
return Config{}, fmt.Errorf("PI_PLAN_MAX_CHARS must be between 1000 and 500000")
}
if cfg.GapDraftTriggerCount < 1 || cfg.GapDraftTriggerCount > 100 {
return Config{}, fmt.Errorf("GAP_DRAFT_TRIGGER_COUNT must be between 1 and 100")
}
if cfg.GapClusterLookbackHours < 1 || cfg.GapClusterLookbackHours > 24*365 {
return Config{}, fmt.Errorf("GAP_CLUSTER_LOOKBACK_HOURS must be between 1 and 8760")
}
if cfg.WebUI.MaxUploadBytes < 1024 || cfg.WebUI.MaxUploadBytes > 200*1024*1024 {
return Config{}, fmt.Errorf("WEBUI_MAX_UPLOAD_MB must be between 1 and 200")
}
if cfg.MessageChannel == "telegram" {
if cfg.Telegram.Token == "" {
@@ -152,6 +190,12 @@ func Load() (Config, error) {
}
}
if cfg.MessageChannel == "webui" {
if strings.TrimSpace(cfg.WebUI.ListenAddr) == "" {
return Config{}, fmt.Errorf("WEBUI_LISTEN_ADDR is required when MESSAGE_CHANNEL=webui")
}
}
if cfg.LLM.APIKey == "" {
return Config{}, fmt.Errorf("LLM_API_KEY is required")
}

View File

@@ -5,28 +5,75 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"strings"
"time"
"laodingbot/internal/config"
"laodingbot/internal/logger"
openai "github.com/openai/openai-go" // imported as openai
"github.com/openai/openai-go/option"
"github.com/openai/openai-go/packages/param"
"github.com/openai/openai-go/shared"
)
type Client interface {
Generate(ctx context.Context, systemPrompt, userPrompt string) (string, error)
}
type FileChatClient interface {
GenerateWithFiles(ctx context.Context, systemPrompt, userPrompt string, fileIDs []string) (string, error)
type PromptMessage struct {
Role string `json:"role"`
Content string `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
Name string `json:"name,omitempty"`
}
type MessageChatClient interface {
GenerateMessages(ctx context.Context, messages []PromptMessage) (string, error)
}
type FileUploader interface {
UploadFile(ctx context.Context, file InputFile, purpose string) (string, error)
}
// ToolCallChatClient 支持原生 function calling 的 LLM 客户端接口。
type ToolCallChatClient interface {
GenerateWithTools(ctx context.Context, messages []PromptMessage, tools []ToolDefinition) (*ChatCompletion, error)
}
// ToolDefinition 描述一个可供 LLM 调用的工具函数定义。
type ToolDefinition struct {
Type string `json:"type"`
Function ToolFunctionDef `json:"function"`
}
// ToolFunctionDef 是工具函数的名称、描述和参数 JSON Schema。
type ToolFunctionDef struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters json.RawMessage `json:"parameters,omitempty"`
}
// ToolCall 是 LLM 在响应中返回的工具调用请求。
type ToolCall struct {
ID string `json:"id"`
Type string `json:"type"`
Function ToolCallFunction `json:"function"`
}
// ToolCallFunction 包含工具调用的函数名和参数。
type ToolCallFunction struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
// ChatCompletion 是 LLM 响应的结构化表示,包含文本内容和可选的工具调用。
type ChatCompletion struct {
Content string
ToolCalls []ToolCall
}
type InputFile struct {
FileName string
MimeType string
@@ -34,178 +81,258 @@ type InputFile struct {
}
type OpenAICompatibleClient struct {
baseURL string
apiKey string
client openai.Client
model string
http *http.Client
disableThinkingParam bool
log *logger.Logger
}
func NewOpenAICompatibleClient(cfg config.LLMConfig, log *logger.Logger) *OpenAICompatibleClient {
opts := []option.RequestOption{
option.WithAPIKey(cfg.APIKey),
option.WithRequestTimeout(60 * time.Second),
}
if strings.TrimSpace(cfg.BaseURL) != "" {
opts = append(opts, option.WithBaseURL(cfg.BaseURL))
}
return &OpenAICompatibleClient{
baseURL: cfg.BaseURL,
apiKey: cfg.APIKey,
client: openai.NewClient(opts...),
model: cfg.Model,
http: &http.Client{Timeout: 60 * time.Second},
disableThinkingParam: shouldDisableThinkingParam(cfg.BaseURL),
log: log,
}
}
type chatRequest struct {
Model string `json:"model"`
Messages []chatMessage `json:"messages"`
}
type chatMessage struct {
Role string `json:"role"`
Content any `json:"content"`
}
type chatContentPart struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
FileID string `json:"file_id,omitempty"`
}
type chatResponse struct {
Choices []struct {
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
type fileUploadResponse struct {
ID string `json:"id"`
Bytes int64 `json:"bytes,omitempty"`
CreatedAt int64 `json:"created_at,omitempty"`
Filename string `json:"filename,omitempty"`
Object string `json:"object,omitempty"`
Purpose string `json:"purpose,omitempty"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Status any `json:"status,omitempty"`
StatusDetails any `json:"status_details,omitempty"`
Data *struct {
ID string `json:"id"`
} `json:"data,omitempty"`
Error *struct {
Message string `json:"message"`
} `json:"error,omitempty"`
}
func (c *OpenAICompatibleClient) Generate(ctx context.Context, systemPrompt, userPrompt string) (string, error) {
return c.generateInternal(ctx, systemPrompt, userPrompt, nil)
}
func (c *OpenAICompatibleClient) GenerateWithFiles(ctx context.Context, systemPrompt, userPrompt string, fileIDs []string) (string, error) {
return c.generateInternal(ctx, systemPrompt, userPrompt, fileIDs)
}
func (c *OpenAICompatibleClient) generateInternal(ctx context.Context, systemPrompt, userPrompt string, fileIDs []string) (string, error) {
if c.log != nil {
c.log.Debugf("llm request start model=%s system_len=%d user_len=%d file_count=%d", c.model, len(systemPrompt), len(userPrompt), len(fileIDs))
}
userContent := buildUserContent(userPrompt, fileIDs)
body := chatRequest{
Model: c.model,
Messages: []chatMessage{
messages := []PromptMessage{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userContent},
},
{Role: "user", Content: userPrompt},
}
b, err := json.Marshal(body)
return c.generateWithMessagesInternal(ctx, messages)
}
func (c *OpenAICompatibleClient) GenerateMessages(ctx context.Context, messages []PromptMessage) (string, error) {
return c.generateWithMessagesInternal(ctx, messages)
}
// GenerateWithTools 使用原生 function calling 发送请求,返回结构化的 ChatCompletion。
func (c *OpenAICompatibleClient) GenerateWithTools(ctx context.Context, messages []PromptMessage, tools []ToolDefinition) (*ChatCompletion, error) {
model := c.model
sdkMessages := buildSDKMessages(messages)
sdkTools := toSDKTools(tools)
if c.log != nil {
c.log.Debugf("llm tool-call request start model=%s messages=%d tools=%d", model, len(sdkMessages), len(sdkTools))
}
params := openai.ChatCompletionNewParams{
Model: shared.ChatModel(model),
Messages: sdkMessages,
}
if len(sdkTools) > 0 {
params.Tools = sdkTools
}
if c.log != nil {
if b, err := json.Marshal(params); err == nil {
c.log.Debugf("llm tool-call request params: %s", string(b))
}
}
resp, err := c.client.Chat.Completions.New(ctx, params, c.chatCompletionRequestOptions()...)
if err != nil {
return nil, fmt.Errorf("llm tool-call request failed: %w", err)
}
if len(resp.Choices) == 0 {
return nil, fmt.Errorf("llm returned empty choices")
}
choice := resp.Choices[0]
resultToolCalls := fromSDKToolCalls(choice.Message.ToolCalls)
if c.log != nil {
toolNames := make([]string, 0, len(resultToolCalls))
for _, tc := range resultToolCalls {
toolNames = append(toolNames, tc.Function.Name)
}
c.log.Infof("llm tool-call response success model=%s content_len=%d tool_calls=%d names=%v finish=%s",
model, len(choice.Message.Content), len(resultToolCalls), toolNames, choice.FinishReason)
}
return &ChatCompletion{
Content: choice.Message.Content,
ToolCalls: resultToolCalls,
}, nil
}
func (c *OpenAICompatibleClient) generateWithMessagesInternal(ctx context.Context, messages []PromptMessage) (string, error) {
model := c.model
baseMessages := normalizePromptMessages(messages)
if len(baseMessages) == 0 {
baseMessages = []PromptMessage{{Role: "user", Content: ""}}
}
systemLen, userLen := promptMessageLengths(baseMessages)
if c.log != nil {
c.log.Debugf("llm request start model=%s system_len=%d user_len=%d", model, systemLen, userLen)
}
sdkMessages := buildSDKMessages(baseMessages)
params := openai.ChatCompletionNewParams{
Model: shared.ChatModel(model),
Messages: sdkMessages,
}
resp, err := c.client.Chat.Completions.New(ctx, params, c.chatCompletionRequestOptions()...)
if err != nil {
if c.log != nil {
c.log.Errorf("marshal llm request failed err=%v", err)
c.log.Errorf("llm request failed err=%v", err)
}
return "", err
return "", fmt.Errorf("llm request failed: %w", err)
}
url := strings.TrimRight(c.baseURL, "/") + "/chat/completions"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(b))
if err != nil {
if len(resp.Choices) == 0 {
if c.log != nil {
c.log.Errorf("build llm request failed err=%v", err)
}
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
if c.log != nil {
c.log.Errorf("llm http request failed err=%v", err)
}
return "", err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
if c.log != nil {
c.log.Errorf("llm read response failed err=%v", err)
}
return "", err
}
var out chatResponse
if err := json.Unmarshal(raw, &out); err != nil {
if c.log != nil {
c.log.Errorf("llm response unmarshal failed status=%d err=%v", resp.StatusCode, err)
}
return "", err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if c.log != nil {
c.log.Errorf("llm bad status=%d", resp.StatusCode)
}
if out.Error != nil && out.Error.Message != "" {
return "", fmt.Errorf("llm error: %s", out.Error.Message)
}
return "", fmt.Errorf("llm error status: %d", resp.StatusCode)
}
if len(out.Choices) == 0 {
if c.log != nil {
c.log.Errorf("llm returned empty choices status=%d", resp.StatusCode)
c.log.Errorf("llm returned empty choices")
}
return "", fmt.Errorf("llm returned empty choices")
}
if c.log != nil {
c.log.Infof("llm response success model=%s output_len=%d", c.model, len(out.Choices[0].Message.Content))
}
return out.Choices[0].Message.Content, nil
content := resp.Choices[0].Message.Content
if c.log != nil {
c.log.Infof("llm response success model=%s output_len=%d", model, len(content))
}
return content, nil
}
func buildUserContent(userPrompt string, fileIDs []string) any {
trimmedPrompt := strings.TrimSpace(userPrompt)
if len(fileIDs) == 0 {
return userPrompt
}
// buildSDKMessages 将 PromptMessage 列表转换为 openai SDK 的消息格式。
func buildSDKMessages(base []PromptMessage) []openai.ChatCompletionMessageParamUnion {
out := make([]openai.ChatCompletionMessageParamUnion, 0, len(base))
parts := make([]chatContentPart, 0, len(fileIDs)+1)
if trimmedPrompt != "" {
parts = append(parts, chatContentPart{Type: "text", Text: userPrompt})
}
for _, id := range fileIDs {
id = strings.TrimSpace(id)
if id == "" {
for _, m := range base {
role := normalizeRole(m.Role)
if role == "" {
continue
}
parts = append(parts, chatContentPart{Type: "file", FileID: id})
out = append(out, toSDKMessage(m, role))
}
if len(parts) == 0 {
return userPrompt
return out
}
// toSDKMessage 将单个 PromptMessage 转换为 openai SDK 消息类型。
func toSDKMessage(m PromptMessage, role string) openai.ChatCompletionMessageParamUnion {
switch role {
case "system":
return openai.SystemMessage(m.Content)
case "user":
return openai.UserMessage(m.Content)
case "assistant":
if len(m.ToolCalls) > 0 {
sdkToolCalls := make([]openai.ChatCompletionMessageToolCallParam, 0, len(m.ToolCalls))
for _, tc := range m.ToolCalls {
sdkToolCalls = append(sdkToolCalls, openai.ChatCompletionMessageToolCallParam{
ID: tc.ID,
Function: openai.ChatCompletionMessageToolCallFunctionParam{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
})
}
return parts
msg := openai.AssistantMessage(m.Content)
msg.OfAssistant.ToolCalls = sdkToolCalls
return msg
}
return openai.AssistantMessage(m.Content)
case "tool":
return openai.ToolMessage(m.Content, m.ToolCallID)
default:
return openai.UserMessage(m.Content)
}
}
// toSDKTools 将内部 ToolDefinition 列表转换为 openai SDK 的 ChatCompletionToolParam 列表。
func toSDKTools(tools []ToolDefinition) []openai.ChatCompletionToolParam {
if len(tools) == 0 {
return nil
}
out := make([]openai.ChatCompletionToolParam, 0, len(tools))
for _, t := range tools {
var params shared.FunctionParameters
if len(t.Function.Parameters) > 0 {
_ = json.Unmarshal(t.Function.Parameters, &params)
}
out = append(out, openai.ChatCompletionToolParam{
Function: shared.FunctionDefinitionParam{
Name: t.Function.Name,
Description: param.NewOpt(t.Function.Description),
Parameters: params,
},
})
}
return out
}
// fromSDKToolCalls 将 openai SDK 响应中的 tool calls 转换为内部 ToolCall 类型。
func fromSDKToolCalls(sdkCalls []openai.ChatCompletionMessageToolCall) []ToolCall {
if len(sdkCalls) == 0 {
return nil
}
out := make([]ToolCall, 0, len(sdkCalls))
for _, tc := range sdkCalls {
out = append(out, ToolCall{
ID: tc.ID,
Type: "function",
Function: ToolCallFunction{
Name: tc.Function.Name,
Arguments: tc.Function.Arguments,
},
})
}
return out
}
func normalizePromptMessages(messages []PromptMessage) []PromptMessage {
out := make([]PromptMessage, 0, len(messages))
for _, m := range messages {
role := normalizeRole(m.Role)
if role == "" {
continue
}
out = append(out, PromptMessage{
Role: role,
Content: m.Content,
ToolCalls: m.ToolCalls,
ToolCallID: m.ToolCallID,
Name: m.Name,
})
}
return out
}
func normalizeRole(role string) string {
r := strings.ToLower(strings.TrimSpace(role))
if r != "system" && r != "user" && r != "assistant" && r != "tool" {
return ""
}
return r
}
func promptMessageLengths(messages []PromptMessage) (int, int) {
systemLen := 0
userLen := 0
for _, m := range messages {
switch normalizeRole(m.Role) {
case "system":
systemLen += len(m.Content)
case "user":
userLen += len(m.Content)
}
}
return systemLen, userLen
}
func (c *OpenAICompatibleClient) UploadFile(ctx context.Context, file InputFile, purpose string) (string, error) {
@@ -220,7 +347,6 @@ func (c *OpenAICompatibleClient) UploadFile(ctx context.Context, file InputFile,
if purpose != "" {
purposes = append(purposes, purpose)
}
// Provider compatibility fallback order.
purposes = appendIfMissing(purposes, "file-extract")
purposes = appendIfMissing(purposes, "batch")
@@ -242,77 +368,24 @@ func (c *OpenAICompatibleClient) UploadFile(ctx context.Context, file InputFile,
}
func (c *OpenAICompatibleClient) uploadFileOnce(ctx context.Context, file InputFile, purpose string) (string, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if err := writer.WriteField("purpose", purpose); err != nil {
return "", err
}
part, err := writer.CreateFormFile("file", file.FileName)
resp, err := c.client.Files.New(ctx, openai.FileNewParams{
File: bytes.NewReader(file.Content),
Purpose: openai.FilePurpose(purpose),
})
if err != nil {
return "", err
}
if _, err := part.Write(file.Content); err != nil {
return "", err
}
if err := writer.Close(); err != nil {
return "", err
return "", fmt.Errorf("llm file upload failed: %w", err)
}
url := strings.TrimRight(c.baseURL, "/") + "/files"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.http.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var out fileUploadResponse
if err := json.Unmarshal(raw, &out); err != nil {
return "", fmt.Errorf("llm file upload response decode failed: %w body=%s", err, clipForError(raw))
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
if strings.TrimSpace(out.Message) != "" {
return "", fmt.Errorf("llm file upload error: %s", out.Message)
}
if out.Error != nil && out.Error.Message != "" {
return "", fmt.Errorf("llm file upload error: %s", out.Error.Message)
}
return "", fmt.Errorf("llm file upload status: %d body=%s", resp.StatusCode, clipForError(raw))
}
fileID := strings.TrimSpace(out.ID)
if fileID == "" && out.Data != nil {
fileID = strings.TrimSpace(out.Data.ID)
}
fileID := strings.TrimSpace(resp.ID)
if fileID == "" {
return "", fmt.Errorf("llm file upload returned empty file id body=%s", clipForError(raw))
return "", fmt.Errorf("llm file upload returned empty file id")
}
if c.log != nil {
c.log.Infof("llm file uploaded name=%s size=%d file_id=%s purpose=%s status=%v", file.FileName, len(file.Content), fileID, purpose, out.Status)
c.log.Infof("llm file uploaded name=%s size=%d file_id=%s purpose=%s", file.FileName, len(file.Content), fileID, purpose)
}
return fileID, nil
}
func clipForError(raw []byte) string {
s := strings.TrimSpace(string(raw))
const max = 400
if len(s) <= max {
return s
}
return s[:max] + "...(truncated)"
}
func appendIfMissing(items []string, value string) []string {
value = strings.TrimSpace(value)
if value == "" {
@@ -325,3 +398,18 @@ func appendIfMissing(items []string, value string) []string {
}
return append(items, value)
}
func (c *OpenAICompatibleClient) chatCompletionRequestOptions() []option.RequestOption {
if !c.disableThinkingParam {
return nil
}
return []option.RequestOption{option.WithJSONSet("enable_thinking", false)}
}
func shouldDisableThinkingParam(baseURL string) bool {
baseURL = strings.ToLower(strings.TrimSpace(baseURL))
if baseURL == "" {
return false
}
return strings.Contains(baseURL, "dashscope.aliyuncs.com")
}

View File

@@ -0,0 +1,24 @@
package llm
import "testing"
func TestShouldDisableThinkingParam(t *testing.T) {
if !shouldDisableThinkingParam("https://dashscope.aliyuncs.com/compatible-mode/v1") {
t.Fatal("expected DashScope base URL to require enable_thinking=false")
}
if shouldDisableThinkingParam("https://api.openai.com/v1") {
t.Fatal("expected standard OpenAI base URL not to require enable_thinking=false")
}
}
func TestChatCompletionRequestOptions(t *testing.T) {
client := &OpenAICompatibleClient{disableThinkingParam: true}
if got := len(client.chatCompletionRequestOptions()); got != 1 {
t.Fatalf("expected 1 request option when disableThinkingParam=true, got %d", got)
}
client.disableThinkingParam = false
if got := len(client.chatCompletionRequestOptions()); got != 0 {
t.Fatalf("expected 0 request options when disableThinkingParam=false, got %d", got)
}
}

View File

@@ -8,8 +8,11 @@ import (
"laodingbot/internal/config"
"laodingbot/internal/logger"
"laodingbot/internal/tools"
"laodingbot/tools/filedoc"
"laodingbot/tools/fileoperation"
"laodingbot/tools/git"
"laodingbot/tools/giteaticket"
"laodingbot/tools/piplan"
"laodingbot/tools/shell"
"laodingbot/tools/websearch"
)
@@ -20,6 +23,9 @@ func RunChild(ctx context.Context, cfg config.Config, log *logger.Logger) error
var gitLog *logger.Logger
var shellLog *logger.Logger
var searchLog *logger.Logger
var fileDocLog *logger.Logger
var piPlanLog *logger.Logger
var giteaTicketLog *logger.Logger
var serverLog *logger.Logger
if log != nil {
log.Infof("toolhost child starting")
@@ -28,6 +34,9 @@ func RunChild(ctx context.Context, cfg config.Config, log *logger.Logger) error
gitLog = log.WithComponent("toolhost.git")
shellLog = log.WithComponent("toolhost.shell")
searchLog = log.WithComponent("toolhost.websearch")
fileDocLog = log.WithComponent("toolhost.filedoc")
piPlanLog = log.WithComponent("toolhost.piplan")
giteaTicketLog = log.WithComponent("toolhost.giteaticket")
serverLog = log.WithComponent("toolhost.server")
}
registry := tools.NewRegistry(registryLog)
@@ -53,6 +62,27 @@ func RunChild(ctx context.Context, cfg config.Config, log *logger.Logger) error
cfg.ToolOutputMaxChars,
searchLog,
))
registry.Register(filedoc.New(
filedoc.Config{
APIKey: cfg.LLM.APIKey,
BaseURL: cfg.LLM.BaseURL,
Model: cfg.LLM.FileModel,
Timeout: time.Duration(cfg.ToolCallTimeoutSec) * time.Second,
},
cfg.ToolOutputMaxChars,
fileDocLog,
))
registry.Register(piplan.New(cfg.PIPlanMaxChars, piPlanLog))
registry.Register(giteaticket.New(
giteaticket.Config{
BaseURL: cfg.Gitea.BaseURL,
Token: cfg.Gitea.Token,
Owner: cfg.Gitea.Owner,
Repo: cfg.Gitea.Repo,
Timeout: time.Duration(cfg.ToolCallTimeoutSec) * time.Second,
},
giteaTicketLog,
))
server := NewServer(registry, serverLog)
if err := server.Serve(ctx, stdin(), stdout()); err != nil && ctx.Err() == nil {

View File

@@ -0,0 +1,492 @@
package webui
import (
"context"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
"sync/atomic"
"time"
"laodingbot/internal/config"
"laodingbot/internal/llm"
"laodingbot/internal/logger"
"laodingbot/internal/memory"
"strconv"
)
type IncomingMessage struct {
ChatID string
UserID string
Text string
}
// StreamEventType 定义流式输出的事件类型
type StreamEventType string
const (
StreamEventTypeThought StreamEventType = "thought" // LLM 思考过程
StreamEventTypeToolCall StreamEventType = "tool_call" // 工具调用请求
StreamEventTypeToolResult StreamEventType = "tool_result" // 工具执行结果
StreamEventTypeFinal StreamEventType = "final" // 最终答案
StreamEventTypeError StreamEventType = "error" // 错误信息
StreamEventTypeWorkspaceStart StreamEventType = "workspace_start" // 工具渲染开始
StreamEventTypeWorkspaceDelta StreamEventType = "workspace_delta" // 工具渲染增量内容
StreamEventTypeWorkspaceEnd StreamEventType = "workspace_end" // 工具渲染结束
)
// StreamEvent 代表流式输出中的一个事件
type StreamEvent struct {
Type StreamEventType `json:"type"`
Content string `json:"content"`
Step int `json:"step,omitempty"`
ToolName string `json:"tool_name,omitempty"`
WorkspaceTitle string `json:"workspace_title,omitempty"` // 仅用于 workspace_start 类型
}
type ChatHandler func(context.Context, IncomingMessage) (string, error)
type StreamChatHandler func(context.Context, IncomingMessage, StreamEventCallback) (string, error)
type StreamEventCallback func(event StreamEvent) error
type UploadHandler func(context.Context, string, string, []llm.InputFile) ([]string, error)
type HistoryHandler func(context.Context, string, int) ([]memory.Message, error)
type Bot struct {
listenAddr string
maxUploadBytes int64
log *logger.Logger
chatHandler ChatHandler
streamChatHandler StreamChatHandler
uploadHandler UploadHandler
historyHandler HistoryHandler
counter uint64
}
type chatRequest struct {
Text string `json:"text"`
SessionID string `json:"session_id"`
UserID string `json:"user_id"`
}
func (r *chatRequest) UnmarshalJSON(data []byte) error {
type rawChatRequest struct {
Text string `json:"text"`
SessionID string `json:"session_id"`
SessionIDCamel string `json:"sessionId"`
UserID string `json:"user_id"`
UserIDCamel string `json:"userId"`
}
var raw rawChatRequest
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
r.Text = raw.Text
r.SessionID = firstNonEmpty(raw.SessionID, raw.SessionIDCamel)
r.UserID = firstNonEmpty(raw.UserID, raw.UserIDCamel)
return nil
}
type chatResponse struct {
Reply string `json:"reply"`
SessionID string `json:"session_id"`
UserID string `json:"user_id"`
}
type uploadResponse struct {
FileID string `json:"file_id"`
FileIDs []string `json:"file_ids"`
FileName string `json:"file_name"`
MimeType string `json:"mime_type"`
SizeBytes int `json:"size_bytes"`
SessionID string `json:"session_id"`
UserID string `json:"user_id"`
}
type errorResponse struct {
Error string `json:"error"`
}
func NewBot(cfg config.WebUIConfig, log *logger.Logger) (*Bot, error) {
if strings.TrimSpace(cfg.ListenAddr) == "" {
return nil, fmt.Errorf("empty webui listen address")
}
if cfg.MaxUploadBytes <= 0 {
return nil, fmt.Errorf("invalid webui max upload bytes")
}
return &Bot{
listenAddr: strings.TrimSpace(cfg.ListenAddr),
maxUploadBytes: cfg.MaxUploadBytes,
log: log,
}, nil
}
func (b *Bot) Run(ctx context.Context, chatHandler ChatHandler, streamChatHandler StreamChatHandler, uploadHandler UploadHandler, historyHandler HistoryHandler) error {
if chatHandler == nil {
return fmt.Errorf("nil webui chat handler")
}
if uploadHandler == nil {
return fmt.Errorf("nil webui upload handler")
}
b.chatHandler = chatHandler
b.streamChatHandler = streamChatHandler
b.uploadHandler = uploadHandler
b.historyHandler = historyHandler
mux := http.NewServeMux()
mux.HandleFunc("/api/chat", b.handleChat)
mux.HandleFunc("/api/chat/stream", b.handleChatStream)
mux.HandleFunc("/api/upload", b.handleUpload)
mux.HandleFunc("/api/history", b.handleHistory)
srv := &http.Server{
Addr: b.listenAddr,
Handler: mux,
}
errCh := make(chan error, 1)
go func() {
err := srv.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
errCh <- err
return
}
errCh <- nil
}()
if b.log != nil {
b.log.Infof("webui http transport started addr=%s", b.listenAddr)
}
select {
case <-ctx.Done():
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
err := <-errCh
if b.log != nil {
b.log.Infof("webui http transport stopped: %v", ctx.Err())
}
if err != nil {
return err
}
return ctx.Err()
case err := <-errCh:
if err != nil && b.log != nil {
b.log.Errorf("webui http transport failed err=%v", err)
}
return err
}
}
func (b *Bot) handleChat(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
return
}
if !strings.Contains(strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type"))), "application/json") {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "content-type must be application/json"})
return
}
if b.chatHandler == nil {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "chat handler not ready"})
return
}
var req chatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid json body"})
return
}
req.Text = strings.TrimSpace(req.Text)
if req.Text == "" {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "text is required"})
return
}
sessionID := b.resolveID(req.SessionID, "sess")
userID := b.resolveID(req.UserID, "user")
reply, err := b.chatHandler(r.Context(), IncomingMessage{
ChatID: sessionID,
UserID: userID,
Text: req.Text,
})
if err != nil {
if b.log != nil {
b.log.Errorf("webui chat handler failed session_id=%s user_id=%s err=%v", sessionID, userID, err)
}
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "chat failed"})
return
}
writeJSON(w, http.StatusOK, chatResponse{
Reply: reply,
SessionID: sessionID,
UserID: userID,
})
}
func (b *Bot) handleHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
return
}
if b.historyHandler == nil {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "history handler not ready"})
return
}
sessionID := strings.TrimSpace(r.URL.Query().Get("session_id"))
if sessionID == "" {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "session_id is required"})
return
}
limitStr := strings.TrimSpace(r.URL.Query().Get("limit"))
limit := 20
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
}
history, err := b.historyHandler(r.Context(), sessionID, limit)
if err != nil {
if b.log != nil {
b.log.Errorf("webui history handler failed session_id=%s err=%v", sessionID, err)
}
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "load history failed"})
return
}
writeJSON(w, http.StatusOK, history)
}
func firstNonEmpty(vals ...string) string {
for _, v := range vals {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
func (b *Bot) handleChatStream(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
return
}
if !strings.Contains(strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type"))), "application/json") {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "content-type must be application/json"})
return
}
if b.streamChatHandler == nil {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "stream chat handler not ready"})
return
}
var req chatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid json body"})
return
}
req.Text = strings.TrimSpace(req.Text)
if req.Text == "" {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "text is required"})
return
}
sessionID := b.resolveID(req.SessionID, "sess")
userID := b.resolveID(req.UserID, "user")
// 设置 SSE 响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming not supported", http.StatusInternalServerError)
return
}
// 创建回调函数来推送 SSE 事件
callback := func(event StreamEvent) error {
data, err := json.Marshal(event)
if err != nil {
return err
}
fmt.Fprintf(w, "data: %s\n\n", string(data))
flusher.Flush()
return nil
}
// 调用流式处理器
reply, err := b.streamChatHandler(r.Context(), IncomingMessage{
ChatID: sessionID,
UserID: userID,
Text: req.Text,
}, callback)
if err != nil {
if b.log != nil {
b.log.Errorf("webui stream chat handler failed session_id=%s user_id=%s err=%v", sessionID, userID, err)
}
// 推送错误事件
errEvent := StreamEvent{
Type: StreamEventTypeError,
Content: "stream error: " + err.Error(),
}
data, _ := json.Marshal(errEvent)
fmt.Fprintf(w, "data: %s\n\n", string(data))
flusher.Flush()
return
}
if b.log != nil {
b.log.Infof("webui stream chat completed session_id=%s user_id=%s reply_len=%d", sessionID, userID, len(reply))
}
}
func (b *Bot) handleUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, errorResponse{Error: "method not allowed"})
return
}
if b.uploadHandler == nil {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "upload handler not ready"})
return
}
r.Body = http.MaxBytesReader(w, r.Body, b.maxUploadBytes)
if err := r.ParseMultipartForm(minInt64(b.maxUploadBytes, 32*1024*1024)); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "request body too large") {
writeJSON(w, http.StatusRequestEntityTooLarge, errorResponse{Error: "file too large"})
return
}
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid multipart form"})
return
}
sessionID := b.resolveID(strings.TrimSpace(r.FormValue("session_id")), "sess")
userID := b.resolveID(strings.TrimSpace(r.FormValue("user_id")), "user")
file, header, err := r.FormFile("file")
if err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "file is required"})
return
}
defer file.Close()
fileName := sanitizeFileName(header.Filename)
if fileName == "" {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "invalid file name"})
return
}
content, err := io.ReadAll(file)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "read file failed"})
return
}
if len(content) == 0 {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: "empty file"})
return
}
mimeType := strings.TrimSpace(header.Header.Get("Content-Type"))
if mimeType == "" {
mimeType = detectMimeByName(fileName)
}
ids, err := b.uploadHandler(r.Context(), sessionID, userID, []llm.InputFile{{
FileName: fileName,
MimeType: mimeType,
Content: content,
}})
if err != nil {
if b.log != nil {
b.log.Errorf("webui upload handler failed session_id=%s user_id=%s file=%s err=%v", sessionID, userID, fileName, err)
}
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "upload failed"})
return
}
if len(ids) == 0 || strings.TrimSpace(ids[0]) == "" {
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "upload succeeded but file_id is empty"})
return
}
writeJSON(w, http.StatusOK, uploadResponse{
FileID: strings.TrimSpace(ids[0]),
FileIDs: ids,
FileName: fileName,
MimeType: mimeType,
SizeBytes: len(content),
SessionID: sessionID,
UserID: userID,
})
}
func (b *Bot) resolveID(raw, prefix string) string {
raw = strings.TrimSpace(raw)
if raw != "" {
return raw
}
n := atomic.AddUint64(&b.counter, 1)
return fmt.Sprintf("%s_%d_%d", prefix, time.Now().UnixNano(), n)
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func minInt64(a, b int64) int64 {
if a < b {
return a
}
return b
}
func detectMimeByName(fileName string) string {
ext := strings.ToLower(strings.TrimSpace(filepath.Ext(fileName)))
if ext == "" {
return "application/octet-stream"
}
m := strings.TrimSpace(mime.TypeByExtension(ext))
if m == "" {
return "application/octet-stream"
}
return m
}
func sanitizeFileName(fileName string) string {
name := strings.TrimSpace(filepath.Base(fileName))
if name == "" || name == "." || name == ".." {
return ""
}
var b strings.Builder
for _, r := range name {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
b.WriteRune(r)
continue
}
b.WriteByte('_')
}
out := strings.TrimSpace(b.String())
if out == "" || out == "." || out == ".." {
return ""
}
if strings.HasPrefix(out, ".") {
out = "file" + out
}
return out
}

View File

@@ -0,0 +1,304 @@
package webui
import (
"bytes"
"context"
"encoding/json"
"errors"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
"laodingbot/internal/config"
"laodingbot/internal/llm"
)
func newTestBot(t *testing.T, maxUploadBytes int64) *Bot {
t.Helper()
b, err := NewBot(config.WebUIConfig{ListenAddr: ":8090", MaxUploadBytes: maxUploadBytes}, nil)
if err != nil {
t.Fatalf("NewBot failed: %v", err)
}
return b
}
func TestHandleChatSuccess(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.chatHandler = func(_ context.Context, msg IncomingMessage) (string, error) {
if msg.ChatID != "s1" || msg.UserID != "u1" || msg.Text != "hello" {
t.Fatalf("unexpected message: %+v", msg)
}
return "ok", nil
}
body := strings.NewReader(`{"text":"hello","session_id":"s1","user_id":"u1"}`)
req := httptest.NewRequest(http.MethodPost, "/api/chat", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChat(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var out chatResponse
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if out.Reply != "ok" || out.SessionID != "s1" || out.UserID != "u1" {
t.Fatalf("unexpected response: %+v", out)
}
}
func TestHandleChatMissingText(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.chatHandler = func(_ context.Context, _ IncomingMessage) (string, error) { return "", nil }
body := strings.NewReader(`{"text":" "}`)
req := httptest.NewRequest(http.MethodPost, "/api/chat", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChat(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
}
func TestHandleUploadSuccess(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.uploadHandler = func(_ context.Context, chatID, userID string, files []llm.InputFile) ([]string, error) {
if chatID != "s1" || userID != "u1" {
t.Fatalf("unexpected ids chat=%s user=%s", chatID, userID)
}
if len(files) != 1 {
t.Fatalf("unexpected files len=%d", len(files))
}
if files[0].FileName != "doc.pdf" || len(files[0].Content) == 0 {
t.Fatalf("unexpected file payload: %+v", files[0])
}
return []string{"file_123"}, nil
}
var payload bytes.Buffer
writer := multipart.NewWriter(&payload)
_ = writer.WriteField("session_id", "s1")
_ = writer.WriteField("user_id", "u1")
fw, err := writer.CreateFormFile("file", "doc.pdf")
if err != nil {
t.Fatalf("CreateFormFile failed: %v", err)
}
_, _ = fw.Write([]byte("pdf-content"))
_ = writer.Close()
req := httptest.NewRequest(http.MethodPost, "/api/upload", &payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
b.handleUpload(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
var out uploadResponse
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if out.FileID != "file_123" || out.SessionID != "s1" || out.UserID != "u1" {
t.Fatalf("unexpected response: %+v", out)
}
}
func TestHandleUploadTooLarge(t *testing.T) {
b := newTestBot(t, 3)
b.uploadHandler = func(_ context.Context, _ string, _ string, _ []llm.InputFile) ([]string, error) {
return []string{"file_should_not_reach"}, nil
}
var payload bytes.Buffer
writer := multipart.NewWriter(&payload)
fw, err := writer.CreateFormFile("file", "a.txt")
if err != nil {
t.Fatalf("CreateFormFile failed: %v", err)
}
_, _ = fw.Write([]byte("12345"))
_ = writer.Close()
req := httptest.NewRequest(http.MethodPost, "/api/upload", &payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
b.handleUpload(w, req)
if w.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("expected 413, got %d body=%s", w.Code, w.Body.String())
}
}
func TestHandleUploadMissingFile(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.uploadHandler = func(_ context.Context, _ string, _ string, _ []llm.InputFile) ([]string, error) {
return []string{"file_should_not_reach"}, nil
}
var payload bytes.Buffer
writer := multipart.NewWriter(&payload)
_ = writer.WriteField("session_id", "s1")
_ = writer.Close()
req := httptest.NewRequest(http.MethodPost, "/api/upload", &payload)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
b.handleUpload(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
}
func TestHandleChatStreamSuccess(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.streamChatHandler = func(_ context.Context, msg IncomingMessage, cb StreamEventCallback) (string, error) {
if msg.ChatID != "s1" || msg.UserID != "u1" || msg.Text != "hello" {
t.Fatalf("unexpected message: %+v", msg)
}
if err := cb(StreamEvent{Type: StreamEventTypeThought, Content: "thinking", Step: 1}); err != nil {
return "", err
}
if err := cb(StreamEvent{Type: StreamEventTypeToolCall, Content: "{\"input\":\"pwd\"}", Step: 1, ToolName: "shell"}); err != nil {
return "", err
}
if err := cb(StreamEvent{Type: StreamEventTypeToolResult, Content: "C:/Project", Step: 1, ToolName: "shell"}); err != nil {
return "", err
}
if err := cb(StreamEvent{Type: StreamEventTypeFinal, Content: "done", Step: 2}); err != nil {
return "", err
}
return "done", nil
}
body := strings.NewReader(`{"text":"hello","session_id":"s1","user_id":"u1"}`)
req := httptest.NewRequest(http.MethodPost, "/api/chat/stream", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
if got := w.Header().Get("Content-Type"); got != "text/event-stream" {
t.Fatalf("expected text/event-stream, got %q", got)
}
var events []StreamEvent
chunks := strings.Split(strings.TrimSpace(w.Body.String()), "\n\n")
for _, chunk := range chunks {
line := strings.TrimSpace(chunk)
if line == "" {
continue
}
if !strings.HasPrefix(line, "data: ") {
t.Fatalf("invalid sse line: %q", line)
}
payload := strings.TrimPrefix(line, "data: ")
var ev StreamEvent
if err := json.Unmarshal([]byte(payload), &ev); err != nil {
t.Fatalf("unmarshal stream event failed: %v payload=%s", err, payload)
}
events = append(events, ev)
}
if len(events) != 4 {
t.Fatalf("expected 4 events, got %d", len(events))
}
if events[0].Type != StreamEventTypeThought {
t.Fatalf("event[0] type mismatch: %s", events[0].Type)
}
if events[1].Type != StreamEventTypeToolCall || events[1].ToolName != "shell" {
t.Fatalf("event[1] mismatch: %+v", events[1])
}
if events[2].Type != StreamEventTypeToolResult || events[2].ToolName != "shell" {
t.Fatalf("event[2] mismatch: %+v", events[2])
}
if events[3].Type != StreamEventTypeFinal || events[3].Content != "done" {
t.Fatalf("event[3] mismatch: %+v", events[3])
}
}
func TestHandleChatStreamHandlerError(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.streamChatHandler = func(_ context.Context, _ IncomingMessage, _ StreamEventCallback) (string, error) {
return "", errors.New("boom")
}
body := strings.NewReader(`{"text":"hello"}`)
req := httptest.NewRequest(http.MethodPost, "/api/chat/stream", body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
respBody := w.Body.String()
if !strings.Contains(respBody, `"type":"error"`) {
t.Fatalf("expected error event in stream, body=%q", respBody)
}
if !strings.Contains(respBody, "stream error: boom") {
t.Fatalf("expected error detail in stream, body=%q", respBody)
}
}
func TestHandleChatStreamValidation(t *testing.T) {
t.Run("method not allowed", func(t *testing.T) {
b := newTestBot(t, 1024*1024)
req := httptest.NewRequest(http.MethodGet, "/api/chat/stream", nil)
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected 405, got %d", w.Code)
}
})
t.Run("content type must be json", func(t *testing.T) {
b := newTestBot(t, 1024*1024)
req := httptest.NewRequest(http.MethodPost, "/api/chat/stream", strings.NewReader(`{"text":"hello"}`))
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
})
t.Run("handler not ready", func(t *testing.T) {
b := newTestBot(t, 1024*1024)
req := httptest.NewRequest(http.MethodPost, "/api/chat/stream", strings.NewReader(`{"text":"hello"}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d", w.Code)
}
})
t.Run("text required", func(t *testing.T) {
b := newTestBot(t, 1024*1024)
b.streamChatHandler = func(_ context.Context, _ IncomingMessage, _ StreamEventCallback) (string, error) {
return "", nil
}
req := httptest.NewRequest(http.MethodPost, "/api/chat/stream", strings.NewReader(`{"text":" "}`))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
b.handleChatStream(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", w.Code)
}
})
}

View File

@@ -0,0 +1,258 @@
---
name: SAFe 铁三角 PI 规划
description: 扮演 SAFe 铁三角PM、架构师、RTE将宏观 Epic 拆解为 PI 规划,输出标准化架构蓝图,并在 Gitea 创建可执行工单。支持从 PDF 等文档提取需求。
---
# Skill: SAFe 铁三角 PI 规划
## 1. 触发条件
当用户的意图匹配以下任意一种时,**必须启用本技能**
- 提交了一份**宏观业务需求**Epic要求进行 PI 级别拆解
- 要求进行**下一个 PI (Program Increment) 的规划**
- 提到 SAFe、PI Planning、铁三角、Feature 拆解、架构跑道等关键词
- 上传了**需求文档**PDF、Word、Markdown并要求分析和拆解
- 要求将规划结果**同步到 Gitea / 创建工单**
**不适用场景**单纯的代码编写、Bug 修复、文件查询等操作性任务。
## 2. 可用工具
| 工具名 | 用途 | 阶段 |
|--------|------|------|
| `extract_file_document` | 提取用户上传的 PDF/文档内容 | 输入准备 |
| `publish_pi_plan` | 将推演结果渲染为标准化架构蓝图 | 规划输出 |
| `create_gitea_ticket` | 在 Gitea 中创建 User Story 工单 | 任务下发 |
| `web_search` | 搜索技术方案、行业标准等参考信息 | 辅助决策 |
## 3. 执行流程
执行本技能时,严格遵循以下分阶段流程。每个阶段对应 ReAct 循环中的一个或多个 Thought → Action → Observation 步骤。
### 阶段 0输入准备 — 文档提取(如有附件)
如果用户提交了文件file_id**必须先提取文档内容**,再进入正式推演。
**Action**: 调用 `extract_file_document`
```
输入: 用户提供的 file_id
```
**Observation**: 获取文档全文结构化摘要,包含标题、核心观点、关键数据。
将提取到的文档内容作为后续推演的输入素材。如果用户同时提交了多个文件,逐个提取后合并为统一的需求上下文。
---
### 阶段 1铁三角推演核心 Thought 过程)
**这是本技能的核心。** 你必须在 Thought 中同时扮演三个角色,按以下顺序进行严格的自我推演和博弈。不可跳过任何视角。
#### 视角 1: 产品管理 (PM) — 决定"做什么"和"为什么做"
在 Thought 中以 `[PM]` 标记此视角的思考:
1. **需求拆解**: 将宏观 Epic 拆解为 2-4 个可在 8-12 周内交付的业务特性 (Feature)
2. **价值假设**: 为每个 Feature 写出清晰的 Benefit Hypothesis — 如果做了这个功能,会带来什么可量化的业务收益
3. **验收标准**: 从业务视角列出每个 Feature 的验收条件 (Acceptance Criteria),不涉及技术实现细节
4. **优先级排序**: 根据业务价值和紧迫性,给出 Feature 的建议交付顺序
**PM 视角的自检问题**
- 每个 Feature 是否独立可交付,还是必须和其他 Feature 一起才有意义?
- 验收标准是否可以被测试团队直接转化为测试用例?
- 是否遗漏了用户(终端使用者)会关心的场景?
#### 视角 2: 系统架构师 (SA) — 决定"技术怎么接"和"底座跑道"
在 Thought 中以 `[SA]` 标记此视角的思考:
1. **架构跑道 (Architectural Runway)**: PM 提出的每个 Feature当前系统能直接支撑吗如果不能需要提前铺设哪些底层基础设施将这些转化为 Enabler
2. **瓶颈识别**: 当前系统架构的薄弱环节在哪?哪些 Enabler 是阻塞性的(不做就无法开始 Feature 开发)?
3. **NFRs 定义**: 强制规定非功能性需求:
- 性能指标QPS、P99 延迟、吞吐量)
- 安全标准(加密协议、认证机制、数据脱敏)
4. **接口契约**: 如果涉及多个子系统交互,定义关键 API 契约
**SA 视角的自检问题**
- 每个 Enabler 是否有明确的"完成标志"(而不是开放性的研究任务)?
- NFRs 的指标是否可量化、可自动化测试?
- 是否过度设计Enabler 数量是否与 Feature 复杂度匹配?
#### 视角 3: 发布火车工程师 (RTE) — 决定"流程风险"与"依赖管理"
在 Thought 中以 `[RTE]` 标记此视角的思考:
1. **依赖分析**: 检查 PM 的 Feature 和 SA 的 Enabler 之间的时序依赖 — 哪些 Enabler 必须先完成才能开始哪些 Feature
2. **风险评估**: 识别潜在的交付风险:
- 技术风险(新技术栈、外部 API 未就绪)
- 资源风险(关键人员依赖、跨团队协调)
- 集成风险(多个 Feature 的集成点)
3. **里程碑建议**: 基于依赖关系,给出关键里程碑的建议时间节点
**RTE 视角的自检问题**
- 是否存在循环依赖?
- 关键路径上的任务是否有 buffer
- 如果某个 Enabler 延期,哪些 Feature 会受影响?
---
### 阶段 2生成标准化 PI 规划
完成三个视角的推演后,**必须立即调用** `publish_pi_plan` 工具。
**Action**: 调用 `publish_pi_plan`,传入推演结果的 JSON
```json
{
"pi_vision": "本 PI 的核心业务愿景(一两句话概括)",
"features": [
{
"feature_id": "FEAT_XXX_001",
"title": "动宾结构的简洁标题",
"benefit_hypothesis": "如果实现此功能,将带来 XXX 业务收益",
"acceptance_criteria": ["AC1: ...", "AC2: ..."]
}
],
"enablers": [
{
"enabler_id": "ENAB_XXX_001",
"title": "技术任务名称",
"architectural_purpose": "为什么需要这个底层改造"
}
],
"nfrs": {
"performance": "具体的性能指标约束",
"security": "具体的安全与合规约束"
},
"dependencies": [
{
"source_id": "ENAB_XXX_001",
"target_id": "FEAT_XXX_001",
"reason": "依赖原因的一句话描述"
}
]
}
```
**命名规范**
- Feature ID: `FEAT_<领域缩写>_<序号>`,如 `FEAT_OTA_001`
- Enabler ID: `ENAB_<技术栈缩写>_<序号>`,如 `ENAB_KAFKA_001`
**Observation**: 获取渲染后的 Markdown 架构蓝图包含愿景、特性清单、Enabler 表、NFRs、依赖关系、执行顺序和质量门禁检查清单。
**⚠️ 关键要求**工具返回的内容Observation不会直接展示给用户。你**必须**将 `publish_pi_plan` 返回的蓝图 Markdown **全文**复制到你的最终回复中,让用户可以看到完整的规划内容。**严禁**仅用一句"蓝图已生成"代替正文输出。
输出蓝图后,征求用户反馈。如果用户没有异议,直接继续执行阶段 3。
**⚠️ 用户确认规则**:当用户回复表示确认(如"确认"、"开始创建工单"、"批量创建"、"没问题"、"可以了"等肯定性表达),**必须立即调用** `create_gitea_ticket` 工具开始创建工单,**严禁**重复输出蓝图或再次征求确认。
---
### 阶段 3任务下发到 Gitea
蓝图展示给用户后,将 Feature 和 Enabler **逐一拆解为 User Story**,通过 `create_gitea_ticket` 在 Gitea 创建工单。如果用户明确表示需要调整,先根据反馈修订蓝图后再创建工单。
#### 拆解原则
- 每个 Feature 拆解为 1-3 个 User Story每个 Story 应在 1-3 天内可完成)
- 每个 Enabler 拆解为 1-2 个技术任务 Story
- Story 的标题采用**动宾结构**(如"实现固件版本解析 API""部署 Kafka 基础镜像"
#### 创建工单
对每个 Story**Action**: 调用 `create_gitea_ticket`
```json
{
"title": "动宾结构的任务标题",
"body": "## 溯源\n- Parent: FEAT_XXX_001\n\n## 任务上下文\n<描述做什么、为什么>\n\n## 验收标准\n- [ ] AC-1: ...\n- [ ] AC-2: ...\n\n## NFRs\n- 性能: ...\n- 安全: ...\n\n## 技术实现思路\n<基于架构师视角补充>",
"labels": ["type/story", "domain/<领域>", "priority/<high|medium|low>", "status/todo"],
"parent_reference_id": "FEAT_XXX_001",
"estimated_hours": 8
}
```
**标签约定**
- `type/story` | `type/enabler` | `type/spike` — 任务类型
- `domain/<领域>` — 业务领域(如 `domain/cloud``domain/vehicle``domain/infra`
- `priority/high` | `priority/medium` | `priority/low` — 优先级
- `status/todo` — 初始状态
**工时估算参考**
- 简单 CRUD / 配置变更: 4-8 小时
- 标准 API 开发 + 单元测试: 8-16 小时
- 基础设施搭建 / 中间件部署: 16-24 小时
- 跨系统集成 + 联调: 16-32 小时
创建完成后,汇总所有工单链接,输出执行看板。
## 4. Thought 内部推演示范
以下示范 Thought 过程的结构(非固定内容,仅展示格式):
```
Thought:
用户提交了一个 Epic"实现面向欧洲市场的车云 OTA 升级平台"。
我需要以铁三角三个视角依次推演。
[PM] 产品管理视角:
- 核心交付价值:让欧洲车主能安全、快速地接收 OTA 升级
- Feature 拆解:
1. FEAT_OTA_001: 云端固件版本依赖检查 — 防止下载不兼容固件
2. FEAT_OTA_002: 端侧断点续传 — 弱网环境下提升下载成功率
3. FEAT_OTA_003: 升级状态实时推送 — 车主可在 App 中看到升级进度
- 各 Feature 的 AC 已确定,均可测试化
[SA] 系统架构师视角:
- FEAT_OTA_001 需要异步版本校验,当前没有合适的消息中间件 → ENAB_KAFKA_001
- FEAT_OTA_002 需要支持 Range 请求的对象存储 → ENAB_S3_001
- NFRsAPI P99 < 200ms, TLS 1.3, VIN 脱敏
- 当前架构可支撑 FEAT_OTA_003无需新增 Enabler
[RTE] 发布火车工程师视角:
- ENAB_KAFKA_001 → FEAT_OTA_001阻塞依赖
- ENAB_S3_001 → FEAT_OTA_002阻塞依赖
- FEAT_OTA_003 无依赖,可并行开发
- 风险Kafka 集群搭建预计 2 周,是关键路径
推演完成,下一步调用 publish_pi_plan 输出标准化蓝图。
Action: publish_pi_plan
Action Input: {"pi_vision": "...", "features": [...], ...}
Observation: (工具返回完整 Markdown 蓝图)
Thought: 蓝图已生成。由于 Observation 不会直接展示给用户,我必须在 Final Answer 中包含蓝图全文内容。同时继续执行阶段 3创建 Gitea 工单。
Final Answer:
(此处粘贴 publish_pi_plan 返回的完整蓝图 Markdown 全文)
```
## 5. 输出规范
最终交付给用户的内容必须包含:
1. **PI 蓝图**`publish_pi_plan` 返回的完整 Markdown 报告(必须全文输出,不可省略或仅用一句话概括)
2. **Gitea 工单汇总**(如已执行阶段 3
- 工单编号与链接列表
- 按执行顺序排列
- 标注关键路径上的任务
3. **风险与建议**RTE 视角识别的关键风险及缓解措施
## 6. 质量约束
- **三视角不可跳过**: 即使需求看起来简单,也必须走完 PM → SA → RTE 三步推演
- **ID 必须唯一**: Feature ID 和 Enabler ID 在同一个 PI 内不可重复
- **AC 必须可测试**: 每条验收标准都应能被转化为自动化测试用例
- **NFRs 必须可量化**: 不接受"性能要好"这样的模糊描述,必须有具体数字
- **依赖必须有理由**: 每条依赖关系都要说明为什么 A 必须先于 B
## 7. 失败回退策略
| 失败场景 | 处理方式 |
|----------|----------|
| 文档提取失败file_id 无效) | 提示用户重新上传或提供文本版需求描述 |
| 需求信息不足以拆解 Feature | 列出缺失信息清单,请求用户补充后重新推演 |
| Gitea 配置缺失token/repo 为空) | 仅输出 PI 蓝图,跳过工单创建,提示用户配置 Gitea 环境变量 |
| Gitea API 调用失败 | 输出 PI 蓝图,附上拟创建工单的 JSON 列表供用户手动创建 |
| 用户对规划有异议 | 记录反馈,调整对应视角的推演,重新生成蓝图 |

208
tools/filedoc/filedoc.go Normal file
View File

@@ -0,0 +1,208 @@
package filedoc
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"laodingbot/internal/logger"
openai "github.com/openai/openai-go"
"github.com/openai/openai-go/option"
"github.com/openai/openai-go/shared"
)
type Config struct {
APIKey string
BaseURL string
Model string
Timeout time.Duration
}
type Tool struct {
client openai.Client
model string
maxOutputChars int
log *logger.Logger
}
func New(cfg Config, maxOutputChars int, log *logger.Logger) *Tool {
if strings.TrimSpace(cfg.Model) == "" {
cfg.Model = "gpt-4o-mini"
}
if cfg.Timeout <= 0 {
cfg.Timeout = 60 * time.Second
}
if maxOutputChars <= 0 {
maxOutputChars = 12000
}
opts := []option.RequestOption{
option.WithAPIKey(strings.TrimSpace(cfg.APIKey)),
option.WithRequestTimeout(cfg.Timeout),
}
if strings.TrimSpace(cfg.BaseURL) != "" {
opts = append(opts, option.WithBaseURL(strings.TrimSpace(cfg.BaseURL)))
}
return &Tool{
client: openai.NewClient(opts...),
model: cfg.Model,
maxOutputChars: maxOutputChars,
log: log,
}
}
func (t *Tool) Name() string { return "extract_file_document" }
func (t *Tool) Description() string {
return "Extract full document details from a file ID via OpenAI. Input: file_id (supports plain ID, fileid://ID, or JSON {\"file_id\":\"...\"})."
}
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
fileID, userFocus, err := parseInput(input)
if err != nil {
return "", err
}
prompt := buildExtractionPrompt(fileID, userFocus)
messages := []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage("fileid://" + fileID),
openai.UserMessage([]openai.ChatCompletionContentPartUnionParam{
openai.TextContentPart(prompt),
}),
}
params := openai.ChatCompletionNewParams{
Model: shared.ChatModel(t.model),
Messages: messages,
}
if t.log != nil {
t.log.Infof("filedoc tool request model=%s file_id=%s", t.model, fileID)
}
resp, err := t.client.Chat.Completions.New(ctx, params)
if err != nil {
return "", fmt.Errorf("filedoc request failed: %w", err)
}
if len(resp.Choices) == 0 {
return "", fmt.Errorf("filedoc returned empty choices")
}
out := strings.TrimSpace(resp.Choices[0].Message.Content)
if out == "" {
out = "未提取到可读的文档内容。请确认 file_id 是否有效以及模型是否支持文件解析。"
}
if len(out) > t.maxOutputChars {
out = out[:t.maxOutputChars]
}
return out, nil
}
func buildExtractionPrompt(fileID, userFocus string) string {
focus := strings.TrimSpace(userFocus)
if focus == "" {
focus = "请输出完整文档信息,包括标题、主题、核心观点、结构大纲、关键术语、重要结论、风险点与后续建议。"
}
return strings.Join([]string{
"请基于所附文件输出完整文档信息。",
"file_id: " + fileID,
"",
"输出要求:",
"1) 文档基本信息:标题、文档类型、语言、可能作者/组织(若可判断)、时间线索(若可判断)。",
"2) 结构化摘要:按章节或逻辑段落给出要点,尽量保持原文顺序。",
"3) 关键数据与事实:列出关键数字、术语、专有名词、约束条件。",
"4) 风险与不确定性:明确哪些信息来源于文档,哪些是无法确认。",
"5) 面向执行的建议:给出可落地的后续行动项。",
"",
"补充关注点:",
focus,
}, "\n")
}
func parseInput(input string) (fileID string, userFocus string, err error) {
raw := strings.TrimSpace(input)
if raw == "" {
return "", "", fmt.Errorf("empty input: expected file_id")
}
if strings.HasPrefix(raw, "{") {
var payload map[string]any
if jsonErr := json.Unmarshal([]byte(raw), &payload); jsonErr == nil {
if id := firstNonEmptyString(payload, "file_id", "fileid", "id", "fileID"); id != "" {
return normalizeFileID(id), firstNonEmptyString(payload, "focus", "query", "instruction", "prompt"), nil
}
}
}
lines := strings.Split(raw, "\n")
for _, line := range lines {
candidate := extractFileIDToken(line)
if candidate != "" {
focus := strings.TrimSpace(strings.ReplaceAll(raw, line, ""))
return normalizeFileID(candidate), focus, nil
}
}
candidate := extractFileIDToken(raw)
if candidate == "" {
return "", "", fmt.Errorf("no file_id found in input")
}
return normalizeFileID(candidate), "", nil
}
func extractFileIDToken(s string) string {
fields := strings.FieldsFunc(s, func(r rune) bool {
switch r {
case ' ', '\t', '\n', '\r', ',', ';', '|':
return true
default:
return false
}
})
for _, f := range fields {
tok := strings.TrimSpace(strings.Trim(f, "\"'()[]{}"))
if tok == "" {
continue
}
lower := strings.ToLower(tok)
if strings.HasPrefix(lower, "fileid://") {
return tok[len("fileid://"):]
}
if strings.HasPrefix(lower, "file_id=") || strings.HasPrefix(lower, "fileid=") {
idx := strings.Index(tok, "=")
if idx >= 0 && idx+1 < len(tok) {
return tok[idx+1:]
}
}
if strings.HasPrefix(lower, "file_") || strings.HasPrefix(lower, "file-") {
return tok
}
}
return ""
}
func normalizeFileID(id string) string {
id = strings.TrimSpace(strings.Trim(id, "\"'"))
if strings.HasPrefix(strings.ToLower(id), "fileid://") {
return strings.TrimSpace(id[len("fileid://"):])
}
return id
}
func firstNonEmptyString(m map[string]any, keys ...string) string {
for _, k := range keys {
if v, ok := m[k]; ok {
if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
return strings.TrimSpace(s)
}
}
}
return ""
}

View File

@@ -0,0 +1,74 @@
package filedoc
import (
"strings"
"testing"
)
func TestNameAndDescription(t *testing.T) {
tool := New(Config{APIKey: "k", Model: "gpt-4o-mini"}, 5000, nil)
if tool.Name() != "extract_file_document" {
t.Fatalf("unexpected tool name: %s", tool.Name())
}
if tool.Description() == "" {
t.Fatal("description should not be empty")
}
}
func TestParseInputPlainFileID(t *testing.T) {
id, focus, err := parseInput("file_ec_452e96aad38940229058f193f5c5b9c6_12553222")
if err != nil {
t.Fatalf("parseInput returned error: %v", err)
}
if id != "file_ec_452e96aad38940229058f193f5c5b9c6_12553222" {
t.Fatalf("unexpected id: %s", id)
}
if focus != "" {
t.Fatalf("expected empty focus, got: %q", focus)
}
}
func TestParseInputFileIDSchemeWithFocus(t *testing.T) {
input := "fileid://file_ec_12345\n重点关注风险与建议"
id, focus, err := parseInput(input)
if err != nil {
t.Fatalf("parseInput returned error: %v", err)
}
if id != "file_ec_12345" {
t.Fatalf("unexpected id: %s", id)
}
if focus == "" {
t.Fatal("expected non-empty focus")
}
}
func TestParseInputJSON(t *testing.T) {
input := `{"file_id":"file_ec_888", "focus":"提取关键结论"}`
id, focus, err := parseInput(input)
if err != nil {
t.Fatalf("parseInput returned error: %v", err)
}
if id != "file_ec_888" {
t.Fatalf("unexpected id: %s", id)
}
if focus != "提取关键结论" {
t.Fatalf("unexpected focus: %s", focus)
}
}
func TestParseInputInvalid(t *testing.T) {
_, _, err := parseInput("hello world")
if err == nil {
t.Fatal("expected parse error")
}
}
func TestBuildExtractionPrompt(t *testing.T) {
p := buildExtractionPrompt("file_ec_abc", "关注测试条目")
if p == "" {
t.Fatal("prompt should not be empty")
}
if p != "" && !(strings.Contains(p, "file_ec_abc") && strings.Contains(p, "关注测试条目")) {
t.Fatalf("unexpected prompt content: %s", p)
}
}

View File

@@ -0,0 +1,336 @@
package giteaticket
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"laodingbot/internal/logger"
)
// Config Gitea 工单工具的配置。
type Config struct {
BaseURL string // Gitea 实例地址,例如 https://gitea.example.com
Token string // Gitea Personal Access Token
Owner string // 仓库所有者(用户名或组织名)
Repo string // 仓库名称
Timeout time.Duration // HTTP 请求超时
}
// TicketInput 对应 tool schema 定义的输入参数。
type TicketInput struct {
Title string `json:"title"`
Body string `json:"body"`
Labels []string `json:"labels"`
ParentReferenceID string `json:"parent_reference_id"`
EstimatedHours int `json:"estimated_hours,omitempty"`
}
// giteaCreateIssueReq Gitea Create Issue API 请求体。
type giteaCreateIssueReq struct {
Title string `json:"title"`
Body string `json:"body"`
Labels []int64 `json:"labels,omitempty"`
}
// giteaIssueResp Gitea Issue API 响应(仅用到的字段)。
type giteaIssueResp struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
HTMLURL string `json:"html_url"`
Title string `json:"title"`
State string `json:"state"`
CreatedAt string `json:"created_at"`
}
// giteaLabelResp Gitea Label API 响应。
type giteaLabelResp struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// Tool 实现 create_gitea_ticket 工具。
type Tool struct {
baseURL string
token string
owner string
repo string
httpClient *http.Client
log *logger.Logger
}
// New 创建一个新的 create_gitea_ticket 工具实例。
func New(cfg Config, log *logger.Logger) *Tool {
baseURL := strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
if cfg.Timeout <= 0 {
cfg.Timeout = 30 * time.Second
}
if log != nil {
log.Infof("giteaticket tool initialized base_url=%q owner=%q repo=%q token_set=%v",
baseURL, strings.TrimSpace(cfg.Owner), strings.TrimSpace(cfg.Repo), strings.TrimSpace(cfg.Token) != "")
}
return &Tool{
baseURL: baseURL,
token: strings.TrimSpace(cfg.Token),
owner: strings.TrimSpace(cfg.Owner),
repo: strings.TrimSpace(cfg.Repo),
httpClient: &http.Client{Timeout: cfg.Timeout},
log: log,
}
}
func (t *Tool) Name() string { return "create_gitea_ticket" }
func (t *Tool) Description() string {
return `DevOps Agent 使用此工具,将 PI Plan 中的 Feature/Enabler 拆解为可执行 User Story并在 Gitea 创建 Issue。输入 JSON: {"title":"...","body":"...","labels":[...],"parent_reference_id":"...","estimated_hours":8}`
}
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
ticket, err := parseInput(input)
if err != nil {
return "", fmt.Errorf("create_gitea_ticket: invalid input: %w", err)
}
if err := validate(ticket); err != nil {
return "", fmt.Errorf("create_gitea_ticket: validation failed: %w", err)
}
if t.baseURL == "" || t.token == "" || t.owner == "" || t.repo == "" {
if t.log != nil {
t.log.Errorf("giteaticket config missing: base_url=%q token_set=%v owner=%q repo=%q",
t.baseURL, t.token != "", t.owner, t.repo)
}
return "", fmt.Errorf("create_gitea_ticket: missing Gitea configuration (base_url, token, owner, repo)")
}
body := buildIssueBody(ticket)
labelIDs, err := t.resolveLabels(ctx, ticket.Labels)
if err != nil {
return "", fmt.Errorf("create_gitea_ticket: label resolution failed: %w", err)
}
if t.log != nil {
t.log.Infof("create_gitea_ticket: creating issue title=%q parent=%s labels=%v",
ticket.Title, ticket.ParentReferenceID, ticket.Labels)
}
issue, err := t.createIssue(ctx, ticket.Title, body, labelIDs)
if err != nil {
return "", fmt.Errorf("create_gitea_ticket: create issue failed: %w", err)
}
result := formatResult(issue, ticket)
return result, nil
}
func parseInput(input string) (*TicketInput, error) {
raw := strings.TrimSpace(input)
if raw == "" {
return nil, fmt.Errorf("empty input")
}
var ticket TicketInput
if err := json.Unmarshal([]byte(raw), &ticket); err != nil {
return nil, fmt.Errorf("JSON parse error: %w", err)
}
return &ticket, nil
}
func validate(t *TicketInput) error {
if strings.TrimSpace(t.Title) == "" {
return fmt.Errorf("title is required")
}
if strings.TrimSpace(t.Body) == "" {
return fmt.Errorf("body is required")
}
if len(t.Labels) == 0 {
return fmt.Errorf("labels is required and must contain at least one label")
}
if strings.TrimSpace(t.ParentReferenceID) == "" {
return fmt.Errorf("parent_reference_id is required")
}
return nil
}
// buildIssueBody 在原始 body 上追加溯源元数据和工时估算。
func buildIssueBody(ticket *TicketInput) string {
var b strings.Builder
b.WriteString(ticket.Body)
b.WriteString("\n\n---\n\n")
b.WriteString("## 📋 SAFe 元数据\n\n")
b.WriteString(fmt.Sprintf("- **溯源 (Parent Reference)**: `%s`\n", ticket.ParentReferenceID))
if ticket.EstimatedHours > 0 {
b.WriteString(fmt.Sprintf("- **预估工时**: %d 小时\n", ticket.EstimatedHours))
}
b.WriteString(fmt.Sprintf("- **标签**: %s\n", strings.Join(ticket.Labels, ", ")))
return b.String()
}
// resolveLabels 通过 Gitea API 查询已有标签,将标签名映射为 ID。
// 如果标签不存在则自动创建。
func (t *Tool) resolveLabels(ctx context.Context, labelNames []string) ([]int64, error) {
existing, err := t.listLabels(ctx)
if err != nil {
return nil, err
}
nameToID := make(map[string]int64, len(existing))
for _, l := range existing {
nameToID[l.Name] = l.ID
}
ids := make([]int64, 0, len(labelNames))
for _, name := range labelNames {
name = strings.TrimSpace(name)
if name == "" {
continue
}
if id, ok := nameToID[name]; ok {
ids = append(ids, id)
} else {
id, err := t.createLabel(ctx, name)
if err != nil {
return nil, fmt.Errorf("create label %q: %w", name, err)
}
ids = append(ids, id)
}
}
return ids, nil
}
func (t *Tool) listLabels(ctx context.Context) ([]giteaLabelResp, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels?limit=50", t.baseURL, t.owner, t.repo)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
t.setAuth(req)
resp, err := t.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("list labels request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("list labels returned %d: %s", resp.StatusCode, string(body))
}
var labels []giteaLabelResp
if err := json.NewDecoder(resp.Body).Decode(&labels); err != nil {
return nil, fmt.Errorf("decode labels response: %w", err)
}
return labels, nil
}
func (t *Tool) createLabel(ctx context.Context, name string) (int64, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/labels", t.baseURL, t.owner, t.repo)
payload := map[string]string{
"name": name,
"color": labelColor(name),
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return 0, err
}
t.setAuth(req)
req.Header.Set("Content-Type", "application/json")
resp, err := t.httpClient.Do(req)
if err != nil {
return 0, fmt.Errorf("create label request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return 0, fmt.Errorf("create label returned %d: %s", resp.StatusCode, string(respBody))
}
var label giteaLabelResp
if err := json.NewDecoder(resp.Body).Decode(&label); err != nil {
return 0, fmt.Errorf("decode create label response: %w", err)
}
return label.ID, nil
}
func (t *Tool) createIssue(ctx context.Context, title, body string, labelIDs []int64) (*giteaIssueResp, error) {
url := fmt.Sprintf("%s/api/v1/repos/%s/%s/issues", t.baseURL, t.owner, t.repo)
payload := giteaCreateIssueReq{
Title: title,
Body: body,
Labels: labelIDs,
}
jsonBody, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
t.setAuth(req)
req.Header.Set("Content-Type", "application/json")
resp, err := t.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("create issue request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("create issue returned %d: %s", resp.StatusCode, string(respBody))
}
var issue giteaIssueResp
if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil {
return nil, fmt.Errorf("decode create issue response: %w", err)
}
return &issue, nil
}
func (t *Tool) setAuth(req *http.Request) {
if t.token != "" {
req.Header.Set("Authorization", "token "+t.token)
}
}
func formatResult(issue *giteaIssueResp, ticket *TicketInput) string {
var b strings.Builder
b.WriteString("✅ Gitea Issue 创建成功\n\n")
b.WriteString(fmt.Sprintf("- **Issue 编号**: #%d\n", issue.Number))
b.WriteString(fmt.Sprintf("- **标题**: %s\n", issue.Title))
b.WriteString(fmt.Sprintf("- **状态**: %s\n", issue.State))
b.WriteString(fmt.Sprintf("- **链接**: %s\n", issue.HTMLURL))
b.WriteString(fmt.Sprintf("- **溯源 (Parent)**: %s\n", ticket.ParentReferenceID))
if ticket.EstimatedHours > 0 {
b.WriteString(fmt.Sprintf("- **预估工时**: %d 小时\n", ticket.EstimatedHours))
}
b.WriteString(fmt.Sprintf("- **标签**: %s\n", strings.Join(ticket.Labels, ", ")))
return b.String()
}
// labelColor 根据标签前缀返回一个辨识度高的颜色。
func labelColor(name string) string {
lower := strings.ToLower(name)
switch {
case strings.HasPrefix(lower, "type/"):
return "#0075ca"
case strings.HasPrefix(lower, "domain/"):
return "#7057ff"
case strings.HasPrefix(lower, "priority/"):
if strings.Contains(lower, "high") || strings.Contains(lower, "critical") {
return "#d73a4a"
}
return "#e4e669"
case strings.HasPrefix(lower, "status/"):
return "#0e8a16"
default:
return "#ededed"
}
}

View File

@@ -0,0 +1,403 @@
package giteaticket
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
func validTicketInput() TicketInput {
return TicketInput{
Title: "实现云端固件版本解析 API",
Body: `## 溯源
- Parent: FEAT_OTA_001
## 任务上下文
云端需要能够解析上传的固件包,提取版本号与依赖关系。
## 验收标准
- [ ] 上传 .bin 固件文件后返回解析结果 JSON
- [ ] 解析结果包含 version, dependencies 字段
## NFRs
- 响应时间 P99 < 500ms
## 技术实现思路
使用 Go 读取固件文件头部 metadata。`,
Labels: []string{"type/story", "domain/cloud", "priority/high", "status/todo"},
ParentReferenceID: "FEAT_OTA_001",
EstimatedHours: 8,
}
}
// ── parseInput tests ──
func TestParseInputValid(t *testing.T) {
in := validTicketInput()
data, _ := json.Marshal(in)
ticket, err := parseInput(string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ticket.Title != in.Title {
t.Errorf("title mismatch: got %q", ticket.Title)
}
if len(ticket.Labels) != 4 {
t.Errorf("expected 4 labels, got %d", len(ticket.Labels))
}
if ticket.ParentReferenceID != "FEAT_OTA_001" {
t.Errorf("parent_reference_id mismatch: got %q", ticket.ParentReferenceID)
}
if ticket.EstimatedHours != 8 {
t.Errorf("estimated_hours mismatch: got %d", ticket.EstimatedHours)
}
}
func TestParseInputEmpty(t *testing.T) {
_, err := parseInput("")
if err == nil {
t.Fatal("expected error for empty input")
}
}
func TestParseInputInvalidJSON(t *testing.T) {
_, err := parseInput("{bad json}")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
// ── validate tests ──
func TestValidateMissingTitle(t *testing.T) {
in := validTicketInput()
in.Title = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "title") {
t.Fatalf("expected title error, got: %v", err)
}
}
func TestValidateMissingBody(t *testing.T) {
in := validTicketInput()
in.Body = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "body") {
t.Fatalf("expected body error, got: %v", err)
}
}
func TestValidateMissingLabels(t *testing.T) {
in := validTicketInput()
in.Labels = nil
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "labels") {
t.Fatalf("expected labels error, got: %v", err)
}
}
func TestValidateMissingParentRef(t *testing.T) {
in := validTicketInput()
in.ParentReferenceID = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "parent_reference_id") {
t.Fatalf("expected parent_reference_id error, got: %v", err)
}
}
func TestValidateOptionalEstimatedHours(t *testing.T) {
in := validTicketInput()
in.EstimatedHours = 0
err := validate(&in)
if err != nil {
t.Fatalf("estimated_hours should be optional, got: %v", err)
}
}
// ── buildIssueBody tests ──
func TestBuildIssueBodyContainsMetadata(t *testing.T) {
in := validTicketInput()
body := buildIssueBody(&in)
if !strings.Contains(body, "FEAT_OTA_001") {
t.Error("body missing parent_reference_id")
}
if !strings.Contains(body, "8 小时") {
t.Error("body missing estimated_hours")
}
if !strings.Contains(body, "type/story") {
t.Error("body missing labels")
}
if !strings.Contains(body, "SAFe 元数据") {
t.Error("body missing metadata section header")
}
}
func TestBuildIssueBodyNoHours(t *testing.T) {
in := validTicketInput()
in.EstimatedHours = 0
body := buildIssueBody(&in)
if strings.Contains(body, "预估工时") {
t.Error("body should not contain estimated hours when 0")
}
}
// ── labelColor tests ──
func TestLabelColor(t *testing.T) {
tests := []struct {
name string
expected string
}{
{"type/story", "#0075ca"},
{"domain/cloud", "#7057ff"},
{"priority/high", "#d73a4a"},
{"priority/low", "#e4e669"},
{"status/todo", "#0e8a16"},
{"custom-label", "#ededed"},
}
for _, tc := range tests {
got := labelColor(tc.name)
if got != tc.expected {
t.Errorf("labelColor(%q) = %q, want %q", tc.name, got, tc.expected)
}
}
}
// ── Name / Description tests ──
func TestNameAndDescription(t *testing.T) {
tool := New(Config{}, nil)
if tool.Name() != "create_gitea_ticket" {
t.Errorf("unexpected name: %s", tool.Name())
}
if tool.Description() == "" {
t.Error("description should not be empty")
}
}
// ── Integration test with mock Gitea server ──
func TestCallWithMockGitea(t *testing.T) {
var mu sync.Mutex
var createdIssue map[string]interface{}
labelIDCounter := int64(100)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify auth header
auth := r.Header.Get("Authorization")
if auth != "token test-token" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
switch {
// GET /api/v1/repos/owner/repo/labels
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/labels"):
w.Header().Set("Content-Type", "application/json")
// Return one existing label
json.NewEncoder(w).Encode([]giteaLabelResp{
{ID: 1, Name: "type/story"},
})
// POST /api/v1/repos/owner/repo/labels
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/labels"):
var payload map[string]string
json.NewDecoder(r.Body).Decode(&payload)
mu.Lock()
labelIDCounter++
id := labelIDCounter
mu.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(giteaLabelResp{
ID: id,
Name: payload["name"],
})
// POST /api/v1/repos/owner/repo/issues
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/issues"):
json.NewDecoder(r.Body).Decode(&createdIssue)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(giteaIssueResp{
ID: 42,
Number: 42,
HTMLURL: "https://gitea.example.com/owner/repo/issues/42",
Title: createdIssue["title"].(string),
State: "open",
CreatedAt: "2026-03-11T10:00:00Z",
})
default:
http.Error(w, "not found", http.StatusNotFound)
}
}))
defer server.Close()
tool := New(Config{
BaseURL: server.URL,
Token: "test-token",
Owner: "owner",
Repo: "repo",
}, nil)
in := validTicketInput()
data, _ := json.Marshal(in)
result, err := tool.Call(context.Background(), string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(result, "#42") {
t.Error("result missing issue number")
}
if !strings.Contains(result, "创建成功") {
t.Error("result missing success message")
}
if !strings.Contains(result, "FEAT_OTA_001") {
t.Error("result missing parent reference")
}
if !strings.Contains(result, "8 小时") {
t.Error("result missing estimated hours")
}
// Verify the issue body sent to Gitea contains SAFe metadata
if body, ok := createdIssue["body"].(string); ok {
if !strings.Contains(body, "SAFe 元数据") {
t.Error("issue body missing SAFe metadata")
}
if !strings.Contains(body, "FEAT_OTA_001") {
t.Error("issue body missing parent reference")
}
} else {
t.Error("createdIssue body not captured")
}
}
func TestCallMissingConfig(t *testing.T) {
tool := New(Config{}, nil)
in := validTicketInput()
data, _ := json.Marshal(in)
_, err := tool.Call(context.Background(), string(data))
if err == nil || !strings.Contains(err.Error(), "missing Gitea configuration") {
t.Fatalf("expected config error, got: %v", err)
}
}
func TestCallInvalidInput(t *testing.T) {
tool := New(Config{
BaseURL: "http://localhost",
Token: "t",
Owner: "o",
Repo: "r",
}, nil)
_, err := tool.Call(context.Background(), "not json")
if err == nil {
t.Fatal("expected error for invalid input")
}
}
func TestCallValidationError(t *testing.T) {
tool := New(Config{
BaseURL: "http://localhost",
Token: "t",
Owner: "o",
Repo: "r",
}, nil)
in := validTicketInput()
in.Title = ""
data, _ := json.Marshal(in)
_, err := tool.Call(context.Background(), string(data))
if err == nil || !strings.Contains(err.Error(), "title") {
t.Fatalf("expected title validation error, got: %v", err)
}
}
// Test that labels resolution creates missing labels
func TestCallCreatesNewLabels(t *testing.T) {
createdLabels := []string{}
var mu sync.Mutex
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/labels"):
w.Header().Set("Content-Type", "application/json")
// No existing labels
json.NewEncoder(w).Encode([]giteaLabelResp{})
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/labels"):
var payload map[string]string
json.NewDecoder(r.Body).Decode(&payload)
mu.Lock()
createdLabels = append(createdLabels, payload["name"])
mu.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(giteaLabelResp{
ID: int64(len(createdLabels)),
Name: payload["name"],
})
case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/issues"):
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(giteaIssueResp{
ID: 1, Number: 1, HTMLURL: "http://test/1", Title: "t", State: "open",
})
default:
http.Error(w, "not found", http.StatusNotFound)
}
}))
defer server.Close()
tool := New(Config{
BaseURL: server.URL,
Token: "tok",
Owner: "o",
Repo: "r",
}, nil)
in := validTicketInput()
data, _ := json.Marshal(in)
_, err := tool.Call(context.Background(), string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
mu.Lock()
defer mu.Unlock()
if len(createdLabels) != 4 {
t.Errorf("expected 4 labels created, got %d: %v", len(createdLabels), createdLabels)
}
}
func TestFormatResult(t *testing.T) {
issue := &giteaIssueResp{
Number: 99,
Title: "测试任务",
State: "open",
HTMLURL: "https://gitea.example.com/issues/99",
}
ticket := &TicketInput{
ParentReferenceID: "ENAB_KAFKA_001",
Labels: []string{"type/enabler"},
EstimatedHours: 4,
}
result := formatResult(issue, ticket)
if !strings.Contains(result, "#99") {
t.Error("missing issue number")
}
if !strings.Contains(result, "ENAB_KAFKA_001") {
t.Error("missing parent reference")
}
if !strings.Contains(result, "4 小时") {
t.Error("missing estimated hours")
}
}

309
tools/piplan/piplan.go Normal file
View File

@@ -0,0 +1,309 @@
package piplan
import (
"context"
"encoding/json"
"fmt"
"strings"
"laodingbot/internal/logger"
)
// Feature 产品经理视角输出的业务特性。
type Feature struct {
FeatureID string `json:"feature_id"`
Title string `json:"title"`
BenefitHypothesis string `json:"benefit_hypothesis"`
AcceptanceCriteria []string `json:"acceptance_criteria"`
}
// Enabler 系统架构师视角输出的技术赋能特性(架构跑道)。
type Enabler struct {
EnablerID string `json:"enabler_id"`
Title string `json:"title"`
ArchitecturalPurpose string `json:"architectural_purpose"`
}
// NFRs 非功能性需求。
type NFRs struct {
Performance string `json:"performance"`
Security string `json:"security"`
}
// Dependency RTE 梳理的任务依赖关系。
type Dependency struct {
SourceID string `json:"source_id"`
TargetID string `json:"target_id"`
Reason string `json:"reason"`
}
// PIPlanInput publish_pi_plan 工具的完整输入结构。
type PIPlanInput struct {
PIVision string `json:"pi_vision"`
Features []Feature `json:"features"`
Enablers []Enabler `json:"enablers"`
NFRs NFRs `json:"nfrs"`
Dependencies []Dependency `json:"dependencies"`
}
// Tool 实现 SAFe PI 规划发布工具。
type Tool struct {
maxOutputChars int
log *logger.Logger
}
// New 创建一个新的 publish_pi_plan 工具实例。
func New(maxOutputChars int, log *logger.Logger) *Tool {
if maxOutputChars <= 0 {
maxOutputChars = 20000
}
return &Tool{
maxOutputChars: maxOutputChars,
log: log,
}
}
func (t *Tool) Name() string { return "publish_pi_plan" }
func (t *Tool) Description() string {
return `当铁三角PM, 架构师, RTE完成 PI 规划推演后,调用此工具输出标准化的架构蓝图与任务清单。输入为 JSON包含 pi_vision, features, enablers, nfrs, dependencies 字段。`
}
func (t *Tool) Call(ctx context.Context, input string) (string, error) {
plan, err := parseInput(input)
if err != nil {
return "", fmt.Errorf("publish_pi_plan: invalid input: %w", err)
}
if err := validate(plan); err != nil {
return "", fmt.Errorf("publish_pi_plan: validation failed: %w", err)
}
if t.log != nil {
t.log.Infof("publish_pi_plan: features=%d enablers=%d deps=%d",
len(plan.Features), len(plan.Enablers), len(plan.Dependencies))
}
output := render(plan)
if len([]rune(output)) > t.maxOutputChars {
output = string([]rune(output)[:t.maxOutputChars])
}
return output, nil
}
func parseInput(input string) (*PIPlanInput, error) {
raw := strings.TrimSpace(input)
if raw == "" {
return nil, fmt.Errorf("empty input")
}
var plan PIPlanInput
if err := json.Unmarshal([]byte(raw), &plan); err != nil {
return nil, fmt.Errorf("JSON parse error: %w", err)
}
return &plan, nil
}
func validate(p *PIPlanInput) error {
if strings.TrimSpace(p.PIVision) == "" {
return fmt.Errorf("pi_vision is required")
}
if len(p.Features) == 0 {
return fmt.Errorf("at least one feature is required")
}
for i, f := range p.Features {
if strings.TrimSpace(f.FeatureID) == "" {
return fmt.Errorf("features[%d].feature_id is required", i)
}
if strings.TrimSpace(f.Title) == "" {
return fmt.Errorf("features[%d].title is required", i)
}
if strings.TrimSpace(f.BenefitHypothesis) == "" {
return fmt.Errorf("features[%d].benefit_hypothesis is required", i)
}
if len(f.AcceptanceCriteria) == 0 {
return fmt.Errorf("features[%d].acceptance_criteria requires at least one item", i)
}
}
for i, e := range p.Enablers {
if strings.TrimSpace(e.EnablerID) == "" {
return fmt.Errorf("enablers[%d].enabler_id is required", i)
}
if strings.TrimSpace(e.Title) == "" {
return fmt.Errorf("enablers[%d].title is required", i)
}
if strings.TrimSpace(e.ArchitecturalPurpose) == "" {
return fmt.Errorf("enablers[%d].architectural_purpose is required", i)
}
}
if strings.TrimSpace(p.NFRs.Performance) == "" {
return fmt.Errorf("nfrs.performance is required")
}
if strings.TrimSpace(p.NFRs.Security) == "" {
return fmt.Errorf("nfrs.security is required")
}
for i, d := range p.Dependencies {
if strings.TrimSpace(d.SourceID) == "" {
return fmt.Errorf("dependencies[%d].source_id is required", i)
}
if strings.TrimSpace(d.TargetID) == "" {
return fmt.Errorf("dependencies[%d].target_id is required", i)
}
}
return nil
}
// render 将 PI 规划输入渲染为标准化的 Markdown 架构蓝图与任务清单。
func render(p *PIPlanInput) string {
var b strings.Builder
// ── 标题 ──
b.WriteString("# PI 规划架构蓝图与任务清单\n\n")
// ── 1. PI 愿景 ──
b.WriteString("## 1. PI 愿景\n\n")
b.WriteString(strings.TrimSpace(p.PIVision))
b.WriteString("\n\n")
// ── 2. 业务特性清单 (Features) ──
b.WriteString("## 2. 业务特性清单 (Features)\n\n")
for _, f := range p.Features {
b.WriteString(fmt.Sprintf("### %s — %s\n\n", f.FeatureID, f.Title))
b.WriteString(fmt.Sprintf("**业务价值假设**: %s\n\n", f.BenefitHypothesis))
b.WriteString("**验收标准 (AC)**:\n\n")
for j, ac := range f.AcceptanceCriteria {
b.WriteString(fmt.Sprintf("- [ ] AC-%d: %s\n", j+1, ac))
}
b.WriteString("\n")
}
// ── 3. 技术赋能特性 (Enablers / 架构跑道) ──
b.WriteString("## 3. 技术赋能特性 (Enablers / 架构跑道)\n\n")
if len(p.Enablers) == 0 {
b.WriteString("_无技术赋能特性。_\n\n")
} else {
b.WriteString("| Enabler ID | 名称 | 架构意图 |\n")
b.WriteString("|------------|------|----------|\n")
for _, e := range p.Enablers {
b.WriteString(fmt.Sprintf("| %s | %s | %s |\n",
e.EnablerID, e.Title, e.ArchitecturalPurpose))
}
b.WriteString("\n")
}
// ── 4. 非功能性需求 (NFRs) ──
b.WriteString("## 4. 非功能性需求 (NFRs)\n\n")
b.WriteString(fmt.Sprintf("- **性能**: %s\n", p.NFRs.Performance))
b.WriteString(fmt.Sprintf("- **安全与合规**: %s\n", p.NFRs.Security))
b.WriteString("\n")
// ── 5. 依赖关系图 ──
b.WriteString("## 5. 依赖关系\n\n")
if len(p.Dependencies) == 0 {
b.WriteString("_无跨任务依赖。_\n\n")
} else {
b.WriteString("| 前置任务 (Source) | 后续任务 (Target) | 依赖原因 |\n")
b.WriteString("|-------------------|-------------------|----------|\n")
for _, d := range p.Dependencies {
reason := d.Reason
if reason == "" {
reason = "—"
}
b.WriteString(fmt.Sprintf("| %s | %s | %s |\n",
d.SourceID, d.TargetID, reason))
}
b.WriteString("\n")
}
// ── 6. 建议执行顺序 ──
b.WriteString("## 6. 建议执行顺序\n\n")
order := computeExecutionOrder(p)
for i, id := range order {
b.WriteString(fmt.Sprintf("%d. %s\n", i+1, id))
}
b.WriteString("\n")
// ── 7. 质量门禁检查清单 ──
b.WriteString("## 7. 质量门禁检查清单\n\n")
b.WriteString("### 业务验收测试用例\n\n")
for _, f := range p.Features {
for j, ac := range f.AcceptanceCriteria {
b.WriteString(fmt.Sprintf("- [ ] [%s] AC-%d: %s\n", f.FeatureID, j+1, ac))
}
}
b.WriteString("\n### 非功能性验证\n\n")
b.WriteString(fmt.Sprintf("- [ ] 性能压测: %s\n", p.NFRs.Performance))
b.WriteString(fmt.Sprintf("- [ ] 安全扫描: %s\n", p.NFRs.Security))
b.WriteString("\n")
return b.String()
}
// computeExecutionOrder 根据依赖关系计算拓扑排序的执行顺序。
// 先排 Enabler再排 Feature无依赖的排在前面。
func computeExecutionOrder(p *PIPlanInput) []string {
// 收集所有 ID
allIDs := make([]string, 0, len(p.Enablers)+len(p.Features))
idSet := make(map[string]bool)
for _, e := range p.Enablers {
allIDs = append(allIDs, e.EnablerID)
idSet[e.EnablerID] = true
}
for _, f := range p.Features {
allIDs = append(allIDs, f.FeatureID)
idSet[f.FeatureID] = true
}
// 构建入度表和邻接表
inDegree := make(map[string]int)
adj := make(map[string][]string)
for _, id := range allIDs {
inDegree[id] = 0
}
for _, d := range p.Dependencies {
if !idSet[d.SourceID] || !idSet[d.TargetID] {
continue
}
adj[d.SourceID] = append(adj[d.SourceID], d.TargetID)
inDegree[d.TargetID]++
}
// Kahn 拓扑排序
queue := make([]string, 0)
// 先加入度为 0 的 Enabler再加入度为 0 的 Feature保持稳定顺序
for _, e := range p.Enablers {
if inDegree[e.EnablerID] == 0 {
queue = append(queue, e.EnablerID)
}
}
for _, f := range p.Features {
if inDegree[f.FeatureID] == 0 {
queue = append(queue, f.FeatureID)
}
}
var result []string
for len(queue) > 0 {
curr := queue[0]
queue = queue[1:]
result = append(result, curr)
for _, next := range adj[curr] {
inDegree[next]--
if inDegree[next] == 0 {
queue = append(queue, next)
}
}
}
// 如果存在环,将未排序的节点追加到末尾并标记
if len(result) < len(allIDs) {
for _, id := range allIDs {
if inDegree[id] > 0 {
result = append(result, id+" ⚠️(循环依赖)")
}
}
}
return result
}

334
tools/piplan/piplan_test.go Normal file
View File

@@ -0,0 +1,334 @@
package piplan
import (
"context"
"encoding/json"
"strings"
"testing"
)
func validInput() PIPlanInput {
return PIPlanInput{
PIVision: "实现车云一体化 OTA 系统,支撑百万级终端设备的安全固件升级",
Features: []Feature{
{
FeatureID: "FEAT_OTA_001",
Title: "云端固件版本依赖检查",
BenefitHypothesis: "上线后减少 30% 的固件回退率",
AcceptanceCriteria: []string{
"上传固件时自动解析并记录版本依赖关系",
"下发升级任务时自动校验设备当前版本是否满足依赖",
"不满足依赖时返回明确的错误提示及所需前置版本",
},
},
{
FeatureID: "FEAT_OTA_002",
Title: "端侧断点续传",
BenefitHypothesis: "弱网环境下固件下载成功率提升至 99.5%",
AcceptanceCriteria: []string{
"支持分片下载与本地缓存校验",
"网络恢复后自动续传,无需用户干预",
},
},
},
Enablers: []Enabler{
{
EnablerID: "ENAB_KAFKA_001",
Title: "搭建跨可用区的高可用 Kafka 集群",
ArchitecturalPurpose: "为高并发 OTA 状态机提供可靠消息管道",
},
{
EnablerID: "ENAB_S3_001",
Title: "对象存储多区域同步",
ArchitecturalPurpose: "保证固件文件在多区域的低延迟分发",
},
},
NFRs: NFRs{
Performance: "API 响应时间 P99 < 200ms吞吐量 > 10000 QPS",
Security: "车云通信必须使用 TLS 1.3,敏感数据必须脱敏",
},
Dependencies: []Dependency{
{
SourceID: "ENAB_KAFKA_001",
TargetID: "FEAT_OTA_001",
Reason: "版本检查服务依赖 Kafka 进行异步事件通知",
},
{
SourceID: "ENAB_S3_001",
TargetID: "FEAT_OTA_002",
Reason: "断点续传需要对象存储支持 Range 请求",
},
},
}
}
func TestParseInputValid(t *testing.T) {
in := validInput()
data, _ := json.Marshal(in)
plan, err := parseInput(string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if plan.PIVision != in.PIVision {
t.Errorf("pi_vision mismatch: got %q", plan.PIVision)
}
if len(plan.Features) != 2 {
t.Errorf("expected 2 features, got %d", len(plan.Features))
}
if len(plan.Enablers) != 2 {
t.Errorf("expected 2 enablers, got %d", len(plan.Enablers))
}
if len(plan.Dependencies) != 2 {
t.Errorf("expected 2 dependencies, got %d", len(plan.Dependencies))
}
}
func TestParseInputEmpty(t *testing.T) {
_, err := parseInput("")
if err == nil {
t.Fatal("expected error for empty input")
}
}
func TestParseInputInvalidJSON(t *testing.T) {
_, err := parseInput("{not json}")
if err == nil {
t.Fatal("expected error for invalid JSON")
}
}
func TestValidateMissingVision(t *testing.T) {
in := validInput()
in.PIVision = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "pi_vision") {
t.Fatalf("expected pi_vision error, got: %v", err)
}
}
func TestValidateNoFeatures(t *testing.T) {
in := validInput()
in.Features = nil
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "feature") {
t.Fatalf("expected feature error, got: %v", err)
}
}
func TestValidateFeatureMissingID(t *testing.T) {
in := validInput()
in.Features[0].FeatureID = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "feature_id") {
t.Fatalf("expected feature_id error, got: %v", err)
}
}
func TestValidateFeatureMissingAC(t *testing.T) {
in := validInput()
in.Features[0].AcceptanceCriteria = nil
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "acceptance_criteria") {
t.Fatalf("expected acceptance_criteria error, got: %v", err)
}
}
func TestValidateEnablerMissingPurpose(t *testing.T) {
in := validInput()
in.Enablers[0].ArchitecturalPurpose = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "architectural_purpose") {
t.Fatalf("expected architectural_purpose error, got: %v", err)
}
}
func TestValidateNFRsMissingPerformance(t *testing.T) {
in := validInput()
in.NFRs.Performance = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "performance") {
t.Fatalf("expected performance error, got: %v", err)
}
}
func TestValidateNFRsMissingSecurity(t *testing.T) {
in := validInput()
in.NFRs.Security = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "security") {
t.Fatalf("expected security error, got: %v", err)
}
}
func TestValidateDepMissingSourceID(t *testing.T) {
in := validInput()
in.Dependencies[0].SourceID = ""
err := validate(&in)
if err == nil || !strings.Contains(err.Error(), "source_id") {
t.Fatalf("expected source_id error, got: %v", err)
}
}
func TestRenderContainsSections(t *testing.T) {
in := validInput()
out := render(&in)
sections := []string{
"# PI 规划架构蓝图与任务清单",
"## 1. PI 愿景",
"## 2. 业务特性清单 (Features)",
"## 3. 技术赋能特性 (Enablers / 架构跑道)",
"## 4. 非功能性需求 (NFRs)",
"## 5. 依赖关系",
"## 6. 建议执行顺序",
"## 7. 质量门禁检查清单",
}
for _, s := range sections {
if !strings.Contains(out, s) {
t.Errorf("output missing section: %s", s)
}
}
}
func TestRenderContainsFeatureDetails(t *testing.T) {
in := validInput()
out := render(&in)
if !strings.Contains(out, "FEAT_OTA_001") {
t.Error("output missing FEAT_OTA_001")
}
if !strings.Contains(out, "云端固件版本依赖检查") {
t.Error("output missing feature title")
}
if !strings.Contains(out, "AC-1") {
t.Error("output missing acceptance criteria numbering")
}
}
func TestRenderContainsEnablerTable(t *testing.T) {
in := validInput()
out := render(&in)
if !strings.Contains(out, "ENAB_KAFKA_001") {
t.Error("output missing ENAB_KAFKA_001")
}
if !strings.Contains(out, "ENAB_S3_001") {
t.Error("output missing ENAB_S3_001")
}
}
func TestRenderContainsNFRs(t *testing.T) {
in := validInput()
out := render(&in)
if !strings.Contains(out, "P99 < 200ms") {
t.Error("output missing performance NFR")
}
if !strings.Contains(out, "TLS 1.3") {
t.Error("output missing security NFR")
}
}
func TestRenderContainsDependencies(t *testing.T) {
in := validInput()
out := render(&in)
if !strings.Contains(out, "ENAB_KAFKA_001") || !strings.Contains(out, "FEAT_OTA_001") {
t.Error("output missing dependency pair")
}
}
func TestComputeExecutionOrder(t *testing.T) {
in := validInput()
order := computeExecutionOrder(&in)
// Enablers should come before their dependent Features
enablerIdx := map[string]int{}
featureIdx := map[string]int{}
for i, id := range order {
if strings.HasPrefix(id, "ENAB_") {
enablerIdx[id] = i
} else if strings.HasPrefix(id, "FEAT_") {
featureIdx[id] = i
}
}
if enablerIdx["ENAB_KAFKA_001"] >= featureIdx["FEAT_OTA_001"] {
t.Error("ENAB_KAFKA_001 should come before FEAT_OTA_001")
}
if enablerIdx["ENAB_S3_001"] >= featureIdx["FEAT_OTA_002"] {
t.Error("ENAB_S3_001 should come before FEAT_OTA_002")
}
}
func TestComputeExecutionOrderNoDeps(t *testing.T) {
in := validInput()
in.Dependencies = nil
order := computeExecutionOrder(&in)
if len(order) != 4 {
t.Errorf("expected 4 items, got %d", len(order))
}
// Enablers first, then Features (stable order)
if order[0] != "ENAB_KAFKA_001" || order[1] != "ENAB_S3_001" {
t.Errorf("enablers should come first, got: %v", order)
}
}
func TestCallEndToEnd(t *testing.T) {
tool := New(0, nil)
in := validInput()
data, _ := json.Marshal(in)
result, err := tool.Call(context.Background(), string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(result, "PI 规划架构蓝图") {
t.Error("output missing title")
}
}
func TestCallInvalidInput(t *testing.T) {
tool := New(0, nil)
_, err := tool.Call(context.Background(), "not json")
if err == nil {
t.Fatal("expected error for invalid input")
}
}
func TestCallMissingRequiredField(t *testing.T) {
tool := New(0, nil)
in := validInput()
in.PIVision = ""
data, _ := json.Marshal(in)
_, err := tool.Call(context.Background(), string(data))
if err == nil {
t.Fatal("expected validation error")
}
}
func TestNameAndDescription(t *testing.T) {
tool := New(0, nil)
if tool.Name() != "publish_pi_plan" {
t.Errorf("unexpected name: %s", tool.Name())
}
if tool.Description() == "" {
t.Error("description should not be empty")
}
}
func TestMaxOutputTruncation(t *testing.T) {
tool := New(100, nil)
in := validInput()
data, _ := json.Marshal(in)
result, err := tool.Call(context.Background(), string(data))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result) > 100 {
t.Errorf("output should be truncated to 100 chars, got %d", len(result))
}
}

View File

@@ -15,8 +15,8 @@ import (
// Config 定义了网络搜索工具所需的配置参数。
type Config struct {
Engine string // 搜索引擎类型,支持 "duckduckgo" 或 "brave"
APIKey string // 搜索引擎的 API KeyBrave 搜索必填)
Engine string // 搜索引擎类型,支持 "duckduckgo"、"brave" 或 "tavily"
APIKey string // 搜索引擎的 API KeyBrave 或 Tavily 搜索必填)
}
// Tool represents a web search tool.
@@ -85,6 +85,8 @@ func (t *Tool) Call(ctx context.Context, input string) (string, error) {
switch t.engine {
case "brave":
result, err = t.searchBrave(ctx, query)
case "tavily":
result, err = t.searchTavily(ctx, query)
default:
result, err = t.searchDuckDuckGo(ctx, query)
}
@@ -126,12 +128,20 @@ func (t *Tool) searchDuckDuckGo(ctx context.Context, query string) (string, erro
return "", fmt.Errorf("read response body failed: %w", err)
}
if t.log != nil {
t.log.Debugf("duckduckgo raw response: %s", string(body))
}
var ddg duckDuckGoResponse
if err := json.Unmarshal(body, &ddg); err != nil {
return "", fmt.Errorf("parse duckduckgo response failed: %w", err)
}
return t.formatDuckDuckGoResult(query, ddg), nil
result := t.formatDuckDuckGoResult(query, ddg)
if t.log != nil {
t.log.Infof("duckduckgo search finished, content_found=%v", (ddg.Answer != "" || ddg.AbstractText != "" || len(ddg.RelatedTopics) > 0))
}
return result, nil
}
// duckDuckGoResponse 从 DuckDuckGo 获取的即时结果 JSON 映射结构。
@@ -239,11 +249,19 @@ func (t *Tool) searchBrave(ctx context.Context, query string) (string, error) {
return "", fmt.Errorf("read response body failed: %w", err)
}
if t.log != nil {
t.log.Debugf("brave search raw response: %s", string(body))
}
var braveResp braveSearchResponse
if err := json.Unmarshal(body, &braveResp); err != nil {
return "", fmt.Errorf("parse brave response failed: %w", err)
}
if t.log != nil {
t.log.Infof("brave search finished, results_count=%d", len(braveResp.Web.Results))
}
return t.formatBraveResult(query, braveResp), nil
}
@@ -286,3 +304,92 @@ func (t *Tool) formatBraveResult(query string, resp braveSearchResponse) string
return strings.TrimSpace(b.String())
}
// searchTavily uses the Tavily Search API (requires API key).
func (t *Tool) searchTavily(ctx context.Context, query string) (string, error) {
if t.apiKey == "" {
return "", fmt.Errorf("WEB_SEARCH_API_KEY is required for Tavily engine")
}
apiURL := "https://api.tavily.com/search"
payload := map[string]interface{}{
"api_key": t.apiKey,
"query": query,
"search_depth": "basic",
"include_answer": true,
"include_images": false,
"include_raw_content": false,
"max_results": 5,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("marshal tavily payload failed: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, strings.NewReader(string(jsonData)))
if err != nil {
return "", fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := t.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodySnippet, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return "", fmt.Errorf("tavily search returned status %d: %s", resp.StatusCode, string(bodySnippet))
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 512*1024))
if err != nil {
return "", fmt.Errorf("read response body failed: %w", err)
}
if t.log != nil {
t.log.Debugf("tavily search raw response: %s", string(body))
}
var tavilyResp tavilyResponse
if err := json.Unmarshal(body, &tavilyResp); err != nil {
return "", fmt.Errorf("parse tavily response failed: %w", err)
}
return t.formatTavilyResult(query, tavilyResp), nil
}
type tavilyResponse struct {
Answer string `json:"answer"`
Results []tavilyResult `json:"results"`
}
type tavilyResult struct {
Title string `json:"title"`
URL string `json:"url"`
Content string `json:"content"`
Score float64 `json:"score"`
}
func (t *Tool) formatTavilyResult(query string, resp tavilyResponse) string {
b := strings.Builder{}
b.WriteString("Search: " + query + "\n")
b.WriteString("Engine: Tavily\n\n")
if resp.Answer != "" {
b.WriteString("Answer: " + resp.Answer + "\n\n")
}
if len(resp.Results) == 0 && resp.Answer == "" {
b.WriteString("No results found.\n")
return strings.TrimSpace(b.String())
}
for i, r := range resp.Results {
b.WriteString(fmt.Sprintf("%d. %s\n %s\n %s\n\n", i+1, r.Title, r.URL, r.Content))
}
return strings.TrimSpace(b.String())
}

View File

@@ -1,9 +1,62 @@
package websearch
import (
"context"
"fmt"
"testing"
)
func TestTavilyIntegration(t *testing.T) {
// 这是一个集成测试,会实际请求网络。
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
// 这里的 API Key 从环境变量中获取或手动填写的测试环境变量中读取。
apiKey := "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"
if apiKey == "" {
t.Skip("TAVILY_API_KEY is not set")
}
tool := New(Config{Engine: "tavily", APIKey: apiKey}, 4000, nil)
ctx := context.Background()
query := "wuhan weather today"
result, err := tool.Call(ctx, query)
if err != nil {
t.Fatalf("Tavily search failed: %v", err)
}
fmt.Printf("Tavily search result for '%s':\n%s\n", query, result)
if result == "" {
t.Fatal("expected non-empty result from Tavily")
}
}
func TestDuckDuckGoIntegration(t *testing.T) {
// 这是一个集成测试,会实际请求网络。
// 在 CI 环境中可能需要跳过。
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
tool := New(Config{Engine: "duckduckgo"}, 4000, nil)
ctx := context.Background()
query := "wuhan weather"
result, err := tool.Call(ctx, query)
if err != nil {
t.Fatalf("DuckDuckGo search failed: %v", err)
}
fmt.Printf("DuckDuckGo search result for '%s':\n%s\n", query, result)
if result == "" {
t.Fatal("expected non-empty result from DuckDuckGo")
}
}
func TestNewDefaultEngine(t *testing.T) {
tool := New(Config{}, 4000, nil)
if tool.Name() != "web_search" {