Load plugin workspace declarations and expose GET /api/workspaces (T4)
Why: T4 workspaces are declared by plugins via provides.workspaces (Core PluginWorkspace is the authoritative type); the host must surface the merged overlay for the frontend to render in OverlayHost. Evidence: go test green - plugins suite new TestWorkspacesOverlay (manifest + workspaces/main.json loads, ListWorkspaces returns the declaration with shortcut and body); full internal sweep unchanged. Impact: plugins host loads workspaces/*.json assets and exposes ListWorkspaces; server adds GET /api/workspaces passthrough; no behavior change without the manifest key. Refs: docs/proposals/2026-09-30-ui-inbox-command-workspace.md T4; task card feature/workspaces-r1
This commit is contained in:
@@ -27,6 +27,7 @@ type Loaded struct {
|
||||
Workflows []core.WorkflowDef
|
||||
Intents []json.RawMessage // agent-intent overlay 形态(目录侧消费)
|
||||
Commands []json.RawMessage // Agent-Command spec overlay(cmdbus 注册)
|
||||
Workspaces []core.PluginWorkspace // 工作区弹层声明(OverlayHost 渲染)
|
||||
Prompts map[string]string // 文件名 → 内容(画像模板)
|
||||
PluginSettings []core.SettingDesc
|
||||
Defaults map[string]any // 插件设置默认值(并入有效设置)
|
||||
@@ -188,6 +189,15 @@ func (h *Host) loadOne(dir string) error {
|
||||
ld.Commands = append(ld.Commands, specs...)
|
||||
}
|
||||
|
||||
// workspaces/*.json:工作区弹层声明(T4;OverlayHost 渲染,声明权威在 Core 类型)。
|
||||
for _, name := range m.Provides.Workspaces {
|
||||
var ws []core.PluginWorkspace
|
||||
if err := readAsset(dir, "workspaces", name, &ws); err != nil {
|
||||
return fmt.Errorf("plugins: %s: workspaces %s: %w", m.ID, name, err)
|
||||
}
|
||||
ld.Workspaces = append(ld.Workspaces, ws...)
|
||||
}
|
||||
|
||||
// prompts/*.md:画像模板(source=plugin,可被用户复制微调)。
|
||||
for _, name := range m.Provides.Prompts {
|
||||
content, err := os.ReadFile(filepath.Join(dir, "prompts", name+".md"))
|
||||
@@ -238,6 +248,17 @@ func (h *Host) collectIntentsLocked() []json.RawMessage {
|
||||
return out
|
||||
}
|
||||
|
||||
// ListWorkspaces 全部已装载插件的工作区声明(GET /api/workspaces 数据源)。
|
||||
func (h *Host) ListWorkspaces() []core.PluginWorkspace {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
var out []core.PluginWorkspace
|
||||
for _, ld := range h.loaded {
|
||||
out = append(out, ld.Workspaces...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// collectCommandsLocked 汇总全部已装载插件的命令 overlay(调用方持锁)。
|
||||
func (h *Host) collectCommandsLocked() []json.RawMessage {
|
||||
var out []json.RawMessage
|
||||
|
||||
@@ -125,3 +125,33 @@ func TestAPIVersionGuard(t *testing.T) {
|
||||
t.Fatal("incompatible apiVersion should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspacesOverlay(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
st, err := store.OpenSQLite(filepath.Join(dir, "test.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
reg := tools.NewRegistry()
|
||||
reg.RegisterBuiltin()
|
||||
pluginDir := filepath.Join(dir, "plugins")
|
||||
os.MkdirAll(filepath.Join(pluginDir, "wsdemo", "workspaces"), 0o755)
|
||||
os.WriteFile(filepath.Join(pluginDir, "wsdemo", "plugin.json"), []byte(`{"id":"wsdemo","name":"WS","version":"1.0.0","apiVersion":"1.0","provides":{"workspaces":["main"]}}`), 0o644)
|
||||
os.WriteFile(filepath.Join(pluginDir, "wsdemo", "workspaces", "main.json"), []byte(`[
|
||||
{"id":"demo.governance","title":"治理总览","icon":"🛡","shortcut":"5","body":[{"type":"markdown","markdown":"声明式工作区"}]}
|
||||
]`), 0o644)
|
||||
setSvc := settings.NewService(st)
|
||||
h := New(st, reg, setSvc, pluginDir)
|
||||
if errs := h.LoadDir(); len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("load errs: %v", errs)
|
||||
}
|
||||
}
|
||||
}
|
||||
ws := h.ListWorkspaces()
|
||||
if len(ws) != 1 || ws[0].ID != "demo.governance" || ws[0].Shortcut != "5" || len(ws[0].Body) != 1 {
|
||||
t.Fatalf("workspaces: %+v", ws)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +178,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/conversations/{id}/plan", s.getPlan)
|
||||
mux.HandleFunc("GET /api/conversations/{id}/todos", s.getTodos)
|
||||
mux.HandleFunc("GET /api/commands", s.listCommands)
|
||||
mux.HandleFunc("GET /api/workspaces", s.listWorkspaces)
|
||||
mux.HandleFunc("POST /api/commands/{id}", s.execCommand)
|
||||
mux.HandleFunc("GET /api/commands/audit", s.listAudits)
|
||||
mux.HandleFunc("GET /api/parsers", s.listParsers)
|
||||
@@ -587,6 +588,15 @@ func (s *Server) execCommand(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, code, out)
|
||||
}
|
||||
|
||||
// listWorkspaces 插件工作区声明(T4;前端合并其内置工作区后渲染 OverlayHost)。
|
||||
func (s *Server) listWorkspaces(w http.ResponseWriter, r *http.Request) {
|
||||
ws := []core.PluginWorkspace{}
|
||||
if s.plugins != nil {
|
||||
ws = s.plugins.ListWorkspaces()
|
||||
}
|
||||
writeJSON(w, http.StatusOK, ws)
|
||||
}
|
||||
|
||||
// listAudits 审计查询(会话维度或全局;入口对称的可核查凭证)。
|
||||
func (s *Server) listAudits(w http.ResponseWriter, r *http.Request) {
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
|
||||
Reference in New Issue
Block a user