> For the complete documentation index, see [llms.txt](https://scx.gitbook.io/sc/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://scx.gitbook.io/sc/scp-docs/liao-jie-geng-duo.md).

# 了解更多

## 关于拓扑视图 <a href="#about-topology" id="about-topology"></a>

### 线路的组成

欲了解机场的质量，一个方法便是去查询这个机场的服务器的运营商与线路，这两者将直接关乎一个机场节点的延迟和线路质量。

一个节点线路的通常形式如下，所展示的为简化后的形式。

* **直连节点**：你的设备 → 国内运营商网络 → 国际出口 → 国际线路 → 节点服务器（落地） → 目标网站
* **中转节点**：你的设备 → 国内运营商网络 → 国内中转服务器 → 专线（IEPL/IPLC）→ 节点服务器 → 目标网站

我们从这些线路中简化为两个概念：

* **入口**：接受客户端发出流量的服务器，直连节点中的入口为节点服务器，中转节点中的入口为国内中转服务器。优质的入口通常设置在国内，并且为不同运营商的用户配置了对应运营商的线路，这样可以大大降低访问延迟。
* **出口**：流量经过转发后，最终与目标网站建立连接的服务器。直连节点的服务器既做入口又做出口，中转服务器中的出口负责接受专线另一头入口发来的流量，将其转发给目标网站。优质出口的 IP 类型或者运营商通常较不错，这样可以大大降低用户访问网站时因为 IP 质量被拦截或封禁的可能性。

我们了解了一个节点的入口和出口，基本上就能了解这个节点的线路概况。而拓扑视图就是专为了解一个机场的入口和出口情况所设计的。

***

### 查看拓扑视图

下面将使用一个拓扑视图的示例带您了解拓扑视图。

<figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2FiSFMouoZ8pqn4iTXfc1w%2FDankecloud%E6%8B%93%E6%89%91%E5%88%86%E6%9E%90.jpg?alt=media&amp;token=62b4a299-8429-4c7e-b60c-611654273b60" alt=""><figcaption></figcaption></figure>

拓扑视图主要有两个部分：**入口面板**和**出口面板**。

#### **入口面板**

入口面板展示的是节点的入口信息。每一行代表一个入口。

在该图中，单个入口的定义指的是**一个入口服务器**，区分依据为 IP 地址。

* **地区列** 展示了入口服务器所在的地区，该项会尽量以中文显示，服务器在中国大陆时，地区会显示详细的地级市+运营商，不在时会显示为中文国家名称或者国家缩写。
* **自治编号列** 展示了入口服务器 IP 对应网络的自治编号，在直连等某些场景下会有用。
* **组织列** 展示了入口服务器 IP 对应的运营商。
* **栈列** 展示了该入口服务器支持接入的 IP 类型（IPv4/IPv6/Both），优质的入口服务器通常会提供 IPv6 支持。
* **出口数量** 展示了多少节点将其拿来做入口，通常情况下，中转线路机场的多个节点会共用一个入口以节省成本，这通常不影响节点质量。

#### **出口面板**

入口面板展示的是节点的流量出口/落地信息。

* **入口列** 标识了对应行的节点的流量入口是哪个，其序号与入口面板的入口服务器序号对应。
* **地区列** 标识了节点落地的国家/地区，由对应国家/地区的英文简写表示。
* **自治编号列** 展示了流量出口/落地服务器 IP 对应网络的自治编号。
* **组织列** 展示了流量出口/落地服务器 IP 对应的运营商。
* **栈列** 展示了流量出口/落地服务器支持接入的 IP 类型（IPv4/IPv6/Both），优质的落地服务器通常会提供 IPv6 支持。
* **簇列** 当某些节点共享同一个落地/出口，则这些节点共享一个簇。当多个节点共享一个簇，则大多情况下代表这些节点是**复用**的，即机场将一个节点复制产生多个配置来伪造节点数量。在某些情况下机场会为某些节点提供不同运营商/协议的接入方式，则这些节点可能也会共享一个簇，这是正常现象。

***

## 传入测速配置的最佳实践

该节将向您讲述在 Miaoko+ 测试新协议或者直接使用订阅链接不起效时的最佳做法。

### 新协议测试

针对部分新协议（例如 SS2022, Vless Reality, Mieru, Anytls 等）进行的测试，我们的建议是优先使用 **Clash 配置进行测试**。

使用 Clash 配置进行测试有两大原因：

1. 我们的后端就在使用 Mihomo 内核的 Clash，直接使用 Clash 配置能获得更好的兼容性。
2. 新协议的节点链接和订阅转换支持良莠不齐。\
   在原来运营 Miaoko+ 时，我们收到了很多关于新协议的客诉，经过研究发现市面上许多**代理软件导出的节点链接格式都大相径庭**，许多代理软件生成的新协议节点链接都是只有自己才能解析的格式，订阅转换根本无法处理。并且目前的 Subconverter 社区版本对于协议的解析和转换也存在很多问题，用市面上最常用的订阅转换 API 转换出来的**配置文件根本无法使用**，我们也在维护自己的 Subconverter 版本以尽量支持更多软件的节点链接，但使用原生 Clash 配置仍然是最好的方案。

如果您确实找不到 Clash 版本，请按照如下方案进行尝试。

1. 找一个使用新版 Subconverter 的订阅转换网站，例如 [sub.v1.mk](https://sub.v1.mk/)，生成一条订阅转换链接。
2. 找一个原生 Clash 客户端，例如 Clash Verge 或者 FlClash，在客户端内下载该链接。
3. 对转换后的配置文件进行测试，如果这个配置文件能够正常连接，则您可以在测速 Bot 中使用转换后的订阅链接进行测试。如果不能，请检查转换后的配置文件内容和您的节点链接是否一致（可参考 [Mihomo Wiki](https://wiki.metacubex.one/en/config/proxies/)），或者使用其他能够正常连接该节点的客户端（例如 NekoBox, V2RayN, ShadowRocket等），比较转换后的配置文件和正常连接该节点的客户端内的配置是否正确。

常见的转换问题包括但不限于：

* TLS 安全层的 skip-cert-verify 被忽略
* Vless 的 XTLS Vision 加密字段配置错误
* Server 和 SNI 混淆

如果您认为您无法胜任此任务，请随时联系我们的技术团队，我们敬候垂询。

<figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2FzcaiwYS4QRDS7vo68UMt%2Fimage.png?alt=media&amp;token=1631780c-da95-4ae6-965c-0db7b254749f" alt=""><figcaption><p>核对转换后的配置</p></figcaption></figure>

***

### 拉取不到订阅

在使用 Miaoko+ 的时候，您可能会遇见“无法获取订阅配置文件”的错误，而您的客户端又能正常更新，常见的原因有：

1. 我们的服务器无法访问机场的订阅服务器，亦或者是后者屏蔽了海外 IP 或者是测速机器人。
2. 某些机场的订阅需要特殊 UA 才能拉取。
3. 我们的订阅转换服务无法识别您的订阅内容。

针对问题 1 和 2，我们推荐您使用可获取订阅内容的方式下载相应订阅后，将订阅内容装到文件内发给机器人，或者是将订阅内容上传到 Github Gist，再向机器人发送 Gist 链接。步骤如下：

1. A. 如果您的浏览器能直接访问订阅链接，请直接打开。通常情况下浏览器会显示订阅链接的内容或者下载下来一个订阅文件。<br>

   <figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2Ff3T7FwifSIJd2UfCYYyG%2F%E5%B7%B2%E6%89%93%E7%A0%81%E5%9B%BE%E7%89%87.png?alt=media&amp;token=9c48ce45-9414-4bce-8244-11e03f000054" alt="" width="563"><figcaption></figcaption></figure>

   B. 如果你的浏览器打不开订阅链接，但是 Clash 系的客户端可以，请用该客户端下载订阅，接着点开订阅文件的内容。

   <div><figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2FyqbTCyik1mBuwrgG9prn%2Fimage.png?alt=media&amp;token=310f0c28-6bd8-4d4d-a254-80d13579edf5" alt=""><figcaption></figcaption></figure> <figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2FNsvHq0CnmfUVBVJ1pYPo%2F%E5%B7%B2%E6%89%93%E7%A0%81%E5%9B%BE%E7%89%87%20(1).png?alt=media&amp;token=2e1fb1a4-d35b-41da-917a-99c331aeaddc" alt="" width="563"><figcaption></figcaption></figure></div>
2. 如果订阅文件的格式为 Clash，请将订阅内容复制到一个文件中，接着将文件发送给 Waku Bot，用测速指令回复该文件即可发起测试。详情请参考 [SpeedCentre+ 文档](/sc/scp-docs/miaokoplus-buy.md#fa-qi-ce-shi) 。\
   如果订阅文件格式不为 Clash 或者您使用的是 Miaoko Bot，请进行下一步。
3. 注册或登录您的 Github 账户，然后访问 [gist.github.com](https://gist.github.com/) ，将刚才的订阅内容复制到文本框中，创建一个 Gist （Secret Gist 即可）。
4. 创建完成后，右键（电脑）或者长按（手机）页面上的 Raw 按钮复制链接，将这个链接发送给测速机器人即可进行测速。

   <figure><img src="https://3663504323-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FLAVSWReLAyQHcKIkTIuu%2Fuploads%2FJ2NPLVtlPvK8KZlyGZOC%2F%E5%B7%B2%E6%89%93%E7%A0%81%E5%9B%BE%E7%89%87%20(2).png?alt=media&amp;token=4ed497af-2760-40cb-8c36-915445fb9252" alt=""><figcaption></figcaption></figure>

当您尝试了上述方法但是无济于事，或者您无法操作时，请随时联系我们的技术团队。

***

## 测速拓展

### 反劫持测速

SpeedCentre+ 支持反劫持测速，您可以通过反劫持测速获得节点更为真实的测速结果，现介绍两种反劫持测速方式。

#### 内置反劫持

我们的后端已经内置了预设的反劫持测速链接（dl.google.com），您可以在传入的 [TaskRequestConfigs](/sc/scp-docs/api-wen-dang/models.md) 中将测速 URL 配置为 `INTL_ANTIHIJACK`  来启用内置反劫持测速。

由于内置反劫持使用 google.com 来伪装测速流量，因此在测试某些针对 Google 送中优化的节点时可能获得不准确的结果。您可以使用自定义反劫持来解决此问题。

#### 自定义反劫持

SpeedCentre+ 现支持自定义测速链接的反劫持方案，您需要准备单独的一个域名以使用自定义反劫持。

在使用自定义反劫持前，请将您的某个域名 CNAME 解析到您所使用的测速链接服务器的域名。在发起测试时，请将 [TaskRequestConfigs](/sc/scp-docs/api-wen-dang/models.md) 中将测速 URL 配置为 `CNAME域名 > 下载 URL` 。测速时，后端会向目标节点发起CNAME域名的连接请求，而不是真实地址的连接请求，从而有效避免劫持。

该方法可能并非一劳永逸，请理性使用。

***

### Telegram 下载测速

SpeedCentre+ 现支持 Telegram 下载测速。在发起测试时，请将 [TaskRequestConfigs](/sc/scp-docs/api-wen-dang/models.md) 中将测速 URL 配置为某个公开频道的大文件的分享链接形式 (例：`https://t.me/examplechannel/5`)，下载线程配置也依然可用。

请注意，在超过 10 万名成员的公开频道中，某些访问量较高的文件可能会被 Telegram 传送到 [CDN DC](https://core.telegram.org/cdn) 进行分发。如果您需要测试您的节点在热门文件/媒体分享频道的文件下载速度，请优先选择来自这些大频道的文件，相反的，如果您想测试您的节点的正常 Telegram 下载速度，请自行上传一个文件到一个公开频道，或者是选用小型公开频道**自己上传**的文件（从大型公开频道不带来源转发的文件依然有可能部署在 CDN DC 上）。

***

## 自定义脚本

SpeedCentre+ 测试支持 Miaospeed 脚本。您可以在测试中传入脚本以支持多样化的动态测试。

您可以直接使用已有的 Miaospeed 脚本进行测试，也可以自行编写脚本进行测试。

### 使用脚本

SpeedCentre+ 支持两种脚本：媒体脚本（MediaScript）和 IP 脚本（IPScript）。媒体脚本负责测试各种流媒体的解锁情况，或者是检测节点的各种服务的支持情况，并将结果以文字和背景颜色的形式返回。IP 脚本负责获取节点的落地 IP，并且对 IP 进行 GEOIP 查询，以供拓扑测试使用。

若要调用媒体脚本，请在 [TaskRequestConfigs](/sc/scp-docs/api-wen-dang/models.md) 的 `Scripts` 字段放入一些 `type: media` 的脚本，然后在 `TEST_SCRIPT` 矩阵的 [TaskRequestMatrixEntry](/sc/scp-docs/api-wen-dang/models.md) 中的 `Param` 填入对应的脚本 ID。

如果没有自己的测试脚本，您可以调用 SpeedCentre+ 的内置脚本，调用方式为在 `Param` 填入 `INTERNAL::ScriptID` 。欲获取内置脚本列表，请参考 [Copilot API](/sc/scp-docs/api-wen-dang/jiao-ben.md#get-api-v1-scripts)。

### 编写脚本

Miaospeed 脚本为 JavaScript 脚本，使用 goja 运行，该库为 ECMAScript 5.1 的部分实现。

运行时提供了数个工具函数和变量，以便于您在脚本中调用。

{% hint style="warning" %}
我们**不推荐也不允许在脚本中使用异步特性**，包括但不限于：

* `async / await`
* `Promise`
* `setTimeout` / `setInterval`
* `.then()` / `.catch()` 链式调用

脚本引擎**不保证**存在可驱动异步调度的事件循环。即使语法层面不报错，Promise 的回调也可能永远不会被执行，导致脚本静默挂起或结果丢失。

所有的内置函数都为同步方法，无需也不应使用异步写法。
{% endhint %}

***

#### **编写不同类型的脚本**

**Media Script**

**入口函数**：`handler()`，无参数。

**返回格式**：返回一个对象，支持以下字段：

| 字段           | 类型       | 说明                        |
| ------------ | -------- | ------------------------- |
| `text`       | `string` | 展示的结果文本，如 `"解锁"` / `"失败"` |
| `color`      | `string` | 文字颜色（不常用，请使用 background）  |
| `background` | `string` | 背景颜色，RGB格式，每个颜色值由逗号分隔     |

所有字段均为可选，引擎只读取存在的字段。

常用的 Background 如下，我们建议您使用这些预设颜色以保证统一的视觉风格：

```javascript
const C_NA = '142,140,142'; // 错误
const C_UNL = '186,230,126'; // 解锁
const C_FAIL = '239,107,115'; // 失败
const C_UNK = '92,207,230'; // 未知
const C_CN = '250,213,149'; // 送中/部分解锁等
```

**示例**

```javascript
const C_NA = '142,140,142';
const C_UNL = '186,230,126';
const C_FAIL = '239,107,115';
const C_UNK = '92,207,230';
const C_CN = '250,213,149';

function handler() {
  const resp = fetch("https://example-streaming.com/check", {
    headers: {
      'user-agent':
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
    },
    noRedir: false,
    retry: 3,
    timeout: 5000,
  });

  if (!resp) {
    return {
      text: 'N/A',
      background: C_NA,
    };
  } else if (resp.statusCode == 200) {
    return {
      text: `解锁`,
      background: C_UNL,
    };
  } else if (resp.statusCode == 403) {
    return {
      text: `失败`,
      background: C_FAIL,
    };
  }
}
```

***

**IP Script**

用于自定义 GEOIP 查询逻辑。

**入口一：`ip_resolve()`（可选）**

查询节点的落地 IP，返回一个 IP列表。

**返回值**：`string[]`，IP 地址列表。

```javascript
function get_ip_example() {
	const urls = ["https://ipv4.example.com/myip", "https://ipv6.example.com/myip"];
	const ipret = [];
	urls.forEach((url) => {
		const content = (get(fetch(url, {
			headers: {
				'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36',
			},
			retry: 1,
			timeout: 3000,
		}), "body") || "").trim();

		if (content) {
      ipret.push(content);
    }
	});
	return ipret;
}

const ip_resolve = get_ip_example;
```

**入口二：`handler(ip)`（必填）**

核心查询入口，接收一个 IP 地址，返回该 IP 的 GEOIP 信息。

**参数**：`ip` — `string`，待查询的 IP 地址。

**返回值**：对象，字段如下：

| 字段                 | 类型       | JSON 键名            | 说明                     |
| ------------------ | -------- | ------------------ | ---------------------- |
| `organization`     | `string` | `organization`     | 组织名称                   |
| `longitude`        | `number` | `longitude`        | 经度                     |
| `latitude`         | `number` | `latitude`         | 纬度                     |
| `timezone`         | `string` | `timezone`         | 时区，如 `"Asia/Shanghai"` |
| `isp`              | `string` | `isp`              | ISP 名称                 |
| `asn`              | `number` | `asn`              | ASN 编号                 |
| `asn_organization` | `string` | `asn_organization` | ASN 归属组织               |
| `country`          | `string` | `country`          | 国家名称                   |
| `ip`               | `string` | `ip`               | IP 地址                  |
| `continent_code`   | `string` | `continent_code`   | 洲际代码，如 `"AS"`          |
| `country_code`     | `string` | `country_code`     | 国家代码，如 `"CN"`          |

所有字段均为可选，未能获取的字段留空或填零值即可。

**示例**

```javascript
function handler_ipexample(ip) {
	const isv6 = ip.includes(":")
	let geoip_api = `https://ipv4.example.com/json/${ip}`
	if (isv6) {
		geoip_api = `https://ipv6.example.com/json/${ip}`
	}
	const content = fetch(geoip_api, {
		headers: {
			'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36",
		},
		retry: 1,
		timeout: 3000,
	});
	const ret = safeParse(get(content, "body"));
	return ret;
}

function handler(ip) {
	let result = {};
	result = handler_ipexample(ip)
	if (result && result.ip) {
		return result;
	}
	return result;
}
```

***

#### **内置工具函数**

#### `get(data, path, defaults?)`

安全地从嵌套对象中按点分隔路径读取值，任意中间节点为 `null` 或 `undefined` 时返回默认值，而不是抛出异常。

**参数**

| 参数         | 类型       | 说明                    |
| ---------- | -------- | --------------------- |
| `data`     | `object` | 要读取的源对象               |
| `path`     | `string` | 点分隔的键路径，如 `"a.b.c"`   |
| `defaults` | `any`    | 路径不存在时的返回值，默认为 `null` |

**示例**

```javascript
const obj = { a: { b: { c: 42 } } };

get(obj, "a.b.c");        // 42
get(obj, "a.x.c", -1);   // -1，中间节点不存在时安全返回默认值
get(null, "a.b");         // null
```

***

#### `safeStringify(data)` / `__json_stringify(data)`

`JSON.stringify` 的安全封装。序列化失败时返回空字符串 `''`，而不是抛出异常。

**参数**

| 参数     | 类型    | 说明     |
| ------ | ----- | ------ |
| `data` | `any` | 要序列化的值 |

**返回值**：`string`，失败时为 `''`

**示例**

```javascript
safeStringify({ code: 200 });   // '{"code":200}'
safeStringify(undefined);       // ''
```

***

#### `safeParse(data)` / `__json_parse(data)`

`JSON.parse` 的安全封装。解析失败时返回空对象 `{}`，而不是抛出异常。

**参数**

| 参数     | 类型       | 说明            |
| ------ | -------- | ------------- |
| `data` | `string` | 要解析的 JSON 字符串 |

**返回值**：`object`，失败时为 `{}`

**示例**

```javascript
safeParse('{"code":200}');   // { code: 200 }
safeParse("not json");       // {}
```

***

#### `print(...args)` / `debug(...args)`

输出调试信息，支持传入任意数量的参数。我们建议使用该方法输出信息，而不是使用 `console.log` 。

**示例**

```javascript
debug("status:", resp.statusCode);   // 输出到后端日志
print("done");
```

***

#### 内置网络函数

#### `fetch(url, params?)`

发起一个 **同步** HTTP 请求，通过代理节点或宿主网络获取 HTTP 响应。

**参数**

`url`：`string`，请求地址。

`params`（可选）：`object`，请求配置项：

| 字段        | 类型        | 默认值     | 说明                      |
| --------- | --------- | ------- | ----------------------- |
| `method`  | `string`  | `"GET"` | HTTP 方法                 |
| `body`    | `string`  | `""`    | 请求体                     |
| `headers` | `object`  | `{}`    | 请求头键值对                  |
| `cookies` | `object`  | `{}`    | Cookie 键值对              |
| `useHost` | `boolean` | `false` | 为 `true` 时绕过代理，使用宿主网络直连 |
| `noRedir` | `boolean` | `false` | 为 `true` 时禁止跟随 HTTP 重定向 |
| `retry`   | `number`  | `0`     | 失败重试次数                  |
| `timeout` | `number`  | `3000`  | 超时时间（毫秒）                |

**返回值**

请求成功时返回如下对象，失败时返回 `null`：

| 字段           | 类型       | 说明                            |
| ------------ | -------- | ----------------------------- |
| `status`     | `string` | HTTP 状态文本，如 `"200 OK"`        |
| `statusCode` | `number` | HTTP 状态码，如 `200`              |
| `body`       | `string` | 响应体文本                         |
| `headers`    | `object` | 响应头                           |
| `cookies`    | `array`  | 响应 Cookie 列表                  |
| `method`     | `string` | 实际使用的请求方法                     |
| `url`        | `string` | 实际请求的 URL                     |
| `redirects`  | `array`  | 重定向链（`noRedir` 为 `false` 时有效） |

**示例**

```javascript
// 基本 GET 请求（走代理）
const resp = fetch("https://api.example.com/data");
if (resp === null) {
    print("请求失败");
} else {
    const data = safeParse(resp.body);
    print("状态码:", resp.statusCode);
}

// POST 请求，使用宿主网络直连
const resp2 = fetch("https://api.example.com/submit", {
    method: "POST",
    body: safeStringify({ key: "value" }),
    headers: { "Content-Type": "application/json" },
    timeout: 5000,
    retry: 2,
    useHost: true,
});
```

***

#### `netcat(addr, data, params?)`

向指定地址发起一个**同步** TCP 连接，发送原始字节数据并读取响应，适用于非 HTTP 协议测试。

**参数**

`addr`：`string`，目标地址，格式为 `"host:port"`。

`data`：`string`，要发送的原始数据。

`params`（可选）：`object`，配置项：

| 字段         | 类型        | 默认值     | 说明                      |
| ---------- | --------- | ------- | ----------------------- |
| `useHost`  | `boolean` | `false` | 为 `true` 时绕过代理，使用宿主网络直连 |
| `timeout`  | `number`  | `3000`  | 超时时间（毫秒）                |
| `retry`    | `number`  | `0`     | 失败重试次数                  |
| `readLine` | `boolean` | `false` | 为 `true` 时仅读取第一行响应      |

**返回值**

始终返回如下对象：

| 字段      | 类型       | 说明              |
| ------- | -------- | --------------- |
| `data`  | `string` | 服务端响应的原始数据      |
| `error` | `string` | 错误信息，无错误时为 `""` |

**示例**

```javascript
// 发送原始 TCP 数据
const result = netcat("example.com:80", "HEAD / HTTP/1.0\r\n\r\n", {
    timeout: 2000,
    readLine: true,   // 只读首行
});

if (result.error !== "") {
    print("TCP 错误:", result.error);
} else {
    print("响应:", result.data);
}
```

***

#### 内置变量

#### `proxy`

类型：`object`

脚本执行时由引擎自动注入，描述当前被测代理节点的基本信息。该对象为**只读**。

| 字段        | 类型       | 说明                     |
| --------- | -------- | ---------------------- |
| `Name`    | `string` | 节点名称                   |
| `Address` | `string` | 节点地址，格式通常为 `host:port` |
| `Type`    | `string` | 节点协议类型                 |

**示例**

```javascript
print("当前节点:", proxy.Name);
print("地址:", proxy.Address);
print("协议:", proxy.Type);

// 结合 fetch 使用，在日志中标注节点信息
const resp = fetch("https://api.example.com/check");
if (resp === null) {
    print(proxy.Name, "请求失败");
} else {
    print(proxy.Name, "状态码:", resp.statusCode);
}
```

***

### 注意事项

1. **不要使用异步**：如前文所述，`async/await`、`Promise` 在此环境下行为不可靠。
2. **始终检查 `fetch` 返回值**：网络不可达时返回 `null`，直接访问其属性会导致脚本崩溃。
3. **使用 `safeParse` 替代 `JSON.parse`**：避免因响应体格式异常导致脚本异常退出。
4. **使用 `get` 读取深层对象**：避免因中间字段缺失触发 `TypeError`。

***

## 以兼容性方式调用某些矩阵（Matrix）

当您在使用的某些测速机器人（如 Miaoko）不支持某些新矩阵，您可以使用兼容性方式调用并显示结果。该功能仅限 Miaospeed 兼容 API 调用。

#### 工作原理

在 Media Script 中声明一个特殊的全局常量 `MS_MATRIX_ENTRY`，引擎会调用对应矩阵并将原始结果注入脚本，再由脚本中的 `matrice_formatter(result)` 函数负责将结果转换为展示格式。

`MS_MATRIX_ENTRY → 引擎调用对应矩阵 → matrice_formatter(result) → handler() 读取格式化结果`

***

#### `MS_MATRIX_ENTRY` 声明

```javascript
const MS_MATRIX_ENTRY = {
    name:   "MATRIX_NAME",   // 矩阵名称
    params: "",               // 传递给矩阵的参数
};
```

| 字段       | 类型       | 说明             |
| -------- | -------- | -------------- |
| `name`   | `string` | 要调用的矩阵标识名      |
| `params` | `string` | 矩阵参数，不需要时传空字符串 |

***

#### `matrice_formatter(result)`

接收矩阵原始返回值，输出与 Media Script `handler()` 相同格式的展示对象。

**参数**：`result` — 矩阵返回的原始结果对象，具体字段因矩阵而异。

**返回值**：`{ text, color?, background? }`，与 Media Script 返回格式一致。

***

示例（劫持检测）

```javascript
const MS_MATRIX_ENTRY = {
    name: "TEST_HIJACK_DETECTION",
    params: ""
}

function matrice_formatter(result) {
    let retn = [result.speedIP, result.realIP];
    if (retn[0] === "" || retn[1] === "") {
        return {
            text: "检测失败",
            background: C_NA
        };
    }else{
        if (retn[0] !== retn[1]) {
            return {
                text: `劫持 ${maskIP(retn[0])}/${maskIP(retn[1])}`,
                background: C_FAIL
            };
        }else{
            return {
                text: `未劫持`,
                background: C_UNL
            };
        }
    }
}

function maskIP(ipStr) {
    const mask = "*";
    const IPv4Keep = 2;
    const IPv6Keep = 4;

    // 分离 CIDR 后缀（如 /64）
    let cidr = "";
    const cidrIndex = ipStr.indexOf("/");
    if (cidrIndex !== -1) {
        cidr = ipStr.slice(cidrIndex);
        ipStr = ipStr.slice(0, cidrIndex);
    }

    // 分离 IPv6 zone（如 %eth0）
    let zone = "";
    const zoneIndex = ipStr.lastIndexOf("%");
    if (zoneIndex !== -1 && ipStr.includes(":")) {
        zone = ipStr.slice(zoneIndex);
        ipStr = ipStr.slice(0, zoneIndex);
    }

    // 去除 IPv6 方括号
    ipStr = ipStr.replace(/^\[/, "").replace(/\]$/, "");

    // 尝试 IPv4
    if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ipStr)) {
        const parts = ipStr.split(".");
        for (let i = IPv4Keep; i < 4; i++) parts[i] = mask;
        return parts.join(".") + zone + cidr;
    }

    // 尝试 IPv6
    if (ipStr.includes(":")) {
        // 展开简写形式（::）
        const expandIPv6 = (ip) => {
            const parts = ip.split("::");
            if (parts.length === 1) return ip.split(":");
            const left = parts[0] ? parts[0].split(":") : [];
            const right = parts[1] ? parts[1].split(":") : [];
            const missing = 8 - (left.length + right.length);
            return [...left, ...Array(missing).fill("0"), ...right];
        };

        let hextets = expandIPv6(ipStr);
        for (let i = IPv6Keep; i < 8; i++) hextets[i] = mask;
        return hextets.join(":") + zone + cidr;
    }

    // 非 IP 地址，原样返回
    return ipStr;
}
```
