@opentiny/next-sdk 0.2.2 → 0.2.4

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/index.ts CHANGED
@@ -38,3 +38,17 @@ export { getAISDKTools } from './agent/utils/getAISDKTools'
38
38
  // 方便的二维码类
39
39
  export { QrCode, type QrCodeOption } from './remoter/QrCode'
40
40
  export type * from './agent/type'
41
+
42
+ // Web 端 Skill 公共能力:解析 skill 文档、生成 systemPrompt、内置 list_skills / get_skill_content 工具
43
+ export {
44
+ getSkillOverviews,
45
+ formatSkillsForSystemPrompt,
46
+ getSkillMdPaths,
47
+ getSkillMdContent,
48
+ getMainSkillPaths,
49
+ getMainSkillPathByName,
50
+ parseSkillFrontMatter,
51
+ createSkillTools,
52
+ type SkillMeta,
53
+ type SkillToolsSet
54
+ } from './skills/index'
package/package.json CHANGED
@@ -1,24 +1,39 @@
1
1
  {
2
2
  "name": "@opentiny/next-sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
+ "homepage": "https://docs.opentiny.design/next-sdk/guide/",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git@github.com:opentiny/next-sdk.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/opentiny/next-sdk/issues"
12
+ },
13
+ "keywords": [
14
+ "next-sdk",
15
+ "opentiny",
16
+ "mcp",
17
+ "sdk",
18
+ "agent"
19
+ ],
5
20
  "license": "MIT",
6
21
  "author": "Chunhui Mo",
7
22
  "main": "dist/index.js",
8
23
  "module": "dist/index.js",
9
24
  "types": "dist/index.d.ts",
10
- "description": "OpenTiny NEXT SDK",
25
+ "description": "A frontend intelligent application development toolkit designed to simplify WebAgent integration and usage, supporting multiple programming languages and frontend frameworks to help developers rapidly implement intelligent features",
11
26
  "dependencies": {
12
27
  "@modelcontextprotocol/sdk": "~1.25.2",
13
28
  "@opentiny/next": "^0.3.2",
14
- "@ai-sdk/openai": "^2.0.76",
29
+ "@ai-sdk/openai": "^3.0.0",
15
30
  "@ai-sdk/deepseek": "1.0.30",
16
- "@ai-sdk/provider": "^2.0.0",
17
- "@ai-sdk/mcp": "^0.0.11",
31
+ "@ai-sdk/provider": "^3.0.0",
32
+ "@ai-sdk/mcp": "^1.0.0",
18
33
  "qrcode": "^1.5.4",
19
34
  "zod": "^3.25.76",
20
35
  "ajv": "^8.17.1",
21
- "ai": "5.0.106"
36
+ "ai": "^6.0.0"
22
37
  },
23
38
  "devDependencies": {
24
39
  "typescript": "~5.9.3",
@@ -1,13 +1,14 @@
1
1
  import { QrCode } from './QrCode'
2
- import { Tooltip } from './tooltips';
3
- import chat from './svgs/chat.svg?url';
4
- import scan from './svgs/scan.svg?url';
5
- import link from './svgs/link.svg?url';
6
- import qrCode from './svgs/qrcode.svg?url';
7
- import iconCopy from './svgs/icon-copy.svg?url';
8
-
9
- const DEFAULT_REMOTE_URL = 'https://agent.opentiny.design/tiny-robot'
2
+ import { Tooltip } from './tooltips'
3
+ import chat from './svgs/chat.svg?url'
4
+ import scan from './svgs/scan.svg?url'
5
+ import link from './svgs/link.svg?url'
6
+ import qrCode from './svgs/qrcode.svg?url'
7
+ import iconCopy from './svgs/icon-copy.svg?url'
8
+
9
+ const DEFAULT_REMOTE_URL = 'https://chat.opentiny.design'
10
10
  const DEFAULT_QR_CODE_URL = 'https://ai.opentiny.design/next-remoter'
11
+ const DEFAULT_LOGO_URL = 'https://ai.opentiny.design/next-remoter/svgs/logo-next-no-bg-left.svg'
11
12
 
12
13
  /** 菜单项配置接口 */
13
14
  export interface MenuItemConfig {
@@ -20,7 +21,7 @@ export interface MenuItemConfig {
20
21
  /** 菜单文字颜色 */
21
22
  active?: boolean
22
23
  /** 识别码 */
23
- know?: boolean
24
+ know?: boolean
24
25
  /** 菜单项描述 */
25
26
  desc?: string
26
27
  /** 菜单项提示 */
@@ -42,8 +43,10 @@ export interface FloatingBlockOptions {
42
43
  sessionId: string
43
44
  /** 菜单项配置 */
44
45
  menuItems?: MenuItemConfig[]
45
- /** 遥控端页面地址,默认为: https://agent.opentiny.design/tiny-robot */
46
+ /** 遥控端页面地址,默认为: https://chat.opentiny.design */
46
47
  remoteUrl?: string
48
+ /** 悬浮Logo的url地址,默认为: https://ai.opentiny.design/next-remoter/svgs/logo-next-no-bg-left.svg */
49
+ logoUrl?: string
47
50
  }
48
51
 
49
52
  // 动作类型
@@ -83,7 +86,7 @@ const getDefaultMenuItems = (options: FloatingBlockOptions): MenuItemConfig[] =>
83
86
  know: true,
84
87
  showCopyIcon: true,
85
88
  icon: scan
86
- },
89
+ }
87
90
  ]
88
91
  }
89
92
 
@@ -93,6 +96,8 @@ class FloatingBlock {
93
96
  private floatingBlock!: HTMLDivElement
94
97
  private dropdownMenu!: HTMLDivElement
95
98
  private menuItems: MenuItemConfig[]
99
+ /** 即将关闭dropdown的定时器 */
100
+ private closingTimer = 0
96
101
 
97
102
  // 计算 sessionPrefix 属性
98
103
  private get sessionPrefix(): string {
@@ -107,34 +112,37 @@ class FloatingBlock {
107
112
  this.options = {
108
113
  ...options,
109
114
  qrCodeUrl: options.qrCodeUrl || DEFAULT_QR_CODE_URL,
110
- remoteUrl: options.remoteUrl || DEFAULT_REMOTE_URL
115
+ remoteUrl: options.remoteUrl || DEFAULT_REMOTE_URL,
116
+ logoUrl: options.logoUrl || DEFAULT_LOGO_URL
111
117
  }
112
118
 
113
- // 合并默认菜单项配置和用户配置
114
- this.menuItems = this.mergeMenuItems(options.menuItems)
119
+ // 合并默认菜单项配置和用户配置。 用户不传入任何menu时, 则不自动合并默认菜单了,即不渲染任何菜单。
120
+ if (options.menuItems && options.menuItems.length === 0) {
121
+ this.menuItems = []
122
+ } else {
123
+ this.menuItems = this.mergeMenuItems(options.menuItems)
124
+ }
115
125
 
116
126
  this.init()
117
127
  }
118
128
 
119
129
  private getImageUrl = (asset: string | undefined): HTMLImageElement | undefined => {
120
- if (!asset) return;
121
- const img = new Image();
122
- img.src = asset;
123
- return img;
130
+ if (!asset) return
131
+ const img = new Image()
132
+ img.src = asset
133
+ return img
124
134
  }
125
135
 
126
136
  private renderItem = (): void => {
127
137
  this.menuItems
128
138
  .filter((item) => item.show !== false) // 过滤掉show为false的菜单项
129
- .map(
130
- (item) => {
131
- const wrapper = document.getElementById(`tiny-remoter-icon-item-${item.action}`) as HTMLDivElement;
132
- if (!wrapper) return;
133
- wrapper.innerHTML = '';
134
- const img = this.getImageUrl(item.icon);
135
- if (img) wrapper.appendChild(img);
136
- }
137
- );
139
+ .map((item) => {
140
+ const wrapper = document.getElementById(`tiny-remoter-icon-item-${item.action}`) as HTMLDivElement
141
+ if (!wrapper) return
142
+ wrapper.innerHTML = ''
143
+ const img = this.getImageUrl(item.icon)
144
+ if (img) wrapper.appendChild(img)
145
+ })
138
146
  }
139
147
 
140
148
  /**
@@ -174,7 +182,7 @@ class FloatingBlock {
174
182
  this.floatingBlock.className = 'tiny-remoter-floating-block'
175
183
  this.floatingBlock.innerHTML = `
176
184
  <div class="tiny-remoter-floating-block__icon">
177
- <img style="display: block; width: 56px;" src="${DEFAULT_QR_CODE_URL}/svgs/logo-next-no-bg-left.svg" alt="icon" />
185
+ <img style="display: block; width: 56px;" src="${this.options.logoUrl}" alt="icon" />
178
186
  </div>
179
187
  `
180
188
 
@@ -211,13 +219,14 @@ class FloatingBlock {
211
219
  <div class="tiny-remoter-dropdown-item__desc-wrapper">
212
220
  <div class="tiny-remoter-dropdown-item__desc ${item.active ? 'tiny-remoter-dropdown-item__desc--active' : ''} ${item.know ? 'tiny-remoter-dropdown-item__desc--know' : ''}">${item.desc}</div>
213
221
  <div>
214
- ${item.showCopyIcon
215
- ? `
222
+ ${
223
+ item.showCopyIcon
224
+ ? `
216
225
  <div class="tiny-remoter-copy-icon" id="${item.action}" data-action="${item.action}">
217
226
  <img src="${iconCopy}"/>
218
227
  </div>
219
228
  `
220
- : ''
229
+ : ''
221
230
  }
222
231
  </div>
223
232
  </div>
@@ -234,11 +243,33 @@ class FloatingBlock {
234
243
  this.readyTips(`remote-url`)
235
244
  }
236
245
 
237
-
238
246
  private bindEvents(): void {
239
247
  // 绑定浮动块点击事件
240
248
  this.floatingBlock.addEventListener('click', () => {
241
- this.toggleDropdown()
249
+ this.showAIChat()
250
+ })
251
+
252
+ // 浮动块悬浮处理
253
+ this.floatingBlock.addEventListener('mouseenter', () => {
254
+ this.openDropdown()
255
+
256
+ if (this.closingTimer) {
257
+ window.clearTimeout(this.closingTimer)
258
+ this.closingTimer = 0
259
+ }
260
+ })
261
+ this.floatingBlock.addEventListener('mouseleave', () => {
262
+ this.shouldCloseDropdown()
263
+ })
264
+ // 悬浮菜单进入,则阻止关闭
265
+ this.dropdownMenu.addEventListener('mouseenter', (e: Event) => {
266
+ if (this.closingTimer) {
267
+ window.clearTimeout(this.closingTimer)
268
+ this.closingTimer = 0
269
+ }
270
+ })
271
+ this.dropdownMenu.addEventListener('mouseleave', (e: Event) => {
272
+ this.shouldCloseDropdown()
242
273
  })
243
274
 
244
275
  // 绑定菜单项点击事件
@@ -280,20 +311,24 @@ class FloatingBlock {
280
311
  })
281
312
  }
282
313
 
283
- private toggleDropdown(): void {
284
- if (this.isExpanded) {
285
- this.closeDropdown()
286
- } else {
287
- this.openDropdown()
314
+ private openDropdown(): void {
315
+ // 没有menuItems,则返回
316
+ if (!this.menuItems || (this.menuItems && this.menuItems.length === 0)) {
317
+ return
288
318
  }
289
- }
290
319
 
291
- private openDropdown(): void {
292
320
  this.isExpanded = true
293
321
  this.floatingBlock.classList.add('expanded')
294
322
  this.dropdownMenu.classList.add('show')
295
323
  }
296
324
 
325
+ private shouldCloseDropdown() {
326
+ this.closingTimer = window.setTimeout(() => {
327
+ this.closeDropdown()
328
+ }, 300)
329
+ }
330
+
331
+ /** 真正的立即关闭 */
297
332
  private closeDropdown(): void {
298
333
  this.isExpanded = false
299
334
  this.floatingBlock.classList.remove('expanded')
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Web 端 Skill 公共能力模块(next-sdk)
3
+ * - 提供解析、概况、systemPrompt 拼接、按路径/名称查文档
4
+ * - 提供 createSkillTools:供 remoter 注入 get_skill_content 工具,大模型可按需加载技能文档
5
+ */
6
+
7
+ import { tool } from 'ai'
8
+ import { z } from 'zod'
9
+
10
+ /** 主 SKILL.md 路径格式:仅匹配一级子目录下的 SKILL.md,如 ./calculator/SKILL.md */
11
+ const MAIN_SKILL_PATH_REG = /^\.\/[^/]+\/SKILL\.md$/
12
+
13
+ /** 从 front matter 中提取 name 和 description 的正则(--- 与 --- 之间) */
14
+ const FRONT_MATTER_BLOCK_REG = /^---\s*\n([\s\S]+?)\s*\n---/
15
+
16
+ /** 单个技能的概况信息(从主 SKILL.md 的 front matter 提取) */
17
+ export interface SkillMeta {
18
+ /** 技能名称,与 skill 目录名一致 */
19
+ name: string
20
+ /** 技能描述,用于 systemPrompt */
21
+ description: string
22
+ /** 主 SKILL.md 相对路径,如 ./calculator/SKILL.md */
23
+ path: string
24
+ }
25
+
26
+ /**
27
+ * 从主 SKILL.md 的 YAML front matter 中用正则提取 name、description
28
+ */
29
+ export function parseSkillFrontMatter(content: string): { name: string; description: string } | null {
30
+ // 先提取 --- 之间的文本块
31
+ const blockMatch = content.match(FRONT_MATTER_BLOCK_REG)
32
+ if (!blockMatch?.[1]) return null
33
+ const block = blockMatch[1]
34
+
35
+ // 分别匹配 name 和 description 字段(支持任意顺序)
36
+ const nameMatch = block.match(/^name:\s*(.+)$/m)
37
+ const descMatch = block.match(/^description:\s*(.+)$/m)
38
+
39
+ const name = nameMatch?.[1]?.trim()
40
+ const description = descMatch?.[1]?.trim()
41
+
42
+ return name && description ? { name, description } : null
43
+ }
44
+
45
+ /**
46
+ * 获取所有「主 SKILL.md」的路径(一级子目录下的 SKILL.md)
47
+ */
48
+ export function getMainSkillPaths(modules: Record<string, string>): string[] {
49
+ return Object.keys(modules).filter((path) => MAIN_SKILL_PATH_REG.test(path))
50
+ }
51
+
52
+ /**
53
+ * 获取所有技能的概况列表(name、description、path),用于 systemPrompt 或列表展示
54
+ */
55
+ export function getSkillOverviews(modules: Record<string, string>): SkillMeta[] {
56
+ const mainPaths = getMainSkillPaths(modules)
57
+ const list: SkillMeta[] = []
58
+ for (const path of mainPaths) {
59
+ const content = modules[path]
60
+ if (!content) continue
61
+ const parsed = parseSkillFrontMatter(content)
62
+ if (!parsed) continue
63
+ list.push({
64
+ name: parsed.name,
65
+ description: parsed.description,
66
+ path
67
+ })
68
+ }
69
+ return list
70
+ }
71
+
72
+ /**
73
+ * 格式化为大模型 systemPrompt 可用的技能说明文本
74
+ * @param skills 不传则需由调用方传入从 getSkillOverviews 得到的结果
75
+ */
76
+ export function formatSkillsForSystemPrompt(skills: SkillMeta[]): string {
77
+ if (skills.length === 0) return ''
78
+ const lines = skills.map((s) => `- **${s.name}**: ${s.description}`)
79
+ return `## 可用技能\n\n${lines.join('\n')}\n\n当需要用到某技能时,请使用 get_skill_content 工具获取该技能的完整文档内容。`
80
+ }
81
+
82
+ /**
83
+ * 获取所有已加载的 md 文件路径(含主 SKILL.md 与 reference 等)
84
+ */
85
+ export function getSkillMdPaths(modules: Record<string, string>): string[] {
86
+ return Object.keys(modules)
87
+ }
88
+
89
+ /**
90
+ * 根据相对路径获取某个 md 文档的原始内容
91
+ */
92
+ export function getSkillMdContent(modules: Record<string, string>, path: string): string | undefined {
93
+ return modules[path]
94
+ }
95
+
96
+ /**
97
+ * 根据技能 name 查找其主 SKILL.md 的路径(name 与目录名一致)
98
+ */
99
+ export function getMainSkillPathByName(modules: Record<string, string>, name: string): string | undefined {
100
+ return getMainSkillPaths(modules).find((p) => p.startsWith(`./${name}/SKILL.md`))
101
+ }
102
+
103
+ // ============ 内置工具:供 remoter 注入,替代业界 skill 中「读取文档」的操作 ============
104
+
105
+ /** AI SDK Tool 类型,用于 extraTools 合并,不写死泛型避免与 ai 包版本强绑定 */
106
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
+ export type SkillToolsSet = Record<string, any>
108
+
109
+ /**
110
+ * 根据 skillMdModules 创建供 AI 调用的工具集
111
+ * - get_skill_content: 按技能名或路径获取完整文档内容,便于大模型自动识别并加载技能
112
+ * remoter 可将返回的 tools 合并进 extraTools 注入 agent
113
+ */
114
+ export function createSkillTools(modules: Record<string, string>): SkillToolsSet {
115
+ const getSkillContent = tool({
116
+ description:
117
+ '根据技能名称或文档路径获取该技能的完整 Markdown 文档内容。传入 skillName(如 calculator)或 path(如 ./calculator/SKILL.md)',
118
+ inputSchema: z.object({
119
+ skillName: z.string().optional().describe('技能名称,与目录名一致,如 calculator'),
120
+ path: z.string().optional().describe('文档相对路径,如 ./calculator/SKILL.md 或 ./product-guide/reference/xxx.md')
121
+ }),
122
+ execute: (args: { skillName?: string; path?: string }) => {
123
+ const { skillName, path: pathArg } = args
124
+ let content: string | undefined
125
+ if (pathArg) {
126
+ content = getSkillMdContent(modules, pathArg)
127
+ } else if (skillName) {
128
+ const mainPath = getMainSkillPathByName(modules, skillName)
129
+ content = mainPath ? getSkillMdContent(modules, mainPath) : undefined
130
+ }
131
+ if (content === undefined) {
132
+ return { error: '未找到对应技能文档', skillName: skillName ?? pathArg }
133
+ }
134
+ return { content, path: pathArg ?? getMainSkillPathByName(modules, skillName!) }
135
+ }
136
+ })
137
+
138
+ return {
139
+ get_skill_content: getSkillContent
140
+ }
141
+ }