xiaoyuan-assistant 0.5.45

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.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * 小园内置浏览器/页面能力。
3
+ * 这些能力不依赖 DOM,AI 识别到对应 function 后由 SDK 直接执行。
4
+ */
5
+
6
+ function nextFrame() {
7
+ return new Promise((resolve) => {
8
+ if (typeof requestAnimationFrame === 'function') {
9
+ requestAnimationFrame(() => resolve())
10
+ } else {
11
+ setTimeout(resolve, 0)
12
+ }
13
+ })
14
+ }
15
+
16
+ function waitForPopState(timeout = 8000) {
17
+ if (typeof window === 'undefined') return Promise.resolve(false)
18
+
19
+ return new Promise((resolve) => {
20
+ let settled = false
21
+ const finish = (changed) => {
22
+ if (settled) return
23
+ settled = true
24
+ window.removeEventListener('popstate', onPopState)
25
+ window.removeEventListener('pageshow', onPageShow)
26
+ clearTimeout(timer)
27
+ resolve(changed)
28
+ }
29
+
30
+ const onPopState = () => finish(true)
31
+ const onPageShow = () => finish(true)
32
+ const timer = setTimeout(() => finish(false), timeout)
33
+
34
+ window.addEventListener('popstate', onPopState, { once: true })
35
+ window.addEventListener('pageshow', onPageShow, { once: true })
36
+ })
37
+ }
38
+
39
+ function buildConsoleUrl() {
40
+ if (typeof window === 'undefined' || typeof location === 'undefined') {
41
+ return ''
42
+ }
43
+
44
+ const url = new URL(window.location.href)
45
+ url.searchParams.set('console', '')
46
+ // 保持用户期望的 ?console / &console 形式,而不是 ?console=。
47
+ return url.toString().replace(/([?&])console=/, '$1console')
48
+ }
49
+
50
+ export function getBuiltinFunctions() {
51
+ return [
52
+ {
53
+ name: 'refreshPage',
54
+ description: '刷新当前大屏页面,重新加载当前页面',
55
+ params: {},
56
+ handler: async () => {
57
+ if (typeof window === 'undefined') {
58
+ return { success: false, message: '当前环境无法刷新页面' }
59
+ }
60
+
61
+ // 让本次方法调用有机会返回给小园执行器,再刷新页面。
62
+ setTimeout(() => window.location.reload(), 80)
63
+ return {
64
+ success: true,
65
+ action: 'refreshPage',
66
+ message: '已刷新当前页面。'
67
+ }
68
+ }
69
+ },
70
+ {
71
+ name: 'goBack',
72
+ description: '返回浏览历史中的上一个页面',
73
+ params: {},
74
+ handler: async () => {
75
+ if (typeof window === 'undefined' || !window.history) {
76
+ return { success: false, message: '当前环境无法返回上一页面' }
77
+ }
78
+
79
+ const wait = waitForPopState()
80
+ window.history.back()
81
+ const changed = await wait
82
+ return {
83
+ success: true,
84
+ action: 'goBack',
85
+ changed,
86
+ message: changed ? '已返回上一页面。' : '已发起返回上一页面操作。'
87
+ }
88
+ }
89
+ },
90
+ {
91
+ name: 'goForward',
92
+ description: '前往浏览历史中的下一个页面',
93
+ params: {},
94
+ handler: async () => {
95
+ if (typeof window === 'undefined' || !window.history) {
96
+ return { success: false, message: '当前环境无法前往下一页面' }
97
+ }
98
+
99
+ const wait = waitForPopState()
100
+ window.history.forward()
101
+ const changed = await wait
102
+ return {
103
+ success: true,
104
+ action: 'goForward',
105
+ changed,
106
+ message: changed ? '已前往下一页面。' : '已发起前往下一页面操作。'
107
+ }
108
+ }
109
+ },
110
+ {
111
+ name: 'openDataCenter',
112
+ description: '打开当前大屏对应的数据中台,在当前地址后追加 console 参数并重新加载页面',
113
+ params: {},
114
+ handler: async () => {
115
+ if (typeof window === 'undefined') {
116
+ return { success: false, message: '当前环境无法打开数据中台' }
117
+ }
118
+
119
+ const url = buildConsoleUrl()
120
+ if (!url) {
121
+ return { success: false, message: '无法生成数据中台地址' }
122
+ }
123
+
124
+ setTimeout(() => {
125
+ window.location.href = url
126
+ }, 80)
127
+
128
+ return {
129
+ success: true,
130
+ action: 'openDataCenter',
131
+ url,
132
+ message: '已打开数据中台。'
133
+ }
134
+ }
135
+ },
136
+ {
137
+ name: 'closeDataCenter',
138
+ description: '关闭当前数据中台模式,从当前地址移除 console 参数并重新加载页面',
139
+ params: {},
140
+ handler: async () => {
141
+ if (typeof window === 'undefined' || typeof location === 'undefined') {
142
+ return { success: false, message: '当前环境无法关闭数据中台' }
143
+ }
144
+
145
+ const url = new URL(window.location.href)
146
+ url.searchParams.delete('console')
147
+
148
+ setTimeout(() => {
149
+ window.location.href = url.toString()
150
+ }, 80)
151
+
152
+ return {
153
+ success: true,
154
+ action: 'closeDataCenter',
155
+ url: url.toString(),
156
+ message: '已关闭数据中台。'
157
+ }
158
+ }
159
+ },
160
+ {
161
+ name: 'scrollPageTop',
162
+ description: '将当前页面滚动到顶部',
163
+ params: {},
164
+ handler: async () => {
165
+ if (typeof window === 'undefined') return { success: false, message: '当前环境无法滚动页面' }
166
+ window.scrollTo({ top: 0, behavior: 'smooth' })
167
+ await nextFrame()
168
+ return { success: true, action: 'scrollPageTop', message: '已滚动到页面顶部。' }
169
+ }
170
+ },
171
+ {
172
+ name: 'scrollPageBottom',
173
+ description: '将当前页面滚动到底部',
174
+ params: {},
175
+ handler: async () => {
176
+ if (typeof window === 'undefined') return { success: false, message: '当前环境无法滚动页面' }
177
+ window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' })
178
+ await nextFrame()
179
+ return { success: true, action: 'scrollPageBottom', message: '已滚动到页面底部。' }
180
+ }
181
+ }
182
+ ]
183
+ }
184
+
185
+ export function registerBuiltinFunctions(registry) {
186
+ const unregisters = getBuiltinFunctions().map((item) => registry.registerFunction(item))
187
+ return () => unregisters.forEach((unregister) => unregister?.())
188
+ }
@@ -0,0 +1,156 @@
1
+ function escape(value) {
2
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(String(value))
3
+ return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\$&')
4
+ }
5
+
6
+ function parseParam(value) {
7
+ if (value == null || value === '') return null
8
+ const raw = String(value)
9
+ try { return JSON.parse(raw) } catch (_) { return raw }
10
+ }
11
+
12
+ /**
13
+ * 精确定位 AI DOM:
14
+ * data-ai-function = 操作名
15
+ * data-ai-param = 当前这个 DOM 对应的目标参数
16
+ */
17
+ export function findAiElement(name, params = {}) {
18
+ const functionName = String(name || '').trim()
19
+ if (!functionName) return null
20
+
21
+ const value = params?.value ?? params?.param ?? params?.target ?? params?.id ?? params?.aiId ?? params?.name ?? params?.aiName
22
+ const base = `[data-ai-function="${escape(functionName)}"]`
23
+
24
+ if (value !== undefined && value !== null && value !== '') {
25
+ const rawValue = typeof value === 'object' ? JSON.stringify(value) : String(value)
26
+ const direct = document.querySelector(`${base}[data-ai-param="${escape(rawValue)}"]`)
27
+ if (direct) return direct
28
+
29
+ if (typeof value === 'object') {
30
+ const candidates = document.querySelectorAll(base)
31
+ const target = JSON.stringify(value)
32
+ for (const el of candidates) {
33
+ if (JSON.stringify(parseParam(el.dataset.aiParam)) === target) return el
34
+ }
35
+ }
36
+ return null
37
+ }
38
+
39
+ const only = document.querySelectorAll(base)
40
+ return only.length === 1 ? only[0] : null
41
+ }
42
+
43
+ /**
44
+ * 为 Planner 提供紧凑的 DOM 能力清单。
45
+ * 同名 Function 聚合 params,避免把整棵 DOM 树重复发送给模型。
46
+ */
47
+ export function scanAiElements() {
48
+ const map = new Map()
49
+ const elements = document.querySelectorAll('[data-ai-function]')
50
+
51
+ for (const el of elements) {
52
+ const name = String(el.dataset.aiFunction || '').trim()
53
+ if (!name) continue
54
+
55
+ const item = map.get(name) || {
56
+ name,
57
+ description: el.dataset.aiDescription || '',
58
+ params: []
59
+ }
60
+
61
+ if (!item.description && el.dataset.aiDescription) {
62
+ item.description = el.dataset.aiDescription
63
+ }
64
+
65
+ const param = parseParam(el.dataset.aiParam)
66
+ const key = typeof param === 'object' ? JSON.stringify(param) : String(param ?? '')
67
+ if (param !== null && !item.params.some((v) => String(v.key) === key)) {
68
+ item.params.push({
69
+ key,
70
+ value: param
71
+ })
72
+ }
73
+
74
+ map.set(name, item)
75
+ }
76
+
77
+ return [...map.values()].map((item) => ({
78
+ name: item.name,
79
+ description: String(item.description || '').slice(0, 180),
80
+ params: item.params.slice(0, 80).map((v) => v.value)
81
+ }))
82
+ }
83
+
84
+ function waitForDocumentReady() {
85
+ if (document.readyState === 'complete' || document.readyState === 'interactive') return Promise.resolve()
86
+ return new Promise((resolve) => {
87
+ const done = () => {
88
+ document.removeEventListener('DOMContentLoaded', done)
89
+ window.removeEventListener('load', done)
90
+ resolve()
91
+ }
92
+ document.addEventListener('DOMContentLoaded', done, { once: true })
93
+ window.addEventListener('load', done, { once: true })
94
+ })
95
+ }
96
+
97
+ /**
98
+ * 等待精确 Function + Param 出现在当前页面。
99
+ * 首次直接 querySelector,只有不存在时才启用 MutationObserver / 低频轮询。
100
+ */
101
+ export async function waitForAiElement(name, params = {}, options = {}) {
102
+ const timeout = Math.max(0, Number(options.timeout ?? 6000))
103
+ const interval = Math.max(50, Number(options.interval ?? 120))
104
+ await waitForDocumentReady()
105
+
106
+ const immediate = findAiElement(name, params)
107
+ if (immediate || timeout === 0) return immediate || null
108
+
109
+ return new Promise((resolve) => {
110
+ let finished = false
111
+ let timer = null
112
+ let poll = null
113
+ let observer = null
114
+
115
+ const finish = (el) => {
116
+ if (finished) return
117
+ finished = true
118
+ if (timer) clearTimeout(timer)
119
+ if (poll) clearInterval(poll)
120
+ if (observer) observer.disconnect()
121
+ resolve(el || null)
122
+ }
123
+
124
+ const check = () => {
125
+ const el = findAiElement(name, params)
126
+ if (el) finish(el)
127
+ }
128
+
129
+ const root = document.documentElement || document.body
130
+ if (typeof MutationObserver !== 'undefined' && root) {
131
+ observer = new MutationObserver(check)
132
+ observer.observe(root, {
133
+ subtree: true,
134
+ childList: true,
135
+ attributes: true,
136
+ attributeFilter: ['data-ai-function', 'data-ai-param']
137
+ })
138
+ }
139
+
140
+ poll = window.setInterval(check, interval)
141
+ timer = window.setTimeout(() => finish(null), timeout)
142
+ check()
143
+ })
144
+ }
145
+
146
+ export async function waitForRender(frames = 1, delay = 20) {
147
+ let count = Math.max(0, Number(frames))
148
+ while (count-- > 0) {
149
+ if (typeof requestAnimationFrame === 'function') {
150
+ await new Promise((resolve) => requestAnimationFrame(resolve))
151
+ } else {
152
+ await new Promise((resolve) => setTimeout(resolve, 0))
153
+ }
154
+ }
155
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay))
156
+ }