@zhama/tego-plugin-sdk 1.36.2 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/agent/RULES.md +123 -0
  2. package/agent/guides/access.md +39 -0
  3. package/agent/guides/backend.md +76 -0
  4. package/agent/guides/claw.md +18 -0
  5. package/agent/guides/desktop-web.md +48 -0
  6. package/agent/guides/frontend.md +83 -0
  7. package/agent/guides/host-api-versions.md +34 -0
  8. package/agent/guides/i18n.md +17 -0
  9. package/agent/guides/manifest.md +26 -0
  10. package/agent/guides/migrations.md +17 -0
  11. package/agent/guides/storage.md +18 -0
  12. package/agent/guides/testing.md +17 -0
  13. package/agent/guides/ui.md +22 -0
  14. package/agent/reference/plugin-sdk-api.md +561 -0
  15. package/bin/tego-plugin-pack.mjs +1 -1
  16. package/bin/tego-plugin.mjs +4 -2
  17. package/dist/chunk-2NTAWYCG.js +1 -0
  18. package/dist/chunk-2P6UEAPV.cjs +1 -0
  19. package/dist/chunk-45Y4CR4R.cjs +1 -0
  20. package/dist/{chunk-PWSTCQRN.cjs → chunk-4LKRXDYY.cjs} +13 -12
  21. package/dist/{chunk-7XHF3YSK.js → chunk-56QOZQTY.js} +1 -1
  22. package/dist/{chunk-C2OCOPZQ.cjs → chunk-6HNG5OZI.cjs} +1 -1
  23. package/dist/{chunk-2ORO36BY.js → chunk-DEGQDJF6.js} +1 -1
  24. package/dist/chunk-EIVA5BR6.js +1 -0
  25. package/dist/{chunk-P2Q2AKWP.js → chunk-FCYAAOHC.js} +17 -16
  26. package/dist/chunk-HZ4YWRYX.cjs +5 -0
  27. package/dist/chunk-I5Z3ECC4.js +5 -0
  28. package/dist/chunk-INCA3GFW.js +1 -0
  29. package/dist/chunk-IVJECPGG.js +1 -0
  30. package/dist/{chunk-KMN5RHLX.cjs → chunk-MWMGJJ74.cjs} +1 -1
  31. package/dist/chunk-N4SXZZPY.cjs +1 -0
  32. package/dist/chunk-SKNPNPJP.cjs +1 -0
  33. package/dist/dev.cjs +165 -59
  34. package/dist/dev.d.cts +37 -1
  35. package/dist/dev.d.ts +37 -1
  36. package/dist/dev.js +168 -62
  37. package/dist/doctor.cjs +8 -0
  38. package/dist/doctor.d.cts +62 -0
  39. package/dist/doctor.d.ts +62 -0
  40. package/dist/doctor.js +8 -0
  41. package/dist/frontend.d.cts +33 -1
  42. package/dist/frontend.d.ts +33 -1
  43. package/dist/hono.cjs +1 -0
  44. package/dist/hono.d.cts +84 -0
  45. package/dist/hono.d.ts +84 -0
  46. package/dist/hono.js +1 -0
  47. package/dist/index.cjs +1 -1
  48. package/dist/index.d.cts +6 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.js +1 -1
  51. package/dist/manifest.cjs +1 -1
  52. package/dist/manifest.d.cts +55 -2
  53. package/dist/manifest.d.ts +55 -2
  54. package/dist/manifest.js +1 -1
  55. package/dist/migrations-sync.cjs +1 -0
  56. package/dist/migrations-sync.d.cts +20 -0
  57. package/dist/migrations-sync.d.ts +20 -0
  58. package/dist/migrations-sync.js +1 -0
  59. package/dist/pack.cjs +1 -1
  60. package/dist/pack.d.cts +5 -0
  61. package/dist/pack.d.ts +5 -0
  62. package/dist/pack.js +1 -1
  63. package/dist/publish.cjs +1 -1
  64. package/dist/publish.js +1 -1
  65. package/dist/react.cjs +1 -0
  66. package/dist/react.d.cts +101 -0
  67. package/dist/react.d.ts +101 -0
  68. package/dist/react.js +1 -0
  69. package/dist/scaffold.cjs +671 -17
  70. package/dist/scaffold.d.cts +42 -18
  71. package/dist/scaffold.d.ts +42 -18
  72. package/dist/scaffold.js +673 -19
  73. package/dist/server-runtime.cjs +1 -1
  74. package/dist/server-runtime.d.cts +27 -2
  75. package/dist/server-runtime.d.ts +27 -2
  76. package/dist/server-runtime.js +1 -1
  77. package/dist/testing.cjs +6 -2
  78. package/dist/testing.d.cts +19 -1
  79. package/dist/testing.d.ts +19 -1
  80. package/dist/testing.js +6 -2
  81. package/dist/verify-FF2RD2MV.cjs +1 -0
  82. package/dist/verify-W7ZTUCO4.js +1 -0
  83. package/dist/vite.cjs +1 -1
  84. package/dist/vite.d.cts +27 -1
  85. package/dist/vite.d.ts +27 -1
  86. package/dist/vite.js +1 -1
  87. package/package.json +31 -3
  88. package/dist/chunk-3WA3M347.cjs +0 -1
  89. package/dist/chunk-5CU33JVY.js +0 -1
  90. package/dist/chunk-7K37PVGV.js +0 -5
  91. package/dist/chunk-BTHXQWHT.js +0 -1
  92. package/dist/chunk-ROFUQLOZ.cjs +0 -1
  93. package/dist/chunk-W7C357A6.cjs +0 -5
@@ -0,0 +1,561 @@
1
+ # Plugin SDK API 参考
2
+
3
+ Tego 插件可以通过 SDK 接入宿主页面、业务后端、租户人员目录、文件存储、知识服务和数字员工协作。前端从 `mount(el, ctx)` 获取宿主上下文,TypeScript 后端从生命周期和请求处理函数获取 `host`,Python 后端通过 `host()` 获取安装所属的宿主服务。
4
+
5
+ 本文面向插件开发者,按能力列出公开入口、主要参数、返回结果和调用条件。基准日期:2026-10-07;当前源码的 **Host API 为 `1.22.0`**,TypeScript 包 `@tego/plugin-sdk` 与 Python 包 `tego-plugin-sdk` 的包版本均为 `0.1.0`。`hostApiRange` 对应 Host API 版本,不能填写 SDK 包版本。实际安装环境须满足该版本范围及所用能力的声明。
6
+
7
+ ## 能力总览
8
+
9
+ | 开发需求 | 接口入口 | 能力 |
10
+ | ------------------ | ------------------------------------------- | ------------------------------------------------ |
11
+ | 接入插件页面 | `@tego/plugin-sdk/frontend` | 挂载、路由、导航、当前用户、配置、插件 API 请求 |
12
+ | 页面与 AI 协作 | `ctx.ai`、`@tego/chat/plugin-surface` | AI 草稿、AI 填表、记录旁的会话面板 |
13
+ | 打开 Office 产物 | `ctx.office.open` | 经权限复核后打开文件副本 |
14
+ | 接入业务后端 | `@tego/plugin-sdk/server` | HTTP 请求、公开请求、启动和停止、受管下载授权 |
15
+ | 保存业务数据 | `host.db` | 插件专属 PostgreSQL schema、参数化查询、事务 |
16
+ | 保存和交付文件 | `host.storage` | 对象读写、上传链接、下载链接、流式传输、复制 |
17
+ | 更新页面和通知用户 | `host.events`、`host.notifications` | 实时变更提示、持久化站内通知 |
18
+ | 读取组织与人员 | `host.directory` | 人员和组织查询、身份、成员匹配、主体校验 |
19
+ | 管理普通账号 | `host.accounts` | 邀请、直接注册、查询和重发邀请、重置密码 |
20
+ | 接入知识服务 | `host.knowledge` | 引用核验、插件知识集合、文件解析、搜索、权限投影 |
21
+ | 获取协作依据 | `host.sources`、`host.delegations` | 已授权来源读取、当前工作委托查询 |
22
+ | 生成图表图片 | `host.diagrams` | Mermaid 和静态 HTML 转 PNG |
23
+ | 用事件启动员工任务 | `host.proactive` | 启动用户监听本插件事件的定时任务 |
24
+ | 控制插件业务权限 | `@tego/plugin-sdk/access` | 权限目录、角色、资源范围、路由策略、审计 |
25
+ | 构建和交付插件 | `vite`、`pack`、`publish`、`testing` 子入口 | 前端构建、ZIP 打包、模板编译、发布、后端测试 |
26
+
27
+ ## TypeScript 公开入口
28
+
29
+ 公开导入路径以 [package.json 的 exports](../../packages/plugin-sdk/package.json) 为准。不要依赖未导出的 `src/*` 路径。
30
+
31
+ | 导入路径 | 主要内容 |
32
+ | --------------------------------------------- | -------------------------------------------------------------------------------------------------- |
33
+ | `@tego/plugin-sdk` | Manifest 类型、版本兼容、路径、语言、配置和安装披露辅助函数 |
34
+ | `@tego/plugin-sdk/manifest` | Manifest Zod schema、解析、权限目录检查、Python wheel 与依赖锁检查 |
35
+ | `@tego/plugin-sdk/frontend` | 前端模块和上下文、AI 表单类型及辅助函数 |
36
+ | `@tego/plugin-sdk/server` | 后端模块、Host API 类型、目录/账号/知识/图表输入 schema 和返回类型 |
37
+ | `@tego/plugin-sdk/access` | 权限目录、引擎、主体、范围、SQL 片段、管理服务和错误类型 |
38
+ | `@tego/plugin-sdk/access/hono` | Hono 路由策略与权限管理路由、策略覆盖检查 |
39
+ | `@tego/plugin-sdk/access/drizzle` | 权限表的 Drizzle 定义 |
40
+ | `@tego/plugin-sdk/access/implications` | 权限蕴含展开、授权合并、范围排序 |
41
+ | `@tego/plugin-sdk/access.sql` | 权限表 SQL 资源,供 Python 插件迁移使用 |
42
+ | `@tego/plugin-sdk/access-catalog.schema.json` | 权限目录 JSON Schema 资源 |
43
+ | `@tego/plugin-sdk/vite` | `tegoPluginFrontend`、`TEGO_PLUGIN_DEV_OVERRIDES` |
44
+ | `@tego/plugin-sdk/server-runtime` | `serveBackend`、`readUserContext`、`readPublicContext`、`createScopedDb`、`checkDatabaseReachable` |
45
+ | `@tego/plugin-sdk/egress` | `connectTcp`、`startTcpTunnel` |
46
+ | `@tego/plugin-sdk/gateway-signature` | 用户/公开请求/存储授权签名和验签、网关密钥派生 |
47
+ | `@tego/plugin-sdk/mcp-identity` | MCP 身份上下文签名、请求头生成、验签和 SHA-256 |
48
+ | `@tego/plugin-sdk/pack` | 打包、后端构建、模板编译、Python 依赖锁、前端 watch |
49
+ | `@tego/plugin-sdk/publish` | `publishPlugin`、`publishPackedPlugin` |
50
+ | `@tego/plugin-sdk/testing` | `createTestPluginHost`、`TEST_TENANT_ID` 及测试宿主类型 |
51
+
52
+ `pack`、`publish`、`testing` 提供 ESM 入口;其余代码入口当前同时提供 ESM 与 CommonJS。两个 SQL/JSON 路径是资源文件。
53
+
54
+ ## 前端 API
55
+
56
+ 源码:[frontend.ts](../../packages/plugin-sdk/src/frontend.ts)、[ai-form.ts](../../packages/plugin-sdk/src/ai-form.ts)。
57
+
58
+ ### 页面生命周期
59
+
60
+ `definePluginFrontend({ mount })` 是类型推断辅助函数。入口默认导出该模块,`mount(el, ctx)` 返回完整卸载函数,用于销毁组件、取消订阅和清理登记的能力。宿主切换插件内部 URL 时不会重新挂载,应订阅路由变化。
61
+
62
+ ```ts
63
+ import { definePluginFrontend } from '@tego/plugin-sdk/frontend';
64
+
65
+ export default definePluginFrontend({
66
+ mount(el, ctx) {
67
+ const renderRoute = () => {
68
+ el.textContent = `${ctx.user.displayName}:${ctx.route.current().path}`;
69
+ };
70
+ renderRoute();
71
+ const unsubscribe = ctx.route.subscribe(renderRoute);
72
+ return () => {
73
+ unsubscribe();
74
+ el.replaceChildren();
75
+ };
76
+ },
77
+ });
78
+ ```
79
+
80
+ ### 宿主上下文
81
+
82
+ | 成员 | 参数或返回值 | 用途与约束 |
83
+ | -------------------------------------- | -------------------------------------------- | --------------------------------------------------------------- |
84
+ | `ctx.pluginId`、`ctx.basePath` | 字符串 | 当前插件 ID、宿主内页面前缀 |
85
+ | `ctx.user` | `userId`、`tenantId`、`roles`、`displayName` | 当前用户;业务权限使用 access SDK,不根据宿主角色名自行判断 |
86
+ | `ctx.activeAvatar?` | `{ id, name }` | 当前选中的数字员工;用于界面上下文,不能作为机器身份或授权证明 |
87
+ | `ctx.delegation?` | `current`、`subscribe`、`open` | 宿主统一的工作授权状态与授权对话框;见下文「工作授权」 |
88
+ | `ctx.config` | `Record<string, unknown>` | 仅前端配置,已应用默认值;不包含后端和 Claw 配置或秘密 |
89
+ | `ctx.api.request(path, init?)` | `Promise<Response>` | 自动附加认证,路径相对当前插件后端命名空间 |
90
+ | `ctx.api.json<T>(path, init?)` | `Promise<T>` | JSON 请求;非 2xx 会抛错 |
91
+ | `ctx.route.current()` | `{ path, search }` | 当前插件内路径及带 `?` 的查询串 |
92
+ | `ctx.route.subscribe(listener)` | 取消订阅函数 | 监听宿主导航;可接入 `useSyncExternalStore` |
93
+ | `ctx.navigate(to)` | 宿主路径 | 在宿主内导航,支持绝对宿主路径 |
94
+ | `ctx.can(navKey)` | `boolean` | 是否为 manifest 声明的导航 key;不替代资源授权 |
95
+ | `ctx.publicPagesUrl` | 以 `/` 结尾的绝对地址 | 构建公开页面链接或二维码;未声明公开前端时访问返回 404 |
96
+ | `ctx.hostUrl(path)` | 绝对地址 | 将宿主签发的上传/下载路径转换为浏览器可访问地址 |
97
+ | `ctx.layout.collapseSidebar()` | 恢复函数 | 收起宿主导航侧栏;页面结束使用时调用返回函数 |
98
+ | `ctx.events.subscribe(listener)` | 取消订阅函数 | 接收 `{ name, payload, timestamp }`,名称已去掉插件命名空间前缀 |
99
+ | `ctx.events.connected()` | `boolean` | 查询事件连接状态 |
100
+ | `ctx.events.subscribeStatus(listener)` | 取消订阅函数 | 监听事件连接状态变化 |
101
+ | `ctx.platform?` | `'web' \| 'desktop'` | 1.37;运行的壳。代码应在两端都正确,不要据此分叉业务逻辑 |
102
+ | `ctx.openExternal?(url)` | `Promise<void>` | 1.37;在系统浏览器(桌面端)或新标签页(web)打开外部链接 |
103
+ | `ctx.download?({ path \| blob, fileName })` | `Promise<void>` | 1.37;保存宿主路径或页面生成的文件;桌面端弹出原生保存对话框 |
104
+ | `ctx.copyText?(text)` | `Promise<boolean>` | 1.37;复制到剪贴板,壳拒绝时返回 `false` |
105
+
106
+ web 与桌面端(Tauri)的差异:桌面端 CSP 不允许 `eval`/`new Function`/`blob:` 脚本,`location.origin` 是 `tauri://localhost`,`ctx.api` 经 Rust 代理整包缓冲(上限 100 MB),`window.open` 与 `<a download>` 不可靠;web 端 CSP 只允许连接平台。外链、下载、复制请用上面 1.37 的能力,详见 SDK 的 `agent/guides/desktop-web.md`。
107
+
108
+ `ctx.api` 只能访问当前插件后端。事件流会发送给当前租户中打开该插件的用户,没有历史重放;payload 应只携带变化提示和必要 ID。页面收到事件后重新调用已授权的业务 API,断线期间保留刷新机制。
109
+
110
+ ### 工作授权
111
+
112
+ 授权由宿主统一处理,插件不再自建「授权 / 撤销」按钮,也不再调用 `/_host/assistant` 开关委托。manifest 声明 `workDelegation` 且 `hostApiRange` 起点不低于 1.33 时,宿主提供 `ctx.delegation`:
113
+
114
+ | 成员 | 返回值 | 用途与约束 |
115
+ | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------- |
116
+ | `current(avatarId?)` | `PluginDelegationState` | 默认取当前选中员工;首次读取时开始加载。状态未变时返回同一对象,可直接接入 `useSyncExternalStore` |
117
+ | `subscribe(listener)` | 取消订阅函数 | 状态变化时通知 |
118
+ | `open({ avatarId? })` | `Promise<PluginDelegationState>` | 打开宿主对话框:未授权时请求授权,已授权时查看或撤销;对话框关闭后返回最新状态 |
119
+
120
+ `status` 取值:`loading`、`no_employee`(未选员工)、`ineligible`(员工角色不在 `allowedAvatarRoles` 内)、`not_ready`(插件尚未在该员工上就绪)、`granted`、`not_granted`;`actions` 仅在 `granted` 时非空。
121
+
122
+ - 用户首次带着未授权的员工进入插件时,宿主自动弹出对话框;用户拒绝后插件照常可用,宿主不再对该员工自动弹出,需要授权的按钮调用 `open()` 即可再次打开。
123
+ - 授权按员工、按人记录;授权与撤销写入治理审计并记录当时的插件版本,插件升级不要求重新授权。
124
+ - 对话框内容来自 manifest:`summary`(每种语言不超过 120 字符)、`privacyPolicyUrl`(https)、`conversationCapture`(为读取 `host.sources` 的插件提供「记录我与该员工的新对话」选项,默认不勾选),以及后端声明的 `egress`。这三个字段要求 `hostApiRange` 不低于 `^1.33.0`。
125
+ - 没有前端的插件无需额外工作:员工使用指南里的授权项会打开同一个对话框。`workDelegation` 必须配有后端部件,委托以签名的 `delegation` 随机器请求到达后端,由后端核验。
126
+ - `settingsRoute` 已弃用,宿主不再把使用指南的授权项链接到插件页面。
127
+
128
+ ### AI 草稿与 AI 填表
129
+
130
+ | API | 输入 | 输出 |
131
+ | ---------------------- | ---------------------------------------------- | --------------------------- |
132
+ | `ctx.ai?.draft` | `{ message, signal: AbortSignal, timeoutMs? }` | `Promise<{ text: string }>` |
133
+ | `ctx.ai?.registerForm` | `{ id, title, element, getSnapshot, apply }` | 取消登记函数 |
134
+
135
+ `draft` 使用所选数字员工进行独立生成,不向聊天投递消息、不授予工具写入能力。草稿仍须通过业务校验和正式保存接口。
136
+
137
+ Host API `1.27.0` 起支持 `timeoutMs`:默认 `120000` 毫秒,必须是 `1000`~`600000` 之间的整数;非法值在派发请求前拒绝。依赖此参数的插件必须声明 `hostApiRange: "^1.27.0"`,避免旧宿主忽略参数而仍按默认时限执行。模型执行预算向上取整到秒,宿主在此基础上增加 30 秒接收最终结果,RPC 再增加 5 秒等待;例如 `600000` 对应模型 600 秒、宿主 630 秒、RPC 635 秒。宿主等待从员工连接解析完成、派发请求前开始计时;模型服务自身的超时限制仍可能先触发。取消 `signal`、断开连接或切换员工会立即请求中止,并丢弃后到结果。
138
+
139
+ 登记表单时,`element` 必须已挂载;`getSnapshot()` 返回最新的 `{ fields, instructions? }`。每个字段包含 `name`、`label`、`type`、`value`,枚举字段增加 `options: [{ value, label }]`。支持 `text`、`number`、`boolean`、`select`、`date`,值为字符串、数字、布尔值或 `null`。只登记可见、可编辑、非秘密字段;`apply(values)` 只更新本地表单状态,由用户沿原保存流程提交。
140
+
141
+ 同一入口还导出 `createPluginAiFormRegistry()`、`buildPluginAiFormPrompt()`、`resolvePluginAiFormPatch()`。它们分别用于表单登记管理、提示构造和回填校验;正常插件接入通常只需 `registerForm`。补丁解析会检查类型/枚举,并返回 `{ values, conflicts }`,保护生成期间用户手工修改的字段。
142
+
143
+ ### Office 产物打开
144
+
145
+ ```ts
146
+ await ctx.office?.open({
147
+ sourcePath: '/artifacts/42/office-metadata',
148
+ // signal: abortController.signal,
149
+ });
150
+ ```
151
+
152
+ `sourcePath` 是当前插件后端的元数据接口,须先验证业务权限,再返回 `PluginOfficeArtifact`:`{ fileName, sha256, sizeBytes, downloadPath }`。`downloadPath` 必须是 `storage.managedPath` 签发的路径。宿主复核归属、下载权限、文件长度及哈希后打开独立副本,编辑保存不会写回插件正式产物。单次预览读取上限为 64 MiB;旧宿主没有 `ctx.office`,应保留下载入口。
153
+
154
+ ### 共享界面与会话组件
155
+
156
+ 宿主共享的模块为 `react`、`react-dom`、`react-dom/client`、`react/jsx-runtime`、`@tego/ui`、`react-i18next`、`lucide-react`、`@tego/chat/plugin-surface`。前端使用 `@tego/ui` 组件,翻译命名空间使用 `pluginI18nNamespace(ctx.pluginId)`。
157
+
158
+ `@tego/chat/plugin-surface` 提供 `ChatSurface`、`ChatPanel`、`useTopic` 和 `ConversationScope`、`Topic` 等类型。记录页优先使用自带宿主会话上下文的 `ChatSurface`;单独的 `ChatPanel` 需要自行准备相应 provider。这些是共享模块的 API,不属于 `plugin-sdk/frontend` 的导出。插件需要 TanStack Query 时自行打包并挂载 `QueryClientProvider`。
159
+
160
+ `@tego/chat` 不对外发布,对外发布的 SDK 也不包含它的类型:`@tego/chat/plugin-surface` 只供仓库内插件使用,类型取自 workspace 包。
161
+
162
+ ## 后端生命周期与请求上下文
163
+
164
+ 源码:[server.ts](../../packages/plugin-sdk/src/server.ts)、[server-runtime.ts](../../packages/plugin-sdk/src/server-runtime.ts)。
165
+
166
+ | API | 签名 | 行为 |
167
+ | --------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------- |
168
+ | `definePluginBackend` | `(module: PluginBackendModule)` | 类型推断辅助,返回模块 |
169
+ | `fetch` | `(request, ctx, host) → Promise<Response>` | 必需,处理已验证用户或数字员工请求 |
170
+ | `fetchPublic?` | `(request, publicCtx, host) → Promise<Response>` | 仅处理 manifest 声明的公开路由;未提供时返回 404 |
171
+ | `start?` | `(host) → void / Promise<void>` | 数据库与配置就绪后启动安装级维护任务;没有用户身份 |
172
+ | `stop?` | `() → void / Promise<void>` | 优雅退出时停止维护并等待在途工作 |
173
+ | `authorizeStorage?` | `(request, policyCtx, host) → Promise<Response>` | 处理已验签的 `POST /api/internal/storage/authorize`;允许下载时返回 `{ allowed: true }` |
174
+ | `serveBackend` | `(module) → void` | Node 后端 runner,负责宿主客户端、健康检查、验签、请求分派和生命周期 |
175
+
176
+ `PluginUserContext` 包含 `userId`、`tenantId`、`roles`,机器请求还可有 `avatarId` 和已签名的 `delegation: { id, actions }`,actions 为 `records.read`、`work.prepare`。调用者身份由宿主网关核验,插件不解析用户 token;普通请求、公开请求、存储授权请求分别处理。
177
+
178
+ `PluginPublicContext` 包含 `tenantId`、`installId`、`route`、`clientIp` 及可选 `avatarId`,没有用户身份或业务权限。公开链接内的业务凭证仍由插件核验。存储授权上下文包含 `installId`、`tenantId`、`issuedAt`、`bodyHash`,不能冒充普通用户请求。
179
+
180
+ 自定义运行时可使用 `readUserContext(headers, { key, nowMs })`、`readPublicContext(headers, { key, nowMs })`,验签失败返回 `null`;`createScopedDb(databaseUrl)` 创建数据库接口,`checkDatabaseReachable(db, databaseUrl)` 成功返回 `null`、失败返回诊断字符串。一般插件使用 `serveBackend`,无需自行解析网关头。
181
+
182
+ ## 后端 Host API
183
+
184
+ `host.pluginId`、`host.tenantId` 标明当前安装所属插件与租户。Host API 凭证属于安装,不是当前用户的会话凭证;调用宿主能力之前,业务接口仍须核验操作者权限。类型中带 `?` 的能力使用前需要做能力检测。
185
+
186
+ ### 数据库与配置
187
+
188
+ | API | 返回值 | 说明 |
189
+ | -------------------------------- | ---------------------------------- | ------------------------------------------------------------ |
190
+ | `host.db.query<T>(sql, params?)` | `Promise<T[]>` | 参数化 SQL,仅当前插件 schema;没有宿主业务表访问权 |
191
+ | `host.db.transaction(body)` | `Promise<T>` | 同一连接执行事务,`body(tx)` 抛错会回滚;内部使用 `tx.query` |
192
+ | `host.config.current()` | `Record<string, unknown>` | 最近一次后端配置快照,启动服务前已经读取 |
193
+ | `host.config.refresh()` | `Promise<Record<string, unknown>>` | 主动重新读取,例如密钥轮换;没有配置推送通道 |
194
+ | `host.log.debug/info/warn/error` | `void` | `(message, fields?)`,结构化日志;不要记录密码、token 或密钥 |
195
+
196
+ 数据库由 `declares.database` 控制,有后端时默认开启;关闭时不能使用数据库能力。前端、后端、Claw 配置分别声明和存储,后端只能读取后端部分。
197
+
198
+ ### 文件存储
199
+
200
+ 源码与返回类型:[PluginScopedStorage](../../packages/plugin-sdk/src/server.ts)。所有 key 都相对当前安装的存储前缀。
201
+
202
+ | API | 主要输入 | 返回值 |
203
+ | ------------- | ------------------------------------------ | ------------------------------------------------------------------------ |
204
+ | `put` | `(key, body: Uint8Array, contentType?)` | `Promise<void>`;小对象写入,上限 25 MiB |
205
+ | `get` | `(key, { firstBytes? }?)` | `Promise<Uint8Array \| null>`;可只读取文件头 |
206
+ | `head` | `(key)` | `Promise<{ size, contentType, versionTag? } \| null>` |
207
+ | `delete` | `(key)` | `Promise<void>` |
208
+ | `uploadPath` | `(key, { maxBytes?, ttlSeconds? }?)` | `Promise<{ path, maxBytes, expiresAt }>`;浏览器单次 PUT 上传 |
209
+ | `signedPath` | `(key, ttlSeconds?)` | `Promise<string>`;短期下载路径 |
210
+ | `signedUrl` | `(key, ttlSeconds?)` | `Promise<string>`;仅宿主知道公开地址时保证绝对 URL,优先用 `signedPath` |
211
+ | `managedPath` | `(key, policy, ttlSeconds?)` | `Promise<string>`;每次新下载实时复核权限,最长 300 秒 |
212
+ | `copy?` | `(sourceKey, destinationKey)` | `Promise<PluginObjectHead>`;当前命名空间内复制 |
213
+ | `getStream?` | `(key)` | `Promise<ReadableStream<Uint8Array> \| null>` |
214
+ | `putStream?` | `(key, body, { sizeBytes, contentType? })` | `Promise<void>`;必须声明准确长度 |
215
+
216
+ `uploadPath`、`signedPath`、`managedPath` 的路径交给前端 `ctx.hostUrl(path)`。大文件优先让浏览器直接上传,完成后用 `head` 和 `get(key, { firstBytes })` 校验大小与文件类型。
217
+
218
+ 受管下载要求声明 `storage.managed` 并实现实时授权回调。policy 包含 `subject: { kind: 'user', userId }` 或 `{ kind: 'public', grantId }`、`resourceType`、`resourceId`、`purpose: 'view' | 'export'`,以及可选 `customerId`。授权回调必须核验 key 与资源归属、当前权限;每次 GET 或 Range 请求开始前复核,已授权开始的传输可完成。声明受管存储后,普通 signed 下载不作为绕过授权的路径。
219
+
220
+ 复制与流式传输要求 `storage.transfer`。SDK 本身不为插件提供跨命名空间读取或总存储用量配额 API。
221
+
222
+ ### 事件与通知
223
+
224
+ | API | 输入 | 输出与约束 |
225
+ | ------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
226
+ | `host.events.publish(topic, payload)` | 名称、JSON payload | `Promise<void>`;宿主加 `plugin.<pluginId>.` 前缀,仅发布,后端没有订阅接口 |
227
+ | `host.notifications.send(input)` | `recipientId`、`eventKey`、`title`、`body`、`path`、可选 `priority` | `Promise<{ id }>`;声明 `declares.notifications: true`;优先级为 `normal` 或 `urgent` |
228
+
229
+ 通知是持久化站内消息,按安装、事件 key 和接收人去重。`path` 使用插件内相对路径,例如 `/records/42`,宿主会添加插件页面前缀。事件用于页面刷新提示,不能代替可靠任务队列或通知。
230
+
231
+ ### 事件触发任务
232
+
233
+ 源码:[server.ts](../../packages/plugin-sdk/src/server.ts) 的 `PluginProactiveEvent`。Host API 1.26.0 起提供,要求 `permissions.required` 包含 `proactive.emit`,并且有后端部分。
234
+
235
+ | API | 输入 | 输出与约束 |
236
+ | ----------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
237
+ | `host.proactive?.emit(input)` | `userId`、`eventType`、`eventId`,可选 `occurredAtMs`、`payload` | `Promise<{ source, runs }>`;`source` 固定为 `plugin:<pluginId>`;每个命中的任务一条 `{ jobId, avatarId, accepted, runId?, reason? }` |
238
+
239
+ 用户以自己的身份用 `tego.cron.add` 建好 `schedule: { kind: 'event', source: 'plugin:<pluginId>', eventType }` 的定时任务后,插件发出同 `eventType` 的事件即可启动它。只匹配该用户、本租户、已启用的任务;没有任务监听时返回空的 `runs`,不算错误。
240
+
241
+ - `eventType` 最长 160 个字符、`eventId` 最长 100 个字符,都不能含空白;`payload` 序列化后最多 16000 个字符,员工看到的是标记为外部不可信内容的事件块
242
+ - 同一 `eventId` 重发返回已排队的那次运行,可放心重试
243
+ - `reason`:`already-running` 表示该任务正在运行,本次没有排队也没有登记,是否稍后用同一 `eventId` 重发由插件决定;`authority-inactive` 表示员工的定时任务仍存放在旧版本地存储;`access-denied` 表示该用户已不能使用这位员工(此时没有 `jobId`)
244
+
245
+ 使用者侧的说明见[任务主动化](../../docs/guides/proactive-and-steering/proactive-tasks.md)。
246
+
247
+ ### 人员与组织目录
248
+
249
+ 源码:[directory.ts](../../packages/plugin-sdk/src/directory.ts)。要求 `permissions.required` 包含 `directory.read`。
250
+
251
+ | API | 输入 | 输出 |
252
+ | -------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------- |
253
+ | `host.directory?.identity(userId)` | 用户 UUID | `{ active, roleNames, memberships, checkedAt }`,成员关系含 `unitId`、`ancestors`、`role` |
254
+ | `host.directory?.list(input)` | `{ kind: 'users' | 'units', search?, ids?, offset?, limit? }` | `{ items, hasMore }`;默认 50,最多 100 条 |
255
+ | `host.directory?.validate(principals)` | 主体数组,最多 200 | `{ invalid: number[] }`,无效主体的输入索引 |
256
+ | `host.directory?.match(input)` | `{ userId, principals }` | `{ active, matched: number[], checkedAt }`,匹配主体的输入索引 |
257
+
258
+ 主体为 `{ kind: 'user', id }` 或 `{ kind: 'unit', id, includeDescendants?, roles? }`。组织支持团队、部门、子公司;目录条目提供显示名、可选全名/账号/邮箱、状态及组织父级信息。所有查询限定当前租户。`roleNames` 用于 access 引擎的宿主角色绑定,不应用来另写插件业务授权逻辑。
259
+
260
+ ### 普通平台账号
261
+
262
+ 源码:[accounts.ts](../../packages/plugin-sdk/src/accounts.ts)。要求 `accounts.provision`,插件须先校验操作者对目标成员的管理权限。
263
+
264
+ | API | 输入 | 输出 |
265
+ | ----------------------------------------------- | ----------------------------------------- | ------------------------------------ |
266
+ | `host.accounts?.inviteUser(input)` | `requestId`、`organizationName`、用户身份 | `AccountInvitation`,发送邀请 |
267
+ | `host.accounts?.provisionUser(input)` | 同邀请输入;新账号增加 `password` | `{ id, userId }`,直接注册,不发邮件 |
268
+ | `host.accounts?.getInvitation(invitationId)` | 邀请 UUID | `AccountInvitation` |
269
+ | `host.accounts?.resendInvitation(invitationId)` | 邀请 UUID | `AccountInvitation` |
270
+ | `host.accounts?.resetPassword(input)` | `{ userId, newPassword }` | `{ userId }` |
271
+
272
+ 已有账号可指定 `userId`;新账号提供 `username`、`email`、`fullName`,可选 `phone`。`requestId` 用于幂等。邀请结果包含 `id`、`userId`、`status`、`deliveryStatus`、`error`、`expiresAt`。密码长 8–128 位,须含大小写字母和数字;平台管理员密码不能从插件重置。账号租户、配额和平台角色由宿主确定,插件不设置平台管理员角色、不保存明文密码。
273
+
274
+ ### 知识引用与插件知识集合
275
+
276
+ 源码:[knowledge.ts](../../packages/plugin-sdk/src/knowledge.ts)。
277
+
278
+ `host.knowledge?.verify({ userId, avatarId, references })` 返回 `{ references: VerifiedPluginKnowledgeReference[] }`。每次最多 20 条,引用包含 `knowledgeBaseId`、`documentId`、`paragraphId`,可选 `quote`、`expectedContentSha256`。返回权威段落内容、文档名、标题、内容哈希、核验时间。要求 `knowledge.read`、当前用户对知识的可读权限,以及该用户对员工有效的 `work.prepare` 委托;它是引用核验接口。
279
+
280
+ `host.knowledge?.collections` 管理当前安装所属的知识集合,要求 `knowledge.collections`:
281
+
282
+ | 方法 | 主要输入 | 输出或行为 |
283
+ | ----------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
284
+ | `ensure` | `{ key, name, accessMode?, policyScope? }` | `{ collectionId, status: 'ready' }`,确保集合存在;不代表文档已完成索引 |
285
+ | `permissions` | `{ key, grants: [{ subjectType, subjectId }] }` | `{ aclVersion }`;读者类型为 user/team/role/tenant |
286
+ | `import` | `{ key, documents: [{ externalId, title, text, metadata? }] }` | `{ accepted, taskId }`;每批最多 50 份文本 |
287
+ | `importFiles` | `{ key, documents: [{ externalId, title, storageKey, fileName, contentSha256, metadata? }] }` | `{ accepted, taskId }`;每批最多 20 个已存储文件 |
288
+ | `status` | `{ key }` | `{ status, documents, pending, failed, updatedAt }`;empty/indexing/partial/ready |
289
+ | `documentsStatus` | `{ key, externalIds }` | `{ documents }`,各文档处理状态、内容哈希及元数据 |
290
+ | `documentContent` | `{ key, externalId, page?, pageSize? }` | `{ paragraphs, total, page, pageSize }`;按阅读顺序,最多每页 100 段 |
291
+ | `search` | `{ key, query, topK?, filter? }` | `{ hits }`;包含 externalId、文档/段落 ID、分数、snippet 和元数据,topK 最多 50 |
292
+ | `remove` | `{ key, externalIds }` | `{ removed }`,删除指定文档 |
293
+ | `delete` | `{ key }` | `{ deleted }`,永久退役集合 key 及知识库 |
294
+
295
+ `documentsStatus({key, offset})` 不提供 externalIds 时分页列出本安装映射的全部文档(每页 200,返回 hasMore);Python 对应 `list_documents(key, offset=...)`。投影必须覆盖完整映射:不属于当前可读发布版本的文档使用空 override,不能只更新当前 desired_documents 的子集。
296
+
297
+ 集合 key 为 1–64 位小写字母、数字、下划线或连字符,并以字母或数字开头。导入以 `externalId` 标识文档,任务异步完成;读取尚未索引完成或失败文档的正文会返回 409。元数据支持最多 32 个标量键,搜索 filter 按键做等值匹配,数组值表示匹配其中任一项。
298
+
299
+ `accessMode: 'managed'` 要求额外声明 `knowledge.managed`。宿主 API 1.29 起,插件在写入路径通过原 SDK 权限引擎计算读者,把结果写入现有 ACL;页面、工具、正文和知识附件读取只检查同一份知识库 ACL,不回调插件。`policyScope` 为弃用兼容字段,可省略,没有运行时用途。
300
+
301
+ 新增 `access({ key?, enabled?, identityRevision?, syncRevision? })`:无 key 表示本安装全部 managed 集合。禁读返回 `{ enabled, identityRevision, syncRevision, validUntil }`;恢复读取必须携带身份和同步批次两个版本。宿主身份变更或角色到期使旧版本失效,恢复返回 409,插件应重新计算。`validUntil` 非空时宿主在现有知识库可读状态检查中限制快照有效期;到期后暂不可读,直到重新投影,原 ACL 授权格式不变。新 managed 集合默认 disabled;导入和版本替换前必须禁读。`permissions`、`documentPermissions`、`import` 和 `importFiles` 写入 managed 集合必须携带禁读返回的 `syncRevision`;过期批次返回 409。宿主按安装串行处理这些写入及禁读操作,返回失败前排空在途请求,防止旧任务在新任务之后写回。
302
+
303
+ 新增 `documentPermissions({ key, syncRevision, documents: [{ externalId, aclMode, aclVersion?, grants }] })`:每批 1–200 个唯一 externalId,仅能修改本安装资源;内部调用现有 ACL 更新服务,跨租户主体拒绝,显式 aclVersion 保留 409 冲突。`inherit` 清空文档授权,`override` 写入 use 读者;未就绪、已撤回或版本不匹配的文档应设空 override。
304
+
305
+ `permissions` 和 `documentPermissions` 的 grants 最多 999 个主体。managed 投影拒绝 tenant 全员候选授权,插件角色 ID 不能作为宿主 role ID;库级所有者 manage 授权保留。先禁读,再收权和投影,完成核验后恢复读取。业务变更提交后的扩权失败必须返回待同步状态并持久化重试。切换步骤见[维护窗口说明](../operations/knowledge-acl-cutover.md)。
306
+
307
+ ### 来源与工作委托
308
+
309
+ | API | 输入 | 返回值与约束 |
310
+ | -------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------- |
311
+ | `host.sources?.read(id, actor)` | `actor: { userId, avatarId, metadataOnly? }` | `{ source: unknown }`;宿主复核来源归属、源数据是否存在、捕获授权 |
312
+ | `host.delegations?.list(userId)` | 用户 ID | `{ delegations: [{ avatarId, enabled, actions }] }`;当前安装的工作委托,用于转交工作前复核 |
313
+
314
+ `sources.read` 读取平台已授权捕获的来源,不是任意会话查询接口。`delegations.list` 只查询,不能创建或扩张委托;来源捕获、会话绑定和委托管理的其他平台 REST 接口不在 `PluginHostApi` 方法列表中。
315
+
316
+ ### 图表渲染
317
+
318
+ 源码:[diagrams.ts](../../packages/plugin-sdk/src/diagrams.ts)。要求 `diagrams.render`。
319
+
320
+ `host.diagrams?.status()` 返回 `{ ready, reason? }`。`render({ kind, source, destinationKey, width?, height? })` 支持 `kind: 'mermaid' | 'html'`,输出 `{ key, width, height, size, sha256, renderer }`,PNG 写入当前安装的存储命名空间。默认画布上限 1200×800;宽允许 320–2048,高允许 240–2048,底部多余空白会裁去。
321
+
322
+ 只接收源码文本,不接收服务器路径或远程页面 URL。HTML 为受限静态结构和样式,Mermaid 不允许配置覆盖与链接;渲染时关闭外网。可用于流程图和静态业务图示。
323
+
324
+ ## 插件业务权限 API
325
+
326
+ 源码:[access/index.ts](../../packages/plugin-sdk/src/access/index.ts),设计:[plugin-access.md](../../docs/design/plugin-access.md)。使用时声明 `access: { catalog: '<目录路径>' }`、`hostApiRange: '^1.22.0'` 和 `directory.read`,权限表通过插件自己的迁移创建。
327
+
328
+ ### 权限目录与请求会话
329
+
330
+ | API | 用途 |
331
+ | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
332
+ | `defineAccess(input)` | TypeScript 中定义带类型推断的权限目录 |
333
+ | `accessCatalogFromJson(json)` | 返回已验证的带类型权限目录;无效时抛错 |
334
+ | `validateAccessCatalog(input)` | 返回 `{ ok: true, catalog }` 或 `{ ok: false, issues }` |
335
+ | `createAccess({ catalog, evaluators?, principals?, domains?, hooks?, cacheSize? })` | 创建引擎;内置 user/unit 主体和 all/own 范围,支持业务范围及域扩展 |
336
+ | `access.reconcile(host)` | 启动时同步内置角色与目录;内置角色由目录管理 |
337
+ | `access.forRequest(ctx, host)` | 为当前已签名请求创建 `AccessSession` |
338
+ | `access.subjectFor(userId, host)` | 读取当前有效身份;无效用户返回 null |
339
+ | `access.forSubject(subject, host)` | 为已解析主体创建会话,例如受管下载授权 |
340
+ | `session.can(permission, options?)` | 是否具有功能权限 |
341
+ | `session.holdsAnywhere(permission)` | 是否在全局或任一已分配业务域拥有权限 |
342
+ | `session.scope(permission, options?)` | 当前范围或 null |
343
+ | `session.authorize(permission, resource?, options?)` | 返回 `Decision`,同时判断资源范围 |
344
+ | `session.assert(permission, resource?, options?)` | 拒绝时抛 `AccessDeniedError`;可用 conceal 隐藏范围外资源 |
345
+ | `session.filter(permission, ref, options?)` | 生成列表 SQL 条件,与单条资源判定使用同一求值器 |
346
+ | `session.grants(domain?)`、`snapshot(options?)`、`manages(options?)` | 有效授权、UI 权限快照、是否持有 access.manage |
347
+
348
+ 内置 `own` 使用资源的 `ownerId`;数据库列名不同时通过 `ScopeRef` 映射,或提供自定义 `ScopeEvaluator`。SQL 辅助包括 `sql`、`ident`、`renderFragment`、`bindFragment`、`orFragments`、`TRUE_FRAGMENT`、`FALSE_FRAGMENT`,用于把范围条件安全合并到参数化 SQL。
349
+
350
+ 目录声明权限、范围、内置角色、菜单映射、默认角色、宿主角色绑定及员工允许的委托动作。员工仅能使用目录显式允许的权限,且受当前用户权限与签名委托共同约束;不能为了员工放宽正式业务权限。角色变更产生的派生数据放在 `hooks.onChange(tx, change, context)`,与权限变更在同一事务提交。
351
+
352
+ ### Hono 路由策略与管理 API
353
+
354
+ `accessHono({ session })` 返回 `policy`、`routes`、`session`、`resourceOf`、`decisionOf`:
355
+
356
+ - `policy.public()`:仅 manifest 声明的公开路由。
357
+ - `policy.authenticated()`:已签名请求,业务授权由 handler 完成。
358
+ - `policy.permission(permission, options?)`:功能权限。
359
+ - `policy.resource(permission, loader, options?)`:功能权限与加载到的资源范围。
360
+ - `routes()`:下表的标准 `/access` 管理路由。
361
+ - `assertRouteCoverage(app, { publicRoutes? })`:检查每条路由恰好有一个策略。
362
+ - `policyOf(handler)`:读取策略描述,供检查和适配使用。
363
+
364
+ | 标准路由 | 用途 |
365
+ | ----------------------------------------------------- | ---------------------------- |
366
+ | `GET /access/me` | 当前用户权限、菜单和角色快照 |
367
+ | `GET /access/catalog` | 权限目录 |
368
+ | `GET /access/roles`、`POST /access/roles` | 查询、创建自定义角色 |
369
+ | `PUT /access/roles/:id`、`DELETE /access/roles/:id` | 修改、删除自定义角色 |
370
+ | `POST /access/roles/:id/clone` | 克隆角色 |
371
+ | `GET /access/assignments`、`POST /access/assignments` | 查询、创建主体角色分配 |
372
+ | `DELETE /access/assignments/:id` | 删除分配 |
373
+ | `GET /access/principals` | 查询可分配主体 |
374
+ | `GET /access/explain/:userId` | 解释用户授权来源 |
375
+ | `GET /access/settings`、`PUT /access/settings` | 权限设置 |
376
+ | `GET /access/audit` | 权限变更审计 |
377
+
378
+ 除 `/access/me` 外,路由携带管理策略,由 `AccessAdmin` 核验实际权限;角色和分配等管理操作要求 `access.manage`,审计查询要求 `access.audit`。`AccessAdmin` 提供相应 `catalogView`、`listRoles/createRole/updateRole/deleteRole/cloneRole`、`listAssignments/createAssignment/deleteAssignment`、`principals`、`explain`、`settings/updateSettings`、`audit` 方法。内置角色只读,自定义角色的授权受管理员自身可授范围限制。
379
+
380
+ `access/drizzle` 导出 `tegoAccessRoles`、`tegoAccessAssignments`、`tegoAccessSettings`、`tegoAccessAudit`。TypeScript 插件重新导出这些定义并生成迁移;Python 使用 SDK SQL 资源接入自身迁移。权限语义变更须提升目录 version。
381
+
382
+ ### 前端权限组件
383
+
384
+ 配套包 `@tego/plugin-access-ui` 提供 `AccessProvider`、`useAccess`、`Can`、`filterNav`、`AccessSettings` 和 `useAccessClient` 等接口。它随插件打包,在插件自己的 Query provider 内挂载 `AccessProvider`,用于菜单、按钮和设置页;前端显示控制不能代替后端路由策略。
385
+
386
+ ## Manifest 与通用辅助 API
387
+
388
+ 下列 API 从根入口导出,除另有标注。完整类型签名见 [index.ts](../../packages/plugin-sdk/src/index.ts)。
389
+
390
+ | 分类 | API |
391
+ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
392
+ | 版本 | `HOST_API_VERSION`、`SHARED_MODULE_IDS`、`isValidHostApiRange`、`satisfiesHostApiRange` |
393
+ | 插件部分 | `declaredPartNames`、`resolvePluginParts`、`resolvePluginScope`、`resolvePluginDeclares`;manifest 文件名、部分默认值、运行时/平台/导航图标常量 |
394
+ | 页面和 API 路径 | `pluginBasePath`、`pluginNavPath`、`pluginApiPath` |
395
+ | 菜单和语言 | `pluginMenuKey`、`pluginMenuPermission`、`pluginI18nNamespace`、`pluginI18nKey`、`localizedTextToString`、`PLUGIN_SUPPORTED_LOCALES`、`MAX_PLUGIN_MENU_PERMISSION_LENGTH` |
396
+ | 公开路由 | `formatPublicRoute`、`matchPublicRoute`、`publicApiBaseFromPagePath`、公开页面/API 路径常量、`PLUGIN_PUBLIC_ROUTE_METHODS` |
397
+ | 配置 | `applyConfigDefaults`、`configFieldLabel`、`redactSecrets`、`restoreSecrets`、`summarizeClawConfig`、`summarizeConfigDeclaration`、配置分区/秘密占位符/空摘要常量 |
398
+ | 安装披露 | `buildInstallDisclosure`、`diffInstallDisclosure`、`requiresBackendConfig`、`auditDeclarationGaps` |
399
+ | Manifest 校验,来自 `/manifest` | `tegoPluginManifestSchema`、`parseTegoPluginManifest`、`checkPluginAccessCatalog` |
400
+ | Python 依赖检查,来自 `/manifest` | `checkPythonVendoring`、`normalizePackageName`、`parseRequirementsLock`、`parseWheelFilename`、Python lock/wheel/runner 常量 |
401
+
402
+ 根入口同时导出 `TegoPluginManifest`、插件各部分、配置、安装披露、发现结果等类型。`parseTegoPluginManifest(value)` 返回 `{ ok, manifest?, errors }`;仅接受精确 `X.Y.Z` 或 `^X.Y.Z` 的 Host API 范围。配置是扁平字段 schema,前端配置不能标记敏感字段。
403
+
404
+ ### 能力声明与业务授权的区别
405
+
406
+ | 声明位置 | 典型字段 | 作用 |
407
+ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
408
+ | `declares` | database/storage/events/notifications | 向安装者披露使用的宿主服务;database 决定是否建库,notifications 有宿主调用检查 |
409
+ | `permissions.required` | directory.read、accounts.provision、knowledge.read、knowledge.collections、knowledge.managed、storage.managed、storage.transfer、diagrams.render、proactive.emit | 需要宿主能力检查的安装声明 |
410
+ | `access.catalog` | 业务权限、角色、范围、菜单、agent.requires | 插件内部谁可以对哪些记录执行哪些操作 |
411
+ | `parts.backend.publicRoutes` | 方法与路径 | 允许匿名请求到达指定公开路由 |
412
+ | `parts.backend.egress` | 对外目标 | 出站网络声明,通过宿主代理访问 |
413
+
414
+ ### 工具的审计说明
415
+
416
+ `audit.tools` 按工具名(claw 部分 `contracts.tools` 里的名字)声明该工具在审计记录里怎么显示。没有声明时,审计员只能看到工具名和原始参数;有声明时看到的是业务语言,例如「查询客户 C123 的持仓」。
417
+
418
+ ```json
419
+ "audit": {
420
+ "tools": {
421
+ "crm_query_customer": {
422
+ "category": "read",
423
+ "title": { "zh-CN": "查询客户 {customerId} 的持仓", "en": "Look up holdings of {customerId}" },
424
+ "target": "customerId",
425
+ "risk": "medium",
426
+ "dataClasses": ["pii", "account"]
427
+ }
428
+ }
429
+ }
430
+ ```
431
+
432
+ - `title` 里的 `{参数名}` 用调用参数填充,参数已在容器内脱敏
433
+ - `target` 指向标识业务对象的参数,用于审计检索
434
+ - `risk: "high"` 让每次调用都带风险标记;`dataClasses` 含 `pii`、`account`、`trade` 时标记为涉及敏感数据
435
+ - 上传时会拒绝声明了 claw 未注册工具的条目(多半是工具改名后忘了同步);未声明的工具在安装确认页标为「缺少审计说明」
436
+
437
+ `declares` 不能一概当作运行时安全隔离;前端也不是第三方代码沙箱。业务授权必须在后端使用 access SDK,不能只靠 `ctx.can`、安装声明或界面隐藏。
438
+
439
+ ## 构建 发布与测试 API
440
+
441
+ | 入口 | API | 用途 |
442
+ | ---------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
443
+ | `/vite` | `tegoPluginFrontend({ entry, pluginId?, outDir?, development? })` | 生成宿主可加载的 SystemJS 前端配置,处理共享依赖及样式 |
444
+ | `/pack` | `packPlugin({ root, outDir?, fileName?, frontend?, publicFrontend?, backend?, claw?, log? })` | 构建并输出统一 ZIP,返回 `PackedPlugin` |
445
+ | `/pack` | `buildPluginBackend(part, { root, outDir, entry?, external? })` | part 为解析后的 ResolvedBackendPart,构建 Node 后端 bundle |
446
+ | `/pack` | `buildAgentTemplate(dir)`、`buildPluginTemplates({ root, log? })` | 编译岗位目录;后者写入已声明模板的 template.json |
447
+ | `/pack` | `lockPlugin({ root, python? })` | 从 requirements.in 生成 Python requirements.lock |
448
+ | `/pack` | `watchPluginFrontend(options)` | 前端重建与开发服务,页面重新挂载,不保留页面状态 |
449
+ | `/publish` | `publishPlugin(options)` | 打包后上传、检查、提交 release;默认审批并安装 |
450
+ | `/publish` | `publishPackedPlugin(packed, options)` | 发布已打包文件 |
451
+ | `/testing` | `createTestPluginHost(options)` | 临时真实数据库 schema、迁移、事件/错误记录器和内存存储 |
452
+
453
+ 发布 options 包含 `root`(已打包发布除外)、`server`、管理员 `token`,可选 `avatarIds`、`meta`、`approve`、`install`、`log`、`fetchImpl`。`approve` 与 `install` 默认 true;这两个函数会改变目标环境,使用前明确发布意图。
454
+
455
+ 测试 options 为 `pluginId`、`migrationsDir`(绝对路径),可选 `databaseUrl`、`label`、`config`、`tenantId`。返回 harness 的 `host` 可交给后端 `fetch`;`reset()` 清表与记录、`close()` 释放 schema,另有 `failPublish()`、`setConfig()`、`publishedEvents`、`loggedErrors`、`storedObjects`。测试宿主要求真实 PostgreSQL,不覆盖全部远程可选能力;测试目录/知识等服务时按实际需求补充替身。
456
+
457
+ 命令行入口:
458
+
459
+ ```bash
460
+ # 在插件包根目录执行
461
+ pnpm exec tego-plugin-pack
462
+ pnpm exec tego-plugin-pack --watch
463
+ pnpm exec tego-plugin-pack templates
464
+ pnpm exec tego-plugin-pack lock
465
+
466
+ # 发布会默认审批并安装;只留草稿可加 --no-approve
467
+ pnpm exec tego-plugin publish --server "$TEGO_SERVER_URL" --token "$TEGO_ADMIN_TOKEN"
468
+ ```
469
+
470
+ 在本仓的创建、调试与门禁流程见 [plugins README](../README.md)。
471
+
472
+ ## 签名与出站网络 API
473
+
474
+ 这些入口用于协议适配或特殊驱动接入,普通后端优先使用标准 runner。
475
+
476
+ | 入口 | API | 用途 |
477
+ | -------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- |
478
+ | `/gateway-signature` | `signPluginContext`、`verifyPluginContext` | 已认证请求上下文签名和验签 |
479
+ | `/gateway-signature` | `signPluginPublicContext`、`verifyPluginPublicContext` | 公开请求上下文签名和验签 |
480
+ | `/gateway-signature` | `signPluginStoragePolicy`、`verifyPluginStoragePolicy` | 受管下载策略及正文哈希验签 |
481
+ | `/gateway-signature` | `derivePluginGatewayKey` | 安装网关密钥派生;不要在插件中自行签发调用者身份 |
482
+ | `/mcp-identity` | `sha256Hex`、`signMcpIdentity`、`buildMcpIdentityHeaders`、`verifyMcpIdentity` | MCP 可信身份传递与核验 |
483
+ | `/egress` | `connectTcp(host, port)` | 经已配置代理连接 manifest 声明的 TCP 目标,返回 Socket |
484
+ | `/egress` | `startTcpTunnel(host, port)` | 为不接收 Socket 的驱动创建 loopback 桥,返回 Server;结束后关闭 |
485
+
486
+ 两个签名入口还导出相应上下文类型、请求头与版本常量。签名有时效检查,不能把普通请求头或前端选择的员工 ID 当可信身份。出站网络能力须在 manifest 声明并由运行环境提供代理。
487
+
488
+ ## Python SDK 对照
489
+
490
+ 源码:[Python SDK](../../packages/plugin-sdk-python/src/tego_plugin_sdk/),要求 Python 3.12 及以上。Host API 方法当前为同步客户端,字典请求体仍使用宿主协议字段名,例如 `userId`、`externalId`,不要把所有 JSON key 自动转成 snake_case。
491
+
492
+ | TypeScript | Python | 说明 |
493
+ | ------------------------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
494
+ | `definePluginBackend`、`serveBackend` | `TegoPlugin(app, public_app?, on_start?, on_stop?)` 和 SDK runner | 导出名为 plugin 的对象;用户与公开 app 分开 |
495
+ | 请求参数 `ctx` | `user_context(request)`、`public_context(request)` | 已验签上下文;属性使用 user_id/tenant_id 等 Python 命名 |
496
+ | `host.pluginId`、`host.tenantId` | `host().plugin_id`、`host().tenant_id` | Python 额外公开 install_id |
497
+ | `host.db.query` | `host().db.query(sql, params)` | Python SQL 使用 psycopg 的 `%s` 等参数格式 |
498
+ | `host.db.transaction(body)` | `with host().db.transaction() as tx:` | Python 使用上下文管理器 |
499
+ | `config.current/refresh` | `host().config.current/refresh` | 返回配置字典 |
500
+ | `storage.put/get/head/delete` | `host().storage.put/get/head/delete` | put 使用 bytes;get 的 first_bytes 为独立可选参数 |
501
+ | `storage.signedUrl/signedPath/managedPath/uploadPath` | `signed_url/signed_path/managed_path/upload_path` | host().storage 下;managed_path 的 policy 字典使用协议字段名 |
502
+ | `storage.copy/getStream/putStream` | 当前未封装 | Python Storage 没有这些方法 |
503
+ | `events.publish`、`notifications.send` | `host().events.publish`、`host().notifications.send` | notifications.send 使用 Python 关键字参数,直接返回通知 ID 字符串 |
504
+ | `proactive.emit` | `host().proactive.emit` | 关键字参数 user_id、event_type、event_id,可选 occurred_at_ms、payload |
505
+ | `log.debug/info/warn/error` | `host().log.debug/info/warning/error` | Python 日志字段使用关键字参数 |
506
+ | `directory.identity/validate/list/match` | `host().directory.identity/validate/list/match` | match 接收 user_id 与 principals 两个参数 |
507
+ | `accounts.provisionUser/inviteUser/resetPassword` | `host().accounts.provision_user/invite_user/reset_password` | reset_password 接收 user_id、new_password 两个参数 |
508
+ | `accounts.getInvitation/resendInvitation` | `get_invitation/resend_invitation` | host().accounts 下 |
509
+ | `knowledge.verify` | `host().knowledge.verify(body)` | 核验引用 |
510
+ | `knowledge.collections` | **`host().collections`** | Python 的集合入口在 HostApi 顶层 |
511
+ | `collections.import/importFiles` | `import_documents/import_files` | Python 以 key、documents 分开传入 |
512
+ | `collections.documentsStatus/documentContent` | `documents_status/document_content` | Python documents_status 返回列表,document_content 返回分页字典 |
513
+ | `collections.ensure/permissions/status/search/remove/delete` | 同名方法 | search 返回 hits 列表,remove 返回数量,delete 返回布尔值 |
514
+ | `sources.read`、`delegations.list` | `host().sources.read`、`host().delegations.list` | sources.read 的 user_id/avatar_id 为关键字参数 |
515
+ | `diagrams.status/render` | 当前未封装 | Python HostApi 没有 diagrams 属性 |
516
+ | access 引擎 | `tego_plugin_sdk.access` | AccessEngine、AccessSession、AccessAdmin、目录、主体、范围与 SQL 辅助 |
517
+ | Hono 策略与覆盖检查 | `tego_plugin_sdk.access.fastapi` | AccessFastApi、session_per_request、assert_route_coverage 等 |
518
+ | TCP 与 HTTP 出站 | `tego_plugin_sdk.egress` | connect_tcp、tcp_tunnel、client、check、declared_hosts、EgressNotDeclared |
519
+
520
+ Python FastAPI 的路由以 `dependencies=[policy.permission(...)]` 等方式挂策略,`AccessFastApi.router()` 提供同一组 `/access` 路由。受管下载授权路由由插件在自身 app 中实现,SDK 先验签并把上下文放到 `request.scope['tego.storage_policy']`,没有 TypeScript 的 `authorizeStorage` 模块字段。
521
+
522
+ ## Host API 兼容版本
523
+
524
+ 以下是常用能力的版本节点。新插件采用权限目录时直接声明 `^1.22.0`;兼容旧宿主时,声明所需最低版本并检测可选成员。
525
+
526
+ | Host API | 常用新增能力 |
527
+ | -------- | -------------------------------------------------- |
528
+ | 1.1.0 | ChatSurface、话题创建 metadata |
529
+ | 1.2.0 | 前端路由 store、用户显示名 |
530
+ | 1.3.0 | 前端插件事件流 |
531
+ | 1.4.0 | 插件配置 |
532
+ | 1.5.0 | directory |
533
+ | 1.6.0 | knowledge 引用核验 |
534
+ | 1.7.0 | 权威来源段落引用 |
535
+ | 1.9.0 | 通用 AI 表单填写 |
536
+ | 1.10.0 | Python 后端、出站声明、公开路由/页面、插件知识集合 |
537
+ | 1.12.0 | 大对象上传路径、文件头读取、浏览器 hostUrl |
538
+ | 1.14.0 | 前端侧栏收起请求 |
539
+ | 1.15.0 | 受管下载 |
540
+ | 1.17.0 | 直接注册普通用户 |
541
+ | 1.18.0 | 普通账号密码重置 |
542
+ | 1.19.0 | 存储复制与流式传输 |
543
+ | 1.20.0 | Mermaid 与 HTML 图表渲染 |
544
+ | 1.21.0 | 前端 Office 产物打开 |
545
+ | 1.22.0 | 插件权限目录、access SDK、后端 tenantId |
546
+ | 1.26.0 | `proactive.emit` 事件触发任务 |
547
+ | 1.27.0 | AI 草稿可配置执行预算 `timeoutMs` |
548
+ | 1.37.0 | 前端 `platform`、`openExternal`、`download`、`copyText`(web 与桌面端行为一致) |
549
+
550
+ ## 能力边界与相关文档
551
+
552
+ - `PluginHostApi` 没有直接调用数字员工工具或启动任意员工任务的接口,也没有通用平台 REST 客户端。`proactive.emit` 只启动用户自己建好、监听本插件事件的定时任务。
553
+ - Claw 工具注册使用 OpenClaw 自身插件 SDK;统一 ZIP 的 Claw 部分由本 SDK 打包,运行时 API 不等同于后端 Host API。
554
+ - `agentTemplates`、`workDelegation`、`conversationRecordRoutes` 是 manifest 协作声明,不是同名的 Host API 对象。员工模板更新由管理员查看差异后确认。
555
+ - 知识搜索、Office、账号和存储各有自己的业务授权条件;拥有安装凭证不等于拥有用户业务权限。
556
+
557
+ 维护时以 [TypeScript exports](../../packages/plugin-sdk/package.json)、[前端契约](../../packages/plugin-sdk/src/frontend.ts)、[后端契约](../../packages/plugin-sdk/src/server.ts)、[Node 客户端](../../packages/plugin-sdk/src/server-runtime.ts)、[Python 客户端](../../packages/plugin-sdk-python/src/tego_plugin_sdk/host_api.py) 为准。更完整的配置和打包说明见 [SDK README](../../packages/plugin-sdk/README.md),宿主行为见[插件平台架构](../../docs/architecture/plugin-platform.md)与[后端协议](../../docs/architecture/plugin-backend-protocol.md)。
558
+
559
+ `collections.access({})`(Python:`collections.access()`)只读检查宿主身份版本,不禁读、不生成写入 token;返回的空 `syncRevision` 不能用于 ACL 写入。后台无源变更且身份快照相同时跳过投影,查询链不会调用此接口。
560
+
561
+ 知识库 ACL 投影接口(集合读取暂停、身份版本及按 externalId 批量移动文档)要求 Host API `^1.36.0`;`policyScope` 仅保留为弃用兼容字段。
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var r=Object.defineProperty;var c=(e,t)=>r(e,"name",{value:t,configurable:!0});import{buildPluginTemplates as s,lockPlugin as l,packPlugin as a,watchPluginFrontend as d}from"../dist/pack.js";const o=process.argv.slice(2);function n(e){const t=o.indexOf(`--${e}`);return t===-1?void 0:o[t+1]}c(n,"flag");const i=n("root")??process.cwd();try{if(o[0]==="lock"){const e=await l({root:i});console.log(`\u2705 ${e}`)}else if(o[0]==="templates")(await s({root:i})).length===0&&console.log("\u2705 templates up to date");else if(o.includes("--watch")){const e=n("port");await d({root:i,...e?{port:Number(e)}:{}})}else{const e=n("python-include");await a({root:i,outDir:n("out-dir"),fileName:n("file-name"),...e?{backend:{python:{include:e.split(",")}}}:{}})}}catch(e){console.error(`\u2716 ${e instanceof Error?e.message:String(e)}`),process.exitCode=1}
2
+ var r=Object.defineProperty;var i=(e,t)=>r(e,"name",{value:t,configurable:!0});import{buildPluginTemplates as s,lockPlugin as l,packPlugin as a,watchPluginFrontend as d}from"../dist/pack.js";const o=process.argv.slice(2);function n(e){const t=o.indexOf(`--${e}`);return t===-1?void 0:o[t+1]}i(n,"flag");const c=n("root")??process.cwd();try{if(o[0]==="lock"){const e=await l({root:c});console.log(`\u2705 ${e}`)}else if(o[0]==="templates")(await s({root:c})).length===0&&console.log("\u2705 templates up to date");else if(o.includes("--watch")){const e=n("port");await d({root:c,...e?{port:Number(e)}:{}})}else{const e=n("python-include");await a({root:c,outDir:n("out-dir"),fileName:n("file-name"),doctor:!o.includes("--no-doctor"),...e?{backend:{python:{include:e.split(",")}}}:{}})}}catch(e){console.error(`\u2716 ${e instanceof Error?e.message:String(e)}`),process.exitCode=1}