--- url: /doc_v2/TestFlight/zh/guide/AppIntent.md --- # AppIntent `AppIntentManager` 用于在 **Scripting** 中注册并管理 `AppIntent`,它是 Widget、Live Activity、ControlWidget 等控件执行脚本逻辑的核心机制。 所有的 `AppIntent` **必须** 定义在 `app_intents.tsx` 文件中,且在执行时其运行环境 `Script.env` 为 `"app_intents"`。 通过 `AppIntentManager` 注册的意图可以被 Widget / Live Activity / ControlWidget 中的 **Button** 与 **Toggle** 控件调用,以在用户交互时触发对应的脚本逻辑。 *** ## 一、类型定义 ### `AppIntent` 表示一个具体的应用意图实例。 | 字段名 | 类型 | 描述 | | ---------- | ------------------- | ---------------------------------------- | | `script` | `string` | 脚本路径,由系统内部生成。 | | `name` | `string` | 意图名称,唯一标识该 AppIntent。 | | `protocol` | `AppIntentProtocol` | 该意图实现的协议类型(如普通、音频播放、音频录制、Live Activity)。 | | `params` | `T` | 意图执行时的参数。 | *** ### `AppIntentFactory` 表示一个 **工厂函数**,用于通过参数创建 `AppIntent` 实例。 ```ts type AppIntentFactory = (params: T) => AppIntent ``` *** ### `AppIntentPerform` 表示一个执行函数,用于在意图被触发时执行实际逻辑。 ```ts type AppIntentPerform = (params: T) => Promise ``` *** ### `AppIntentProtocol` `AppIntentProtocol` 是枚举类型,用于指定意图的协议(行为类别)。 | 枚举成员 | 描述 | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `AppIntent` (0) | 普通 AppIntent。用于执行一般操作的意图。 | | `AudioPlaybackIntent` (1) | 播放、暂停或修改音频播放状态的意图。 | | `AudioRecordingIntent` (2) | (iOS 18.0+)启动、停止或修改音频录制状态的意图。**注意**:在 iOS/iPadOS 中,当使用 `AudioRecordingIntent` 协议时,必须在开始录音时启动一个 **Live Activity** 并在录音持续时保持活跃,否则音频录制将会停止。 | | `LiveActivityIntent` (3) | 启动、暂停或修改 Live Activity 的意图。 | *** ## 二、AppIntentManager 类 ### `AppIntentManager.register(options): AppIntentFactory` 注册一个新的 `AppIntent`。 通过指定 `name`、`protocol` 和 `perform` 函数来注册,当控件(Button/Toggle)被触发时,系统会自动调用 `perform` 函数执行逻辑。 ```ts static register(options: { name: string; protocol: AppIntentProtocol; perform: AppIntentPerform; }): AppIntentFactory ``` #### 参数: | 参数名 | 类型 | 描述 | | ---------- | --------------------- | ------------------------------------- | | `name` | `string` | AppIntent 的名称,需唯一,用于标识该意图。 | | `protocol` | `AppIntentProtocol` | AppIntent 的协议类型。 | | `perform` | `AppIntentPerform` | 当控件触发该意图时执行的异步函数,参数为控件传递过来的 `params`。 | #### 返回值: - **`AppIntentFactory`**:返回一个工厂函数,可通过传入参数创建 `AppIntent` 实例。 #### 示例: ```tsx /// app_intents.tsx export const ToggleDoorIntent = AppIntentManager.register({ name: "ToggleDoorIntent", protocol: AppIntentProtocol.AppIntent, perform: async ({ id, newState }: { id: string; newState: boolean }) => { // 自定义逻辑:切换门的状态 await setDoorState(id, newState) // 通知控件刷新状态(如 ControlWidgetToggle) ControlWidget.reloadToggles() } }) ``` 在控件文件中(如 `control_widget_toggle.tsx`): ```tsx ControlWidget.present( ) ``` 在小组件中使用(如 `widget.tsx`): ```tsx ``` *** ## 三、执行时环境 所有通过 `AppIntentManager` 定义的 AppIntent 在执行时,`Script.env` 会自动为 `"app_intents"`。 这意味着在 `perform` 函数中可以安全地使用适合 `"app_intents"` 环境的 API(如访问网络、更新 Live Activity 状态、触发控件刷新等)。 *** ## 四、最佳实践 1. **集中管理**:所有 AppIntent 必须定义在 `app_intents.tsx` 文件中,避免分散。 2. **类型安全**:在 `perform` 和控件参数中定义严格的参数类型 `T`,以确保开发时的自动补全与类型检查。 3. **协议匹配**:根据控件行为选择合适的 `AppIntentProtocol`,例如: - 普通操作 → `AppIntent` - 控制音频播放 → `AudioPlaybackIntent` - 控制音频录制 → `AudioRecordingIntent`(iOS 18+ 且需保持 Live Activity) - 启动/暂停 Live Activity → `LiveActivityIntent` 4. **状态刷新**:执行完 `perform` 后,如需更新 UI 状态(例如切换门锁开关),请调用 `ControlWidget.reloadButtons()` 、 `ControlWidget.reloadToggles()` 或 `Widget.reloadAll()`。 --- url: /doc_v2/TestFlight/zh/guide/Assistant/Assistant Conversation APIs.md --- # 智能助手会话 API Conversation API 用于**启动、控制和展示一个由系统托管的 Assistant 对话会话**。 该会话对应一个**完整的聊天页面(Chat Page)**,由 Scripting App 统一管理 UI、状态和模型交互。 与 `requestStreaming` / `requestStructuredData` 的区别在于: - Conversation API 面向**交互式聊天体验** - 系统负责消息发送、流式输出、Provider 切换、UI 渲染 - 开发者只需关注“何时开始 / 何时结束 / 是否展示” *** ## 会话生命周期概览 一个典型的会话生命周期如下: 1. `startConversation` —— 创建会话(可选自动开始) 2. `present` —— 展示 Assistant 聊天页面 3. 用户与 Assistant 进行交互 4. `dismiss` —— 临时关闭聊天页面(会话仍存在) 5. `present` —— 再次展示会话 6. `stopConversation` —— 结束会话并释放资源 重要约束: - **同一时间只能存在一个活动会话** - 若已有会话在运行,再次调用 `startConversation` 会抛出错误 - 调用 `stopConversation` 会自动触发 `dismiss` *** ## startConversation ### API 定义 ```ts function startConversation(options: { message: string images?: UIImage[] autoStart?: boolean systemPrompt?: string modelId?: string provider?: Provider }): Promise ``` *** ### 参数说明 #### options.message - 类型:`string` - 必填 - 会话的**初始用户消息** - 相当于聊天页面中的第一条用户输入 *** #### options.images(可选) - 类型:`UIImage[]` - 会与 `message` 一起作为首条用户消息发送 - 适用于: - 图片分析 - 拍照 / 截图后直接发起对话 *** #### options.autoStart(可选) - 类型:`boolean` - 默认值:`false` 行为说明: - `true`: - 创建会话后立即开始生成回复 - `false`: - 仅创建会话,不自动发送 - 通常配合 `present` 使用,由用户点击发送 *** #### options.systemPrompt(可选) - 类型:`string` 说明: - 若未提供: - 使用 Scripting Assistant 内置 system prompt - Assistant Tools 可用 - 若提供: - 完全替换默认 system prompt - **Assistant Tools 将不可用** 适用场景: - 构建高度定制的聊天角色 - 禁用工具调用,仅使用纯模型能力 *** #### options.modelId(可选) - 类型:`string` - 指定本次会话使用的模型 - 用户仍可在聊天页面中手动切换模型(若 UI 允许) *** #### options.provider(可选) - 类型:`Provider` - 指定默认 Provider - 用户可在聊天页面中更改 Provider(若 UI 允许) *** ### 返回值 ```ts Promise ``` - 会话创建成功即 resolve - 若已有会话存在,将 reject *** ## present ### API 定义 ```ts function present(): Promise ``` *** ### 行为说明 - 展示当前会话对应的 Assistant 聊天页面 - 若页面已展示,调用不会产生额外效果 - 可在以下场景调用: - `startConversation` 之后首次展示 - `dismiss` 后重新展示同一会话 *** ### 返回值 ```ts Promise ``` - 当聊天页面被用户关闭时 resolve *** ## dismiss ### API 定义 ```ts function dismiss(): Promise ``` *** ### 行为说明 - 关闭 Assistant 聊天页面 - **不会终止会话** - 会话状态、历史消息仍保留 适用场景: - 临时让出界面空间 - 页面跳转或多任务切换 *** ### 返回值 ```ts Promise ``` - 页面成功关闭后 resolve *** ## stopConversation ### API 定义 ```ts function stopConversation(): Promise ``` *** ### 行为说明 - 彻底终止当前会话 - 自动调用 `dismiss` - 清理会话状态与资源 - 结束后可再次调用 `startConversation` 创建新会话 *** ### 返回值 ```ts Promise ``` *** ## 会话状态相关常量 ### Assistant.isAvailable ```ts const isAvailable: boolean ``` - 表示当前用户是否**具备使用 Assistant 的权限** - 若为 `false`: - 所有 Conversation API 均不可用 *** ### Assistant.isPresented ```ts const isPresented: boolean ``` - 表示 Assistant 聊天页面当前是否处于展示状态 *** ### Assistant.hasActiveConversation ```ts const hasActiveConversation: boolean ``` - 表示当前是否存在一个活动会话 - 常用于防止重复调用 `startConversation` *** ## 使用示例 ### 示例一:最常见的使用方式 ```ts await Assistant.startConversation({ message: "帮我总结这篇文章的要点", autoStart: true }) await Assistant.present() ``` *** ### 示例二:创建会话但不自动发送 ```ts await Assistant.startConversation({ message: "我们来讨论一下系统架构设计", autoStart: false }) await Assistant.present() // 由用户在 UI 中手动点击发送 ``` *** ### 示例三:暂时关闭,再次展示 ```ts await Assistant.startConversation({ message: "分析这张图片", images: [image], autoStart: true }) await Assistant.present() // 用户关闭页面 await Assistant.dismiss() // 稍后再次展示同一会话 await Assistant.present() ``` *** ### 示例四:结束当前会话并重新开始 ```ts if (Assistant.hasActiveConversation) { await Assistant.stopConversation() } await Assistant.startConversation({ message: "开始一个新的话题", autoStart: true }) await Assistant.present() ``` *** ## 使用建议与最佳实践 - 将 Conversation API 视为“**托管聊天界面**” - 不要在同一业务流中混用 Conversation API 与 `requestStreaming` - 在调用 `startConversation` 前检查 `hasActiveConversation` - 若仅需要数据或一次性输出,应使用: - `requestStructuredData` - `requestStreaming` - 若用户需要持续交互体验,应使用 Conversation API *** ## 设计边界说明 - Conversation API 不适合无 UI 场景 - 不适合后台自动化任务 - 不适合需要完全控制 Prompt / Token / 输出格式的场景 --- url: /doc_v2/TestFlight/zh/guide/Assistant/Assistant Quick Start.md --- # 快速了解智能助手 Scripting 的 Assistant API 提供了三类能力,分别面向 **数据处理**、**流式输出** 和 **交互式聊天** 三种不同使用场景。 在使用之前,建议先明确你的需求属于哪一类。 *** ## Assistant API 分类总览 | API 分类 | 主要方法 | 适用场景 | | ------ | ---------------------------------------------------------------- | ---------------------- | | 结构化数据 | `requestStructuredData` | 从文本 / 图片中提取结构化 JSON 数据 | | 流式生成 | `requestStreaming` | 实时展示 AI 输出内容 | | 会话聊天 | `startConversation` / `present` / `dismiss` / `stopConversation` | 托管式聊天体验 | *** ## requestStructuredData **用途** 用于请求**严格符合 JSON Schema 的结构化结果**。 **适合场景** - 解析票据、发票、账单 - 从自然语言中提取字段 - 生成配置、规则、表单数据 - 需要直接用于程序逻辑的数据 **特点** - 返回值稳定、可预测 - 不适合长文本或展示型输出 - 适合后台或无 UI 场景 **一句话总结** > 需要“数据”,用 `requestStructuredData` *** ## requestStreaming **用途** 用于获取**流式输出**,在模型生成过程中持续接收内容。 **适合场景** - 聊天气泡逐字显示 - 长文本生成(文章、说明、分析) - 需要低延迟反馈的 UI **特点** - 支持文本、推理、用量等多种 Chunk - 可边生成边渲染 - 不保证输出结构 **一句话总结** > 需要“过程”和“实时展示”,用 `requestStreaming` *** ## Conversation API(会话聊天) **相关方法** - `startConversation` - `present` - `dismiss` - `stopConversation` **用途** 用于创建并展示一个**系统托管的 Assistant 聊天页面**。 **适合场景** - 类 ChatGPT 的交互体验 - 用户需要多轮对话 - 希望系统管理 UI、Provider 切换、消息状态 **特点** - 内置完整聊天 UI - 自动处理流式输出 - 同一时间仅支持一个会话 **一句话总结** > 需要“完整聊天体验”,用 Conversation API *** ## 如何选择合适的 API ### 常见选择指南 - **我要解析一张账单 →** `requestStructuredData` - **我要展示 AI 写文章的过程 →** `requestStreaming` - **我要打开一个聊天页面让用户和 AI 对话 →** Conversation API - **我不需要 UI,只要结果 →** `requestStructuredData` 或 `requestStreaming` - **我希望系统帮我处理聊天 UI →** Conversation API *** ## 简单示例 ### 结构化数据 ```ts const data = await Assistant.requestStructuredData(...) ``` *** ### 流式输出 ```ts const stream = await Assistant.requestStreaming(...) for await (const chunk of stream) { // handle chunk } ``` *** ### 聊天会话 ```ts await Assistant.startConversation({ message: "你好", autoStart: true }) await Assistant.present() ``` *** ## 使用建议 - 不要在同一业务流程中混用 Conversation API 和 `requestStreaming` - 有明确数据结构需求时,优先选择 `requestStructuredData` - 展示型输出和交互体验优先考虑 `requestStreaming` 或 Conversation API *** ## 下一步 如果你需要更深入的内容,可以继续阅读: - `requestStructuredData` 详细文档 - `requestStreaming` 详细文档 - Conversation API 生命周期说明 --- url: /doc_v2/TestFlight/zh/guide/Assistant/requestStreaming.md --- # 请求流式数据 `requestStreaming` 用于向 Assistant 请求**流式输出(Streaming Response)**。 与一次性返回完整结果不同,该 API 会在模型生成内容的过程中**持续返回数据片段(Chunk)**,调用方可以边接收边处理,从而实现: - 实时展示 AI 输出(打字机效果) - 流式日志 / 分段结果处理 - 长文本生成的低延迟体验 - 在生成过程中提前终止或切换 UI 状态 该 API 返回的是一个 **`ReadableStream`**,你可以通过 `for await ... of` 逐块读取。 *** ## API 定义 ```ts function requestStreaming(options: { systemPrompt?: string | null messages: MessageItem | MessageItem[] provider?: Provider modelId?: string }): Promise> ``` *** ## 参数说明 ### options.systemPrompt(可选) - 类型:`string | null` - 指定本次请求使用的 system prompt - 若未提供: - 使用 Assistant 内置的默认 system prompt - 若显式传入: - 将**完全替换默认 system prompt** - Assistant Tools 将**不可用** 适用场景: - 构建专用角色(如代码审查、翻译、摘要) - 严格约束模型行为或输出风格 *** ### options.messages - 类型:`MessageItem | MessageItem[]` - 必填 - 用于描述对话上下文的消息列表 #### MessageItem 结构 ```ts type MessageItem = { role: "user" | "assistant" content: MessageContent | MessageContent[] } ``` - `role` - `"user"`:用户输入 - `"assistant"`:历史 Assistant 输出(用于上下文补全) *** ### MessageContent 类型 #### 文本内容 ```ts type MessageTextContent = | string | { type: "text"; content: string } ``` *** #### 图片内容 ```ts type MessageImageContent = { type: "image" content: string // data:image/...;base64,... } ``` *** #### 文档内容 ```ts type MessageDocumentContent = { type: "document" content: { mediaType: string data: string // base64 } } ``` *** ### options.provider(可选) - 类型:`Provider` - 指定使用的 AI Provider - 若未指定: - 使用 Assistant 当前配置的默认 Provider - 支持: - `"openai"` - `"gemini"` - `"anthropic"` - `"deepseek"` - `"openrouter"` - `{ custom: string }` *** ### options.modelId(可选) - 类型:`string` - 指定具体模型 ID - 必须与 Provider 实际支持的模型匹配 - 若未指定,使用 Provider 默认模型 *** ## 返回值 ```ts Promise> ``` 该 Promise resolve 后,你将获得一个可异步迭代的流对象。 *** ## StreamChunk 类型说明 `requestStreaming` 的流中会返回以下三类 Chunk。 *** ### StreamTextChunk(文本输出) ```ts type StreamTextChunk = { type: "text" content: string } ``` - 表示 Assistant 生成的**可展示文本** - 多个 chunk 拼接后构成完整回复 *** ### StreamReasoningChunk(推理输出) ```ts type StreamReasoningChunk = { type: "reasoning" content: string } ``` - 表示模型的**中间推理过程** - 是否返回、返回粒度取决于 Provider / Model *** ### StreamUsageChunk(用量信息) ```ts type StreamUsageChunk = { type: "usage" content: { totalCost: number | null cacheReadTokens: number | null cacheWriteTokens: number | null inputTokens: number outputTokens: number } } ``` 说明: - 通常在流的**末尾**返回一次 - 不同 Provider 支持的字段略有差异 - `totalCost` 可能为 `null`(例如 Provider 未提供费用信息) *** ## 使用示例 ### 示例一:最基本的流式请求 ```ts const stream = await Assistant.requestStreaming({ messages: { role: "user", content: "给我讲一个简短的科幻故事" }, provider: "openai" }) let text = "" for await (const chunk of stream) { if (chunk.type === "text") { text += chunk.content console.log(chunk.content) } } ``` *** ### 示例二:区分文本、推理和用量 ```ts const stream = await Assistant.requestStreaming({ systemPrompt: "你是一个严谨的技术写作助手", messages: [ { role: "user", content: "解释什么是 HTTP/3" } ] }) let answer = "" let reasoningLog = null let usage = null for await (const chunk of stream) { switch (chunk.type) { case "text": answer += chunk.content break case "reasoning": reasoningLog = (reasoningLog ?? "") + chunk.content break case "usage": usage = chunk.content break } } console.log(answer) console.log(usage) ``` *** ### 示例三:包含图片与文档的流式请求 ```ts const stream = await Assistant.requestStreaming({ messages: [ { role: "user", content: [ { type: "text", content: "请分析这份文档的核心内容" }, { type: "document", content: { mediaType: "application/pdf", data: "JVBERi0xLjQKJcfs..." } } ] } ], provider: "anthropic" }) for await (const chunk of stream) { if (chunk.type === "text") { console.log(chunk.content) } } ``` *** ## 使用建议与注意事项 - 流式结果**必须按顺序消费**,不可并发读取 - UI 场景下建议: - 文本 chunk 实时渲染 - reasoning chunk 仅用于调试 - usage chunk 延迟处理 - 若中途不再需要结果,应主动中止读取,避免无意义消耗 - 并非所有 Provider / Model 都会返回 reasoning 或 usage - 不同 Provider 的 chunk 粒度不同,不应假设单次 chunk 是完整句子 --- url: /doc_v2/TestFlight/zh/guide/Assistant/requestStructuredData.md --- # 请求结构化数据 `requestStructuredData` 用于向 Assistant 请求**严格符合指定 JSON Schema 的结构化 JSON 数据**。 该 API 适合在你需要**可预测、可直接用于程序逻辑**的数据结果时使用,而不是自由文本。 典型使用场景包括: - 从自然语言中提取结构化字段 - 解析发票、收据、账单、票据 - 生成配置对象、规则数据 - 在不同 AI Provider / Model 之间获得一致的数据结构 *** ## 支持的 JSON Schema 类型 Scripting 提供了一套轻量级、跨模型可用的 Schema 描述方式,由三种基础类型组成。 ### Primitive(基础类型) ```ts type JSONSchemaPrimitive = { type: "string" | "number" | "boolean" required?: boolean description: string } ``` *** ### Object(对象类型) ```ts type JSONSchemaObject = { type: "object" properties: Record required?: boolean description: string } ``` *** ### Array(数组类型) ```ts type JSONSchemaArray = { type: "array" items: JSONSchemaType required?: boolean description: string } ``` *** ## API 定义 ### 不包含图片输入 ```ts function requestStructuredData( prompt: string, schema: JSONSchemaArray | JSONSchemaObject, options?: { provider: Provider modelId?: string } ): Promise ``` *** ### 包含图片输入 ```ts function requestStructuredData( prompt: string, images: string[], schema: JSONSchemaArray | JSONSchemaObject, options?: { provider: Provider modelId?: string } ): Promise ``` *** ## 参数说明 ### prompt - 类型:`string` - 必填 - 向模型说明**需要解析或生成什么结构化数据** - 强烈建议在 prompt 中明确: - 时间格式(如 ISO-8601) - 金额是否为纯数字 - 缺失字段的处理规则 *** ### images(可选) - 类型:`string[]` - 每一项必须是 Data URI,例如: ```text data:image/png;base64,iVBORw0KGgoAAAANS... ``` - 并非所有 Provider / Model 都支持图片输入 - 图片数量过多可能导致请求失败 *** ### schema - 类型:`JSONSchemaArray | JSONSchemaObject` - 必填 - 定义模型**唯一允许返回的 JSON 结构** - 每一个字段都应提供清晰的 `description`,这是保证结果稳定的关键 *** ### options.provider - 类型:`Provider` - 可选,未指定时使用 Assistant 当前默认 Provider - 支持: - `"openai"` - `"gemini"` - `"anthropic"` - `"deepseek"` - `"openrouter"` - `{ custom: string }` *** ### options.modelId(可选) - 类型:`string` - 指定具体模型 ID - 必须与 Provider 实际支持的模型匹配 - 未指定时使用 Provider 默认模型 *** ## 返回值 ```ts Promise ``` - `R` 为调用方声明的泛型类型 - 返回结果应严格符合 Schema 描述 - 若模型无法生成合法结构,Promise 将被 reject *** ## 使用示例 ### 示例一:解析票据 / 收据,提取消费项目、时间和金额 该示例演示如何将一段票据文本解析为结构化数据,包括: - 整体消费时间(`purchasedAt`) - 币种(`currency`) - 消费项目列表(`items`) - 项目名称 - 项目时间(若无则为空) - 金额 - 合计金额(`total`) ```ts type ReceiptItem = { name: string time: string amount: number } type ReceiptParsed = { purchasedAt: string currency: string items: ReceiptItem[] total: number } const receiptText = ` Star Coffee 2026-01-08 14:23 Latte (Large) $5.50 Blueberry Muffin $3.20 Tax $0.79 Total $9.49 ` const parsed = await Assistant.requestStructuredData( [ "请分析以下票据文本,并提取结构化信息:", "- purchasedAt:整体消费时间,使用 ISO-8601 格式,若无法判断则返回空字符串", "- currency:币种代码(如 USD / EUR / CNY),若无法判断则返回空字符串", "- items:仅包含实际消费项目,不包含税费、合计等行", " - name:项目名称", " - time:项目级时间,若无则返回空字符串", " - amount:数值类型的金额", "- total:合计金额,若无则返回 -1", "", "票据内容:", receiptText ].join("\n"), { type: "object", description: "票据解析结果", properties: { purchasedAt: { type: "string", description: "整体消费时间(ISO-8601),若无则为空字符串" }, currency: { type: "string", description: "币种代码,若无法判断则为空字符串" }, items: { type: "array", description: "消费项目列表(不包含税费、合计等)", items: { type: "object", description: "单个消费项目", properties: { name: { type: "string", description: "项目名称" }, time: { type: "string", description: "项目时间(ISO-8601),若无则为空字符串" }, amount: { type: "number", description: "项目金额" } } } }, total: { type: "number", description: "合计金额,若不存在则为 -1" } } }, { provider: "openai" } ) // 建议在业务层将空字符串 / -1 归一化为 null console.log(parsed) ``` *** ### 示例二:生成数组结构 ```ts type Expense = { name: string amount: number } const expenses = await Assistant.requestStructuredData( "列出三项常见的日常支出及其大致金额", { type: "array", description: "支出列表", items: { type: "object", description: "单项支出", properties: { name: { type: "string", description: "支出名称" }, amount: { type: "number", description: "金额" } } } }, { provider: "gemini" } ) ``` *** ### 示例三:结合图片生成结构化结果 ```ts type ImageSummary = { description: string containsText: boolean } const summary = await Assistant.requestStructuredData( "分析这张图片的主要内容", ["data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."], { type: "object", description: "图片分析结果", properties: { description: { type: "string", description: "图片内容描述" }, containsText: { type: "boolean", description: "是否包含可识别文本" } } }, { provider: "openai" } ) ``` *** ## 使用建议与注意事项 - 当返回结果用于业务逻辑时,优先使用 `requestStructuredData` - Schema 描述越明确,结果越稳定 - 复杂业务规则不要放在 Schema 中,应由业务代码处理 --- url: /doc_v2/TestFlight/zh/guide/AssistantTool/Assistant Tool With User Approval.md --- # 需要用户批准的 Assistant Tool 在 Scripting 中实现 **需要用户批准(Approval)** 的 Assistant Tool。 这类工具通常涉及 **隐私、权限、数据修改或不可逆操作**,必须在执行前获得用户明确同意。 *** ## 一、何时必须使用 Approval 模式 只要工具满足以下任一条件,就应使用 Approval 模式: - 访问用户隐私数据(定位、照片、联系人、日历、健康数据等) - 触发系统权限请求 - 修改、删除或批量写入用户文件 - 行为结果对用户具有长期或不可逆影响 - 用户有必要在执行前理解“将发生什么” Approval 的核心目标是 **让用户知情并确认**。 *** ## 二、assistant\_tool.json 配置要求 需要 Approval 的工具必须在配置文件中声明: ```json { "displayName": "Request Current Location", "id": "request_current_location", "description": "Requests the user's current location one time.", "icon": "location.fill", "color": "systemBlue", "parameters": [], "requireApproval": true, "autoApprove": true, "scriptEditorOnly": false } ``` ### 关键字段说明 - `requireApproval: true` 表示工具执行前必须进入批准流程(弹出批准对话框或走自动批准逻辑)。 - `autoApprove`(重要:工具层面的自动批准许可开关) `autoApprove` 的含义是:**该工具是否允许被“自动批准”**。 自动批准是否真的发生,取决于两个条件同时满足: - 用户在 **chat preset** 中开启了 “auto-approve tools / 自动批准工具” - 该工具在 `assistant_tool.json` 中配置了 `autoApprove: true` 结论是: - 用户开启 auto-approve,但工具 `autoApprove: false` → **仍然必须弹窗让用户手动确认** - 工具 `autoApprove: true`,但用户没开启 auto-approve → **仍然必须弹窗让用户手动确认** - 用户开启 auto-approve 且工具 `autoApprove: true` → **工具会自动批准并直接执行** - `scriptEditorOnly` 决定该工具是否仅在脚本编辑器环境中可用。 *** ## 三、Approval 的两阶段模型 需要 Approval 的 Assistant Tool 分为两个阶段: 1. **Approval Request 阶段** 负责生成批准对话框内容(或用于自动批准时的“解释文本”)。 2. **Execute With Approval 阶段** 在用户确认(或自动批准)后执行真实逻辑,并返回执行结果。 对应实现分别使用两个注册函数: - `AssistantTool.registerApprovalRequest` - `AssistantTool.registerExecuteToolWithApproval` *** ## 四、注册 Approval Request 函数 ### 注册方式 ```ts AssistantTool.registerApprovalRequest

(requestFn) ``` ### 函数签名 ```ts type AssistantToolApprovalRequestFn

= ( params: P, scriptEditorProvider?: ScriptEditorProvider ) => Promise<{ title?: string message: string previewButton?: { label: string action: () => void } primaryButtonLabel?: string secondaryButtonLabel?: string }> ``` *** ### Approval Request 的设计原则 - `message` 必须清晰描述“将要做什么”与“为什么需要批准” - 文案面向用户表达,避免内部实现细节 - **此阶段不应执行任何有副作用的操作** - 所有真实执行逻辑必须放在 execute 阶段 *** ### 基本示例 ```ts const approvalRequest: AssistantToolApprovalRequestFn<{}> = async () => { return { message: "The assistant wants to request your current location.", primaryButtonLabel: "Allow", secondaryButtonLabel: "Cancel" } } AssistantTool.registerApprovalRequest(approvalRequest) ``` *** ## 五、Preview Button 的使用场景 `previewButton` 用于在用户批准前展示“预期结果”,提升可预期性与信任度。 适合使用 preview 的场景: - 文件修改(展示 diff) - 批量编辑结果预览 - 将要导出的数据摘要 ### 示例:展示文件 diff ```ts const approvalRequest: AssistantToolApprovalRequestFn<{ path: string }> = async ( params, editor ) => { if (!editor) { return { message: "This tool must be used in the script editor." } } const current = await editor.getFileContent(params.path) return { message: `The assistant wants to modify ${params.path}.`, primaryButtonLabel: "Apply Changes", secondaryButtonLabel: "Cancel", previewButton: { label: "Preview Diff", action: () => { if (current != null) { editor.openDiffEditor(params.path, current + "\n// New content") } } } } } ``` *** ## 六、注册 Execute With Approval 函数 ### 注册方式 ```ts AssistantTool.registerExecuteToolWithApproval

(executeFn) ``` ### 函数签名 ```ts type AssistantToolExecuteWithApprovalFn

= ( params: P, userAction: UserActionForApprovalRequest, scriptEditorProvider?: ScriptEditorProvider ) => Promise<{ success: boolean message: string }> ``` *** ## 七、userAction 的处理规范 ```ts type UserActionForApprovalRequest = { primaryConfirmed: boolean secondaryConfirmed: boolean } ``` 处理原则: - 仅当 `primaryConfirmed === true` 时执行真实逻辑 - `secondaryConfirmed === true` 通常表示用户取消 - 两者都为 `false` 可视为未确认、关闭对话框或流程中断 ### 示例:定位请求(含取消分支) ```ts const executeWithApproval: AssistantToolExecuteWithApprovalFn<{}> = async ( params, { primaryConfirmed } ) => { if (!primaryConfirmed) { return { success: false, message: "User cancelled the location request." } } try { const location = await Location.requestCurrent() return { success: true, message: [ "User location retrieved successfully.", `${location.latitude}`, `${location.longitude}` ].join("\n") } } catch { return { success: false, message: "Failed to retrieve user location." } } } ``` *** ## 八、autoApprove 与 userAction 的关系(行为层面) 在“自动批准”真正发生时(用户 preset 开启 auto-approve 且工具 `autoApprove: true`): - 系统会跳过用户手动点击过程,直接进入 execute 阶段 - execute 阶段依然会收到 `userAction`,通常等价于“主按钮已确认”的语义(例如 `primaryConfirmed: true`) 因此,你的 execute 逻辑应始终以 `primaryConfirmed` 作为“是否允许执行”的唯一门槛,这样可以同时兼容: - 用户手动点击 Allow - 系统自动批准并执行 - 用户点击 Cancel 或未确认 *** ## 九、scriptEditorProvider 在 Approval 模式下的使用 - 当 `scriptEditorOnly: true`: - approval request 和 execute 都会收到 `scriptEditorProvider` - 可用于 `openDiffEditor`、批量读写、lint 信息等 - 当 `scriptEditorOnly: false`: - `scriptEditorProvider` 可能为 `undefined` - 不应依赖编辑器能力 *** ## 十、返回 message 的 UX 建议 需要 Approval 的工具,返回文案需要更明确: - 用户取消:清晰说明已取消 - 成功:摘要 + 必要的结构化字段 - 失败:可操作建议(例如提示检查权限) 示例: ```text Location request was cancelled by the user. ``` ```text Location retrieved successfully. 39.9042 116.4074 ``` *** ## 十一、测试函数的使用 注册后返回测试方法用于调试: ```ts testApprovalFn({}) testExecuteFn({}, { primaryConfirmed: true, secondaryConfirmed: false }) ``` 它用于验证逻辑路径与返回值格式,不等同于真实 UI 批准流程。 *** ## 十二、设计建议总结 - Approval 的本质是“知情 + 可控” - `autoApprove` 是 **工具层面的允许自动批准开关**,必须与用户 preset 同时开启才会自动执行 - approval request 阶段只做说明与预览,不做副作用 - execute 阶段必须严格遵循 `primaryConfirmed` 才执行真实操作 - preview 对编辑器类工具非常关键:优先提供 diff/摘要预览 --- url: /doc_v2/TestFlight/zh/guide/AssistantTool/Assistant Tool Without User Approval.md --- # 无需用户批准的 Assistant Tool 无需 Approval 的 Assistant Tool 通常用于**低风险、无副作用或纯逻辑处理类能力**,可被 Assistant 直接调用并立即执行。 *** ## 一、适用场景与设计边界 在选择“无需 Approval”实现方式前,应明确以下设计原则。 ### 适合使用无需 Approval 的场景 - 不涉及任何系统权限或隐私数据 - 不读取或修改用户的敏感信息 - 不会对用户产生不可逆的影响 - 纯计算、纯解析、纯生成类任务 常见示例包括: - 文本或代码格式化 - 结构化数据解析(JSON / YAML / CSV 等) - 根据输入参数生成模板代码 - 在脚本编辑器中做安全、可预测的文件修改(如补全注释) *** ### 不适合使用无需 Approval 的场景 - 访问定位、照片、联系人、日历等隐私能力 - 写入或覆盖用户文件,且无法回滚 - 会触发系统弹窗或权限请求 - 行为结果不易被用户预期 上述场景应使用 **需要 Approval 的 Assistant Tool**(见文档3)。 *** ## 二、assistant\_tool.json 配置 无需 Approval 的工具在配置文件中必须明确声明: ```json { "displayName": "Format Script", "id": "format_script", "description": "Formats the current script files according to the project style.", "icon": "wand.and.stars", "color": "systemIndigo", "parameters": [], "requireApproval": false, "autoApprove": false, "scriptEditorOnly": true } ``` ### 关键字段说明 - `requireApproval: false` 表示该工具执行时不会弹出用户批准对话框。 - `autoApprove` 在此模式下通常无实际意义,可设置为 `false`。 - `scriptEditorOnly` 决定该工具是否仅在脚本编辑器中可用。 若为 `true`,执行函数将接收到 `ScriptEditorProvider`。 *** ## 三、执行函数注册方式 无需 Approval 的工具只需要注册一个执行函数: ```ts AssistantTool.registerExecuteTool

(executeFn) ``` 对应的函数类型为: ```ts type AssistantToolExecuteFn

= ( params: P, scriptEditorProvider?: ScriptEditorProvider ) => Promise<{ success: boolean message: string }> ``` *** ## 四、最小实现示例(无参数) ```ts type FormatParams = {} const formatScript: AssistantToolExecuteFn = async ( params, scriptEditorProvider ) => { if (!scriptEditorProvider) { return { success: false, message: "This tool can only be used inside the script editor." } } const files = scriptEditorProvider.getAllFiles() for (const file of files) { const content = await scriptEditorProvider.getFileContent(file) if (!content) continue const formatted = content.trim() await scriptEditorProvider.updateFileContent(file, formatted) } return { success: true, message: "All script files have been formatted successfully." } } const testFormatTool = AssistantTool.registerExecuteTool(formatScript) ``` *** ## 五、带参数的实现示例 ### 参数定义 ```ts type ReplaceParams = { searchText: string replaceText: string } ``` ### 执行逻辑 ```ts const replaceInScripts: AssistantToolExecuteFn = async ( params, scriptEditorProvider ) => { if (!scriptEditorProvider) { return { success: false, message: "Script editor context is required." } } const files = scriptEditorProvider.getAllFiles() let affectedFiles = 0 for (const file of files) { const content = await scriptEditorProvider.getFileContent(file) if (!content) continue if (!content.includes(params.searchText)) continue const updated = content.replaceAll( params.searchText, params.replaceText ) await scriptEditorProvider.updateFileContent(file, updated) affectedFiles++ } return { success: true, message: `Replaced text in ${affectedFiles} file(s).` } } AssistantTool.registerExecuteTool(replaceInScripts) ``` *** ## 六、返回结果(message)的设计规范 执行函数返回的 `message` 是 **Assistant 后续推理与回复的输入**,应遵循以下建议: - 第一行给出明确的结果摘要 - 避免返回冗长、未结构化的内容 - 必要时用多行文本或轻量标签结构化信息 推荐模式: ```text Operation completed successfully. Affected files: 3 ``` 或: ```text Replaced content summary: 3 foo bar ``` *** ## 七、错误处理建议 - 所有可预期错误应转化为 `{ success: false, message }` - 不要抛出未捕获异常 - 错误信息应可读、可定位问题,但避免泄露内部实现细节 示例: ```ts return { success: false, message: "Invalid parameters: searchText cannot be empty." } ``` *** ## 八、进度反馈(可选) 对于耗时较长的工具,可使用: ```ts AssistantTool.report("Formatting file: index.tsx") ``` 建议仅在关键阶段调用,避免频繁上报。 *** ## 九、测试函数的使用 `registerExecuteTool` 返回的测试函数可直接在脚本编辑器中运行: ```ts testFormatTool({}) ``` 测试函数的作用包括: - 验证参数是否正确映射 - 验证逻辑执行是否符合预期 - 在无需 Assistant 会话的情况下调试工具 *** ## 十、最佳实践总结 - 无需 Approval ≠ 无风险,谨慎评估工具影响 - 尽量让工具行为**可预测、可重复** - 避免隐式副作用和隐藏修改 - 返回信息应服务于 Assistant 的下一步决策 --- url: /doc_v2/TestFlight/zh/guide/AssistantTool/AssistantTool User-Initiated Cancellation.md --- # 处理用户主动取消智能助手工具 为提升长时间运行工具的用户体验,AssistantTool 新增了 **用户主动取消(Cancel)** 支持。 当用户在工具执行过程中点击“取消”时,开发者可以选择性地通过 `onCancel` 回调返回已完成的部分结果;如果未实现该回调,系统会自动处理取消逻辑,开发者无需额外处理。 该机制适用于搜索、分析、爬取、批处理、流式生成等耗时或多阶段工具。 *** ## 能力概述 新增能力包含以下 API: ```ts type OnCancel = () => string | null | undefined var onCancel: OnCancel | null | undefined const isCancelled: boolean ``` *** ## 核心语义说明 ### onCancel 是可选实现 - 开发者可以选择实现 `onCancel` - 不实现 `onCancel` 也是完全正确、被官方支持的用法 当开发者没有设置 `onCancel` 时: - 用户点击“取消” - 工具会被系统标记为已取消 - 执行函数后续返回的任何结果都会被系统忽略 - Assistant 不会消费这些返回值 结论是,开发者无需为“用户取消”编写任何额外逻辑,也不会产生错误行为。 *** ### 实现 onCancel 的目的 实现 `onCancel` 的唯一目的,是在用户取消时主动返回“已经完成的部分结果”,以提升用户体验。 这是一种增强能力,而不是强制要求。 *** ## isCancelled 的语义 - `AssistantTool.isCancelled` 在用户取消后立即变为 `true` - 该值在执行函数内随时可读取 - 用于控制是否继续执行后续步骤、循环或资源占用操作 *** ## onCancel 的注册时机 `onCancel` 必须在工具的执行函数内部注册。 原因包括: - 工具执行完成后,系统会自动将 `onCancel` 设为 `null` - 每次执行都是独立的生命周期 - 在执行函数外注册不会生效 可注册的位置包括: - `registerExecuteTool` - `registerExecuteToolWithApproval` *** ## 不实现 onCancel 的最小示例 ```ts const executeTool = async () => { await doSomethingSlow() return { success: true, message: "Operation completed." } } ``` 行为说明: - 用户点击取消后,工具被系统判定为已取消 - 上述返回结果会被自动忽略 - 不需要额外判断或清理逻辑 *** ## 实现 onCancel 并返回部分结果的示例 ```ts const executeTool = async () => { const partialResults: string[] = [] AssistantTool.onCancel = () => { return [ "Operation was cancelled by the user.", "Partial results:", ...partialResults ].join("\n") } for (const item of items) { if (AssistantTool.isCancelled) break const result = await process(item) partialResults.push(result) } return { success: true, message: partialResults.join("\n") } } ``` 行为说明: - 用户取消时,`onCancel` 会被立即调用 - 已完成的 `partialResults` 会作为 message 返回给 Assistant - 后续执行通过 `isCancelled` 判断及时停止 *** ## 典型使用场景 适合实现 `onCancel` 的工具类型包括: - 多源搜索与聚合 - 多篇文章抓取与解析 - 项目级扫描与分析 - 批量计算或生成任务 - 长时间运行的推理流程 不适合或不需要实现 `onCancel` 的场景包括: - 瞬时完成的工具 - 没有中间结果的操作 - 取消后没有任何可返回内容的任务 *** ## onCancel 的返回值规范 `onCancel` 的返回值含义如下: - 返回 `string` 该字符串会作为工具被取消时返回给 Assistant 的 `message` - 返回 `null` 或 `undefined` 表示不返回任何消息,属于合法行为,但通常不推荐 推荐的实践是: - 明确说明工具已被用户取消 - 若返回部分结果,清楚标注为“Partial results / 已完成部分” *** ## 与 Approval 流程的关系 - `onCancel` 发生在执行阶段 - 不影响 Approval Request 阶段 - 适用于手动批准与 autoApprove 自动批准后的执行过程 需要区分的两个概念: - `secondaryConfirmed` 表示用户在批准阶段拒绝或取消执行 - `onCancel` 表示用户在执行过程中中途取消 *** ## 常见错误与注意事项 ### 在执行函数外注册 onCancel 这是无效的用法,因为执行上下文已经结束。 *** ### 在 onCancel 中执行耗时或副作用操作 `onCancel` 应快速返回,不应发起网络请求、文件写入或其他副作用操作。 *** ### 忽略 isCancelled 继续执行 即使实现了 `onCancel`,也应在长流程中显式检查 `isCancelled`,避免在取消后继续消耗资源。 *** ## 推荐的执行结构 ```ts const executeTool = async () => { const partial = [] AssistantTool.onCancel = () => { return formatPartialResult(partial) } for (const item of items) { if (AssistantTool.isCancelled) break const r = await process(item) partial.push(r) } return { success: true, message: formatFinalResult(partial) } } ``` *** ## 总结 - 用户取消是一种正常的用户行为,而不是异常 - `onCancel` 是一种可选的体验增强能力 - 不实现 `onCancel` 不会破坏工具行为 - 系统会自动忽略取消后的执行结果 - 实现 `onCancel` 只是为了更优雅地收尾 --- url: /doc_v2/TestFlight/zh/guide/AssistantTool/Overview and Workflow.md --- # AssistantTool 总览与工作流 Assistant Tool 是 Scripting 中提供给 Assistant 的“可扩展工具机制”。它让脚本作者把某些能力封装成结构化工具,由 Assistant 在对话中按需调用,并把执行结果以文本 `message` 的形式回传给 Assistant 继续推理和回复用户。 AssistantTool 的设计目标是两点: - 让 Assistant 获得额外能力(例如定位、文件处理、项目内批量编辑、数据处理),但仍保持可控与可审计(尤其是敏感能力要走用户批准)。 - 让工具具备明确输入、明确输出和可测试性(注册函数返回 testFn)。 *** ## AssistantTool 的两种执行类型 从实现角度,AssistantTool 有两条主路径,对应你 API 里的两个注册函数: ### 不需要用户批准(No Approval) - `assistant_tool.json`:`requireApproval: false` - 代码侧注册: - `AssistantTool.registerExecuteTool

(executeFn)` 适合: - 不涉及隐私/权限/破坏性操作 - 纯计算、纯格式化、纯解析、纯生成内容 - 对用户设备状态没有敏感读取 ### 需要用户批准(With Approval) - `assistant_tool.json`:`requireApproval: true` - 代码侧注册: - `AssistantTool.registerApprovalRequest

(requestFn)` 生成批准弹窗内容 - `AssistantTool.registerExecuteToolWithApproval

(executeFn)` 在用户点按钮后执行 适合: - 任何可能涉及隐私/权限(定位、照片、联系人、日历等) - 可能修改用户数据、文件、或产生副作用的操作 - 希望用户先看预览再决定(`previewButton`) > `autoApprove`:该工具是否允许被“自动批准” *** ## ScriptEditorOnly 模式(是否只在脚本编辑器可用) `assistant_tool.json` 的 `scriptEditorOnly` 控制这个工具是否“仅限脚本编辑器环境”。 - `scriptEditorOnly: false` - 工具可在普通对话环境中被调用(面向终端用户) - `scriptEditorProvider` 通常不会提供 - `scriptEditorOnly: true` - 工具只在脚本编辑器里可用(面向脚本作者/开发者) - 执行函数会额外拿到 `scriptEditorProvider?: ScriptEditorProvider` - 用于“编辑器内工具”:批量修改文件、插入/替换内容、打开 diff、读取 lint 错误等 *** ## 工具创建与文件结构 创建工具后,会生成两个核心文件: - `assistant_tool.json` - 定义工具元信息、参数结构、是否需要批准、是否仅编辑器可用等 - `assistant_tool.tsx` - 编写工具逻辑,并用 `AssistantTool.register...` 系列函数注册 ### assistant\_tool.json(元信息的职责边界) `assistant_tool.json` 的职责是: - 给 UI 展示用(displayName/icon/color/description) - 给工具路由用(id 唯一标识) - 给调用约束用(parameters/requireApproval/autoApprove/scriptEditorOnly) 它不承载执行逻辑,执行逻辑永远在 `assistant_tool.tsx` 里。 *** ## 参数输入与类型约定 工具入参来自 `assistant_tool.json` 的 `parameters`,最终会以 `params: P` 传给: - `AssistantToolApprovalRequestFn

(params, scriptEditorProvider?)` - `AssistantToolExecuteWithApprovalFn

(params, userAction, scriptEditorProvider?)` - `AssistantToolExecuteFn

(params, scriptEditorProvider?)` 这里的关键约定是: - `P` 是你在 `assistant_tool.tsx` 里声明的参数类型(例如 `type MyParams = { ... }`) - 实际运行时系统负责把 JSON 参数映射成 `params` - 如果工具没有参数,则 `P = {}` *** ## Assistant 调用工具的典型流程 下面用“运行期”的视角描述一次完整链路(不包含 testFn): ### A. Assistant 决定调用哪个工具 Assistant 在对话中基于目标任务和可用工具列表,选择某个 `id` 的工具,并构造参数 `params`。 ### B. 如果 requireApproval = true 1. 系统调用 `registerApprovalRequest` 注册的 `requestFn(params, scriptEditorProvider?)` 2. requestFn 返回对话框内容: - `message` - 可选 `title` - 可选 `previewButton`(用于展示预期输出、diff、或摘要) - 可选按钮文案(primary/secondary) 3. 用户点击按钮后,系统得到 `UserActionForApprovalRequest`: - `primaryConfirmed` - `secondaryConfirmed` 4. 系统调用 `registerExecuteToolWithApproval` 注册的 `executeFn(params, userAction, scriptEditorProvider?)` 5. executeFn 返回 `{ success, message }` 给 Assistant ### C. 如果 requireApproval = false 1. 系统直接调用 `registerExecuteTool` 注册的 `executeFn(params, scriptEditorProvider?)` 2. executeFn 返回 `{ success, message }` 给 Assistant *** ## message 的返回约定(给 Assistant 的“工具输出协议”) 执行函数统一返回: ```ts { success: boolean message: string } ``` 建议把 `message` 当成“可被 Assistant 继续消费的结构化文本”,常见策略: - 简单场景:直接自然语言描述结果 - 需要结构化:用轻量标记(你示例里用 `...`) - 需要多段信息:用换行拼接,第一行做摘要,后续提供字段 不建议在 `message` 返回超长原始内容(例如整项目所有文件内容),更推荐返回摘要 + 指引(或让用户在 UI 中通过 diff/preview 看)。 *** ## 测试函数(testFn)的用途与边界 每个注册函数都会返回一个 testFn,用于“在脚本编辑器中模拟调用”: - `registerApprovalRequest` → `AssistantToolApprovalRequestTestFn

` - `registerExecuteToolWithApproval` → `AssistantToolExecuteWithApprovalTestFn

` - `registerExecuteTool` → `AssistantToolExecuteTestFn

` testFn 的价值: - 快速验证参数映射是否正确 - 验证工具逻辑是否按预期返回 `success/message` - 在开发阶段不依赖真实 Assistant 对话触发 *** ## report(message) 的定位 `AssistantTool.report(message: string, id?: string)` 用于在工具执行期间上报过程信息(例如“正在读取文件…/正在生成 diff…/正在请求定位…”)。`id`参数用于更新已有的报告,如果你的报告是流式生成的,这个参数很有用。 建议用法: - 长耗时任务:阶段性 report,提升可感知性 - 与 UI/日志结合:便于调试与回溯 - 不要高频刷屏:只在关键阶段上报 --- url: /doc_v2/TestFlight/zh/guide/AssistantTool/ScriptEditorProvider and Editor Types.md --- # ScriptEditorProvider 及编辑器相关类型详解 本篇文档详细说明 Assistant Tool 在 **脚本编辑器环境(scriptEditorOnly)** 下可使用的编辑器能力,包括 `ScriptEditorProvider` 接口本身,以及与之配套的 `ScriptEditorFileOperation`、`ScriptLintError` 等类型。 *** ## 一、ScriptEditorProvider 的定位与职责 `ScriptEditorProvider` 是 Assistant Tool 与 **脚本编辑器(Script Editor)** 之间的通信接口。 当满足以下条件时,工具的执行函数会收到该对象: - `assistant_tool.json` 中设置了 `scriptEditorOnly: true` - 工具在脚本编辑器环境中被执行(包括测试函数) 它的核心职责是: - 提供对脚本项目文件系统的受控访问 - 支持结构化、可追踪的文件修改 - 提供 lint / 语法检查结果 - 支持 diff 预览而非直接破坏性修改 *** ## 二、项目级信息接口 ### `scriptName` ```ts readonly scriptName: string ``` 表示当前脚本项目的名称。 常见用途: - 在返回 `message` 中标注作用范围 - 在日志或 `AssistantTool.report` 中显示上下文信息 *** ## 三、文件与目录查询接口 ### 判断文件是否存在 ```ts exists(relativePath: string): boolean ``` - `relativePath` 为相对于脚本项目根目录的路径 - 用于安全检查或条件性创建文件 *** ### 获取所有文件夹 ```ts getAllFolders(): string[] ``` 返回项目中所有文件夹路径(相对路径)。 典型用途: - 批量生成文件 - 判断项目结构 - 构建导航或分组逻辑 *** ### 获取所有文件 ```ts getAllFiles(): string[] ``` 返回项目中所有文件路径(相对路径)。 典型用途: - 全项目扫描 - 批量格式化 / 搜索 / 替换 - lint 错误定位 *** ## 四、文件内容读取与写入 ### 读取文件内容 ```ts getFileContent(relativePath: string): Promise ``` - 文件不存在时返回 `null` - 建议在调用前用 `exists` 或空值判断 *** ### 更新整个文件内容 ```ts updateFileContent(relativePath: string, content: string): Promise ``` - 用新内容完全替换原文件 - 适合格式化、重写等确定性操作 - 不建议在复杂编辑场景下频繁使用 *** ### 写入文件(自动创建) ```ts writeToFile(relativePath: string, content: string): Promise ``` - 文件不存在时自动创建 - 文件存在时覆盖 - 常用于生成新文件或模板 *** ## 五、结构化编辑接口(推荐) 相比直接整体替换文件内容,**结构化编辑接口更安全、更可控**。 *** ### ScriptEditorFileOperation 类型 ```ts type ScriptEditorFileOperation = { startLine: number content: string } ``` 语义说明: - `startLine`:**基于 1 的行号** - `content`:要插入或替换的文本内容 - 不包含结束行,具体行为由调用接口决定 *** ### 插入内容 ```ts insertContent( relativePath: string, operations: ScriptEditorFileOperation[] ): Promise ``` 行为说明: - 在指定行号 **之前** 插入内容 - 多个 operation 按数组顺序依次执行 - 行号以原始文件为基准,建议从后往前插入以避免偏移 适合场景: - 插入 import / 注释 / 新函数 - 自动补充代码块 *** ### 替换内容 ```ts replaceInFile( relativePath: string, operations: ScriptEditorFileOperation[] ): Promise ``` 行为说明: - 从 `startLine` 开始替换对应行内容 - 通常用于精确替换某一行或一段代码 - 不适合模糊搜索型替换 *** ## 六、Diff 预览接口 ### `openDiffEditor` ```ts openDiffEditor(relativePath: string, content: string): void ``` 该方法用于 **在不真正写入文件的前提下**,向用户展示: - 当前文件内容 vs 预期新内容 - 清晰的改动范围 推荐用法: - Approval Request 阶段 - 作为 previewButton 的 action - 所有批量或破坏性修改前 *** ## 七、Lint 与语法错误信息 ### ScriptLintError 类型 ```ts type ScriptLintError = { line: number message: string } ``` 表示脚本中的一个 lint 或语法错误。 *** ### 获取 lint 错误 ```ts getLintErrors(): Record ``` 返回结构: - key:文件路径(relativePath) - value:该文件中的 lint 错误数组 *** ### 典型使用模式 - 扫描所有 lint 错误 - 定位错误行 - 自动修复简单问题(需谨慎) - 汇总错误信息返回给 Assistant 示例: ```ts const errors = editor.getLintErrors() for (const file in errors) { for (const error of errors[file]) { // error.line // error.message } } ``` *** ## 八、ScriptEditorProvider 的使用边界 重要约束与建议: - 所有路径必须是 **相对路径** - 不应假设文件内容一定存在 - 不要并发修改同一个文件 - 尽量使用结构化编辑接口而非全文替换 - 批量修改时优先提供 diff 预览 *** ## 九、编辑器类 Assistant Tool 的推荐模式 一个成熟的编辑器类工具通常遵循以下流程: 1. 使用 `getAllFiles` / `getLintErrors` 扫描项目 2. 计算将要发生的修改 3. 在 Approval Request 阶段: - 提供清晰说明 - 使用 `openDiffEditor` 作为 preview 4. 在 Execute 阶段: - 严格按确认结果执行 - 使用结构化编辑 API 5. 返回简洁、结构化的执行结果 *** ## 十、小结 - `ScriptEditorProvider` 是 Assistant Tool 与脚本编辑器之间的桥梁 - 它提供 **受控、结构化、可预览** 的文件操作能力 - 编辑器类工具应优先考虑用户可理解性与可回滚性 - 结合 Approval + preview,可以构建高信任度的编辑体验 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.3/Animation and Transition.md --- # 动画和过渡 Scripting 通过 `Observable` / `useObservable`、`Animation`、`Transition`、`withAnimation` 以及视图的 `animation` / `transition` 属性,基本对齐了 SwiftUI 的动画能力,包括: - **属性动画**:数值、颜色、布局等属性随状态变化平滑过渡 - **过渡动画**:视图插入 / 移除时的进出效果(如淡入淡出、滑入滑出、翻转) - **显式动画**:通过 `withAnimation` 包裹一段「状态更新代码」统一加动画 ## Animation 类 `Animation` 用来描述「属性变化的时间曲线与节奏」,类似 SwiftUI 的 `Animation`。 ### 工厂方法(创建动画) #### `Animation.default()` ```ts static default(): Animation ``` - 创建一个默认动画(通常是系统预设的 ease-in-out 曲线) - 无需配置,适合「只想要一个普通的过渡效果」的场景 示例: ```tsx 默认动画 ``` *** #### `Animation.linear(duration?)` ```ts static linear(duration?: DurationInSeconds | null): Animation ``` - 匀速动画,整段时间内速度保持恒定 - `duration`:动画持续时间(秒),可选,不传时使用默认时长 适合:进度条数值增长、颜色线性变化等。 *** #### `Animation.easeIn(duration?)` ```ts static easeIn(duration?: DurationInSeconds | null): Animation ``` - 开始慢、后面加速 - 适合:元素「加速进入」的感觉 *** #### `Animation.easeOut(duration?)` ```ts static easeOut(duration?: DurationInSeconds | null): Animation ``` - 开始快、结尾慢 - 适合:元素「减速停止」的感觉,如卡片滑入后停在目标位置 *** #### `Animation.bouncy(options?)` ```ts static bouncy(options?: { duration?: DurationInSeconds extraBounce?: number }): Animation ``` - 带回弹效果的动画 - 参数: - `duration`:总时长(秒) - `extraBounce`:额外弹性,越大越明显 适合:按钮点击放大回弹、卡片弹出等「有趣」的动效。 *** #### `Animation.smooth(options?)` ```ts static smooth(options?: { duration?: DurationInSeconds extraBounce?: number }): Animation ``` - 相对柔和、过渡自然的动画 - 与 `bouncy` 相比,弹性感更弱,更偏「丝滑」 *** #### `Animation.snappy(options?)` ```ts static snappy(options?: { duration?: DurationInSeconds extraBounce?: number }): Animation ``` - 动作「干脆利落」,响应速度快 - 常见于触控反馈、选中高亮等瞬间反馈场景 *** #### `Animation.spring(options?)` ```ts static spring(options?: { blendDuration?: number } & ({ duration?: DurationInSeconds bounce?: number response?: never dampingFraction?: never } | { response?: number dampingFraction?: number duration?: never bounce?: never })): Animation ``` 支持两种配置方式(注意互斥): 1. **基于时长的弹簧动画** - `duration`: 动画持续时间 - `bounce`: 弹性大小 2. **物理参数模式** - `response`: 响应速度(值越小反馈越快) - `dampingFraction`: 阻尼系数(0\~1,越大越「稳」,越小越「弹」) 额外参数: - `blendDuration`:动画混合时长,用于多动画衔接场景(可选) 示例: ```tsx // 简单弹簧 const anim1 = Animation.spring({ duration: 0.4, bounce: 0.3 }) // 高级弹簧 const anim2 = Animation.spring({ response: 0.25, dampingFraction: 0.7 }) ``` *** #### `Animation.interactiveSpring(options?)` ```ts static interactiveSpring(options?: { response?: number dampingFraction?: number blendDuration?: number }): Animation ``` - 面向「交互驱动」的弹簧动画,例如拖拽结束后的回弹 - 参数与 `spring` 的物理参数模式类似,语义更偏向手势交互 *** #### 0 `Animation.interpolatingSpring(options?)` ```ts static interpolatingSpring(options?: { mass?: number stiffness: number damping: number initialVelocity?: number } | { duration?: DurationInSeconds bounce?: number initialVelocity?: number mass?: never stiffness?: never damping?: never }): Animation ``` 两种配置方式(互斥): 1. **物理参数模式** - `mass`: 质量 - `stiffness`: 刚度 - `damping`: 阻尼 - `initialVelocity`: 初速度(可选) 2. **时长 + 弹性模式** - `duration`: 动画时长 - `bounce`: 弹性 - `initialVelocity`: 初速度(可选) 适合对动态效果「非常在意手感」的高级场景。 *** ### 修改已有动画(链式 API) #### `delay(time)` ```ts delay(time: DurationInSeconds): Animation ``` - 使动画延迟 `time` 秒后再开始 - 返回一个新的 `Animation` 实例(原动画不变) 示例: ```tsx const [animValue, setAnimValue] = useState(0) const anim = Animation .spring({ duration: 0.4, bounce: 0.3 }) .delay(0.2) 延迟弹簧 ``` *** #### `repeatCount(count, autoreverses?)` ```ts repeatCount(count: number, autoreverses?: boolean): Animation ``` - 重复执行动画 `count` 次 - `autoreverses`(默认 `true`):是否来回反向播放 示例: ```tsx const pulse = Animation .easeIn(0.6) .repeatCount(3, true) 闪烁三次 ``` *** #### `repeatForever(autoreverses?)` ```ts repeatForever(autoreverses?: boolean): Animation ``` - 无限次重复动画 - 适合加载动画、呼吸灯效果等 *** ### Animation 实战示例 #### 示例 1:基本大小动画 ```tsx import { VStack, Button, Rectangle, useObservable, } from "scripting" export function Demo() { const size = useObservable(80) return `) await webView.present({ navigationTitle: '网页视图示例' }) webView.dispose() ``` --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/Device/index.md --- # 设备 `Device` 命名空间提供对当前设备硬件、系统环境、语言区域、屏幕信息、电池状态、方向感知、接近传感器以及网络接口等信息的访问能力,并提供相关状态变化的监听接口。 该 API 主要用于根据设备环境动态调整 UI、行为逻辑或系统能力使用方式。 *** ## Orientation 表示设备当前的物理朝向。 ```ts type Orientation = | "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight" | "faceUp" | "faceDown" | "unknown" ``` ### 说明 - `portrait`:竖屏,Home 键在下(或标准竖屏方向) - `portraitUpsideDown`:竖屏倒置 - `landscapeLeft`:横屏,设备向左旋转 - `landscapeRight`:横屏,设备向右旋转 - `faceUp`:设备平放,屏幕朝上 - `faceDown`:设备平放,屏幕朝下 - `unknown`:无法确定方向 *** ## InterfaceOrientation 表示App UI 的可旋转方向 ```ts type InterfaceOrientation = | "portrait" | "portraitUpsideDown" | "landscape" | "landscapeLeft" | "landscapeRight" | "all" | "allButUpsideDown" ``` ### 说明 - `portrait`:竖屏,Home 键在下(或标准竖屏方向) - `portraitUpsideDown`:竖屏倒置 - `landscape`:横屏,设备向左旋转 - `landscapeLeft`:横屏,设备向左旋转 - `landscapeRight`:横屏,设备向右旋转 - `all`:所有可旋转方向 - `allButUpsideDown`:除了竖屏,所有可旋转方向 *** ## NetworkInterface 描述单个网络接口地址的信息。 ```ts type NetworkInterface = { address: string netmask: string | null family: "IPv4" | "IPv6" mac: string | null isInternal: boolean cidr: string | null } ``` ### 字段说明 - `address`:IP 地址 - `netmask`:子网掩码 - `family`:地址族,IPv4 或 IPv6 - `mac`:MAC 地址(部分系统或接口可能为 null) - `isInternal`:是否为内部接口(如 loopback) - `cidr`:CIDR 表示形式,例如 `192.168.1.10/24` *** ## BatteryState 表示当前电池状态。 ```ts type BatteryState = "full" | "charging" | "unplugged" | "unknown" ``` ### 说明 - `full`:电量已充满 - `charging`:正在充电 - `unplugged`:未连接电源 - `unknown`:无法确定状态 *** ## Device Information ### model ```ts const model: string ``` 设备型号,例如 `"iPhone"`、`"iPad"`。 *** ### localizedModel ```ts const localizedModel: string ``` 本地化后的设备型号名称。 *** ### systemVersion ```ts const systemVersion: string ``` 当前操作系统版本号,例如 `"18.2"`。 *** ### systemName ```ts const systemName: string ``` 操作系统名称,例如 `"iOS"`、`"iPadOS"`、`"macOS"`。 *** ### isiPad / isiPhone ```ts const isiPad: boolean const isiPhone: boolean ``` 指示当前设备是否为 iPad 或 iPhone。 *** ### screen ```ts const screen: { width: number height: number scale: number } ``` 屏幕信息: - `width`:屏幕宽度(逻辑像素) - `height`:屏幕高度(逻辑像素) - `scale`:屏幕缩放比例(如 2、3) *** ## Battery & Sensors ### batteryState ```ts const batteryState: BatteryState ``` 当前电池状态。 *** ### batteryLevel ```ts const batteryLevel: number ``` 当前电量百分比,范围为 `0.0` 到 `1.0`。 *** ### proximityState ```ts const proximityState: boolean ``` 接近传感器状态,`true` 表示设备靠近用户(例如通话时贴近耳朵)。 *** ## Orientation & Layout ### isLandscape / isPortrait / isFlat ```ts const isLandscape: boolean const isPortrait: boolean const isFlat: boolean ``` - `isLandscape`:是否处于横屏 - `isPortrait`:是否处于竖屏 - `isFlat`:设备是否平放(face up / face down) *** ### orientation ```ts const orientation: Orientation ``` 当前设备物理方向。 *** ### supportedInterfaceOrientations ```ts var supportedInterfaceOrientations: InterfaceOrientation[] ``` 获取和设置当前支持的旋转方向。 #### 示例 ```tsx function Page() { useEffect(() => { Device.supportedInterfaceOrientations = ["all"] return () => { Device.supportedInterfaceOrientations = ["portrait"] } }, []) return ... } ``` ## Appearance & Environment ### colorScheme ```ts const colorScheme: ColorScheme ``` 当前系统颜色模式,例如浅色或深色模式。 *** ### isiOSAppOnMac ```ts const isiOSAppOnMac: boolean ``` 指示当前进程是否为运行在 macOS 上的 iPhone / iPad App(Mac Catalyst 或 iOS App on Mac)。 *** ## Locale & Language ### systemLocale ```ts const systemLocale: string ``` 当前系统 Locale,例如 `"en_US"`。 *** ### preferredLanguages ```ts const preferredLanguages: string[] ``` 用户偏好的语言列表,例如: ```ts ["en-US", "zh-Hans-CN"] ``` *** ### systemLocales(已废弃) ```ts const systemLocales: string[] ``` 已废弃,请使用 `preferredLanguages`。 *** ### systemLanguageTag ```ts const systemLanguageTag: string ``` 语言标签,例如 `"en-US"`。 *** ### systemLanguageCode ```ts const systemLanguageCode: string ``` 语言代码,例如 `"en"`。 *** ### systemCountryCode ```ts const systemCountryCode: string | undefined ``` 国家代码,例如 `"US"`。 *** ### systemScriptCode ```ts const systemScriptCode: string | undefined ``` 书写系统代码,例如 `"Hans"`(简体中文)。 *** ## Wake Lock ### isWakeLockEnabled ```ts const isWakeLockEnabled: Promise ``` 查询当前是否启用了屏幕唤醒锁定(防止设备自动锁屏)。 *** ### setWakeLockEnabled ```ts function setWakeLockEnabled(enabled: boolean): void ``` 启用或禁用 Wake Lock。 说明: - 仅在 **Scripting App** 内可用 - 启用后可防止设备自动休眠 *** ## Battery Listeners ### addBatteryStateListener ```ts function addBatteryStateListener( callback: (state: BatteryState) => void ): void ``` 监听电池状态变化。 *** ### removeBatteryStateListener ```ts function removeBatteryStateListener( callback?: (state: BatteryState) => void ): void ``` 移除电池状态监听器。 - 未传入 `callback` 时将移除所有监听器 *** ### addBatteryLevelListener ```ts function addBatteryLevelListener( callback: (level: number) => void ): void ``` 监听电量变化。 *** ### removeBatteryLevelListener ```ts function removeBatteryLevelListener( callback?: (level: number) => void ): void ``` 移除电量监听器。 *** ## Orientation Listeners ### addOrientationListener ```ts function addOrientationListener( callback: (orientation: Orientation) => void ): void ``` 开始监听设备方向变化。 注意事项: - 必须先调用该方法才能接收方向变化 - 系统方向锁开启时不会生效 *** ### removeOrientationListener ```ts function removeOrientationListener( callback?: (orientation: Orientation) => void ): void ``` 移除方向监听器。 - 未传入 `callback` 时将停止所有方向监听并结束观察 *** ## Proximity Listeners ### addProximityStateListener ```ts function addProximityStateListener( callback: (state: boolean) => void ): void ``` 监听接近传感器状态变化。 *** ### removeProximityStateListener ```ts function removeProximityStateListener( callback?: (state: boolean) => void ): void ``` 移除接近传感器监听器。 *** ## Network ### networkInterfaces ```ts function networkInterfaces(): Record ``` 获取当前设备的网络接口信息。 返回值说明: - Key:接口名称(如 `en0`、`lo0`) - Value:该接口对应的地址信息数组 适用于网络诊断、本地 IP 获取、调试用途。 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/Device/index_example.md --- # 示例 ```tsx import { Button, Device, List, Navigation, NavigationStack, Script, Text, VStack } from "scripting" function Example() { const dismiss = Navigation.useDismiss() const details: { name: string value: string | boolean | number }[] = [ { name: "Device.isiPhone", value: Device.isiPhone }, { name: "Device.isiPad", value: Device.isiPad, }, { name: "Device.systemVersion", value: Device.systemVersion, }, { name: "Device.systemName", value: Device.systemName, }, { name: "Device.isPortrait", value: Device.isPortrait, }, { name: "Device.isLandscape", value: Device.isLandscape, }, { name: "Device.isFlat", value: Device.isFlat, }, { name: "Device.batteryLevel", value: Device.batteryLevel, }, { name: "Device.batteryState", value: Device.batteryState, } ] return }} > {details.map(item => {item.name} {typeof item.value} )} } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/Intent.md --- # Intent Scripting 支持通过 `intent.tsx` 文件创建 iOS Intents,实现脚本与系统分享扩展(Share Sheet)和快捷指令(Shortcuts)的深度集成。你可以接收来自用户的文本、图片、文件和 URL 等输入,并返回结果供调用方使用。通过 UI 展示、数据处理与结果返回,可构建灵活且强大的自动化流程。 *** ## 一、创建和配置 Intent ### 1. 创建 Intent 脚本 1. 在 Scripting 中新建一个脚本项目。 2. 添加名为 `intent.tsx` 的文件,并编写处理逻辑和可选的 UI 组件。 ### 2. 配置支持的输入类型 点击编辑器顶部标题栏中的项目名称,打开 **Intent 设置页面**,选择该脚本支持的输入类型,如: - 文本(Text) - 图片(Image) - 文件路径(File URL) - URL 配置后,该脚本就能在分享扩展或 Shortcuts 中处理相应类型的数据。 *** ## 二、处理输入数据 在 `intent.tsx` 中,可通过以下 API 访问用户传入的数据: | 属性名 | 说明 | | -------------------------- | ---------------------------------------- | | `Intent.shortcutParameter` | Shortcuts 中传入的单个参数,包含 `.type` 和 `.value` | | `Intent.textsParameter` | 文本字符串数组 | | `Intent.urlsParameter` | URL 字符串数组 | | `Intent.imagesParameter` | 图片数组(UIImage 实例) | | `Intent.fileURLsParameter` | 文件路径数组(本地 URL) | 示例: ```ts if (Intent.shortcutParameter) { if (Intent.shortcutParameter.type === "text") { console.log(Intent.shortcutParameter.value) } } ``` *** ## 三、返回结果 使用 `Script.exit(result)` 结束脚本执行并返回结果给调用方,例如 Shortcuts 或另一个脚本。支持的返回类型包括: - 文本:`Intent.text(value)` - 富文本:`Intent.attributedText(value)` - URL:`Intent.url(value)` - JSON 数据:`Intent.json(value)` - 文件路径:`Intent.file(value)` 或 `Intent.fileURL(value)` 示例: ```ts import { Script, Intent } from "scripting" Script.exit(Intent.text("处理完成")) ``` *** ## 四、展示交互式 UI 你可以使用 `Navigation.present()` 呈现一个自定义界面,展示输入信息或收集用户反馈。在 UI 交互结束后调用 `Script.exit()` 返回结果。 示例: ```ts import { Intent, Script, Navigation, VStack, Text } from "scripting" function MyIntentView() { return ( {Intent.textsParameter?.[0]} ) } async function run() { await Navigation.present() Script.exit() } run() ``` *** ## 五、在分享扩展中使用 当脚本项目启用了对应类型的输入支持,Scripting 会自动集成到系统分享菜单: 1. 用户选中内容(如 Safari 中的文字或图片),点击分享按钮。 2. 分享列表中选择 **Scripting**。 3. 显示支持当前输入类型的脚本列表,供用户执行。 *** ## 六、与 Shortcuts 集成 你可以在 Shortcuts 应用中调用 Scripting 脚本: - **运行脚本(Run Script)**:后台执行,无 UI。 - **在 App 中运行脚本(Run Script in App)**:前台执行,支持 UI 展示。 操作步骤: 1. 在 Shortcuts 中添加 “Run Script” 或 “Run Script in App” 操作。 2. 选择目标脚本。 3. 配置参数,执行脚本。 *** ## 七、Intent API 参考 ### `Intent` 类属性 | 属性 | 类型 | 说明 | | ------------------- | ------------------- | ------------------------------------- | | `shortcutParameter` | `ShortcutParameter` | Shortcuts 传入的参数对象,包含 `type` 和 `value` | | `textsParameter` | `string[]` | 文本输入数组 | | `urlsParameter` | `string[]` | URL 字符串数组 | | `imagesParameter` | `UIImage[]` | 图片数组(路径或图片对象) | | `fileURLsParameter` | `string[]` | 文件路径数组(本地 URL) | ### `Intent` 类方法 | 方法 | 返回类型 | 示例 | | ------------------------------ | --------------------------- | ------------------------------------- | | `Intent.text(value)` | `IntentTextValue` | `Intent.text("内容")` | | `Intent.attributedText(value)` | `IntentAttributedTextValue` | `Intent.attributedText("富文本")` | | `Intent.url(value)` | `IntentURLValue` | `Intent.url("https://example.com")` | | `Intent.json(value)` | `IntentJsonValue` | `Intent.json({ key: "value" })` | | `Intent.file(filePath)` | `IntentFileValue` | `Intent.file("/path/to/file.txt")` | | `Intent.fileURL(filePath)` | `IntentFileURLValue` | `Intent.fileURL("/path/to/file.pdf")` | | `Intent.image(UIImage)` | `IntentImageValue` | `Intent.image(uiImage)` | *** ## 八、最佳实践与注意事项 - 所有脚本应显式调用 `Script.exit()` 以确保内存安全。 - 推荐在 UI 脚本中使用 `await Navigation.present()` 之后再调用 `Script.exit()`。 - 对于大文件或图像,建议使用 “Run Script in App” 模式,以避免系统内存限制导致的崩溃。 - 如果脚本需要共享数据,可通过 URL Scheme 或 `queryParameters` 实现。 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/ItemProvider.md --- # ItemProvider `ItemProvider` 用于表示一个**可按需加载的数据提供者**,常见于拖放、文件导入、Photos 选择等场景。 它并不直接包含数据本身,而是描述**可以如何、安全地获取数据**。 `ItemProvider` 支持加载对象、文本、URL、原始数据以及文件路径,并对文件访问施加了明确的安全访问边界。 *** ## 核心概念 - `ItemProvider` 描述的是能力,而不是数据 - 所有加载行为都必须遵循系统的安全作用域规则 - 文件类资源只能在受控的回调作用域内访问 - 是否支持原地访问(in-place)由底层系统决定 *** ## 属性 ### registeredTypes ```ts readonly registeredTypes: UTType[] ``` 表示该 `ItemProvider` 在语义上可以提供的所有类型。 - 包含直接类型以及可推导的父类型 - 用于判断内容大类或调试用途 - 不保证一定存在对应的底层文件表示 *** ### registeredInPlaceTypes ```ts readonly registeredInPlaceTypes: UTType[] ``` 表示该 `ItemProvider` 支持原地访问(open-in-place)的类型集合。 - 常见于视频、音频、文档等大文件 - 是否真正原地访问需以加载结果为准 *** ## 能力判断方法 ### hasItemConforming ```ts hasItemConforming(type: UTType): boolean ``` 判断内容在语义上是否符合指定类型。 - 判断宽松 - 会考虑 UTType 的继承关系 - 适合用于业务分支判断 *** ### hasRepresentationConforming ```ts hasRepresentationConforming(type: UTType): boolean ``` 判断是否存在一个真实的、可加载的底层表示符合指定类型。 - 判断严格 - 适合用于文件处理或精确格式要求的场景 *** ### hasInPlaceRepresentationConforming ```ts hasInPlaceRepresentationConforming(type: UTType): boolean ``` 判断是否存在支持原地访问的底层表示。 - 常用于大文件加载策略选择 *** ## 对象加载能力判断 ### canLoadUIImage ```ts canLoadUIImage(): boolean ``` 判断是否可以加载为 `UIImage` 对象。 - 适合 UI 展示 - 不保证原始文件格式或元数据 *** ### canLoadLivePhoto ```ts canLoadLivePhoto(): boolean ``` 判断是否可以加载为 `LivePhoto` 对象。 - 用于区分静态图片与 Live Photo - 返回 `true` 时可调用 `loadLivePhoto` *** ## 加载方法 ### loadUIImage ```ts loadUIImage(): Promise ``` 加载一个 `UIImage` 对象。 - 适合轻量展示 - 不适合用于文件级处理或资源保真 *** ### loadLivePhoto ```ts loadLivePhoto(): Promise ``` 加载一个 `LivePhoto` 对象。 - 包含图片与配对视频 - 适合展示、保存或进一步处理 *** ### loadURL ```ts loadURL(): Promise ``` 加载一个 URL 字符串。 - 可能是网页 URL - 也可能是文件 URL *** ### loadText ```ts loadText(): Promise ``` 加载纯文本内容。 - 支持 plain text - 富文本会被降级为纯文本 *** ### loadData ```ts loadData(type: UTType): Promise ``` 加载指定类型的原始二进制数据。 - 数据会整体加载进内存 - 适合 JSON、配置文件、小体积资源 - 不适合视频、音频等大文件 *** ## 文件路径加载(安全作用域) 文件路径的加载需要遵循系统的安全限制,所有文件访问都必须在指定的回调作用域内完成。 *** ### loadFilePath ```ts loadFilePath(type: UTType): Promise ``` 加载指定类型的文件路径,如果文件不存在或无法加载,返回 `null`。如果可以加载,文件会被复制到应用组的临时目录中,并返回文件路径。 如果你不再需要文件,请删除它。 示例: ```ts const filePath = provider.loadFilePath("public.movie") ``` *** ## 创建 ItemProvider ### fromUIImage ```ts ItemProvider.fromUIImage(image: UIImage): ItemProvider ``` 从 `UIImage` 创建 `ItemProvider`。 - 仅提供静态图片能力 - 不包含 Live Photo 或原始资源信息 *** ### fromText ```ts ItemProvider.fromText(text: string): ItemProvider ``` 从文本创建 `ItemProvider`。 *** ### fromURL ```ts ItemProvider.fromURL(url: string): ItemProvider | null ``` 从 URL 字符串创建 `ItemProvider`。 - URL 不合法时返回 `null` - 支持网页 URL 与文件 URL *** ### fromFilePath ```ts ItemProvider.fromFilePath(path: string): ItemProvider ``` 从文件路径创建 `ItemProvider`。 - 保留原始文件 - 适合视频、音频、文档等资源 - 支持原地访问能力判断 *** ## 使用建议 - 使用 `hasItemConforming` 进行内容类型判断 - 使用对象加载方法进行 UI 展示 - 使用文件路径加载方法处理大文件 - 文件路径只能在 `perform` 回调作用域内访问 - 不应在回调外部延迟访问安全作用域文件 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/MediaComposer/MediaComposer Example.md --- # MediaComposer 示例 本示例演示如何使用 `MediaComposer` 将 **视频 + 图片 + 音频** 组合成一个最终视频文件,并导出到脚本目录中。 示例流程包括: 1. 选择音频文件 2. 选择一张图片 3. 选择一个视频 4. 构建视频时间线(Video + Image) 5. 在指定时间点插入音频 6. 导出合成后的视频 *** ## 示例代码 ```tsx import { Path, Script } from "scripting" console.present().then(() => Script.exit()) async function run() { try { const audioPath = (await DocumentPicker.pickFiles({ types: ["public.audio"] })).at(0) if (audioPath == null) { console.error("no audio") return } console.log("Audio Picked") const imageResult = (await Photos.pick({ filter: PHPickerFilter.images() })).at(0) const imagePath = await imageResult?.itemProvider.loadFilePath("public.image") if (!imagePath) { console.log("No image") return } console.log("Image picked") const videoResult = (await Photos.pick({ filter: PHPickerFilter.videos() })).at(0) const videoPath = await videoResult?.itemProvider.loadFilePath("public.movie") if (videoPath == null) { console.log("No video") return } console.log("Video Picked") console.log("Start composing...") const exportPath = Path.join( Script.directory, "dest.mp4" ) const exportResult = await MediaComposer.composeAndExport({ exportPath, timeline: { videoItems: [{ videoPath: videoPath }, { imagePath: imagePath, duration: MediaTime.make({ seconds: 5, preferredTimescale: 600 }) }], audioClips: [{ path: audioPath, at: MediaTime.make({ seconds: 5, preferredTimescale: 600 }) }] } }) console.log( "Result:", exportResult.exportPath, "\n", exportResult.duration.getSeconds() ) } catch (e) { console.error(e) } } run() ``` *** ## 时间线解析 ### 视频 / 图片时间线(videoItems) ```ts videoItems: [ { videoPath }, { imagePath, duration: MediaTime.make({ seconds: 5, preferredTimescale: 600 }) } ] ``` - 第一个 `VideoItem` 是完整视频 - 第二个 `VideoItem` 是一张图片,显示 5 秒 - 所有 `videoItems` **按顺序依次拼接** - 最终视频总时长 = 视频时长 + 图片 5 秒 *** ### 音频时间线(audioClips) ```ts audioClips: [{ path: audioPath, at: MediaTime.make({ seconds: 5, preferredTimescale: 600 }) }] ``` - 音频在最终视频的 **第 5 秒开始播放** - 不指定 `at` 时,音频会顺序接在前一个外部音频之后 - 音频不会影响最终视频时长 *** ## 导出结果 ```ts { exportPath: string duration: MediaTime } ``` - `exportPath`:导出文件的完整路径 - `duration`:最终视频时长(仅由 `videoItems` 决定) *** ## 常见错误与边界情况 ### 1. ImageClip 未指定 duration ```ts { imagePath: "...", // 缺少 duration } ``` **问题:** - ImageClip 没有天然时长 - 不指定 `duration` 会导致合成失败 **解决方案:** - 必须显式提供 `MediaTime` *** ### 2. 使用浮点秒数而非 MediaTime ```ts // 错误 at: 5 ``` **正确做法:** ```ts at: MediaTime.make({ seconds: 5, preferredTimescale: 600 }) ``` MediaComposer 中 **所有时间必须使用 MediaTime**。 *** ### 3. 混合不同 timescale 导致精度问题 **问题:** - 不同音视频资源使用不同 timescale - 在剪辑、拼接、淡入淡出时可能出现边界误差 **建议:** - 在脚本中统一使用 `preferredTimescale: 600` - 对外部时间先做 `convertScale` *** ### 4. 音频超出视频范围 **行为说明:** - 音频即使超过视频末尾,也不会延长最终视频 - 超出部分会被自动截断 *** ### 5. 同时存在视频原音与外部音频但音量异常 **原因:** - 默认情况下,外部音频与视频原音会同时混合 - 未配置 ducking 时,可能出现人声被盖住的问题 *** ## 音频 Ducking 行为说明 ### 什么是 Ducking Ducking 指的是: > 当视频原音(如人声)存在时,自动降低外部音频(如背景音乐)的音量 *** ### Ducking 配置 ```ts exportOptions: { ducking: { enabled: true, duckedVolume: 0.25, attackSeconds: 0.15, releaseSeconds: 0.25 } } ``` #### 参数说明 - **enabled** 是否启用 ducking,默认 `true` - **duckedVolume** 被压低后的外部音频音量(0…1) - **attackSeconds** 在视频原音开始前,音量下降的过渡时间 - **releaseSeconds** 在视频原音结束后,音量恢复的过渡时间 *** ### Ducking 生效条件 Ducking 仅在以下条件同时满足时生效: 1. `VideoClip.keepOriginalAudio === true` 2. 存在外部 `audioClips` 3. `exportOptions.ducking.enabled !== false` *** ## 音频混音规则总结 1. **视频原音** - 只有在 `keepOriginalAudio: true` 时才参与混音 2. **外部音频** - 可指定时间点或顺序拼接 - 可设置 `volume`、`fade`、`loopToFitVideoDuration` 3. **最终混音顺序** - 所有音频会被混合到单一音轨 - 不会改变视频时长 - Ducking 在混音阶段自动应用 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/MediaComposer/MediaTime.md --- # MediaTime `MediaTime` 用于表示音视频处理中的**精确时间点或时间长度**,是 Scripting 中 MediaComposer 时间系统的基础类型。 它在语义上对应于底层媒体框架中的“带时间基准的时间值”(如 AVFoundation 的 `CMTime`),但对脚本侧提供了更安全、可读、可计算的抽象。 `MediaTime` 既可以表示**确定的数值时间**,也可以表示**无效、无限或不确定时间**,并支持严格的时间运算与比较。 *** ## 核心特性 - 使用 **value + timescale** 或 **seconds + preferredTimescale** 精确构造时间 - 支持时间缩放(convertScale)及多种舍入策略 - 支持加减运算与大小比较 - 明确区分有效时间、无效时间、无限时间和不确定时间 - 适用于时间线计算、剪辑、对齐、放置(at)、淡入淡出等所有时间相关场景 *** ## 时间精度模型 `MediaTime` 的底层模型基于以下概念: - **value**:整数时间值 - **timescale**:每秒的时间单位数 例如: - `value = 300`, `timescale = 600` 表示 0.5 秒 - `value = 18000`, `timescale = 600` 表示 30 秒 通过 timescale,`MediaTime` 可以精确表达帧级或采样级时间,而不依赖浮点数。 *** ## 只读属性 ### secondes ```ts readonly secondes: number ``` 当前时间对应的秒数(浮点数形式)。 这是一个**派生值**,主要用于展示或调试,不建议用于时间计算。 *** ### isValid ```ts readonly isValid: boolean ``` 表示该时间是否是一个有效、可用于计算的时间值。 当时间为 `invalid`、`indefinite` 或无穷大时,该值为 `false`。 *** ### isPositiveInfinity / isNegativeInfinity ```ts readonly isPositiveInfinity: boolean readonly isNegativeInfinity: boolean ``` 表示该时间是否为正无穷或负无穷。 常用于内部边界标记或时间线计算中的极值判断。 *** ### isIndefinite ```ts readonly isIndefinite: boolean ``` 表示该时间是否为“不确定时间”。 通常用于尚未解析出真实时长的媒体资源。 *** ### isNumeric ```ts readonly isNumeric: boolean ``` 表示该时间是否是一个可参与数值计算的时间。 只有在该值为 `true` 时,才应进行加减或比较操作。 *** ### hasBeenRounded ```ts readonly hasBeenRounded: boolean ``` 表示该时间是否在构造或转换过程中发生过舍入。 对于帧精度或采样精度要求较高的场景,该属性可用于调试或验证。 *** ## 时间转换 ### convertScale ```ts convertScale(newTimescale: number, method: MediaTimeRoundingMethod): MediaTime ``` 将当前时间转换为新的 timescale,并使用指定的舍入策略。 **典型用途:** - 对齐视频帧时间(如 600、90000) - 对齐音频采样时间(如 44100、48000) - 避免不同时间基准混用导致的误差 *** ## 时间值获取 ### getSeconds ```ts getSeconds(): number ``` 返回当前时间对应的秒数(浮点数)。 该方法等价于读取 `secondes`,但在语义上更明确。 *** ## 时间运算 ### plus / minus ```ts plus(other: MediaItem): MediaItem minus(other: MediaItem): MediaItem ``` 执行时间加法或减法运算,返回新的 `MediaTime`。 - 运算双方必须为可计算时间 - 不会修改原对象 - 运算结果遵循内部时间基准规则 *** ## 时间比较 ```ts lt(other: MediaItem): boolean gt(other: MediaItem): boolean lte(other: MediaItem): boolean gte(other: MediaItem): boolean eq(other: MediaItem): boolean neq(other: MediaItem): boolean ``` 用于比较两个时间的大小或相等性。 - 支持严格比较 - 对无效或非数值时间的比较结果是确定性的 - 推荐在进行时间线排序、裁剪判断、边界检测时使用 *** ## 静态构造方法 ### make ```ts static make(options: { value: number timescale: number } | { seconds: number preferredTimescale: number }): MediaTime ``` 用于创建一个 `MediaTime` 实例。 #### 使用 value + timescale ```ts MediaTime.make({ value: 300, timescale: 600 }) ``` 适用于需要精确控制时间单位的场景。 #### 使用 seconds + preferredTimescale ```ts MediaTime.make({ seconds: 5, preferredTimescale: 600 }) ``` 适用于脚本层以“秒”为主的时间描述方式。 *** ### zero ```ts static zero(): MediaTime ``` 返回一个表示 **0 秒** 的时间。 *** ### invalid ```ts static invalid(): MediaTime ``` 返回一个无效时间。 用于显式表示错误、缺失或不可用的时间值。 *** ### indefinite ```ts static indefinite(): MediaTime ``` 返回一个不确定时间。 通常用于媒体尚未加载完成、时长未知的状态。 *** ### positiveInfinity / negativeInfinity ```ts static positiveInfinity(): MediaTime static negativeInfinity(): MediaTime ``` 返回正无穷或负无穷时间。 主要用于内部时间线边界控制,不建议在普通脚本逻辑中使用。 *** ## 使用建议与注意事项 - **避免直接使用浮点秒数进行时间计算**,应始终使用 `MediaTime` - 不同媒体资源可能使用不同的 timescale,必要时显式调用 `convertScale` - 在比较或运算前,建议检查 `isNumeric` - 在构建时间线(如 `at`、`sourceTimeRange`)时,统一 timescale 可减少误差 *** ## 在 MediaComposer 中的典型用途 - 指定音频或视频片段的放置时间(`AudioClip.at`) - 定义剪辑的起点与时长(`TimeRange`) - 计算最终导出视频的精确时长 - 控制淡入淡出、对齐、循环等时间行为 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/MediaComposer/Quick Start.md --- # 快速开始 `MediaComposer` 用于在 Scripting 中 **组合视频、图片与音频时间线并导出最终媒体文件**。 它封装了一套稳定的时间线模型,支持视频剪辑、图片片段、音频叠加、淡入淡出、音频 ducking、导出参数控制等高级能力。 该模块适用于: - 视频与图片混合生成短片 - 给视频添加背景音乐、配音或音效 - 使用图片序列生成视频 - 自动化视频处理与内容生成脚本 *** ## 设计概览 MediaComposer 的核心由三部分组成: 1. **时间模型** 使用 `MediaTime` / `TimeRange` 精确描述时间点与时长 2. **时间线模型** - `VideoItem[]`:视频或图片片段(顺序拼接) - `AudioClip[]`:音频轨道(可指定时间点或自动顺序放置) 3. **导出系统** 通过统一的 `composeAndExport` 接口完成渲染与导出 *** ## 时间线结构 ```ts timeline: { videoItems: VideoItem[] audioClips: AudioClip[] } ``` - **videoItems** 定义视觉时间线,视频与图片会严格按数组顺序依次排列 - **audioClips** 定义音频时间线,可自由指定放置时间(`at`),或顺序追加 最终导出的视频时长由 **videoItems 决定**。 *** ## VideoItem ```ts type VideoItem = XOR ``` `VideoItem` 表示时间线中的一个“视觉片段”,可以是 **视频** 或 **图片**,但不能同时是两者。 *** ## VideoClip(视频片段) ```ts type VideoClip = { videoPath: string sourceTimeRange?: TimeRange | null keepOriginalAudio?: boolean fade?: FadeConfig | null } ``` ### videoPath - 视频文件路径 - 支持本地视频文件 *** ### sourceTimeRange ```ts sourceTimeRange?: TimeRange | null ``` - 指定从源视频中使用的时间范围 - 不提供时,默认使用整个视频 **常见用途:** - 裁剪视频片段 - 只取某一段作为素材 *** ### keepOriginalAudio ```ts keepOriginalAudio?: boolean ``` - 是否保留视频自带的音频 - 默认值:`false` **说明:** - 为 `true` 时,视频原音会参与混音 - 可与外部 `audioClips` 同时存在 - 是否对外部音频进行 ducking 由 `ExportOptions.ducking` 控制 *** ### fade ```ts fade?: FadeConfig | null ``` - 视频片段的淡入淡出配置 - 会覆盖全局视频淡入淡出设置(如果存在) *** ## ImageClip(图片片段) ```ts type ImageClip = { imagePath: string duration: MediaTime contentMode?: "fit" | "crop" backgroundColor?: Color fade?: FadeConfig | null } ``` `ImageClip` 用于将一张静态图片作为视频时间线中的一个片段。 *** ### imagePath - 图片文件路径 - 支持常见图片格式(JPEG / PNG / HEIC 等) *** ### duration ```ts duration: MediaTime ``` - 图片片段在视频中的显示时长 - 必须显式指定 *** ### contentMode ```ts contentMode?: "fit" | "crop" ``` - 控制图片如何适配渲染尺寸 - 默认值:`fit` 说明: - `fit`:完整显示图片,可能留黑边 - `crop`:填满画面,超出部分裁剪 *** ### backgroundColor ```ts backgroundColor?: Color ``` - 图片未覆盖区域的背景色 - 通常与 `fit` 模式搭配使用 *** ### fade ```ts fade?: FadeConfig | null ``` - 图片片段的淡入淡出配置 - 支持与视频片段统一使用 *** ## AudioClip(音频片段) ```ts type AudioClip = { path: string sourceTimeRange?: TimeRange | null at?: MediaTime volume?: number fade?: FadeConfig | null loopToFitVideoDuration?: boolean } ``` 音频片段用于在最终视频中添加背景音乐、配音或音效。 *** ### path - 音频文件路径 *** ### sourceTimeRange - 指定使用音频的某一时间段 - 默认使用整个音频文件 *** ### at ```ts at?: MediaTime ``` - 指定音频在最终时间线中的放置时间 - 不指定时: - 按顺序接在前一个外部音频片段之后 *** ### volume ```ts volume?: number ``` - 单个音频片段的音量(0…1) - 默认值:1 *** ### fade - 音频淡入淡出配置 - 常用于背景音乐的自然过渡 *** ### loopToFitVideoDuration ```ts loopToFitVideoDuration?: boolean ``` - 是否循环音频以匹配视频总时长 - 常用于背景音乐 *** ## FadeConfig(淡入淡出) ```ts type FadeConfig = { fadeInSeconds?: number fadeOutSeconds?: number } ``` - 单位:秒 - 可用于视频、图片、音频 - 未指定时默认为 0 *** ## ExportOptions(导出配置) ```ts type ExportOptions = { renderSize?: Size frameRate?: number scaleMode?: VideoScaleMode globalVideoFade?: FadeConfig | null externalAudioBaseVolume?: number ducking?: DuckingConfig presetName?: ExportPreset outputFileType?: ExportFileType colorSpacePolicy?: ColorSpacePolicy } ``` ### 常用说明 - **renderSize** 最终视频分辨率,默认 1080×1920 - **frameRate** 渲染帧率,默认 30 - **globalVideoFade** 全局视频淡入淡出(可被单个 clip 覆盖) - **ducking** 当视频存在原音时,自动降低外部音频音量 - **presetName / outputFileType** 控制编码质量与文件格式 - **colorSpacePolicy** 控制输出文件的颜色空间,默认为`forceSDR`,可选`keepSource`。 *** ## composeAndExport ```ts function composeAndExport(options: { exportPath: string timeline: { videoItems: VideoItem[] audioClips: AudioClip[] } exportOptions?: ExportOptions overwrite?: boolean }): Promise<{ exportPath: string duration: MediaTime }> ``` ### 参数说明 - **exportPath** 导出文件路径 - **timeline.videoItems** 视频 / 图片时间线(顺序执行) - **timeline.audioClips** 音频时间线(可自由放置) - **exportOptions** 导出配置,可选 - **overwrite** 是否覆盖已有文件,默认 `true` *** ### 返回结果 ```ts { exportPath: string duration: MediaTime } ``` - **exportPath**:最终导出路径 - **duration**:最终视频时长(由 videoItems 决定) *** ## 使用建议与最佳实践 - 始终使用 `MediaTime` 描述时间,避免直接使用浮点秒数 - 图片片段必须显式指定 `duration` - 音频与视频的时间线是 **独立但最终混合** 的 - 对复杂项目,建议统一 timescale(如 600) - 背景音乐推荐使用 `loopToFitVideoDuration` *** ## 典型使用场景 - 图片 + 视频混合短片 - 自动生成带背景音乐的视频 - 视频剪辑与配音合成 - 内容创作与自动化视频生成 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/SharedAudioSession.md --- # SharedAudioSession 通过 `SharedAudioSession`,你可以在脚本中方便地管理和操作共享音频会话(audio session)。音频会话充当脚本、Scripting 应用、操作系统和底层音频硬件之间的中介,允许你有效地配置和控制音频的行为。 *** ## 功能简介 - 获取和设置音频会话的类别(category)、模式(mode)和选项(options)。 - 配置音频输入和输出的首选采样率(sample rate)。 - 处理音频中断事件。 - 查询设备所支持的类别和模式。 - 根据具体的应用场景(如视频录制、语音聊天、后台播放等)来定制音频行为。 *** ## 方法和属性 ### 1. **会话类别与选项** #### `category` 获取当前音频会话的类别(Category)。 ```typescript const category = await SharedAudioSession.category console.log(category) // 示例输出:'playback' ``` #### `categoryOptions` 获取当前音频会话类别的选项(Options)。 ```typescript const options = await SharedAudioSession.categoryOptions console.log(options) // 示例输出:['mixWithOthers', 'allowAirPlay'] ``` #### `setCategory(category: AudioSessionCategory, options: AudioSessionCategoryOptions[])` 设置音频会话的类别并指定其选项。 ```typescript await SharedAudioSession.setCategory('playback', ['mixWithOthers']) ``` *** ### 2. **会话模式** #### `mode` 获取当前音频会话模式(Mode)。 ```typescript const mode = await SharedAudioSession.mode console.log(mode) // 示例输出:'videoChat' ``` #### `setMode(mode: AudioSessionMode)` 设置音频会话模式。 ```typescript await SharedAudioSession.setMode('voiceChat') ``` *** ### 3. **采样率 (Sample Rate)** #### `preferredSampleRate` 获取当前首选采样率(单位为 Hz)。 ```typescript const sampleRate = await SharedAudioSession.preferredSampleRate console.log(sampleRate) // 示例输出:44100 ``` #### `setPreferredSampleRate(sampleRate: number)` 设置音频输入和输出的首选采样率。 ```typescript await SharedAudioSession.setPreferredSampleRate(48000) ``` *** ### 4. **音频中断处理** #### `addInterruptionListener(listener: AudioSessionInterruptionListener)` 监听音频中断事件。 ```typescript SharedAudioSession.addInterruptionListener((type) => { if (type === 'began') { console.log('音频中断开始') } else if (type === 'ended') { console.log('音频中断结束') } }) ``` #### `removeInterruptionListener(listener: AudioSessionInterruptionListener)` 移除音频中断监听器。 ```typescript SharedAudioSession.removeInterruptionListener(myListener) ``` *** ### 5. **设备功能查询** #### `availableCategories` 获取设备上可用的音频会话类别列表。 ```typescript const categories = await SharedAudioSession.availableCategories console.log(categories) // 示例输出:['playback', 'record', 'soloAmbient'] ``` #### `availableModes` 获取设备上可用的音频会话模式列表。 ```typescript const modes = await SharedAudioSession.availableModes console.log(modes) // 示例输出:['default', 'videoChat', 'voiceChat'] ``` *** ### 6. **其他属性** #### `isOtherAudioPlaying` 检查设备上是否有其他音频正在播放。 ```typescript const isPlaying = await SharedAudioSession.isOtherAudioPlaying console.log(isPlaying) // 示例输出:true ``` #### `secondaryAudioShouldBeSilencedHint` 检查次要音频是否应该被静音。 ```typescript const shouldSilence = await SharedAudioSession.secondaryAudioShouldBeSilencedHint console.log(shouldSilence) // 示例输出:false ``` #### `allowHapticsAndSystemSoundsDuringRecording` 检查录音期间是否允许触觉反馈和系统声音。 ```typescript const allowHaptics = await SharedAudioSession.allowHapticsAndSystemSoundsDuringRecording console.log(allowHaptics) // 示例输出:true ``` #### `prefersNoInterruptionsFromSystemAlerts` 检查音频会话是否偏好不被系统警报打断。 ```typescript const prefersNoInterruptions = await SharedAudioSession.prefersNoInterruptionsFromSystemAlerts console.log(prefersNoInterruptions) // 示例输出:false ``` *** ### 7. **会话激活** #### `setActive(active: boolean, options?: AudioSessionSetActiveOptions[])` 激活或停用共享音频会话,可指定激活选项。 ```typescript await SharedAudioSession.setActive( true, ['notifyOthersOnDeactivation'] ) ``` *** ### 8. **系统设置** #### `setAllowHapticsAndSystemSoundsDuringRecording(value: boolean)` 启用或禁用在录音期间允许触觉反馈和系统声音。 ```typescript await SharedAudioSession.setAllowHapticsAndSystemSoundsDuringRecording(true) ``` #### `setPrefersNoInterruptionsFromSystemAlerts(value: boolean)` 设置是否偏好不被系统警报打断。 ```typescript await SharedAudioSession.setPrefersNoInterruptionsFromSystemAlerts(true) ``` *** ### 9. **系统输出音量** #### `outputVolume: number` 获取当前系统输出音量(范围为 0 到 1)。 #### outputVolume 监听事件 类型类型 ```ts type AudioSessionOutputVolumeListener = (newValue: number, oldValue: number) => void ``` ##### `addOutputVolumeListener(listener: AudioSessionOutputVolumeListener): void` 添加系统输出音量监听器。 ##### `removeOutputVolumeListener(listener: AudioSessionOutputVolumeListener): void` 移除系统输出音量监听器。 *** ## 枚举(Enumerations) ### **AudioSessionSetActiveOptions** 定义激活选项: - `'notifyOthersOnDeactivation'` ### **AudioSessionCategory** 定义音频会话的类别: - `'ambient'` - `'multiRoute'` - `'playAndRecord'` - `'playback'` - `'record'` - `'soloAmbient'` ### **AudioSessionCategoryOptions** 定义类别的可选行为: - `'mixWithOthers'` - `'duckOthers'` - `'interruptSpokenAudioAndMixWithOthers'` - `'allowBluetooth'` - `'allowBluetoothA2DP'` - `'allowAirPlay'` - `'defaultToSpeaker'` - `'overrideMutedMicrophoneInterruption'` ### **AudioSessionMode** 指定会话模式: - `'default'` - `'gameChat'` - `'measurement'` - `'moviePlayback'` - `'spokenAudio'` - `'videoChat'` - `'videoRecording'` - `'voiceChat'` - `'voicePrompt'` ### **AudioSessionInterruptionType** 指定中断类型: - `'began'` - `'ended'` - `'unknown'` *** 通过此接口,你可以在 Scripting 应用中对音频会话进行深度管理,非常适合构建对音频依赖较高的脚本,如音乐播放器和视频会议工具等。 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/onDrag and onDrop View Modifiers.md --- # onDrag 和 onDrop 修饰符 Scripting 提供了一套与 SwiftUI Drag & Drop 行为模型高度一致的 API,用于在脚本中实现视图间、应用内或跨应用的拖拽与放置操作。 该能力主要由以下三部分构成: - **onDrag**:将当前视图声明为拖拽源 - **onDrop**:将当前视图声明为放置目标 - **DropInfo / ItemProvider / UTType**:描述拖拽内容与状态的上下文对象 拖拽与放置是一个严格受系统控制的交互流程,部分 API 只能在特定回调中调用,文档中会明确指出这些限制。 *** ## 核心数据类型 ### DropInfo `DropInfo` 描述一次拖拽在当前放置视图上的实时状态。该对象仅在 `onDrop` 相关回调中有效。 #### 属性 ##### location: Point - 表示拖拽当前位置 - 坐标空间为 **放置视图自身的本地坐标系** - 可用于实现基于位置的高亮、插入指示线、排序逻辑等 #### 方法 ##### hasItemsConforming(types: UTType\[]): boolean - 用于判断拖拽内容中,是否至少有一个项目符合指定的 UTType - 常用于: - `validateDrop` - `dropEntered` - `dropUpdated` - 不会实际加载数据,仅用于能力判断 ##### itemProviders(types: UTType\[]): ItemProvider\[] - 返回符合指定 UTType 的 `ItemProvider` 列表 - **仅允许在 `performDrop` 回调中调用** - 在该方法返回后,系统将撤销对拖拽数据的访问权限 > 重要约束 > 必须在 `performDrop` 方法作用域内 **立即开始** 对 ItemProvider 的数据加载(如 `loadData`、`loadText`)。 > 不允许延迟到其他回调或异步逻辑中再发起加载。 *** ## DropOperation `DropOperation` 用于描述当前拖拽更新阶段,目标视图期望执行的操作类型。 可选值如下: - `"copy"` 表示复制数据(最常见,用于文件、文本、图片等) - `"move"` 表示移动数据(通常仅用于应用内部拖拽) - `"cancel"` 取消本次拖拽,不执行任何数据传输 - `"forbidden"` 明确禁止当前拖拽行为,系统通常会显示禁止指示 `DropOperation` 通常由 `dropUpdated` 回调返回,用于动态控制拖拽行为。 *** ## DragDropProps `DragDropProps` 是所有支持拖拽与放置能力的视图可选属性集合。 *** ## onDrag ### 用途 将当前视图声明为 **拖拽源**,允许用户从该视图开始一次拖拽操作。 ### 定义 ```ts onDrag?: { data: () => ItemProvider preview: VirtualNode } ``` ### 参数说明 #### data ```ts data: () => ItemProvider ``` - 返回一个 `ItemProvider` - 用于描述拖拽时传递的数据内容 - 支持文本、图片、文件、URL、自定义类型等 - 每次拖拽开始时调用 > 建议 > 仅在该回调中构造 ItemProvider,不要复用旧实例,以确保数据状态正确。 #### preview ```ts preview: VirtualNode ``` - 指定拖拽开始后显示的预览视图 - 系统会自动将其渲染为拖拽浮层 - 预览视图默认居中于源视图 *** ## onDrop ### 用途 将当前视图声明为 **放置目标**,并通过一组回调精细控制拖拽验证、状态变化与最终数据接收。 ### 定义 ```ts onDrop?: { types: UTType[] validateDrop?: (info: DropInfo) => boolean dropEntered?: (info: DropInfo) => void dropUpdated?: (info: DropInfo) => DropOperation | null dropExited?: (info: DropInfo) => void performDrop: (info: DropInfo) => boolean } ``` *** ### onDrop.types ```ts types: UTType[] ``` - 声明该视图 **允许接收的内容类型** - 如果拖拽内容不包含任意一个匹配类型: - 放置区域不会激活 - `validateDrop` 不会被调用 - 视觉高亮不会出现 *** ### validateDrop ```ts validateDrop?: (info: DropInfo) => boolean ``` - 用于判断是否允许开始一次放置操作 - 返回 `false` 将直接拒绝拖拽 - 常见用途: - 检查类型数量 - 校验业务状态(如只允许空列表接收) 默认行为:始终返回 `true` *** ### dropEntered ```ts dropEntered?: (info: DropInfo) => void ``` - 当拖拽进入放置区域时触发 - 通常用于: - 显示高亮 - 显示插入占位符 - 触发动画状态 *** ### dropUpdated ```ts dropUpdated?: (info: DropInfo) => DropOperation | null ``` - 当拖拽在放置区域内部移动时反复调用 - 用于动态返回期望的 `DropOperation` 返回值说明: - 返回具体的 `DropOperation`:更新当前拖拽行为 - 返回 `null`: - 使用上一次返回的有效值 - 若没有历史值,默认使用 `"copy"` *** ### dropExited ```ts dropExited?: (info: DropInfo) => void ``` - 当拖拽离开放置区域时触发 - 常用于清理高亮、移除占位 UI *** ### performDrop ```ts performDrop: (info: DropInfo) => boolean ``` - **最关键的回调** - 表示用户已松手,系统允许你读取拖拽数据 - 返回值: - `true`:表示成功接收并处理了拖拽 - `false`:表示放置失败 #### 重要约束(必须遵守) - 必须在该方法作用域内: - 调用 `info.itemProviders(...)` - 并立即开始数据加载 - 不允许: - 将 ItemProvider 保存到外部 - 在异步回调中延迟访问拖拽数据 这是系统级安全限制,不遵守将导致数据无法访问。 *** ## 典型使用流程总结 1. 用户从 `onDrag` 视图开始拖拽 2. 系统根据 `onDrop.types` 判断是否激活目标 3. 调用 `validateDrop` 4. 进入放置区域 → `dropEntered` 5. 移动过程中 → 多次 `dropUpdated` 6. 离开区域 → `dropExited` 7. 松手 → `performDrop` 8. 在 `performDrop` 中读取并处理数据 *** ## 设计建议与最佳实践 - 始终精确声明 `UTType`,避免过于宽泛 - 在 `dropUpdated` 中返回 `"forbidden"` 可显式阻止非法拖拽 - 复杂数据解析逻辑应在 `ItemProvider` 加载完成后的异步回调中完成,而不是在 `performDrop` 中同步阻塞 - 跨应用拖拽时,优先使用系统标准类型(text、image、file、url) --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.6/onDropContent.md --- # 接收外部拖拽内容 `onDropContent` 是 Scripting 提供的一个视图修饰符,用于将当前视图设置为**拖放目标(Drop Target)**,以接收从其他 App 拖拽进入的文件、图片或文本内容。 *** ## 功能说明 通过 `onDropContent`,你可以实现以下能力: - 接收来自其他 App 的拖拽内容 - 使用 UTType 精确限制可接收的数据类型 - 实时感知拖拽指针是否悬停在视图上方 - 在内容被放下时,通过 `ItemProvider` 启动数据加载流程 - 对安全作用域文件建立持久访问权限 *** ## 修饰符定义 ```ts onDropContent?: { types: UTType[] isTarget: { value: boolean onChanged: (value: boolean) => void } | Observable perform: (attachments: ItemProvider[]) => boolean } ``` *** ## 参数说明 ### types 用于指定当前视图**可以接收的内容类型列表**,类型值为 UTType 字符串。 当拖拽内容不包含任意匹配的类型时: - 当前视图不会激活为放置目标 - `isTarget` 不会发生变化 - `perform` 不会被调用 示例: ```ts types: ["public.image", "public.movie"] ``` *** ### isTarget 用于表示拖拽操作是否悬停在当前视图上方。 - 当拖拽进入视图区域时,值为 `true` - 当拖拽移出视图区域时,值为 `false` 支持以下两种形式: - 绑定对象形式 ```ts { value: boolean onChanged: (value: boolean) => void } ``` - Observable 形式 ```ts Observable ``` Observable 形式适合与 `useObservable` 搭配使用,语义更简洁。 *** ### perform 当符合 `types` 要求的内容被成功放下时触发。 ```ts perform: (attachments: ItemProvider[]) => boolean ``` - 参数 `attachments` 为 `ItemProvider` 数组 - 每一个 `ItemProvider` 表示一个被拖入的内容项 - 函数返回值表示是否成功处理了此次拖放操作 返回值说明: - 返回 `true` 表示拖放被成功接收 - 返回 `false` 表示未处理该拖放内容 *** ## perform 的执行规则(重要) 在 `perform` 中需要遵循以下规则: - 必须在 `perform` 函数的同步执行过程中**启动对 ItemProvider 的加载** - 允许使用 `Promise` / `then` 等方式延迟完成加载 - 不允许在 `perform` 返回之后,再通过其他回调或事件启动加载 - 返回 `false` 时,系统会认为该拖放未被接受 原因说明: - 拖放内容受系统安全机制保护 - 只有在 `perform` 执行期间,脚本才拥有对拖放数据的访问权限 - 若未在此期间启动加载,后续将无法访问对应资源 *** ## ItemProvider 的使用方式 在 `perform` 中,开发者应当通过 `ItemProvider` 判断类型并启动加载。 常见流程包括: - 使用 `hasItemConforming` 判断内容类型 - 根据内容类型选择合适的加载方式 - 对文件类资源获取路径并进行后续处理 *** ## 示例用法 ```tsx const isTarget = useObservable(false) return { const images: UIImage[] = [] const videos: string[] = [] let found = false for (const attachment of attachments) { if (attachment.hasItemConforming("public.png")) { found = true attachment.loadUIImage().then(image => { if (image != null) { images.push(image) } }) } else if (attachment.hasItemConforming("public.movie")) { found = true attachment.loadFilePath("public.movie").then(filePath => { if (filePath != null) { // 为安全作用域文件创建书签 FileManager.addFileBookmark(filePath) videos.push(filePath) } }) } } return found } }} > ... ``` *** ## 安全作用域文件访问 通过 `onDropContent` 获取的文件路径,通常属于**安全作用域资源**。 这类路径在以下情况下可能失效: - `perform` 返回之后 - App 重启 - 脚本生命周期结束 为保证后续仍可访问文件,建议在获取路径后创建文件书签。 *** ## FileManager.addFileBookmark ```ts FileManager.addFileBookmark(path: string, name?: string): string | null ``` 说明: - 为指定文件或文件夹创建安全作用域书签 - 适用于通过 `Photos`、`onDropContent` 等 API 获取的路径 - 返回书签名称,用于后续访问或移除 示例: ```ts const bookmarkName = FileManager.addFileBookmark(filePath) ``` *** ## FileManager.removeFileBookmark ```ts FileManager.removeFileBookmark(name: string): boolean ``` 说明: - 移除指定名称的文件书签 - 当不再需要访问对应文件时应及时调用 - 返回是否成功移除 示例: ```ts FileManager.removeFileBookmark(bookmarkName) ``` *** ## 使用建议 - 在 `types` 中尽量明确声明可接收的内容类型 - 在 `perform` 中只负责启动加载,不要等待加载完成 - 对图片等轻量内容可直接加载为对象 - 对视频、音频、文档等资源优先使用文件路径 - 对需要长期访问的文件务必创建书签 - 在资源不再使用时移除对应书签 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/AVPlayer.md --- # 音视频播放器 `AVPlayer` 用于播放音频或视频资源,支持播放控制、速率控制、循环播放、播放状态监听以及媒体元数据读取等能力。 你可以通过 `setSource()` 设置媒体源(本地文件或远程 URL),然后使用 `play()` 开始播放。 *** ## 入门指南 ```ts const player = new AVPlayer() if (player.setSource("https://example.com/audio.mp3")) { player.onReadyToPlay = () => { player.play() } player.onEnded = () => { console.log("播放完成") } } else { console.error("设置媒体源失败") } ``` *** ## API 参考 ### 属性 #### `volume: number` 控制播放音量,取值范围为 `0.0`(静音)到 `1.0`(最大音量)。 ```ts player.volume = 0.5 ``` *** #### `duration: DurationInSeconds` 媒体的总时长(秒)。 在媒体尚未加载完成前,该值为 `0`。 ```ts console.log(player.duration) ``` *** #### `currentTime: DurationInSeconds` 当前播放时间(秒)。 可通过设置该值来跳转播放位置。 ```ts player.currentTime = 30 ``` *** #### `rate: number` 当前实际播放速率。 - `1.0` 表示正常速度 - 小于 `1.0` 表示慢速播放 - 大于 `1.0` 表示快速播放 ```ts player.rate = 1.25 ``` *** #### `defaultRate: number` 默认播放速率,用于**开始播放时**的速率选择。 - 当调用 `play()` 且未传入 `atRate` 参数时,会使用 `defaultRate` - 修改 `defaultRate` **不会立即影响当前正在播放的速率** - 常用于控制「下次开始播放」时的速率 ```ts player.defaultRate = 1.5 ``` 典型使用场景: - 用户在播放前选择了一个“默认倍速” - 下次调用 `play()` 时自动使用该倍速 *** #### `timeControlStatus: TimeControlStatus` 指示播放器当前的播放状态: - `paused` 播放器已暂停或尚未开始播放 - `waitingToPlayAtSpecifiedRate` 等待满足播放条件(例如网络缓冲) - `playing` 正在播放 *** #### `numberOfLoops: number` 设置循环播放次数: - `0`:不循环 - 正整数:循环指定次数 - 负数:无限循环 ```ts player.numberOfLoops = -1 ``` *** ### 方法 #### `setSource(filePathOrURL: string): boolean` 设置媒体播放源,支持: - 本地文件路径 - 远程 URL 返回值: - `true`:设置成功 - `false`:设置失败 *** #### `play(atRate?: number): boolean` 开始播放当前媒体。 - 若传入 `atRate`,则以该速率开始播放 - 若未传入 `atRate`,则使用 `defaultRate` - 播放过程中可通过修改 `rate` 动态调整速率 ```ts player.play() // 使用 defaultRate player.play(1.25) // 以 1.25 倍速开始播放 ``` 返回值: - `true`:成功开始播放 - `false`:播放失败 *** #### `pause()` 暂停当前播放。 *** #### `stop()` 停止播放,并将播放位置重置到起始位置。 *** #### `dispose()` 释放播放器占用的所有资源,并移除内部观察者。 当播放器不再使用时必须调用,以避免资源泄露。 *** #### `loadMetadata(): Promise` 加载当前媒体的完整元数据。 返回: - `AVMetadataItem[]` - 若未设置媒体源或无元数据,则返回 `null` ```ts const metadata = await player.loadMetadata() ``` *** #### `loadCommonMetadata(): Promise` 加载当前媒体的通用元数据(Common Metadata)。 这些元数据提供跨格式统一的 `commonKey`,常用于获取标题、艺术家、专辑等信息。 ```ts const common = await player.loadCommonMetadata() ``` *** ### 回调事件 #### `onReadyToPlay?: () => void` 当媒体已准备好并可开始播放时触发。 *** #### `onTimeControlStatusChanged?: (status: TimeControlStatus) => void` 当播放状态发生变化时触发,例如: - 等待缓冲 → 播放中 - 播放中 → 暂停 *** #### `onEnded?: () => void` 当媒体播放完成时触发。 *** #### `onError?: (message: string) => void` 播放过程中发生错误时触发,参数为错误描述信息。 *** ## 音频会话说明 `AVPlayer` 依赖系统的共享音频会话。 在播放前应正确配置并激活音频会话。 ```ts await SharedAudioSession.setCategory('playback', ['mixWithOthers']) await SharedAudioSession.setActive(true) ``` 处理中断(如来电): ```ts SharedAudioSession.addInterruptionListener(type => { if (type === 'began') { player.pause() } else if (type === 'ended') { player.play() } }) ``` *** ## 常见用法示例 ### 使用默认倍速播放 ```ts player.defaultRate = 1.5 player.play() ``` *** ### 临时指定倍速播放 ```ts player.play(2.0) ``` *** ### 循环播放 ```ts player.numberOfLoops = 3 player.play() ``` *** ### 读取通用元数据 ```ts const metadata = await player.loadCommonMetadata() if (metadata) { const title = metadata.find(i => i.commonKey === 'title') console.log(await title?.stringValue) } ``` *** ## 最佳实践 1. **区分 defaultRate 与 rate** - `defaultRate` 用于“开始播放时” - `rate` 用于“当前播放过程中” 2. **始终释放资源** - 播放结束或不再使用时调用 `dispose()` 3. **处理播放状态** - 使用 `onTimeControlStatusChanged` 更新 UI(加载中 / 播放中) 4. **播放前配置音频会话** - 避免后台、静音或混音行为不符合预期 5. **元数据读取时机** - 在 `onReadyToPlay` 之后读取元数据更稳定 *** ## 完整示例 ```ts const player = new AVPlayer() await SharedAudioSession.setCategory('playback', ['mixWithOthers']) await SharedAudioSession.setActive(true) player.defaultRate = 1.25 if (player.setSource("https://example.com/audio.mp3")) { player.onReadyToPlay = () => { player.play() } player.onEnded = () => { console.log("播放完成") player.dispose() } player.onError = message => { console.error("播放错误:", message) player.dispose() } const metadata = await player.loadCommonMetadata() if (metadata) { const title = metadata.find(i => i.commonKey === 'title') console.log("标题:", await title?.stringValue) } } ``` --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/AssistantTool User-Initiated Cancellation.md --- # 处理用户主动取消智能助手工具 为提升长时间运行工具的用户体验,AssistantTool 新增了 **用户主动取消(Cancel)** 支持。 当用户在工具执行过程中点击“取消”时,开发者可以选择性地通过 `onCancel` 回调返回已完成的部分结果;如果未实现该回调,系统会自动处理取消逻辑,开发者无需额外处理。 该机制适用于搜索、分析、爬取、批处理、流式生成等耗时或多阶段工具。 *** ## 能力概述 新增能力包含以下 API: ```ts type OnCancel = () => string | null | undefined var onCancel: OnCancel | null | undefined const isCancelled: boolean ``` *** ## 核心语义说明 ### onCancel 是可选实现 - 开发者可以选择实现 `onCancel` - 不实现 `onCancel` 也是完全正确、被官方支持的用法 当开发者没有设置 `onCancel` 时: - 用户点击“取消” - 工具会被系统标记为已取消 - 执行函数后续返回的任何结果都会被系统忽略 - Assistant 不会消费这些返回值 结论是,开发者无需为“用户取消”编写任何额外逻辑,也不会产生错误行为。 *** ### 实现 onCancel 的目的 实现 `onCancel` 的唯一目的,是在用户取消时主动返回“已经完成的部分结果”,以提升用户体验。 这是一种增强能力,而不是强制要求。 *** ## isCancelled 的语义 - `AssistantTool.isCancelled` 在用户取消后立即变为 `true` - 该值在执行函数内随时可读取 - 用于控制是否继续执行后续步骤、循环或资源占用操作 *** ## onCancel 的注册时机 `onCancel` 必须在工具的执行函数内部注册。 原因包括: - 工具执行完成后,系统会自动将 `onCancel` 设为 `null` - 每次执行都是独立的生命周期 - 在执行函数外注册不会生效 可注册的位置包括: - `registerExecuteTool` - `registerExecuteToolWithApproval` *** ## 不实现 onCancel 的最小示例 ```ts const executeTool = async () => { await doSomethingSlow() return { success: true, message: "Operation completed." } } ``` 行为说明: - 用户点击取消后,工具被系统判定为已取消 - 上述返回结果会被自动忽略 - 不需要额外判断或清理逻辑 *** ## 实现 onCancel 并返回部分结果的示例 ```ts const executeTool = async () => { const partialResults: string[] = [] AssistantTool.onCancel = () => { return [ "Operation was cancelled by the user.", "Partial results:", ...partialResults ].join("\n") } for (const item of items) { if (AssistantTool.isCancelled) break const result = await process(item) partialResults.push(result) } return { success: true, message: partialResults.join("\n") } } ``` 行为说明: - 用户取消时,`onCancel` 会被立即调用 - 已完成的 `partialResults` 会作为 message 返回给 Assistant - 后续执行通过 `isCancelled` 判断及时停止 *** ## 典型使用场景 适合实现 `onCancel` 的工具类型包括: - 多源搜索与聚合 - 多篇文章抓取与解析 - 项目级扫描与分析 - 批量计算或生成任务 - 长时间运行的推理流程 不适合或不需要实现 `onCancel` 的场景包括: - 瞬时完成的工具 - 没有中间结果的操作 - 取消后没有任何可返回内容的任务 *** ## onCancel 的返回值规范 `onCancel` 的返回值含义如下: - 返回 `string` 该字符串会作为工具被取消时返回给 Assistant 的 `message` - 返回 `null` 或 `undefined` 表示不返回任何消息,属于合法行为,但通常不推荐 推荐的实践是: - 明确说明工具已被用户取消 - 若返回部分结果,清楚标注为“Partial results / 已完成部分” *** ## 与 Approval 流程的关系 - `onCancel` 发生在执行阶段 - 不影响 Approval Request 阶段 - 适用于手动批准与 autoApprove 自动批准后的执行过程 需要区分的两个概念: - `secondaryConfirmed` 表示用户在批准阶段拒绝或取消执行 - `onCancel` 表示用户在执行过程中中途取消 *** ## 常见错误与注意事项 ### 在执行函数外注册 onCancel 这是无效的用法,因为执行上下文已经结束。 *** ### 在 onCancel 中执行耗时或副作用操作 `onCancel` 应快速返回,不应发起网络请求、文件写入或其他副作用操作。 *** ### 忽略 isCancelled 继续执行 即使实现了 `onCancel`,也应在长流程中显式检查 `isCancelled`,避免在取消后继续消耗资源。 *** ## 推荐的执行结构 ```ts const executeTool = async () => { const partial = [] AssistantTool.onCancel = () => { return formatPartialResult(partial) } for (const item of items) { if (AssistantTool.isCancelled) break const r = await process(item) partial.push(r) } return { success: true, message: formatFinalResult(partial) } } ``` *** ## 总结 - 用户取消是一种正常的用户行为,而不是异常 - `onCancel` 是一种可选的体验增强能力 - 不实现 `onCancel` 不会破坏工具行为 - 系统会自动忽略取消后的执行结果 - 实现 `onCancel` 只是为了更优雅地收尾 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/Device.md --- # 设备 `Device` 命名空间提供对当前设备硬件、系统环境、语言区域、屏幕信息、电池状态、方向感知、接近传感器以及网络接口等信息的访问能力,并提供相关状态变化的监听接口。 该 API 主要用于根据设备环境动态调整 UI、行为逻辑或系统能力使用方式。 *** ## Orientation 表示设备当前的物理朝向。 ```ts type Orientation = | "portrait" | "portraitUpsideDown" | "landscapeLeft" | "landscapeRight" | "faceUp" | "faceDown" | "unknown" ``` ### 说明 - `portrait`:竖屏,Home 键在下(或标准竖屏方向) - `portraitUpsideDown`:竖屏倒置 - `landscapeLeft`:横屏,设备向左旋转 - `landscapeRight`:横屏,设备向右旋转 - `faceUp`:设备平放,屏幕朝上 - `faceDown`:设备平放,屏幕朝下 - `unknown`:无法确定方向 *** ## InterfaceOrientation 表示App UI 的可旋转方向 ```ts type InterfaceOrientation = | "portrait" | "portraitUpsideDown" | "landscape" | "landscapeLeft" | "landscapeRight" | "all" | "allButUpsideDown" ``` ### 说明 - `portrait`:竖屏,Home 键在下(或标准竖屏方向) - `portraitUpsideDown`:竖屏倒置 - `landscape`:横屏,设备向左旋转 - `landscapeLeft`:横屏,设备向左旋转 - `landscapeRight`:横屏,设备向右旋转 - `all`:所有可旋转方向 - `allButUpsideDown`:除了竖屏,所有可旋转方向 *** ## NetworkInterface 描述单个网络接口地址的信息。 ```ts type NetworkInterface = { address: string netmask: string | null family: "IPv4" | "IPv6" mac: string | null isInternal: boolean cidr: string | null } ``` ### 字段说明 - `address`:IP 地址 - `netmask`:子网掩码 - `family`:地址族,IPv4 或 IPv6 - `mac`:MAC 地址(部分系统或接口可能为 null) - `isInternal`:是否为内部接口(如 loopback) - `cidr`:CIDR 表示形式,例如 `192.168.1.10/24` *** ## BatteryState 表示当前电池状态。 ```ts type BatteryState = "full" | "charging" | "unplugged" | "unknown" ``` ### 说明 - `full`:电量已充满 - `charging`:正在充电 - `unplugged`:未连接电源 - `unknown`:无法确定状态 *** ## Device Information ### model ```ts const model: string ``` 设备型号,例如 `"iPhone"`、`"iPad"`。 *** ### localizedModel ```ts const localizedModel: string ``` 本地化后的设备型号名称。 *** ### systemVersion ```ts const systemVersion: string ``` 当前操作系统版本号,例如 `"18.2"`。 *** ### systemName ```ts const systemName: string ``` 操作系统名称,例如 `"iOS"`、`"iPadOS"`、`"macOS"`。 *** ### isiPad / isiPhone ```ts const isiPad: boolean const isiPhone: boolean ``` 指示当前设备是否为 iPad 或 iPhone。 *** ### screen ```ts const screen: { width: number height: number scale: number } ``` 屏幕信息: - `width`:屏幕宽度(逻辑像素) - `height`:屏幕高度(逻辑像素) - `scale`:屏幕缩放比例(如 2、3) *** ## Battery & Sensors ### batteryState ```ts const batteryState: BatteryState ``` 当前电池状态。 *** ### batteryLevel ```ts const batteryLevel: number ``` 当前电量百分比,范围为 `0.0` 到 `1.0`。 *** ### proximityState ```ts const proximityState: boolean ``` 接近传感器状态,`true` 表示设备靠近用户(例如通话时贴近耳朵)。 *** ## Orientation & Layout ### isLandscape / isPortrait / isFlat ```ts const isLandscape: boolean const isPortrait: boolean const isFlat: boolean ``` - `isLandscape`:是否处于横屏 - `isPortrait`:是否处于竖屏 - `isFlat`:设备是否平放(face up / face down) *** ### orientation ```ts const orientation: Orientation ``` 当前设备物理方向。 *** ### supportedInterfaceOrientations ```ts var supportedInterfaceOrientations: InterfaceOrientation[] ``` 获取和设置当前支持的旋转方向。 #### 示例 ```tsx function Page() { useEffect(() => { Device.supportedInterfaceOrientations = ["all"] return () => { Device.supportedInterfaceOrientations = ["portrait"] } }, []) return ... } ``` ## Appearance & Environment ### colorScheme ```ts const colorScheme: ColorScheme ``` 当前系统颜色模式,例如浅色或深色模式。 *** ### isiOSAppOnMac ```ts const isiOSAppOnMac: boolean ``` 指示当前进程是否为运行在 macOS 上的 iPhone / iPad App(Mac Catalyst 或 iOS App on Mac)。 *** ## Locale & Language ### systemLocale ```ts const systemLocale: string ``` 当前系统 Locale,例如 `"en_US"`。 *** ### preferredLanguages ```ts const preferredLanguages: string[] ``` 用户偏好的语言列表,例如: ```ts ["en-US", "zh-Hans-CN"] ``` *** ### systemLocales(已废弃) ```ts const systemLocales: string[] ``` 已废弃,请使用 `preferredLanguages`。 *** ### systemLanguageTag ```ts const systemLanguageTag: string ``` 语言标签,例如 `"en-US"`。 *** ### systemLanguageCode ```ts const systemLanguageCode: string ``` 语言代码,例如 `"en"`。 *** ### systemCountryCode ```ts const systemCountryCode: string | undefined ``` 国家代码,例如 `"US"`。 *** ### systemScriptCode ```ts const systemScriptCode: string | undefined ``` 书写系统代码,例如 `"Hans"`(简体中文)。 *** ## Wake Lock ### isWakeLockEnabled ```ts const isWakeLockEnabled: Promise ``` 查询当前是否启用了屏幕唤醒锁定(防止设备自动锁屏)。 *** ### setWakeLockEnabled ```ts function setWakeLockEnabled(enabled: boolean): void ``` 启用或禁用 Wake Lock。 说明: - 仅在 **Scripting App** 内可用 - 启用后可防止设备自动休眠 *** ## Battery Listeners ### addBatteryStateListener ```ts function addBatteryStateListener( callback: (state: BatteryState) => void ): void ``` 监听电池状态变化。 *** ### removeBatteryStateListener ```ts function removeBatteryStateListener( callback?: (state: BatteryState) => void ): void ``` 移除电池状态监听器。 - 未传入 `callback` 时将移除所有监听器 *** ### addBatteryLevelListener ```ts function addBatteryLevelListener( callback: (level: number) => void ): void ``` 监听电量变化。 *** ### removeBatteryLevelListener ```ts function removeBatteryLevelListener( callback?: (level: number) => void ): void ``` 移除电量监听器。 *** ## Orientation Listeners ### addOrientationListener ```ts function addOrientationListener( callback: (orientation: Orientation) => void ): void ``` 开始监听设备方向变化。 注意事项: - 必须先调用该方法才能接收方向变化 - 系统方向锁开启时不会生效 *** ### removeOrientationListener ```ts function removeOrientationListener( callback?: (orientation: Orientation) => void ): void ``` 移除方向监听器。 - 未传入 `callback` 时将停止所有方向监听并结束观察 *** ## Proximity Listeners ### addProximityStateListener ```ts function addProximityStateListener( callback: (state: boolean) => void ): void ``` 监听接近传感器状态变化。 *** ### removeProximityStateListener ```ts function removeProximityStateListener( callback?: (state: boolean) => void ): void ``` 移除接近传感器监听器。 *** ## Network ### networkInterfaces ```ts function networkInterfaces(): Record ``` 获取当前设备的网络接口信息。 返回值说明: - Key:接口名称(如 `en0`、`lo0`) - Value:该接口对应的地址信息数组 适用于网络诊断、本地 IP 获取、调试用途。 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/Location.md --- # 定位(Location) Location API 用于获取设备的地理位置、进行正向与反向地理编码、从系统地图中选择位置,以及访问设备的方向与指南针信息。该 API 适用于脚本运行环境、交互式界面以及部分支持位置更新的小组件场景,并遵循系统的权限与精度限制。 ## LocationAccuracy 表示脚本期望接收的位置精度级别。 **类型定义** ```ts type LocationAccuracy = | "best" | "tenMeters" | "hundredMeters" | "kilometer" | "threeKilometers" | "bestForNavigation" | "reduced" ``` **说明** - `best` 请求设备可提供的最高精度。 - `tenMeters` 大约 10 米级别的精度。 - `hundredMeters` 大约 100 米级别的精度。 - `kilometer` 大约 1 公里级别的精度。 - `threeKilometers` 较粗略的 3 公里级别精度。 - `bestForNavigation` 面向导航场景,精度和更新频率最高,耗电量也更高。 - `reduced` 使用系统提供的低精度位置,常见于用户仅授权“模糊位置”的情况。 ## LocationInfo 表示一个基础的地理坐标信息。 **类型定义** ```ts type LocationInfo = { latitude: number longitude: number timestamp: number } ``` **字段说明** - `latitude` 纬度,单位为度。 - `longitude` 经度,单位为度。 - `timestamp` 位置采集时间,毫秒级时间戳。 ## LocationPlacemark 表示一个对人类友好的地理位置信息,通常由地理编码或反向地理编码返回。 **类型定义** ```ts type LocationPlacemark = { location?: LocationInfo region?: string timeZone?: string name?: string thoroughfare?: string subThoroughfare?: string locality?: string subLocality?: string administrativeArea?: string subAdministrativeArea?: string postalCode?: string isoCountryCode?: string country?: string inlandWater?: string ocean?: string areasOfInterest?: string[] } ``` **说明** Placemark 可能包含地址、城市、省份、国家、兴趣点等信息。具体字段是否存在取决于系统地图数据和地理位置本身。 ## Heading Heading 表示设备的方向与指南针相关信息。 **类型定义** ```ts type Heading = { headingAccuracy: number trueHeading: number magneticHeading: number timestamp: Date x: number y: number z: number } ``` **字段说明** - `headingAccuracy` 报告方向与真实地磁方向之间的最大误差,单位为度。 - `trueHeading` 相对于真北的方向角,单位为度。 - `magneticHeading` 相对于磁北的方向角,单位为度。 - `timestamp` 方向数据生成的时间。 - `x`、`y`、`z` 三轴地磁场原始数据,单位为微特斯拉。 ## 授权与配置 ### isAuthorizedForWidgetUpdates ```ts const isAuthorizedForWidgetUpdates: boolean ``` 表示当前小组件是否有资格接收位置更新。该值受系统权限和小组件能力限制影响。 ### accuracy ```ts const accuracy: LocationAccuracy ``` 当前配置的位置精度级别。 ### setAccuracy ```ts function setAccuracy(accuracy: LocationAccuracy): Promise ``` 设置脚本期望的位置精度。更高的精度可能会增加耗电量,并可能触发系统权限请求。 **示例** ```ts await Location.setAccuracy("hundredMeters") ``` ## 获取当前位置 ### requestCurrent ```ts function requestCurrent( options?: { forceRequest?: boolean } ): Promise ``` 请求当前设备的位置。 默认情况下,如果系统中存在可用的缓存位置,会直接返回缓存结果;如果不存在缓存位置,则会发起一次新的定位请求。 当 `forceRequest` 为 `true` 时,会忽略缓存位置,始终请求最新位置。 **示例** ```ts const location = await Location.requestCurrent() if (location) { console.log(location.latitude, location.longitude) } ``` 强制请求最新位置: ```ts const location = await Location.requestCurrent({ forceRequest: true }) ``` ### pickFromMap ```ts function pickFromMap(): Promise ``` 打开系统内置地图界面,让用户手动选择一个位置。 **示例** ```ts const picked = await Location.pickFromMap() if (picked) { console.log("Picked location:", picked.latitude, picked.longitude) } ``` ## 地理编码 ### reverseGeocode ```ts function reverseGeocode(options: { latitude: number longitude: number locale?: string }): Promise ``` 将经纬度坐标转换为可读的地址信息。 **示例** ```ts const placemarks = await Location.reverseGeocode({ latitude: 39.9042, longitude: 116.4074, locale: "zh-CN" }) console.log(placemarks?.[0]?.locality) ``` ### geocodeAddress ```ts function geocodeAddress(options: { address: string locale?: string }): Promise ``` 将文本地址转换为地理位置信息。 **示例** ```ts const results = await Location.geocodeAddress({ address: "天安门", locale: "zh-CN" }) const location = results?.[0]?.location ``` ## 方向与指南针 ### requestHeading ```ts function requestHeading(): Promise ``` 获取最近一次上报的方向信息。如果尚未开始方向更新,则返回 `null`。 **示例** ```ts const heading = await Location.requestHeading() if (heading) { console.log(heading.trueHeading) } ``` ### startUpdatingHeading ```ts function startUpdatingHeading(): Promise ``` 开始持续监听设备方向变化。 ### stopUpdatingHeading ```ts function stopUpdatingHeading(): void ``` 停止方向更新。 ### addHeadingListener ```ts function addHeadingListener( listener: (heading: Heading) => void ): void ``` 添加一个方向变化监听器。 **示例** ```ts await Location.startUpdatingHeading() Location.addHeadingListener(heading => { console.log("Heading:", heading.trueHeading) }) ``` ### removeHeadingListener ```ts function removeHeadingListener( listener?: (heading: Heading) => void ): void ``` 移除方向监听器。如果未传入参数,将移除所有监听器。 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Database and Connection.md --- # 数据库和连接 本章节介绍 SQLite 数据库的打开方式、连接配置以及与连接生命周期相关的行为说明。 SQLite 在 Scripting 中以**全局命名空间**的形式提供,无需导入即可直接使用。 *** ## 打开数据库 ### 打开磁盘数据库 ```ts const dbPath = Path.join( FileManager.appGroupDocumentsDirectory, "app.db" ) const db = SQLite.open(dbPath) ``` `SQLite.open` 用于打开或创建一个位于脚本数据目录中的 SQLite 数据库。 - 如果数据库文件不存在,将自动创建 - 多次调用 `open` 打开同一路径,内部会复用数据库资源 - 返回的 `Database` 实例用于后续所有数据库操作 *** ### 打开内存数据库 ```ts const db = SQLite.openInMemory("temp") ``` `SQLite.openInMemory` 用于创建一个仅存在于内存中的数据库。 - 内存数据库不会写入磁盘 - 当脚本结束或数据库被释放时,数据将全部丢失 - 适用于临时计算、测试或中间结果处理场景 `name` 参数用于区分不同的内存数据库实例。 *** ## 数据库配置(Configuration) 在打开数据库时,可以传入可选的配置对象: ```ts const db = SQLite.open(dbPath, { foreignKeysEnabled: true, readonly: false, journalMode: "wal", busyMode: 5, maximumReaderCount: 5, label: "main-db" }) ``` ### foreignKeysEnabled ```ts foreignKeysEnabled: boolean ``` 是否启用外键约束。 - `true`:启用 SQLite 外键约束(等同于 `PRAGMA foreign_keys = ON`) - `false`:禁用外键约束 建议在需要使用外键约束时显式开启。 *** ### readonly ```ts readonly: boolean ``` 是否以只读模式打开数据库。 - `true`:数据库仅允许查询操作,所有写操作将失败 - `false`:允许读写操作 只读模式适用于: - 数据浏览工具 - 只读分析脚本 - 防止意外修改数据的场景 *** ### journalMode ```ts journalMode: "wal" | "default" ``` 设置数据库的日志模式。 - `"wal"`:启用 Write-Ahead Logging,适合并发读写场景 - `"default"`:使用 SQLite 默认日志模式 在多数应用场景下,推荐使用 `"wal"`。 *** ### busyMode ```ts busyMode: "immediateError" | number ``` 控制数据库被锁定时的行为。 - `"immediateError"`:如果数据库被占用,立即抛出错误 - `number`:表示等待锁释放的最长时间(单位:秒) 示例: ```ts busyMode: 3 ``` 表示在数据库被锁定时,最多等待 3 秒。 *** ### maximumReaderCount ```ts maximumReaderCount: number ``` 限制同时存在的最大读取连接数量。 该配置用于控制并发读取行为,防止在高并发场景下占用过多系统资源。 - 较小的值可以减少资源占用 - 较大的值可以提高并发读取能力 *** ### label ```ts label: string | null ``` 为数据库连接指定一个可读标签。 该标签主要用于: - 调试 - 日志输出 - 内部诊断 对数据库行为本身没有影响。 *** ## Database 实例说明 `SQLite.open` 和 `SQLite.openInMemory` 返回一个 `Database` 实例。 ```ts const db: Database ``` 该实例: - 表示一个逻辑数据库连接 - 是所有 SQL 执行、事务和 Schema 操作的入口 - 不暴露底层连接、线程或队列细节 开发者无需关心数据库连接的创建、销毁或线程调度。 *** ## 并发与线程模型说明 SQLite 的并发控制和线程调度由 Scripting 内部处理: - JavaScript 侧不会直接接触多线程 - 所有数据库操作通过内部队列串行或受控并发执行 - 配置项(如 `busyMode`、`maximumReaderCount`)用于影响内部调度策略 这使得 SQLite API 在脚本层面具备以下特性: - 行为可预测 - 不需要手动加锁 - 不会出现跨线程访问问题 *** ## 使用建议 - 需要长期持久化数据时,使用磁盘数据库 - 临时计算或测试场景,使用内存数据库 - 有外键依赖时,显式开启 `foreignKeysEnabled` - 并发读写较多时,启用 `"wal"` 日志模式 - 工具型脚本或分析脚本可考虑只读模式 *** ## 下一步 完成数据库连接后,通常会继续以下操作: - 执行 SQL 与查询数据 - 使用事务批量写入数据 - 创建或管理表和索引 请继续阅读后续文档: - **Executing SQL & Queries** - **Transactions** - **Schema Management** --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Executing SQL and Query.md --- # 执行 SQL 和查询 本章节介绍如何在 SQLite 中执行 SQL 语句、绑定参数以及查询数据。 所有操作均通过 `Database` 实例完成,SQLite 会在内部处理连接、线程与调度细节,JavaScript 侧只需关注 SQL 与数据本身。 *** ## 执行 SQL ### execute ```ts db.execute(sql: string, arguments?: Arguments): Promise ``` `execute` 用于执行不返回结果集的 一个或多个 SQL 语句,常用于: - 创建或修改表结构 - 插入、更新、删除数据 - 执行 PRAGMA 语句 示例: ```ts await db.execute( "CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" ) ``` ```ts await db.execute( "UPDATE user SET age = ? WHERE name = ?", [19, "Tom"] ) ``` *** ## 参数绑定(Arguments) SQLite 支持两种参数绑定方式:**位置参数** 和 **命名参数**。 ### 位置参数 ```ts await db.execute( "INSERT INTO user (name, age) VALUES (?, ?)", ["Tom", 18] ) ``` 参数数组中的值会按顺序绑定到 SQL 中的 `?` 占位符。 *** ### 命名参数 ```ts await db.execute( "INSERT INTO user (name, age) VALUES (:name, :age)", { name: "Lucy", age: 20 } ) ``` 命名参数通过对象形式传入,键名需与 SQL 中的参数名一致。 *** ### DatabaseValue 类型 参数值支持以下类型: - `string` - `number` - `boolean` - `Data` - `Date` - `null` `Date` 和 `Data` 会按照 SQLite 约定方式进行转换和存储。 *** ## 查询数据 SQLite 提供三种查询方法,适用于不同的使用场景。 *** ### fetchAll ```ts db.fetchAll(sql: string, arguments?: Arguments): Promise ``` 执行查询并返回**所有结果行**。 示例: ```ts const users = await db.fetchAll<{ name: string; age: number }>( "SELECT name, age FROM user" ) ``` 当查询没有返回任何结果时,返回空数组。 *** ### fetchOne ```ts db.fetchOne(sql: string, arguments?: Arguments): Promise ``` 执行查询并返回**第一行结果**。 示例: ```ts const user = await db.fetchOne<{ name: string }>( "SELECT name FROM user WHERE age = ?", [18] ) ``` 使用场景: - 明确只期望一条结果(如按主键查询) - 查询聚合结果(如 `COUNT(*)`) 如果查询未返回任何结果,该方法将抛出错误。 *** ### fetchSet ```ts db.fetchSet(sql: string, arguments?: Arguments): Promise ``` 执行查询并返回去重后的结果集合。 该方法适用于以下场景: - 查询某一列的唯一值集合 - 需要在逻辑层面消除重复结果 示例: ```ts const names = await db.fetchSet<{ name: string }>( "SELECT name FROM user" ) ``` 返回结果中不会包含重复记录。 *** ## 类型映射说明 查询结果中的字段值会自动映射为 JavaScript 类型: - SQLite INTEGER → `number` - SQLite REAL → `number` - SQLite TEXT → `string` - SQLite BLOB → `Data` - SQLite NULL → `null` 返回对象的结构由查询语句决定,SQLite 不会强制要求与泛型 `T` 完全匹配,但建议保持一致以提高可读性与类型安全。 *** ## 错误处理 以下情况会导致方法抛出错误: - SQL 语法错误 - 参数数量或名称不匹配 - 违反约束(如唯一键、外键) - `fetchOne` 查询未返回结果 - 数据库被锁定且 `busyMode` 超时 建议在需要时使用 `try / catch` 捕获错误: ```ts try { await db.execute("INSERT INTO user (name) VALUES (?)", ["Tom"]) } catch (e) { console.error(e) } ``` *** ## 使用建议 - 使用 `execute` 执行无返回结果的 SQL - 查询多行数据时使用 `fetchAll` - 明确只需要一行结果时使用 `fetchOne` - 需要去重结果时使用 `fetchSet` - 优先使用参数绑定,避免字符串拼接 SQL - 复杂写操作应放入事务中执行 *** ## 下一步 当需要保证多条写操作的原子性,或在失败时自动回滚,请继续阅读: - **Transactions** --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Overview.md --- # 概述 SQLite 模块为 Scripting 提供了一套结构化、类型友好且可预测的数据库访问 API,用于在脚本中安全地读写 SQLite 数据库。 该模块是全局命名空间,不需要导入。它基于原生 SQLite 能力封装,支持磁盘数据库与内存数据库,覆盖常见的数据查询、事务管理、表结构管理以及 Schema 反射等需求,适用于本地数据持久化、缓存、日志记录以及中小规模结构化数据存储场景。 *** ## 快速开始 ### 打开数据库 ```ts const dbPath = Path.join(FileManager.appGroupDocumentsDirectory, "app.db") const db = SQLite.open(dbPath) ``` 打开一个位于脚本数据目录中的 SQLite 数据库。如果文件不存在,将自动创建。 也可以打开一个内存数据库: ```ts const db = SQLite.openInMemory("temp") ``` *** ### 执行 SQL ```ts await db.execute( "CREATE TABLE user (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)" ) await db.execute( "INSERT INTO user (name, age) VALUES (?, ?)", ["Tom", 18] ) ``` 支持位置参数和命名参数两种形式。 *** ### 查询数据 ```ts const users = await db.fetchAll<{ name: string; age: number }>( "SELECT name, age FROM user" ) const user = await db.fetchOne<{ name: string }>( "SELECT name FROM user WHERE age = ?", [18] ) ``` *** ### 使用事务 ```ts await db.transcation([ { sql: "INSERT INTO user (name, age) VALUES (?, ?)", args: ["Tom", 18] }, { sql: "INSERT INTO user (name, age) VALUES (?, ?)", args: ["Lucy", 20] } ]) ``` 事务以 **步骤列表(steps)** 的形式声明,所有步骤将按顺序执行,任一步失败都会触发回滚。 *** ## 核心能力概览 SQLite 模块主要提供以下能力: - 数据库连接管理 - SQL 执行与参数绑定 - 结构化数据查询 - 显式事务管理 - 表与索引的创建与删除 - 数据库 Schema 信息查询 *** ## 文档结构说明 SQLite 模块的文档按照功能拆分为多个部分,你可以根据需求查阅对应章节: - **Database & Connection** 打开数据库、配置选项、只读模式与并发相关说明 - **Executing SQL & Queries** 执行 SQL、参数绑定规则、查询方法说明 - **Transactions** 事务模型、事务类型以及设计约束说明 - **Schema Management** 创建和管理表、索引的结构化 API - **Schema Introspection** 查询表结构、主键、外键和索引信息 - **Types Reference** SQLite 模块中使用的类型与枚举定义 *** ## 适用场景 SQLite 模块适用于以下场景: - 本地数据持久化 - 脚本级缓存与状态存储 - 中小规模结构化数据管理 - 构建基于 SQLite 的工具型脚本 - 数据迁移、分析与调试辅助工具 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Schema Introspection.md --- # 模式探测(Schema Introspection) 下面是 **Schema Introspection** 的中文说明文档,作为 SQLite 模块中**数据库结构查询与反射能力**的章节,重点介绍如何获取表、列、主键、外键和索引信息,适用于调试、迁移、工具脚本等场景,风格与前文保持一致,可直接发布。 *** \##Schema Introspection 本章节介绍如何使用 SQLite 提供的 Schema Introspection API 来查询和分析数据库结构。 Schema Introspection 允许脚本在运行时获取数据库的结构信息,包括表是否存在、列定义、主键、外键以及索引情况。这些能力通常用于: - 数据迁移与版本管理 - 工具型脚本(如数据库浏览器、分析工具) - 运行时结构校验 - 调试与诊断 *** ## 获取 Schema 版本 ### schemaVersion ```ts db.schemaVersion(): Promise ``` 返回当前数据库的 schema 版本号。 该值通常用于: - 判断数据库是否需要迁移 - 与外部版本管理逻辑配合使用 示例: ```ts const version = await db.schemaVersion(); ``` *** ## 检查表是否存在 ### tableExists ```ts db.tableExists(tableName: string, schemaName?: string): Promise ``` 判断指定表是否存在。 - `schemaName` 默认为主 schema - 返回 `true` 表示表存在 示例: ```ts const exists = await db.tableExists("user"); ``` *** ## 查询列信息 ### columnsIn ```ts db.columnsIn(tableName: string, schemaName?: string): Promise ``` 返回指定表中所有列的结构信息。 *** ### ColumnInfo ```ts type ColumnInfo = { name: string; type: string; defaultValueSQL: string | null; isNotNull: boolean; primaryKeyIndex: number; }; ``` 字段说明: - `name`:列名 - `type`:列类型 - `defaultValueSQL`:默认值的 SQL 表达式 - `isNotNull`:是否为 NOT NULL - `primaryKeyIndex`:主键中的顺序(非主键为 0) 示例: ```ts const columns = await db.columnsIn("user"); ``` *** ## 查询主键信息 ### primaryKey ```ts db.primaryKey(tableName: string, schemaName?: string): Promise ``` 返回指定表的主键信息。 *** ### PrimaryKeyInfo ```ts type PrimaryKeyInfo = { columns: string[]; rowIDColumn: string | null; isRowID: boolean; }; ``` 字段说明: - `columns`:主键列名数组 - `rowIDColumn`:对应的 rowid 列名(如果存在) - `isRowID`:是否使用 SQLite 隐式 rowid 作为主键 示例: ```ts const pk = await db.primaryKey("user"); ``` *** ## 查询外键信息 ### foreignKeys ```ts db.foreignKeys(tableName: string, schemaName?: string): Promise ``` 返回指定表中定义的所有外键信息。 *** ### ForeignKeyInfo ```ts type ForeignKeyInfo = { id: number; originColumns: string[]; destinationTable: string; destinationColumns: string[]; mapping: { origin: string; destination: string; }[]; }; ``` 字段说明: - `id`:外键 ID - `originColumns`:本表中的外键列 - `destinationTable`:引用的目标表 - `destinationColumns`:目标表中的列 - `mapping`:列之间的一一映射关系 示例: ```ts const fks = await db.foreignKeys("order"); ``` *** ## 查询索引信息 ### indexes ```ts db.indexes(tableName: string, schemaName?: string): Promise ``` 返回指定表上的所有索引信息。 *** ### IndexInfo ```ts type IndexInfo = { name: string; columns: string[]; isUnique: boolean; origin: "createIndex" | "primaryKeyConstraint" | "uniqueConstraint"; }; ``` 字段说明: - `name`:索引名称 - `columns`:索引包含的列 - `isUnique`:是否为唯一索引 - `origin`:索引来源 - `"createIndex"`:通过 `createIndex` 创建 - `"primaryKeyConstraint"`:主键约束生成 - `"uniqueConstraint"`:唯一约束生成 示例: ```ts const indexes = await db.indexes("user"); ``` *** ## 检查唯一键组合 ### isTableHasUniqueKeys ```ts db.isTableHasUniqueKeys( tableName: string, uniqueKeys: string[] ): Promise ``` 判断指定表是否存在**完全匹配**给定列组合的唯一约束或唯一索引。 示例: ```ts const hasUnique = await db.isTableHasUniqueKeys("user", ["email"]); ``` 该方法常用于: - 判断是否需要创建唯一索引 - 在迁移或初始化阶段避免重复定义约束 *** ## 使用建议 - 在执行结构变更前,先使用 introspection API 判断当前状态 - 数据迁移逻辑中优先使用结构判断而非假设 - 工具型脚本可以结合 Schema Introspection 构建可视化或分析能力 - 不要在高频业务路径中频繁调用结构查询 API *** ## 总结 Schema Introspection 为 SQLite 提供了运行时结构反射能力,使脚本可以安全、可靠地感知数据库当前状态。 它通常与以下能力配合使用: - **Schema Management**:定义和修改结构 - **Transactions**:保证结构变更的原子性 - **Executing SQL & Queries**:在已知结构基础上操作数据 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Schema Management.md --- # 模式管理(Schema Management) 本章节介绍如何使用 SQLite 的结构化 API 来创建、修改和删除表与索引。 与直接拼接 SQL 不同,Schema Management 提供了一套**声明式、可读性更强且更安全**的方式来管理数据库结构,适用于长期维护的数据模型。 *** ## 创建表 ### createTable ```ts db.createTable( name: string, options: { columns: ColumnDefinition[] ifNotExists?: boolean } ): Promise ``` `createTable` 用于创建一张新表。 示例: ```ts await db.createTable("user", { ifNotExists: true, columns: [ { name: "id", type: "INTEGER", primaryKey: true, autoIncrement: true }, { name: "name", type: "TEXT", notNull: true }, { name: "age", type: "INTEGER" } ] }) ``` *** ### ColumnDefinition ```ts type ColumnDefinition = { name: string type: string primaryKey?: boolean autoIncrement?: boolean notNull?: boolean unique?: boolean indexed?: boolean checkSQL?: string collation?: DatabaseCollation defaultValue?: DatabaseValue defaultSQL?: string references?: ColumnReferences } ``` 每一列的定义包含以下常用属性: - `name`:列名 - `type`:SQLite 列类型(如 `INTEGER`、`TEXT`) - `primaryKey`:是否为主键 - `autoIncrement`:是否启用自增(仅适用于整数主键) - `notNull`:是否为 NOT NULL - `unique`:是否添加唯一约束 - `indexed`:是否为该列创建索引 - `checkSQL`:CHECK 约束表达式 - `collation`:列排序规则 - `defaultValue`:默认值(参数化形式) - `defaultSQL`:默认值(原生 SQL 表达式) - `references`:外键引用定义 *** ### 默认值说明 `defaultValue` 与 `defaultSQL` 二者互斥,应选择其一: ```ts { name: "createdAt", type: "INTEGER", defaultSQL: "CURRENT_TIMESTAMP" } ``` ```ts { name: "status", type: "TEXT", defaultValue: "active" } ``` *** ### 外键引用 ```ts references?: { table: string column?: string onDelete?: "cascade" | "restrict" | "setNull" | "setDefault" onUpdate?: "cascade" | "restrict" | "setNull" | "setDefault" deferred?: boolean } ``` 示例: ```ts { name: "userId", type: "INTEGER", references: { table: "user", column: "id", onDelete: "cascade" } } ``` *** ## 重命名表 ### renameTable ```ts db.renameTable(name: string, newName: string): Promise ``` 示例: ```ts await db.renameTable("user", "users") ``` *** ## 删除表 ### dropTable ```ts db.dropTable(name: string): Promise ``` 示例: ```ts await db.dropTable("temp_data") ``` *** ## 创建索引 ### createIndex ```ts db.createIndex( name: string, options: { table: string columns: string[] unique?: boolean ifNotExists?: boolean condition?: string } ): Promise ``` 示例: ```ts await db.createIndex("idx_user_name", { table: "user", columns: ["name"], unique: false }) ``` *** ### 条件索引 ```ts await db.createIndex("idx_active_user", { table: "user", columns: ["name"], condition: "age >= 18" }) ``` *** ## 删除索引 ### dropIndex ```ts db.dropIndex(name: string): Promise ``` 示例: ```ts await db.dropIndex("idx_user_name") ``` *** ### dropIndexOn ```ts db.dropIndexOn(tableName: string, columns: string[]): Promise ``` 用于删除指定表和列组合上的索引。 示例: ```ts await db.dropIndexOn("user", ["name"]) ``` *** ## 设计说明 Schema Management API 的设计遵循以下原则: - **结构优先于字符串拼接** 提供清晰的结构定义,降低 SQL 拼写错误风险 - **声明式而非命令式** 明确表达“表应该是什么样子”,而不是“如何拼 SQL” - **与 SQLite 原生能力一一映射** 不引入额外抽象,避免隐藏行为 *** ## 使用建议 - 对长期存在的表结构,优先使用 `createTable` - 使用 `ifNotExists` 避免重复创建 - 对高频查询字段显式创建索引 - 使用外键时,确保已启用 `foreignKeysEnabled` - Schema 变更建议结合事务或迁移逻辑执行 *** ## 下一步 在完成表和索引的创建后,你可能需要: - 查询数据库中已有的表结构 - 获取列、主键、外键信息 - 分析索引和约束情况 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/SQLite/Transcation.md --- # 事务(Transcation) 本章节介绍 SQLite 中的事务模型、事务类型。 SQLite 的事务 API 采用\*\*基于步骤(step-based)\*\*的声明式模型,用于在脚本层面提供可预测、可控且安全的事务行为。 *** ## 事务概述 事务用于将多条数据库操作组合为一个原子操作单元: - 所有步骤成功执行时,事务提交 - 任意步骤失败时,事务回滚 - 回滚后数据库状态保持不变 *** ## transcation ```ts db.transcation( steps: TranscationStep[], options?: { kind?: "deferred" | "immediate" | "exclusive" } ): Promise ``` `transcation` 用于执行一个事务,该事务由一组有序的 SQL 步骤组成。 *** ## 事务步骤(TranscationStep) ```ts type TranscationStep = { sql: string args?: Arguments | null } ``` 每一个事务步骤包含: - `sql`:要执行的 SQL 语句 - `args`:可选的参数绑定 示例: ```ts await db.transcation([ { sql: "INSERT INTO user (name, age) VALUES (?, ?)", args: ["Tom", 18] }, { sql: "INSERT INTO user (name, age) VALUES (?, ?)", args: ["Lucy", 20] } ]) ``` 步骤会按照声明顺序依次执行。 *** ## 事务类型(Transaction Kind) 事务支持三种类型,对应 SQLite 原生的事务模式。 ```ts kind?: "deferred" | "immediate" | "exclusive" ``` ### deferred 默认事务类型。 - 事务开始时不立即获取锁 - 在首次读或写操作时才尝试获取锁 - 适合大多数普通事务场景 *** ### immediate - 事务开始时立即尝试获取写锁 - 如果无法获取写锁,将立即失败 - 适用于需要确保后续写操作一定能执行的场景 *** ### exclusive - 事务开始时获取排他锁 - 阻止其他读写操作 - 适用于需要完全独占数据库的特殊场景 *** ## 错误与回滚行为 在事务执行过程中,如果发生以下情况: - SQL 执行失败 - 参数绑定错误 - 违反约束(唯一键、外键等) - 数据库锁冲突 SQLite 将自动回滚整个事务,并抛出错误。 示例: ```ts try { await db.transcation([ { sql: "INSERT INTO user (id, name) VALUES (1, 'Tom')" }, { sql: "INSERT INTO user (id, name) VALUES (1, 'Lucy')" } ]) } catch (e) { console.error("Transaction failed:", e) } ``` *** ## 使用建议 - 将一组必须同时成功的写操作放入同一个事务 - 优先使用默认的 `deferred` 事务类型 - 对于关键写操作,可使用 `immediate` 提前锁定 - 避免在事务中执行耗时或无关的操作 - 不要在事务中依赖条件分支来决定是否执行步骤 *** ## 下一步 完成事务操作后,你可能还需要: - 创建和管理表结构 - 定义索引和约束 - 查询数据库 Schema 信息 请继续阅读: - **Schema Management** - **Schema Introspection** --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/VideoRecorder/Quick Start/index.md --- # 快速掌握视频录制 `VideoRecorder` 是 Scripting 提供的高层视频采集与录制接口,对底层 `AVCaptureSession`、`AVAssetWriter`、音视频同步、方向处理等复杂逻辑进行了封装,以 **状态驱动** 的方式向脚本侧暴露一个稳定、可组合的录制 API。 该模块适用于: - 视频录制(支持暂停 / 恢复) - 同步音频录制 - 高帧率 / 高码率 / ProRes 编码 - 拍照(录像过程中抓帧) - 实时对焦、曝光、变焦、补光灯控制 - 与 UI 解耦的预览展示(通过 `VideoRecorderPreviewView`) *** ## 核心设计理念 ### 状态机驱动 `VideoRecorder` 内部采用明确的状态机模型,所有操作都受当前状态约束,避免非法调用导致的崩溃或未定义行为。 ```ts type State = | "idle" | "preparing" | "ready" | "recording" | "paused" | "stopping" | "finished" | "failed" ``` - **idle** 初始状态,尚未创建或已重置会话 - **preparing** 正在配置采集会话、设备、编码参数 - **ready** 会话已准备完成,可以开始录制 - **recording** 正在录制视频(音视频同步写入) - **paused** 录制已暂停,时间线被冻结 - **stopping** 正在结束录制并写入文件尾 - **finished** 录制完成,`details` 中包含输出文件路径 - **failed** 出现错误,`details` 中包含错误信息 *** ## Capture Session ```ts class AVCaptureSession { private constructor() } ``` `VideoRecorder.session` 暴露的是一个只读的 `AVCaptureSession` 实例,用于: - 绑定预览视图 - 与其他需要底层 Session 的组件协作 该对象**不能自行创建或修改**,生命周期由 `VideoRecorder` 管理。 *** ## 录制配置(Configuration) ```ts type Configuration = { camera?: { position: "front" | "back" preferredTypes?: CameraType[] } frameRate?: number audioEnabled?: boolean sessionPreset?: SessionPreset videoCodec?: VideoCodec videoBitRate?: number orientation?: VideoOrientation mirrorFrontCamera?: boolean autoConfigAppAudioSession?: boolean } ``` ### camera - `position` 使用前置或后置摄像头 - `preferredTypes` 优先选择的物理摄像头类型,例如: - `"wide"` - `"ultraWide"` - `"telephoto"` - `"triple"` 如果不提供,将根据 `position` 自动选择系统默认组合。 *** ### frameRate 支持的帧率: - 24 - 30(默认) - 60 - 120(设备支持的前提下) *** ### audioEnabled 是否录制音频,默认 `true`。 *** ### sessionPreset 控制采集分辨率与质量,例如: - `"high"` - `"hd1920x1080"` - `"hd4K3840x2160"` *** ### videoCodec 支持多种编码格式,包括: - `"hevc"`(默认) - `"h264"` - `"hevcWithAlpha"` - `"proRes422"` - `"proRes4444"` - `"appleProRes4444XQ"` - `"proResRAW"` 等 ⚠️ 部分 ProRes 编码对设备与系统版本有要求。 *** ### videoBitRate 视频码率(bps),默认 `5_000_000`。 仅对部分编码器生效。 *** ### orientation 录制时的视频方向: - `"portrait"`(默认) - `"landscapeLeft"` - `"landscapeRight"` 该值影响最终文件的方向元数据与写入顺序。 *** ### mirrorFrontCamera 是否镜像前置摄像头画面,默认 `false`。 *** ### autoConfigAppAudioSession 是否由系统自动配置 `AVAudioSession`,默认 `true`。 - `true` 系统会根据摄像头方向、麦克风位置自动调整 Audio Session **不会在录制结束后恢复原状态** - `false` 由应用自行管理 Audio Session ⚠️ 如果配置不兼容,可能导致录制失败 *** ## 状态与监听 ### 获取当前状态 ```ts function getState(): Promise ``` 返回当前 `VideoRecorder` 的状态。 *** ### 监听状态变化 ```ts function addStateListener( listener: (state: State, details?: string) => void ): void ``` - `state` 新状态 - `details` - `failed`:错误信息 - `finished`:输出文件路径 ```ts function removeStateListener( listener?: (state: State, details?: string) => void ): void ``` 不传参数将移除所有监听器。 *** ## 生命周期控制 ### prepare ```ts function prepare(configuration?: Configuration): Promise ``` - 创建并配置采集会话 - 请求相机 / 麦克风权限 - 初始化编码器 成功后进入 `ready` 状态。 *** ### start ```ts function start(toPath: string): Promise ``` - 开始录制 - 视频写入指定路径 - 进入 `recording` 状态 *** ### pause / resume ```ts function pause(): Promise function resume(): Promise ``` - 暂停与恢复时间线 - 不会生成新文件 - 适用于长时间录制、分段控制 *** ### stop ```ts function stop(options?: { closeSession?: boolean }): Promise ``` - 正常结束录制 - 进入 `finished` 状态 - `details` 中返回最终文件路径 *** ### cancel ```ts function cancel(options?: { closeSession?: boolean }): Promise ``` - 中断录制 - 删除已生成的文件 - 不进入 `finished` *** ### reset ```ts function reset(): Promise ``` - 关闭采集会话 - 清理内部状态 - 状态回到 `idle` 适用于彻底释放资源或重新切换摄像头。 *** ## 拍照能力 ```ts function takePhoto(): Promise ``` - 仅在 `recording` 状态有效 - 返回当前帧的静态图片 - 不影响视频录制 *** ## 相机控制能力 ### 补光灯(Torch) ```ts const hasTorch: boolean const torchMode: "auto" | "on" | "off" function setTorchMode(mode: "auto" | "on" | "off"): void ``` *** ### 对焦与曝光 ```ts function setFocusPoint(point: { x: number; y: number }): void function setExposurePoint(point: { x: number; y: number }): void function resetFocus(): void function resetExposure(): void ``` - 坐标为 **归一化坐标**(0\~1) - 左上角为 `{ x: 0, y: 0 }` *** ### 变焦 ```ts const minZoomFactor: number const maxZoomFactor: number const currentZoomFactor: number function setZoomFactor(factor: number): void function rampZoomFactor(toFactor: number, rate: number): void function resetZoom(): void ``` iOS 18+ 额外提供: ```ts const displayZoomFactor: number const displayZoomFactorMultiplier: number ``` 用于 UI 层展示更符合用户直觉的倍率值。 *** ## 典型使用流程 ```ts await VideoRecorder.prepare(config) await VideoRecorder.start(path) // recording await VideoRecorder.pause() await VideoRecorder.resume() await VideoRecorder.stop() // or await VideoRecorder.cancel() await VideoRecorder.reset() ``` *** ## 使用建议与注意事项 - `prepare → start → stop / cancel` 是一条完整生命周期 - 不建议在 `recording` 状态切换摄像头,应先 `reset` - 高帧率 + ProRes 对设备性能与存储要求较高 - 若关闭 `autoConfigAppAudioSession`,需自行保证音频会话兼容性 - 预览 UI 与录制逻辑解耦,推荐通过 `VideoRecorderPreviewView` 展示画面 --- url: /doc_v2/TestFlight/zh/guide/Changelog/2.4.7/VideoRecorder/Quick Start/index_example.md --- # 示例 ```tsx import { Button, Navigation, NavigationStack, Script, useEffect, Path, MagnifyGesture, useObservable, VideoRecorderPreviewView, VStack, Toolbar, ToolbarItem, ToolbarItemGroup } from "scripting" const recorder = VideoRecorder function View() { // Access dismiss function. const dismiss = Navigation.useDismiss() const state = useObservable("idle") const displayZoom = useObservable(1) const startZoom = useObservable(1) const volume = useObservable(0) const toastVisible = useObservable(false) const toastMessage = useObservable("") const position = useObservable("back") function showToast(message: string) { toastVisible.setValue(true) toastMessage.setValue(message) } useEffect(() => { const listener = (value: number, old: number) => { console.log("old:", old, "new:", value) volume.setValue(value) } SharedAudioSession.addOutputVolumeListener(listener) return () => { SharedAudioSession.removeOutputVolumeListener(listener) } }, []) async function prepare() { await recorder.prepare({ camera: { position: position.value, // preferredTypes: ["triple"] }, frameRate: 30, audioEnabled: true, orientation: "portrait", sessionPreset: "high", videoCodec: "appleProRes4444XQ", // autoConfigAppAudioSession: false }) } useEffect(() => { prepare().then(() => { recorder.start( Path.join( FileManager.documentsDirectory, "test.mov" ) ) }).catch(e => { showToast("Failed to prepare:" + String(e)) }) recorder.addStateListener(( newState, details ) => { state.setValue(newState) if (newState === "ready") { // recorder.rampZoomFactor(0.5, 4 } if (newState === "failed") { Dialog.alert(details!) } }) return () => { recorder.reset() } }, []) return `) await webView.present({ navigationTitle: '网页视图示例' }) webView.dispose() ``` --- url: /doc_v2/TestFlight/zh/guide/Intent/Intent.continueInForeground.md --- # Intent.continueInForeground `Intent.continueInForeground` 用于在脚本从 Shortcuts 中后台执行时,**请求系统将流程转移到 Scripting App 的前台继续运行**。 此过程需要用户明确确认。 适用场景包括: - 需要展示完整 UI(如表单、列表、导航页面) - 需要用户在 App 内进行交互操作 - 后续步骤无法在后台执行 调用此方法后,系统会弹出确认对话框: - 用户 **允许** → Scripting App 打开到前台,脚本继续执行 - 用户 **取消** → 当前脚本立即终止 - 此行为完全由系统管理,开发者无需手动处理跳转流程 由于该能力基于 iOS 26 引入的 AppIntents 行为: **该 API 只能在 iOS 26 及以上系统使用。** *** \##API 定义 ```ts function continueInForeground( dialog?: Dialog | null, options?: { alwaysConfirm?: boolean; }, ): Promise; ``` *** \##参数说明 ## dialog?: Dialog | null 用于提示用户为什么需要切换到前台继续执行。 `Dialog` 的类型格式支持三种形式: ```ts type Dialog = | string | { full: string; supporting: string } | { full: string; supporting: string; systemImageName: string } | { full: string; systemImageName: string }; ``` 示例: ```ts "是否前往应用继续执行?"; ``` 或带辅助说明: ```ts { full: "需要在应用中继续执行下一步操作", supporting: "接下来的步骤需要完整的 UI 交互。", systemImageName: "app" } ``` 若传入 `null`,系统可能不显示提示,仅直接触发系统确认(不推荐)。 *** ## `options?: { alwaysConfirm?: boolean }` 用于控制系统是否每次都显示确认提示。 - `alwaysConfirm: false`(默认) 系统一般会根据上下文自动判断是否需要确认。 - `alwaysConfirm: true` 每次调用都会提示用户明确确认。 示例: ```ts { alwaysConfirm: true; } ``` *** \##执行流程 执行 `await Intent.continueInForeground(...)` 时: 1. 快捷指令执行暂停 2. 系统弹出确认对话框 3. 用户选择: - **确认** → 打开 Scripting App → 脚本继续 - **取消** → 脚本立即终止 4. 后续脚本在 Scripting App 前台环境中继续执行 **注意:脚本不会在后台继续运行,必须等待用户操作。** *** \##典型应用场景 推荐在以下场景调用: - 需要展示完整的导航界面或交互表单(如示例中的 TextField) - 需要使用 `Navigation.present` 呈现 UI - 需要 App 内操作如: - 预览文件 - 编辑长文本 - 选择复杂数据 - 多步骤流程 不推荐在以下情况使用: - 单纯的数据处理,不需要 UI - 简单操作已经可通过 SnippetIntent 完成 *** \##完整示例代码 以下示例展示如何从 Shortcuts 通过 `continueInForeground` 切换到 Scripting App 前台,然后展示 UI 让用户输入文本,输入结束后再返回 Shortcuts。 ```tsx // intent.tsx import { Button, Intent, List, Navigation, NavigationStack, Script, Section, TextField, useState, } from "scripting"; function View() { const dismiss = Navigation.useDismiss(); const [text, setText] = useState(""); return (

); } async function runIntent() { // 请求系统将执行流程切换到 Scripting App 前台 await Intent.continueInForeground("Do you want to open the app and continue?"); // 在前台呈现交互式 UI,用户填写文本 const text = await Navigation.present(); // 可选:返回到快捷指令界面 Safari.openURL("shortcuts://"); // 返回结果给 Shortcuts Script.exit(Intent.text(text ?? "No text return")); } runIntent(); ``` *** \##注意事项与最佳实践 - **必须运行在 iOS 26+**,否则会抛出异常或行为不可用。 - 若脚本依赖用户输入、复杂 UI 或操作,请使用该 API 触发前台模式。 - 对话内容应清晰说明需要用户切换前台的原因,提升用户信任度。 - 若用户拒绝,脚本将终止,开发者无需自行处理取消逻辑。 - 可以与 SnippetIntent 结合,构建完整的后台 UI + 前台 UI 混合流程。 --- url: /doc_v2/TestFlight/zh/guide/Intent/Intent.requestConfirmation.md --- # Intent.requestConfirmation `Intent.requestConfirmation` 用于在脚本执行过程中,**向用户请求确认某项操作**。 调用后,系统会暂停脚本执行,并展示一个基于 **SnippetIntent 的 UI** 作为确认界面,同时可显示提示对话内容。 确认流程行为: - 用户 **确认** → Promise resolve,脚本继续执行 - 用户 **取消** → 当前脚本终止执行 - 确认界面通过传入的 **SnippetIntent** 的 UI 定义 - 系统自动管理此流程,无需开发者处理 UI 呈现逻辑 **该 API 仅可在 iOS 26 及以上系统使用。** *** \##API 定义 ```ts function requestConfirmation( actionName: ConfirmationActionName, snippetIntent: AppIntent, options?: { dialog?: Dialog; showDialogAsPrompt?: boolean; }, ): Promise; ``` *** \##参数说明 ## actionName: ConfirmationActionName 用于告诉系统“要确认的行为语义是什么”,系统会根据该值生成自然语言文案。例如: - `"set"` → “确定要设置…?” - `"buy"` → “确定要购买…?” - `"toggle"` → “是否切换…?” 可选值如下(与苹果 AppIntents 框架一致): ``` "add" | "addData" | "book" | "buy" | "call" | "checkIn" | "continue" | "create" | "do" | "download" | "filter" | "find" | "get" | "go" | "log" | "open" | "order" | "pay" | "play" | "playSound" | "post" | "request" | "run" | "search" | "send" | "set" | "share" | "start" | "startNavigation" | "toggle" | "turnOff" | "turnOn" | "view" ``` 选择合适的语义有助于提高确认界面的自然体验。 *** ## snippetIntent: SnippetIntent 必须是一个 **注册为 SnippetIntent 类型的 AppIntent**: ```ts AppIntent; ``` 用户在确认界面中看到的内容就是该 SnippetIntent 的 `perform()` 返回的 UI,例如选项列表、内容预览等。 *** ## `options?: { dialog?: Dialog; showDialogAsPrompt?: boolean }` ### dialog?: Dialog 用于在确认 UI 上方或系统对话框中显示提示文本。 支持四种格式: ```ts type Dialog = | string | { full: string; supporting: string } | { full: string; supporting: string; systemImageName: string } | { full: string; systemImageName: string }; ``` 示例: ```ts "是否继续?"; ``` 更复杂的: ```ts { full: "确定要设置此颜色吗?", supporting: "此操作将更新应用的主题颜色。", systemImageName: "paintpalette" } ``` 用途: - 解释确认动作含义 - 提醒用户可能产生的影响 - 提供更友好的交互上下文 *** ### showDialogAsPrompt?: boolean 默认值:`true` 决定系统是否以「提示弹窗」方式显示对话文本。 设为 `false` 时,文本可能以更沉浸的方式显示在 Snippet 卡片内部。 *** \##执行流程 调用 `await Intent.requestConfirmation(...)` 时脚本执行顺序如下: 1. 脚本暂停执行 2. 系统展示确认界面(SnippetIntent UI + 可选 dialog 文案) 3. 用户进行交互: - **确认** → Promise resolve,脚本继续 - **取消** → 脚本终止执行 4. 不需要开发者手动关闭 UI 此流程完全由系统管理。 *** \##使用场景 以下场景推荐使用 `requestConfirmation`: - 修改重要设置(如主题颜色、隐私设置) - 对数据执行有副作用的操作(如删除、更新、重置) - 流程中一步需用户明确授权 - 启动某个需要用户选择的 UI 子流程(如颜色选择器、账号切换器) 不适用场景: - 简单数据处理,不需要用户确认 - 可以在后台无 UI 完成的操作 *** \##完整示例代码 以下示例展示如何使用 `requestConfirmation` 请求用户确认一次颜色选择,并在确认后继续执行脚本。 假设你已有两个 SnippetIntent: - `PickColorIntent`:颜色选择 UI - `ShowResultIntent`:结果展示 UI ## intent.tsx 示例 ```tsx import { Intent, Script } from "scripting"; import { PickColorIntent, ShowResultIntent } from "./app_intents"; async function runIntent() { // 第一步:请求用户确认颜色选择 await Intent.requestConfirmation("set", PickColorIntent(), { dialog: { full: "确定要设置此颜色吗?", supporting: "此操作将更新应用的主题颜色。", systemImageName: "paintpalette", }, }); // 第二步:读取来自 Shortcuts 的输入(如果有) const text = Intent.shortcutParameter?.type === "text" ? Intent.shortcutParameter.value : "No text parameter from Shortcuts"; // 第三步:呈现最终 SnippetIntent const snippet = Intent.snippetIntent({ snippetIntent: ShowResultIntent({ content: text }), }); Script.exit(snippet); } runIntent(); ``` *** \##注意事项与最佳实践 - **必须运行在 iOS 26+** 提前检查系统版本或优雅降级。 - **总是提供清晰的 dialog 文案** 确认行为应让用户理解,不应仅依赖 Snippet UI 本身。 - **用于重要或可逆性较差的操作** 如修改设置、启动后台任务、提交数据等。 - **与 SnippetIntent 配合使用效果最佳** 因为确认 UI 直接展示 SnippetIntent 的视图。 - **用户取消时脚本会被系统直接终止** 不要在后续代码中假设脚本一定会继续执行。 --- url: /doc_v2/TestFlight/zh/guide/Intent/Quick Start.md --- # 快速开始 Scripting 支持通过 `intent.tsx` 文件创建 iOS Intents,实现脚本与系统分享扩展(Share Sheet)和快捷指令(Shortcuts)的深度集成。你可以接收来自用户的文本、图片、文件和 URL 等输入,并返回结果供调用方使用。通过 UI 展示、数据处理与结果返回,可构建灵活且强大的自动化流程。 *** ## 一、创建和配置 Intent ### 1. 创建 Intent 脚本 1. 在 Scripting 中新建一个脚本项目。 2. 添加名为 `intent.tsx` 的文件,并编写处理逻辑和可选的 UI 组件。 ### 2. 配置支持的输入类型 点击编辑器顶部标题栏中的项目名称,打开 **Intent 设置页面**,选择该脚本支持的输入类型,如: - 文本(Text) - 图片(Image) - 文件路径(File URL) - URL 配置后,该脚本就能在分享扩展或 Shortcuts 中处理相应类型的数据。 *** ## 二、处理输入数据 在 `intent.tsx` 中,可通过以下 API 访问用户传入的数据: | 属性名 | 说明 | | -------------------------- | ---------------------------------------- | | `Intent.shortcutParameter` | Shortcuts 中传入的单个参数,包含 `.type` 和 `.value` | | `Intent.textsParameter` | 文本字符串数组 | | `Intent.urlsParameter` | URL 字符串数组 | | `Intent.imagesParameter` | 图片数组(UIImage 实例) | | `Intent.fileURLsParameter` | 文件路径数组(本地 URL) | 示例: ```ts if (Intent.shortcutParameter) { if (Intent.shortcutParameter.type === "text") { console.log(Intent.shortcutParameter.value) } } ``` *** ## 三、返回结果 使用 `Script.exit(result)` 结束脚本执行并返回结果给调用方,例如 Shortcuts 或另一个脚本。支持的返回类型包括: - 文本:`Intent.text(value)` - 富文本:`Intent.attributedText(value)` - URL:`Intent.url(value)` - JSON 数据:`Intent.json(value)` - 文件路径:`Intent.file(value)` 或 `Intent.fileURL(value)` 示例: ```ts import { Script, Intent } from "scripting" Script.exit(Intent.text("处理完成")) ``` *** ## 四、展示交互式 UI 你可以使用 `Navigation.present()` 呈现一个自定义界面,展示输入信息或收集用户反馈。在 UI 交互结束后调用 `Script.exit()` 返回结果。 示例: ```ts import { Intent, Script, Navigation, VStack, Text } from "scripting" function MyIntentView() { return ( {Intent.textsParameter?.[0]} ) } async function run() { await Navigation.present() Script.exit() } run() ``` *** ## 五、在分享扩展中使用 当脚本项目启用了对应类型的输入支持,Scripting 会自动集成到系统分享菜单: 1. 用户选中内容(如 Safari 中的文字或图片),点击分享按钮。 2. 分享列表中选择 **Scripting**。 3. 显示支持当前输入类型的脚本列表,供用户执行。 *** ## 六、与 Shortcuts 集成 你可以在 Shortcuts 应用中调用 Scripting 脚本: - **运行脚本(Run Script)**:后台执行,无 UI。 - **在 App 中运行脚本(Run Script in App)**:前台执行,支持 UI 展示。 操作步骤: 1. 在 Shortcuts 中添加 “Run Script” 或 “Run Script in App” 操作。 2. 选择目标脚本。 3. 配置参数,执行脚本。 *** ## 七、Intent API 参考 ### `Intent` 类属性 | 属性 | 类型 | 说明 | | ------------------- | ------------------- | ------------------------------------- | | `shortcutParameter` | `ShortcutParameter` | Shortcuts 传入的参数对象,包含 `type` 和 `value` | | `textsParameter` | `string[]` | 文本输入数组 | | `urlsParameter` | `string[]` | URL 字符串数组 | | `imagesParameter` | `UIImage[]` | 图片数组(路径或图片对象) | | `fileURLsParameter` | `string[]` | 文件路径数组(本地 URL) | ### `Intent` 类方法 | 方法 | 返回类型 | 示例 | | ------------------------------ | --------------------------- | ------------------------------------- | | `Intent.text(value)` | `IntentTextValue` | `Intent.text("内容")` | | `Intent.attributedText(value)` | `IntentAttributedTextValue` | `Intent.attributedText("富文本")` | | `Intent.url(value)` | `IntentURLValue` | `Intent.url("https://example.com")` | | `Intent.json(value)` | `IntentJsonValue` | `Intent.json({ key: "value" })` | | `Intent.file(filePath)` | `IntentFileValue` | `Intent.file("/path/to/file.txt")` | | `Intent.fileURL(filePath)` | `IntentFileURLValue` | `Intent.fileURL("/path/to/file.pdf")` | | `Intent.image(UIImage)` | `IntentImageValue` | `Intent.image(uiImage)` | *** ## 八、最佳实践与注意事项 - 所有脚本应显式调用 `Script.exit()` 以确保内存安全。 - 推荐在 UI 脚本中使用 `await Navigation.present()` 之后再调用 `Script.exit()`。 - 对于大文件或图像,建议使用 “Run Script in App” 模式,以避免系统内存限制导致的崩溃。 - 如果脚本需要共享数据,可通过 URL Scheme 或 `queryParameters` 实现。 --- url: /doc_v2/TestFlight/zh/guide/Intent/SnippetIntent.md --- # SnippetIntent SnippetIntent 是一种特殊类型的 AppIntent,可在 Shortcuts 中生成原生的 Snippet UI 卡片。它适用于: - 多步骤表单式交互 - 从 Shortcuts 中获取用户输入 - 键值选择、确认、展示结果等轻量级交互 - 在 Shortcuts 工作流中内嵌 UI 组件 SnippetIntent 特点如下: 1. 在 Scripting 中必须通过 `AppIntentManager.register` 注册 2. `protocol` 必须为 `AppIntentProtocol.SnippetIntent` 3. `perform()` 必须返回一个 `VirtualNode`(TSX UI) 4. 在脚本中必须以 `Intent.snippetIntent()` 封装后返回 5. Shortcuts 必须使用「Show Snippet Intent」动作才能显示 Snippet UI *** \##系统要求 **SnippetIntent 只能在 iOS 26 及以上系统运行。** 在 iOS 26 以下环境: - 无法调用 `Intent.snippetIntent` - 无法使用 `Intent.requestConfirmation` - Shortcuts 中不存在「Show Snippet Intent」动作 - SnippetIntent 类型的 AppIntent 不会被 Shortcuts 正常识别 *** \##注册 SnippetIntent(app\_intents.tsx) 在 `app_intents.tsx` 中声明 SnippetIntent: ```tsx export const PickColorIntent = AppIntentManager.register({ name: "PickColorIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async () => { return ; }, }); ``` 再例如: ```tsx export const ShowResultIntent = AppIntentManager.register({ name: "ShowResultIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async ({ content }: { content: string }) => { return ; }, }); ``` 要求: - `protocol` 必须为 `AppIntentProtocol.SnippetIntent` - `perform()` 必须返回 `VirtualNode` - 与普通 AppIntent 区别在于返回的是 UI,而非数据 *** \##SnippetIntent 返回值封装:Intent.snippetIntent SnippetIntent 不能直接作为 JS 返回值,必须通过 `Intent.snippetIntent()` 包装成 `IntentSnippetIntentValue`。 ```tsx const snippetValue = Intent.snippetIntent({ value: Intent.text("Some value returning for Shortcuts"), snippetIntent: ShowResultIntent({ content: "Example Text", }), }); Script.exit(snippetValue); ``` ### 类型定义 ```ts type SnippetIntentValue = { value?: | IntentAttributedTextValue | IntentFileURLValue | IntentJsonValue | IntentTextValue | IntentURLValue | IntentFileValue | null; snippetIntent: AppIntent; }; declare class IntentSnippetIntentValue extends IntentValue<"SnippetIntent", SnippetIntentValue> { value: SnippetIntentValue; type: "SnippetIntent"; } ``` 封装的返回值可被 Shortcuts 的「Show Snippet Intent」动作识别并展示 UI。 *** \##Snippet 确认界面:Intent.requestConfirmation SnippetIntent 支持在执行逻辑中先请求用户确认某个操作。此能力同样基于 iOS 26。 ```ts Intent.requestConfirmation( actionName: ConfirmationActionName, intent: AppIntent, options?: { dialog?: Dialog; showDialogAsPrompt?: boolean; } ): Promise ``` ### ConfirmationActionName 这些名称会影响 Shortcuts UI 中呈现的文案,例如 “Set …”、“Add …”、“Toggle …” 等。 示例值: ``` "add" | "addData" | "book" | "buy" | "call" | "checkIn" | "continue" | "create" | "do" | "download" | "filter" | "find" | "get" | "go" | "log" | "open" | "order" | "pay" | "play" | "playSound" | "post" | "request" | "run" | "search" | "send" | "set" | "share" | "start" | "startNavigation" | "toggle" | "turnOff" | "turnOn" | "view" ``` ### 示例 ```tsx await Intent.requestConfirmation("set", PickColorIntent()); ``` 效果: - Shortcuts 弹出 PickColorIntent 对应的 Snippet UI - 用户点击确认后 Promise resolve - 用户取消时脚本执行终止 *** \##Shortcuts 的「Show Snippet Intent」动作(iOS 26+) Shortcuts 在 iOS 26 新增动作: **Show Snippet Intent** 用于展示 SnippetIntent 返回的 Snippet UI。 ### 与其他动作对比 | Shortcuts 动作 | 显示界面 | 支持 SnippetIntent | 场景 | | ---------------------------- | -------------------- | ---------------- | ---------------- | | Run Script | 无 UI | 否 | 纯数据处理 | | Run Script in App | Scripting App UI(前台) | 否 | 大型 UI、文件选择等 | | Show Snippet Intent(iOS 26+) | Snippet 卡片 UI | 是 | SnippetIntent 场景 | 使用方式: 1. 在 Shortcuts 中添加「Show Snippet Intent」 2. 选择脚本项目(需包含 intent.tsx) 3. 脚本返回 `Intent.snippetIntent(...)` 4. Shortcuts 显示 Snippet UI *** \##IntentMemoryStorage — 跨 AppIntent 状态共享 ## 1. 为什么需要 IntentMemoryStorage 由于系统行为,每次 Intent 执行后: - AppIntent 的 `perform()` 执行完毕后立即销毁上下文 - `intent.tsx` 执行完并调用 `Script.exit()` 后脚本上下文也会完全释放 因此无法依赖 JS 变量在多个 Intent 之间保持状态。 例如: - PickColorIntent(选择颜色) - SetColorIntent(设置颜色) - ShowResultIntent(展示颜色结果) 在这些 Intent 之间共享状态必须依赖持久化存储。 ## 2. IntentMemoryStorage 提供轻量级、跨 Intent 的共享存储 API 定义: ```ts namespace IntentMemoryStorage { function get(key: string): T | null; function set(key: string, value: any): void; function remove(key: string): void; function contains(key: string): boolean; function clear(): void; function keys(): string[]; } ``` 用途: - 存储小量状态,例如当前颜色、当前步骤、临时选项 - 在多个 AppIntent 之间共享数据 - 生命周期跨 Intent 调用,但随脚本生命周期管理 ### 示例:存储用户颜色选择 ```ts IntentMemoryStorage.set("color", "systemBlue"); const color = IntentMemoryStorage.get("color"); ``` ### 建议 - 不要存储大型数据(如大图像、长文本) - 大型数据请使用: - `Storage`(持久键值存储) - `FileManager` 写入 appGroupDocumentsDirectory IntentMemoryStorage 适合作为临时状态共享,不适合当作数据库使用。 *** \##完整示例(iOS 26+) ## app\_intents.tsx ```tsx export const SetColorIntent = AppIntentManager.register({ name: "SetColorIntent", protocol: AppIntentProtocol.AppIntent, perform: async (color: Color) => { IntentMemoryStorage.set("color", color); }, }); export const PickColorIntent = AppIntentManager.register({ name: "PickColorIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async () => { return ; }, }); export const ShowResultIntent = AppIntentManager.register({ name: "ShowResultIntent", protocol: AppIntentProtocol.SnippetIntent, perform: async ({ content }: { content: string }) => { const color = IntentMemoryStorage.get("color") ?? "systemBlue"; return ; }, }); ``` ## intent.tsx ```tsx async function runIntent() { // 1. 通过 Snippet 请求用户确认颜色 await Intent.requestConfirmation("set", PickColorIntent()); // 2. 从 Shortcuts 输入中读取文本 const textContent = Intent.shortcutParameter?.type === "text" ? Intent.shortcutParameter.value : "No text parameter from Shortcuts"; // 3. 创建 SnippetIntent 返回结果 const snippetIntentValue = Intent.snippetIntent({ snippetIntent: ShowResultIntent({ content: textContent }), }); Script.exit(snippetIntentValue); } runIntent(); ``` --- url: /doc_v2/TestFlight/zh/guide/Interactive Widget and LiveActivity.md --- # 可互动的小组件和灵动岛 **Scripting** 应用支持在 **小组件** 和 **LiveActivity(灵动岛)** 中添加互动的功能,使您可以通过 `Button` 和 `Toggle` 组件创建动态、交互式的 UI。这些控件可以执行 **AppIntent** 来触发操作,从而增强小组件和 LiveActivity 的功能。 *** ## 1. AppIntent 简介 ### 什么是 AppIntent? **AppIntent** 定义了一个由控件(如 `Button` 或 `Toggle`)触发的特定操作,用于小组件或 LiveActivity UI。AppIntent 将 UI 组件与可执行逻辑连接起来,实现无缝交互。 ### 支持的协议 AppIntent 可以实现以下协议: - **`AppIntent`**:通用意图,用于触发自定义操作。 - **`AudioPlaybackIntent`**:处理音频播放(如播放、暂停或切换音频状态)。 - **`AudioRecordingIntent`**:管理音频录制状态(需要 iOS 18+,并且在录制期间保持 LiveActivity 活跃)。 - **`LiveActivityIntent`**:修改或管理 LiveActivity 状态。 *** ## 2. 注册 AppIntent 在使用 **AppIntent** 之前,必须通过 `AppIntentManager.register` 方法在 `app_intents.tsx` 文件中注册。 ### 示例:注册 AppIntent ```typescript // app_intents.tsx import { AppIntentManager, AppIntentProtocol } from "scripting" // 注册不带参数的 AppIntent const IntentWithoutParams = AppIntentManager.register({ name: "IntentWithoutParams", protocol: AppIntentProtocol.AppIntent, perform: async (params: undefined) => { // 执行自定义操作 console.log("Intent 被触发") // 可选:刷新小组件 Widget.reloadAll() } }) // 注册带参数的 AppIntent const ToggleIntentWithParams = AppIntentManager.register({ name: "ToggleIntentWithParams", protocol: AppIntentProtocol.AudioPlaybackIntent, perform: async (audioName: string) => { // 根据参数执行操作 console.log(`切换音频播放状态:${audioName}`) Widget.reloadAll() } }) ``` *** ## 3. 在小组件或 LiveActivity UI 中使用 AppIntent 注册完 AppIntent 后,可以在 `widget.tsx` 或 LiveActivity UI 文件中的 `Button` 和 `Toggle` 等交互组件中链接这些 AppIntent。 ### 示例:在小组件中使用 AppIntent ```typescript // widget.tsx import { VStack, Button, Toggle } from "scripting" import { IntentWithoutParams, ToggleIntentWithParams } from "./app_intents" import { model } from "./model" function WidgetView() { return ( ] }} trailingSwipeActions={{ actions: [ , ] }} /> )} } async function run() { await Navigation.present({ element: }) Script.exit() } run() ``` --- url: /doc_v2/TestFlight/zh/guide/View Modifiers/Symbol Style.md --- # 符号样式 这些修饰符用于配置 SF Symbols(系统符号图标)的显示样式和动画效果,常用于 `` 组件。 *** ### `symbolRenderingMode` 设置符号图像的 **渲染模式**。 #### 类型 ```ts symbolRenderingMode?: SymbolRenderingMode ``` #### 可选值(SymbolRenderingMode): - `"monochrome"`:单色模式,使用当前前景色绘制 - `"hierarchical"`:层次渲染,根据不同图层设置不透明度(适合语义着色) - `"multicolor"`:使用符号内置颜色 - `"palette"`:分层渲染,可自定义每一层的颜色样式(需搭配 `foregroundStyle`) #### 示例 ```tsx ``` *** ### `foregroundStyle` 设置符号或前景元素的颜色样式。 #### 类型 ```ts foregroundStyle?: | ShapeStyle | DynamicShapeStyle | { primary: ShapeStyle | DynamicShapeStyle secondary: ShapeStyle | DynamicShapeStyle tertiary?: ShapeStyle | DynamicShapeStyle } ``` #### 说明: - 在 `"monochrome"` 模式下使用单个颜色或渐变; - 在 `"palette"` 模式下使用 `{ primary, secondary, tertiary }` 对象指定多层样式; - `tertiary` 可选,仅在符号有三层图层时有效。 *** ### `symbolVariant` 为符号添加特定的 **视觉变体**。 #### 类型 ```ts symbolVariant?: SymbolVariants ``` #### 可选值(SymbolVariants): - `"none"`:无变体,原始符号样式 - `"fill"`:填充样式 - `"circle"`:包裹在圆形轮廓中 - `"square"`:包裹在方形轮廓中 - `"rectangle"`:包裹在矩形轮廓中 - `"slash"`:斜杠样式,表示禁止/关闭等状态 #### 示例 ```tsx ``` *** ### `symbolEffect` 为符号添加 **动画效果**,支持静态应用或绑定数值以触发动画。 #### 类型 ```ts symbolEffect?: SymbolEffect ``` #### 使用方式: ##### 1. 静态符号效果(SymbolEffect 简写字符串) ```tsx ``` ##### 2. 动态绑定符号效果(每次值变化时触发动画) ```tsx ``` 每次 `isLiked` 状态变化时,图标会执行 bounce 动画。 *** ### 可用 Symbol 动效分类(DiscreteSymbolEffect) | 类别 | 动效关键字 | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 弹跳 Bounce | `bounce`, `bounceByLayer`, `bounceDown`, `bounceUp`, `bounceWholeSymbol` | | 呼吸 Breathe | `breathe`, `breatheByLayer`, `breathePlain`, `breathePulse`, `breatheWholeSymbol` | | 脉冲 Pulse | `pulse`, `pulseByLayer`, `pulseWholeSymbol` | | 旋转 Rotate | `rotate`, `rotateByLayer`, `rotateClockwise`, `rotateCounterClockwise`, `rotateWholeSymbol` | | 颜色变化 VariableColor | `variableColor`, `variableColorIterative`, `variableColorDimInactiveLayers`, `variableColorHideInactiveLayers`, `variableColorCumulative` | | 摇晃 Wiggle | `wiggle`, `wiggleLeft`, `wiggleRight`, `wiggleUp`, `wiggleDown`, `wiggleForward`, `wiggleBackward`, `wiggleByLayer`, `wiggleWholeSymbol`, `wiggleClockwise`, `wiggleCounterClockwise` | *** ### 综合示例 ```tsx ``` 上述示例中: - 使用了分层渲染(hierarchical); - 添加了圆形变体(circle); - 设置了 `indigo` 颜色; - 每当 `isNotified` 变化时,符号执行 `breathePulse` 动画。 *** ## 修饰符汇总表 | 修饰符 | 说明 | | --------------------- | ------------------------- | | `symbolRenderingMode` | 设置符号图标的渲染模式(单色、多色、层次、调色板) | | `foregroundStyle` | 设置符号的颜色风格,可支持多图层配色 | | `symbolVariant` | 添加符号样式变体,如填充、圆形、斜杠等 | | `symbolEffect` | 添加符号动画,可静态或绑定值驱动 | --- url: /doc_v2/TestFlight/zh/guide/View Modifiers/Text Field.md --- # 文本输入框 这些修饰符可用于自定义 `TextField` 组件的行为和外观,包括键盘类型、自动更正、自动大写、提交操作等。 *** ## `onSubmit` 为文本字段添加提交时触发的操作。 ### 类型 ```ts onSubmit?: (() => void) | { triggers: SubmitTriggers action: () => void } ``` ### 行为说明 - 若直接提供函数形式: ```tsx console.log('提交了')} /> ``` 等价于: ```tsx console.log('提交了') }} /> ``` - 也可以明确指定触发提交操作的方式: ```tsx console.log('搜索提交') }} /> ``` ### `SubmitTriggers` 可选值: - `"text"`:由文本输入控件(如 `TextField`、`SecureField`)触发。 - `"search"`:由搜索输入框(使用 `searchable` 修饰符)触发。 *** ## `keyboardType` 设置聚焦输入时显示的键盘类型。 ### 类型 ```ts keyboardType?: KeyboardType ``` ### 可选值: - `'default'` - `'numberPad'` - `'phonePad'` - `'namePhonePad'` - `'URL'` - `'decimalPad'` - `'asciiCapable'` - `'asciiCapableNumberPad'` - `'emailAddress'` - `'numbersAndPunctuation'` - `'twitter'` - `'webSearch'` ### 示例 ```tsx ``` *** ## `autocorrectionDisabled` 控制是否启用系统的自动更正功能。 ### 类型 ```ts autocorrectionDisabled?: boolean ``` ### 默认值 - `true` — 默认禁用自动更正。 ### 示例 ```tsx ``` *** ## `textInputAutocapitalization` 设置文本输入时的自动大写行为。 ### 类型 ```ts textInputAutocapitalization?: TextInputAutocapitalization ``` ### 可选值 - `"never"` – 不自动大写。 - `"characters"` – 每个字母都大写。 - `"sentences"` – 每个句子的首字母大写。 - `"words"` – 每个单词的首字母大写。 ### 示例 ```tsx ``` *** ## `submitScope` 阻止当前视图触发的提交操作向上传递到父级视图的 `onSubmit` 处理器。 ### 类型 ```ts submitScope?: boolean ``` ### 默认值 - `false` — 默认允许事件向上传递。 ### 示例 ```tsx ``` 启用此项后,该字段的提交事件将不会触发父视图中的提交处理逻辑。 ## `submitLabel` 设置提交按钮的文本。 ### 类型 ```ts submitLabel?: "continue" | "return" | "send" | "go" | "search" | "join" | "done" | "next" | "route" ``` ### 示例 ```tsx ``` --- url: /doc_v2/TestFlight/zh/guide/View Modifiers/Text View Modifiers.md --- # 文本修饰符 以下属性可用于为基于文本的视图(如 `Text` 或 `Label`)设置样式和格式,其功能与 SwiftUI 的内建修饰符类似。通过自定义这些属性,您可以控制文本的字体、字重、设计、间距及其他排版特性。 ## 概览 这些属性通常作为属性传递给与文本相关的组件,如 `Text` 或 `Label`。例如,您可以设置字体大小、启用加粗格式,或添加自定义颜色的下划线——无需手动调用多个修饰符。 ```tsx Stylish Text Here ``` 在上面的示例中,文本使用了自定义字体、半粗体、斜体风格、红色下划线,限制为两行,并居中对齐。 *** ## 字体配置 ### `font` 定义文本的字体和大小。 - **数字**:提供一个数字(例如 `14`)时,将应用该大小的系统字体。 - **预设字体名称**(`Font` 类型):使用内建的文本样式之一(如 `"largeTitle"`、`"title"`、`"headline"`、`"subheadline"`、`"body"`、`"callout"`、`"footnote"`、`"caption"`)。系统会根据样式决定大小和字重。 - **包含名称和大小的对象**:指定 `name` 和 `size` 来应用自定义字体。 ```tsx 系统字体,大小为 20 系统标题字体 自定义字体 ``` *** ### `fontWeight` 设置字体的粗细程度。可选值包括从 `"ultraLight"` 到 `"black"`。 ```tsx 加粗文本 ``` *** ### `fontWidth` 指定字体的宽度变体(如果可用)。可选值有 `"compressed"`、`"condensed"`、`"expanded"` 和 `"standard"`,也可以使用数字(如果支持)。 ```tsx 压缩宽度字体 ``` *** ### `fontDesign` 修改字体设计风格。可选值包括 `"default"`、`"monospaced"`、`"rounded"`、`"serif"`。 ```tsx 圆角字体设计 ``` *** ## 文本格式 ### `minScaleFactor` 一个介于 0 到 1 之间的数字,表示当文本超出空间限制时最多可以缩小到原始大小的多少。例如,`0.5` 表示文本可以缩小到 50%。 ```tsx 当文本超出时会稍微缩小。 ``` *** ### `bold` 如果为 `true`,应用加粗字体。 ```tsx 这是加粗文本 ``` *** ### `baselineOffset` 调整文本相对于基线的垂直位置。正值向上移动,负值向下移动。 ```tsx 文本向上偏移 ``` *** ### `kerning` 控制字符间距。正值增加间距,负值减小间距。 ```tsx 字符间距增加 ``` *** ### `italic` 如果为 `true`,应用斜体样式。 ```tsx 斜体文本 ``` *** ### `monospaced` 强制所有子文本使用等宽字体(如果可用)。 ```tsx 等宽字体文本 ``` *** ### `monospacedDigit` 使用固定宽度数字,而其他字符保持原样。适用于表格或计时器中的数字对齐。 ```tsx 数字等宽对齐 1234 ``` *** ## 文本装饰 ### `strikethrough` 应用删除线(贯穿文本)。可以提供颜色,或一个包含样式和颜色的对象。 - **仅颜色**:`strikethrough="red"` - **对象**:`strikethrough={{ pattern: 'dash', color: 'blue' }}` ```tsx 灰色删除线文本 红色点状删除线 ``` *** ### `underline` 以下划线方式装饰文本,使用方式与 `strikethrough` 类似。 - **仅颜色**:`underline="blue"` - **对象**:`underline={{ pattern: 'dashDot', color: 'green' }}` ```tsx 蓝色下划线文本 粉色点状下划线 ``` *** ## 行数、行间距与布局控制 ### `lineLimit` 指定文本最多显示的行数。可以: - 提供一个数字来设置最大行数; - 或提供一个对象 `{ min?: number; max: number; reservesSpace?: boolean }`,来指定最小和最大行数,并选择是否预留最大行数空间以避免布局跳动。 ```tsx 如果超出一行将被截断。 可显示 2 到 4 行文本,并始终预留 4 行空间,避免布局变化。 ``` *** ### `lineSpacing` 设置行间距,单位为像素。 ```tsx 设置行间距为 5 像素 ``` *** ### `multilineTextAlignment` 设置多行文本的对齐方式:`"leading"`(左对齐)、`"center"`(居中)或 `"trailing"`(右对齐)。 ```tsx 多行文本居中显示。 ``` *** ### `truncationMode` 指定文本太长时的截断方式。 #### 类型 ```ts type TruncationMode = "head" | "middle" | "tail" ``` #### 描述 定义截断的位置: - `"head"`:截断行首,保留末尾。 - `"middle"`:截断中间,保留首尾。 - `"tail"`:截断尾部,保留开头。 ```tsx 这是一段可能会被截断的很长文本。 ``` *** ### `allowsTightening?: boolean` 是否允许系统在必要时压缩字符间距以适应一行内显示。 #### 类型 `boolean` #### 默认值 `false` #### 描述 设置为 `true` 时,系统可以压缩字距以避免截断,并改善在受限空间下的布局适应性。 ```tsx 在需要时压缩的文本 ``` *** ## 总结 通过组合这些属性,您可以完全掌控文本视图的排版,而无需多个包装组件或修饰符。无论您需要加粗、斜体、带自定义字符间距和下划线的标题,还是仅限两行显示的正文文本,这些选项都能满足广泛的文本样式需求。 --- url: /doc_v2/TestFlight/zh/guide/View Modifiers/Toast.md --- # Toast提示 `toast` 修饰器用于在视图上显示一个临时提示框(Toast)。 它通常用于短暂地展示消息或反馈信息,例如“保存成功”、“操作完成”、“网络错误”等。 Toast 可以包含简单的文本消息,也可以自定义内容视图。 你可以控制其显示位置、持续时间、背景颜色、圆角、阴影等外观属性。 *** ## 类型定义 ```ts toast?: { duration?: number | null position?: "top" | "bottom" | "center" backgroundColor?: Color | null textColor?: Color | null cornerRadius?: number | null shadowRadius?: number | null } & ( | { message: string; content?: never } | { message?: never; content: VirtualNode } ) & ({ isPresented: boolean onChanged: (isPresented: boolean) => void } | { isPresented: Observable }) ``` *** ## 属性说明 ### `isPresented: boolean` 和 `onChanged(isPresented: boolean): void` **说明**: 使用`isPresented`和`onChanged`来控制Toast的显示和隐藏。 **示例**: ```tsx const [showToast, setShowToast] = useState(false) toast={{ isPresented: showToast, onChanged: setShowToast, message: "Saved successfully" }} ``` *** ### `isPresented: Observable` **说明**:使用 `isPresented` 作为 `Observable` 来控制 Toast 的显示和隐藏。 **示例**: ````tsx const showToast = useObservable(false) toast={{ isPresented: showToast, message: "Saved successfully" }} --- ### `duration?: number | null` **说明**: Toast 显示的持续时间(单位:秒)。 默认值为 `2` 秒。 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, duration: 3, message: "Action completed" }} ```` *** ### `position?: "top" | "bottom" | "center"` **说明**: 控制 Toast 在屏幕上的显示位置。 可选值: - `"top"`:顶部显示 - `"bottom"`:底部显示(默认) - `"center"`:居中显示 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, position: "top", message: "New message received" }} ``` *** ### `backgroundColor?: Color | null` **说明**: 设置 Toast 的背景颜色。可以使用任意支持的 `Color` 类型。 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, backgroundColor: "blue", message: "Upload successful" }} ``` *** ### `textColor?: Color | null` **说明**: 设置 Toast 文本的颜色。 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, textColor: "white", message: "Download failed" }} ``` *** ### `cornerRadius?: number | null` **说明**: 设置 Toast 的圆角大小。 默认值为 `16`。 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, cornerRadius: 8, message: "Item added" }} ``` *** ### `shadowRadius?: number | null` **说明**: 设置阴影的模糊半径。 默认值为 `4`。 **示例**: ```tsx toast={{ isPresented: showToast, onChanged: setShowToast, shadowRadius: 6, message: "Success" }} ``` *** ## 显示文本消息 **示例**: ```tsx function View() { const [showToast, setShowToast] = useState(false) return ( ``` ### 执行 AppIntent 的按钮 ```tsx