Spring AI LoomAgent API Documentation
Base URL:
http://localhost:8080(default port for the test environment) Version: 1.2.0 Authentication: The project uses a BFF (Backend-For-Frontend) + HttpOnly Cookie auth model. After login, the server sets aloom-agent-sessioncookie viaSet-Cookieheader. The browser automatically includes this cookie in subsequent requests. No token storage or manual header management is required.
Table of Contents
- 1. User Authentication
- 2. Conversation Management
- 3. Streaming Chat
- 4. File Management
- 5. Knowledge Base Management
- 6. Skill Management
- 7. MCP Tools
- 8. Skill Tools (LLM-callable)
- 9. Knowledge Tools (LLM-callable)
- 10. Terminal Management
- 11. Sub-task & Schedule APIs
- 12. Admin APIs
- 13. Data Models
- 14. Configuration Properties
- Appendix: Endpoint Summary
1. User Authentication
1.1 Check Auto-Login Status
POST /spring/ai/loom/user/isAutoLogin
Request Body: None
Response: boolean
| Value | Description |
|---|---|
true |
Auto-login is enabled |
false |
Auto-login is not enabled |
1.2 User Login
POST /spring/ai/loom/user/login
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | Username |
verified |
string | No | Verification info |
Example:
{
"username": "testuser",
"verified": ""
}
Response (UserResponseRecord):
| Field | Type | Description |
|---|---|---|
token |
string | Session token (also set as HttpOnly cookie) |
nickname |
string | User nickname |
Response Headers:
Set-Cookie: loom-agent-session=<token>; Max-Age=86400; Path=/; HttpOnly; SameSite=Lax
Example:
{
"token": "567fb50c-b293-403b-a903-f6b7b597c318",
"nickname": "用户"
}
1.3 User Logout
POST /spring/ai/loom/user/logout
Request Body: None
Response: true (boolean)
Effect: The session token is invalidated server-side and the loom-agent-session cookie is cleared (Max-Age=0).
2. Conversation Management
2.1 List Conversations
GET /spring/ai/loom/conversation
Headers: Must include authentication info.
Response: ConversationRecord[]
| Field | Type | Description |
|---|---|---|
conversationId |
string | Conversation ID |
title |
string | Conversation title |
Example:
[
{
"conversationId": "conv-001",
"title": "First Conversation"
}
]
2.2 Get Conversation History
GET /spring/ai/loom/conversation/{conversationId}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
conversationId |
string | Conversation ID |
Response: Object[] — Spring AI chat memory messages plus injected tool-call entries.
The array contains two shapes:
(a) Chat memory messages (unchanged shape — USER / ASSISTANT):
| Field | Type | Description |
|---|---|---|
messageType |
string | USER or ASSISTANT |
text |
string | Message content |
metadata |
object | For ASSISTANT, may carry thinking (per-turn reasoning, injected by ChatReasoningHistory.attachThinking) |
media |
array | Attached media (usually empty) |
(b) Tool-call entries (messageType = "TOOL_CALL", injected by ToolCallHistory from loom_tool_call_log):
| Field | Type | Description |
|---|---|---|
messageType |
string | Always TOOL_CALL |
toolName |
string | e.g. askUser, startSubTask, getCurrentTime |
toolCallId |
string | Spring AI tool-call id |
arguments |
object | Parsed tool arguments. Parse failure degrades to {"_raw": "...", "_parseError": true} |
result |
string | Tool return value, truncated to 2 KB |
isError |
boolean | Whether the tool returned an error |
durationMs |
number | Tool execution time |
truncated |
boolean | true when result was truncated |
createdAt |
string | ISO-8601 timestamp |
Why tool calls are injected separately: SPRING_AI_CHAT_MEMORY has no metadata column (only conversation_id / content / type / timestamp / sequence_id), so tool invocations are not stored there. Without injection, historical replay would lose the askUser cards and sub-task chips entirely.
Truncation thresholds (two layers, deliberately distinct): the whole arguments JSON triggers truncation above 1024 chars, then each non-exempt field is capped at 256 chars; result is capped at 2048 chars. askUser.optionsJson is exempt — it is the sole source of the card's options.
Cross-user read: returns 403 with an empty array (same shape as success, so client code does not break).
2.3 Delete Conversation
DELETE /spring/ai/loom/conversation/{conversationId}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
conversationId |
string | Conversation ID |
Response: true (boolean)
3. Streaming Chat
3.1 SSE Streaming Chat
POST /spring/ai/loom/stream
Content-Type: application/json
Accept: text/event-stream
Request Body (ChatRequestRecord):
| Field | Type | Required | Description |
|---|---|---|---|
message |
string | Yes | User message content |
conversationId |
string | No | Conversation ID. A new session is created if omitted. |
mcps |
string[] | No | List of MCP tool names to enable |
enabledKnowledgeIds |
string[] | No | Knowledge base IDs for RAG retrieval (multiple) |
fileIds |
string[] | No | List of associated file IDs (supports multiple files) |
selectedSkillName |
string | No | Skill name picked via the / picker; injected as a strong directive into the system prompt, bypassing LLM tool-selection bias |
enabledToolGroups |
string[] | No | Tool groups the user ticked in the panel. null/empty falls back to the role-granted full set; non-empty is intersected with role grants |
Example:
{
"message": "Please summarize this document",
"conversationId": "conv-001",
"knowledgeId": "kb-001",
"mcps": [],
"fileIds": ["file-abc123", "file-def456"]
}
Response: SSE (Server-Sent Events) stream.
Each event returns a ChatResponseRecord:
| Field | Type | Description |
|---|---|---|
content |
string | AI response text fragment |
reasoningContent |
string | Reasoning/thinking trace (optional) |
askUser |
object | null | 3rd component. null = normal content frame; non-null = an askUser question-card event (AskUserEvent, fields below). The frontend renders an inline choice card and the askUser tool blocks until answered |
subTaskEvent |
object | null | 4th component. null = normal frame; non-null = a sub-task lifecycle event (SubTaskEvent) driving the compact chip shown live in the chat stream |
askUser (AskUserEvent) fields:
| Field | Type | Description |
|---|---|---|
questionId |
string | UUID; index for the answer endpoint |
question |
string | Question body |
header |
string | Short title/chip (nullable) |
background |
string | Background context (nullable) |
options |
object[] | 2-4 options, each {label, description} |
multiSelect |
boolean | Whether multiple options can be selected |
allowCustomInput |
boolean | Whether a custom "other" input is allowed |
timeoutSeconds |
number | Frontend countdown; same value as the tool's blocking timeout |
SSE Event Example:
data: {"content":"Hello","reasoningContent":""}
data: {"content":"!","reasoningContent":""}
data: {"content":"How can","reasoningContent":""}
data: {"content":"I help you?","reasoningContent":""}
3.2 AskUser answer endpoint
POST /spring/ai/loom/ask/{questionId}/answer
Content-Type: application/json
Submits the answer to a question card (the askUser tool is blocking and waiting).
- Request body:
{"answer": "option label"}(single-select / custom input) or{"answer": ["label1","label2"]}(multi-select) 200 {"ok":true}— answer delivered, the blocked tool thread is woken up400 {"error":"invalid answer"}— malformed body /answermissing or blank404 {"error":"not found"}— unknown questionId, cross-user submission, or the question already timed out / was cancelled (all three share one response to prevent existence leaks)
4. File Management
4.1 Upload File
POST /spring/ai/loom/file/upload
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | File to upload |
Response:
| Field | Type | Description |
|---|---|---|
fileId |
string | File ID returned after upload |
status |
string | Upload status, "success" on success |
Example:
{
"fileId": "file-abc123",
"status": "success"
}
4.2 List File Tree
GET /spring/ai/loom/file
GET /spring/ai/loom/file/tree
Response: Directory tree JSON (recursive structure).
| Field | Type | Description |
|---|---|---|
name |
string | File or directory name |
type |
string | "file" or "directory" |
size |
number | File size in bytes (files only) |
children |
array | Child items (directories only) |
Example:
{
"name": ".",
"type": "directory",
"children": [
{
"name": "report.pdf",
"type": "file",
"size": 102400
},
{
"name": "docs",
"type": "directory",
"children": [
{ "name": "guide.md", "type": "file", "size": 4096 }
]
}
]
}
4.3 View File by Path
GET /spring/ai/loom/file/by-path/view?path=report.pdf
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | File path relative to user's file directory |
Response: 307 Temporary Redirect → /file/view/{fileId} (WOPI file viewer).
If the file is not yet registered in
file_info, a temporary record (usage="temp") is auto-created to obtain afileId.
4.4 Download File by Path
GET /spring/ai/loom/file/by-path/download?path=report.pdf
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
path |
string | Yes | File path relative to user's file directory |
Response: 307 Temporary Redirect → /wopi/files/{fileId}/contents.
4.5 Download File by ID
GET /spring/ai/loom/file/{id}/download
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id |
string | File ID |
Response: File binary stream, Content-Disposition header includes original filename.
5. Knowledge Base Management
5.0 Feature Flags
GET /spring/ai/loom/api/features
Response: { "knowledge": boolean } — knowledge is true iff the container has a VectorStore bean (RAG chain active). When spring.ai.loom.agent.rag.enabled=false (or no EmbeddingModel bean exists), it is false and the frontend hides the knowledge-space (📚) button.
5.1 Check Knowledge Upload Status
GET /spring/ai/loom/knowledge/checkKnowledgeUpload
Response: boolean — Whether knowledge upload functionality is available (false when RAG is globally disabled).
5.2 List Knowledge Bases
GET /spring/ai/loom/knowledge
Response: KnowledgeRecord[]
| Field | Type | Description |
|---|---|---|
id |
string | Knowledge base ID |
username |
string | Creator username |
name |
string | Knowledge base name |
Example:
[
{
"id": "kb-001",
"username": "admin",
"name": "Product Docs"
}
]
5.3 Create Knowledge Base
PUT /spring/ai/loom/knowledge
Content-Type: application/json
Request Body (KnowledgeRecord):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Knowledge base name |
Example:
{
"name": "Product Docs"
}
Response: The created KnowledgeRecord (including the generated id).
5.4 Delete Knowledge Base
DELETE /spring/ai/loom/knowledge/{knowledgeId}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
knowledgeId |
string | Knowledge base ID |
Deleting a knowledge base cascades to clean up associated files and vector data.
Response: number — Number of deleted records.
5.5 Upload File to Knowledge Base
POST /spring/ai/loom/knowledge/{knowledgeId}/upload
Content-Type: multipart/form-data
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
knowledgeId |
string | Knowledge base ID |
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | File to upload |
Response:
{
"fileId": "file-xyz789",
"status": "success"
}
5.6 List Files in Knowledge Base
GET /spring/ai/loom/knowledge/{knowledgeId}/file
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
knowledgeId |
string | Knowledge base ID |
Response: FileRecord[] (same format as 4.2).
5.7 Delete File from Knowledge Base
DELETE /spring/ai/loom/knowledge/{knowledgeId}/file/{fileId}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
knowledgeId |
string | Knowledge base ID |
fileId |
string | File ID |
Response: number — Number of deleted records.
5.8 Knowledge Market
Knowledge market enables sharing knowledge bases across users. Approval flow: submit → PENDING → admin approve/reject (
POST /admin/market-knowledge/{id}/approve|reject); only APPROVED entries are publicly listed and pullable. Re-submitting a REJECTED entry archives the old row (id-preserving, incl. reject comment/reviewer/time) intoloom_market_knowledge_archiveand creates a NEW PENDING row.
5.8.1 Browse Approved Market Knowledge Bases
Kept v1 leg (roles.js dependency):
GET /api/knowledge-marketstill returns the APPROVED-only list; the richer v2 list (GET /market-knowledge, paged with tag/category filters) is the primary browse endpoint for the frontend.
GET /spring/ai/loom/api/knowledge-market?page=1&size=20
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
int | No | Page number (default 1) |
size |
int | No | Items per page (default 20) |
Response: MarketKnowledgeRecord[] — Approved market knowledge bases.
| Field | Type | Description |
|---|---|---|
id |
string | Market knowledge ID |
username |
string | Original author username |
name |
string | Knowledge base name |
description |
string | Knowledge base description |
status |
string | APPROVED |
submittedAt |
string | Submission timestamp |
reviewedAt |
string | Review timestamp |
5.8.2 Subscribe to Market Knowledge Base
POST /spring/ai/loom/api/knowledge-market/{marketId}/pull
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
marketId |
string | Market knowledge ID |
Response: {"success": true} on success. Creates a subscription in loom_user_knowledge with source=MARKET_PULLED. Requires the entry to be APPROVED — non-APPROVED returns 403.
5.8.3 Submit Knowledge Base to Market
POST /spring/ai/loom/api/knowledge/{knowledgeId}/submit
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
knowledgeId |
string | Knowledge base ID |
Response: MarketKnowledgeRecord — The created market entry with status='PENDING' (awaits admin review).
Behavior: If a same-(username, name) entry exists → depends on its status: REJECTED → old row archived to loom_market_knowledge_archive (id-preserving, includes reject comment/reviewer/time), a NEW PENDING row is inserted (new id). PENDING / APPROVED → in-place content update, status untouched (APPROVED never demotes). Otherwise → INSERT a new PENDING row (created_by_kind='USER').
5.8.4 Withdraw Market Submission
DELETE /spring/ai/loom/api/knowledge-market/{marketId}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
marketId |
string | Market knowledge ID |
Response: {"success": true} on success.
Behavior: Unified DELETE endpoint for both author withdraw and admin takedown. Internal permission check decides path:
- Author:
DELETE FROM loom_market_knowledge WHERE id=? AND username=?(only deletes own) - Admin:
DELETE FROM loom_market_knowledge WHERE id=?(any row)
Cascading cleanup (both paths): Auto-deletes loom_user_knowledge (subscriber rows) + loom_role_knowledge (role grants) referencing the market entry.
Approval endpoints (v2 admin router — the v1 /api/knowledge-market/{marketId}/approve|reject paths remain retired; approve/reject now live under /admin):
POST /admin/market-knowledge/{id}/approve— admin approve:status=APPROVED,reviewed_at/reviewed_bysetPOST /admin/market-knowledge/{id}/reject— admin reject:status=REJECTED; body{"comment": "..."}is required (empty/missing →400both client-side and server-side)
Admin endpoints:
GET /admin/market-knowledge— list all market knowledge bases (all statuses: PENDING / APPROVED / REJECTED)POST /admin/market-knowledge— admin create: directAPPROVED+created_by_kind='ADMIN'. Body:MarketCreateRequest(name/description/content/category)PUT /admin/market-knowledge/{id}— edit fields (MarketUpdateRequest) — cannot change status (no status field); state changes only via approve/rejectDELETE /admin/market-knowledge/{marketId}— admin takedown (cascade cleanup ofloom_user_knowledge+loom_role_knowledgereference rows)- Plus per-id admin legs mirroring the skill market:
/official,/featured-rank,/category,/announcement(PUT + DELETE),/reviews/{username}(DELETE),/stats-reset,/tags(PUT + GET)
5.8.5 Admin Approve / Reject Market Submission
POST /spring/ai/loom/admin/market-knowledge/{id}/approve
POST /spring/ai/loom/admin/market-knowledge/{id}/reject
Admin-only (non-admin → 403). approve sets status=APPROVED and records reviewed_at / reviewed_by. reject sets status=REJECTED and requires a non-blank comment in the body ({"comment": "reason"}) — missing/blank → 400. The author sees the reject reason in 我的发布 and can re-submit: the REJECTED row is archived to loom_market_knowledge_archive (id-preserving, includes comment/reviewer/time) and a fresh PENDING row is created.
5.8.6 Admin Reject Market Submission (legacy v1 path)
POST /spring/ai/loom/api/knowledge-market/{marketId}/reject
removed — the v1 KB approve/reject legs are retired; use the v2 admin endpoints
POST /admin/market-knowledge/{id}/approve|reject(see § 5.8.5) instead.
5.8.7 List My Subscribed Knowledge Bases
GET /spring/ai/loom/api/knowledge-market/my-pulled
Response: MarketKnowledgeRecord[] — Knowledge bases the current user has subscribed to from the market.
5.8.8 List My Market Submissions
GET /spring/ai/loom/api/knowledge-market/my-submitted
Response: MarketKnowledgeRecord[] — Knowledge bases the current user has submitted to the market (PENDING / APPROVED / REJECTED).
6. Skill Management
All skills live in the database (tables
market_skill/user_skill/role_skill). The ymlskills[]block is no longer read — it was replaced by 6 system-seededuser_skillrows plus an admin-controlled market workflow.Three sources show up in the
sourcefield ofuser_skill:
USER_CREATED— created by the user via API or chat UI; fully editable (name/desc/content/default_loaded)MARKET_PULLED— pulled from the approved market; default_loaded toggle only (name / content / desc locked to the market snapshot; re-pull to update, or usePOST /skill/{name}/duplicateto copy as a new USER_CREATED skill and edit that)ROLE_GRANTED— auto-injected from a role authorization; read-only (locked=true; cannot edit or delete)Every list/get call auto-syncs
role_skill→user_skillfor the current user, so a freshly authorized skill is visible immediately on next list.
6.1 List Current User's Skills
GET /spring/ai/loom/skill
Response: SkillRecord[] (the LLM-facing view)
| Field | Type | Description |
|---|---|---|
name |
string | Skill name |
description |
string | Skill description |
load |
boolean | Whether the LLM preloads this skill into the system prompt |
content |
string | Skill content (resolved: if stored as classpath:xxx, the resource is read at this point) |
source |
string | USER_CREATED / MARKET_PULLED / ROLE_GRANTED (admin no longer gets a MARKET_VIEW union; the market Tab is the only place to browse APPROVED market entries) |
For admins, this list shows only their own user_skill entries (the same view as for any other user). Browse the approved market via the GET /market-skills endpoint.
6.2 Create / Overwrite a Skill (USER_CREATED)
PUT /spring/ai/loom/skill
Content-Type: application/json
Request Body (SkillRecord):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Skill name |
description |
string | No | Skill description |
load |
boolean | No | Whether the LLM preloads this skill; defaults to true |
content |
string | Yes | Skill content / prompt template (supports classpath: prefix which is resolved on read) |
{param}placeholders insidecontentare LLM-interpreted at runtime, not declared as structured form fields. MCP tool references insidecontentuse@tool_nameand resolve to the role-authorized MCPs.
Example:
{
"name": "email_writer",
"description": "Professional email writing assistant",
"load": true,
"content": "You are an email assistant. The recipient is {recipient}..."
}
Response: true (boolean) — false if a ROLE_GRANTED skill with the same name is locked.
6.3 Patch Description / Default-Loaded
PATCH /spring/ai/loom/skill/{name}
Content-Type: application/json
For USER_CREATED skills — change description and/or default_loaded without overwriting content. For MARKET_PULLED skills — only default_loaded may change; a description change is rejected with 403 (locked to the market snapshot). Returns 400 if the skill is ROLE_GRANTED (locked).
Request Body (UserSkillPatchRequest):
| Field | Type | Description |
|---|---|---|
description |
string | New description (omit to leave unchanged) |
defaultLoaded |
boolean | New default-loaded flag (omit to leave unchanged) |
6.4 Get a Single Skill (LLM-facing)
GET /spring/ai/loom/skill/{name}
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Skill name |
Response: SkillRecord. Admins see only their own user_skill (no market-view fallback — consistent with §6.2).
6.5 Delete a Skill
DELETE /spring/ai/loom/skill/{name}
Returns 400 if the skill is ROLE_GRANTED (locked). Returns true on success.
6.6 Trigger Role Sync Manually
POST /spring/ai/loom/skill/sync
Re-runs the role_skill → user_skill sync for the current user. Mostly for debugging — the same sync runs automatically on every list/get.
6.7 Skill Market — Browse (any user)
GET /spring/ai/loom/market-skills
Returns all market_skill rows with status='APPROVED', ordered by author, name (the version field has been removed — no version DESC ordering any more). Each item has the full MarketSkill model (id, name, description, content, author, status, submittedAt, reviewedAt, reviewedBy, reviewComment).
6.8 Skill Market — View One
GET /spring/ai/loom/market-skills/{id}
Path Parameters: id = market_skill.id (Long)
6.9 Skill Market — Pull into My user_skill
POST /spring/ai/loom/market-skills/{id}/pull
Creates / updates a MARKET_PULLED user_skill row from the given market_skill. Throws 403 if the market skill isn't APPROVED (approval flow — only approved entries are pullable). Throws 400/403 if:
- A
ROLE_GRANTEDlock with the same name already exists - A same-name
USER_CREATEDskill exists in youruser_skill(403— pull refuses to overwrite self-built content; delete it first or useduplicate)
A same-name MARKET_PULLED row is refreshed in place (content updated to the latest market snapshot, no error).
6.10 Submit My Skill to Market
POST /spring/ai/loom/user/market-skills
Content-Type: application/json
Submits a new market_skill row with status=PENDING and author=currentUser (approval flow — awaits admin approve/reject; created_by_kind='USER').
Request Body (MarketSkillSubmitRequest):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Skill name |
description |
string | No | Description |
content |
string | Yes | Prompt template |
The version field has been removed — the (author, name) pair is the unique constraint.
Behavior: If (author, name) already exists in market_skill, the outcome depends on its status:
- REJECTED → the old row is archived to
market_skill_archive(id-preserving, includes reject comment/reviewer/time), and a NEW PENDING row is inserted (new id). The author'suser_skill.market_skill_idbacklink is rebound to the new row. - PENDING / APPROVED → in-place content/desc update, status untouched (APPROVED never demotes back to PENDING).
Otherwise INSERT a new row with status=PENDING. The author's user_skill.market_skill_id is bound to the market_skill row so that subsequent save propagates to all pullers.
6.11 Admin — Market CRUD (any status)
Admin-only. Auth-gated by
auth.adminPathPatternsand double-checked in the handler.
| Method | Path | Description |
|---|---|---|
| GET | /spring/ai/loom/admin/market-skills |
List all statuses (PENDING / APPROVED / REJECTED); optional ?status=PENDING filter (there is no separate /pending endpoint), plus page / size / category / query / sortBy |
| POST | /spring/ai/loom/admin/market-skills |
Admin create — direct status=APPROVED + created_by_kind='ADMIN' (bypasses PENDING). Body: MarketCreateRequest (name / description / content / category) |
| PUT | /spring/ai/loom/admin/market-skills/{id} |
Edit any field of any market skill. Body: MarketUpdateRequest — cannot change status (no status field); state changes only via approve/reject |
| DELETE | /spring/ai/loom/admin/market-skills/{id} |
Cascade-deletes from user_skill and role_skill(这就是"下架",拉取者失去该 skill) |
| POST | /admin/market-skills/{id}/approve |
Admin approve: status=APPROVED, reviewed_at / reviewed_by set |
| POST | /admin/market-skills/{id}/reject |
Admin reject: status=REJECTED; body {"comment": "..."} required (empty/missing → 400). Author re-submit of a REJECTED row archives it to market_skill_archive and creates a NEW PENDING row |
| never existed | /admin/market-skills/pending |
No separate endpoint — use GET /admin/market-skills?status=PENDING |
| PUT | /admin/market-skills/{id}/official | /featured-rank | /category | /announcement | /stats-reset | /tags |
Per-row admin legs (official flag / featured rank / category / announcement / stats reset / tag management); plus GET .../tags, DELETE .../announcement, DELETE .../reviews/{username} |
Request bodies:
MarketCreateRequest (POST — admin create):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Skill name |
description |
string | No | Description |
content |
string | Yes | Prompt template |
category |
string | No | Category |
MarketUpdateRequest (PUT — admin edit; no status field — status changes only via approve/reject):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | Rename; null = no change |
description |
string | No | Description |
content |
string | No | Prompt template |
category |
string | No | Category |
isOfficial |
boolean | No | Official flag |
featuredRank |
int | No | Featured ranking |
MarketSkillUpsertRequest(+ISkillMarketService.adminCreate/adminUpdate/adminDelete) are@Deprecated(ADR-T03 shim, kept 1 minor version) — the v2createApproved/update/deletepaths above are the supported entry points.
6.12 Admin — Authorize a Skill to a Role
GET /spring/ai/loom/admin/roles/{code}/skills
PUT /spring/ai/loom/admin/roles/{code}/skills
GET returns the role's currently authorized market_skill list as RoleSkillItem[] (each item has marketSkillId + defaultLoaded).
PUT replaces the whole list. Request body:
{
"items": [
{"marketSkillId": 1, "defaultLoaded": true},
{"marketSkillId": 5, "defaultLoaded": false}
]
}
defaultLoaded defaults to true if omitted. The role's users will see these skills injected into their user_skill (source=ROLE_GRANTED, locked=true) on their next list/sync.
7. MCP Tools
7.1 List MCP Servers and Tools
GET /spring/ai/chat/loom/mcp
Response: McpRecord[]
| Field | Type | Description |
|---|---|---|
name |
string | MCP server name |
title |
string | MCP server title (display label) |
version |
string | MCP version |
description |
string | MCP description |
defaultSelected |
boolean | Whether selected by default |
tools |
ToolRecord[] | Tool list |
ToolRecord:
| Field | Type | Description |
|---|---|---|
name |
string | Tool name |
description |
string | Tool description |
Example:
[
{
"name": "weather-mcp",
"title": "Weather",
"version": "1.2.0",
"description": "Provides real-time weather query service",
"defaultSelected": true,
"tools": [
{
"name": "getWeather",
"description": "Query current weather for a specified city"
}
]
}
]
8. Skill Tools (LLM-callable)
The @Tool methods registered by ISkillTool for the LLM. These are separate from the admin REST APIs in ## 6. Skill Management (which serve the admin console UI).
The full skill catalog (name + description) is auto-injected into the system prompt under the
技能section. The LLM does not need a list tool to discover skills — it calls@getSkilldirectly to fetch full content. This mirrors howIKnowledgeToolhas nolistKnowledgeBasestool.
8.1 getSkill — fetch one skill's content
@Tool: getSkill
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | The skill's name |
Returns: Text with 技能名, 技能描述, and full 技能内容. Throws if the user has no access to the skill.
8.2 createOrUpdateSkill — create or update a user-owned skill
@Tool: createOrUpdateSkill
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Skill name; trimmed; max length 128 |
description |
string | No | Skill description |
content |
string | Yes | Skill prompt content (non-empty) |
Behavior:
- If the user already owns a skill with this name, content/description are overwritten.
ROLE_GRANTED/MARKET_PULLEDskills are locked and will return 403.- Returns "已创建技能 X" or "已更新技能 X".
9. Knowledge Tools (LLM-callable)
The @Tool methods registered by IKnowledgeTool for tool-based RAG retrieval. The list of enabled knowledge bases is auto-injected into the system prompt under the 【知识库】 section — the LLM does not need a list tool to discover them.
11.1 searchKnowledge — vector search a knowledge base
@Tool: searchKnowledge
| Parameter | Type | Required | Description |
|---|---|---|---|
knowledgeId |
string | Yes | Target knowledge base ID |
query |
string | Yes | Search query (semantic similarity) |
topK |
integer | No | Max chunks to return; defaults to rag.topK (config) |
Returns: Top-k chunks with similarity score and text content. Empty result returns "未检索到相关文档片段". Access check: only own / subscribed / role-granted KBs are searchable; otherwise returns "没有权限访问该知识库".
Similarity threshold: Configured via rag.similarityThreshold (default 0.50). Chunks below the threshold are filtered out before being returned.
10. Terminal Management
10.1 Start Process
@Tool: startProcess
Start a terminal process or REPL session. Supports two modes: Shell mode (one-shot command like ls, cat) and REPL mode (long-running interactive session like python, node). REPL mode uses PTY (pseudo-terminal) when available for full terminal interaction.
| Parameter | Type | Required | Description |
|---|---|---|---|
command |
string | Yes | Command to execute. Shell mode: any shell command. REPL mode: interpreter (e.g. python, node) |
workingDir |
string | No | Working directory (default: ~/.loom/users/{username}/file/) |
repl |
boolean | No | Whether REPL mode. true = long interactive session; false/omitted = one-shot command |
timeout |
long | No | Wait timeout in milliseconds (default 30000ms) |
10.2 Interact with Process
@Tool: interactWithProcess
Send input to a running REPL session and wait for response.
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session ID (returned by startProcess) |
input |
string | Yes | Input to send (newline auto-appended) |
timeout |
long | No | Response wait timeout in ms (default 10000) |
10.3 Read Process Output
@Tool: readProcessOutput
Read output from a running process. Supports three modes: new (unread content since last read, default), tail (last N lines), absolute (from character position N).
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session ID |
mode |
string | No | Read mode: new / tail / absolute |
position |
int | No | Absolute character position (only when mode=absolute) |
lines |
int | No | Number of lines (only when mode=tail, default 50) |
10.4 Force Terminate
@Tool: forceTerminate
Force-terminate a managed terminal session.
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session ID |
10.5 List Sessions
@Tool: listSessions
List all active terminal sessions for the current user.
10.6 Get Process Info
@Tool: getProcessInfo
Get detailed info for a single session, including full output, process state, working directory, PTY mode, etc.
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session ID |
10.7 Send Signal
@Tool: sendSignal
Send a control signal to a terminal session. PTY mode supports: interrupt (Ctrl+C), eof (Ctrl+D), quit (Ctrl+\). Non-PTY mode only supports interrupt via destroy.
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
string | Yes | Session ID |
signal |
string | Yes | Signal type: interrupt / eof / quit |
10.8 List System Processes
@Tool: listProcesses
List all running OS processes (like ps or Task Manager). Supports pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
maxResults |
int | No | Max results per page (default 50, max 200) |
page |
int | No | Page number, 0-based (default 0) |
10.9 Kill Process
@Tool: killProcess
Force-terminate a system process by PID.
| Parameter | Type | Required | Description |
|---|---|---|---|
pid |
long | Yes | Process ID |
force |
boolean | No | Whether to use forceful termination (default true) |
11. Sub-task & Schedule APIs
Sub-tasks are per-conversation (loom-subtask-{user}-{conv}-{taskId}); schedules are per-conversation (loom-sched-{user}-{conv}-{name}) and persist to the loom-agent-owned loom_scheduled_task table (migration). The flex.schedule.limits constraints are 10m min-interval / 72h max-lifetime / strict mode by default in the test app.
11.1 List Sub-task Limits
GET /spring/ai/loom/subtask/limits
Response: { "maxConcurrent": int, "maxHistory": int }
11.2 List Active Sub-tasks
GET /spring/ai/loom/subtask/list/active?conversationId=...
11.3 List Sub-task History
GET /spring/ai/loom/subtask/list/history?conversationId=...&limit=...
11.4 Kill a Sub-task
POST /spring/ai/loom/subtask/kill/{id}
Permission: Owner only. Cross-conversation kill returns 403.
11.5 Delete a Sub-task History Entry
DELETE /spring/ai/loom/subtask/history/{id}
11.6 List Schedule Limits
GET /spring/ai/loom/schedule/limits
Response: { "minInterval": "PT10M", "maxLifetime": "PT72H", "mode": "strict" } (ISO 8601 durations)
11.7 List All Schedules
GET /spring/ai/loom/schedule/list
Response: TaskInfo[] — all schedules visible to the current user.
11.8 Cancel a Schedule
POST /spring/ai/loom/schedule/cancel
Content-Type: application/json
{ "name": "loom-sched-alice-conv-1-防干眼提醒" }
Note: The body field is name, not taskName. The value is the FULL namespaced name (returned by list). Returns 403 for non-owner or non-existent task. Also deletes the corresponding loom_scheduled_task row so the ScheduleRestoreListener doesn't resurrect it on restart.
11.9 Cancel All Schedules for a Conversation
POST /spring/ai/loom/schedule/by-conversation/{conversationId}/cancel-all
11.10 Get Schedule Execution History
GET /spring/ai/loom/schedule/history/{name}
Permission: Owner only (returns 403 for non-owner).
11.11 Get Schedule History for a Conversation
GET /spring/ai/loom/schedule/history/by-conversation/{conversationId}
12. Admin APIs
All admin endpoints require the caller to have user_info.type = 'ADMIN'; non-admin access returns 403 ({"message": "无权限"}). The admin pages under admin/*.html are the canonical UI for these endpoints.
12.1 User Management
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/users |
List all users |
| POST | /admin/users |
Create user (body: CreateUserRequest — username, nickname, password, type). Returns 400 if name taken |
| DELETE | /admin/users/{username} |
Delete user; refuses to delete self |
| GET | /admin/users/{username}/conversations |
List a user's conversations (admin view) |
| GET | /admin/users/{username}/roles |
Get user's roles |
| PUT | /admin/users/{username}/roles |
Replace user's roles (body: SetUserRolesRequest — {"roleCodes": ["dev", "ops"]}). Returns 400 {"error":"用户不存在: xxx"} or {"error":"角色不存在: xxx"} |
| PUT | /admin/users/{username}/password |
Reset a user's password (body: ResetPasswordRequest — {"newPassword": "..."}, min 6 chars). No old password required — an admin cannot know the user's chosen password. Returns 400 if the target is an ADMIN (resetting another admin would allow silent account takeover), if the user does not exist, or if the password is too short/long. The UI disables the button on ADMIN rows. Note: since the admin types the new password, the admin knows it — the modal tells the admin to relay it to the user. |
12.2 Conversation Management (admin)
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/conversations/{conversationId}/turns |
List every turn with token usage |
| GET | /admin/conversations/{conversationId}/messages |
List raw ChatMessages |
| POST | /admin/conversations/clean-batch |
Hard-delete soft-deleted conversations in batch |
12.3 Token Usage Statistics & Logs
| Method | Path | Purpose |
|---|---|---|
| GET | /admin/stats/tokens/monthly?year=2026&month=7 |
Per-user aggregation for the month (defaults to current month if omitted) |
12.4 Role / MCP / Skill / Knowledge Management
All under /admin/... and already documented in:
- § 5 Knowledge Base Management —
admin/knowledge*,/api/knowledge-market*, and the Knowledge Market approval flow (§5.8) - § 6 Skill Management —
admin/market-skills*,/admin/roles/{code}/skills - § 7 MCP Tools —
admin/mcps*,admin/mcp-tools*
12.5 Admin UI Pages (admin/*.html)
| Page | Purpose |
|---|---|
admin/console.html |
User list + role assignment + password reset + batch content cleanup. No longer links to user.html — the user-conversation entry point was consolidated into the Log page's second tab |
admin/roles.html |
RBAC roles + grant MCP / Skill / Knowledge |
admin/market-skills.html |
Approve / reject / directly CRUD Skill |
admin/knowledge-market.html |
Approve / reject / directly CRUD Knowledge |
admin/mcps.html |
Maintain Chinese descriptions for SDK MCP tools |
admin/conversation.html |
Drill into any user's conversation turns (admin only) |
admin/user.html?username=X |
Single-user view: 6-month usage + roles + conversation list; each row drills into conversation.html |
admin/stats.html |
Two tabs: (1) Token usage stats (chart + table, usernames not clickable); (2) Conversation log — user list → user.html → conversation.html |
The Log page's two tabs deliberately separate concerns: the Token tab is a pure statistics view and requires data for the current month, whereas the Conversation-log tab lists all users and works on a fresh deployment with no usage data.
All admin pages share a fixed left sidebar (see README "Admin Console" section).
13. Data Models
ChatRequestRecord
{
"message": "string",
"conversationId": "string",
"mcps": ["string"],
"enabledKnowledgeIds": ["string"],
"fileIds": ["string"],
"selectedSkillName": "string",
"enabledToolGroups": ["string"]
}
ChatResponseRecord
{
"content": "string",
"reasoningContent": "string",
"askUser": null,
"subTaskEvent": null
}
askUser is null for normal frames; when non-null it carries an AskUserEvent question card (see § 3.1/3.2).
subTaskEvent is null for normal frames; when non-null it carries a SubTaskEvent describing a sub-task's lifecycle (started / completed / failed), which the frontend renders as a compact chip in the chat stream. Note the chip is a transient stream event — it is not persisted, so historical replay reconstructs it from messageType=TOOL_CALL entries instead (see § 2.2).
ConversationRecord
{
"conversationId": "string",
"title": "string"
}
UserRequestRecord
{
"username": "string",
"verified": "string"
}
UserResponseRecord
{
"token": "string",
"nickname": "string"
}
Note: The
tokenis also set as an HttpOnlySet-Cookieheader (loom-agent-session). The browser will automatically include this cookie in subsequent requests. Clients do not need to store or send the token manually.
FileRecord
{
"id": "string",
"knowledgeId": "string",
"fileName": "string",
"size": 0,
"uploadTime": "2026-05-10T10:30:00",
"path": "string",
"usage": "conversation",
"mimeType": "application/pdf"
}
KnowledgeRecord
{
"id": "string",
"username": "string",
"name": "string"
}
McpRecord
{
"name": "string",
"title": "string",
"version": "string",
"description": "string",
"defaultSelected": true,
"tools": [
{
"name": "string",
"description": "string"
}
]
}
SkillRecord
{
"name": "string",
"description": "string",
"load": true,
"content": "string",
"source": "USER_CREATED | MARKET_PULLED | ROLE_GRANTED"
}
The response shape is the same as the PUT request body (
name/description/load/content), plus asourcefield set by the server to indicate where the skill was loaded from.sourceis one ofUSER_CREATED/MARKET_PULLED/ROLE_GRANTED.
14. Configuration Properties
All properties are prefixed with spring.ai.loom.agent in application.yml.
14.1 Basic Configuration
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.defaultSystem |
string | Skill discovery prompt | Default system prompt |
spring.ai.loom.agent.init |
boolean | true |
Whether to initialize the ChatClient |
14.2 RAG Configuration
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.rag.similarityThreshold |
double | 0.0 |
Vector retrieval similarity threshold |
spring.ai.loom.agent.rag.topK |
int | 4 |
Number of top results to retrieve |
14.3 MCP Configuration
spring.ai.loom.agent.mcps is an array. Each entry contains:
| Property | Type | Description |
|---|---|---|
name |
string | MCP server name |
title |
string | Display label |
description |
string | Description info |
defaultSelected |
boolean | Whether selected by default |
tools[].name |
string | Tool name |
tools[].description |
string | Tool description |
14.4 Skill Configuration (no longer read from yml)
The spring.ai.loom.agent.skills[] yml block is no longer read. See §6 Skill Management for the database-driven flow. 6 system skills are seeded on first launch.
14.5 Vector Store Configuration (H2-backed JVector)
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.jvector.m |
int | 16 |
HNSW graph parameter M |
spring.ai.loom.agent.jvector.efConstruction |
int | 100 |
ef parameter at build time |
spring.ai.loom.agent.jvector.efSearch |
int | 10 |
ef parameter at search time |
Persistence: vectors live in the H2 table
loom_vector_store(embedding BLOB, little-endian float32) and are hydrated into the in-memory JVector HNSW graph onApplicationReadyEvent— no boot-time re-embedding. The legacy~/.loom/jvector-index/json files are retired. Changing the embedding model invalidates stored vectors (dim-guarded rows are skipped with a WARN); wipe the table and re-upload knowledge documents.
14.6 Authentication Configuration
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.auth.enabled |
boolean | true |
Authentication master switch |
spring.ai.loom.agent.auth.pathPatterns |
string[] | ["/spring/ai/loom/**"] |
Paths requiring authentication (Ant-style patterns) |
spring.ai.loom.agent.auth.excludePathPatterns |
string[] | (see below) | Paths excluded from authentication |
spring.ai.loom.agent.auth.cookie.name |
string | loom-agent-session |
Session cookie name |
spring.ai.loom.agent.auth.cookie.maxAge |
int | 86400 |
Cookie max age in seconds (24 hours) |
14.7 File Storage Configuration
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.usersBasePath |
string | ~/.loom/users |
User-tree root; per-user sandbox = {usersBasePath}/{username}/file/ |
Files uploaded to the same directory with duplicate names are automatically renamed with a suffix:
file.txt→file(1).txt→file(2).txt.
14.7.1 File Tool Configuration (IFileTool)
The file tool (IFileTool) is configured via spring.ai.loom.agent.file.* with a set of default safety/resource limits tailored for LLM tool-call scenarios.
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.file.enabled |
boolean | true |
Deprecated since M3 — no effect; IFileTool is a universal tool (always visible) |
spring.ai.loom.agent.file.maxFileSize |
long | 5242880 (5 MB) |
Per-call upper bound on file read/write size (bytes). Exceeding this is rejected outright, to avoid OOM and LLM context overflow. |
spring.ai.loom.agent.file.maxMediaSize |
long | 1048576 (1 MB) |
Upper bound on media files (images / audio). Base64-encoded size ≈ 4/3 of the original, so the limit is stricter than for text. |
spring.ai.loom.agent.file.maxWalkDepth |
int | 5 |
Upper bound on depth for directoryTree / recursive listing / search. |
spring.ai.loom.agent.file.maxWalkEntries |
int | 1000 |
Upper bound on entries returned per listDirectory / directoryTree call. |
spring.ai.loom.agent.file.maxSearchResults |
int | 500 |
Upper bound on searchFiles hit count. |
spring.ai.loom.agent.file.deleteConfirmToken |
string | I_CONFIRM_DELETE |
The deleteFileOrDirectory tool requires this exact string as an explicit argument before it executes (guards against accidental LLM deletions). Can be replaced with a shorter token (e.g. YES) to save tokens. |
spring.ai.loom.agent.file.excludedDirs |
string[] | [".git", "node_modules", "target", "build", "dist", ".idea", ".vscode", ".gradle", "out", "bin"] |
Directory names skipped during traversal (exact match, not glob). Keeps directoryTree / searchFiles from dumping tens of thousands of target/classes/*.class entries on a Spring Boot project. |
Safety mechanisms:
- All path resolution is delegated to
PathSecurityUtils.assertInsideUserDir(resolved, userDir, mustExist), which uniformly handles: ..traversal (a singlePath.normalizedefeats it)- Symlink escape (
Path.toRealPathfollows the chain) — even if the user drops a symlink insideuserDirpointing atC:\Windows, the tool will not read through it - Size-bypass on case-insensitive filesystems (Windows / macOS)
- Atomic writes:
writeFile/editFilewrite to a.tmpfile and thenFiles.move(ATOMIC_MOVE)it into place, so a mid-write power loss won't corrupt the target. Falls back to non-atomic replacement across volumes. editFileuniqueness check: ifoldTextmatches more than once in the file the call is rejected, forcing the LLM to provide more precise surrounding context.
14.8 End-to-End Deployment Configuration (ICompileAndDeployTool)
ICompileAndDeployTool performs the full deployment pipeline in a single LLM tool call: git clone → buildTool build (maven / npm / pip) → docker build → docker run → health check. Supports Maven, Node.js (backend and static-frontend → nginx), and Python projects. All settings live under spring.ai.loom.agent.compile.*.
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.compile.enabled |
boolean | true |
Deprecated since M3 — no effect; bean always created, visibility RBAC-gated via role_tool.tool_compile |
spring.ai.loom.agent.compile.mavenHome |
string | auto-discover | Optional Maven install dir; falls back to maven.mavenHome and PATH |
spring.ai.loom.agent.compile.dockerCmd |
string | docker |
Optional override for the docker CLI binary |
spring.ai.loom.agent.compile.imageTemplates |
map | (6 pre-set templates) | Pre-set base-image templates keyed by alias; see below |
spring.ai.loom.agent.compile.extraRunArgs |
string[] | [] |
Extra docker run args injected between --name and the image name |
Base-image templates (built-in):
| Alias | Image | Default ENTRYPOINT |
|---|---|---|
java17 |
eclipse-temurin:17-jre-alpine |
["java","-jar","app.jar"] |
java21 |
eclipse-temurin:21-jre-alpine |
["java","-jar","app.jar"] |
nginx |
nginx:1.27-alpine |
["nginx","-g","daemon off;"] |
python3 |
python:3.12-slim |
["python","app.py"] |
node20 |
node:20-alpine |
["node","dist/index.js"] |
node20-serve |
nginx:1.27-alpine |
["nginx","-g","daemon off;"] |
Tool-call parameters (Map, case-insensitive, all optional except gitUrl, port, and containerPort):
gitUrl(required) — git repository URLgitUsername/gitPassword— credentials for private reposbranch— branch to clone (defaults to remote HEAD)port— host port the container will publish (the port the caller accesses viahttp://localhost:{port}/{healthPath})containerPort— Container port the application listens on inside the container (required, no yml fallback; referenceserver.portin application.yml)subDir— Subdirectory of a multi-module repo to deploy (required when root has nopom.xml— tool returnsfailotherwise)buildTool— Build tool / project type:maven/npm/npm-frontend/pip(optional; auto-detected frompom.xml/package.json/requirements.txt/pyproject.toml)imageName/containerName— Docker image and container names (defaults derived from timestamp)healthPath— both the health-check path and the access URL path (e.g.healthPath=sql-forge-demo→http://localhost:8080/sql-forge-demo)baseImage— template alias (java17/java21/nginx/python3/node20/node20-serve) or full image name (e.g.openjdk:17-slim)runCommand— string array overriding the template's default ENTRYPOINT (rare)
Example yml overriding the default templates:
spring:
ai:
loom:
agent:
compile:
image-templates:
java17:
image: eclipse-temurin:17-jre-alpine
command: [java, -jar, app.jar]
nginx:
image: nginx:1.27-alpine
command: [nginx, -g, "daemon off;"]
Example tool invocation:
{
"gitUrl": "https://gitee.com/wb04307201/sql-forge-demo.git",
"port": 8081,
"containerPort": 8080,
"subDir": "sql-forge-web",
"baseImage": "java17",
"healthPath": "sql-forge-demo"
}
14.9 Git Configuration (IGitTool)
IGitTool provides Git operations (init, clone, status, commit, branch, etc.) via Eclipse JGit. The bean is always created; visibility is RBAC-gated — an admin must grant tool_git to a role (/admin/roles/{code}/tools, persisted in role_tool). The git.enabled yml flag is deprecated (no effect).
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.git.enabled |
boolean | false |
Deprecated since M3 — no functional effect; visibility is RBAC-gated via role_tool.tool_git |
spring.ai.loom.agent.git.username |
string | — | Git username for remote authentication |
spring.ai.loom.agent.git.token |
string | — | Git token / password for remote authentication |
Example:
spring:
ai:
loom:
agent:
git:
username: your-username
token: your-token
# Note: no 'enabled' flag — grant tool_git to a role in the admin console instead
14.10 Maven Build Configuration
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.maven.enabled |
boolean | false |
Deprecated since M3 — no functional effect; the bean is created when maven-invoker is on the classpath (a default lib dependency) and visibility is RBAC-gated via role_tool.tool_maven. Compile/package for deployment scenarios is handled by ICompileAndDeployTool |
spring.ai.loom.agent.maven.mavenHome |
string | — | Maven install directory. When empty, the tool auto-discovers: it tries the MAVEN_HOME / M2_HOME environment variables first, then scans common Windows paths (e.g. C:\developer\apache-maven-*, C:\Program Files\Apache Maven). Auto-discovery does not rely on the system PATH, so a broken or shadowing mvn wrapper (e.g. a global npm mvn) won't cause maven-invoker to throw Error configuring command line. |
spring.ai.loom.agent.maven.localRepository |
string | — | Local repository path (uses the default path when empty) |
spring.ai.loom.agent.maven.maxOutputLines |
int | 200 |
Maximum output lines (truncated when exceeded) |
spring.ai.loom.agent.maven.defaultTimeoutMs |
long | 300000 |
Default execution timeout in milliseconds (5 minutes) |
All Maven tool operations are scoped to
{usersBasePath}/{username}/file/; paths outside that range are rejected.Troubleshooting tip —
MavenInvocationException: Error configuring command line: this meansmaven-invokercould not find a usablemvn/mvn.cmd. The tool startup log prints the resolvedmavenHometogether with a diagnostic hint listing every path it searched, the environment variables it looked at, and how to fix it. The most common cause is a broken or shadowingmvnonPATH(e.g. a global npmmvnwrapper); in that case, explicitly setspring.ai.loom.agent.maven.mavenHomeinapplication.ymlto point at the real Maven install directory to bypass it.Troubleshooting tip — "file is locked" errors when deleting the project directory on Windows: in older versions this was caused by
maven-invoker 3.3.0/plexus-utils 3.3.0because (a) the JVM shutdown hook they register on the exception/cancel path never releases the heldProcessreference, and (b)Invoker.executedoes not expose the child-process handle, so it cannot propagate cancel/timeout down to the mvn child process. The result: a cancelled or timed-out Maven call would leave the mvn child running and continuing to hold mmap handles ontarget/classesand~/.m2/repository/*.jar, locking those files on Windows. The new version no longer usesInvoker.executeto run the process — it forks mvn directly withProcessBuilder, does a clean timeout viaProcess.waitFor(timeout, unit), then callsProcess.destroyForciblyand explicitly closes the streams on timeout. No JVM shutdown hook is registered any more, so the mvn child is always killed on timeout/cancel. If you still see locks after upgrading, it is most likely an orphan mvn process left behind by a previous JVM — find it withtasklist /FI "IMAGENAME eq cmd.exe"andtaskkill /F /PID <pid>it.
14.11 Tool Group Switches (deprecated)
Since M3, the
*.enabledswitches below no longer gate tool beans. All always-onI*Toolbeans are created unconditionally (plusIHtmlRenderTool, gated only byplaywrighton the classpath). Visibility is governed by universal tools (@ToolGroup(defaultGranted=true)— visible to every logged-in user) vs RBAC tools (tool_git/tool_maven/tool_compile/tool_render, granted per role via/admin/roles/{code}/tools, persisted inrole_tool). See TOOLS.md §1 for the full model. The properties remain for backward compatibility only.
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.loom.agent.time.enabled |
boolean | true |
Deprecated — no effect (ITimeTool is universal) |
spring.ai.loom.agent.file.enabled |
boolean | true |
Deprecated — no effect (IFileTool is universal) |
spring.ai.loom.agent.skill.enabled |
boolean | true |
Deprecated — no effect (ISkillTool is universal) |
spring.ai.loom.agent.git.enabled |
boolean | false |
Deprecated — no effect; IGitTool bean always created, visibility RBAC-gated via role_tool.tool_git |
spring.ai.loom.agent.maven.enabled |
boolean | false |
Deprecated — no effect; IMavenTool bean created when maven-invoker on classpath, visibility RBAC-gated via role_tool.tool_maven |
spring.ai.loom.agent.git.username |
string | — | HTTP(S) Git auth username (clone/pull/push) |
spring.ai.loom.agent.git.token |
string | — | HTTP(S) Git auth token / password |
spring.ai.loom.agent.gitUsername |
string | — | Legacy top-level alias, equivalent to git.username |
spring.ai.loom.agent.gitToken |
string | — | Legacy top-level alias, equivalent to git.token |
Example — expose the Git tool to a role: no yml change needed; in the admin console (角色管理 → base → 编辑/授权 → 授权本地工具组) grant tool_git to the role.
To replace a tool implementation, provide your own
@Bean IGitTool/@Bean IMavenTool—@ConditionalOnMissingBeanalways gives user-provided beans precedence.
Appendix: Endpoint Summary
| # | Method | Path | Description |
|---|---|---|---|
| 1 | POST |
/spring/ai/loom/user/isAutoLogin |
Check auto-login status |
| 2 | POST |
/spring/ai/loom/user/login |
User login (sets session cookie) |
| 2a | POST |
/spring/ai/loom/user/logout |
User logout (invalidates session) |
| 3 | GET |
/spring/ai/loom/conversation |
List conversations |
| 4 | GET |
/spring/ai/loom/conversation/{id} |
Get conversation history |
| 5 | DELETE |
/spring/ai/loom/conversation/{id} |
Delete conversation |
| 6 | POST |
/spring/ai/loom/stream |
SSE streaming chat |
| 7 | POST |
/spring/ai/loom/file/upload |
Upload file |
| 8 | GET |
/spring/ai/loom/file or /spring/ai/loom/file/tree |
List file tree (directory tree JSON) |
| 8a | GET |
/spring/ai/loom/file/by-path/view |
View file by path (redirects) |
| 8b | GET |
/spring/ai/loom/file/by-path/download |
Download file by path (redirects) |
| 8c | GET |
/spring/ai/loom/file/{id}/download |
Download file by ID (binary stream) |
| 9 | GET |
/spring/ai/loom/knowledge/checkKnowledgeUpload |
Check knowledge base status |
| 10 | GET |
/spring/ai/loom/knowledge |
List knowledge bases |
| 11 | PUT |
/spring/ai/loom/knowledge |
Create knowledge base |
| 12 | DELETE |
/spring/ai/loom/knowledge/{id} |
Delete knowledge base (cascade) |
| 13 | POST |
/spring/ai/loom/knowledge/{id}/upload |
Upload file to knowledge base |
| 14 | GET |
/spring/ai/loom/knowledge/{id}/file |
List files in knowledge base |
| 15 | DELETE |
/spring/ai/loom/knowledge/{id}/file/{fileId} |
Delete file from knowledge base |
| 15a | GET |
/spring/ai/loom/api/knowledge-market |
Browse approved market knowledge (kept v1 leg; v2 GET /market-knowledge is the primary list) |
| 15b | POST |
/spring/ai/loom/api/knowledge-market/{marketId}/pull |
Subscribe to market knowledge (APPROVED-only, else 403) |
| 15c | POST |
/spring/ai/loom/api/knowledge/{knowledgeId}/submit |
Submit knowledge to market (→ PENDING) |
| 15d | DELETE |
/spring/ai/loom/api/knowledge-market/{marketId} |
Withdraw market submission |
| 15e | POST |
/spring/ai/loom/admin/market-knowledge/{id}/approve |
Admin approve market submission (v2 admin router) |
| 15f | POST |
/spring/ai/loom/admin/market-knowledge/{id}/reject |
Admin reject market submission (comment required) |
| 15g | GET |
/spring/ai/loom/api/knowledge-market/my-pulled |
List my subscribed market knowledge |
| 15h | GET |
/spring/ai/loom/api/knowledge-market/my-submitted |
List my market submissions |
| 16 | GET |
/spring/ai/chat/loom/mcp |
Get MCP servers and tools |
| 17 | GET |
/spring/ai/loom/skill |
List all skills |
| 18 | PUT |
/spring/ai/loom/skill |
Create or update a skill |
| 19 | GET |
/spring/ai/loom/skill/{name} |
Get skill details |
| 20 | DELETE |
/spring/ai/loom/skill/{name} |
Delete a skill |