@opentiny/next-sdk 0.3.2 → 0.3.3-alpha.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 (47) hide show
  1. package/WebMcp.ts +6 -0
  2. package/core.ts +1 -0
  3. package/dist/SimulatorMask-BHVXyogh-BFEGpD5S.js +1048 -0
  4. package/dist/SimulatorMask-BHVXyogh-CCYbrb84.js +801 -0
  5. package/dist/WebMcp.d.ts +3 -0
  6. package/dist/core.d.ts +1 -0
  7. package/dist/core.js +16 -15
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.es.dev.js +8201 -2640
  10. package/dist/index.es.js +21518 -16896
  11. package/dist/index.js +4033 -1847
  12. package/dist/index.umd.dev.js +9557 -3051
  13. package/dist/index.umd.js +500 -66
  14. package/dist/initialize-builtin-WebMCP-HgObT902.js +6279 -0
  15. package/dist/mcpsdk@1.25.3.dev.js +253 -94
  16. package/dist/mcpsdk@1.25.3.es.dev.js +253 -94
  17. package/dist/mcpsdk@1.25.3.es.js +5689 -5531
  18. package/dist/mcpsdk@1.25.3.js +32 -27
  19. package/dist/page-tools/a11y-tree.d.ts +103 -0
  20. package/dist/page-tools/bridge.d.ts +0 -6
  21. package/dist/page-tools/initialize-builtin-WebMCP.d.ts +1 -0
  22. package/dist/page-tools/page-agent-tool.d.ts +9 -0
  23. package/dist/page-tools/page-state-cache.d.ts +36 -0
  24. package/dist/webagent.dev.js +3614 -1880
  25. package/dist/webagent.es.dev.js +3370 -1532
  26. package/dist/webagent.es.js +15958 -14259
  27. package/dist/webagent.js +74 -66
  28. package/dist/webmcp-full.dev.js +7625 -719
  29. package/dist/webmcp-full.es.dev.js +6437 -580
  30. package/dist/webmcp-full.es.js +12146 -7590
  31. package/dist/webmcp-full.js +631 -29
  32. package/dist/webmcp.dev.js +8002 -25
  33. package/dist/webmcp.es.dev.js +6932 -3
  34. package/dist/webmcp.es.js +6180 -859
  35. package/dist/webmcp.js +602 -1
  36. package/index.ts +2 -14
  37. package/package.json +11 -4
  38. package/page-tools/a11y-tree.ts +532 -0
  39. package/page-tools/bridge.ts +48 -14
  40. package/page-tools/initialize-builtin-WebMCP.ts +20 -0
  41. package/page-tools/page-agent-prompt.md +123 -0
  42. package/page-tools/page-agent-tool.ts +223 -0
  43. package/page-tools/page-state-cache.ts +78 -0
  44. package/remoter/createRemoter.ts +2 -1
  45. package/remoter/svgs/logo.svg +45 -0
  46. package/vite-env.d.ts +5 -0
  47. package/dist/AgentModelProvider-t6BqhAGn.js +0 -4194
@@ -0,0 +1,20 @@
1
+ import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill'
2
+ import { setupModelContextBridge } from './bridge'
3
+ import { isBrowser } from '../utils/env'
4
+
5
+ let initialized = false
6
+
7
+ export const initializeBuiltinWebMCP = () => {
8
+ if (isBrowser()) {
9
+ try {
10
+ if (initialized) return
11
+
12
+ initializeWebMCPPolyfill()
13
+ setupModelContextBridge()
14
+
15
+ initialized = true
16
+ } catch (err) {
17
+ console.warn('[next-sdk] 自动注入 modelContext polyfill 和桥接同步失败:', err)
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,123 @@
1
+ ## 角色
2
+
3
+ 你是浏览器页面的操作工具,你拥有识别网页内容,点击,填写,选择下拉框和滚动页面等功能。
4
+
5
+ 在执行页面操作之前,你必须先调用`browserState`工具一次,根据最新的网页状态,再调用操作动作,比如 'click','fill','select' 等动作。
6
+
7
+ <intro>
8
+ 你擅长以下任务:
9
+ 1. 导航复杂网站并提取精确信息
10
+ 2. 自动化表单提交和交互式网页操作
11
+ 3. 收集和保存信息
12
+ 4. 在代理循环中有效运行
13
+ 5. 高效执行多样化的网页任务
14
+ </intro>
15
+
16
+ <input>
17
+ 在每一步,你的输入将包括:
18
+ 1. <agent_history>:一个按时间顺序排列的事件流,包括你之前的操作及其结果。
19
+ 2. <agent_state>:当前的 <user_request> 和 <step_info>。
20
+ 3. <browser_state>:当前 URL、页面标题、可交互元素总数以及语义化 YAML 树。
21
+ </input>
22
+
23
+ <browser_state>
24
+
25
+ 浏览器状态包含以下内容:
26
+
27
+ - url: 当前查看页面的 URL。
28
+ - title: 当前查看页面的标题。
29
+ - 可交互元素总数:带有 `[ref=N]` 标记的元素数量。
30
+ - YAML 树:页面的语义化结构,格式如下:
31
+
32
+ ```yaml
33
+ - region:
34
+ - main:
35
+ - button [cursor=pointer] [ref=9]: 产品文档
36
+ - button [selected] [cursor=pointer] [ref=47]: 40元/月 2核CPU 2GB内存
37
+ - radio [checked] [cursor=pointer] [ref=53]: 自动生成密码
38
+ - button [cursor=pointer] [ref=74]: 立即购买
39
+ ```
40
+
41
+ 节点格式说明:
42
+ - `- role [state1] [state2] [ref=N]: accessible name`
43
+ - `role`:ARIA 语义角色(如 button/link/radio/heading/list/listitem 等)
44
+ - `[state]`:可选状态标记,如 `[checked]` `[selected]` `[disabled]` `[expanded]` `[cursor=pointer]`
45
+ - `[ref=N]`:可交互元素的唯一操作索引,**只有带 ref 的节点才能被操作**
46
+ - `accessible name`:元素的语义化名称(通过 aria-label/aria-labelledby/innerText 等计算得出)
47
+ - 缩进表示父子关系
48
+
49
+ </browser_state>
50
+
51
+ <browser_state_diff>
52
+
53
+ 在交互过程中,为了提高效率和减少上下文占用,工具会生成页面的增量差异(Diff)信息。
54
+ Diff 格式示例如下:
55
+ ```diff
56
+ - button [ref=9]: 产品文档
57
+ + button [ref=9]: 产品文档 (已点击)
58
+ ```
59
+ 或者,当页面结构发生改变时,会展示新增或移除的节点差异。
60
+
61
+ ### 页面状态获取四原则(重要,请严格遵守):
62
+
63
+ > 与业界 AI 编辑器(Cursor / Windsurf)按需读取文件而非全量加载的理念完全一致——**先精准搜索,再按需拉取全量**,将发送给大模型的 token 降至最低。
64
+
65
+ 1. **按关键词精准搜索(最高优先级)**:当你已知要找什么类型的元素(如按钮、输入框、特定名称的链接)时,**优先使用 `searchTree` 动作**,传入 role 名称、元素文本或状态关键词搜索。它只返回命中行和上下文,token 消耗比全量树减少 80%+ 。
66
+ - 示例:查找所有按钮 → `{"action": "searchTree", "query": "button"}`
67
+ - 示例:查找含"提交"文本的节点 → `{"action": "searchTree", "query": "提交"}`
68
+ - 示例:查找已勾选的复选框 → `{"action": "searchTree", "query": "checked"}`
69
+ - 示例:精确定位某个 ref → `{"action": "searchTree", "query": "#5"}`
70
+ 2. **首次获取全量**:首次进入页面、页面发生重大刷新、或 `searchTree` 无法找到所需信息时,调用 `browserState` 并指定 `responseMode` 为 `full` 或 `both` 获取完整 A11y 树。
71
+ 3. **增量优先**:执行 `click`、`fill`、`select`、`scroll` 操作后,工具默认自动返回 `diff` 增量信息。优先阅读这些 Diff,以便快速确认操作是否生效。
72
+ 4. **按需拉取全量**:如果增量 Diff 不足以支持下一步操作,或需要寻找不在 Diff 中的 `[ref=N]` 节点,且 `searchTree` 也无法定位时,再显式调用 `browserState` 拉取完整树。
73
+
74
+ </browser_state_diff>
75
+
76
+
77
+
78
+ <browser_rules>
79
+ 在使用浏览器和浏览网页时严格遵守以下规则:
80
+
81
+ - 仅与分配有 `[ref=N]` 的元素进行交互。
82
+ - 仅使用明确提供的 ref 索引。
83
+ - 每次操作后 ref 索引会重新分配,不要使用旧的 ref 索引。
84
+ - 如果页面在执行操作后发生变化(例如输入文本操作),分析是否需要与新元素进行交互,例如从列表中选择正确的选项。
85
+ - 默认情况下,仅列出可见视口中的元素。如果你怀疑需要交互的相关内容在屏幕外,请使用滚动操作。仅在页面下方或上方还有更多像素时才滚动。
86
+ - 你可以使用 num_pages 参数滚动特定页数(例如,0.5 表示半页,2.0 表示两页)。
87
+ - 如果出现验证码,告诉用户你无法解决验证码。完成任务并请用户解决它。
88
+ - 如果缺少预期元素,尝试滚动或导航回退。
89
+ - 除非某些条件发生变化,否则不要重复同一操作超过 3 次。
90
+ - 如果你向输入字段填充文本且操作序列被中断,通常意味着某些内容发生了变化,例如下拉建议弹出。
91
+ - 如果 <user_request> 包含特定的页面信息,如产品类型、评分、价格、位置等,尝试应用过滤器以提高效率。
92
+ - <user_request> 是最终目标。如果用户指定了明确的步骤,它们始终具有最高优先级。
93
+ - 如果你向字段输入文本,可能需要按回车键、点击搜索按钮或从下拉列表中选择以完成操作。
94
+ - 如果不需要,不要登录页面。如果没有凭据,不要登录。
95
+ - 有两种类型的任务,首先思考你正在处理哪种类型的请求:
96
+
97
+ 1. 非常具体的逐步说明:
98
+
99
+ - 非常精确地遵循它们,不要跳过步骤。尝试按要求完成所有内容。
100
+
101
+ 2. 开放式任务。自己规划,在实现目标时保持创造性。
102
+
103
+ - 如果在开放式任务中陷入困境(例如登录或验证码),可以重新评估任务并尝试替代方法,例如有时即使页面的某些部分可访问或通过 Web 搜索获取一些信息,也会意外弹出登录框。
104
+ </browser_rules>
105
+
106
+ <capability>
107
+ - 你只能处理单页应用。不要跳出当前页面。
108
+ - 如果链接将在新页面中打开(例如,<a target="_blank">),不要点击该链接。
109
+ - 任务失败是可以接受的。
110
+ - 用户可能是错的。如果用户的请求无法实现、不适当,或者你没有足够的信息或工具来实现它。告诉用户提出更好的请求。
111
+ - 网页可能已损坏。所有网页或应用都有 bug。有些 bug 会使你的工作变得困难。鼓励你告诉用户当前页面的问题。你的反馈(包括失败)对用户很有价值。
112
+ - 过于努力可能有害。来回重复某些操作或在知识有限的情况下推动复杂程序可能导致不良结果和有害副作用。用户宁愿你以失败告终完成任务。
113
+ - 如果你对当前网页或任务没有相关知识。你必须要求用户提供具体说明和详细步骤。
114
+ </capability>
115
+
116
+ <task_completion_rules>
117
+ 你必须在以下情况时结束任务:
118
+
119
+ - 当你完全完成用户请求时。
120
+ - 当你感到困惑或无法解决用户请求时。或者用户请求不清楚或包含不当内容。
121
+ - 如果绝对不可能继续。
122
+
123
+ </task_completion_rules>
@@ -0,0 +1,223 @@
1
+ import { initializeBuiltinWebMCP } from './initialize-builtin-WebMCP'
2
+ import { z } from 'zod'
3
+ import { zodToJsonSchema } from 'zod-to-json-schema'
4
+ import pageAgentPrompt from './page-agent-prompt.md?raw'
5
+ import { PageController, clickElement, inputTextElement, selectOptionElement } from '@page-agent/page-controller'
6
+ import { buildA11yTree, searchA11yTree, type RefMap } from './a11y-tree'
7
+ import { PageStateCache } from './page-state-cache'
8
+
9
+ declare global {
10
+ interface Window {
11
+ __webmcpcli_interactiveWhitelist?: Element[]
12
+ __webmcpcli_interactiveBlacklist?: Element[]
13
+ __webmcpcli_beforeGetBrowserState?: (() => void) | null
14
+ }
15
+ }
16
+
17
+ /** 在浏览器页面中注册 page-agent-tool, 用于页面的内容获取和操作,页面的动效 */
18
+ export function registerPageAgentTool() {
19
+ initializeBuiltinWebMCP()
20
+
21
+ window.__webmcpcli_interactiveWhitelist = [] // 白名单元素列表,存在则识别为交互元素
22
+ window.__webmcpcli_interactiveBlacklist = [] // 黑名单,反之
23
+ window.__webmcpcli_beforeGetBrowserState = null // 指定网站覆盖该函数,用于设置当前网站的黑白名单
24
+
25
+ // 保留 PageController 仅用于 showMask/hideMask(UX 遮罩层)
26
+ const pageController = new PageController({ enableMask: true })
27
+
28
+ // ─── 状态 Diff 缓存
29
+ const stateCache = new PageStateCache()
30
+
31
+ // 当前 ref 索引 → HTMLElement 映射(每次 buildA11yTree 后更新)
32
+ let currentRefMap: RefMap = new Map()
33
+
34
+ // ─── inputSchema 与原版完全一致(对外 API 不变)──────────────────────────
35
+ const inputSchema = z.object({
36
+ action: z.enum(['browserState', 'click', 'fill', 'select', 'scroll', 'executeJavascript', 'searchTree'] as const)
37
+ .describe(`执行的动作名称, 每一次执行 'click', 'fill', 'select'动作之前,**必须**要先调用 'browserState' 去获取页面的最新状态。
38
+ browserState: '查询整个页面的浏览器状态;返回页面的标题、URL、YAML格式的语义化页面树',
39
+ click: '根据元素索引点击;',
40
+ fill: '根据元素索引填写文本;';
41
+ select: '根据元素索引选择下拉框选项;';
42
+ scroll: '滚动页面的动作,可以指定水平滚动还是上下滚动; 不带元素索引时:滚动整个文档。带元素索引时:滚动该索引处的容器(或其最近的可滚动祖先元素)'
43
+ executeJavascript: '执行javascript代码'
44
+ searchTree: '按关键词搜索无障碍树,返回带行号的匹配节点及上下文,无需获取全量树。适合快速定位特定元素(如所有按钮、特定名称的链接等),显著减少上下文消耗。必须提供 query 参数。'
45
+ `),
46
+ index: z.number().min(0).optional().describe('执行动作 of the element index, 动作为 click,fill,select时,必须提供元素索引'),
47
+ text: z.string().optional().describe('执行动作的文本内容, 动作为 fill,select 时,必须提供文本内容'),
48
+ down: z.boolean().optional().describe('执行上下滚动时,必须提供down参数'),
49
+ right: z.boolean().optional().describe('执行水平滚动方向, 必须提供right参数'),
50
+ numPages: z
51
+ .number()
52
+ .optional()
53
+ .describe('执行动作的滚动页数, 动作为 scroll时,可以提供滚动页数,建议每次滚动0.1页,该值不要大于5.'),
54
+ pixels: z.number().int().min(0).optional().describe('执行动作的滚动像素数,动作为 scroll时,可以提供滚动像素数'),
55
+ script: z.string().optional().describe('执行的javascript代码,动作为 executeJavascript时,必须提供script参数'),
56
+ query: z.string().optional().describe('搜索关键词,动作为 searchTree 时必须提供。支持按 role(如 button、link)、节点名称、状态(如 checked)或 ref 索引(如 #3)搜索'),
57
+ contextLines: z.number().int().min(0).max(10).default(2).describe('searchTree 时每个命中行前后保留的上下文行数,默认 2'),
58
+ maxMatches: z.number().int().min(1).max(50).default(20).describe('searchTree 时最大返回分组数,默认 20'),
59
+ responseMode: z.enum(['full', 'diff', 'both'] as const)
60
+ .optional()
61
+ .default('diff')
62
+ .describe('返回浏览器状态的模式。full: 仅返回当前全量 A11y 树;diff: 仅返回与上一次状态的增量差异;both: 同时返回全量 A11y 树与增量差异。默认值为 diff。')
63
+ })
64
+
65
+ // ─── 辅助:构建错误响应 ───────────────────────────────────────────────────
66
+ async function errContent(msg: string) {
67
+ await pageController.hideMask()
68
+ return { content: [{ type: 'text', text: msg }] }
69
+ }
70
+
71
+ // ─── 核心:构建 browserState 响应(全量 or 增量 Diff)────────────────────
72
+ async function buildBrowserStateResponse(responseMode: 'full' | 'diff' | 'both' = 'diff'): Promise<{ content: Array<{ type: string; text: string }> }> {
73
+ const url = window.location.href
74
+ const title = document.title
75
+
76
+ // 获取用户自定义黑名单与白名单
77
+ const blacklist = (window.__webmcpcli_interactiveBlacklist ?? []) as Element[]
78
+ const whitelist = (window.__webmcpcli_interactiveWhitelist ?? []) as Element[]
79
+
80
+ // 生成语义化 ARIA YAML 树 + 刷新 refMap
81
+ const { yaml, refMap } = buildA11yTree(document.body, blacklist, whitelist)
82
+ currentRefMap = refMap
83
+
84
+ // 计算 Diff
85
+ const diff = stateCache.update(url, yaml)
86
+
87
+ await pageController.hideMask()
88
+
89
+ // 根据 responseMode 组装 content
90
+ let displayContent = ''
91
+ if (responseMode === 'full') {
92
+ displayContent = yaml
93
+ } else if (responseMode === 'diff') {
94
+ displayContent = diff.isFullRefresh ? yaml : diff.diffText
95
+ } else if (responseMode === 'both') {
96
+ displayContent = `【全量页面树】:\n${yaml}\n\n【增量差异】:\n${diff.isFullRefresh ? '(首次/刷新,无增量差异)' : diff.diffText}`
97
+ }
98
+
99
+ // 拼装 JSON 格式状态,与 webmcp-cli 的 state 提取逻辑对齐
100
+ const stateObj = {
101
+ url,
102
+ title,
103
+ content: displayContent
104
+ }
105
+ const text = `浏览器状态: ${JSON.stringify(stateObj)}`
106
+ return { content: [{ type: 'text', text }] }
107
+ }
108
+
109
+ // ─── 工具注册(名称与 inputSchema 与原版完全一致)────────────────────────
110
+ ;(document as any).modelContext.registerTool({
111
+ name: 'page-agent-tool',
112
+ description: pageAgentPrompt,
113
+ // @ts-ignore
114
+ inputSchema: zodToJsonSchema(inputSchema) as any,
115
+ async execute(args: any) {
116
+ await pageController.showMask()
117
+ const mode = args.responseMode ?? 'diff'
118
+ try {
119
+ // ── browserState:生成语义化 YAML + Diff ──────────────────────────
120
+ if (args.action === 'browserState') {
121
+ if (window.__webmcpcli_beforeGetBrowserState) {
122
+ window.__webmcpcli_beforeGetBrowserState()
123
+ }
124
+ return buildBrowserStateResponse(mode)
125
+
126
+ // ── click:用底层 clickElement(el) 操作,操作后自动返回 diff ───────
127
+ } else if (args.action === 'click') {
128
+ if (args.index === undefined) return errContent('点击结果: 缺少元素索引')
129
+ const el = currentRefMap.get(args.index)
130
+ if (!el) return errContent(`点击结果: 无效的 ref 索引 ${args.index},请先调用 browserState 刷新页面状态`)
131
+ await clickElement(el)
132
+ // 操作成功后自动返回 diff/both/full
133
+ return buildBrowserStateResponse(mode)
134
+
135
+ // ── fill:输入文本,操作后自动返回 diff ───────────────────────────
136
+ } else if (args.action === 'fill') {
137
+ if (args.index === undefined || !args.text) return errContent('填写结果: 缺少元素索引或文本内容')
138
+ const el = currentRefMap.get(args.index)
139
+ if (!el) return errContent(`填写结果: 无效的 ref 索引 ${args.index},请先调用 browserState 刷新页面状态`)
140
+
141
+ let targetEl = el
142
+ if (!(targetEl instanceof HTMLInputElement) && !(targetEl instanceof HTMLTextAreaElement)) {
143
+ const innerInput = el.querySelector('input, textarea')
144
+ if (innerInput) {
145
+ targetEl = innerInput as HTMLElement
146
+ }
147
+ }
148
+
149
+ if (targetEl instanceof HTMLInputElement && targetEl.readOnly) {
150
+ targetEl.value = args.text
151
+ targetEl.dispatchEvent(new Event('input', { bubbles: true }))
152
+ targetEl.dispatchEvent(new Event('change', { bubbles: true }))
153
+ targetEl.dispatchEvent(new Event('blur', { bubbles: true }))
154
+ } else {
155
+ await inputTextElement(targetEl, args.text)
156
+ }
157
+ return buildBrowserStateResponse(mode)
158
+
159
+ // ── select:选择下拉框,操作后自动返回 diff ───────────────────────
160
+ } else if (args.action === 'select') {
161
+ if (args.index === undefined || !args.text) return errContent('选择结果: 缺少元素索引或文本内容')
162
+ const el = currentRefMap.get(args.index) as HTMLSelectElement
163
+ if (!el) return errContent(`选择结果: 无效的 ref 索引 ${args.index},请先调用 browserState 刷新页面状态`)
164
+ await selectOptionElement(el, args.text)
165
+ return buildBrowserStateResponse(mode)
166
+
167
+ // ── scroll:滚动页面,操作后自动返回 diff ─────────────────────────
168
+ } else if (args.action === 'scroll') {
169
+ if (!args.down && !args.right) return errContent('滚动结果: 缺少滚动方向参数')
170
+
171
+ // 确定滚动目标(有 index 时滚动该元素容器,否则滚动整个文档)
172
+ const scrollTarget = args.index !== undefined
173
+ ? (currentRefMap.get(args.index) ?? window)
174
+ : window
175
+
176
+ if (args.right) {
177
+ const pixels = args.pixels ?? 300
178
+ scrollTarget.scrollBy({ left: args.right ? pixels : -pixels, behavior: 'smooth' })
179
+ } else {
180
+ const pixels = args.pixels ?? Math.round((args.numPages ?? 1) * window.innerHeight)
181
+ scrollTarget.scrollBy({ top: args.down ? pixels : -pixels, behavior: 'smooth' })
182
+ }
183
+
184
+ // 等待滚动动画完成后再采集状态
185
+ await new Promise(r => setTimeout(r, 400))
186
+ return buildBrowserStateResponse(mode)
187
+ // ── executeJavascript:执行任意 JS ────────────────────────────────
188
+ } else if (args.action === 'executeJavascript') {
189
+ if (!args.script) return errContent('脚本执行异常: 缺少javascript代码')
190
+ // eslint-disable-next-line no-new-func
191
+ const result = await new Function(`return (async () => { ${args.script} })()`)()
192
+ await pageController.hideMask()
193
+ return {
194
+ content: [{ type: 'text', text: `脚本执行结果: ${JSON.stringify(result)}` }]
195
+ }
196
+
197
+ // ── searchTree:关键词搜索无障碍树,返回带行号的精准结果 ──────────
198
+ } else if (args.action === 'searchTree') {
199
+ if (!args.query) return errContent('搜索失败: 缺少 query 参数')
200
+ const blacklist = (window.__webmcpcli_interactiveBlacklist ?? []) as Element[]
201
+ const whitelist = (window.__webmcpcli_interactiveWhitelist ?? []) as Element[]
202
+ const { text } = searchA11yTree(
203
+ args.query,
204
+ document.body,
205
+ blacklist,
206
+ whitelist,
207
+ {
208
+ contextLines: args.contextLines,
209
+ maxMatches: args.maxMatches,
210
+ },
211
+ )
212
+ await pageController.hideMask()
213
+ return {
214
+ content: [{ type: 'text', text }]
215
+ }
216
+ }
217
+ } catch (error) {
218
+ await pageController.hideMask()
219
+ return { content: [{ type: 'text', text: `异常: ${String(error)}` }] }
220
+ }
221
+ }
222
+ })
223
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * page-state-cache.ts
3
+ *
4
+ * 页面状态 Diff 缓存模块
5
+ * - 缓存上一次 buildA11yTree 生成的 YAML 文本
6
+ * - 每次操作后计算 diff,只返回 +/- 变化行,大幅节省 Token
7
+ */
8
+
9
+ import { diffLines } from 'diff'
10
+
11
+ export interface PageSnapshot {
12
+ url: string
13
+ yaml: string
14
+ }
15
+
16
+ export interface DiffResult {
17
+ /** 是否为全量刷新(首次加载或 URL 变化) */
18
+ isFullRefresh: boolean
19
+ /** 上一次的 URL(用于拼接 URL 变化提示) */
20
+ prevUrl: string
21
+ /** 新增行数 */
22
+ addedLines: number
23
+ /** 删除行数 */
24
+ removedLines: number
25
+ /** 只含 +/- 行的 diff 文本(不含相同行,节省 Token) */
26
+ diffText: string
27
+ }
28
+
29
+ export class PageStateCache {
30
+ private prev: PageSnapshot | null = null
31
+
32
+ /** URL 变化或首次调用时,需要全量输出 */
33
+ isFullRefresh(currentUrl: string): boolean {
34
+ return !this.prev || this.prev.url !== currentUrl
35
+ }
36
+
37
+ /**
38
+ * 更新缓存并返回 diff 结果
39
+ * @param url 当前页面 URL
40
+ * @param yaml 当前 YAML 树文本
41
+ */
42
+ update(url: string, yaml: string): DiffResult {
43
+ const isFullRefresh = this.isFullRefresh(url)
44
+ const prevUrl = this.prev?.url ?? ''
45
+
46
+ let addedLines = 0
47
+ let removedLines = 0
48
+ let diffText = ''
49
+
50
+ if (!isFullRefresh && this.prev) {
51
+ const changes = diffLines(this.prev.yaml, yaml)
52
+ const buf: string[] = []
53
+ changes.forEach(part => {
54
+ // 跳过相同行,只保留变化行,大幅节省传给 AI 的 Token 数量
55
+ if (!part.added && !part.removed) return
56
+ const lines = part.value.split('\n').filter(Boolean)
57
+ if (part.added) {
58
+ addedLines += lines.length
59
+ lines.forEach(l => buf.push(`+ ${l}`))
60
+ } else {
61
+ removedLines += lines.length
62
+ lines.forEach(l => buf.push(`- ${l}`))
63
+ }
64
+ })
65
+ diffText = buf.join('\n')
66
+ }
67
+
68
+ // 更新缓存
69
+ this.prev = { url, yaml }
70
+
71
+ return { isFullRefresh, prevUrl, addedLines, removedLines, diffText }
72
+ }
73
+
74
+ /** 强制清空缓存(页面刷新 / 手动重置场景) */
75
+ invalidate(): void {
76
+ this.prev = null
77
+ }
78
+ }
@@ -5,10 +5,11 @@ import scan from './svgs/scan.svg?url'
5
5
  import link from './svgs/link.svg?url'
6
6
  import qrCode from './svgs/qrcode.svg?url'
7
7
  import iconCopy from './svgs/icon-copy.svg?url'
8
+ import logo from './svgs/logo.svg?url'
8
9
 
9
10
  const DEFAULT_REMOTE_URL = 'https://chat.opentiny.design'
10
11
  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'
12
+ const DEFAULT_LOGO_URL = logo
12
13
 
13
14
  /** 菜单项配置接口 */
14
15
  export interface MenuItemConfig {
@@ -0,0 +1,45 @@
1
+ <svg width="100.000000" height="100.000000" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2
+ <desc>
3
+ Created with Pixso.
4
+ </desc>
5
+ <defs>
6
+ <radialGradient gradientTransform="translate(67.5526 65.1155) rotate(51.0683) scale(22.991 20.6919)" cx="0.000000" cy="0.000000" r="1.000000" id="paint_radial_9_3036_0" gradientUnits="userSpaceOnUse">
7
+ <stop stop-color="#5ADDF8"/>
8
+ <stop stop-color="#AEBFFF"/>
9
+ <stop offset="0.329153" stop-color="#1476FF"/>
10
+ <stop offset="0.667785" stop-color="#655AF8"/>
11
+ <stop offset="0.868255" stop-color="#948CFF"/>
12
+ <stop offset="1.000000" stop-color="#D25AF8"/>
13
+ <stop offset="1.000000" stop-color="#5A8BF8"/>
14
+ </radialGradient>
15
+ <linearGradient x1="69.000122" y1="85.000198" x2="69.500107" y2="70.500183" id="paint_linear_9_3039_0" gradientUnits="userSpaceOnUse">
16
+ <stop stop-color="#000000" stop-opacity="0.000000"/>
17
+ <stop offset="1.000000" stop-color="#000000"/>
18
+ </linearGradient>
19
+ <radialGradient gradientTransform="translate(26.9999 24.7118) rotate(38.3655) scale(77.7996 109.496)" cx="0.000000" cy="0.000000" r="1.000000" id="paint_radial_9_3040_0" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#5ADDF8"/>
21
+ <stop stop-color="#AEBFFF"/>
22
+ <stop offset="0.329153" stop-color="#1476FF"/>
23
+ <stop offset="0.667785" stop-color="#655AF8"/>
24
+ <stop offset="0.868255" stop-color="#948CFF"/>
25
+ <stop offset="1.000000" stop-color="#D25AF8"/>
26
+ <stop offset="1.000000" stop-color="#5A8BF8"/>
27
+ </radialGradient>
28
+ </defs>
29
+ <rect id="NEXT-LOGO-无背景渐变-左" width="100.000000" height="99.999939" fill="#FFFFFF" fill-opacity="0"/>
30
+ <mask id="mask9_3032" mask-type="alpha" maskUnits="userSpaceOnUse" x="0.000000" y="0.000061" width="99.999939" height="99.999939">
31
+ <rect id="画板 1773" y="0.000061" width="99.999962" height="99.999947" fill="#000000" fill-opacity="1.000000"/>
32
+ </mask>
33
+ <g mask="url(#mask9_3032)">
34
+ <path id="矢量 1" d="M64.07 81.01L64 63L82 63.07L67.51 82.23C66.39 83.71 64.07 82.9 64.07 81.01Z" fill="url(#paint_radial_9_3036_0)" fill-opacity="1.000000" fill-rule="evenodd"/>
35
+ <g opacity="0.100000" style="mix-blend-mode:normal">
36
+ <path id="矢量 1" d="M64.07 81.01L64 63L82 63.07L67.51 82.23C66.39 83.71 64.07 82.9 64.07 81.01Z" fill="url(#paint_linear_9_3039_0)" fill-opacity="1.000000" fill-rule="evenodd"/>
37
+ </g>
38
+ <rect id="矩形 5" x="12.000000" y="19.000153" rx="26.999992" width="75.999977" height="53.999985" fill="url(#paint_radial_9_3040_0)" fill-opacity="1.000000"/>
39
+ <rect id="矩形 5" x="12.500000" y="19.500153" rx="26.499992" width="74.999977" height="52.999985" fill="#000000" fill-opacity="0"/>
40
+ <rect id="矩形 5" x="12.500000" y="19.500153" rx="26.499992" width="74.999977" height="52.999985" stroke="#000000" stroke-opacity="0" stroke-width="1.000000"/>
41
+ <path id="path" d="M54.61 59.06C55.78 60.05 57.1 60.66 58.57 60.89C60.05 61.12 61.43 61.04 62.72 60.65C64 60.26 65.96 58.58 65.96 58.58C65.96 58.58 70.98 54.73 70.98 45.61C70.98 36.5 66.5 32.5 66.5 32.5C66.5 32.5 65.28 31.31 63.5 31C61.71 30.68 60.57 30.98 59.34 31.33C49.11 34.23 37.62 34.33 27.42 31.33C26.19 30.98 25.09 30.93 24.11 31.17C23.13 31.42 22.29 31.89 21.58 32.59C18.35 35.81 17.12 41.06 17 45.61C16.87 50.09 18.03 55.24 21.06 58.54C21.96 59.52 23.02 60.22 24.22 60.63C25.41 61.04 26.68 61.12 28.01 60.89C29.34 60.66 30.52 60.05 31.54 59.06C31.98 58.62 32.61 58.07 33.43 57.4C34.24 56.73 35.16 56.08 36.21 55.44C37.25 54.8 38.38 54.25 39.61 53.8C40.84 53.35 42.09 53.13 43.38 53.13C44.64 53.13 45.84 53.36 47 53.83C48.15 54.29 49.21 54.84 50.18 55.48C51.14 56.12 52 56.78 52.74 57.45C53.49 58.11 54.11 58.65 54.61 59.06Z" fill="#FFFFFF" fill-opacity="1.000000" fill-rule="nonzero"/>
42
+ <rect id="矩形 840" x="58.016602" y="38.000046" rx="2.000000" width="3.999999" height="11.999996" transform="rotate(1.39597 58.016602 38.000046)" fill="#1476FF" fill-opacity="1.000000"/>
43
+ <path id="合并" d="M28.97 38C28.17 37.97 27.38 38.42 27.01 39.19L23.2 47.11C22.71 48.13 23.15 49.34 24.18 49.81C25.22 50.28 26.45 49.83 26.94 48.8L28.97 44.59L30.99 48.81C31.48 49.83 32.72 50.28 33.75 49.81C34.79 49.34 35.23 48.13 34.74 47.11L30.93 39.19C30.56 38.42 29.78 37.98 28.97 38Z" fill="#1476FF" fill-opacity="1.000000" fill-rule="evenodd"/>
44
+ </g>
45
+ </svg>
package/vite-env.d.ts CHANGED
@@ -2,4 +2,9 @@
2
2
  declare module '*.svg?url' {
3
3
  const src: string;
4
4
  export default src;
5
+ }
6
+
7
+ declare module '*.md?raw' {
8
+ const content: string;
9
+ export default content;
5
10
  }