ms-vite-plugin 1.4.74 → 1.4.76

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,495 @@
1
+ # AI 模块 (AI)
2
+
3
+ 模块表是内置全局 `ai`,直接调用即可。不要 `require` 宿主模块。
4
+
5
+ AI 模块让脚本直接在手机上调用大模型:看图回答问题、做是非判断、从屏幕提取结构化数据、多轮对话。
6
+
7
+ 支持三种接口协议,覆盖绝大多数模型服务:
8
+
9
+ | `protocol` | 适用服务 |
10
+ | ----------- | ------------------------------------------------------------------------------------- |
11
+ | `openai` | DeepSeek、通义千问、Kimi、智谱、SiliconFlow、本机 Ollama / vLLM 等 |
12
+ | `anthropic` | 国内一般走 openai 兼容口,不必单独配 |
13
+ | `gemini` | 国内一般走 openai 兼容口,不必单独配 |
14
+
15
+ 模型配置(地址、Key、模型名)直接写在脚本里,脚本开头 `ai.setProvider` 一次即可;脚本停止后配置自动清空,不会存到手机上。
16
+
17
+ 所有方法同步阻塞,失败时返回 `nil` / `false`,原因用 `ai.lastError()` 查看。
18
+
19
+ ## 快速开始
20
+
21
+ ```lua
22
+ -- 1. 脚本开头登记模型(只需一次)。通义千问 Qwen3.5 起原生多模态,看图、问字都能用。
23
+ ai.setProvider({
24
+ name = "通义千问",
25
+ protocol = "openai",
26
+ baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
27
+ apiKey = "sk-xxxxxxxx",
28
+ model = "qwen3.5-flash",
29
+ })
30
+
31
+ -- 2. 直接问
32
+ print(ai.ask("用一句话介绍你自己"))
33
+
34
+ -- 3. 看着屏幕判断
35
+ if ai.judge("当前是否已经登录成功?", { image = "screen" }) then
36
+ print("已登录")
37
+ end
38
+
39
+ -- 4. 从屏幕某个区域提取数据
40
+ local data = ai.extract("提取验证码", {
41
+ image = "screen", x = 100, y = 800, ex = 600, ey = 900,
42
+ schema = { code = "string" },
43
+ })
44
+ if data then print(data.code) end
45
+ ```
46
+
47
+ ## 图片参数
48
+
49
+ 图片参数和 image 模块**完全一样**,没有单独的截图开关:
50
+
51
+ | 参数 | 含义 |
52
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `image` | `imageId`,与所有 `image.*` 函数的 `imageId` 参数一样:`image.captureScreen()` 等返回的图片 ID、`"screen"`(当前屏幕)、res 目录或手机绝对路径的图片文件 |
54
+ | `x`, `y`, `ex`, `ey` | 对 `image` 裁剪,同 `image.clip(imageId, x, y, ex, ey)`:左上角 / 右下角坐标,全 0 不裁剪。只写区域不写 `image` 时按 `"screen"` |
55
+ | `images` | 多张图的数组,元素同 `image`,不裁剪 |
56
+
57
+ ```lua
58
+ -- 当前全屏
59
+ ai.ask("屏幕上有几个按钮?", { image = "screen" })
60
+
61
+ -- 屏幕区域:左上 (0, 200) 到右下 (750, 600)
62
+ ai.ask("这一块显示的价格是多少?", { image = "screen", x = 0, y = 200, ex = 750, ey = 600 })
63
+
64
+ -- 已有的图片 ID
65
+ local id = image.captureScreen()
66
+ ai.ask("描述这张图", { image = id, x = 0, y = 0, ex = 750, ey = 400 })
67
+ image.release(id)
68
+
69
+ -- 图片文件
70
+ ai.ask("这张图里是什么", { image = "sample.png" })
71
+
72
+ -- 一次传多张:模板图 + 当前屏幕
73
+ ai.judge("第二张图里有没有第一张图的图标?", { images = { "icon.png", "screen" } })
74
+ ```
75
+
76
+ ## 费用说明
77
+
78
+ 模型按 token 计费,其中图片是大头:一张 iPhone 全屏截图约 1400 ~ 1500 token,文字提问通常只有几十个。
79
+
80
+ - **能裁区域就不发全屏。** 只发需要看的那一块,token 通常能少 5 ~ 10 倍。
81
+ - **循环里保持提示词不变。** 三家服务都有「前缀缓存」:连续请求里开头相同的部分按约 1/10 计费。AI 模块已经把 `system` 排最前、文字排在图片之前,循环里每次只有图片变,`system` 和问题文字都能命中缓存。不要往 `system` 或问题里拼时间、序号这类每次都变的内容。
82
+ - **不同场景用不同模型。** 便宜的文本模型做默认,视觉模型只在需要看图时用(见 [setProvider](#setprovider---登记或更新模型供应商))。
83
+
84
+ ## 数据类型
85
+
86
+ ### AiProvider
87
+
88
+ ```lua
89
+ ---@class AiProvider
90
+ ---@field id string 供应商 id,不传时自动生成
91
+ ---@field name string 展示名,例如 "DeepSeek",调用时可用它选模型
92
+ ---@field protocol "openai"|"anthropic"|"gemini"
93
+ ---@field baseUrl string 接口根地址,例如 https://api.deepseek.com
94
+ ---@field apiKey string API Key;openai 协议连本机模型时可为空
95
+ ---@field model string 默认模型名
96
+ ---@field headers table<string, string> 额外请求头,某些网关需要
97
+ ---@field temperature number|nil 默认温度;不传则不发该字段,用模型自己的默认值
98
+ ---@field maxTokens integer|nil 默认最大输出 token;不传则不发该字段,用模型自己的默认值(Anthropic 因接口必填仍默认 2048)
99
+ ---@field reasoningEffort "none"|"low"|"medium"|"high"|"xhigh"|nil 思考级别,仅 openai 协议;不传不发,用服务商默认
100
+ ---@field supportsVision boolean 是否支持图片输入,默认 true
101
+ ---@field isDefault boolean 是否为默认供应商
102
+ ```
103
+
104
+ ### AiOptions
105
+
106
+ `ask` / `judge` / `extract` / `chat` 的第二个参数,所有字段可选:
107
+
108
+ ```lua
109
+ ---@class AiOptions
110
+ ---@field provider string|nil 用哪个供应商:id 或 name;不传用默认
111
+ ---@field model string|nil 临时换模型,覆盖供应商默认模型
112
+ ---@field temperature number|nil 覆盖默认温度
113
+ ---@field maxTokens integer|nil 覆盖默认最大输出 token
114
+ ---@field timeout integer|nil 超时毫秒,默认 90000
115
+ ---@field json boolean|nil 要求模型输出 JSON(extract 自动开启)
116
+ ---@field system string|nil 系统提示(chat 不用这个,写在 messages 里)
117
+ ---@field image string|nil imageId,同 image 模块:图片 ID / "screen" / 图片路径
118
+ ---@field x integer|nil 裁剪区域左上角 X,同 image.clip
119
+ ---@field y integer|nil 裁剪区域左上角 Y
120
+ ---@field ex integer|nil 裁剪区域右下角 X,全 0 不裁剪
121
+ ---@field ey integer|nil 裁剪区域右下角 Y
122
+ ---@field images string[]|nil 多张图,元素同 image,不裁剪
123
+ ---@field schema string|table|nil 仅 extract:期望的 JSON 结构说明
124
+ ```
125
+
126
+ ### AiMessage
127
+
128
+ `chat` 的消息项,图片参数与 `AiOptions` 相同:
129
+
130
+ ```lua
131
+ ---@class AiMessage
132
+ ---@field role "system"|"user"|"assistant"
133
+ ---@field content string
134
+ ---@field image string|nil
135
+ ---@field x integer|nil
136
+ ---@field y integer|nil
137
+ ---@field ex integer|nil
138
+ ---@field ey integer|nil
139
+ ---@field images string[]|nil
140
+ ```
141
+
142
+ ## API 参考
143
+
144
+ ### setProvider - 登记或更新模型供应商。
145
+
146
+ ```lua
147
+ ---@param config table
148
+ ---@return AiProvider|nil
149
+ function setProvider(config) end
150
+ ```
151
+
152
+ **参数:**
153
+
154
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
155
+ | -------- | ------- | -------- | ------ | ----------------------------------------------------------------------------------- |
156
+ | `config` | `table` | 是 | - | 新建至少要有 `baseUrl`、`model`(`protocol` 缺省 openai);同 `id` 更新时没传的字段沿用旧值 |
157
+
158
+ **返回值:**
159
+
160
+ | 类型 | 描述 |
161
+ | ------------ | ------------------------------------------------------------------------------ |
162
+ | `AiProvider` | 保存后的完整配置;`protocol` 不是三者之一或缺 `baseUrl` / `model` 时返回 `nil` |
163
+
164
+ 第一个登记的供应商自动成为默认;`isDefault = true` 会把默认切到它。
165
+
166
+ **示例:**
167
+
168
+ ```lua
169
+ -- 只用一个模型(通义能看图)
170
+ ai.setProvider({
171
+ name = "通义千问", protocol = "openai",
172
+ baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
173
+ apiKey = "sk-xxxxxxxx", model = "qwen3.5-flash",
174
+ })
175
+
176
+ -- 多个模型省钱:便宜的文本模型做默认,视觉模型单独一个,用到时按 name 选
177
+ ai.setProvider({
178
+ name = "文本", protocol = "openai", baseUrl = "https://api.deepseek.com",
179
+ apiKey = "sk-xxxxxxxx", model = "deepseek-v4-flash", supportsVision = false, isDefault = true,
180
+ })
181
+ ai.setProvider({
182
+ name = "视觉", protocol = "openai", baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
183
+ apiKey = "sk-xxxxxxxx", model = "qwen3.5-flash",
184
+ })
185
+ ai.ask("把这段话翻译成英文:你好") -- 走默认的「文本」
186
+ ai.judge("是否出现弹窗?", { image = "screen", provider = "视觉" }) -- 看图时才用视觉模型
187
+
188
+ -- 同一供应商只换模型,不必再登记
189
+ ai.ask("复杂推理题……", { model = "deepseek-v4-pro" })
190
+
191
+ -- 智谱 GLM(OpenAI 兼容口)
192
+ ai.setProvider({
193
+ name = "智谱 GLM", protocol = "openai", baseUrl = "https://open.bigmodel.cn/api/paas/v4",
194
+ apiKey = "sk-xxxxxxxx", model = "glm-5.3-flash",
195
+ })
196
+
197
+ -- 本机 Ollama,不需要 Key
198
+ ai.setProvider({ name = "本机", protocol = "openai", baseUrl = "http://192.168.1.10:11434/v1", model = "qwen3:8b" })
199
+ ```
200
+
201
+ ### getProviders - 列出已登记的供应商。
202
+
203
+ ```lua
204
+ ---@return AiProvider[]
205
+ function getProviders() end
206
+ ```
207
+
208
+ **返回值:**
209
+
210
+ | 类型 | 描述 |
211
+ | -------------- | -------------------------- |
212
+ | `AiProvider[]` | 供应商数组,默认项排在最前 |
213
+
214
+ **示例:**
215
+
216
+ ```lua
217
+ for _, p in ipairs(ai.getProviders()) do
218
+ print(p.name .. " -> " .. p.model)
219
+ end
220
+ ```
221
+
222
+ ### getDefaultProvider - 获取默认供应商。
223
+
224
+ ```lua
225
+ ---@return AiProvider|nil
226
+ function getDefaultProvider() end
227
+ ```
228
+
229
+ **返回值:**
230
+
231
+ | 类型 | 描述 |
232
+ | ------------ | ------------------------------------ |
233
+ | `AiProvider` | 默认供应商;一个都没登记时返回 `nil` |
234
+
235
+ ### setDefaultProvider - 切换默认供应商。
236
+
237
+ ```lua
238
+ ---@param id string
239
+ ---@return boolean
240
+ function setDefaultProvider(id) end
241
+ ```
242
+
243
+ **参数:**
244
+
245
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
246
+ | ------ | ------ | -------- | ------ | --------- |
247
+ | `id` | string | 是 | - | 供应商 id 或 name |
248
+
249
+ **返回值:**
250
+
251
+ | 类型 | 描述 |
252
+ | --------- | ------------------ |
253
+ | `boolean` | id 或 name 存在返回 `true` |
254
+
255
+ ### removeProvider - 删除供应商。
256
+
257
+ ```lua
258
+ ---@param id string
259
+ ---@return boolean
260
+ function removeProvider(id) end
261
+ ```
262
+
263
+ **参数:**
264
+
265
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
266
+ | ------ | ------ | -------- | ------ | --------- |
267
+ | `id` | string | 是 | - | 供应商 id 或 name |
268
+
269
+ **返回值:**
270
+
271
+ | 类型 | 描述 |
272
+ | --------- | --------------------- |
273
+ | `boolean` | 存在并删除返回 `true` |
274
+
275
+ 删掉的是默认供应商时,默认自动转给剩下的第一个。
276
+
277
+ ### ask - 单轮提问,可附带图片。
278
+
279
+ ```lua
280
+ ---@param prompt string
281
+ ---@param options AiOptions|nil
282
+ ---@return string|nil
283
+ function ask(prompt, options) end
284
+ ```
285
+
286
+ **参数:**
287
+
288
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
289
+ | --------- | ----------- | -------- | ------ | ------------------------------------------------- |
290
+ | `prompt` | string | 是 | - | 问题 |
291
+ | `options` | `AiOptions` | 否 | `nil` | 见 [AiOptions](#aioptions),图片参数同 image 模块 |
292
+
293
+ **返回值:**
294
+
295
+ | 类型 | 描述 |
296
+ | -------- | -------------------------------------------------- |
297
+ | `string` | 模型回复文本;失败返回 `nil`,原因看 `lastError()` |
298
+
299
+ **示例:**
300
+
301
+ ```lua
302
+ -- 纯文本
303
+ local t = ai.ask("北京到上海高铁大概几小时?")
304
+
305
+ -- 带系统提示 + 当前屏幕
306
+ local desc = ai.ask("描述当前页面", { system = "你是手机自动化助手,回答尽量简短。", image = "screen" })
307
+
308
+ -- 只看屏幕某个区域
309
+ local price = ai.ask("这个区域里的价格是多少?只回答数字", { image = "screen", x = 0, y = 300, ex = 750, ey = 500 })
310
+
311
+ -- 失败处理
312
+ local r = ai.ask("你好")
313
+ if r == nil then print(ai.lastError()) end
314
+ ```
315
+
316
+ ### judge - 是非判断。
317
+
318
+ ```lua
319
+ ---@param question string
320
+ ---@param options AiOptions|nil
321
+ ---@return boolean
322
+ function judge(question, options) end
323
+ ```
324
+
325
+ 让模型只回答 yes / no,并解析成布尔值。适合做流程分支:「有没有弹窗」「是否到了首页」。
326
+
327
+ **参数:**
328
+
329
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
330
+ | ---------- | ----------- | -------- | ------ | ------------ |
331
+ | `question` | string | 是 | - | 要判断的问题 |
332
+ | `options` | `AiOptions` | 否 | `nil` | 同 `ask` |
333
+
334
+ **返回值:**
335
+
336
+ | 类型 | 描述 |
337
+ | --------- | ---------------------------------------------------------------------------- |
338
+ | `boolean` | 模型回答肯定为 `true`;否定或请求失败为 `false`,失败时 `lastError()` 有内容 |
339
+
340
+ **示例:**
341
+
342
+ ```lua
343
+ if ai.judge("屏幕上是否出现了「领取成功」?", { image = "screen" }) then
344
+ print("领取成功")
345
+ end
346
+
347
+ -- 只看顶部区域,省 token
348
+ while not ai.judge("顶部是否显示「首页」?", { image = "screen", x = 0, y = 0, ex = 0, ey = 200 }) do
349
+ action.click(375, 1500)
350
+ Sleep(1000)
351
+ end
352
+
353
+ -- 区分「回答否」和「请求失败」
354
+ local ok = ai.judge("是否已登录?", { image = "screen" })
355
+ if not ok and ai.lastError() then print("请求失败: " .. ai.lastError()) end
356
+ ```
357
+
358
+ ### extract - 从图片或文本里提取结构化数据。
359
+
360
+ ```lua
361
+ ---@param prompt string
362
+ ---@param options AiOptions|nil
363
+ ---@return table|nil
364
+ function extract(prompt, options) end
365
+ ```
366
+
367
+ 要求模型输出 JSON 并自动解析成表。`schema` 用来告诉模型你想要的结构。
368
+
369
+ **参数:**
370
+
371
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
372
+ | --------- | ----------- | -------- | ------ | --------------------------------------- |
373
+ | `prompt` | string | 是 | - | 提取要求 |
374
+ | `options` | `AiOptions` | 否 | `nil` | 同 `ask`,另支持 `schema`(字符串或表) |
375
+
376
+ **返回值:**
377
+
378
+ | 类型 | 描述 |
379
+ | ------- | -------------------------------------------------------- |
380
+ | `table` | 解析后的表;模型没有输出合法 JSON 或请求失败返回 `nil` |
381
+
382
+ **示例:**
383
+
384
+ ```lua
385
+ -- 提取验证码
386
+ local d = ai.extract("提取图中的验证码", {
387
+ image = "screen", x = 100, y = 800, ex = 600, ey = 900,
388
+ schema = { code = "string" },
389
+ })
390
+ if d then ime.input(d.code) end
391
+
392
+ -- 提取列表
393
+ local items = ai.extract("列出屏幕上所有商品的名称和价格", {
394
+ image = "screen", schema = { { name = "string", price = "number" } },
395
+ })
396
+ if items then
397
+ for _, it in ipairs(items) do print(it.name, it.price) end
398
+ end
399
+
400
+ -- 让模型给坐标(配合 action.click)
401
+ local pos = ai.extract("找到「立即购买」按钮,给出它的中心点像素坐标", {
402
+ image = "screen", schema = { x = "number", y = "number" },
403
+ })
404
+ if pos then action.click(pos.x, pos.y) end
405
+ ```
406
+
407
+ ### chat - 多轮对话。
408
+
409
+ ```lua
410
+ ---@param messages (AiMessage|string)[]
411
+ ---@param options AiOptions|nil
412
+ ---@return string|nil
413
+ function chat(messages, options) end
414
+ ```
415
+
416
+ 自己维护完整对话历史时用它;`ask` 是它的单轮封装。模块不记会话,每次都要把到目前为止的消息整表传进来。
417
+
418
+ 图片写在**那一条 user 消息**上,不要用 `options.image`。`options.system` 也不生效,系统提示写成第一条 `{ role = "system", content = "..." }`。
419
+
420
+ 失败返回 `nil`:不要把空回复推进历史,否则下一轮变成 `user → assistant(nil) → user`,Anthropic 会 400。Anthropic 协议下模块会自动合并连续同角色、并在首条是 assistant 时垫一条占位 user;OpenAI 兼容口更宽松,不必为此改脚本。
421
+
422
+ **参数:**
423
+
424
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
425
+ | ---------- | ----------------------- | -------- | ------ | ----------------------------------------------------------------------- |
426
+ | `messages` | `(AiMessage\|string)[]` | 是 | - | 消息数组;直接写字符串等于 `user` 消息;图片参数写在消息里 |
427
+ | `options` | `AiOptions` | 否 | `nil` | `provider` / `model` / `temperature` / `maxTokens` / `json` / `timeout` |
428
+
429
+ **返回值:**
430
+
431
+ | 类型 | 描述 |
432
+ | -------- | ------------------------ |
433
+ | `string` | 回复文本;失败返回 `nil` |
434
+
435
+ **示例:**
436
+
437
+ ```lua
438
+ local history = { { role = "system", content = "你是手机自动化助手" } }
439
+
440
+ local function talk(content, extra)
441
+ local msg = { role = "user", content = content }
442
+ if extra then
443
+ for k, v in pairs(extra) do msg[k] = v end
444
+ end
445
+ table.insert(history, msg)
446
+ local reply = ai.chat(history)
447
+ if reply == nil then
448
+ table.remove(history)
449
+ print(ai.lastError())
450
+ return nil
451
+ end
452
+ table.insert(history, { role = "assistant", content = reply })
453
+ return reply
454
+ end
455
+
456
+ talk("看下现在在哪个页面", { image = "screen" })
457
+ talk("那下一步该点哪里?")
458
+ ```
459
+
460
+ ### parseJson - 从模型文本里解析 JSON。
461
+
462
+ ```lua
463
+ ---@param text string
464
+ ---@return table|nil
465
+ function parseJson(text) end
466
+ ```
467
+
468
+ 自动剥掉 Markdown 代码块和前后的解释文字。`extract` 内部就是用它解析。
469
+
470
+ **参数:**
471
+
472
+ | 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
473
+ | ------ | ------ | -------- | ------ | -------- |
474
+ | `text` | string | 是 | - | 模型输出 |
475
+
476
+ **返回值:**
477
+
478
+ | 类型 | 描述 |
479
+ | ------- | ------------------------ |
480
+ | `table` | 解析结果;失败返回 `nil` |
481
+
482
+ ### lastError - 最近一次调用失败的原因。
483
+
484
+ ```lua
485
+ ---@return string|nil
486
+ function lastError() end
487
+ ```
488
+
489
+ **返回值:**
490
+
491
+ | 类型 | 描述 |
492
+ | -------- | ------------------------------------ |
493
+ | `string` | 错误文案;上一次调用成功时返回 `nil` |
494
+
495
+ 常见错误:未登记供应商、Key 错误 / 余额不足(接口返回的原文)、模型不支持图片、图片无效或裁剪越界、超时、模型没有输出 JSON。