Repository Wiki
jason5ng32/MyIP

文档助手与测试操作入口

文档助手将 GitBook 的 IPilot 对话面板嵌入 MyIP,并提供读取当前诊断结果、重新运行四项核心测试的工具入口。本页聚焦从打开助手到命令执行及结果回传的前端控制链路。

目的与范围

本文覆盖助手加载与访问条件、面板交互、get_my_test_results / run_my_tests 两个工具、参数归一化、命令总线衔接以及错误与并发边界。适合维护助手集成、排查工具调用失败和扩展可运行测试的开发者。

测试本身的网络探测算法、诊断报告导出与分享、登录服务以及配额实现不属于本页范围;这里只描述它们与助手的接口。当前上下文未提供兄弟页面的确切地址,因此不构造未验证的 Wiki 链接。外部 GitBook 的服务端对话存储、模型执行及确认 UI 的内部实现也不在已读取源码中。

概述

助手不是独立的诊断引擎:读取操作复用 useCollectedReport() 提供的结果,运行操作通过已有应用命令触发测试。这样,同一份页面诊断数据既可用于报告,也可用于助手问答,无需再建立一套采集流程。

主要使用方式如下:

  • 询问文档或提交问题:askDocs(query) 按需加载嵌入脚本,打开面板;非空问题会作为用户消息发送。
  • 解释当前测试结果:get_my_test_results 返回当前收集到的结果,并列出缺失的报告区块。
  • 刷新核心诊断:run_my_tests 可触发 IP 查询、连通性、WebRTC 与 DNS 泄漏测试,完成后返回汇总快照。
  • 交互降级:加载或打开失败时尝试在新标签页打开文档站点。

源码注释将入口定位为官方部署的登录权益:构建时提供 VITE_DOCS_URL,导航入口按 configs.originalSite 限制,打开动作检查登录状态。需要区分:askDocs() 本身只检查文档地址和 store.isSignedIn === true,并不再次检查 originalSite。导航组件的完整显示逻辑未在本次摘录中核实。

来源:use-docs-assistant.js、use-docs-assistant.js。

架构

Loading diagram...

Sources: use-docs-assistant.js、use-docs-assistant.js、DocsAssistant.vue、app-commands.js。

三个层次各有明确职责:

  1. 展示层:DocsAssistant.vue 负责浮动按钮、触屏遮罩、滚动锁定、Escape 关闭及面板状态观察。
  2. 编排层:useDocsAssistant() 负责脚本加载、登录提示、GitBook 配置、工具注册与结果整理。
  3. 执行适配层:normalizeRunSections() 将不可信工具参数转为允许的区块列表;命令总线等待测试所有者注册并调用处理器。

打开、加载与降级

1. 统一打开入口

askDocs(query) 按以下顺序执行:

  1. 未配置文档地址时直接返回,不加载任何助手脚本。
  2. 未严格满足 store.isSignedIn === true 时调用 store.setAlert(...) 提示登录并返回。
  3. 对问题执行 (query || '').trim()。
  4. 若当前 composable 实例的 isOpening 已为真,则忽略本次打开请求。
  5. 等待脚本加载,配置助手、工具、欢迎语与建议问题。
  6. 打开面板并记录用户意图时间;若问题非空,再调用 navigateToAssistant 和 postUserMessage。
  7. 捕获加载、配置或打开阶段异常,输出警告并执行 openDocsSite();最终重置 isOpening。

query 是未声明静态类型的 JavaScript 参数,实际按可执行 trim() 的字符串使用。真值非字符串会在进入 try 之前失败,不能将此接口描述为接受任意 JSON 输入。

2. 脚本只在用户请求时加载

DOCS_URL 去除末尾斜杠后形成 ${DOCS_URL}/~gitbook/embed/script.js。加载前设置 window.gitbookSettings.siteURL,将 iframe 的站点指向文档站,而不是当前应用站点。

模块级 loadPromise 合并多个调用者的加载请求。成功时设置共享 isLoaded;脚本加载失败则删除脚本节点、清空缓存 Promise,允许下次重试。

javascript
1const loadEmbedScript = () => { 2 if (loadPromise) return loadPromise; 3 loadPromise = new Promise((resolve, reject) => { 4 // The embed script derives its siteURL from the embedding page's 5 // origin, which points the iframe at this site instead of the docs. 6 // gitbookSettings is merged last there, so it pins the docs origin. 7 window.gitbookSettings = { ...(window.gitbookSettings || {}), siteURL: `${DOCS_URL}/` }; 8 const script = document.createElement('script'); 9 script.src = EMBED_SRC; 10 script.async = true; 11 script.onload = () => { 12 isLoaded.value = true; 13 resolve(); 14 }; 15 script.onerror = () => { 16 loadPromise = null; // allow a retry on the next attempt 17 script.remove(); 18 reject(new Error('GitBook embed script failed to load')); 19 }; 20 document.head.appendChild(script); 21 }); 22 return loadPromise; 23};

Source: use-docs-assistant.js。

这里没有脚本加载超时计时器,因此“脚本长期悬而未决”不等于 onerror,不能保证自动降级。成功加载后若 window.GitBook 配置调用失败,虽然仍会降级打开文档站,但不会清空已成功的 loadPromise。

3. 每次打开重新配置

配置将名称设置为 IPilot,只启用 tabs: ['assistant'],关闭 trademark,并提供欢迎语、建议问题以及打开文档站的侧栏操作。docsQuestions() 使用 tm('nav.DocsQuestions') 和 rt() 转换本地化消息;重新打开时重新配置,因此语言切换可在下一次打开时生效。

这不代表 GitBook 的全部界面均被本地化:源码说明嵌入组件没有单独的语言选项,输入提示等剩余界面保持英文。

来源:use-docs-assistant.js、use-docs-assistant.js。

工具协议与结果数据

读取结果:get_my_test_results

该工具的输入 schema 是无必填字段的对象。注册项携带 confirmation,图标为 eye,提示文案来自 nav.DocsToolConfirm。执行时返回 output 和用于展示的 summary。

快照不是直接返回 Vue 响应式对象,而是对各区块进行 JSON 深拷贝,并添加本地化名称:

javascript
1const buildResultsSnapshot = () => { 2 const available = Object.keys(sections); 3 const sectionName = (id) => t(SECTION_TITLE_KEYS[id]); 4 return { 5 results: Object.fromEntries(available.map((id) => [id, { 6 name: sectionName(id), 7 ...JSON.parse(JSON.stringify(sections[id])), 8 }])), 9 missingSections: REPORT_SECTION_IDS.filter((id) => !available.includes(id)) 10 .map((id) => ({ id, name: sectionName(id) })), 11 }; 12};

Source: use-docs-assistant.js。

字段结构含义
results以区块 ID 为键的对象当前 collector 中存在的区块;每项添加本地化 name 并合并快照
missingSections{ id, name } 数组REPORT_SECTION_IDS 中尚未出现在 collector 的区块
summary{ icon, text }工具完成后展示的简要信息,不是诊断结论

对象键承担区块 ID 的角色;构建器没有额外保证向每个区块值注入 id 字段。由于对象展开位于 name 后,如果原快照含有同名属性,原值会覆盖新添加的名称。

源码说明 collector 采用报告白名单,排除 whois 等查询工具;本页没有重新审计 collector 内部过滤规则。助手端的快照函数自身不做 IP 脱敏,且注释明确结果可能包含 IP 与地理位置,因此两个工具都注册了用户确认提示。确认交互由外部嵌入组件实现,不能将该配置等同于服务端授权验证。

来源:use-docs-assistant.js。

运行测试:run_my_tests

此工具接受可选的 sections 数组,schema 中的枚举由 RUNNABLE_SECTION_IDS 生成。能够读取的结果范围与能够启动的测试范围不同:例如工具描述提及速度测试结果,但运行入口只支持下列四项。

javascript
1export const RUNNABLE_SECTION_COMMANDS = { 2 ipinfo: { command: 'ipinfo:refresh', payload: {} }, 3 connectivity: { command: 'connectivity:run', payload: { trigger: 'manual' } }, 4 webrtc: { command: 'webrtc:run', payload: { isRefresh: true } }, 5 dnsleak: { command: 'dnsleak:run', payload: { isRefresh: true } }, 6};

Source: docs-run-tests.js。

这些载荷刻意使用手动或刷新语义,以便重新触发已经在页面启动时运行过的测试,而不只是首次初始化。实际网络测试由命令所有者完成,不由助手自行发起探测。

参数归一化边界

normalizeRunSections(args) 并不只信任 JSON schema,而是再次做运行时处理:

输入情况requestedunknown
未传参数、缺少 sections、sections 不是数组全部四项空数组
sections 为空数组全部四项空数组
含合法区块 ID按首次出现顺序保留、去重其他元素转字符串后去重
非空数组但全部未知空数组,即不执行测试全部未知标签

这保留了“未指定就运行全部”与“明确指定但不认识就不运行”的区别,防止把拼写错误的具体请求扩大成完整诊断。

javascript
1export const normalizeRunSections = (args) => { 2 const sections = Array.isArray(args?.sections) ? args.sections : []; 3 if (!sections.length) return { requested: [...RUNNABLE_SECTION_IDS], unknown: [] }; 4 const requested = []; 5 const unknown = []; 6 for (const entry of sections) { 7 if (RUNNABLE_SECTION_IDS.includes(entry)) { 8 if (!requested.includes(entry)) requested.push(entry); 9 } else { 10 const label = String(entry); 11 if (!unknown.includes(label)) unknown.push(label); 12 } 13 } 14 return { requested, unknown }; 15};

Source: docs-run-tests.js。

核心执行流程

Loading diagram...

Sources: use-docs-assistant.js、app-commands.js。

导航与所有者就绪

测试命令的所有者只在首页挂载,因此执行器先检查 route.name,必要时 await router.push('/')。随后为每项测试调用 waitForAppCommand(),避免导航完成但组件还没注册命令时直接执行。

即使参数全部未知、requested 为空,导航语句仍会执行。路由导航位于 Promise.allSettled 之外;如果导航 Promise 拒绝,工具整体拒绝,不会得到逐项 failed 汇总。

并行与部分失败

实际编排代码如下:

javascript
1execute: async (args) => { 2 const { requested, unknown } = normalizeRunSections(args); 3 if (route.name !== 'home') await router.push('/'); 4 const settled = await Promise.allSettled(requested.map(async (id) => { 5 const { command, payload } = RUNNABLE_SECTION_COMMANDS[id]; 6 await waitForAppCommand(command, { timeoutMs: RUN_TEST_TIMEOUT }); 7 await dispatchAppCommand(command, payload, { timeoutMs: RUN_TEST_TIMEOUT }); 8 })); 9 const ran = []; 10 const failed = []; 11 settled.forEach((result, i) => { 12 if (result.status === 'fulfilled') { 13 ran.push(requested[i]); 14 } else { 15 const code = result.reason?.code; 16 const reason = code === 'timeout' || code === 'unavailable' ? code : 'error'; 17 failed.push({ id: requested[i], reason }); 18 } 19 }); 20 const output = { ...buildResultsSnapshot(), ran, failed }; 21 if (unknown.length) output.unknownSections = unknown; 22 return { 23 output, 24 summary: { icon: 'play', text: t('nav.DocsToolRunSummary') }, 25 }; 26},

Source: use-docs-assistant.js。

  • 每个区块内部依次等待注册、执行命令;不同区块之间并行。
  • Promise.allSettled 保留正常测试的完成信息,不会因为一项失败中断整组等待。
  • ran 表示对应命令链 fulfilled,而不是网络检测得到了“健康”结论。
  • failed 只保留 timeout、unavailable;其他错误一律归为 error。
  • 命令处理器的返回值未被直接放入工具输出,实际返回内容在所有命令 settled 后重新从 collector 读取。
  • 快照包含当时全部已收集区块,不仅是本次请求的区块。失败区块的旧结果没有在此处清除;判断结果新鲜度时必须结合 ran 与 failed,不能只看 results 中是否存在该键。

运行工具在读取快照之外增加以下字段:

字段结构返回规则
ran区块 ID 数组本次命令链成功完成的区块
failed{ id, reason } 数组本次命令链失败的区块
unknownSections字符串数组仅在存在未知输入时附加

面板状态、移动端与生命周期

面板状态不能只由 MyIP 的按钮决定:外部嵌入组件也可能改变面板。因此 isOpen 既会在本地主动打开/关闭时更新,也会由 DOM 观察结果修正。

可见性同步

DocsAssistant.vue 在 isLoaded 变真后启动每 300ms 一次的轮询。panelIsVisible() 查询 #gitbook-widget-window,要求元素宽高大于零且 visibility 不为 hidden。它不检查 opacity、屏幕内位置或 iframe 内部业务状态。

本地打开与关闭会调用 markIntent();随后 700ms 内 isSettling() 为真,轮询暂不覆盖状态,避免动画中的旧尺寸把按钮图标翻回去。

来源:DocsAssistant.vue、use-docs-assistant.js。

操作入口与清理

  • 浮动按钮只在 isLoaded 后出现,打开时展示关闭图标,否则展示助手图标;首次加载之前需通过其他 askDocs 调用入口进入。
  • toggleDocs() 根据共享 isOpen 选择 closeDocs() 或 askDocs('')。
  • 触屏条件使用 store.isMobile;打开时添加可点击遮罩,并设置 body.style.overflow = 'hidden'、overscrollBehavior = 'none'。
  • 桌面不锁定页面滚动;Escape 可以关闭已打开面板。
  • 退出登录时若面板打开则关闭,已加载的浮动按钮保留,再次打开会提示登录。
  • 卸载组件时释放滚动锁、清除轮询计时器、移除键盘监听。这里没有删除 GitBook 脚本、销毁外部会话或重置模块级加载状态。

滚动解锁将样式设为空字符串,而不是恢复之前保存的值。若扩展时加入其他模态框,应检查多个组件同时修改 body 样式的相互影响。

来源:DocsAssistant.vue、DocsAssistant.vue。

配置与固定参数

以下区分构建配置、运行时状态和源码常量;不是所有条目都是环境变量。

名称类型默认值或当前值作用
VITE_DOCS_URL构建时字符串空字符串文档站地址;空值使 askDocs() 直接返回;移除末尾斜杠
isDocsConfigured派生布尔值!!DOCS_URL表示地址是否非空,不验证 URL 是否有效
store.isSignedIn运行时状态此处不定义默认值严格等于 true 才允许打开
configs.originalSite运行时状态此处不定义默认值源码注释声明用于官方部署导航入口限制,非 askDocs 内检查
ASSISTANT_NAME字符串常量IPilot外部面板名称;注释说明嵌入组件限制为 32 字符
RUN_TEST_TIMEOUT毫秒数常量60000分别用于等待注册与命令执行
SETTLE_MS毫秒数常量700本地开关后的动画保护窗口
SYNC_INTERVAL毫秒数常量300加载后面板可见性轮询周期
PANEL_SELECTOR字符串常量#gitbook-widget-window与 GitBook 外部 DOM 结构耦合的选择器
DOCK_INSETCSS 字符串常量max(18px, calc((100vw - 1600px) / 2 + 18px))以 1600px 内容区和 18px 间距定位左侧入口

来源:use-docs-assistant.js、DocsAssistant.vue。

API 参考

这些是前端 JavaScript 函数与工具注册协议,不是 HTTP 路由。源码没有 TypeScript 类型声明,下表以实际参数与返回行为说明契约。

接口参数与返回错误或注意事项
useDocsAssistant()无参数;返回 askDocs、closeDocs、setOpen、isOpen、isLoaded、isOpening、docsQuestions依赖 Vue、i18n、router 和 store 上下文
askDocs(query)异步;query 按字符串处理;无业务返回数据配置缺失或未登录直接返回;加载/打开阶段错误降级;非字符串问题可能在 try 前拒绝
closeDocs()无参数;记录关闭意图并令 isOpen 为假仅当 window.GitBook 是函数时调用 close;调用异常未单独捕获
setOpen(value)直接写入共享 isOpen未做类型校验,设计上供面板观察器传入布尔值
docsQuestions()返回本地化建议问题数组依赖 nav.DocsQuestions 的消息结构
isSettling()返回距最后打开/关闭意图是否少于 700ms模块级共享时钟
normalizeRunSections(args)返回 { requested, unknown }对 args?.sections 做数组判定、白名单筛选与去重
waitForAppCommand(name, { timeoutMs } = {})返回所有者注册后 resolve 的 Promise超时拒绝,error.code = 'timeout'
dispatchAppCommand(name, payload = {}, { timeoutMs } = {})返回包装处理器结果的 Promise未注册为 unavailable;超时为 timeout;处理器异常透传

来源:use-docs-assistant.js、docs-run-tests.js、app-commands.js。

失败模式、并发与运行注意事项

超时不是取消

命令总线通过 Promise.race([result, timeout]) 返回超时,并在最终清除计时器,没有向处理器发送中断信号。因此工具返回 timeout 后,原测试仍可能继续并更新页面结果。

等待注册和执行各有独立的 60 秒限额,不是整个工具共用 60 秒:若注册等待接近限额后才成功,随后执行还可再等待约 60 秒。导航耗时不包含在这两个计时器中。工具描述中的“约 10–60 秒”不能视为严格的整体超时保证。

来源:app-commands.js、use-docs-assistant.js。

并发边界

  • 共享加载,局部打开锁:loadPromise、isOpen、isLoaded 为模块级;isOpening 在每次 useDocsAssistant() 中创建。因此不同调用实例可以共享脚本加载,却仍可能先后配置、打开面板并发送消息。
  • 单次请求去重,不跨请求去重:归一化只去除当前 sections 中的重复项;工具执行器没有全局运行锁。
  • 命令所有者唯一,但执行不串行化:总线为一个名称保存一个处理器,并不限制同一处理器被同时调用。处理器自身是否防重入,在本次读取范围内未核实。
  • 注册替换保护:重复注册会警告并覆盖旧处理器;旧注销函数只在注册表仍指向自身时才删除,避免卸载旧组件误删新所有者。
  • 就绪检查与执行不是事务:等待成功之后所有者仍可能被卸载,此时派发会返回 unavailable。

来源:use-docs-assistant.js、use-docs-assistant.js、app-commands.js。

权限与错误信息

登录校验发生在 askDocs(),两个工具的 execute 未再次读取登录状态。退出登录会关闭 UI,但没有显式取消进行中的命令,也没有在此处注销工具。不能仅凭面板关闭推断正在执行的诊断已经停止。

命令总线约定可返回 auth、quota、input 等错误码,但助手聚合时将它们统一折叠为 error。排查时应进一步检查处理器拒绝原因,而不能只依赖助手输出区分登录、配额与输入错误。

来源:DocsAssistant.vue、app-commands.js、use-docs-assistant.js。

性能与排障顺序

懒加载避免在未使用助手时加载第三方脚本;加载完成后,即使面板关闭,300ms 轮询仍持续到组件卸载。快照使用 JSON 序列化深拷贝,其开销随已收集数据量增加;这里没有缓存,也没有对单个区块的序列化异常做隔离。

建议按以下顺序排障:文档地址是否配置 → 登录状态是否严格为真 → 嵌入脚本是否加载 → window.GitBook 配置调用是否成功 → 首页是否进入 → 命令是否注册 → 命令是否完成 → collector 是否出现预期区块。面板图标异常则单独检查 DOM 选择器、可见性判定和 700ms 动画窗口。

扩展与验证

新增可运行测试

当前实现的明确扩展点是 RUNNABLE_SECTION_COMMANDS。增加条目会自动进入由 Object.keys() 派生的枚举和默认“全部运行”集合,但还需要配套完成:

  1. 注册对应命令处理器,并保证其 Promise 在诊断工作完成后才结束。
  2. 确认处理器所在页面与当前“先导航首页”的假设一致。
  3. 将结果接入 collector,并保证报告 schema 与 SECTION_TITLE_KEYS 有对应定义。
  4. 更新工具描述和输入说明:它们目前明确写死“四项核心测试”。
  5. 明确超时后继续执行、重复调用以及敏感信息确认的行为。

这些是从当前依赖关系推导出的维护检查项,不代表新增映射即可自动完成整个集成。

测试证据与建议

检索确认存在 docs-run-tests.test.js,文件头说明针对区块到命令总线映射和工具参数防御性归一化。本次受源码读取预算限制,未读取其断言,也未运行测试,因此不宣称具体用例已通过。

建议验证的关键边界包括:缺省/空/错误类型参数运行全部、非空未知集合不运行、重复项去重、部分失败保留成功信息、导航失败、注册等待超时、执行超时后底层仍继续、多个入口同时打开,以及移动端关闭/卸载后的滚动恢复。

相关链接

Sources

(4 files)
frontend/components/widgets
frontend/composables