来源:2026-10-27 从
agent4j/loopra/src/main/java/site/sorghum/loopra/web/全部 22 个 controller 提取实证。 配套文档:SSE 事件与持久化格式契约。未指定端口时由系统分配空闲端口。 类型记法:s=string,b=boolean,i=int32,l=int64(epoch 毫秒),d=double;*=必填。除特别注明外均为 JSON body(POST/PUT)或 query 参数(GET/DELETE)。
全部控制器返回值(SSE 与二进制端点除外):
| 字段 | 类型 | 说明 |
|---|---|---|
success |
b | 必有 |
error |
s? | 失败消息(中文) |
reason |
s? | 机器可读原因码(仅部分场景) |
data |
T? | 泛型业务数据 |
序列化(snack4 默认,null 字段整体省略):
- 成功:
{"success":true,"data":...}(error/reason不出现;ok(null)→{"success":true}) - 失败:
{"success":false,"error":"..."}(reason/data仅非 null 时出现) - 无统一日期格式:DTO
date为"yyyy-MM-dd HH:mm:ss",createdAt等为 epoch 毫秒。
- 安装顺序:
CrossFilter(-100)→WebAccessFilter(-90);GlobalExceptionFilter兜底。 - Key 生成:优先 env
LOOPRA_ACCESS_KEY(匹配[A-Za-z0-9_-]{43,128}),否则 SecureRandom 32 字节 → Base64url 无 padding(43 字符)。启动打印[Web Interface]: http://127.0.0.1:{port}/index.html?rk={key}。 - 免鉴权路径(仅 GET/HEAD):
/、/config.json、classpath/static{path}存在的资源;前提:非/api、/api/,且 path 不含..与\。 - 校验:header
X-Loopra-Key与 key 常量时间比较。 - rk 参数:仅 header 缺失、GET、且 path 匹配
/api/pets/[^/]+/spritesheet时取 queryrk。 - 每次响应:
Referrer-Policy: no-referrer;非 bundled 路径加Cache-Control: no-store。 - 401 响应体:
{"success":false,"reason":"access_key_required","error":"访问密钥缺失或错误,请输入启动日志中的 rk 密钥"}。 - CORS:allowedOrigins
*、allowCredentials false、allowedHeadersContent-Type,X-Loopra-Key,X-Request-ID,X-Timestamp,Authorization,Mcp-Session-Id,Mcp-Protocol-Version,Last-Event-ID、exposedHeadersMcp-Session-Id;GlobalExceptionFilter 对带 Origin 请求补回显头 +Max-Age:3600;OPTIONS 预检空串 200。 - 异常:ServiceException → 对应 HTTP 状态(默认 400)+
ApiResponse.fail(message, reason);其他异常 → 500 +"服务暂时无法完成请求,请稍后重试"。
以下端点沿用普通 API 包装与公共鉴权;查询必须指定 scope=global|project。project 还要求已注册项目的 workspaceHash,不接受纯聊天或路径。全局请求不依赖当前项目。
项目记忆实际存放于注册项目根目录的 .loopra/memory/memories.json;API 和工具仍使用项目 hash,隔离工作树共享主项目记忆。旧版数据目录中的项目记忆仅在新位置文件缺失时校验并迁移,旧文件保留作备份。
| 方法与路径 | 请求 | data |
|---|---|---|
GET / HEAD /api/memories |
scope、workspaceHash?、query?(标题与内容的空格分词匹配) |
记忆数组,按更新时间倒序;HEAD 无响应体 |
POST /api/memories |
JSON {title,content,kind} |
新记忆,revision:1 |
PUT /api/memories/{id} |
JSON {title,content,kind,revision} |
更新后的记忆,revision 加一 |
DELETE /api/memories/{id} |
查询 revision |
记忆已删除 |
条目为 {id,title,content,kind,source,revision,createdAt,updatedAt},手工 API 写入的 source 固定为 user。参数无效返回 400 / invalid_memory,条目缺失返回 404 / memory_not_found,版本或同名标题冲突返回 409 / memory_conflict,存储失败返回 500 / memory_storage_failed。冲突后先重新读取,不得盲目覆盖。
只读 Agent 工具 memory_search({action?,id?,query?,scope?,offset?,limit?}) 保留同一个入口,支持:
| action | 参数与行为 | 返回 |
|---|---|---|
list(默认) |
列全局及当前项目目录,可指定 scope;不接受 id 或非空 query | {global?:{entries:[{id,title}],total,offset},project?:{entries:[{id,title}],total,offset}},不包含正文 |
read |
必须明确提供 scope 与 id;仅读取该作用域,不接受 query 或分页参数 | {scope,entry},entry 为完整记忆,包含最新 revision |
search |
必须提供非空 query;按标题与正文匹配,可指定 scope | 分页结构同 list,但 entries 为匹配的完整记忆 |
list/search 默认每作用域 10 条、最多 20 条,offset 从零开始;省略 action 且传非空 query 时兼容旧的 search 调用。目录由现有记录生成,更新和删除后不维护另一份索引。HTTP GET /api/memories 仍返回完整条目供管理页使用。
memory({action:"save"|"delete",scope,id?,revision?,title?,content?,kind?}) 写入时 source 固定为 agent;更新、删除必须提供 ID 与最新 revision。纯聊天不可使用项目作用域。以上三种只读操作及写入操作沿用禁用名单与现有审批策略。系统提示词引导新会话先查看目录,再由 Agent 根据对话按需读取正文;不自动加载目录或正文。环境信息使用前核实。
→ 后为 data 字段结构。多数端点先检查 agentService.isReady(),未就绪 400 "Agent 未初始化"。
| 方法+路径 | 参数 | data |
|---|---|---|
POST /stream |
body ChatRequest | SSE 流(见 SSE 契约文档;唯一聊天入口,无同步 POST、无 GET SSE) |
POST /abort |
body ChatRequest(只用 requestId/workspaceHash/sessionName;requestId/sessionName 至少一项) | {status:"stopping"/"pending"/"not_running",matched:n};旧 requestId 对应会话已开始新一轮时返回 409、reason=chat_run_changed,不取消新一轮 |
GET /permissions |
q: workspaceHash*, sessionName* | [{...pending 审批}] |
POST /permission |
body {workspaceHash*, sessionName*, permissionRequestId*, optionId*} |
s "审批已提交";409 "审批已结束或不属于当前会话" |
GET /elicitations |
q: workspaceHash*, sessionName* | [] |
POST /elicitation |
body {workspaceHash*, sessionName*, elicitationRequestId*, action, content:{}} |
s "答案已提交" |
POST /sub-hitl/{subId:i} |
body {action:"approve"|"deny"*} |
{subId:i, action:s} |
| 方法+路径 | 参数 | data |
|---|---|---|
GET /status |
— | AgentStatusDTO{ready:b, model:s, workspace:s, cacheSize:i, historySize:i, hitlMode:s, sessionName:s, promptTokens:l, completionTokens:l} |
GET /session-status |
q: workspaceHash*, sessionName* | SessionStatusDTO{running:b, requestId:s?, title:s?} |
GET /bash-sessions |
q: workspaceHash?, sessionName? | [BashSessionDTO{sessionId:s, owner:s, workspaceHash:s, workspace:s, command:s, workdir:s, startedAt:l, status:s}](按项目与会话名共同隔离;省略 workspaceHash 时使用默认项目,省略 sessionName 时返回全局管理视图) |
POST /bash-sessions/terminate |
body {workspaceHash?, sessionId?, sessionName?} |
s(跨项目或跨会话终止无效;省略 sessionId 时必须有 sessionName) |
GET /bash-sessions/log |
q: workspaceHash?, sessionId*, sessionName? | BashSessionLogDTO{...,output:s} |
GET /history |
q: workspaceHash*, sessionName* | [](编译后历史视图) |
GET /history/events |
q: workspaceHash*, sessionName* | [](ChatMessage 行,见 SSE 契约 §3) |
GET /commands |
— | [{cmd:s, desc:s, args:s}] |
GET /acp-state |
q: workspaceHash*, sessionName* | {plan:[{content,status,priority}], commands:[{name,description,inputHint}]} |
GET /prompt |
q: workspaceHash?, sessionName? | {content:s, length:i} |
GET /project-capabilities |
q: workspaceHash* | {workspacePath, mcpConfigPath, mcpConfigExists:b, projectSkillsPath, projectSkillsDirectoryExists:b, skills:[{name,description,scope,mountAlias,path}], mcpServers:[{name,type,enabled:b,loaded:b,toolCount:i,toolNames:[],error}]} |
POST /project-capabilities/refresh |
q: workspaceHash* | 同上 |
GET /skills |
— | [{name, description, scope:s(""), runAs:s(id), directoryName:s}]。已启用插件的技能 scope 为 @plugin-skills/<插件 id> |
| 方法+路径 | 参数 | data |
|---|---|---|
| GET `` | — | [Engine{id:s, name:s, available:b, detail:s}](首项 native) |
GET /mcp |
q: agentId*, workspaceHash* | [{name, type, source:s("project"/"global"), configuredEnabled:b, enabled:b}] |
PUT /mcp |
body {agentId, workspaceHash, name*, enabled:b*} |
同 GET /mcp(404→400 "MCP 配置不存在,请刷新后重试") |
GET /preferences |
— | {selectedAgent:s?, agents:{}: {agentId: {configId: s|b}}} |
PUT /preferences |
body {selectedAgent*} |
同上 |
GET /session |
q: workspaceHash*, sessionName* | {agentId:s, hasMessages:b} |
GET /config |
q: workspaceHash*, sessionName* | {configOptions:[...]} |
POST /prepare |
body {workspaceHash*, sessionName?, agentId?} |
{sessionName:s, agentId:s, configOptions:[...]}(空 sessionName 新建 loopra-<uuid>) |
PUT /config |
body {workspaceHash, sessionName*, configId*, value*} |
{configOptions:[...]} |
| 方法+路径 | 参数 | data |
|---|---|---|
GET /config |
— | ConfigDTO(apiKey 脱敏 前4+****+后4,短 key→"****";字段全集见契约代理原始输出) |
GET /ui-preferences |
— | {theme:light|dark|system, language:zh|en, fontScale:80|90|100|110|120, notifications:b, sound:b, openWith:s, autoContext:b, animations:b, petEnabled:b?, lastWorkspace:s?, profile:{nickname,avatar,color}?, persisted:b}。文件不存在时 persisted:false 且不创建文件。个人资料、宠物开关、lastWorkspace 没保存过时省略。旧文件里的 hiddenSessions 在下次保存时丢弃 |
PUT /ui-preferences |
body 同上,缺省字段保留已保存值;theme 也接受 浅色/深色/跟随系统,language 也接受 简体中文/English。lastWorkspace 只接受项目 hash(12 位十六进制或 loopra_chat),显式传空表示忘掉记录 |
同 GET,persisted:true。非法外观、语言、缩放、打开应用、个人资料或项目记录 → 400 |
PUT /config |
body {} 任意键值合并 |
s:渠道类→"模型渠道已更新,Agent 已重新初始化",否则 "配置已更新";广播 config.changed 键:model/reasoningEffort/hitl/fastMode/disabledTools |
GET /models |
— | {current, currentChannelId, models:[{name,active,channelId,channelName,displayName}]} |
GET /remote-models |
q: channelId? | [s](GET {baseUrl}/models 取 $.data[].id) |
POST /remote-models |
body {channelId?, baseUrl?, apiKey?, apiProtocol?, specialCompatibility?} |
[s] |
GET /usage |
q: workspaceHash*, sessionName? | UsageDTO{promptTokens,completionTokens,cacheHit,cacheMiss,lastPromptTokens,maxContextTokens,totalTokens,model,hasPrice,inputCost,cacheCost,outputCost,totalCost,currency,contextEstimate:{...},contextSource,contextEstimateScope} |
GET /workspace |
— | s |
POST /workspace |
body {path*} |
{message, workspace, currentSession:null}(400 "无效的工作目录路径: ...") |
GET /workspaces |
— | [{hash,name,path,createdAt,lastAccessedAt,sessionCount,displayName?,linkedProjectHashes?,linkedProjectPaths?}] |
PUT /workspaces/{hash}/name |
body {displayName} |
ProjectInfoDTO |
PUT /workspaces/{hash}/links |
body {linkedProjectHashes?:[hash...],linkedProjectPaths?:[绝对路径...]},至少提供一个列表;省略的列表保留,空数组清空对应列表 |
更新后的 ProjectInfoDTO;已有项目与独立目录按实际目录去重、合计最多 8 个;独立目录无需登记项目,也不出现在项目列表;无效项目、不可用目录、自身目录或纯聊天返回 400 |
DELETE /workspaces/{hash} |
— | s "项目已删除";删除前先把其他项目对该标识的长期关联改写为目录关联,改写失败则不删除 |
PUT /workspaces/order |
body [hash...] |
s "排序已保存" |
GET /workspaces/order |
— | [s] |
项目长期关联保存在主项目元数据中,旧记录缺省为空。linkedProjectPaths 是关联的规范形式,保存独立目录的绝对路径(解析符号链接和 Windows 目录联接),不创建项目记录,也不切换当前工作区;工作台里「选择已有项目」「选择文件夹」和输入路径都只写入该列表。linkedProjectHashes 保留为旧客户端与既有数据的兼容入口,解析时仍按目录生效;删除某个项目时,服务端先把其它项目对它的标识关联改写为目录关联(按目录去重并保留未知字段),因此项目列表删除不会使关联失效。省略列表时保留其原值,兼容只提交项目标识的旧客户端。原生 Agent 的系统提示词列出主项目、副项目及绝对路径;不拼接到用户消息。每轮执行合并两类长期关联与 ChatRequest.linkedProjectHashes 临时关联,按目录去重后最多 8 个,且不递归继承副项目的关联。不可用的长期关联不授予权限(目录关联的元数据保留供用户解除);显式临时关联无效时返回错误。修改从下一轮生效,已开始的轮次沿用其目录快照。外部 ACP 引擎(包括 Codex)通过 additionalDirectories 接收相同的目录列表;会话预热、读取或修改配置与发送均解析默认关联项目和隔离分支目录,避免首次发送无故重建连接。临时关联仍在发送时合并,列表变化后重新建立或恢复会话。
开启 toolsRestrictWorkspace 时,write / edit 接受主项目内相对路径,以及主项目或有效副项目内的绝对路径,仍检查符号链接越界。审批策略将这些目录视为工作区,blockedPaths 在各工作区继续生效。只读工具和 shell cwd 保持原有范围;关联不额外授予副项目的记忆、技能或插件配置。
| GET /loopra-md | — | s |
| PUT /loopra-md | body 裸字符串 | s "loopra.md 已保存,Agent 已重新初始化,新会话将使用更新后的提示词" |
| GET /usage/dashboard | q: days?(7) | {totalPromptTokens,...,dailyStats:[...],modelStats:[...],modelPrices:{}} |
| 方法+路径 | 参数 | data |
|---|---|---|
| GET `` | — | [{name, description, readOnly, readOnlyOverride?, stormExempt, enabled, autoApproved, chat, async, params}];params 来自工具 schema,异步工具包含通用 yield_after_ms |
POST /{name}/toggle |
— | s "已禁用/启用工具: x"(400 "工具不存在: x") |
POST /{name}/auto-toggle |
— | s "已添加/移除自动放行: x" |
POST /{name}/read-only |
body {readOnly:b?}(null=默认) |
s "已将工具分类设置为只读/写入/默认: x" |
GET /{name} |
— | ToolInfoDTO |
McpServerDTO={name(s,匹配^[a-zA-Z0-9_-]+$), type("stdio"/"sse"/"streamable"), enabled(b,默认true), command?, args?:[s], env?:{}, url?, headers?:{}, timeout?:s("30s"/"500ms"/"2m")};stdio 需 command,sse/streamable 需 http(s) url。
| 方法+路径 | 参数 | data |
|---|---|---|
GET /servers |
— | [McpServerDTO] |
POST /servers/add | /update({originalName?, server*})| /remove({name*})| /toggle({name*, enabled}) |
body | DTO / s "服务器已删除" |
POST /servers/check |
body McpServerDTO | s "连接成功" / fail "连接失败,请检查服务器配置"(15s 超时) |
GET /servers/tools |
q: name* | {connected:b, tools:[{name,description}], disallowedTools:[s]} |
POST /servers/tools/save |
body {serverName*, disallowedTools:[s]} |
s "工具权限已保存" |
GET /export |
— | {enabled, name, version, channel, endpoint, allowedTools?, tools:[{name,description,readOnly,enabled,exposed}], endpointUrl} |
PUT /export |
body McpExportConfig(默认 enabled=false,name=loopra,version=1.0.0,channel=streamable,endpoint=/mcp,allowedTools=null=全量) | 同上 |
POST /export/refresh |
— | 同上 |
GET /tunnel | PUT /tunnel/config | POST /tunnel/start | POST /tunnel/stop |
—/{executablePath}/{executablePath?}/— |
{state,running,installed,quickTunnel,mcpReady,platform,configuredExecutablePath,executablePath,localUrl,publicUrl,mcpEndpoint,error,pid:l} |
| 方法+路径 | 参数 | data |
|---|---|---|
| GET `` | q: workspaceHash? | [{name,title,messageCount:l,current:b,mtime:l,worktreeMode:b,agentId:s(默认"native")}](仅含写入过消息的会话:.jsonl 或审计 .events 有内容) |
PUT /order |
q: workspaceHash*; body [sessionName...] |
s "排序已保存" |
GET /current |
q: workspaceHash? | {workspaceHash, sessionName} |
POST /new |
q: workspaceHash*, sessionName?, worktreeMode?:b | {message:"已创建新会话", workspaceHash, sessionName}(惰性:不创建会话日志,首条消息写入时才落盘) |
GET/PUT /{name}/settings |
q: workspaceHash*; body {model?, modelChannelId?, reasoningEffort?} |
同 body |
GET/PUT /{name}/worktree |
body {worktreeMode?:b, mergeMode?:s("manual"/"ai-auto"/"ai-auto-approve")} |
{workspaceHash, sessionName, worktreeMode, mergeMode} |
PUT /{name}/title |
body {title*} |
s |
POST /{name} |
q: workspaceHash* | {workspaceHash, sessionName, switched:b}(惰性切换,允许尚未落盘的会话名) |
DELETE `` | /cleanup(before:l*) | `/{name}` |
— | {message, workspaceHash, sessionName} |
POST /evict-all |
— | s "已清除所有 Agent 缓存" |
POST /{name}/branch |
q: messageCount:i*(>0,按 /api/agent/history 返回的可见行计数,即前端 historyCount;一个 ADK 事件可能占多行,切点取整事件) |
{message:"已分支到新会话",...} |
POST /{name}/fork |
— | {message:"已创建会话分叉",...} |
GET /stats |
— | {cacheSize:i, maxCacheSize:i=50} |
GET /{name}/checklist | /{name}/goal |
q: workspaceHash* | null 或清单/目标结构(见契约代理原始输出) |
POST /file-changes/revert |
body {workspaceHash*, changes:[{path,additions,deletions,created,diff}]} |
{message:"已撤销本次 AI 的文件修改", revertedFiles:i} |
GET `` 与 GET /external → `[{id,displayName,description,remark,version,source,sourceUrl,enabled,state,error,tools:[s],hooks:[s],entry,settings?:b,builtin?:b,requests?:b,icon?:{name?:s,key?:s,dataUrl?:s,hidden?:b},sidebar?:{title,icon?},panel?:{title,icon?},menu?:{title,category?:s,icon?,page?:b,children?:[{id,title,icon?}]}}]`。`hooks` 是已加载的钩子名:`turnBefore`、`systemPrompt`、`llmRequest`、`llmRequestSent`、`llmResponse`、`toolBefore`、`toolAfter`、`compact`、`turnAfter`。`settings` 为真表示声明了设置页,`requests` 为真表示已加载页面后端 `onRequest`。`builtin` 为真表示 `plugins/` 目录里的内置插件,可以停用,不能卸载或单独更新。`sidebar` 出现时表示侧栏有入口,`panel` 表示右侧面板有页签。`menu` 出现时表示设置导航有插件菜单。`icon.hidden:true` 表示插件显式隐藏图标;`icon.name` 是 Lucide 图标名,`icon.key` 要用访问密钥取 `GET /{id}/icons/{key}`。
POST `/install` 与 POST `/external/install`,body `{source*}`。`source` 是 npm 包名(可带版本或 dist-tag)、`npm:@scope/pkg`、`github:owner/repo`、`owner/repo#ref`、`https://github.com/owner/repo`,或本地路径(目录、`.tgz` / `.tar.gz`、`.js` / `.ts`,以及 `file:` 地址)。`.jar` 以及不是 github.com 的 http(s) 地址返回 400。
POST /{id}/update 与 /external/{id}/update,无需请求体 → 更新后的同一插件项。npm 重新解析 latest(即使原安装来源指定了版本),GitHub 保留原 ref 和子目录,本地来源重新读取原路径。保留配置、持久数据、安装顺序和启停状态;下载、编译、包名校验或清单保存失败返回 400,并保留旧包和运行时。内置插件返回 400「内置插件随应用更新」。相同版本也会重新获取,不做版本大小比较。版本展示以已安装项为准,详见更新与版本。
GET /sources → [s];POST /sources,body {source*} → 去重后的源列表;POST /sources/remove,body {source*} → 剩余源列表,已安装插件不受影响。GET /catalog?source=<编码后的仓库地址> → [{id,name,description?,version?,source,icon?}],读取源中的包清单,未安装插件也可提供图标;安装时传该项的 source。默认源为 https://github.com/ezdemo/loopra-plugins,源列表持久化到 ~/.loopra/plugins/sources.json。
POST /{id}/toggle 与 /external/{id}/toggle,body {enabled:b} → 同一插件项。POST /{id}/remove|uninstall 与 /external/{id}/remove → 插件已卸载: <id>。builtin:true 时返回 400,正文是「内置插件不能卸载」。
POST /{id}/ui/open 为这个插件的设置页签发 HttpOnly cookie loopra_plugin_ui(Path /api/plugins/{id}/ui,SameSite=Lax,一小时),响应 {ok:true, page:"/plugins/{id}/ui/t/{ticket}/"}。page 里的票据不是访问密钥,只够读取这个插件的页面文件。GET /{id}/ui 与 /{id}/ui/* 返回入口目录里的静态文件,接受该 cookie 或 X-Loopra-Key,不接受 ?rk=。GET /{id}/ui/t/{ticket} 与其下的文件接受路径里的票据,不必带 cookie;_loopra/bridge.js 是宿主桥。页面的内容安全策略允许该目录下的脚本、样式、图片和字体,也允许内联脚本和样式,connect-src 'none'。POST /{id}/sidebar/open 与 GET /{id}/sidebar、/{id}/sidebar/*、/{id}/sidebar/t/{ticket}/* 同样处理侧栏页面,cookie Path 是 /api/plugins/{id}/sidebar。POST /{id}/menu/open 与 GET /{id}/menu、/{id}/menu/{child}、/{id}/menu/{child}/*、/{id}/menu/t/{ticket}/{child}/* 同样处理设置菜单,cookie Path 是 /api/plugins/{id}/menu。GET /{id}/icons/{key} 返回声明的图标,只接受访问密钥。GET /{id}/config 与 POST /{id}/config 读写插件自己的 JSON 对象(最大 64 KiB),只接受访问密钥。GET /schedules 返回已启用插件声明的定时任务,带 plugin:true。POST /schedules/{id}/run 立刻在纯聊天里执行一次。
工具公开名称是 <插件 id>__<导出名>。只读工具自动放行,写入工具走审批。安装文件在 ~/.loopra/plugins/packages/<id>,清单是 ~/.loopra/plugins/installed.json。
POST /{id}/panel/open 与 GET /{id}/panel、/{id}/panel/*、/{id}/panel/t/{ticket}/* 提供右侧面板页面,cookie Path 为 /api/plugins/{id}/panel,沙箱和资源范围沿用设置页规则。纯聊天也可使用插件面板。
POST /{id}/request,body {action*, value:{}, context?:{workspaceHash?,workspaceDir?,sessionId?,filePath?}} → 插件 onRequest({action,value,config,context}) 返回的 JSON 值。任意已启用、成功加载且声明 onRequest 的插件均可用,不限于 OpenAPI;操作由插件显式分发,不自动调用工具。请求体最多 64 KiB,action 不超过 128 字节,处理最多 30 秒。请求仍需要访问密钥,iframe 使用 service:request / service:result 消息,由工作台固定插件身份并代发当前上下文,不向 iframe 传密钥。后端可声明 openapi、sessions 宿主服务,见页面后端和声明宿主服务。
GET `` → []。POST `/install` 及其他写操作返回 400,提示改到插件页用 npm 包或 GitHub 地址安装。
GitStatusDTO={gitAvailable,initialized,branch,workspacePath,changed:[GitFileChange],untracked:[GitFileChange],model,modelChannelId,ahead:i,behind:i,upstream?};GitFileChange={path,index,workTree,status}(未跟踪项为 index:"?", workTree:"?", status:"U")。
主要端点:GET /branch|diff|status|diff-content|file-content|working-file-content|log(limit 50)|config、POST /init|commit|toggle|config|generate-commit-message|generate-environment-commit-message、GET /environment(+/status|/log)、POST /environment/commit|push|toggle、GET /environment/diff-content|working-file-content、POST /worktree/create({workspaceHash*, sessionName*, discardChanges?},幂等)。workspaceHash 大多可选(空=当前项目)。
POST /checkpoint(q: msgId*)→ {msgId,commitHash,treeHash,createdAt:l};POST /rollback(JSON 或 query:msgId? / rollbackTimestamp? / rollbackCode:b=true;纯聊天忽略 rollbackCode,只撤回消息)→ {msgId,commitHash,treeHash,success,message,rollbackUserText?};GET ``;GET /status;DELETE `/{msgId}`。
实体:{id,title,description,priority(high/medium/low),projectHash,projectName,status(todo/doing/done/failed),scheduleMode(immediate/scheduled),model,modelChannelId,reasoningEffort,hitl,approvalPending:b,scheduledAt:l,summary,sessionName:"req_<id>",createdAt,updatedAt}。
GET ;POST (title*、projectHash*);PUT /{id}(仅 description/priority);DELETE /{id};POST /{id}/comments({text*});GET /{id}/messages;POST /{id}/run(并发上限 2);POST /{id}/approval(approve/deny);POST /{id}/abort。
ScheduledTask={id,name,sessionName,sessionMode("independent"/"existing"/null),cronExpr?,intervalSec:l?,message*,enabled,lastRunAt,nextRunAt,runCount,lastResult,lastError,runs:[...],createdAt,updatedAt};q 均 workspaceHash*。GET 、`/{id}`;POST ;PUT /{id};POST /{id}/toggle|run;DELETE /{id}。校验:message 必填;非 independent 需 sessionName;cron 与 interval 至少一项。
GET /markets(默认 skillhub/clawhub);GET /proxy(action=trending|search);GET /detail(slug*);POST /install(slug*,装到 ~/.loopra/skills);POST /uninstall(slug*, scope?)。
GET /api/sub-agents → [{id,name,description,readOnly,systemPrompt,tools:[s],allowedTools?,enable?,builtin,plugin?,modelChannel?,model?}];PUT 全量保存({profiles:[{id*,name,description,instructions,enable?,readOnly?,allowedTools?,modelChannel?,model?}]})。plugin:true 的角色由已启用插件提供,保存时会被忽略。GET /denied-tools。
GET /api/sub-sessions(q: workspaceHash*, sessionName*)→ [{subSessionId,task,name,title,profile,status,startedAt,endedAt,eventCount,mtime}];GET /{subSessionId}/events;DELETE /{subSessionId}(运行中 400);POST /{subSessionId}/chat(body {"message"})→ SSE(sub_* 事件)。
GET /sources;GET /search(keyword* 空格 AND);POST /sources({docUrl*, headers?, authType?(none/bearer/apikey/basic), authConfig?});DELETE /sources(body {docUrl*});PUT /sources/refresh。
- POST
/api/model-meta/refresh→ s。 - GET
/api/version/→{version,buildTime,name};GET/version/check;POST/version/update(+GET、/stop)。开发桌面构建的版本检查直接返回当前版本和hasNewVersion:false,不请求发布服务。 - GET
/api/desktop-update/state→{supported,development?,unsupportedReason?,currentVersion,latestVersion,status,revision,source,changingSource,percent,transferred,total,bytesPerSecond,error,checkedAt,packageType?}。桌面正式打包产物需具备三段正式版本号和 RFC3339 构建时间;缺失时返回development:true,supported:false,status:"unsupported",保留当前版本并提供原因。 - POST
/api/desktop-update/check、/download、/source({source})、/install;GET/api/desktop-update/notes。开发构建的检查、下载返回原状态,更新源和安装请求返回 400,不触发发布检查或安装;手动读取日志仍可请求正式发布历史,忽略本地开发版本号的大小,且不会启动更新任务或写入下载文件。普通 Web 服务未创建桌面更新器时,这组接口返回 404。 - POST
/api/system/open-workspace({workspaceHash?, application?});GET/open-applications;POST/pick-directory;GET/health({status:"ok",version,buildTime});GET/system/version。 - Pets:GET
/api/pets、/{name}、/{name}/spritesheet(webp,支持 ?rk=)、/active(active 时含 position、scale?、desktop?;desktop 为原生窗口右下角的屏幕坐标)、/desktop({native, dismissed};native 为真时本进程用 mygo 画宠物,页面不再叠在输入框上);PUT/active({name*})、/position(x、y、sizeIndex、scale、desktopX、desktopY;任一桌面坐标为 null 时清除 desktop)、/desktop({shown, name, label, activity, reply, replyId, replyActivity, latest, scale, seen, reset?};seen 小于 dismissed 时忽略显示);DELETE/{name}。 - Files:GET
/api/files/image|tree|search、DELETE/delete(禁删项目根)、POST/paste({content*}→.loopra/paste)。/api/files/image:path必填,workspaceHash与可选sessionName确定文件所属环境。相对路径基于会话实际工作目录(含隔离工作树),缺失工作树不回退主项目;省略会话时保持项目根目录行为。绝对路径沿用只读图片访问语义,可读取临时目录图片。只接受普通文件、最多 5 MiB,以及 PNG/JPEG/GIF/WebP/BMP/AVIF 内容类型;拒绝 SVG。响应为图片二进制,保留鉴权、GET/HEAD、nosniff与no-store。
- PromptPresets:GET/PUT
/api/prompt-presets([{id,label≤24,text≤500}],id 空自动 UUID)。
全局资源,沿用访问密钥鉴权与响应包装,不受当前工作区选择影响:
| 方法与路径 | 请求 / 响应 |
|---|---|
GET / HEAD /bots |
[{id,name,platform:"feishu",appId,ownerId,workspaceHash,enabled,allowedUserIds:[],allowedChatIds:[],chats:[],state,error?,createdAt}],不返回 App Secret |
PUT /bots/{id} |
全量可编辑配置 {name,enabled,allowedUserIds:[],allowedChatIds:[]},返回更新后的 Bot |
DELETE /bots/{id} |
停止连接、取消所属执行与未完成授权,保留项目和历史 |
POST /bots/registrations |
{name?,botId?};botId 表示重新授权已有应用,省略时创建;返回授权状态 |
GET / HEAD /bots/registrations/{id} |
{id,name,state,url?,qrCode?,expiresAt?,botId?,error?};二维码为本地生成的 PNG data URL,响应 Cache-Control:no-store |
DELETE /bots/registrations/{id} |
取消授权轮询,不创建 Bot |
授权状态为 starting / waiting / completed / failed / cancelled;连接状态为 connecting / connected / reconnecting / offline / error / disabled。二维码过期、拒绝或网络错误可以重新获取;重新授权须返回同一 App ID,保留项目及聊天绑定。新授权成功后才创建默认项目,不切换当前项目。
资源不存在返回 404 / bot_not_found,服务关闭返回 503 / bot_unavailable,配置保存失败返回 500 / bot_storage_failed;保存失败保留旧配置和连接。
chats 项为 {chatId,sessionName,chatType,updatedAt}。Bot 只有一个默认项目,多代码库访问复用该项目的 linkedProjectHashes / linkedProjectPaths,不按飞书聊天切换项目。单聊、群聊分别映射会话,工作台与飞书共用历史、执行互斥、取消及工具审批。首版使用 Native,回复通过 post 富文本的 md 节点由飞书原生渲染 Markdown,长回复分段时保留代码围栏及语言标记;附件不会交给模型假定已读取。默认仅 owner 可用,其他用户需授权;群聊还须允许该群并明确 @Bot,忽略 @all。
消息先持久化绑定及最近 512 条消息 ID,再异步执行,避免模型耗时阻塞事件确认。同一聊天执行中不接受第二轮,返回忙碌提示;保存失败不执行,重启后保留去重信息。已接收的失败任务不会因同一消息重复投递自动重跑,需用户再次发送。单轮最长 30 分钟,工作台审批计入该时长。
- 前端产物在 classpath
/static/(GET/HEAD 免鉴权由 §2 规则判定)。 GET /config.json(免鉴权、无 ApiResponse 包装)输出:{\n "apiBase": "/"\n}。- 未匹配路由 404(非 ApiResponse 体)。
ApiResponse省略 null 字段(非 null 才输出)。- 401 固定四段体(含
reason:"access_key_required");常量时间比较 key。 - SSE 帧严格遵守
event:+data:+ 双\n;文本事件(content/reasoning)的 data 是裸 JSON 字符串,需自实现转义("、\、\n、\r、\t、<0x20 →\u00XX)。 - SSE 中途错误一律 HTTP 200 +
error事件 + 紧跟done;仅建流前校验错误用 HTTP 状态码。 - 建流后发生的 409/503(requestId 重复、队列满、会话运行中)通过 SSE
error事件报告。