@77-zhike/app-sdk 0.2.0-d7b1.15 → 0.2.0-d7b1.17
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.
- package/README.md +10 -0
- package/package.json +2 -2
- package/src/client/documents.d.ts +21 -0
- package/src/client/documents.js +39 -0
- package/src/client/index.d.ts +16 -0
- package/src/client/index.js +10 -0
- package/src/client/interactions.d.ts +1 -1
- package/src/client/interactions.js +2 -0
- package/src/client/points.generated.js +8 -0
- package/src/forms/index.d.ts +2 -0
- package/src/server/agents/index.d.ts +37 -0
- package/src/server/agents/index.js +12 -2
- package/src/server/index.d.ts +3 -1
- package/src/server/index.js +1 -1
- package/src/toolchain/agents/discovery.js +30 -3
- package/src/toolchain/agents/exposure.js +31 -0
- package/src/toolchain/agents/manifest.js +25 -6
- package/src/toolchain/ai-capabilities.js +69 -0
- package/src/toolchain/real-build.js +11 -6
- package/src/toolchain/source-discovery.js +22 -2
- package/src/ui/components.generated.js +3 -3
- package/src/ui/conversation.d.ts +1 -1
- package/src/ui/data-table-model.d.ts +14 -0
- package/src/ui/data-table-model.js +98 -0
- package/src/ui/data-table.d.ts +70 -0
- package/src/ui/data-table.js +74 -0
- package/src/ui/factory.js +11 -0
- package/src/ui/index.d.ts +25 -1
- package/src/ui/index.js +1 -1
- package/src/ui/native-dom.js +3 -3
- package/src/ui/slider.d.ts +27 -0
- package/src/ui/slider.js +26 -0
- package/src/ui/text-selection.d.ts +12 -0
- package/src/ui/text-selection.js +14 -0
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# 智客 App SDK 与 CLI
|
|
2
2
|
|
|
3
|
+
## 正文批注与待发同步
|
|
4
|
+
|
|
5
|
+
`TextSelection` 可接收有界 `marks`(id、UTF-16区间、逐字text、1至20的ordinal)与 `onMarkActivate`;Host默认保留编号,通过当前view锚点打开Popover时才高亮对应原文,关闭后清除高亮,点击编号可重新编辑;不向App交付DOM或坐标。通过 `useConversation().state.references ?? []` 派生标记,与Composer待发引用保持同源。
|
|
6
|
+
|
|
7
|
+
annotation的comment允许空字符串,支持“先加入对话、再编辑评论”。保存更新同id,取消保留已加入引用,删除通过removeReference同步移除。仅引用原文而无评论时需在主输入说明要求;至少一条有评论才支持缺省正文发送。`form.TextArea`新增可选labelHidden,隐藏视觉标题但保留可访问名称。使用这些增量前须同步升级SDK、Runtime与双端Host,不能单独向旧宿主发布App。
|
|
8
|
+
|
|
3
9
|
## 操作系统与 Windows 候选
|
|
4
10
|
|
|
5
11
|
当前 npm 发布版的 Developer CLI 仅提供 macOS Keychain 适配。本开发分支新增 Windows Credential Manager 候选:复用现有浏览器 PKCE 和 Session,使用系统 Windows PowerShell 5.1 固定桥接,refresh credential 按当前用户和 API origin 存储。
|
|
@@ -269,3 +275,7 @@ Chart 仅接受纯数据 `labels` 与 `series`,不接受脚本、HTML 或任
|
|
|
269
275
|
附件只读当前 App 会话中用户提交的文件,不扩展自定义 Tool 的权限。新 App 业务 Tool 仍在 agent 目录用 `.tools.ts` 合并定义并由 Integration Runtime 执行。元数据 Schema 必须为静态字面量,execute 可导入项目自己的业务校验函数。
|
|
270
276
|
|
|
271
277
|
真实表单协作可参考 `built-in-apps/opportunity-quotes`:页面以 SDK 布局并列 Form 与 Conversation,callback 接收建议,用户确认后更新本地草稿;正式保存沿原入口。NativeDOM 内仍只允许原有控件,不直接放 Conversation;可以在 Conversation 内用独立 NativeDOM 展示本地建议确认区。
|
|
278
|
+
|
|
279
|
+
### Slider(源码增量,需匹配 Host)
|
|
280
|
+
|
|
281
|
+
`@77-zhike/app-sdk/ui` 提供单值/双端 `Slider`,支持整段拖动、step、minRange、键盘与最终commit。App只消费数值;Host合并在途变化并保留手势草稿。需先发布匹配的SDK和桌面/手机Host,再发布消费App。完整属性与行为见 Developer Docs `app-sdk/components/slider`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@77-zhike/app-sdk",
|
|
3
|
-
"version": "0.2.0-d7b1.
|
|
3
|
+
"version": "0.2.0-d7b1.17",
|
|
4
4
|
"description": "智客 Integration App 一期 SDK 与本地 Toolchain",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"optional": true
|
|
68
68
|
}
|
|
69
69
|
},
|
|
70
|
-
"gitHead": "
|
|
70
|
+
"gitHead": "df9d8e305f4b9d603b33d41840847ee3d28a654b"
|
|
71
71
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JsonValue } from '../ui/index.js';
|
|
2
|
+
/** 受限JSON Schema;服务端校验完整白名单、数组/字符串上限与绑定。 */
|
|
3
|
+
export type RestrictedInputSchema = Readonly<Record<string, JsonValue>>;
|
|
4
|
+
export interface PreparePdfTemplateInput {
|
|
5
|
+
inputSchema: RestrictedInputSchema;
|
|
6
|
+
/** 本次制作的有界、已授权试填数据,不保存到App模板配置。 */
|
|
7
|
+
sampleData: Readonly<Record<string, JsonValue>>;
|
|
8
|
+
}
|
|
9
|
+
export interface OpenPdfDocumentInput {
|
|
10
|
+
/** opaque Job用途引用;不能传URL、附件ID或PDF字节。 */
|
|
11
|
+
documentRef: string;
|
|
12
|
+
action: 'open' | 'download';
|
|
13
|
+
}
|
|
14
|
+
export type PreparedPdfTemplate = Readonly<{templateFileRef: string}>;
|
|
15
|
+
export type OpenPdfDocumentResult = Readonly<{status: 'opened' | 'started' | 'cancelled'}>;
|
|
16
|
+
export function assertDocumentInput(operation: string, input: unknown): PreparePdfTemplateInput | OpenPdfDocumentInput;
|
|
17
|
+
export function assertDocumentResult(operation: string, value: unknown): PreparedPdfTemplate | OpenPdfDocumentResult | null;
|
|
18
|
+
export function createDocumentClient(invoke: (operation: string, input: unknown) => Promise<unknown>): {
|
|
19
|
+
preparePdfTemplate(input: PreparePdfTemplateInput): Promise<PreparedPdfTemplate | null>;
|
|
20
|
+
openPdfDocument(input: OpenPdfDocumentInput): Promise<OpenPdfDocumentResult>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const invalid = () => { throw new Error('runtime_document_input_invalid'); };
|
|
2
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
3
|
+
const ref = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/.test(value);
|
|
4
|
+
function bounded(value, depth = 0) {
|
|
5
|
+
if (depth > 12) invalid();
|
|
6
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
|
|
7
|
+
if (typeof value === 'number' && Number.isFinite(value)) return;
|
|
8
|
+
if (Array.isArray(value)) { if (value.length > 1000) invalid(); for (const item of value) bounded(item, depth + 1); return; }
|
|
9
|
+
if (!object(value)) invalid();
|
|
10
|
+
for (const [key, item] of Object.entries(value)) {
|
|
11
|
+
if (['__proto__', 'prototype', 'constructor'].includes(key)) invalid();
|
|
12
|
+
bounded(item, depth + 1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function assertDocumentInput(operation, input) {
|
|
16
|
+
if (!object(input)) invalid();
|
|
17
|
+
if (operation === 'openPdfDocument') {
|
|
18
|
+
if (Object.keys(input).some(key => !['documentRef', 'action'].includes(key)) || !ref(input.documentRef)
|
|
19
|
+
|| !['open', 'download'].includes(input.action)) invalid();
|
|
20
|
+
} else if (operation === 'preparePdfTemplate') {
|
|
21
|
+
if (Object.keys(input).some(key => !['inputSchema', 'sampleData'].includes(key)) || !object(input.inputSchema)
|
|
22
|
+
|| input.inputSchema.type !== 'object' || input.inputSchema.additionalProperties !== false || !object(input.sampleData)) invalid();
|
|
23
|
+
bounded(input);
|
|
24
|
+
if (new TextEncoder().encode(JSON.stringify(input)).length > 512 * 1024) invalid();
|
|
25
|
+
} else invalid();
|
|
26
|
+
// 快照避免App在异步交互期间原位改动请求。
|
|
27
|
+
return JSON.parse(JSON.stringify(input));
|
|
28
|
+
}
|
|
29
|
+
export function assertDocumentResult(operation, value) {
|
|
30
|
+
if (operation === 'preparePdfTemplate' && value === null) return null;
|
|
31
|
+
if (!object(value) || Object.keys(value).length !== 1) invalid();
|
|
32
|
+
if (operation === 'preparePdfTemplate' ? !ref(value.templateFileRef) : !['opened', 'started', 'cancelled'].includes(value.status)) invalid();
|
|
33
|
+
return Object.freeze({ ...value });
|
|
34
|
+
}
|
|
35
|
+
export function createDocumentClient(invoke) {
|
|
36
|
+
return Object.freeze(Object.fromEntries(['preparePdfTemplate', 'openPdfDocument'].map(operation => [operation,
|
|
37
|
+
input => invoke(operation, assertDocumentInput(operation, input)).then(value => assertDocumentResult(operation, value)),
|
|
38
|
+
])));
|
|
39
|
+
}
|
package/src/client/index.d.ts
CHANGED
|
@@ -169,6 +169,16 @@ export interface PageDefinition {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
export type DefinedPage = Readonly<PageDefinition & { type: "page" }>;
|
|
172
|
+
/** 结果来自同次已校验 Action;身份与导航 owner 由 Host 绑定。 */
|
|
173
|
+
export interface ToolRendererProps<Result = unknown> {
|
|
174
|
+
readonly result: Result;
|
|
175
|
+
readonly actionKey: string;
|
|
176
|
+
}
|
|
177
|
+
export interface ToolRendererDefinition<Result = unknown> {
|
|
178
|
+
key: string;
|
|
179
|
+
Renderer: ComponentType<ToolRendererProps<Result>>;
|
|
180
|
+
}
|
|
181
|
+
export function defineToolRenderer<Result>(definition: ToolRendererDefinition<Result>): Readonly<ToolRendererDefinition<Result> & { type: 'tool-renderer' }>;
|
|
172
182
|
export type FrontendExtensionDefinition =
|
|
173
183
|
| SettingsExtensionDefinition
|
|
174
184
|
| RecordActionDefinition
|
|
@@ -251,3 +261,9 @@ export function queryAnalysis(request: import('./analysis.js').AnalysisRequest):
|
|
|
251
261
|
export type { OpenAssistantOptions } from './interactions.js';
|
|
252
262
|
/** 打开侧边助手;首次发言才调用模型,context 仅作为新会话初始背景。 */
|
|
253
263
|
export function openAssistant(options: import('./interactions.js').OpenAssistantOptions): Promise<void>;
|
|
264
|
+
|
|
265
|
+
export type { RestrictedInputSchema, PreparePdfTemplateInput, OpenPdfDocumentInput, PreparedPdfTemplate, OpenPdfDocumentResult } from './documents.js';
|
|
266
|
+
/** Desktop制作;取消返回null。缺能力或旧Host返回runtime_document_unsupported。 */
|
|
267
|
+
export function preparePdfTemplate(input: import('./documents.js').PreparePdfTemplateInput): Promise<import('./documents.js').PreparedPdfTemplate | null>;
|
|
268
|
+
/** Desktop/H5认证文件交付;started只表示已交浏览器,不证明磁盘保存。 */
|
|
269
|
+
export function openPdfDocument(input: import('./documents.js').OpenPdfDocumentInput): Promise<import('./documents.js').OpenPdfDocumentResult>;
|
package/src/client/index.js
CHANGED
|
@@ -5,6 +5,14 @@ const FRONTEND_EXTENSION_MARKER = Symbol.for("zhike.frontend-extension.v1");
|
|
|
5
5
|
const PAGE_VARIANTS = new Set(["full", "centered"]);
|
|
6
6
|
const PAGE_ICONS = new Set(sdkIconNames);
|
|
7
7
|
|
|
8
|
+
export function defineToolRenderer(definition) {
|
|
9
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)
|
|
10
|
+
|| Object.keys(definition).some(key => !['key', 'Renderer'].includes(key))
|
|
11
|
+
|| typeof definition.key !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(definition.key)
|
|
12
|
+
|| typeof definition.Renderer !== 'function') throw new TypeError('tool_renderer_definition_invalid');
|
|
13
|
+
return Object.freeze({ ...definition, type: 'tool-renderer' });
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
export function defineFrontendExtension(definition) {
|
|
9
17
|
if (!definition || typeof definition !== "object" || Array.isArray(definition)) {
|
|
10
18
|
throw new TypeError("Frontend Extension definition 必须是对象");
|
|
@@ -76,3 +84,5 @@ export function getCurrentUser() { throw new Error('current_user_unavailable');
|
|
|
76
84
|
|
|
77
85
|
export function queryAnalysis() { return Promise.reject(new Error('runtime_analysis_unavailable')); }
|
|
78
86
|
export function openAssistant() { return Promise.reject(new Error('runtime_host_interaction_unavailable')); }
|
|
87
|
+
export function preparePdfTemplate() { return Promise.reject(new Error('runtime_document_unsupported')); }
|
|
88
|
+
export function openPdfDocument() { return Promise.reject(new Error('runtime_document_unsupported')); }
|
|
@@ -11,7 +11,7 @@ export interface ToastOptions { message: string; tone?: 'info' | 'success' | 'er
|
|
|
11
11
|
export interface ConfirmOptions { title: string; description?: string; confirmLabel?: string; cancelLabel?: string }
|
|
12
12
|
export interface OpenExternalUrlOptions { url: string }
|
|
13
13
|
export interface NavigateOptions { destination: import('./destinations.js').AppDestination }
|
|
14
|
-
export type HostInteraction = {operation:'openAssistant';input:OpenAssistantOptions} | {operation:'showToast';input:ToastOptions} | {operation:'confirm';input:ConfirmOptions} | {operation:'navigate';input:NavigateOptions} | {operation:'openExternal';input:OpenExternalOptions} | {operation:'openExternalUrl';input:OpenExternalUrlOptions};
|
|
14
|
+
export type HostInteraction = {operation:'preparePdfTemplate';input:import('./documents.js').PreparePdfTemplateInput} | {operation:'openPdfDocument';input:import('./documents.js').OpenPdfDocumentInput} | {operation:'openAssistant';input:OpenAssistantOptions} | {operation:'showToast';input:ToastOptions} | {operation:'confirm';input:ConfirmOptions} | {operation:'navigate';input:NavigateOptions} | {operation:'openExternal';input:OpenExternalOptions} | {operation:'openExternalUrl';input:OpenExternalUrlOptions};
|
|
15
15
|
export function assertHostInteraction(operation: unknown, input: unknown): Readonly<ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions>;
|
|
16
16
|
export function createClientInteractions(invoke: (operation: string, input: ToastOptions | ConfirmOptions | NavigateOptions | OpenExternalOptions | OpenExternalUrlOptions | OpenAssistantOptions) => Promise<unknown>): {
|
|
17
17
|
openAssistant(input: OpenAssistantOptions): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertDocumentInput } from './documents.js';
|
|
1
2
|
import { assertExternalUrl } from './external-url.js';
|
|
2
3
|
import { assertAppDestination } from './destinations.js';
|
|
3
4
|
|
|
@@ -11,6 +12,7 @@ const externalUrl = value => {
|
|
|
11
12
|
} catch { return false; }
|
|
12
13
|
};
|
|
13
14
|
export function assertHostInteraction(operation, input) {
|
|
15
|
+
if (['preparePdfTemplate', 'openPdfDocument'].includes(operation)) return assertDocumentInput(operation, input);
|
|
14
16
|
if (!input || typeof input !== 'object' || Array.isArray(input)) fail();
|
|
15
17
|
const allowed = operation === 'openAssistant' ? ['scenario','context','title'] : operation === 'showToast' ? ['message','tone'] : operation === 'confirm' ? ['title','description','confirmLabel','cancelLabel'] : operation === 'navigate' ? ['destination'] : operation === 'openExternal' ? ['href'] : operation === 'openExternalUrl' ? ['url'] : [];
|
|
16
18
|
if (!allowed.length || Object.keys(input).some(key=>!allowed.includes(key))) fail();
|
|
@@ -62,6 +62,14 @@ export const frontendPoints = Object.freeze([
|
|
|
62
62
|
"definitionName": "PageDefinition",
|
|
63
63
|
"componentName": "Page",
|
|
64
64
|
"maxPerApp": 100
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"key": "app.tool-renderers",
|
|
68
|
+
"kind": "view",
|
|
69
|
+
"legacyType": "tool-renderer",
|
|
70
|
+
"definitionName": "ToolRendererDefinition",
|
|
71
|
+
"componentName": "Renderer",
|
|
72
|
+
"maxPerApp": 64
|
|
65
73
|
}
|
|
66
74
|
].map(Object.freeze));
|
|
67
75
|
export const frontendPoint = type => frontendPoints.find(point => point.legacyType === type);
|
package/src/forms/index.d.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface FormHandle<S extends FormSchema> {
|
|
|
71
71
|
placeholder?: string;
|
|
72
72
|
disabled?: boolean;
|
|
73
73
|
resizable?: boolean;
|
|
74
|
+
labelHidden?: boolean;
|
|
75
|
+
appearance?: "default" | "revision";
|
|
74
76
|
}>;
|
|
75
77
|
NumberInput: ComponentType<{
|
|
76
78
|
name: FieldName<S, "number">;
|
|
@@ -4,6 +4,9 @@ export interface AppAgentDefinition {
|
|
|
4
4
|
name: string;
|
|
5
5
|
/** App 提供的系统提示词,最多 16KiB UTF-8,不交付 Client Worker。 */
|
|
6
6
|
systemPrompt: string;
|
|
7
|
+
/** 引用同 App 独立 Action 的 key,Schema/execute 只维护一份。 */
|
|
8
|
+
actionRefs?: readonly string[];
|
|
9
|
+
aiExposure?: AppExpertExposure;
|
|
7
10
|
}
|
|
8
11
|
export type ToolSchema = Readonly<Record<string, unknown>>;
|
|
9
12
|
export interface AppToolDefinition<Input, Output> {
|
|
@@ -15,8 +18,42 @@ export interface AppToolDefinition<Input, Output> {
|
|
|
15
18
|
outputSchema: ToolSchema;
|
|
16
19
|
/** 默认 none;output 只允许成功且通过校验的结果进入实时 callback。 */
|
|
17
20
|
clientResult?: 'none' | 'output';
|
|
21
|
+
/** 缺省私有;公开查询声明不代替执行授权。 */
|
|
22
|
+
aiExposure?: AppActionExposure;
|
|
23
|
+
/** 同 App 静态 Tool Renderer;展示不授予服务端执行或业务写入权限。 */
|
|
24
|
+
toolRenderer?: { key: string; showByDefault: boolean };
|
|
18
25
|
/** 仅在所属安装的服务端执行环境运行,凭据沿用现有 Server SDK。 */
|
|
19
26
|
execute(input: Input): Output | Promise<Output>;
|
|
20
27
|
}
|
|
21
28
|
export function defineAgent(definition: AppAgentDefinition): Readonly<AppAgentDefinition>;
|
|
22
29
|
export function defineTool<Input = Record<string, unknown>, Output = unknown>(definition: AppToolDefinition<Input, Output>): Readonly<AppToolDefinition<Input, Output>>;
|
|
30
|
+
|
|
31
|
+
export interface AppActionExposure {
|
|
32
|
+
effect: 'query';
|
|
33
|
+
/** 公开 object 标识;只用于说明适用对象,不授予权限。 */
|
|
34
|
+
subjectTypes: readonly string[];
|
|
35
|
+
usage: string;
|
|
36
|
+
limitations: string;
|
|
37
|
+
/** 已有 inputSchema 中接收 {object, recordId} 的字段;由 Host 绑定。 */
|
|
38
|
+
subjectInput?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AppExpertExposure {
|
|
41
|
+
subjectTypes: readonly string[];
|
|
42
|
+
usage: string;
|
|
43
|
+
limitations: string;
|
|
44
|
+
outputSchema: ToolSchema;
|
|
45
|
+
/** 只允许显式开放的原私有工具子集;独立能力沿 actionRefs。 */
|
|
46
|
+
toolKeys?: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
interface AppDataCapabilityBase {
|
|
49
|
+
key: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
subjectTypes: readonly string[];
|
|
53
|
+
usage: string;
|
|
54
|
+
}
|
|
55
|
+
export type AppDataCapabilityDefinition = AppDataCapabilityBase & (
|
|
56
|
+
{ kind: 'live'; actionRef: string; sourceBinding?: never } |
|
|
57
|
+
{ kind: 'published'; sourceBinding: string; actionRef?: never }
|
|
58
|
+
);
|
|
59
|
+
export function defineDataCapability<T extends AppDataCapabilityDefinition>(definition: T): Readonly<T>;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { assertDataCapability } from '../../toolchain/ai-capabilities.js';
|
|
2
|
+
import { assertAiExposure } from '../../toolchain/agents/exposure.js';
|
|
3
|
+
import { assertToolRendererReference } from '../../toolchain/agents/manifest.js';
|
|
1
4
|
const KEY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
2
5
|
|
|
3
6
|
function fields(value, required, optional = []) {
|
|
@@ -11,7 +14,7 @@ function fields(value, required, optional = []) {
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function defineAgent(definition) {
|
|
14
|
-
fields(definition, ["key", "name", "systemPrompt"]);
|
|
17
|
+
fields(definition, ["key", "name", "systemPrompt"], ["actionRefs", "aiExposure"]);
|
|
15
18
|
if (typeof definition.systemPrompt !== "string" || !definition.systemPrompt.trim() ||
|
|
16
19
|
new TextEncoder().encode(definition.systemPrompt).length > 16384) {
|
|
17
20
|
throw new TypeError("agent_definition_invalid");
|
|
@@ -20,11 +23,18 @@ export function defineAgent(definition) {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
export function defineTool(definition) {
|
|
23
|
-
fields(definition, ["key", "name", "description", "inputSchema", "outputSchema", "execute"], ["clientResult"]);
|
|
26
|
+
fields(definition, ["key", "name", "description", "inputSchema", "outputSchema", "execute"], ["clientResult", "aiExposure", "toolRenderer"]);
|
|
24
27
|
if (typeof definition.execute !== "function" || typeof definition.description !== "string" ||
|
|
25
28
|
!definition.description.trim() || definition.description.length > 1200 ||
|
|
26
29
|
![undefined, "none", "output"].includes(definition.clientResult)) {
|
|
27
30
|
throw new TypeError("agent_tool_definition_invalid");
|
|
28
31
|
}
|
|
32
|
+
if (definition.aiExposure !== undefined) assertAiExposure(definition.aiExposure, definition.inputSchema);
|
|
33
|
+
if (definition.toolRenderer !== undefined) assertToolRendererReference(definition.toolRenderer);
|
|
29
34
|
return Object.freeze({ ...definition, clientResult: definition.clientResult ?? "none" });
|
|
30
35
|
}
|
|
36
|
+
|
|
37
|
+
export function defineDataCapability(definition) {
|
|
38
|
+
assertDataCapability(definition);
|
|
39
|
+
return Object.freeze({ ...definition });
|
|
40
|
+
}
|
package/src/server/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineAgent, defineTool, type AppAgentDefinition, type AppToolDefinition, type ToolSchema } from "./agents/index.js";
|
|
1
|
+
export { defineAgent, defineTool, defineDataCapability, type AppAgentDefinition, type AppToolDefinition, type ToolSchema } from "./agents/index.js";
|
|
2
2
|
import type { JsonValue } from "../index.js";
|
|
3
3
|
export { defineWorkflowStep, defineWorkflowStepExecute } from "./steps/index.js";
|
|
4
4
|
export type { WorkflowObjectSchema, WorkflowScalarSchema, WorkflowStepDefinition, WorkflowStepOutcome, WorkflowStepResult, WorkflowSchemaValue } from "./steps/index.js";
|
|
@@ -319,3 +319,5 @@ export interface WebhookContext {
|
|
|
319
319
|
/** 从 1 开始的业务尝试序号。 */
|
|
320
320
|
readonly attemptNumber: number;
|
|
321
321
|
}
|
|
322
|
+
|
|
323
|
+
export type { AppActionExposure, AppExpertExposure, AppDataCapabilityDefinition } from "./agents/index.js";
|
package/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineAgent, defineTool } from "./agents/index.js";
|
|
1
|
+
export { defineAgent, defineTool, defineDataCapability } from "./agents/index.js";
|
|
2
2
|
const SERVER_FUNCTION_MARKER = Symbol.for("zhike.server-function.v1");
|
|
3
3
|
export { defineWorkflowStep, defineWorkflowStepExecute } from "./steps/index.js";
|
|
4
4
|
export { defineWorkflowTrigger, defineWorkflowTriggerActivate, defineWorkflowTriggerOnEvent, defineWorkflowTriggerDeactivate } from "./triggers/index.js";
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import ts from 'typescript';
|
|
2
2
|
import { posix } from 'node:path';
|
|
3
3
|
import { ZhikeCliError } from '../../errors.js';
|
|
4
|
+
import { assertAiCapabilities } from '../ai-capabilities.js';
|
|
4
5
|
import { assertAgentManifest } from './manifest.js';
|
|
5
6
|
|
|
6
7
|
const AGENT_PATH = /^app\/agents\/([a-z][a-z0-9-]{0,63})\/agent\.ts$/;
|
|
8
|
+
const ACTION_PATH = /^app\/actions\/([a-z][a-z0-9-]{0,63})\.tools\.ts$/;
|
|
9
|
+
const DATA_PATH = /^app\/data-capabilities\/([a-z][a-z0-9-]{0,63})\.ts$/;
|
|
7
10
|
const TOOL_PATH = /^app\/agents\/([a-z][a-z0-9-]{0,63})\/([a-z][a-z0-9-]{0,63})\.tools\.ts$/;
|
|
8
11
|
function invalid(path, message) { throw new ZhikeCliError('zhike_agent_invalid', `${path}: ${message}`); }
|
|
9
12
|
function literal(node, path, depth = 0) {
|
|
@@ -28,7 +31,7 @@ function literal(node, path, depth = 0) {
|
|
|
28
31
|
invalid(path, '元数据不能读取变量、函数或运行时代码');
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
function definition(file, api) {
|
|
34
|
+
export function definition(file, api) {
|
|
32
35
|
const { sourceFile, relativePath } = file;
|
|
33
36
|
const imported = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) &&
|
|
34
37
|
statement.moduleSpecifier.text === '@77-zhike/app-sdk/server' &&
|
|
@@ -62,19 +65,20 @@ export function discoverAgents(files) {
|
|
|
62
65
|
const match = file.relativePath.match(AGENT_PATH);
|
|
63
66
|
if (!match) continue;
|
|
64
67
|
const value = definition(file, 'defineAgent');
|
|
65
|
-
if (value.key !== match[1] || Object.keys(value).some(key => !['key', 'name', 'systemPrompt'].includes(key))) invalid(file.relativePath, 'Agent key 必须等于目录且只接受 key/name/systemPrompt');
|
|
68
|
+
if (value.key !== match[1] || Object.keys(value).some(key => !['key', 'name', 'systemPrompt', 'actionRefs', 'aiExposure'].includes(key))) invalid(file.relativePath, 'Agent key 必须等于目录且只接受 key/name/systemPrompt');
|
|
66
69
|
agents.set(value.key, { ...value, relativePath: file.relativePath, tools: [] });
|
|
67
70
|
}
|
|
68
71
|
if (agents.size > 16) invalid("app/agents", "一个 App 最多 16 个 Agent");
|
|
69
72
|
for (const file of files) {
|
|
70
73
|
const match = file.relativePath.match(TOOL_PATH);
|
|
71
|
-
if (file.relativePath.endsWith('.tools.ts') && !match) invalid(file.relativePath, '工具必须位于 Agent 的直接子目录');
|
|
74
|
+
if (file.relativePath.endsWith('.tools.ts') && !match && !ACTION_PATH.test(file.relativePath)) invalid(file.relativePath, '工具必须位于 Agent 的直接子目录');
|
|
72
75
|
if (/^app\/agents\/.+\/agent\.(?:ts|tsx)$/.test(file.relativePath) && !AGENT_PATH.test(file.relativePath)) invalid(file.relativePath, 'Agent 入口必须是直接子目录 agent.ts');
|
|
73
76
|
for (const statement of file.sourceFile.statements) {
|
|
74
77
|
if (!(ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) || !statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
75
78
|
const specifier = statement.moduleSpecifier.text;
|
|
76
79
|
if (!specifier.startsWith('.')) continue;
|
|
77
80
|
const target = posix.normalize(posix.join(posix.dirname(file.relativePath), specifier)).replace(/\.js$/, '.ts');
|
|
81
|
+
if (ACTION_PATH.test(target.endsWith('.ts') ? target : `${target}.ts`)) invalid(file.relativePath, '独立 Action 只能通过 actionRefs 引用,不能导入执行声明;请抽取普通 helper');
|
|
78
82
|
const tool = (target.endsWith(".ts") ? target : `${target}.ts`).match(TOOL_PATH);
|
|
79
83
|
if (tool && (!match || match[1] !== tool[1])) invalid(file.relativePath, '不能跨 Agent 导入工具声明,请抽取普通 helper');
|
|
80
84
|
}
|
|
@@ -97,3 +101,26 @@ export function discoverAgents(files) {
|
|
|
97
101
|
export function agentManifest({ relativePath, tools, ...agent }) {
|
|
98
102
|
return { ...agent, tools: tools.map(({ relativePath: path, ...tool }) => tool) };
|
|
99
103
|
}
|
|
104
|
+
|
|
105
|
+
export function discoverAiCapabilities(files, agents) {
|
|
106
|
+
const actions = [], dataCapabilities = [];
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const action = file.relativePath.match(ACTION_PATH);
|
|
109
|
+
const data = file.relativePath.match(DATA_PATH);
|
|
110
|
+
if (file.relativePath.startsWith('app/actions/') && file.relativePath.endsWith('.tools.ts') && !action) invalid(file.relativePath, 'Action 必须位于 actions 直接目录');
|
|
111
|
+
if (!action && !data) continue;
|
|
112
|
+
const value = definition(file, action ? 'defineTool' : 'defineDataCapability');
|
|
113
|
+
if (value.key !== (action ?? data)[1]) invalid(file.relativePath, 'key 必须与文件名一致');
|
|
114
|
+
if (action) {
|
|
115
|
+
if (value.execute !== true) invalid(file.relativePath, 'Action 缺少 execute');
|
|
116
|
+
delete value.execute;
|
|
117
|
+
actions.push({...value, clientResult:value.clientResult ?? 'none', artifact:'server', entry:`action/${value.key}`, relativePath:file.relativePath});
|
|
118
|
+
} else dataCapabilities.push({...value, relativePath:file.relativePath});
|
|
119
|
+
}
|
|
120
|
+
actions.sort((a,b)=>a.key.localeCompare(b.key));
|
|
121
|
+
dataCapabilities.sort((a,b)=>a.key.localeCompare(b.key));
|
|
122
|
+
try { assertAiCapabilities({sdkAbiVersion:'2',aiCapabilitiesVersion:'1',actions:actions.map(stripPath),dataCapabilities:dataCapabilities.map(stripPath),agents:agents.map(agentManifest)}); }
|
|
123
|
+
catch { invalid('app/actions', '独立能力声明或跨引用无效'); }
|
|
124
|
+
return {actions, dataCapabilities};
|
|
125
|
+
}
|
|
126
|
+
export function stripPath({relativePath, ...value}) { return value; }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const OBJECT = /^[a-z][a-z0-9_]{0,63}$/;
|
|
2
|
+
export function assertAiExposure(value, inputSchema, expert = false) {
|
|
3
|
+
const invalid = () => { throw new TypeError('app_manifest_ai_exposure_invalid'); };
|
|
4
|
+
const allowed = expert
|
|
5
|
+
? ['subjectTypes', 'usage', 'limitations', 'outputSchema', 'toolKeys']
|
|
6
|
+
: ['subjectTypes', 'usage', 'limitations', 'effect', 'subjectInput'];
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
8
|
+
Object.keys(value).some(key => !allowed.includes(key)) ||
|
|
9
|
+
!Array.isArray(value.subjectTypes) || value.subjectTypes.length > 16 ||
|
|
10
|
+
new Set(value.subjectTypes).size !== value.subjectTypes.length ||
|
|
11
|
+
value.subjectTypes.some(item => typeof item !== 'string' || !OBJECT.test(item))) invalid();
|
|
12
|
+
for (const key of ['usage', 'limitations']) {
|
|
13
|
+
if (typeof value[key] !== 'string' || !value[key].trim() || value[key].length > 1200) invalid();
|
|
14
|
+
}
|
|
15
|
+
if (expert) {
|
|
16
|
+
if (!value.outputSchema || (value.toolKeys !== undefined && (!Array.isArray(value.toolKeys) ||
|
|
17
|
+
new Set(value.toolKeys).size !== value.toolKeys.length ||
|
|
18
|
+
value.toolKeys.some(key => typeof key !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(key))))) invalid();
|
|
19
|
+
} else {
|
|
20
|
+
if (value.effect !== 'query') invalid();
|
|
21
|
+
if (value.subjectInput !== undefined) {
|
|
22
|
+
const subject = inputSchema?.properties?.[value.subjectInput];
|
|
23
|
+
if (typeof value.subjectInput !== 'string' || !Object.hasOwn(inputSchema.properties, value.subjectInput) ||
|
|
24
|
+
subject?.type !== 'object' || subject.additionalProperties !== false ||
|
|
25
|
+
Object.keys(subject.properties ?? {}).sort().join(',') !== 'object,recordId' ||
|
|
26
|
+
subject.properties.object?.type !== 'string' || subject.properties.recordId?.type !== 'string' ||
|
|
27
|
+
!subject.required?.includes('object') || !subject.required?.includes('recordId')) invalid();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertAiExposure } from './exposure.js';
|
|
1
2
|
const KEY = /^[a-z][a-z0-9-]{0,63}$/;
|
|
2
3
|
const FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
3
4
|
const RESERVED = new Set(['tenant_id', 'tenantId', 'member_id', 'memberId', 'installation_id', 'installationId',
|
|
@@ -46,17 +47,35 @@ export function assertAgentSchema(value) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function assertAgentManifest(value) {
|
|
49
|
-
record(value); keys(value, ['key', 'name', 'systemPrompt', 'tools']);
|
|
50
|
+
record(value); keys(value, ['key', 'name', 'systemPrompt', 'tools', 'actionRefs', 'aiExposure']);
|
|
50
51
|
if (typeof value.key !== "string" || value.key.trim() !== value.key || !KEY.test(value.key)) invalid();
|
|
51
52
|
text(value.name, 80); text(value.systemPrompt, 16384);
|
|
52
53
|
if (new TextEncoder().encode(value.systemPrompt).length > 16384 || !Array.isArray(value.tools) || value.tools.length > 32) invalid();
|
|
53
54
|
const seen = new Set();
|
|
54
55
|
for (const tool of value.tools) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
seen.add(tool.key); text(tool.name, 80); text(tool.description, 1200);
|
|
59
|
-
assertAgentSchema(tool.inputSchema); assertAgentSchema(tool.outputSchema);
|
|
56
|
+
if (seen.has(tool.key)) invalid();
|
|
57
|
+
seen.add(tool.key);
|
|
58
|
+
assertToolManifest(tool, `agent-tool/${value.key}/${tool.key}`);
|
|
60
59
|
}
|
|
61
60
|
return value;
|
|
62
61
|
}
|
|
62
|
+
|
|
63
|
+
export function assertToolManifest(tool, entry) {
|
|
64
|
+
record(tool); keys(tool, ['artifact', 'key', 'entry', 'name', 'description', 'inputSchema', 'outputSchema', 'clientResult', 'aiExposure', 'toolRenderer']);
|
|
65
|
+
if (typeof tool.key !== 'string' || tool.key.trim() !== tool.key || !KEY.test(tool.key) ||
|
|
66
|
+
tool.artifact !== 'server' || tool.entry !== entry || !['none', 'output'].includes(tool.clientResult)) invalid();
|
|
67
|
+
text(tool.name, 80); text(tool.description, 1200);
|
|
68
|
+
assertAgentSchema(tool.inputSchema); assertAgentSchema(tool.outputSchema);
|
|
69
|
+
if (tool.toolRenderer !== undefined) assertToolRendererReference(tool.toolRenderer);
|
|
70
|
+
if (tool.aiExposure !== undefined) {
|
|
71
|
+
if (Object.hasOwn(tool.inputSchema.properties, 'explanation')) invalid();
|
|
72
|
+
assertAiExposure(tool.aiExposure, tool.inputSchema);
|
|
73
|
+
}
|
|
74
|
+
return tool;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function assertToolRendererReference(value) {
|
|
78
|
+
record(value); keys(value, ['key', 'showByDefault']);
|
|
79
|
+
if (typeof value.key !== 'string' || !KEY.test(value.key) || typeof value.showByDefault !== 'boolean') invalid();
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { assertToolManifest, assertAgentSchema } from './agents/manifest.js';
|
|
2
|
+
import { assertAiExposure } from './agents/exposure.js';
|
|
3
|
+
|
|
4
|
+
export function assertAiCapabilities(manifest) {
|
|
5
|
+
const invalid = () => { throw new TypeError('app_manifest_ai_capabilities_invalid'); };
|
|
6
|
+
const actions = manifest.actions === undefined ? [] : manifest.actions;
|
|
7
|
+
const data = manifest.dataCapabilities === undefined ? [] : manifest.dataCapabilities;
|
|
8
|
+
const agents = manifest.agents ?? [];
|
|
9
|
+
const hasAiFields = manifest.actions !== undefined || manifest.dataCapabilities !== undefined ||
|
|
10
|
+
agents.some(agent => agent.aiExposure !== undefined || agent.actionRefs !== undefined ||
|
|
11
|
+
agent.tools.some(tool => tool.aiExposure !== undefined));
|
|
12
|
+
if (manifest.aiCapabilitiesVersion !== undefined && manifest.aiCapabilitiesVersion !== '1') invalid();
|
|
13
|
+
if (hasAiFields && manifest.aiCapabilitiesVersion !== '1') invalid();
|
|
14
|
+
if (manifest.aiCapabilitiesVersion !== undefined && manifest.sdkAbiVersion !== '2') invalid();
|
|
15
|
+
if (!Array.isArray(actions) || actions.length > 64 || !Array.isArray(data) || data.length > 64) invalid();
|
|
16
|
+
if (manifest.frontendExtensions !== undefined) {
|
|
17
|
+
const renderers = new Set(manifest.frontendExtensions.filter(item => item.type === 'tool-renderer').map(item => item.key));
|
|
18
|
+
for (const tool of [...actions, ...agents.flatMap(agent => agent.tools)]) {
|
|
19
|
+
if (tool.toolRenderer && !renderers.has(tool.toolRenderer.key)) invalid();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const byKey = new Map();
|
|
23
|
+
for (const action of actions) {
|
|
24
|
+
assertToolManifest(action, `action/${action.key}`);
|
|
25
|
+
if (byKey.has(action.key) || action.clientResult !== 'none') invalid();
|
|
26
|
+
byKey.set(action.key, action);
|
|
27
|
+
}
|
|
28
|
+
const dataKeys = new Set();
|
|
29
|
+
for (const item of data) {
|
|
30
|
+
assertDataCapability(item);
|
|
31
|
+
if (dataKeys.has(item.key)) invalid();
|
|
32
|
+
dataKeys.add(item.key);
|
|
33
|
+
if (item.kind === 'live') {
|
|
34
|
+
const action = byKey.get(item.actionRef);
|
|
35
|
+
if (!action?.aiExposure || item.sourceBinding !== undefined ||
|
|
36
|
+
item.subjectTypes.some(type => !action.aiExposure.subjectTypes.includes(type))) invalid();
|
|
37
|
+
} else if (item.kind === 'published') {
|
|
38
|
+
if (typeof item.sourceBinding !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.sourceBinding) || item.actionRef !== undefined) invalid();
|
|
39
|
+
} else invalid();
|
|
40
|
+
}
|
|
41
|
+
for (const agent of agents) {
|
|
42
|
+
const refs = agent.actionRefs === undefined ? [] : agent.actionRefs;
|
|
43
|
+
if (!Array.isArray(refs) || refs.length > 32 || new Set(refs).size !== refs.length || refs.some(ref => !byKey.has(ref))) invalid();
|
|
44
|
+
if (agent.aiExposure !== undefined) {
|
|
45
|
+
assertAiExposure(agent.aiExposure, null, true);
|
|
46
|
+
assertAgentSchema(agent.aiExposure.outputSchema);
|
|
47
|
+
if (refs.some(ref => !byKey.get(ref).aiExposure) ||
|
|
48
|
+
(agent.aiExposure.toolKeys ?? []).some(key => !agent.tools.find(tool => tool.key === key)?.aiExposure)) invalid();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return manifest;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function assertDataCapability(item) {
|
|
55
|
+
const invalid = () => { throw new TypeError('app_manifest_ai_capabilities_invalid'); };
|
|
56
|
+
if (!item || typeof item !== 'object' || Array.isArray(item) ||
|
|
57
|
+
Object.keys(item).some(key => !['key', 'name', 'description', 'subjectTypes', 'kind', 'sourceBinding', 'actionRef', 'usage'].includes(key)) ||
|
|
58
|
+
typeof item.key !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.key)) invalid();
|
|
59
|
+
for (const [key, max] of [['name', 80], ['description', 1200], ['usage', 1200]]) {
|
|
60
|
+
if (typeof item[key] !== 'string' || !item[key].trim() || item[key].length > max) invalid();
|
|
61
|
+
}
|
|
62
|
+
assertAiExposure({subjectTypes:item.subjectTypes, usage:item.usage, limitations:item.description, effect:'query'});
|
|
63
|
+
if (item.kind === 'published') {
|
|
64
|
+
if (typeof item.sourceBinding !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.sourceBinding) || item.actionRef !== undefined) invalid();
|
|
65
|
+
} else if (item.kind === 'live') {
|
|
66
|
+
if (typeof item.actionRef !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(item.actionRef) || item.sourceBinding !== undefined) invalid();
|
|
67
|
+
} else invalid();
|
|
68
|
+
return item;
|
|
69
|
+
}
|
|
@@ -6,7 +6,7 @@ import { dirname, relative, resolve, sep } from "node:path";
|
|
|
6
6
|
import { build } from "esbuild";
|
|
7
7
|
import ts from "typescript";
|
|
8
8
|
|
|
9
|
-
import { agentManifest } from "./agents/discovery.js";
|
|
9
|
+
import { agentManifest, stripPath } from "./agents/discovery.js";
|
|
10
10
|
import { serializeWebhookResponse } from "./webhook-response.js";
|
|
11
11
|
|
|
12
12
|
import { TOOLING_VERSION } from "../config.js";
|
|
@@ -43,7 +43,7 @@ export async function buildAppBundle({
|
|
|
43
43
|
sources.serverFunctions.length > 0 ||
|
|
44
44
|
sources.webhookHandlers.length > 0 ||
|
|
45
45
|
sources.workflowSteps.length > 0 ||
|
|
46
|
-
sources.workflowTriggers.length > 0 || sources.agents.length > 0
|
|
46
|
+
sources.workflowTriggers.length > 0 || sources.agents.length > 0 || sources.actions.length > 0
|
|
47
47
|
? await buildServerArtifact(projectDirectory, sources) : null;
|
|
48
48
|
} catch (error) {
|
|
49
49
|
const policyError = error.errors?.find(item => item.detail instanceof ZhikeCliError)?.detail;
|
|
@@ -96,7 +96,7 @@ export async function buildAppBundle({
|
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFunctions, agents }) {
|
|
99
|
+
async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFunctions, agents, actions }) {
|
|
100
100
|
const paths = {
|
|
101
101
|
"@77-zhike/app-sdk": [resolve(SDK_SOURCE_ROOT, "index.d.ts")],
|
|
102
102
|
"@77-zhike/app-sdk/client": [resolve(SDK_SOURCE_ROOT, "client", "index.d.ts")],
|
|
@@ -129,6 +129,7 @@ async function assertTypeScriptProgram(projectDirectory, { sourceFiles, serverFu
|
|
|
129
129
|
const handlers = [
|
|
130
130
|
...serverFunctions.map(item => ({ ...item, member: '' })),
|
|
131
131
|
...agents.flatMap(agent => agent.tools.map(item => ({ ...item, member: '.execute' }))),
|
|
132
|
+
...actions.map(item => ({...item, member: '.execute'})),
|
|
132
133
|
];
|
|
133
134
|
const contractSource = [
|
|
134
135
|
'import type { ServerFunction } from "@77-zhike/app-sdk/server";',
|
|
@@ -194,10 +195,10 @@ async function buildServerArtifact(projectDirectory, sources) {
|
|
|
194
195
|
const hasConnectionLifecycle = sources.lifecycleEvents.some(
|
|
195
196
|
({ event }) => event === "connection-added" || event === "connection-removed",
|
|
196
197
|
);
|
|
197
|
-
const tools = sources.agents.flatMap(agent => agent.tools);
|
|
198
|
+
const tools = [...sources.agents.flatMap(agent => agent.tools), ...sources.actions];
|
|
198
199
|
const toolImports = tools.map((tool, index) => `import agentTool${index} from "./src/${tool.relativePath}";`);
|
|
199
200
|
const agentToolDeclarations = `const agentTools = new Map([${tools.map((tool, index) => `[${JSON.stringify(tool.entry)}, agentTool${index}]`).join(",")}]);`;
|
|
200
|
-
const agentToolDispatch = ` if (request.kind === "agent_tool") { const tool = agentTools.get(request.entryKey); if (!tool) throw new Error("runtime_agent_tool_not_found"); return tool.execute(Object.freeze({ ...request.input })); }\n`;
|
|
201
|
+
const agentToolDispatch = ` if (request.kind === "agent_tool" || request.kind === "app_action") { const prefix = request.kind === "app_action" ? "action/" : "agent-tool/"; const tool = typeof request.entryKey === "string" && request.entryKey.startsWith(prefix) ? agentTools.get(request.entryKey) : null; if (!tool) throw new Error("runtime_agent_tool_not_found"); return tool.execute(Object.freeze({ ...request.input })); }\n`;
|
|
201
202
|
const imports = [
|
|
202
203
|
...toolImports,
|
|
203
204
|
...(hasConnectionLifecycle
|
|
@@ -327,7 +328,7 @@ function createSdkBuildPlugin(projectDirectory, sources, target) {
|
|
|
327
328
|
buildApi.onLoad({ filter: /.*/, namespace: "zhike-sdk-runtime" }, (args) => {
|
|
328
329
|
if (args.path.endsWith("/client")) {
|
|
329
330
|
return {
|
|
330
|
-
contents: "export const defineFrontendExtension = (definition) => definition; export const definePage = (definition) => ({ ...definition, type: 'page' });",
|
|
331
|
+
contents: "export const defineFrontendExtension = (definition) => definition; export const definePage = (definition) => ({ ...definition, type: 'page' }); export const defineToolRenderer = (definition) => ({ ...definition, type: 'tool-renderer' });",
|
|
331
332
|
loader: "js",
|
|
332
333
|
};
|
|
333
334
|
}
|
|
@@ -411,6 +412,9 @@ function createManifest({ clientBytes, serverBytes, sources }) {
|
|
|
411
412
|
key,
|
|
412
413
|
})),
|
|
413
414
|
...(sources.agents.length ? { agents: sources.agents.map(agentManifest) } : {}),
|
|
415
|
+
...((sources.actions.length || sources.dataCapabilities.length || sources.agents.some(agent => agent.aiExposure || agent.actionRefs || agent.tools.some(tool => tool.aiExposure))) ? {aiCapabilitiesVersion:'1'} : {}),
|
|
416
|
+
...(sources.actions.length ? {actions:sources.actions.map(stripPath)} : {}),
|
|
417
|
+
...(sources.dataCapabilities.length ? {dataCapabilities:sources.dataCapabilities.map(stripPath)} : {}),
|
|
414
418
|
serverFunctions: sources.serverFunctions.map(({ key }) => ({
|
|
415
419
|
artifact: "server",
|
|
416
420
|
key,
|
|
@@ -444,6 +448,7 @@ function createServerDefinitionsDigest(sources) {
|
|
|
444
448
|
...(sources.agents.length ? { agents: sources.agents.map(({ key, tools }) => ({
|
|
445
449
|
key, tools: tools.map(({ key: toolKey, entry }) => ({ key: toolKey, entry })),
|
|
446
450
|
})) } : {}),
|
|
451
|
+
...(sources.actions.length ? {actions:sources.actions.map(({key,entry})=>({key,entry}))} : {}),
|
|
447
452
|
serverFunctions: sources.serverFunctions.map(({ key }) => ({ key })),
|
|
448
453
|
webhookHandlers: sources.webhookHandlers.map(({ key, methods }) => ({ key, methods })),
|
|
449
454
|
...(sources.workflowSteps.length > 0 ? { workflowSteps: sources.workflowSteps.map(({ key, entry }) => ({ key, entry })) } : {}),
|