<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>随心而记</title><description>Blog</description><link>https://www.mihouo.com/</link><language>zh_CN</language><item><title>使用乾坤微前端嵌入早期不同技术栈的项目</title><link>https://www.mihouo.com/posts/front/embedding-legacy-projects-with-different-tech-stacks-using-qiankun-micro-frontend/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/embedding-legacy-projects-with-different-tech-stacks-using-qiankun-micro-frontend/</guid><description>使用乾坤微前端将不同技术栈项目嵌入到现代应用中</description><pubDate>Tue, 02 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;最终效果&lt;/h2&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/videos/preview-qiankun-2024-07-02.mp4&quot; type=&quot;video/mp4&quot;&amp;gt;
Your browser does not support the video tag.
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;p&gt;最近公司有个新的需求，类似于低代码平台的一个需求，前端主要做的是一个拖拽生成大屏的平台，但是这个平台完全从零开发需要消耗非常多的工时，因此需要从 Github 上找类似的开源的可以满足需求的组件或者项目。&lt;/p&gt;
&lt;p&gt;最终选定了 &lt;a href=&quot;https://github.com/anji-plus/report&quot;&gt;AJ-Report&lt;/a&gt; 这个项目，由于这个需求一期可能要在其他系统上开发（React 技术栈），二期可能要改造形成自己的产品，而这个项目使用的技术栈是 Vue2，不管是对于一期还是二期形成自己的产品它的技术栈都不符合要求，但是通过调研，市面上也没有其他更适合的开源项目了，因此我想要使用微前端的架构将它嵌入到主应用中。&lt;/p&gt;
&lt;h2&gt;一期需求&lt;/h2&gt;
&lt;p&gt;对于一期来说，仅仅将它改造成为子系统并嵌入原有的系统中即可，具体改造配置请见二期需求。&lt;/p&gt;
&lt;h2&gt;二期需求&lt;/h2&gt;
&lt;p&gt;对于二期来说，至少需要三个工程，分别是主应用、开源的可拖拽大屏平台子应用、业务代码子应用。&lt;/p&gt;
&lt;p&gt;由于我们团队技术栈都为 React 因此主应用和子应用的首选技术栈为 React。&lt;/p&gt;
&lt;h3&gt;主应用&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;通过 Creact React App 脚手架创建。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npx create-react-app cra-framework --template typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装乾坤&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm i qiankun -S
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装路由并配置路由&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm install react-router-dom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路由及布局如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;注册并启动子应用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom/client&apos;;
import { BrowserRouter } from &apos;react-router-dom&apos;;
import RouterGurad from &apos;./router/RouterGurad&apos;;
import reportWebVitals from &apos;./reportWebVitals&apos;;
import { registerMicroApps, start, initGlobalState, MicroAppStateActions } from &apos;qiankun&apos;;

const root = ReactDOM.createRoot(
  document.getElementById(&apos;root&apos;) as HTMLElement
);
root.render(
  &amp;lt;React.StrictMode&amp;gt;
    &amp;lt;BrowserRouter&amp;gt;
      &amp;lt;RouterGurad /&amp;gt;
    &amp;lt;/BrowserRouter&amp;gt;
  &amp;lt;/React.StrictMode&amp;gt;
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

registerMicroApps([
  {
    name: &apos;app-vue&apos;,
    entry: &apos;//localhost:9528&apos;,
    container: &apos;#root&apos;,
    activeRule: &apos;/app-vue&apos;,
  },
  {
    name: &apos;cra-sub-web&apos;,
    entry: &apos;//localhost:3002&apos;,
    container: &apos;#main&apos;,
    activeRule: &apos;/cra-sub-web&apos;,
  },
]);
// 启动 qiankun
start();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;可拖拽大屏平台子应用&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;src&lt;/code&gt; 目录新增 &lt;code&gt;public-path.js&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;入口文件 &lt;code&gt;main.js&lt;/code&gt; 修改，为了避免根 id &lt;code&gt;#app&lt;/code&gt; 与其他的 DOM 冲突，需要限制查找范围。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;相关代码主要集中在最后导出bootstrap、mount、unmount函数以及挂载 Vue（有很多 原本存在的与改造成子系统无关的代码）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import Vue from &apos;vue&apos;
import &apos;./public-path&apos;;
import VueRouter from &apos;vue-router&apos;;

// element-ui
import ElementUI from &apos;element-ui&apos;
import &apos;element-ui/lib/theme-chalk/index.css&apos;
import zhLocale from &apos;element-ui/lib/locale/lang/zh-CN&apos;
import &apos;normalize.css/normalize.css&apos;// A modern alternative to CSS resets
import &apos;@/assets/styles/common.css&apos;
import &apos;@/assets/styles/index.scss&apos;// custome global css

// app router vuex filter mixins
import App from &apos;./App&apos;
import router from &apos;./router&apos;
import store from &apos;./store&apos;
import * as filter from &apos;./filter&apos;
import mixins from &apos;@/mixins&apos;
import echarts from &apos;echarts&apos;;
// 全局定义echarts
import ECharts from &apos;vue-echarts&apos;
import &apos;echarts/lib/chart/bar&apos;
import &apos;echarts/lib/component/tooltip&apos;
//import &apos;echarts-liquidfill&apos;
// import &apos;echarts-gl&apos;
Vue.component(&apos;v-chart&apos;, ECharts)
// 全局引入datav
import dataV from &apos;@jiaminghi/data-view&apos;
Vue.use(dataV)
// anji component
import anjiCrud from &apos;@/components/AnjiPlus/anji-crud/anji-crud&apos;
import anjiSelect from &apos;@/components/AnjiPlus/anji-select&apos;
import anjiUpload from &apos;@/components/AnjiPlus/anji-upload&apos;
Vue.component(&apos;anji-upload&apos;, anjiUpload)
Vue.component(&apos;anji-crud&apos;, anjiCrud)
Vue.component(&apos;anji-select&apos;, anjiSelect)

// permission control
import &apos;@/permission&apos;
// 按钮权限的指令
import permission from &apos;@/components/Permission/index&apos;
Vue.use(permission)

import Avue from &apos;@smallwei/avue&apos;;
import &apos;@smallwei/avue/lib/index.css&apos;;
Vue.use(Avue);

// enable element zh-cn
Vue.use(ElementUI, { zhLocale })

// register global filter.
Object.keys(filter).forEach(key =&amp;gt; {
  Vue.filter(key, filter[key])
})

// register global mixins.
Vue.mixin(mixins)

// 分页的全局size配置;
Vue.prototype.$pageSizeAll = [10, 50, 100, 200, 500]

// create the app instance.
// new Vue({ el: &apos;#app&apos;, router, store, render: h =&amp;gt; h(App) })

Vue.config.productionTip = false;

let instance = null;
function render(props = {}) {
  const { container } = props;
  router.base = window.__POWERED_BY_QIANKUN__ ? &apos;/app-vue/&apos; : &apos;/&apos;;

  instance = new Vue({
    el: container ? container.querySelector(&apos;#app&apos;) : &apos;#app&apos;,
    router,
    store,
    render: (h) =&amp;gt; h(App),
  });
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log(&apos;[vue] vue app bootstraped&apos;);
}
export async function mount(props) {
  console.log(&apos;[vue] props from main framework&apos;, props);
  render(props);
}
export async function unmount() {
  console.log(&apos;[vue] vue app unmount&apos;);
  instance.$destroy();
  instance.$el.innerHTML = &apos;&apos;;
  instance = null;
  // router = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;打包配置修改（&lt;code&gt;vue.config.js&lt;/code&gt;）：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于这个 vue 项目使用的脚手架版本的问题并没有 vue.config.js文件，因此直接修改 webpack 配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const devWebpackConfig = merge(baseWebpackConfig, {
  output: {
    path: config.build.assetsRoot,
    filename: &apos;[name].js&apos;,
    publicPath: config.dev.assetsPublicPath,
    library: &apos;app1&apos;,
    libraryTarget: &apos;umd&apos;, // 把微应用打包成 umd 库格式
    jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
  },
   devServer: {
    headers: {
      &apos;Access-Control-Allow-Origin&apos;: &apos;*&apos;,
    },
   }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置完成后访问 http://localhost:3001/app-vue#/bigscreen/designer?reportCode=test_001 如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;业务代码子应用&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;通过 Creact React App 脚手架创建。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npx create-react-app cra-sub-web --template typescript
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在 &lt;code&gt;src&lt;/code&gt; 目录新增 &lt;code&gt;public-path.js&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;设置 &lt;code&gt;history&lt;/code&gt; 模式路由的 &lt;code&gt;base&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? &apos;/app-react&apos; : &apos;/&apos;}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;入口文件 &lt;code&gt;index.js&lt;/code&gt; 修改，为了避免根 id &lt;code&gt;#root&lt;/code&gt; 与其他的 DOM 冲突，需要限制查找范围。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;
import &apos;./public-path&apos;;
import ReactDOM from &apos;react-dom/client&apos;;
import routes from &apos;./router&apos;;
import { BrowserRouter, useRoutes } from &apos;react-router-dom&apos;;
import &apos;./index.css&apos;;

let root = ReactDOM.createRoot(
  document.getElementById(&apos;root&apos;) as HTMLElement
);

function App () {
  return useRoutes(routes)
}
// @ts-ignore
console.log(window.__POWERED_BY_QIANKUN__);

// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  root.render(
    &amp;lt;React.StrictMode&amp;gt;
      {/* @ts-ignore */}
      &amp;lt;BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? &apos;/cra-sub-web&apos; : &apos;/&apos;}&amp;gt;
        &amp;lt;App /&amp;gt;
      &amp;lt;/BrowserRouter&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;
  );
}

export async function bootstrap() {
  console.log(&apos;[react16] react app bootstraped&apos;);
}

export async function mount(props: any) {
  console.log(&apos;[react16] props from main framework&apos;, props);
  const { container } = props;
  root = ReactDOM.createRoot(container ? container.querySelector(&apos;#root&apos;) as HTMLElement : document.getElementById(&apos;root&apos;) as HTMLElement);
  console.log(container ? container.querySelector(&apos;#root&apos;) as HTMLElement : document.getElementById(&apos;root&apos;) as HTMLElement);
  
  root.render(
    &amp;lt;React.StrictMode&amp;gt;
      {/* @ts-ignore */}
      &amp;lt;BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? &apos;/cra-sub-web&apos; : &apos;/&apos;}&amp;gt;
        &amp;lt;App /&amp;gt;
      &amp;lt;/BrowserRouter&amp;gt;
    &amp;lt;/React.StrictMode&amp;gt;
  );
}

export async function unmount(props: any) {
  // 卸载
  root.unmount();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改 &lt;code&gt;webpack&lt;/code&gt; 配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我使用的是 &lt;a href=&quot;https://github.com/timarney/react-app-rewired&quot;&gt;react-app-rewired&lt;/a&gt; 库，安装 react-app-rewired&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install react-app-rewired --save-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 package.json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;react-app-rewired start&quot;,
    &quot;build&quot;: &quot;react-app-rewired build&quot;,
    &quot;test&quot;: &quot;react-app-rewired test&quot;,
    &quot;eject&quot;: &quot;react-scripts eject&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 config-overrides.js 文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const { override, addWebpackAlias } = require(&apos;customize-cra&apos;);
const path = require(&apos;path&apos;);
const { name } = require(&apos;./package&apos;);

module.exports = {
  webpack: override(
    (config) =&amp;gt; {
      config.output.library = `${name}-[name]`;
      config.output.libraryTarget = &apos;umd&apos;;
      // Webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
      config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
      config.output.globalObject = &apos;window&apos;;
      return config;
    },
    addWebpackAlias({
      &apos;@&apos;: path.resolve(__dirname, &apos;src&apos;),
    })
  ),

  devServer: (configFunction) =&amp;gt; {
    const config = configFunction;

    config.headers = {
      &apos;Access-Control-Allow-Origin&apos;: &apos;*&apos;,
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新启动服务 然后访问 http://localhost:3001/cra-sub-web/home 如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;应用间通信&lt;/h2&gt;
&lt;p&gt;根据&lt;a href=&quot;https://qiankun.umijs.org/zh/api#initglobalstatestate&quot;&gt;文档&lt;/a&gt;使用**&lt;code&gt;GlobalState&lt;/code&gt;** ， 使用方式类似于 bus。&lt;/p&gt;
&lt;p&gt;首先在主应用中定义 state&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { initGlobalState, MicroAppStateActions } from &apos;qiankun&apos;;

const state = {
  status: 1,
};

const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) =&amp;gt; {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
  if (state.status === 0) {
    alert(&apos;token 失效，跳转登录页&apos;);
    window.location.href = &apos;/home&apos;;
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子应用从 mount 函数中消费即可，这里将函数保存到 Vue 示例上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export async function mount(props) {
  console.log(&apos;[vue] props from main framework&apos;, props);
  Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange;
  Vue.prototype.$setGlobalState = props.setGlobalState;
  render(props);
}
// 使用
Vue.prototype.$setGlobalState({
  status: 2
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是使用时控制台警告提醒该 api 将要在 qiankun3.0 版本移除，不推荐使用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据 Github Issue  中看到开发者建议将变量挂载到 window对象中，最终修改如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const state = {
  status: 1,
};
// window 添加__qiankun__.$state全局变量
window.__qiankun__ = {};
window.__qiankun__.$state = state;

// 监听 window.__qiankun__.$state 变化
window.__qiankun__.$state = new Proxy(state, {
  set: function (target, key, value) {
    console.log(&apos;set&apos;, key, value);
    if (key === &apos;status&apos; &amp;amp;&amp;amp; value === 0) {
      alert(&apos;token 失效，跳转登录页&apos;);
      window.location.href = &apos;/home&apos;;
    }
    return Reflect.set(target, key, value);
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入子系统 &lt;em&gt;console&lt;/em&gt;**.&lt;em&gt;log&lt;/em&gt;**(&lt;em&gt;window&lt;/em&gt;**.&lt;em&gt;&lt;strong&gt;&lt;strong&gt;qiankun&lt;/strong&gt;&lt;/strong&gt;&lt;/em&gt;.**&lt;em&gt;$state&lt;/em&gt;); 控制台成功打印&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将子系统中的window.&lt;strong&gt;qiankun&lt;/strong&gt;.$state.status设置为 0 成功被主应用监听到。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>OpenCode 入门指南</title><link>https://www.mihouo.com/posts/ai/opencode-%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/</link><guid isPermaLink="true">https://www.mihouo.com/posts/ai/opencode-%E5%85%A5%E9%97%A8%E6%8C%87%E5%8D%97/</guid><description>OpenCode 是一款开源的终端 AI 编程助手，本指南带你从零开始完成安装配置，熟悉常用命令，并介绍 oh-my-openagent 等进阶插件。</description><pubDate>Tue, 07 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;OpenCode 入门指南&lt;/h1&gt;
&lt;h2&gt;1. 前言&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://opencode.ai/&quot;&gt;OpenCode&lt;/a&gt; 是一款开源的终端 AI 编程助手，类似于 Claude Code 和 Gemini CLI，能够直接在终端中与大语言模型对话，完成代码编写、调试、重构等开发任务。&lt;/p&gt;
&lt;p&gt;与传统 IDE 插件不同，OpenCode 运行在终端环境中，天然适配服务器、远程开发等场景，同时支持灵活接入多种模型提供商，让你自由选择最适合的 AI 模型。&lt;/p&gt;
&lt;p&gt;本指南将带你从零开始完成 OpenCode 的安装与配置，熟悉日常使用方式和常用命令，并介绍 oh-my-openagent 等进阶插件，帮助你充分释放 AI 辅助编程的潜力。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 配置网络代理&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Node.js、OpenCode 等工具在安装过程中可能因网络原因下载缓慢甚至失败，建议先完成网络代理配置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开注册链接，注册一个账号：https://love.p6m6.com/#/register?code=sAkUiL1y&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择「入门精灵球」套餐，输入优惠码 &lt;code&gt;耿鬼&lt;/code&gt;（0 元即可购买）
&lt;img src=&quot;proxy-coupon.png&quot; alt=&quot;输入优惠码购买套餐&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;按页面提示下载代理软件，然后点击「导入订阅」
&lt;img src=&quot;proxy-import-subscription.png&quot; alt=&quot;导入订阅&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载完成后，打开代理软件，把模式切换到「规则代理」，然后开启代理&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开浏览器访问 Google，如果能正常显示，说明代理配置成功了&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;终端中使用代理：打开代理软件的设置，找到「复制环境变量」相关选项，切换到 &lt;code&gt;PowerShell&lt;/code&gt; 类型，将复制的内容粘贴到 PowerShell 终端中，之后在该终端中执行的下载操作都会走代理
&lt;img src=&quot;proxy-copy-env.png&quot; alt=&quot;复制环境变量到终端&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS：&lt;/strong&gt; 终端类型选择 &lt;code&gt;bash&lt;/code&gt; 或 &lt;code&gt;zsh&lt;/code&gt;，粘贴到终端（Terminal）中&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows（WSL 环境下）：&lt;/strong&gt; 终端类型选择 &lt;code&gt;bash&lt;/code&gt;，粘贴到 WSL 终端中&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; 每次打开新的终端窗口都需要重新粘贴环境变量，代理才会生效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 安装&lt;/h2&gt;
&lt;h3&gt;3.1 macOS&lt;/h3&gt;
&lt;p&gt;打开终端（Terminal），选择以下任一方式安装：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式一：Homebrew（官方推荐）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install anomalyco/tap/opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式二：一键安装脚本&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;方式三：npm&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g opencode-ai
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 Windows&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Windows 下推荐通过 WSL 使用 OpenCode，这是官方最佳体验方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;右键开始菜单&lt;/code&gt; → &lt;code&gt;Windows PowerShell（管理员）&lt;/code&gt; 或 &lt;code&gt;终端（管理员）&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步：安装 WSL&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后&lt;strong&gt;重启计算机&lt;/strong&gt;。重启后再次打开 PowerShell（管理员），输入以下命令完成环境初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动 WSL
wsl

# 根据提示设置用户名和密码

# 更新软件源
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y

# 安装必要依赖
sudo apt install -y curl git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第二步：安装 OpenCode&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://opencode.ai/install | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.3 验证安装&lt;/h3&gt;
&lt;p&gt;安装完成后，运行以下命令确认是否成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;opencode -v
# 输出示例：1.2.24
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 配置&lt;/h2&gt;
&lt;p&gt;创建并编辑配置文件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/opencode &amp;amp;&amp;amp; nano ~/.config/opencode/opencode.jsonc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows（WSL 环境下）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/opencode &amp;amp;&amp;amp; nano ~/.config/opencode/opencode.jsonc
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;其他编辑方式（推荐）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;VS Code（最推荐，智能提示 + Schema 自动补全）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code ~/.config/opencode/opencode.jsonc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前提：在 WSL 中已安装 VS Code，或在 Windows 端使用 VS Code + Remote-WSL 扩展&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Vim&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim ~/.config/opencode/opencode.jsonc
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;从 Windows 资源管理器直接编辑&lt;/strong&gt;（无需进入 WSL）&lt;/p&gt;
&lt;p&gt;在地址栏输入 &lt;code&gt;\\wsl$\Ubuntu\home\你的WSL用户名\.config\opencode&lt;/code&gt;（将 &lt;code&gt;Ubuntu&lt;/code&gt; 替换为你的发行版名，如 &lt;code&gt;Ubuntu-22.04&lt;/code&gt;），然后用记事本或 Notepad++ 打开 &lt;code&gt;opencode.jsonc&lt;/code&gt; 编辑&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;填入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
  &quot;disabled_providers&quot;: [&quot;google-vertex&quot;, &quot;google-vertex-anthropic&quot;],
  &quot;provider&quot;: {
    &quot;cpa&quot;: {
      &quot;name&quot;: &quot;CPA&quot;,
      &quot;npm&quot;: &quot;@ai-sdk/openai-compatible&quot;,
      &quot;options&quot;: {
        &quot;baseURL&quot;: &quot;你的服务地址&quot;,
        &quot;apiKey&quot;: &quot;你的 API Key&quot;
      },
      &quot;models&quot;: {
        &quot;gpt-latest&quot;: {
          &quot;name&quot;: &quot;gpt-latest(high)&quot;,
          &quot;cost&quot;: {
            &quot;input&quot;: 2.5,
            &quot;output&quot;: 15,
            &quot;cache_read&quot;: 0.25,
            &quot;cache_write&quot;: 0,
            &quot;context_over_200k&quot;: {
              &quot;input&quot;: 5,
              &quot;output&quot;: 22.5,
              &quot;cache_read&quot;: 0.5,
              &quot;cache_write&quot;: 0,
            },
          },
        },
        &quot;gpt-codex-latest&quot;: {
          &quot;name&quot;: &quot;gpt-codex-latest(xhigh)&quot;,
          &quot;cost&quot;: {
            &quot;input&quot;: 1.75,
            &quot;output&quot;: 14,
            &quot;cache_read&quot;: 0.175,
            &quot;cache_write&quot;: 0,
          },
        },
      },
    }
  },
  &quot;autoupdate&quot;: true,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;gpt-latest&lt;/code&gt; 指向最新的 GPT 模型，当前为 &lt;strong&gt;GPT-5.4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gpt-codex-latest&lt;/code&gt; 指向最新的 GPT Codex 模型，当前为 &lt;strong&gt;GPT-5.3-codex&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;模型名称括号中的内容为思考量等级，可选值为 &lt;code&gt;low&lt;/code&gt;、&lt;code&gt;medium&lt;/code&gt;、&lt;code&gt;high&lt;/code&gt;、&lt;code&gt;xhigh&lt;/code&gt;。等级越高，模型能力越强，但 Token 消耗越大、响应速度越慢&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 使用&lt;/h2&gt;
&lt;p&gt;首先进入你的项目目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# macOS
cd ~/project

# Windows（WSL 环境下）
cd /mnt/c/Users/你的用户名/project
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windows 路径与 WSL 路径的对应关系：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Windows 路径&lt;/th&gt;
&lt;th&gt;WSL 路径&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C:\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mnt/c/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;D:\&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mnt/d/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C:\Users\用户名\Documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mnt/c/Users/用户名/Documents&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;5.1 终端模式（TUI）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.2 Web 模式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;opencode web
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 常用命令&lt;/h2&gt;
&lt;h3&gt;6.1 CLI 命令（终端直接输入）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动 TUI 交互界面（最常用）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示版本号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode -h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示帮助信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode upgrade&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;升级到最新版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode run &quot;你的提示&quot;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;非交互模式，快速执行单次任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出所有可用模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode session list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看历史会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode -s &amp;lt;session_id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;使用指定会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode serve&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动无头服务器（供 Desktop / Web 连接）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;启动 Web 界面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看 Token 使用统计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;6.2 TUI 内置命令（进入界面后输入 &lt;code&gt;/&lt;/code&gt;）&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/connect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;连接或切换模型提供商&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;查看并切换模型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;初始化项目（生成 AGENTS.md）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;新建会话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;手动压缩上下文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/undo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;撤销上一步操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/redo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;重做&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/share&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;生成分享链接&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/help&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;显示所有命令&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;7. 进阶配置&lt;/h2&gt;
&lt;h3&gt;7.1 oh-my-openagent 插件&lt;/h3&gt;
&lt;h4&gt;7.1.1 简介&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://ohmyopencode.org/&quot;&gt;oh-my-openagent&lt;/a&gt;（简称 OMO，前身为 oh-my-opencode）是 OpenCode 生态中最受欢迎的多代理插件。它将 OpenCode 从单一 AI 助手升级为一个&lt;strong&gt;高度自治的 AI 团队&lt;/strong&gt;，通过 Sisyphus 主协调代理调度多个专业子代理并行工作，能够将复杂的软件工程任务自动化——几乎做到&quot;交代任务后去喝杯咖啡，回来就完成了&quot;。&lt;/p&gt;
&lt;h4&gt;7.1.2 核心功能&lt;/h4&gt;
&lt;h4&gt;执行模式&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Ultra Work 模式（&lt;code&gt;ulw&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最核心的执行模式。输入 &lt;code&gt;ulw&lt;/code&gt; + 任务描述，所有代理同时激活，自动规划、并行执行、自我纠错，持续运行直到任务完成，无需手动干预。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ralph Loop（&lt;code&gt;/ulw-loop&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;自引用执行循环，与 Ultra Work 配合使用。代理会反复迭代，直到任务完全达成目标——适合需要多轮修改和验证的复杂任务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;深度初始化（&lt;code&gt;/init-deep&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在项目目录树中自动生成分层的 &lt;code&gt;AGENTS.md&lt;/code&gt; 文件，为每个代理提供分层、有作用域的上下文，既节省 Token 又提升代理对项目的理解能力。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;规划模式（&lt;code&gt;/start-work&lt;/code&gt;）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由 Prometheus 代理驱动的面试式规划流程。在编写任何代码之前，它会像真正的工程师一样向你提问、澄清需求、确定范围，然后生成详细的执行计划。&lt;/p&gt;
&lt;h4&gt;质量保障机制&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;IntentGate（意图门控）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在分类或执行操作前，先分析用户的真实意图，防止对提示词的字面误解。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hashline（哈希锚定编辑）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;代理读取的每一行都会标记内容哈希（如 &lt;code&gt;11#VK|&lt;/code&gt;），编辑时引用这些标记，哈希不匹配的修改会被直接拒绝，从源头防止文件损坏。据官方报告，编辑成功率从 6.7% 提升到 68.3%。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Todo Enforcer（任务强制器）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;自动将偏离主线的代理拉回正轨，确保当前任务不会被遗忘或搁置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comment Checker（注释检查器）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;阻止低质量的 AI 生成注释，确保代码注释的专业性和可读性。&lt;/p&gt;
&lt;h4&gt;工具链集成&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;LSP 与 AST-Grep&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LSP 工具：&lt;code&gt;lsp_rename&lt;/code&gt;、&lt;code&gt;lsp_goto_definition&lt;/code&gt;、&lt;code&gt;lsp_find_references&lt;/code&gt;、&lt;code&gt;lsp_diagnostics&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;AST-Grep：跨 25 种语言的模式感知搜索与重写&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;内置 MCP 服务&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Exa&lt;/strong&gt; — 网页搜索&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context7&lt;/strong&gt; — 官方文档查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grep.app&lt;/strong&gt; — GitHub 代码搜索&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Skill-Embedded MCPs（技能内嵌 MCP）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个技能可携带自己的 MCP 服务器，按需启动、用完即销毁，不会占用上下文窗口。内置技能包括 &lt;code&gt;playwright&lt;/code&gt;（浏览器自动化）、&lt;code&gt;git-master&lt;/code&gt;（原子提交 / Rebase）、&lt;code&gt;frontend-ui-ux&lt;/code&gt;（设计优先的 UI 开发）等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tmux 集成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;完整的交互式终端支持，可在代理会话中运行 REPL、调试器和 TUI 应用程序。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;智能模型路由&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据任务类型自动选择最合适的模型（如 Claude 做规划、GPT Codex 写代码），按类别而非具体模型分配，在节省 Token 的同时保持高效。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Code 完全兼容&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;现有的所有 Hooks、Commands、Skills、MCPs 和插件均可直接使用，无需任何改动。&lt;/p&gt;
&lt;h4&gt;7.1.3 安装&lt;/h4&gt;
&lt;p&gt;进入 OpenCode 终端后，直接发送以下内容让 LLM 自动完成安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Install and configure oh-my-opencode by following the instructions here:
https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/heads/dev/docs/guide/installation.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装成功后，重新进入 OpenCode，默认模式变为 &lt;code&gt;Sisyphus (Ultraworker)&lt;/code&gt; 即表示安装成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;安装成功示例&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;7.1.4 配置模型&lt;/h4&gt;
&lt;p&gt;oh-my-openagent 默认配置的模型为 GPT、Claude、Gemini 等官方模型。由于我们使用的是 CPA 提供商，需要手动修改配置文件。&lt;/p&gt;
&lt;p&gt;编辑 oh-my-opencode 配置文件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code ~/.config/opencode/oh-my-opencode.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows（WSL 环境下）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code ~/.config/opencode/oh-my-opencode.json
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;也可使用 &lt;code&gt;nano&lt;/code&gt; 或 &lt;code&gt;vim&lt;/code&gt; 替代 &lt;code&gt;code&lt;/code&gt;，编辑方式参见上方第 4 节的说明。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;将内容替换为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/dev/assets/oh-my-opencode.schema.json&quot;,
  &quot;agents&quot;: {
    &quot;sisyphus&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;hephaestus&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;oracle&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;librarian&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;explore&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;multimodal-looker&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;prometheus&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;metis&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;momus&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;atlas&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    }
  },
  &quot;categories&quot;: {
    &quot;visual-engineering&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;ultrabrain&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;deep&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;xhigh&quot;
    },
    &quot;artistry&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;quick&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;unspecified-low&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-codex-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;unspecified-high&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    },
    &quot;writing&quot;: {
      &quot;model&quot;: &quot;cpa/gpt-latest&quot;,
      &quot;variant&quot;: &quot;high&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;说明：&lt;/strong&gt; 以上配置根据各代理和分类的职责特点，在两个可用模型之间做了差异化分配：需要深度推理的角色（规划、架构、审查）使用 &lt;code&gt;gpt-latest&lt;/code&gt; + &lt;code&gt;xhigh&lt;/code&gt;；需要深度编码的角色使用 &lt;code&gt;gpt-codex-latest&lt;/code&gt; + &lt;code&gt;xhigh&lt;/code&gt;；轻量搜索和快速任务使用 &lt;code&gt;gpt-codex-latest&lt;/code&gt; + &lt;code&gt;high&lt;/code&gt;（成本更低）；通用协调和执行角色使用 &lt;code&gt;gpt-latest&lt;/code&gt; + &lt;code&gt;high&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;7.1.5 Agents 与 Categories 参考&lt;/h4&gt;
&lt;h5&gt;Agents（专业代理角色）&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;代理名称&lt;/th&gt;
&lt;th&gt;主要职责&lt;/th&gt;
&lt;th&gt;推荐模型&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;sisyphus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;主协调者&lt;/td&gt;
&lt;td&gt;Claude Opus / GLM-5&lt;/td&gt;
&lt;td&gt;任务拆解、分配子代理、并行调度、驱动任务完成，所有复杂任务的入口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;hephaestus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;自主深度编码工匠&lt;/td&gt;
&lt;td&gt;GPT-5.3-codex&lt;/td&gt;
&lt;td&gt;探索代码库、研究模式、端到端自主实现功能，擅长深度编码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;prometheus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;战略规划师&lt;/td&gt;
&lt;td&gt;Claude Opus / GLM-5&lt;/td&gt;
&lt;td&gt;面试式规划——向用户提问、明确范围、构建详细计划后再动手&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;oracle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;架构与调试专家&lt;/td&gt;
&lt;td&gt;高推理能力模型&lt;/td&gt;
&lt;td&gt;架构决策、疑难 bug 诊断、技术方案验证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;librarian&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;知识与文档管理员&lt;/td&gt;
&lt;td&gt;通用模型&lt;/td&gt;
&lt;td&gt;代码和文档搜索、解释现有代码、生成说明文档&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;explore&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;快速代码库探索者&lt;/td&gt;
&lt;td&gt;轻量快速模型&lt;/td&gt;
&lt;td&gt;快速扫描和 Grep，定位修改点、收集上下文证据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;multimodal-looker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多模态 / 视觉分析专家&lt;/td&gt;
&lt;td&gt;多模态模型&lt;/td&gt;
&lt;td&gt;处理图片、UI 设计稿、截图，转化为代码或建议&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;metis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;预规划分析师&lt;/td&gt;
&lt;td&gt;高推理能力模型&lt;/td&gt;
&lt;td&gt;与 Prometheus 协作，识别风险、边缘情况、依赖关系、方案权衡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;momus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;严苛审查者 / 质量批评家&lt;/td&gt;
&lt;td&gt;高推理能力模型&lt;/td&gt;
&lt;td&gt;代码与计划审查，发现 bug、安全与性能问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;atlas&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;执行大师 / 部署者&lt;/td&gt;
&lt;td&gt;通用模型&lt;/td&gt;
&lt;td&gt;最终集成、部署、环境配置、验证生产就绪&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; &quot;推荐模型&quot;列为官方建议的理想配置。由于我们当前只接入了 CPA 提供商，实际配置中统一使用 &lt;code&gt;cpa/gpt-latest&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;Categories（任务分类路由）&lt;/h5&gt;
&lt;p&gt;Sisyphus 协调代理在分配任务时，会根据任务特征自动匹配以下分类，每个分类对应不同的模型和努力级别：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类名称&lt;/th&gt;
&lt;th&gt;适用领域&lt;/th&gt;
&lt;th&gt;努力级别&lt;/th&gt;
&lt;th&gt;常见场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;visual-engineering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;前端、UI/UX、视觉设计&lt;/td&gt;
&lt;td&gt;高（视觉 / 多模态）&lt;/td&gt;
&lt;td&gt;构建界面、CSS、组件、设计系统&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ultrabrain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极难逻辑、重型架构、复杂算法&lt;/td&gt;
&lt;td&gt;极高（长思考）&lt;/td&gt;
&lt;td&gt;系统设计、分布式系统、性能关键逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;deep&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;目标导向的深度问题解决&lt;/td&gt;
&lt;td&gt;中高（自主执行）&lt;/td&gt;
&lt;td&gt;端到端功能实现、深度重构、跨模块修改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;artistry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;创意 / 非常规解决方案&lt;/td&gt;
&lt;td&gt;高（发散思考）&lt;/td&gt;
&lt;td&gt;发明新模式、巧妙优化、跳出框框的思路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;quick&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;琐碎、快速的小修改&lt;/td&gt;
&lt;td&gt;极低（最小思考）&lt;/td&gt;
&lt;td&gt;拼写错误、一行改动、快速重命名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;unspecified-low&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;未分类的简单通用任务&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;杂项小任务、样板代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;unspecified-high&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;未分类的重要复杂任务&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;td&gt;重要但未匹配分类的任务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;writing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;文档、注释、README&lt;/td&gt;
&lt;td&gt;自然语言优先&lt;/td&gt;
&lt;td&gt;写文档、变更日志、博客、测试描述&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;除以上内置分类外，oh-my-openagent 也支持自定义分类，可根据团队需求扩展。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7.2 MCP&lt;/h3&gt;
&lt;p&gt;MCP（Model Context Protocol，模型上下文协议）是一种标准化协议，允许 AI 模型访问外部工具和数据源。通过配置 MCP，OpenCode 可以连接本地脚本、远程知识库或专业服务，从而获得更强的能力。&lt;/p&gt;
&lt;p&gt;在配置文件中添加 &lt;code&gt;mcp&lt;/code&gt; 字段即可配置（macOS：&lt;code&gt;~/.config/opencode/opencode.jsonc&lt;/code&gt;，Windows：&lt;code&gt;%USERPROFILE%\.config\opencode\opencode.jsonc&lt;/code&gt;）。OpenCode 支持两种类型的 MCP 服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;远程（remote）&lt;/strong&gt;：连接远程 HTTP/HTTPS 端点，无需本地安装&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地（local）&lt;/strong&gt;：运行本地命令或脚本作为 MCP 服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;推荐 MCP 服务&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Context7 — 官方文档查询&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实时查询任意编程库的最新文档和代码示例，避免 AI 使用过时的 API。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcp&quot;: {
    &quot;context7&quot;: {
      &quot;type&quot;: &quot;remote&quot;,
      &quot;url&quot;: &quot;https://mcp.context7.com/mcp&quot;,
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Grep.app — GitHub 代码搜索&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;搜索 GitHub 上的公开代码，快速找到真实项目中的用法和参考实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcp&quot;: {
    &quot;gh_grep&quot;: {
      &quot;type&quot;: &quot;remote&quot;,
      &quot;url&quot;: &quot;https://mcp.grep.app&quot;,
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Exa — 网页搜索&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;让 AI 具备实时联网搜索能力，获取最新资讯和技术文章。需要 &lt;a href=&quot;https://exa.ai/&quot;&gt;Exa API Key&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;mcp&quot;: {
    &quot;exa&quot;: {
      &quot;type&quot;: &quot;local&quot;,
      &quot;command&quot;: [&quot;npx&quot;, &quot;-y&quot;, &quot;exa-mcp-server&quot;],
      &quot;environment&quot;: {
        &quot;EXA_API_KEY&quot;: &quot;你的 Exa API Key&quot;
      },
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;组合配置示例&lt;/h4&gt;
&lt;p&gt;以上服务可同时启用，合并写入配置文件的 &lt;code&gt;mcp&lt;/code&gt; 字段中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://opencode.ai/config.json&quot;,
  // ... 其他配置（provider 等）
  &quot;mcp&quot;: {
    &quot;context7&quot;: {
      &quot;type&quot;: &quot;remote&quot;,
      &quot;url&quot;: &quot;https://mcp.context7.com/mcp&quot;,
    },
    &quot;gh_grep&quot;: {
      &quot;type&quot;: &quot;remote&quot;,
      &quot;url&quot;: &quot;https://mcp.grep.app&quot;,
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;常用管理命令&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode mcp add&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;交互式添加 MCP 服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode mcp list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;列出已配置的所有 MCP 服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode mcp auth &amp;lt;名称&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对指定服务进行鉴权&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;opencode mcp debug &amp;lt;名称&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;检查连接状态和服务健康度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; MCP 服务会增加上下文消耗，建议只启用当前需要的服务，避免因工具过多导致 Token 超限。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;7.3 推荐技能（Skill）&lt;/h3&gt;
&lt;p&gt;Skill 是 OpenCode 的技能插件系统，每个技能包含领域专属的指令、内嵌 MCP 服务和预设权限，安装后在相关场景中自动激活，无需手动调用。&lt;/p&gt;
&lt;h4&gt;ui-ux-pro-max — AI 驱动的 UI/UX 设计智能&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;适用范围：仅限前端项目。&lt;/strong&gt; 该技能专为前端 UI/UX 开发设计，不适用于后端、CLI 工具、数据处理等非前端场景。如果你的项目不涉及界面开发，无需安装此技能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nextlevelbuilder/ui-ux-pro-max-skill&quot;&gt;ui-ux-pro-max&lt;/a&gt; 是一款面向前端开发者的设计智能技能。当你向 AI 描述 UI 需求时，它会自动生成完整的设计系统（配色、字体、布局、风格、反模式检查），然后基于该系统输出高质量代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心能力&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设计系统生成器&lt;/strong&gt;：用自然语言描述项目，自动输出完整设计系统——布局模式、视觉风格、配色方案、字体组合、特效建议及反模式检查清单&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;161 条行业推理规则&lt;/strong&gt;：覆盖 SaaS、金融科技、电商、医疗、餐饮、游戏、Web3 等细分领域，每条规则包含推荐布局、风格优先级、色彩基调和排版建议&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;67 种 UI 风格&lt;/strong&gt;：Glassmorphism、Neumorphism、Brutalism、Bento Grid、Liquid Glass、Cyberpunk UI、Aurora UI 等主流与前沿风格&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;57 组精选字体搭配&lt;/strong&gt;：附带 Google Fonts 链接，开箱即用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;161 套配色方案&lt;/strong&gt;：按产品类型匹配，确保行业适配性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;25 种图表类型&lt;/strong&gt;：适用于仪表盘和数据分析场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;支持的技术栈&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;框架&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web（默认）&lt;/td&gt;
&lt;td&gt;HTML + Tailwind CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React 生态&lt;/td&gt;
&lt;td&gt;React、Next.js、shadcn/ui&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vue 生态&lt;/td&gt;
&lt;td&gt;Vue、Nuxt.js、Nuxt UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;其他 Web&lt;/td&gt;
&lt;td&gt;Svelte、Astro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;SwiftUI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Jetpack Compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨平台&lt;/td&gt;
&lt;td&gt;React Native、Flutter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;前置依赖：Python 3.x&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装 CLI
npm install -g uipro-cli

# 进入项目目录后初始化（指定 opencode 作为 AI 工具）
cd /path/to/your/project
uipro init --ai opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;安装后无需额外操作，当你在 OpenCode 中发起 UI/UX 相关请求时（如 &lt;code&gt;build&lt;/code&gt;、&lt;code&gt;design&lt;/code&gt;、&lt;code&gt;create&lt;/code&gt;、&lt;code&gt;implement&lt;/code&gt;、&lt;code&gt;review&lt;/code&gt;、&lt;code&gt;fix&lt;/code&gt;、&lt;code&gt;improve&lt;/code&gt;），技能会自动激活并执行以下流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分析你的需求，匹配产品类型和行业规则&lt;/li&gt;
&lt;li&gt;自动生成完整设计系统（配色、字体、布局、风格）&lt;/li&gt;
&lt;li&gt;基于设计系统生成代码，确保色彩、间距、最佳实践一致&lt;/li&gt;
&lt;li&gt;交付前自动检查常见反模式&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示：&lt;/strong&gt; 在提示词中注明技术栈（如&quot;用 Next.js + shadcn/ui&quot;），可获得更精准的代码输出。未指定时默认使用 HTML + Tailwind CSS。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>从 Warp 迁移到 Ghostty</title><link>https://www.mihouo.com/posts/tool-share/from-warp-to-ghostty/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/from-warp-to-ghostty/</guid><description>从 Warp 终端迁移到 Ghostty 的完整过程与配置</description><pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;随着 Claude Code、Cursor Agent 等终端 AI 工具的兴起，终端不再只是敲命令的地方，而是主力开发环境。一个高效、轻量、可控的终端模拟器变得前所未有的重要。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;::github{repo=&quot;ghostty-org/ghostty&quot;}&lt;/p&gt;
&lt;h2&gt;为什么离开 Warp&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.warp.dev/&quot;&gt;Warp&lt;/a&gt; 是一款基于 Rust 开发的现代化终端，主打 AI 辅助、块编辑、命令搜索等创新功能。我使用了大约一年，确实带来了不少便利，但随着使用深入，一些问题逐渐暴露：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;资源占用高&lt;/strong&gt;：GPU 渲染 + 丰富功能 = 明显的内存和 CPU 开销&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./warp-memory.png&quot; alt=&quot;Warp 内存占用&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动慢&lt;/strong&gt;：冷启动 2-3 秒，打断工作心流&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强制登录&lt;/strong&gt;：必须注册账号才能使用，终端工具为什么需要账号？&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内置 AI 鸡肋&lt;/strong&gt;：实际工作中更多依赖 Claude Code，内置 AI 几乎没用过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;闭源&lt;/strong&gt;：无法自定义或贡献代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么选择 Ghostty&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; 由 HashiCorp 联合创始人 Mitchell Hashimoto 开发，使用 Zig 语言编写。它的设计理念恰好解决了我对 Warp 的所有不满：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;极速启动&lt;/strong&gt;：GPU 加速渲染，启动几乎瞬间完成&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源占用低&lt;/strong&gt;：内存仅 Warp 的 1/3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./ghostty-memory.png&quot; alt=&quot;Ghostty 内存占用&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完全开源&lt;/strong&gt;：MIT 协议，社区活跃&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无需账号&lt;/strong&gt;：开箱即用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生体验&lt;/strong&gt;：遵循 macOS 设计规范&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置简洁&lt;/strong&gt;：单文件配置，语法直观&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;brew install --cask ghostty
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以从 &lt;a href=&quot;https://ghostty.org/&quot;&gt;官网&lt;/a&gt; 直接下载。&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;Ghostty 使用单文件配置 &lt;code&gt;~/.config/ghostty/config&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/ghostty &amp;amp;&amp;amp; touch ~/.config/ghostty/config

# 安装字体（配置中使用）
brew install --cask font-maple-mono-nf-cn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的完整配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ========== 字体设置 ==========
# 主字体：Maple Mono NF CN
font-family = Maple Mono NF CN
font-size = 14
# 行高设置（Maple Font 官方推荐 1.8 行高，实测最佳值为 30%）
adjust-cell-height = 25%
# 启用 Maple Mono 连字和字体特性（官方推荐配置）
# calt: 基本连字 | zero: 带点的零 | cv01: 去除间隙
# ss01: 分离等号 | ss02: 分离比较符 | ss07: 强制 &amp;gt;&amp;gt; 连字 | ss08: 双箭头
font-feature = calt, cv01, cv03, ss01, ss02, ss03
# 字体粗细（让字体更粗，类似 Tabby 600/700 配置）
font-thicken = true

# ========== 主题设置 ==========
theme = GitHub Dark Dimmed

# ========== 窗口尺寸 ==========
# 设置窗口初始大小（以字符为单位）
window-width = 125
window-height = 35
# 记住窗口位置和大小
window-save-state = always

# ========== 光标设置 ==========
# 光标样式：block（粗方块光标，类似 Tabby）
cursor-style = bar
# 光标闪烁
cursor-style-blink = true
# 光标颜色（使用明亮的橙色，更醒目）
cursor-color = #ff9e64
# 光标下的文本颜色
cursor-text = #1a1b26

# ========== 窗口外观 ==========
# 窗口内边距
window-padding-x = 8
window-padding-y = 8
# 窗口装饰样式（macOS）- tabs 样式支持标签栏
macos-titlebar-style = tabs
# 保持窗口阴影
macos-window-shadow = true

# ========== 其他优化 ==========
# 背景略微透明（可选，1.0 为完全不透明）
background-opacity = 0.97
# 滚动缓冲区大小（单位：字节）
# 适合 Claude Code 等长对话应用
scrollback-limit = 200000000
# 在输入时隐藏鼠标
mouse-hide-while-typing = true

# ========== Shell Integration ==========
# 启用 shell 集成，但明确禁用 cursor 特性（避免覆盖我们的光标设置）
shell-integration = zsh
shell-integration-features = no-cursor,title
keybind = shift+enter=text:\x1b\r
keybind = ctrl+enter=text:\\\r
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;主题&lt;/h2&gt;
&lt;p&gt;Ghostty 内置大量主题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ghostty +list-themes
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;theme = tokyonight
theme = catppuccin-mocha
theme = dracula
theme = nord
theme = GitHub Dark Dimmed  # 我在用的
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自定义主题可放在 &lt;code&gt;~/.config/ghostty/themes/&lt;/code&gt; 目录下。&lt;/p&gt;
&lt;h2&gt;迁移调整&lt;/h2&gt;
&lt;p&gt;Warp 开箱即用，Ghostty 需要手动配置。但这也意味着更高的可控性。&lt;/p&gt;
&lt;h3&gt;命令行增强&lt;/h3&gt;
&lt;p&gt;Warp 内置了历史补全、语法高亮。在 Ghostty 中通过 zsh 插件实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install zsh-autosuggestions zsh-syntax-highlighting zsh-completions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;~/.zshrc&lt;/code&gt; 中添加以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -----------------------
# Homebrew Zsh completions (must be BEFORE oh-my-zsh / compinit)
# -----------------------
if type brew &amp;amp;&amp;gt;/dev/null; then
  export BREW_PREFIX=&quot;$(brew --prefix)&quot;
  FPATH=&quot;$BREW_PREFIX/share/zsh-completions:$FPATH&quot;
  FPATH=&quot;$BREW_PREFIX/share/zsh/site-functions:$FPATH&quot;
fi

# -----------------------
# Zsh plugins (Homebrew)
# -----------------------

# zsh-autosuggestions - 根据历史命令自动补全建议（灰色提示）
if [[ -n &quot;$BREW_PREFIX&quot; &amp;amp;&amp;amp; -f &quot;$BREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh&quot; ]]; then
  source &quot;$BREW_PREFIX/share/zsh-autosuggestions/zsh-autosuggestions.zsh&quot;
fi

# zsh-syntax-highlighting - 命令语法高亮（必须放在最后）
if [[ -n &quot;$BREW_PREFIX&quot; &amp;amp;&amp;amp; -f &quot;$BREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh&quot; ]]; then
  source &quot;$BREW_PREFIX/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh&quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
如果使用 Oh My Zsh，&lt;strong&gt;不要&lt;/strong&gt;将 &lt;code&gt;zsh-autosuggestions&lt;/code&gt; 和 &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt; 放入 &lt;code&gt;plugins=()&lt;/code&gt; 中，否则会与 Homebrew 安装的版本重复加载。
:::&lt;/p&gt;
&lt;p&gt;这三个插件分别提供：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;zsh-autosuggestions&lt;/strong&gt;：历史命令补全（按 &lt;code&gt;→&lt;/code&gt; 确认）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zsh-syntax-highlighting&lt;/strong&gt;：语法高亮，错误命令显示红色&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zsh-completions&lt;/strong&gt;：增强的 Tab 补全&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;提示符主题 - Spaceship&lt;/h3&gt;
&lt;p&gt;Warp 有漂亮的内置提示符，Ghostty 中可以用 &lt;a href=&quot;https://spaceship-prompt.sh/&quot;&gt;Spaceship&lt;/a&gt; 实现类似效果。&lt;/p&gt;
&lt;h4&gt;1. 安装 Oh My Zsh（前置条件）&lt;/h4&gt;
&lt;p&gt;Spaceship 依赖 Oh My Zsh 框架，需先安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
安装脚本会将原有 &lt;code&gt;~/.zshrc&lt;/code&gt; 备份为 &lt;code&gt;~/.zshrc.pre-oh-my-zsh&lt;/code&gt;，并生成新的配置文件。安装完成后需手动将原配置迁移到新文件中。
:::&lt;/p&gt;
&lt;h4&gt;2. 安装 Spaceship 主题&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/spaceship-prompt/spaceship-prompt.git \
  &quot;${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/spaceship-prompt&quot; --depth=1

# 创建符号链接
ln -sf \
  &quot;${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/spaceship-prompt/spaceship.zsh-theme&quot; \
  &quot;${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/spaceship.zsh-theme&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;3. 启用主题&lt;/h4&gt;
&lt;p&gt;Oh My Zsh 安装后会自动生成 &lt;code&gt;~/.zshrc&lt;/code&gt;，找到 &lt;code&gt;ZSH_THEME&lt;/code&gt; 那一行，修改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export ZSH=&quot;$HOME/.oh-my-zsh&quot;
ZSH_THEME=&quot;spaceship&quot;
source &quot;$ZSH/oh-my-zsh.sh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spaceship 会显示 Git 分支、Node 版本、执行时间等上下文信息，非常适合开发者使用。&lt;/p&gt;
&lt;h3&gt;历史命令搜索 - Atuin&lt;/h3&gt;
&lt;p&gt;Warp 的历史命令弹出列表很方便，&lt;a href=&quot;https://github.com/atuinsh/atuin&quot;&gt;Atuin&lt;/a&gt; 是更强大的替代品：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install atuin

# 初始化（添加到 ~/.zshrc）
eval &quot;$(atuin init zsh)&quot;

# 导入现有的 shell 历史记录
atuin import auto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Atuin 配置文件位于 &lt;code&gt;~/.config/atuin/config.toml&lt;/code&gt;，我的配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ↑ 键触发的过滤范围：全局（所有目录/会话/主机）
filter_mode_shell_up_key_binding = &quot;global&quot;

# ↑ 键触发的搜索模式：前缀匹配（适合 ssh、git 等命令）
search_mode_shell_up_key_binding = &quot;prefix&quot;

# 紧凑样式
style = &quot;compact&quot;

# 内联高度（行数）
inline_height = 14

# 关闭预览（命令太长时的预览）
show_preview = false

# 关闭帮助行和标签栏，界面更简洁
show_help = false
show_tabs = false

# 回车直接执行命令（按 Tab 可编辑）
enter_accept = true

[sync]
# 启用 sync v2
records = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
配置完成后，按 &lt;code&gt;↑&lt;/code&gt; 键即可弹出历史命令列表，输入关键字即可快速筛选。
:::&lt;/p&gt;
&lt;p&gt;Atuin 的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全文搜索&lt;/strong&gt;：&lt;code&gt;Ctrl+R&lt;/code&gt; 弹出交互式搜索&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨设备同步&lt;/strong&gt;：可选云端同步&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统计分析&lt;/strong&gt;：命令使用频率统计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文感知&lt;/strong&gt;：按目录、会话筛选&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./atuin-ui.png&quot; alt=&quot;Atuin 界面示意&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;对比&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;Warp&lt;/th&gt;
&lt;th&gt;Ghostty&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;启动速度&lt;/td&gt;
&lt;td&gt;2-3 秒&lt;/td&gt;
&lt;td&gt;&amp;lt; 0.5 秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内存占用&lt;/td&gt;
&lt;td&gt;~600 MB&lt;/td&gt;
&lt;td&gt;~100 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开源&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要账号&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;内置 AI&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;块编辑&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自定义程度&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;快捷键&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;快捷键&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;水平分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+D&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;垂直分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+D&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+[&lt;/code&gt; / &lt;code&gt;Cmd+]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;关闭分屏&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+W&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开配置&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd+Shift+,&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::tip
修改配置文件后 Ghostty 会自动重载，无需重启。
:::&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Warp 像一辆带自动驾驶的车；Ghostty 更像一台手动挡。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;更累，但我终于知道每个动作是谁做的。&lt;/p&gt;
</content:encoded></item><item><title>让等待可见：起名场景的流式输出优化实践</title><link>https://www.mihouo.com/posts/front/naming_streaming_article/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/naming_streaming_article/</guid><description>通过流式输出与渐进渲染，让用户从&quot;干等&quot;变成&quot;边看边等&quot;</description><pubDate>Sun, 21 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;功能背景&lt;/h2&gt;
&lt;p&gt;用户在小程序里填好宝宝信息（姓氏、性别、出生时间），点击&quot;开始智能起名&quot;，期望快速拿到一组&lt;strong&gt;可直接参考、解释清晰&lt;/strong&gt;的名字建议。&lt;/p&gt;
&lt;p&gt;但起名不是简单地&quot;吐出几个字&quot;。为了让建议更可信、更方便用户决策，页面需要展示多个模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;八字分析与取名方向&lt;/strong&gt;：先告诉用户整体思路&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多个名字建议&lt;/strong&gt;：通常 5~10 个&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每个名字的详细解释&lt;/strong&gt;：寓意、五行补益、读音搭配、综合评分等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内容越完整，用户越好做决定——但生成时间也越长。&lt;/p&gt;
&lt;h2&gt;原方案的问题&lt;/h2&gt;
&lt;p&gt;优化前的交付方式是&quot;一次性整包返回&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户点击&quot;开始起名&quot;&lt;/li&gt;
&lt;li&gt;页面进入 loading 状态&lt;/li&gt;
&lt;li&gt;后端调用大模型生成&lt;strong&gt;完整的结构化 JSON&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;等 JSON 全部生成完毕后，一次性返回前端&lt;/li&gt;
&lt;li&gt;页面拿到完整结果后，一次性渲染所有模块&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;工程上很直观，但用户体验上把整个过程变成了黑盒。&lt;/p&gt;
&lt;h3&gt;用户的真实感受&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;用户不是怕慢，而是怕&quot;什么都看不到的慢&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在旧流程里，用户只能看到一个&quot;加载中&quot;——没有进展、没有反馈、不知道还要等多久。&lt;/p&gt;
&lt;p&gt;这导致了几个典型问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;怀疑卡住&lt;/strong&gt;：反复点击按钮，产生重复请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提前放弃&lt;/strong&gt;：看不到希望，直接退出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;感知放大&lt;/strong&gt;：即使后端已经在生成，用户也完全不知道&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问题本质&lt;/h3&gt;
&lt;p&gt;不是内容太多，而是交付方式太&quot;整包&quot;。&lt;/p&gt;
&lt;p&gt;打个比方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;旧方案像&lt;strong&gt;交作业&lt;/strong&gt;：必须写完整篇论文才能交&lt;/li&gt;
&lt;li&gt;用户体验像&lt;strong&gt;等快递&lt;/strong&gt;：物流不更新，完全不知道进度&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们想要的是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;起名可以像&quot;直播&quot;一样，生成一点就展示一点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;优化目标&lt;/h2&gt;
&lt;p&gt;聚焦起名场景，我们定了三个核心目标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更早出现有价值的内容&lt;/strong&gt;：优先展示八字分析，而不是空等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;名字逐条出现&lt;/strong&gt;：边看边等，不必等全部生成完&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;明确的进度反馈&lt;/strong&gt;：避免用户误以为卡住或已结束&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;目标不是&quot;让模型变快&quot;，而是让等待从&quot;黑盒&quot;变成&quot;有反馈、有进展&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;方案设计&lt;/h2&gt;
&lt;h3&gt;整体思路：分段输出 + 渐进渲染&lt;/h3&gt;
&lt;p&gt;把起名结果从&quot;单次大返回&quot;改成&quot;多阶段流式输出&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  八字分析   │ ──▶ │  名字 1     │ ──▶ │  名字 2     │ ──▶ ...
│  (先输出)   │     │  (追加)     │     │  (追加)     │
└─────────────┘     └─────────────┘     └─────────────┘
      ↓                   ↓                   ↓
   用户开始阅读       结果在增长           持续有反馈
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;阶段 1：先输出八字分析&lt;/h3&gt;
&lt;p&gt;八字分析是用户最想先看到的&quot;方向性信息&quot;。它一出现，用户就能开始阅读，并对接下来的名字建议建立期待。&lt;/p&gt;
&lt;h3&gt;阶段 2：名字逐条追加&lt;/h3&gt;
&lt;p&gt;名字建议不需要等全部生成完再展示。每生成一条，就在页面上追加一条，让结果持续&quot;增长&quot;。&lt;/p&gt;
&lt;h3&gt;阶段 3：加载中提示卡片&lt;/h3&gt;
&lt;p&gt;当用户看到八字分析和 1~2 个名字后，最容易产生新疑问：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;怎么不动了？是不是卡住了？&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;因此我们增加了一个明确的提示模块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当已出现部分结果、但系统仍在生成时&lt;/li&gt;
&lt;li&gt;页面展示&quot;剩余模块正在加载&quot;的卡片&lt;/li&gt;
&lt;li&gt;卡片包含进度提示，如&quot;已生成 2/5 个名字，更多内容加载中…&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这张卡片的价值不是&quot;加个转圈&quot;，而是给用户稳定的心理预期：&lt;strong&gt;结果还在来，我可以先看已有内容&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;技术实现要点&lt;/h2&gt;
&lt;h3&gt;通信协议：SSE（Server-Sent Events）&lt;/h3&gt;
&lt;p&gt;我们选择 SSE 而非 WebSocket，原因是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;起名是&lt;strong&gt;单向推送&lt;/strong&gt;场景，不需要双向通信&lt;/li&gt;
&lt;li&gt;SSE 基于 HTTP，实现简单、兼容性好&lt;/li&gt;
&lt;li&gt;自动重连机制，对弱网更友好&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;基本的数据格式设计：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 每个 SSE 事件的 payload 结构
interface StreamChunk {
  type: &apos;bazi&apos; | &apos;name&apos; | &apos;done&apos; | &apos;error&apos;;
  data: BaziAnalysis | NameSuggestion | null;
  progress?: {
    current: number;
    total: number;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;前端状态管理&lt;/h3&gt;
&lt;p&gt;使用状态机管理整个流式过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type StreamState =
  | { status: &apos;idle&apos; }
  | { status: &apos;loading&apos;; bazi: null; names: [] }
  | { status: &apos;streaming&apos;; bazi: BaziAnalysis; names: NameSuggestion[] }
  | { status: &apos;done&apos;; bazi: BaziAnalysis; names: NameSuggestion[] }
  | { status: &apos;error&apos;; error: string };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心处理逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const eventSource = new EventSource(&apos;/api/naming/stream&apos;);

eventSource.onmessage = (event) =&amp;gt; {
  const chunk: StreamChunk = JSON.parse(event.data);

  switch (chunk.type) {
    case &apos;bazi&apos;:
      setState(prev =&amp;gt; ({ ...prev, bazi: chunk.data }));
      break;
    case &apos;name&apos;:
      setState(prev =&amp;gt; ({
        ...prev,
        names: [...prev.names, chunk.data],
        progress: chunk.progress
      }));
      break;
    case &apos;done&apos;:
      setState(prev =&amp;gt; ({ ...prev, status: &apos;done&apos; }));
      eventSource.close();
      break;
    case &apos;error&apos;:
      setState({ status: &apos;error&apos;, error: chunk.data });
      eventSource.close();
      break;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;渲染优化&lt;/h3&gt;
&lt;p&gt;为了让追加动画更流畅，名字卡片使用了简单的入场动画：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.name-card {
  animation: fadeInUp 0.3s ease-out;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;边界情况处理&lt;/h2&gt;
&lt;h3&gt;网络中断&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;SSE 自带重连机制，断开后会自动尝试重连&lt;/li&gt;
&lt;li&gt;前端记录已接收的 &lt;code&gt;progress.current&lt;/code&gt;，重连时带上偏移量&lt;/li&gt;
&lt;li&gt;后端支持断点续传，避免重复生成&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;生成失败&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;单个名字生成失败：跳过该条，继续生成下一个，最终告知用户&quot;部分内容生成失败&quot;&lt;/li&gt;
&lt;li&gt;整体失败：展示错误提示 + 重试按钮，已展示的内容保留&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;用户中途离开&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;页面隐藏时暂停渲染（使用 &lt;code&gt;visibilitychange&lt;/code&gt; 事件）&lt;/li&gt;
&lt;li&gt;返回时恢复，避免后台持续消耗资源&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优化效果&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指标&lt;/th&gt;
&lt;th&gt;优化前&lt;/th&gt;
&lt;th&gt;优化后&lt;/th&gt;
&lt;th&gt;变化&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;首屏内容出现时间&lt;/td&gt;
&lt;td&gt;~8s&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;td&gt;-75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户等待期间跳出率&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;-67%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;重复点击率（误操作）&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;-81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用户满意度评分&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;td&gt;+18%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;数据来源：优化上线后两周的埋点统计&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;适用场景延伸&lt;/h2&gt;
&lt;p&gt;这套&quot;流式输出 + 渐进渲染&quot;的模式，适用于所有&lt;strong&gt;内容长、生成慢、可拆分&lt;/strong&gt;的 AI 场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI 报告生成&lt;/strong&gt;：体检报告、财务分析、竞品调研&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长文本创作&lt;/strong&gt;：营销文案、文章大纲、多轮对话摘要&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多步骤任务&lt;/strong&gt;：表单智能填充、代码生成、翻译校对&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索增强&lt;/strong&gt;：RAG 场景下的多源结果聚合&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;核心判断标准：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结果是否可以拆成多个独立模块？&lt;/li&gt;
&lt;li&gt;用户是否可以&quot;边看边等&quot;？&lt;/li&gt;
&lt;li&gt;先出现的内容是否有独立价值？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果三个答案都是&quot;是&quot;，就值得考虑流式输出。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;起名这类&quot;内容长、结构化强&quot;的 AI 能力，真正影响体验的往往不是&quot;最终花多少秒&quot;，而是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户在&lt;strong&gt;前几秒&lt;/strong&gt;能不能看到价值&lt;/li&gt;
&lt;li&gt;系统有没有把&lt;strong&gt;进度讲清楚&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;页面在生成过程中是不是仍然&lt;strong&gt;可用&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把起名改成流式展示，本质上是在做一件事：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;把不可见的等待，变成可见的进展。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;扫码体验&lt;/h2&gt;
&lt;p&gt;想亲自感受流式输出的效果？扫描下方二维码，立即体验智能起名功能：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./qr-code.jpg&quot; alt=&quot;扫码体验智能起名&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>氛围编程（Vibe Coding）入门指南：让 AI 帮你写代码</title><link>https://www.mihouo.com/posts/ai/claude-code-cli-guide/</link><guid isPermaLink="true">https://www.mihouo.com/posts/ai/claude-code-cli-guide/</guid><description>什么是氛围编程？如何零基础上手 AI 编程助手？本文以 Claude Code CLI 为例，手把手教你安装配置，并展示真实项目中的使用效果。</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一、什么是氛围编程？&lt;/h2&gt;
&lt;h3&gt;一个新概念的诞生&lt;/h3&gt;
&lt;p&gt;2025 年 2 月，&lt;strong&gt;Andrej Karpathy&lt;/strong&gt;（前特斯拉 AI 总监、OpenAI 联合创始人）在社交媒体上提出了一个新词：&lt;strong&gt;氛围编程（Vibe Coding）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;他这样描述：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;你完全沉浸在氛围中，拥抱指数级增长，忘掉代码的存在。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;听起来有点玄乎？其实很简单：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;氛围编程 = 用自然语言告诉 AI 你想要什么，让它帮你写代码。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你不再需要一行一行敲代码，而是像和同事沟通一样，描述清楚需求，AI 就能帮你实现。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;AI 编程工具的演进&lt;/h3&gt;
&lt;p&gt;在正式开始之前，我们先了解一下目前市面上的 AI 编程工具。它们大致可以分为三类：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🌐 第一类：网页对话型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代表：DeepSeek、豆包、ChatGPT&lt;/li&gt;
&lt;li&gt;特点：像一位&quot;博学的顾问&quot;，你问它答&lt;/li&gt;
&lt;li&gt;局限：它看不到你的代码，你需要手动复制粘贴&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;🔌 第二类：IDE 插件型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代表：通义灵码、GitHub Copilot、Cursor&lt;/li&gt;
&lt;li&gt;特点：像一位&quot;贴心的副驾&quot;，能在编辑器里帮你补全代码&lt;/li&gt;
&lt;li&gt;局限：视野局限于当前文件，无法理解整个项目&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;🤖 第三类：AI Agent 型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代表：Claude Code、Codex CLI、Gemini CLI&lt;/li&gt;
&lt;li&gt;特点：像一位&quot;全能的开发者&quot;，能理解整个项目并直接操作文件&lt;/li&gt;
&lt;li&gt;局限：需要一点命令行基础（但别担心，本文会教你）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;本文要介绍的就是第三类 —— AI Agent。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;什么是 Claude Code？&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code&lt;/a&gt; 是 Anthropic 公司（Claude 模型的研发方）推出的&lt;strong&gt;命令行 AI 编程助手&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;打个比方&lt;/strong&gt;：如果把 DeepSeek 比作&quot;只能动嘴的军师&quot;，那么 &lt;strong&gt;Claude Code 就是&quot;既能动脑又能动手的全能助理&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它不仅能思考和回答问题，还能直接操作你的项目（当然，每次操作前都会征求你的同意）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🔍 &lt;strong&gt;读取文件&lt;/strong&gt;：自动分析整个项目结构，比如&quot;你的 React 项目有哪些组件&quot;&lt;/li&gt;
&lt;li&gt;✏️ &lt;strong&gt;编辑文件&lt;/strong&gt;：直接修改代码并保存，不用你手动复制粘贴&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;运行命令&lt;/strong&gt;：执行 &lt;code&gt;npm test&lt;/code&gt;、&lt;code&gt;git commit&lt;/code&gt; 等命令，帮你验证代码&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;为什么是命令行？&lt;/h3&gt;
&lt;p&gt;你可能会问：&quot;命令行黑乎乎的，看起来好复杂，为什么不做成图形界面？&quot;&lt;/p&gt;
&lt;p&gt;这是个好问题。在 AI 编程场景下，&lt;strong&gt;命令行反而是最高效的选择&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;直接调用开发工具&lt;/strong&gt;：Git、npm、测试框架本身就在命令行运行，AI 可以直接使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局视野&lt;/strong&gt;：IDE 插件只能看当前文件，命令行工具能分析整个项目&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;并行工作&lt;/strong&gt;：你可以在 IDE 里写代码，同时让终端里的 AI 帮你写测试&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;我的真实体验&lt;/h3&gt;
&lt;p&gt;作为一名前端开发，我这段时间一直在使用 AI Agent 工具。&lt;/p&gt;
&lt;p&gt;目前已经基本实现：&lt;strong&gt;100% 代码由 AI 生成，我只负责描述需求 + 验收测试&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;下图是一个完全由 AI 生成的 BI 即席分析页面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bi-adhoc-analysis.png&quot; alt=&quot;BI 即席分析页面 —— 完全由 AI 生成&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;二、手把手安装配置&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 &lt;strong&gt;目标&lt;/strong&gt;：完成本章后，你就能在终端里和 AI 对话写代码了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;第一步：安装 Claude Code&lt;/h3&gt;
&lt;p&gt;打开你的终端（macOS 按 &lt;code&gt;Cmd + 空格&lt;/code&gt; 搜索&quot;终端&quot;，Windows 搜索&quot;PowerShell&quot;），输入以下命令：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS 用户：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://claude.ai/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Windows 用户（PowerShell）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;irm https://claude.ai/install.ps1 | iex
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;通过 npm 安装（推荐前端开发者）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;小提示&lt;/strong&gt;：如果你是前端开发，电脑上通常已经安装了 Node.js，直接用 npm 安装最省事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;第二步：选择 AI 模型&lt;/h3&gt;
&lt;p&gt;安装完成后，你需要告诉 Claude Code 使用哪个 AI 模型。这里有两条路：&lt;/p&gt;
&lt;h4&gt;方案 A：官方订阅&lt;/h4&gt;
&lt;p&gt;Claude 官方提供 Max 订阅服务，每月 $200 美元，可以直接使用最强的 Claude 模型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./claude-pricing.png&quot; alt=&quot;Claude 官方定价&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;提醒&lt;/strong&gt;：国内用户可能无法直接访问 Anthropic 官方服务，需要特殊网络环境。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;方案 B：接入第三方模型（推荐国内用户）&lt;/h4&gt;
&lt;p&gt;好消息是 Claude Code 是开源的，支持接入国产大模型！以下是几个不错的选择：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔥 清华智谱 GLM-4.6&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官网：&lt;a href=&quot;https://bigmodel.cn/&quot;&gt;bigmodel.cn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;特点：国产模型，中文理解能力强&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;💰 MiniMax M2&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官网：&lt;a href=&quot;https://www.minimaxi.com/&quot;&gt;minimaxi.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;特点：性价比高，接入简单&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;🌋 火山引擎 doubao-seed-code&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官网：&lt;a href=&quot;https://www.volcengine.com/&quot;&gt;volcengine.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;特点：字节跳动旗下，适合代码生成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然这些模型与顶级模型（Claude Opus 4.5、GPT-5.1）有差距，但&lt;strong&gt;价格便宜、访问稳定&lt;/strong&gt;，非常适合入门学习。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;第三步：配置第三方模型&lt;/h3&gt;
&lt;p&gt;下面以 &lt;strong&gt;MiniMax M2&lt;/strong&gt; 为例，手把手教你配置：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1️⃣ 获取 API Key&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 &lt;a href=&quot;https://platform.minimaxi.com/&quot;&gt;MiniMax 开放平台&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;注册并登录账号&lt;/li&gt;
&lt;li&gt;进入控制台，创建一个 API Key（一串类似密码的字符串）&lt;/li&gt;
&lt;li&gt;复制保存好这个 Key&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;2️⃣ 创建配置文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在你的用户目录下，创建或编辑 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;alwaysThinkingEnabled&quot;: true,
  &quot;env&quot;: {
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;把你的API Key粘贴到这里&quot;,
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;https://api.minimaxi.com/anthropic&quot;,
    &quot;ANTHROPIC_MODEL&quot;: &quot;MiniMax-M2&quot;,
    &quot;API_TIMEOUT_MS&quot;: &quot;3000000&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;路径说明&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS：&lt;code&gt;~&lt;/code&gt; 就是 &lt;code&gt;/Users/你的用户名&lt;/code&gt;，完整路径是 &lt;code&gt;/Users/你的用户名/.claude/settings.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Windows：完整路径是 &lt;code&gt;C:\Users\你的用户名\.claude\settings.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;第四步：创建系统提示词（进阶配置）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这一步是可选的，但&lt;strong&gt;强烈推荐&lt;/strong&gt;。系统提示词可以让 AI 按照你的习惯工作，比如用中文回复、遵循特定的代码规范等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建 &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 文件，写入你的个性化指令。&lt;/p&gt;
&lt;p&gt;下面是我自己在用的配置（你可以直接复制使用）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;### 🌏 语言规范
Always respond in Chinese-simplified

1. 只允许使用简体中文回答 - 所有思考、分析、解释和回答都必须使用简体中文
2. 中文优先 - 优先使用中文术语、表达方式和命名规范
3. 中文注释 - 生成的代码注释和文档都应使用中文
4. 中文思维 - 思考过程和逻辑分析都使用中文进行

### 🎯 基本原则（不可违反）

1. **质量第一**：代码质量和系统安全不可妥协
2. **思考先行**：编码前必须深度分析和规划
3. **工具优先**：优先使用验证过的最佳工具链
4. **透明记录**：关键决策和变更必须可追溯
5. **持续改进**：从每次执行中学习和优化
6. **结果导向**：以目标达成为最终评判标准

---

## 📊 质量标准

### 🏗️ 工程原则

- **架构设计**：遵循 SOLID、DRY、关注点分离、YAGNI（精益求精）
- **代码质量**：
  - 清晰命名、合理抽象
  - 必要的中文注释（关键流程、核心逻辑、重点难点）
  - 删除无用代码，修改功能不保留旧的兼容性代码
  - 若无显式要求不要编写任何兼容代码
- **完整实现**：禁止 MVP/占位/TODO，必须完整可运行

&amp;lt;plan_tool_usage&amp;gt;
- 对于中等规模或更大规模的任务（例如：多文件修改、添加新的接口/命令行工具（CLI）功能，或进行多步骤的调查），在执行任何代码或工具操作之前，必须在 TODO/plan 工具中创建并维护一个详细的计划。
- 创建 2-5 个里程碑或目标项；避免包含过于细小的步骤或重复性的操作任务（例如：“打开文件”、“运行测试”等）。切勿使用像“实现整个功能”这样的笼统描述。
- 在工具中维护任务的状态：任何时候只能有一个任务处于 “in_progress”（进行中）状态；任务完成后请将其标记为 “completed”（已完成）；及时更新任务状态（切勿连续多次调用工具而不进行任何更新）。切勿直接将任务状态从 “pending”（待处理）变为 “completed”：必须先将其设置为 “in_progress”（如果任务确实可以立即完成，可以在同一条更新中同时将其标记为 “in_progress” 和 “completed”）。切勿事后批量完成多个任务。
- 在任务结束时，确保所有任务要么已完成，要么已被明确取消或推迟。
- 任务结束时的基本规则：所有任务的状态应为 “in_progress” 或 “pending” 均为零；对于未完成的任务，需明确说明其原因并完成它们或取消/推迟它们。
- 如果你需要通过聊天来说明一个中等复杂性的任务计划，请将该计划同步到 TODO/plan 工具中，并在后续更新中引用这些任务项。
- 对于非常简单、耗时较短的任务（例如：修改单个文件，代码量不超过 10 行），你可以省略使用该工具。如果仍然需要通过聊天来说明任务计划，只需用 1-2 句话简洁地描述任务目标，无需包含具体的操作步骤或详细的任务清单。
- 在进行任何非琐碎的代码修改之前（例如：应用补丁、多文件编辑或进行复杂的系统配置），请确保当前计划中有一个与你要执行的操作相匹配的任务被标记为 “in_progress”；如有必要，请先更新计划。
- 如果任务范围发生变化（例如：需要拆分、合并或重新排列任务项），请在继续执行之前更新计划。切勿在编码过程中让计划状态变得过时。
- 任何时候都只能有一个任务处于 “in_progress” 状态；如果出现多个任务同时处于 “inProgress” 状态，请立即调整状态，确保只有当前阶段的任务处于进行中。
&amp;lt;plan_tool_usage&amp;gt;

### ⚡ 性能标准

- **算法意识**：考虑时间复杂度和空间复杂度
- **资源管理**：优化内存使用和 IO 操作
- **边界处理**：处理异常情况和边界条件

### 🧪 测试要求

- **测试驱动**：可测试设计，单元测试覆盖,后台执行单元测试时，最大不能超过 60s，避免任务卡死。
- **质量保证**：静态检查、格式化、代码审查
- **持续验证**：自动化测试和集成验证

---

## 🛠️ 工具使用指南

### 🔍 代码分析

- **首选**：`Serena`符号工具（`get_symbols_overview` → `find_symbol`）
- **备选**：`Read` + `Grep` + `Glob`组合
- **降级**：直接文件读取（需记录决策依据）

### 📚 知识查询

- **技术文档**：`Context7`（先 `resolve-library-id` 后 `get-library-docs`）
- **网页搜索**：`extra`
- **GitHub 文档**：`DeepWiki`

### 💭 分析规划

- **深度思考**：`Sequential-Thinking`（规划前必须执行）
- **知识管理**：`Memory`（读取约束，存储决策）

### 🔧 命令执行标准

**路径处理：**

- 始终使用双引号包裹文件路径
- 优先使用正斜杠 `/` 作为路径分隔符
- 确保跨平台兼容性

**工具优先级：**

1. `rg` (ripgrep) &amp;gt; `grep` 用于内容搜索
2. 专用工具 (Read/Write/Edit) &amp;gt; 系统命令
3. 批量工具调用提高效率

---

## ⚠️ 危险操作确认机制

### 🚨 高风险操作清单

执行以下操作前**必须获得明确确认**：

- **文件系统**：删除文件/目录、批量修改、移动系统文件
- **代码提交**：`git commit`、`git push`、`git reset --hard`
- **系统配置**：修改环境变量、系统设置、权限变更
- **数据操作**：数据库删除、结构变更、批量更新
- **网络请求**：发送敏感数据、调用生产环境 API
- **包管理**：全局安装/卸载、更新核心依赖

### 📝 确认格式模板

---
⚠️ 危险操作检测！
操作类型：[具体操作]
影响范围：[详细说明]
风险评估：[潜在后果]

请确认是否继续？[需要明确的&quot;是&quot;、&quot;确认&quot;、&quot;继续&quot;]
---

---

## ✅ 关键检查点

### 🚀 任务开始

**尽量并行化工具调用；同时采用批量读取（read_file）和批量修改（apply_patch）的方式来加速整个处理过程。**

- [ ] 读取相关 Memory，回显关键约束
- [ ] 根据任务特征选择适配策略
- [ ] 确认工具可用性和降级方案

### 💻 编码前

- [ ] 完成 `Sequential-Thinking` 分析
- [ ] 使用`Serena`等工具理解现有代码
- [ ] 制定实施计划和质量标准

### 🔍 实施中

- [ ] 遵循选定的质量标准
- [ ] 记录重要决策和变更理由
- [ ] 及时处理异常和边界情况

### ✨ 完成后

- [ ] 验证功能正确性和代码质量
- [ ] 更新相关测试和文档
- [ ] 总结经验，更新 Memory 和最佳实践

---

## 🎨 终端输出风格指南

### 💬 语言与语气

- **友好自然**：像专业朋友对话，避免生硬书面语
- **适度点缀**：在标题或要点前使用 🎯✨💡⚠️🔍 等 emoji 强化视觉引导
- **直击重点**：开篇用一句话概括核心思路（尤其对复杂问题）

---

### 📐 内容组织与结构

- **层次分明**：用标题、子标题划分内容层级，长内容分节展示
- **要点清晰**：将长段落拆分为短句或条目，每点聚焦一个 idea
- **逻辑流畅**：多步骤任务用有序列表（1. 2. 3.），并列项用无序列表（- 或 *）
- **合理分隔**：不同信息块之间用空行或 `---` 分隔，提升可读性

&amp;gt; ❌ 避免在终端中使用复杂表格（尤其内容长、含代码或需连贯叙述时）

---

### 🎯 视觉与排版优化

- **简洁明了**：控制单行长度，适配终端宽度（建议 ≤80 字符）
- **适当留白**：合理使用空行，避免信息拥挤
- **对齐一致**：统一缩进与符号风格（如统一用 `-` 而非混用 `*`）
- **重点突出**：关键信息用 **粗体** 或 *斜体* 强调

---

### 🧩 技术内容规范

#### 代码与数据展示

- **代码块**：多行代码、配置或日志务必用带语言标识的 Markdown 代码块（如 ```python）
- **聚焦核心**：示例代码省略无关部分（如导入语句），突出关键逻辑
- **差异标记**：修改内容用 `+` / `-` 标注，便于快速识别变更
- **行号辅助**：必要时添加行号（如调试场景）

#### 结构化数据

- **优先列表**：大多数场景用列表替代表格
- **慎用表格**：仅当需严格对齐结构化数据（如参数对比）时使用 Markdown 表格

---

### 🚀 交互与用户体验

- **即时反馈**：快速响应，避免长时间无输出
- **状态可见**：重要操作显示进度或当前状态（如“正在处理…”）
- **错误友好**：清晰说明错误原因，并提供可操作的解决建议
- **引导下一步**：结尾给出实用建议、行动指南或鼓励进一步提问

---



### ✅ 输出结尾建议

- 复杂内容后附**简短总结**，重申核心要点
- **最终答案的编写规范：**  
- **对于微小的修改（单个文件，修改内容不超过10行）：** 仅需使用2–5句话或3个要点进行描述，无需使用标题；除非必要，否则不要添加任何简短的代码片段（不超过3行）。  
- **对于中等规模的修改（涉及单个文件或少量文件）：** 使用不超过6个要点进行描述，总共可以使用6–10句话；最多只能添加1–2个简短的代码片段（每个片段不超过8行）。  
- **对于大规模的修改（涉及多个文件）：** 应分别对每个文件进行总结，每个文件使用1–2个要点进行描述；除非代码非常关键，否则避免直接在最终答案中嵌入代码片段。  
- **禁止在最终答案中包含“修改前/修改后”的对比内容、完整的方法体，或过长（需要滚动才能查看的）代码块；建议使用文件名或符号名称来引用相关内容。**  
- **除非用户明确要求，或者修改过程会导致构建、代码检查（如yarn、tsc、eslint等）失败，否则不要在答案中提及这些过程或工具的使用情况。** 如果这些检查过程顺利完成（即没有产生任何错误或警告），则无需特别说明。  

**关于代码与格式的注意事项：**  
- 对于需要突出显示的代码片段，请使用等宽字体（monospace）进行显示；同时，请避免将代码片段与**符号（如**）混合使用。  
- **除非修改过程需要记录构建、代码检查的结果，或者这些步骤会影响到修改的顺利进行，否则不要在答案中提及相关的日志或工具信息。**  
- 对于简单的修改，只需简要说明修改的内容、涉及的位置以及修改的结果即可，无需进行复杂的总结。  
- **避免使用过多的代码注释或长篇的代码示例；建议通过引用文件名、符号或函数名称来说明修改的具体内容。**  
- **当代码示例能更直观地说明问题时，优先使用自然语言进行描述（即直接引用文件名、符号或函数名称）；只有在确实需要避免混淆的情况下，才允许使用代码片段，且每个代码片段的长度应控制在规定的范围内。**  
- **对于代码库中的代码片段：** 如果必须引用代码库中的代码，可以使用相应的引用格式，但在最终答案中应避免使用行号和文件路径作为前缀；每个代码片段的长度最多不超过1–2行。
---
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;第五步：安装 MCP 插件（可选进阶）&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;🔧 &lt;strong&gt;什么是 MCP？&lt;/strong&gt; MCP（Model Context Protocol）是一套插件协议，可以给 AI 扩展更多能力，比如操作浏览器、深度思考等。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你想让 Claude Code 更强大，可以安装以下插件（在终端中逐条执行）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. Chrome DevTools - 让 AI 能操作浏览器
claude mcp add --transport stdio --scope user chrome-devtools \
  -- npx chrome-devtools-mcp@latest

# 2. Sequential Thinking - 增强 AI 的深度思考能力
claude mcp add --transport stdio --scope user sequential-thinking \
  -- npx -y @modelcontextprotocol/server-sequential-thinking

# 3. Serena - 代码符号分析工具
claude mcp add --transport stdio --scope user serena \
  -- uvx --from git+https://github.com/oraios/serena \
     serena start-mcp-server --enable-web-dashboard false

# 4. Context7 - 技术文档查询
claude mcp add --transport stdio --scope user context7 \
  -- npx -y @upstash/context7-mcp
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 初学者可以先跳过这一步，等熟悉基本用法后再来扩展。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h3&gt;🎉 开始使用！&lt;/h3&gt;
&lt;p&gt;配置完成后，打开终端，进入你的项目目录，输入 &lt;code&gt;claude&lt;/code&gt; 即可启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /path/to/your/project
claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到这样的界面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./claude-code-home.png&quot; alt=&quot;Claude Code CLI 主页&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;试着问它一个问题&lt;/strong&gt;，比如&quot;这是一个什么项目？&quot;&lt;/p&gt;
&lt;p&gt;AI 会先思考你的意图，然后调用工具分析项目：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./claude-code-thinking.png&quot; alt=&quot;AI 正在思考和分析&quot; /&gt;&lt;/p&gt;
&lt;p&gt;分析完成后，它会给出清晰的总结：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./claude-code-result.png&quot; alt=&quot;分析结果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;恭喜你！🎊 现在你已经成功配置好了 AI 编程助手，可以开始氛围编程之旅了。&lt;/p&gt;
&lt;h2&gt;三、其他 AI 编程工具推荐&lt;/h2&gt;
&lt;p&gt;除了 Claude Code，市面上还有几款值得关注的 AI Agent 工具。根据你的开发方向，可以选择最适合的：&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🥇 Codex CLI（OpenAI 出品）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;官网&lt;/strong&gt;：&lt;a href=&quot;https://developers.openai.com/codex/cli/&quot;&gt;developers.openai.com/codex/cli&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：后端开发者&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI 官方出品，使用 GPT 系列模型&lt;/li&gt;
&lt;li&gt;遇到问题会先&quot;深度推理&quot;，反复推演边界情况&lt;/li&gt;
&lt;li&gt;特别适合逻辑复杂的后端场景&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;💰 低成本使用方式&lt;/strong&gt;：
目前 ChatGPT Team 有 0$/1$ 试用活动，可以同时使用 ChatGPT 官网和 Codex CLI。一个月成本约 10 元人民币，性价比极高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推荐指数&lt;/strong&gt;：⭐⭐⭐⭐⭐&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🤖 Droid CLI（Factory.ai 出品）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;官网&lt;/strong&gt;：&lt;a href=&quot;https://factory.ai/product/cli&quot;&gt;factory.ai/product/cli&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：需要多角色协作的复杂项目&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;理念独特：不是&quot;给你一个助手&quot;，而是&quot;给你一支军队&quot;&lt;/li&gt;
&lt;li&gt;将 AI 拆分为不同的&quot;专职机器人&quot;，模拟真实研发团队&lt;/li&gt;
&lt;li&gt;支持多种顶级模型（Claude、GPT、Gemini 等）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./droid-cli.png&quot; alt=&quot;Droid CLI 界面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💰 低成本使用方式&lt;/strong&gt;：
&lt;s&gt;注册送 4000 万 Token（约 108 美元）&lt;/s&gt;，目前活动已结束，价格有所上涨（约 6 元/1000 万 Token）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推荐指数&lt;/strong&gt;：⭐⭐⭐（活动结束后性价比下降）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🎨 Gemini CLI（Google 出品）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;官网&lt;/strong&gt;：&lt;a href=&quot;https://geminicli.com/&quot;&gt;geminicli.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;适合人群&lt;/strong&gt;：前端 UI 开发者&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心特点&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Google 官方出品，使用 Gemini 系列模型&lt;/li&gt;
&lt;li&gt;Gemini 3 PRO 对 UI/视觉类任务表现出色&lt;/li&gt;
&lt;li&gt;工具成熟度略逊于 Claude Code 和 Codex CLI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;💰 低成本使用方式&lt;/strong&gt;：
Google One 学生优惠，可免费使用一年 PRO 订阅。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;推荐指数&lt;/strong&gt;：⭐⭐⭐（仅推荐前端 UI 场景）&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;📊 工具对比总结&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;适合场景&lt;/th&gt;
&lt;th&gt;模型&lt;/th&gt;
&lt;th&gt;推荐度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;全栈开发&lt;/td&gt;
&lt;td&gt;Claude&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;后端开发&lt;/td&gt;
&lt;td&gt;GPT&lt;/td&gt;
&lt;td&gt;⭐⭐⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Droid CLI&lt;/td&gt;
&lt;td&gt;团队协作&lt;/td&gt;
&lt;td&gt;多模型&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;前端 UI&lt;/td&gt;
&lt;td&gt;Gemini&lt;/td&gt;
&lt;td&gt;⭐⭐⭐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;四、实战案例：AI 能做什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;🎯 这一章通过真实项目案例，展示 AI Agent 的各项能力。看完你就知道它能帮你做什么了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;案例一：快速理解陌生代码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：接手一个新项目，需要快速了解某个功能的实现逻辑。&lt;/p&gt;
&lt;p&gt;在重构中升 CDP 系统的 Datart 前端项目时，我需要了解&quot;数据视图中不同字段类型如何显示不同图标&quot;。&lt;/p&gt;
&lt;p&gt;如果没有 AI，我可能需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在代码中搜索相关关键词&lt;/li&gt;
&lt;li&gt;逐个文件阅读理解&lt;/li&gt;
&lt;li&gt;画出调用关系图&lt;/li&gt;
&lt;li&gt;花费数小时甚至一天&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;有了 AI，我只需要问一句&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;了解一下数据视图接口返回的 data.model.columns 中，type 类型有哪些？
目前我已知的有 STRING 和 NUMERIC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 立刻给出完整答案：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./field-types.png&quot; alt=&quot;字段类型分析结果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原来有 8 种类型！前 4 种是常用类型，后面是变量/脚本处理场景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;继续追问前端渲染逻辑&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;不同的 type 在前端页面中渲染有什么特殊处理吗？
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 分析后返回：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./field-frontend.png&quot; alt=&quot;前端渲染逻辑&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在我完全理解了这个功能的实现，可以让 AI 直接帮我修改代码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./code-modify-1.png&quot; alt=&quot;代码修改过程 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./code-modify-2.png&quot; alt=&quot;代码修改过程 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最终效果 —— 不同字段类型显示对应图标：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./field-icon-result.png&quot; alt=&quot;字段图标效果&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;案例二：批量修改业务逻辑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：产品经理提出需求变更，需要修改多处相关代码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;原始需求：中升 CDP 看板的&quot;导出 Excel&quot;功能，默认是根据查询条件导出的。但产品经理发现：如果用户只切换了查询条件却没点&quot;查询&quot;，导出的数据和页面显示的不一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个问题涉及全局多处导出功能，手动找很容易遗漏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;让 AI 来处理&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./export-logic-1.png&quot; alt=&quot;导出逻辑修改 1&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./export-logic-2.png&quot; alt=&quot;导出逻辑修改 2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;AI 自动找到所有相关位置并一并修改，省时省力还不容易出错。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;案例三：优化页面布局&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：页面上的指标过多时，标签显示不完整，需要优化布局。&lt;/p&gt;
&lt;p&gt;原始问题 —— 指标太多导致 Tag 被截断：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./layout-issue.png&quot; alt=&quot;布局问题&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我发给 AI 一张截图 + 一句话描述&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;列中支持多个指标，拖入多个指标导致指标 Tag 展示不完整，请合理优化前端展示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 思考后给出了三种解决方案：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./layout-solutions.png&quot; alt=&quot;三种解决方案&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它选择了方案一（水平滚动），效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./layout-scroll.png&quot; alt=&quot;水平滚动方案&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我不太满意，要求换行展示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;不要横向滚动条，换行展示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 立刻调整：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./layout-wrap.png&quot; alt=&quot;换行展示方案&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从发现问题到解决，只花了几分钟。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;案例四：联网搜索最新资讯&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：了解技术圈的最新动态。&lt;/p&gt;
&lt;p&gt;最近 Meta 的 React 团队和 Vercel 的 Next.js 团队联合发布了两个最高危险等级的安全漏洞，我想了解详情。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;直接问 AI&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查询最近 Next.js 爆出的最高危险等级漏洞，详细介绍下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 自动联网搜索并整理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./web-search.png&quot; alt=&quot;联网搜索结果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比自己去各个网站翻找高效多了。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;案例五：根据原型图生成页面&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;场景&lt;/strong&gt;：拿到产品原型图，需要快速实现页面。&lt;/p&gt;
&lt;p&gt;这是产品给的原型：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./prototype.png&quot; alt=&quot;产品原型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;我把图片发给 AI，并说明&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;这是一个 B 端原型，请使用项目中已有的组件库实现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AI 生成的页面&lt;/strong&gt;（左侧菜单和水印是长截图的 bug）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./generated-page.png&quot; alt=&quot;AI 生成的页面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然还需要微调，但基本框架已经出来了，节省了大量重复劳动。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;五、写在最后&lt;/h2&gt;
&lt;h3&gt;氛围编程的本质&lt;/h3&gt;
&lt;p&gt;氛围编程不是让 AI 完全取代程序员，而是改变了我们的工作方式：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;传统模式&lt;/strong&gt;：需求 → 思考 → 编码 → 调试 → 测试&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;氛围编程&lt;/strong&gt;：需求 → 描述给 AI → 审查 AI 的输出 → 微调 → 验收&lt;/p&gt;
&lt;p&gt;你的角色从&quot;代码的生产者&quot;变成了&quot;代码的审核者&quot;。这意味着：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ 你需要&lt;strong&gt;更强的需求理解能力&lt;/strong&gt;：能准确描述你想要什么&lt;/li&gt;
&lt;li&gt;✅ 你需要&lt;strong&gt;更强的代码审查能力&lt;/strong&gt;：能判断 AI 写的代码好不好&lt;/li&gt;
&lt;li&gt;✅ 你需要&lt;strong&gt;更广的技术视野&lt;/strong&gt;：知道什么方案是合理的&lt;/li&gt;
&lt;li&gt;❌ 你&lt;strong&gt;不再需要&lt;/strong&gt;记住每个 API 的参数顺序&lt;/li&gt;
&lt;li&gt;❌ 你&lt;strong&gt;不再需要&lt;/strong&gt;手写重复的模板代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;给初学者的建议&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;从简单任务开始&lt;/strong&gt;：先让 AI 帮你写个函数、改个样式，熟悉交互方式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学会描述需求&lt;/strong&gt;：越具体越好，比如&quot;把按钮改成蓝色&quot;不如&quot;把提交按钮的背景色改成 #1890ff&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保持批判性思维&lt;/strong&gt;：AI 也会犯错，要审查它的输出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多尝试不同工具&lt;/strong&gt;：Claude Code、Codex CLI、Gemini CLI 各有特点，找到适合自己的&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;常见问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q：AI 写的代码能用于生产环境吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：可以，但必须经过审查和测试。我目前的工作流是：AI 生成 → 我审查 → 跑测试 → 部署。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：这会让程序员失业吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：短期内不会。AI 目前更像是效率工具，能让一个人干两个人的活，但还无法独立完成复杂项目。长期来看，会改变行业对程序员的能力要求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q：国产模型够用吗？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：对于日常开发任务，国产模型基本够用。复杂的架构设计和疑难 bug 排查，顶级模型会更可靠。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;希望这篇文章能帮你开启氛围编程之旅。如果有问题，欢迎在评论区交流！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;小贴士&lt;/strong&gt;：工具在不断更新，本文内容可能会过时。建议收藏官方文档：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.openai.com/codex/cli/&quot;&gt;Codex CLI 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>如何配置 Claude Code 和 Codex</title><link>https://www.mihouo.com/posts/ai/claude-code-and-codex-configuration/</link><guid isPermaLink="true">https://www.mihouo.com/posts/ai/claude-code-and-codex-configuration/</guid><description>如今 Vibe Coding 越来越流行了，本文介绍下此领域下最流行的两个工具 Claude Code 和 Codex 的持久化配置</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Claude Code 和 Codex 是什么&lt;/h2&gt;
&lt;p&gt;Claude Code 是 Anthropic 的“代理式”编码工具，既有本地 CLI、VS Code 插件，也有云端/Web 形态（研究预览，Pro/Max 可用）。可理解/修改代码、运行命令、并支持子代理、检查点回滚等增强自治能力。&lt;/p&gt;
&lt;p&gt;Codex 是 OpenAI 推出的本地终端编码代理（开源，Rust 实现）。支持读取/编辑/运行当前目录代码，带交互式 TUI、脚本化 exec 模式、细粒度沙箱与审批策略，以及原生 MCP 集成（STDIO 与可流式 HTTP，含 OAuth）。同时有统一的 config.toml 供 CLI 与 IDE 扩展共享。&lt;/p&gt;
&lt;h2&gt;使用场景&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;OpenAI Codex CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;形态&lt;/td&gt;
&lt;td&gt;终端、VS Code 扩展、&lt;strong&gt;Web（云端）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;终端 CLI + IDE 扩展&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;价格&lt;/td&gt;
&lt;td&gt;昂贵，即使是使用第三方中转站&lt;/td&gt;
&lt;td&gt;较为便宜，可使用 Team 订阅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行位置&lt;/td&gt;
&lt;td&gt;本地；或&lt;strong&gt;云端隔离沙箱&lt;/strong&gt;并行跑任务（自动 PR/进度跟踪）&lt;/td&gt;
&lt;td&gt;本地在你的工作目录内读/改/跑（带交互式 TUI/脚本模式）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全模型&lt;/td&gt;
&lt;td&gt;基于 &lt;strong&gt;文件系统+网络双重隔离&lt;/strong&gt; 的沙箱，减少权限弹窗，Git 走受控代理&lt;/td&gt;
&lt;td&gt;“审批模式”可选：默认 &lt;strong&gt;Auto&lt;/strong&gt;（工作区内自动），越权/联网需批准，也可只读/全开放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP（工具扩展）&lt;/td&gt;
&lt;td&gt;官方文档与插件生态；可通过 MCP 连接外部工具&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;原生 MCP&lt;/strong&gt;：支持 STDIO 与&lt;strong&gt;可流式 HTTP&lt;/strong&gt;（含 OAuth，需要开启 &lt;code&gt;experimental_use_rmcp_client&lt;/code&gt;），CLI 与 IDE &lt;strong&gt;共用同一 &lt;code&gt;config.toml&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;支持（含 WSL/Git Bash 路线）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;实验性&lt;/strong&gt;，官方建议用 &lt;strong&gt;WSL&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;典型场景&lt;/td&gt;
&lt;td&gt;多仓库/并行小改动、TDD 修复、云端受控环境&lt;/td&gt;
&lt;td&gt;本地项目深度改造、脚本化集成、和 IDE 同步用一套配置&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Node 环境安装&lt;/h2&gt;
&lt;p&gt;版本需大于等于 20&lt;/p&gt;
&lt;p&gt;如果你是前端工程师，建议使用 &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;nvm&lt;/a&gt; 来管理 Node.js 版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看远程 Node.js 版本
nvm ls-remote
# 安装 Node.js LTS
nvm install --lts
# 切换到 Node.js LTS
nvm use --lts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功安装后，运行以下命令检查版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Claude Code 配置&lt;/h2&gt;
&lt;h3&gt;终端中使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装 CLI
npm install -g @anthropic-ai/claude-code
# 启动（终端）
claude
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;VS Code 中使用&lt;/h3&gt;
&lt;p&gt;从市场安装 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code&quot;&gt;Claude Code for VS Code&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;授权&lt;/h3&gt;
&lt;p&gt;编辑文件 &lt;code&gt;~/.claude/settings.json&lt;/code&gt; 文件添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;env&quot;: {
    &quot;ANTHROPIC_AUTH_TOKEN&quot;: &quot;&quot;, // 你的 Claude API 密钥 如果你使用的是第三方中转站，请填入中转站的 API 密钥
    &quot;ANTHROPIC_BASE_URL&quot;: &quot;&quot;, // 通常为 https://api.anthropic.com/v1 如果你使用的是第三方中转站，请填入中转站的 API 地址
    &quot;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC&quot;: &quot;1&quot;
  },
  &quot;permissions&quot;: {
    &quot;allow&quot;: [],
    &quot;deny&quot;: []
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Codex 配置&lt;/h2&gt;
&lt;h3&gt;终端中使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装（二选一）
npm install -g @openai/codex
# 启动交互式 TUI
codex
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;VS Code 中使用&lt;/h3&gt;
&lt;p&gt;从市场安装 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=openai.chatgpt&quot;&gt;OpenAI Codex for VS Code&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;授权&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;~/.codex&lt;/code&gt; 目录&lt;/p&gt;
&lt;p&gt;创建并编辑文件 &lt;code&gt;~/.codex/config.toml&lt;/code&gt; 文件添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_provider = &quot;codex&quot;
model = &quot;gpt-5&quot; #可更改为model = &quot;gpt-5-codex&quot;
model_reasoning_effort = &quot;high&quot;
disable_response_storage = true

[model_providers.codex]
name = &quot;codex&quot;
base_url = &quot;https://api.openai.com/v1&quot; # 通常为 https://api.openai.com/v1 如果你使用的是第三方中转站，请填入中转站的 API 地址
wire_api = &quot;responses&quot;
requires_openai_auth = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建并编辑文件 &lt;code&gt;~/.codex/auth.json&lt;/code&gt; 文件添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;OPENAI_API_KEY&quot;: &quot;your-api-key-here&quot; // 你的 OpenAI API 密钥 如果你使用的是第三方中转站，请填入中转站的 API 密钥
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>基于 Antd Table 组件组件的二次封装，使其支持同列相同数据下的合并</title><link>https://www.mihouo.com/posts/front/a-wrapper-based-on-the-antd-table-component/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/a-wrapper-based-on-the-antd-table-component/</guid><description>基于 Antd Table 二次封装，实现同列相同数据自动合并</description><pubDate>Wed, 10 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近在进行业务开发时，经常遇到同列相同数据下的合并，为了方便后续使用，我封装了一个基于 Antd Table 组件的二次封装，可以方便的实现同列相同数据下的合并&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image.png&quot; alt=&quot;同列相同数据下的合并&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现思路&lt;/h2&gt;
&lt;p&gt;Antd Table 组件的 Column 描述列描述对象中 onCell 属性可以用来设置单元格的样式，因此我们只需要在 onCell 属性中设置合并单元格 rowSpan 值即可。&lt;/p&gt;
&lt;p&gt;简单介绍下 rowSpan 属性，rowSpan 属性用来设置单元格的跨行数，如果设置为 2，则表示单元格跨两行。 同时需要将被合并的单元格的 rowSpan 值设置为 0，来避免被合并的单元格被显示。&lt;/p&gt;
&lt;h3&gt;实现代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useMemo } from &apos;react&apos;
import { Table, TableProps } from &apos;antd&apos;

export default function SameGroupMergeTable(props: TableProps) {
  const { columns, dataSource } = props;

  const innerColumns = useMemo(() =&amp;gt; {
    if (!columns || !dataSource) return [];
    return columns.map((column: any) =&amp;gt; {
      if (column.mergeColumn &amp;amp;&amp;amp; !column.onCell) {
        return {
          ...column,
          onCell: (record: any, rowIndex: number | undefined) =&amp;gt; {
            const dataIndex = column.dataIndex;
            const sameKey = dataSource.filter((item) =&amp;gt; item[dataIndex] === record[dataIndex]);
            if (sameKey.length === 1) {
              return { rowSpan: 1 };
            }
            const sameKeyFirstIndex = dataSource.findIndex((item) =&amp;gt; item[dataIndex] === record[dataIndex]);
            if (rowIndex === sameKeyFirstIndex) {
              return { rowSpan: sameKey.length };
            }
            return { rowSpan: 0 };
          },
        };
      }
      // 如果 column 中设置了 mergeColumn 和 onCell 属性，则需要重写 onCell 属性，因为可能需要用到 onCell 属性中的其他参数实现其他功能
      if (column.mergeColumn &amp;amp;&amp;amp; column.onCell) {
        return {
          ...column,
          // 重写 onCell, 给 onCell 传递一个参数，合并单元格的值（rowSpan）
          onCell: (record: any, rowIndex: number | undefined) =&amp;gt; {
            const dataIndex = column.dataIndex;
            const sameKey = dataSource.filter((item) =&amp;gt; item[dataIndex] === record[dataIndex]);
            if (sameKey.length === 1) {
              return column.onCell(record, rowIndex, { rowSpan: 1 });
            }
            const sameKeyFirstIndex = dataSource.findIndex((item) =&amp;gt; item[dataIndex] === record[dataIndex]);
            if (rowIndex === sameKeyFirstIndex) {
              return column.onCell(record, rowIndex, { rowSpan: sameKey.length });
            }
            return column.onCell(record, rowIndex, { rowSpan: 0 });
          },
        };
      }
      return column;
    });
  }, [columns, dataSource]);

  return (
    &amp;lt;Table {...props} columns={innerColumns} /&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import SameGroupMergeTable from &apos;@/src/components/SameGroupMergeTable&apos;;

const columns = [
  {
    title: &apos;Name&apos;,
    dataIndex: &apos;name&apos;,
    // 使用方式一：设置 mergeColumn 属性
    mergeColumn: true,
  },
  {
    title: &apos;Name&apos;,
    dataIndex: &apos;name&apos;,
    // 使用方式二：设置 mergeColumn 和 onCell 属性
    mergeColumn: true,
    onCell: (_record, rowIndex, mergeObject) =&amp;gt; {
      if (rowIndex === 0) {
        return {
          ...mergeObject,
          style: {
            backgroundColor: &quot;#E6F4FF&quot;,
          }
        }
      }
      return mergeObject;
    },
  },
];
const dataSource = [];
const Example = () =&amp;gt; {
  return &amp;lt;SameGroupMergeTable
    columns={columns}
    dataSource={dataSource}
  /&amp;gt;;
};

export default Example;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
通常情况下只需要设置 mergeColumn 属性即可&lt;/p&gt;
&lt;p&gt;如果需要合并的列需要用到 onCell 属性中的其他参数实现其他功能，这通过 onCell 函数第三个形参 mergeObject 来实现。
:::&lt;/p&gt;
</content:encoded></item><item><title>React Hook PDF 生成</title><link>https://www.mihouo.com/posts/front/react-hook-pdf-generation/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/react-hook-pdf-generation/</guid><description>使用 html2canvas 和 jsPDF 生成 PDF，封装成 Hook 复用</description><pubDate>Mon, 25 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在项目开发过程中，我们经常需要生成 PDF 文件。但是，生成 PDF 文件的流程比较复杂，需要处理很多细节问题。&lt;/p&gt;
&lt;p&gt;通常情况下如果按照页面原样生成 PDF 文件，只需要使用 html2canvas 和 jsPDF 即可。&lt;/p&gt;
&lt;p&gt;代码也和简单，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import html2canvas from &apos;html2canvas&apos;;
import jsPDF from &apos;jspdf&apos;;

const Example = () =&amp;gt; {
  const onDownload = () =&amp;gt; {
    html2canvas(document.body).then((canvas) =&amp;gt; {
      const imgData = canvas.toDataURL(&apos;image/png&apos;);
      const pdf = new jsPDF();
      pdf.addImage(imgData, &apos;PNG&apos;, 0, 0);
      pdf.save(&apos;example.pdf&apos;);
    });
  };

  return &amp;lt;button onClick={onDownload}&amp;gt;Download PDF&amp;lt;/button&amp;gt;;
};

export default Example;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只是一份最简单的代码，但是实际项目中，我们还需要处理很多细节问题，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PDF 中的超链接可以点击 (实现 PDF 中的超链接可以点击 - 通过遍历页面中所有 a 标签，并添加蒙层，设置对应的位置，点击蒙层时，跳转到对应的超链接)&lt;/li&gt;
&lt;li&gt;PDF 文件体积较大 (文件体积非常大, &amp;gt; 80 MB - 降低分辨率，压缩图片，压缩文件)&lt;/li&gt;
&lt;li&gt;PDF 与页面样式不一致 (antd 表格中的额外行的 + 按钮样式错位问题，导出的 PDF 中变为了纵向排列的一个横和一个竖 - 隐藏原 ::before/::after 伪元素，用一个内联 SVG 覆盖，导出后再还原)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;为了简化使用方式并解决上述问题，我封装了一个 hook，用于生成 PDF 文件。&lt;/p&gt;
&lt;h3&gt;hook 使用方式如下：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { usePdfGenerator } from &apos;@/src/hooks/usePdfGenerator&apos;;
import { Button } from &apos;antd&apos;;

const Example = () =&amp;gt; {
  const { contentRef, generatePdf, isLoading } = usePdfGenerator();
  const onDownload = () =&amp;gt; {
    generatePdf(&apos;example&apos;);
    // 如果你需要获取到导出的文件，可以传入一个回调函数，或者直接使用 generatePdf 的返回值
    const file = generatePdf(&apos;example&apos;, {}, (file) =&amp;gt; {
      console.log(&apos;file&apos;, file);
    });
  };

  return &amp;lt;div ref={contentRef}&amp;gt;
    &amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;
    &amp;lt;Button loading={isLoading} onClick={onDownload}&amp;gt;Download PDF&amp;lt;/Button&amp;gt;
  &amp;lt;/div&amp;gt;;
};

export default Example;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning
回调函数和返回值返回的是生成的图片文件，这是由于我们项目有一个导出 word 的需求，需要前端将图片传给后端使用。
:::&lt;/p&gt;
&lt;h3&gt;hook 源码如下：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import { useRef, useCallback, useState } from &apos;react&apos;;
import html2canvas from &apos;html2canvas&apos;;
import jsPDF from &apos;jspdf&apos;;

type GeneratePdfOptions = {
  // 输出图片类型：JPEG 体积更小，PNG 无损但体积更大
  imageType?: &apos;jpeg&apos; | &apos;png&apos;;
  // 当 imageType 为 JPEG 时生效，范围 0-1，建议 0.6~0.85
  jpegQuality?: number;
  // 二次缩放比例，0-1。用于在不明显损伤清晰度的前提下降低像素数，建议 0.7~0.9。
  downscaleRatio?: number;
  // 目标最大文件体积（KB），用于自适应压缩 PNG（通过逐步降低分辨率实现，尽量不损伤清晰度）
  maxFileSizeKB?: number;
  // 是否下载
  isDownload?: boolean;
  // 回调函数
  callback?: (file: File) =&amp;gt; void;
};

export const usePdfGenerator = () =&amp;gt; {
  // 1. 创建一个 ref 来附加到需要导出的 DOM 节点上
  const contentRef = useRef(null);
  const [isLoading, setIsLoading] = useState(false);
  // 2. 使用 state 来管理生成过程中的加载状态，提升用户体验
  // const [isLoading, setIsLoading] = useState(false);

  // 3. 使用 useCallback 来包装我们的核心函数，以避免不必要的重渲染
  const generatePdf = useCallback(async (
    fileName = &apos;document&apos;,
    options: GeneratePdfOptions = {},
    callback: (file: File) =&amp;gt; void = () =&amp;gt; { }
  ): Promise&amp;lt;File | undefined&amp;gt; =&amp;gt; {
    const { imageType = &apos;png&apos;, jpegQuality = 0.82, downscaleRatio = 0.88, isDownload = true } = options;
    const element: HTMLElement = contentRef.current!;

    if (!element) {
      console.error(&quot;无法生成 PDF，因为找不到引用的内容元素。&quot;);
      return undefined;
    }

    setIsLoading(true); // 开始生成，设置加载状态为 true
    // 预先准备 a 标签样式恢复器，并在渲染前将计算后的颜色写入内联，确保导出跟页面一致
    const anchorsInDom = Array.from(element.querySelectorAll(&apos;a&apos;)) as HTMLAnchorElement[];
    const restoreAnchorInlineStyles: Array&amp;lt;() =&amp;gt; void&amp;gt; = [];
    anchorsInDom.forEach((a) =&amp;gt; {
      const prevColor = a.style.color;
      const prevTdColor = (a.style as any).textDecorationColor as string;
      const prevTextDecoration = a.style.textDecoration;
      const cs = window.getComputedStyle(a);
      const computedColor = cs.color;
      const computedTdColor = (cs as any).textDecorationColor || computedColor;
      a.style.color = computedColor;
      try { (a.style as any).textDecorationColor = computedTdColor; } catch (_) { }
      if ((cs.textDecorationLine || &apos;&apos;).includes(&apos;underline&apos;) &amp;amp;&amp;amp; !a.style.textDecoration) {
        a.style.textDecoration = &apos;underline&apos;;
      }
      restoreAnchorInlineStyles.push(() =&amp;gt; {
        a.style.color = prevColor;
        try { (a.style as any).textDecorationColor = prevTdColor; } catch (_) { }
        a.style.textDecoration = prevTextDecoration;
      });
    });

    // 修复 antd Table 展开/收起图标（+/-）在 html2canvas 下的渲染异常：
    // 思路：隐藏原 ::before/::after 伪元素，用一个内联 SVG 覆盖，导出后再还原
    const cleanupExpandIconFixers: Array&amp;lt;() =&amp;gt; void&amp;gt; = [];
    const styleTag = document.createElement(&apos;style&apos;);
    styleTag.setAttribute(&apos;data-pdf-export-fix&apos;, &apos;expand-icon&apos;);
    styleTag.textContent = `
      .ant-table-row-expand-icon.__pdf_export_fix::before,
      .ant-table-row-expand-icon.__pdf_export_fix::after {
        content: none !important;
        background: none !important;
        border: 0 !important;
      }
    `;
    document.head.appendChild(styleTag);
    cleanupExpandIconFixers.push(() =&amp;gt; {
      try { document.head.removeChild(styleTag); } catch (_) { }
    });

    const expandButtons = Array.from(
      element.querySelectorAll(&apos;button.ant-table-row-expand-icon&apos;)
    ) as HTMLButtonElement[];
    expandButtons.forEach((btn) =&amp;gt; {
      const cs = window.getComputedStyle(btn);
      const beforeStyle = window.getComputedStyle(btn, &apos;::before&apos;);
      const afterStyle = window.getComputedStyle(btn, &apos;::after&apos;);
      const isTransparent = (c: string | null | undefined) =&amp;gt; !c || c === &apos;transparent&apos; || c === &apos;rgba(0, 0, 0, 0)&apos;;
      const pickColor = (...colors: Array&amp;lt;string | null | undefined&amp;gt;) =&amp;gt; colors.find((c) =&amp;gt; !isTransparent(c));
      const strokeColor = (pickColor(
        beforeStyle.backgroundColor,
        (beforeStyle as any).borderColor,
        afterStyle.backgroundColor,
        (afterStyle as any).borderColor,
        cs.backgroundColor,
        cs.color,
        &apos;rgba(0,0,0,0.45)&apos;
      ) || &apos;rgba(0,0,0,0.45)&apos;) as string;
      const prevPosition = btn.style.position;
      const prevClassName = btn.className;
      btn.style.position = prevPosition || &apos;relative&apos;;
      if (!btn.classList.contains(&apos;__pdf_export_fix&apos;)) btn.classList.add(&apos;__pdf_export_fix&apos;);

      const overlay = document.createElement(&apos;span&apos;);
      overlay.style.position = &apos;absolute&apos;;
      overlay.style.inset = &apos;0&apos;;
      overlay.style.display = &apos;flex&apos;;
      overlay.style.alignItems = &apos;center&apos;;
      overlay.style.justifyContent = &apos;center&apos;;
      overlay.style.pointerEvents = &apos;none&apos;;
      overlay.style.zIndex = &apos;1&apos;;

      const expanded = btn.getAttribute(&apos;aria-expanded&apos;) === &apos;true&apos;;
      // 使用 16x16 视口绘制 + 或 -，线宽 1.5，颜色取自按钮 color
      const svg = expanded
        ? `&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
           &amp;lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 16 16&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;
             &amp;lt;line x1=&quot;3&quot; y1=&quot;8&quot; x2=&quot;13&quot; y2=&quot;8&quot; stroke=&quot;${strokeColor}&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot;/&amp;gt;
           &amp;lt;/svg&amp;gt;`
        : `&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
           &amp;lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 16 16&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&amp;gt;
             &amp;lt;line x1=&quot;3&quot; y1=&quot;8&quot; x2=&quot;13&quot; y2=&quot;8&quot; stroke=&quot;${strokeColor}&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot;/&amp;gt;
             &amp;lt;line x1=&quot;8&quot; y1=&quot;3&quot; x2=&quot;8&quot; y2=&quot;13&quot; stroke=&quot;${strokeColor}&quot; stroke-width=&quot;1.5&quot; stroke-linecap=&quot;round&quot;/&amp;gt;
           &amp;lt;/svg&amp;gt;`;
      overlay.innerHTML = svg;
      btn.appendChild(overlay);

      cleanupExpandIconFixers.push(() =&amp;gt; {
        try { btn.removeChild(overlay); } catch (_) { }
        try { btn.className = prevClassName; } catch (_) { }
        btn.style.position = prevPosition;
      });
    });

    try {

      // 调用 html2canvas。设置 scale 可以提高清晰度。
      const canvasScale = 2; // html2canvas 渲染比例，需要参与后续坐标换算
      const canvas = await html2canvas(element, {
        scale: canvasScale, // 提高分辨率
        useCORS: true, // 允许加载跨域图片
        logging: false, // 不在控制台打印日志
        backgroundColor: &apos;#FFFFFF&apos;,
      });

      // 对原始画布做高质量下采样，必要时自适应压缩（针对 PNG 通过降低分辨率实现）
      const exportCanvas = document.createElement(&apos;canvas&apos;);
      const ctx = exportCanvas.getContext(&apos;2d&apos;);
      if (!ctx) throw new Error(&apos;无法获取 2D 上下文&apos;);

      const originalWidth = canvas.width;
      const originalHeight = canvas.height;
      let currentDownscale = Math.min(1, Math.max(0.5, downscaleRatio));
      const minDownscale = 0.6; // 限制最小降采样比例，避免明显模糊
      const maxFileSizeBytes = Math.max(1, Math.round((options.maxFileSizeKB ?? 320) * 1024));

      const redrawWithDownscale = (ratio: number) =&amp;gt; {
        exportCanvas.width = Math.max(1, Math.round(originalWidth * ratio));
        exportCanvas.height = Math.max(1, Math.round(originalHeight * ratio));
        ctx.imageSmoothingEnabled = true;
        // @ts-ignore: 浏览器支持性检测
        if (ctx.imageSmoothingQuality) ctx.imageSmoothingQuality = &apos;high&apos;;
        ctx.clearRect(0, 0, exportCanvas.width, exportCanvas.height);
        ctx.drawImage(
          canvas,
          0,
          0,
          originalWidth,
          originalHeight,
          0,
          0,
          exportCanvas.width,
          exportCanvas.height
        );
      };

      // 初次绘制
      redrawWithDownscale(currentDownscale);

      // 针对 PNG，自适应降低分辨率以控制文件体积；JPEG 则交给质量参数处理
      const getPngBlob = async (): Promise&amp;lt;Blob&amp;gt; =&amp;gt; new Promise((resolve, reject) =&amp;gt; {
        try {
          exportCanvas.toBlob((blob) =&amp;gt; {
            if (blob) resolve(blob);
            else reject(new Error(&apos;无法导出 PNG Blob&apos;));
          }, &apos;image/png&apos;);
        } catch (e) {
          reject(e);
        }
      });

      let tentativePngBlob: Blob | undefined;
      for (let attempt = 0; attempt &amp;lt; 6; attempt += 1) {
        // 生成 PNG Blob 并检测体积
        // eslint-disable-next-line no-await-in-loop
        const blob = await getPngBlob();
        tentativePngBlob = blob;
        if (blob.size &amp;lt;= maxFileSizeBytes || currentDownscale &amp;lt;= minDownscale) {
          break;
        }
        // 进一步轻微缩小 8-12% 之间，尽量减少清晰度损失
        currentDownscale = Math.max(minDownscale, Math.round(currentDownscale * 0.9 * 100) / 100);
        redrawWithDownscale(currentDownscale);
      }

      const mime = imageType === &apos;png&apos; ? &apos;image/png&apos; : &apos;image/jpeg&apos;;
      const quality = imageType === &apos;png&apos; ? undefined : Math.min(1, Math.max(0.5, jpegQuality));
      const imgData = exportCanvas.toDataURL(mime, quality as any);

      // 创建 jsPDF 实例 (A4 尺寸)
      const pdf = new jsPDF({
        orientation: &apos;portrait&apos;, // p: 纵向, l: 横向
        unit: &apos;mm&apos;,
        format: &apos;a4&apos;,
      });
      // console.log(canvas);

      // 计算 PDF 页面尺寸和图片尺寸
      const pdfWidth = pdf.internal.pageSize.getWidth();
      const pdfHeight = pdf.internal.pageSize.getHeight();
      const imgWidth = exportCanvas.width;
      const imgHeight = exportCanvas.height;

      // 设置边距：左右 36px，上下 20px（转换为毫米）
      const marginLeft = 36 * 0.264583; // 转换像素为毫米 (1px = 0.264583mm)
      const marginTop = 20 * 0.264583;

      // 计算考虑边距后的可用空间
      const availableWidth = pdfWidth - (marginLeft * 2);
      const availableHeight = pdfHeight - (marginTop * 2);

      // 重新计算比例以适应边距
      const ratioWithMargins = Math.min(availableWidth / imgWidth, availableHeight / imgHeight);
      const finalWidth = imgWidth * ratioWithMargins;
      const finalHeight = imgHeight * ratioWithMargins;

      // 计算图片在 PDF 页面中的位置（考虑边距）
      const x = marginLeft;
      const y = marginTop;

      // 将图片添加到 PDF
      const addImageType = imageType === &apos;png&apos; ? &apos;PNG&apos; : &apos;JPEG&apos;;
      pdf.addImage(imgData, addImageType as any, x, y, finalWidth, finalHeight);

      // 在 PDF 上叠加可点击链接区域
      // 思路：
      // 1) 遍历 element 内所有 &amp;lt;a&amp;gt; 标签
      // 2) 使用 getClientRects() 获取每个可见行的矩形（处理换行）
      // 3) 将 DOM CSS 像素 -&amp;gt; 画布像素（乘以 canvasScale）-&amp;gt; PDF 毫米（乘以 ratioWithMargins）
      // 4) 叠加到 (x, y) 的偏移位置
      const elementRect = element.getBoundingClientRect();
      // 使用 html2canvas 实际产出的画布尺寸与元素 CSS 尺寸，构建精准的 CSSpx-&amp;gt;Canvaspx 比例
      const elementCssWidth = Math.max(1, element.scrollWidth || elementRect.width);
      const elementCssHeight = Math.max(1, element.scrollHeight || elementRect.height);
      const cssToCanvasScaleX = imgWidth / elementCssWidth;
      const cssToCanvasScaleY = imgHeight / elementCssHeight;

      // 由最终绘制尺寸得到 Canvaspx-&amp;gt;PDFmm 的比例（分别保留 X/Y，避免浮点误差累积）
      const canvasToPdfScaleX = finalWidth / imgWidth;
      const canvasToPdfScaleY = finalHeight / imgHeight;

      // 辅助：累加从目标节点到根容器（element）之间所有可滚动祖先的滚动偏移
      const getAccumulatedScrollOffset = (node: Element, root: Element) =&amp;gt; {
        let cur: Element | null = node.parentElement;
        let accLeft = 0;
        let accTop = 0;
        while (cur &amp;amp;&amp;amp; cur !== root &amp;amp;&amp;amp; cur !== document.body &amp;amp;&amp;amp; cur !== document.documentElement) {
          const style = window.getComputedStyle(cur);
          const overflowY = style.overflowY;
          const overflowX = style.overflowX;
          const isScrollableY = overflowY === &apos;auto&apos; || overflowY === &apos;scroll&apos;;
          const isScrollableX = overflowX === &apos;auto&apos; || overflowX === &apos;scroll&apos;;
          if (isScrollableY) accTop += (cur as HTMLElement).scrollTop;
          if (isScrollableX) accLeft += (cur as HTMLElement).scrollLeft;
          cur = cur.parentElement;
        }
        return { accLeft, accTop };
      };
      const anchors = Array.from(element.querySelectorAll(&apos;a[href]&apos;)) as HTMLAnchorElement[];

      const isValidHref = (href: string | null): href is string =&amp;gt; {
        if (!href) return false;
        const lowered = href.trim().toLowerCase();
        return (
          lowered.startsWith(&apos;http://&apos;) ||
          lowered.startsWith(&apos;https://&apos;) ||
          lowered.startsWith(&apos;mailto:&apos;) ||
          lowered.startsWith(&apos;tel:&apos;)
        );
      };

      anchors.forEach((anchor) =&amp;gt; {
        if (!isValidHref(anchor.getAttribute(&apos;href&apos;))) return;
        const url = anchor.getAttribute(&apos;href&apos;) as string;

        const clientRects = anchor.getClientRects();
        if (!clientRects || clientRects.length === 0) return;

        // 滚动累加（处理 antd Table 等内部可滚动容器）
        const { accLeft, accTop } = getAccumulatedScrollOffset(anchor, element);

        for (let i = 0; i &amp;lt; clientRects.length; i += 1) {
          const rect = clientRects[i];

          // DOM CSS px -&amp;gt; 相对 element 的 px
          // 额外加上所有中间可滚动容器的滚动偏移，映射到内容原点坐标系
          const relLeftPx = (rect.left - elementRect.left) + accLeft;
          const relTopPx = (rect.top - elementRect.top) + accTop;
          const widthPx = rect.width;
          const heightPx = rect.height;

          if (widthPx &amp;lt;= 0 || heightPx &amp;lt;= 0) continue;

          // CSS px -&amp;gt; 画布 px（使用实际比例，避免仅依赖配置 scale 导致的偏差）
          const leftCanvasPx = relLeftPx * cssToCanvasScaleX;
          const topCanvasPx = relTopPx * cssToCanvasScaleY;
          const widthCanvasPx = widthPx * cssToCanvasScaleX;
          const heightCanvasPx = heightPx * cssToCanvasScaleY;

          // 画布 px -&amp;gt; PDF mm，并考虑边距偏移 (x, y)
          const leftMm = x + leftCanvasPx * canvasToPdfScaleX;
          const topMm = y + topCanvasPx * canvasToPdfScaleY;
          const widthMm = widthCanvasPx * canvasToPdfScaleX;
          const heightMm = heightCanvasPx * canvasToPdfScaleY;
          const overlayHeightMm = heightMm * 2; // 扩大高度以增强可点击容错

          // 叠加链接注解（使用 any 以兼容类型定义差异）
          try {
            (pdf as any).link(leftMm, topMm, widthMm, overlayHeightMm, { url });
          } catch (e) {
            // 某些版本也可用 pdf.link(x, y, w, h, urlNumber) 或 textWithLink
            try {
              // 退化方案：放置一个透明的文本链接（尽量不影响视觉）
              const fontSize = 0.01; // 极小字号，尽量不可见
              const prevSize = (pdf as any).getFontSize?.() ?? 16;
              (pdf as any).setFontSize?.(fontSize);
              (pdf as any).textWithLink?.(&apos; &apos;, leftMm, topMm + fontSize, { url });
              (pdf as any).setFontSize?.(prevSize);
            } catch (_) {
              // 忽略
            }
          }
        }
      });


      if (isDownload) {
        // 保存 PDF
        pdf.save(`${fileName}.pdf`);
      }
      // 返回 PNG 图片文件（而非 PDF），若之前已进行自适应压缩则优先使用该 Blob
      const pngBlob: Blob = tentativePngBlob ?? await new Promise((resolve, reject) =&amp;gt; {
        try {
          exportCanvas.toBlob((blob) =&amp;gt; {
            if (blob) resolve(blob);
            else reject(new Error(`无法导出 ${imageType.toUpperCase()} Blob`));
          }, mime);
        } catch (e) {
          reject(e);
        }
      });
      const pngFile = new File([pngBlob], `${fileName}.${imageType}`, { type: mime });
      callback(pngFile);
      return pngFile;
    } catch (error) {
      console.error(&quot;生成 PDF 时出错:&quot;, error);
    } finally {
      // 还原 a 标签的临时内联颜色，避免影响真实页面
      try { restoreAnchorInlineStyles.forEach((fn) =&amp;gt; fn()); } catch (_) { }
      // 还原 antd 展开图标修复
      try { cleanupExpandIconFixers.forEach((fn) =&amp;gt; fn()); } catch (_) { }
      setIsLoading(false); // 结束生成，无论成功或失败都将加载状态设为 false
    }
    return undefined;
  }, []); // 空依赖数组意味着此函数在组件生命周期内不会改变

  // 4. 返回 ref、生成函数和加载状态，以便在组件中使用
  return { contentRef, generatePdf, isLoading };
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Cursor Agent 初体验</title><link>https://www.mihouo.com/posts/tool-share/cursor-agent-first-experience/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/cursor-agent-first-experience/</guid><description>使用 Cursor Agent 完全用自然语言编程完成 Web 项目</description><pubDate>Mon, 28 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;Cursor 是最近非常火的一款 AI 编程工具，本文主要介绍 Cursor Agent 的初体验。实现了完全使用自然语言进行编程，最终完成一个简单的 Web 项目，包含前端、后端、数据库、以及大语言模型的 Api 调用等。&lt;/p&gt;
&lt;p&gt;虽然我使用 Cursor 已经有一段时间了，但是很少使用 Cursor Agent 功能，之前一直使用的是 Cursor Tab，最近刚好看到一个项目——&lt;a href=&quot;https://github.com/Anduin2017/HowToCook&quot;&gt;HowToCook&lt;/a&gt;，是一个收集菜谱的项目，项目中包含了很多菜谱，使用 Markdown 格式存储，我这里使用 Cursor 的 Agent 功能，使用自然语言指示 Cursor 基于此项目将菜谱转换为一个 Web 项目，提供可视化界面，并提供菜谱的搜索功能。&lt;/p&gt;
&lt;h2&gt;项目实现流程&lt;/h2&gt;
&lt;p&gt;遍历项目中的所有菜谱，将菜谱传给大语言模型，由大语言模型生成菜谱的 JSON 格式数据，然后将 JSON 数据存入到 MongoDB 中。最终将 MongoDB 中的数据转换为前端可用的数据格式，并提供搜索功能。&lt;/p&gt;
&lt;p&gt;上述所有流程均使用 Cursor Agent 完成，我提供自然语言的描述，Cursor Agent 会根据我的描述，自动生成代码，并执行。&lt;/p&gt;
&lt;h2&gt;Cursor 消耗&lt;/h2&gt;
&lt;p&gt;最终基本完成项目后使用 Cursor 的消耗如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cursor-cost.png&quot; alt=&quot;Cursor 消耗&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
并不是提问题 338 次，而是一次提问会消耗多个次数，它是根据 Token 计数来计算的。
:::&lt;/p&gt;
&lt;h2&gt;项目代码&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;DevilC0822/HowToCook&quot;}&lt;/p&gt;
&lt;p&gt;项目在线地址：&lt;a href=&quot;https://cook.mihouo.com/&quot;&gt;程序员做饭指南&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;优势:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;开发效率非常高，对于小产品，可以快速实现、迭代。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;需要指导写出详细的提示词，如果只是一些简单的描述，Cursor Agent 生成的代码基本处于黑盒状态，无法进行精细的控制。&lt;/li&gt;
&lt;li&gt;对于庞大的任务无法处理，需要将任务拆分成多个小任务，然后使用 Cursor Agent 完成。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>哪吒探针添加脚本测试归档结果数据</title><link>https://www.mihouo.com/posts/server/nezha-agent-script/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/nezha-agent-script/</guid><description>基于哪吒探针扩展脚本，实现测试数据归档</description><pubDate>Wed, 14 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;哪吒探针是一款开源的探针工具，支持多种操作系统，包括 Linux、Windows 等。它可以帮助用户监控服务器性能、网络状态、硬件信息等，并提供详细的报告和分析。&lt;/p&gt;
&lt;p&gt;由于手里有多台 VPS，它们的测试数据需要归档，所以基于哪吒探针写了一个脚本，扩展其功能，实现在哪吒探针页面端归档结果数据。方便统一查看测试结果，如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image.png&quot; alt=&quot;哪吒探针归档结果数据&quot; /&gt;
&lt;img src=&quot;./image-1.png&quot; alt=&quot;哪吒探针归档结果数据-1&quot; /&gt;
&lt;img src=&quot;./image-2.png&quot; alt=&quot;哪吒探针归档结果数据-2&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;如何配置&lt;/h2&gt;
&lt;p&gt;在哪吒探针管理端，自定义脚本处加入以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
const nodeCheckResult = {
  36: {  // 服务器节点 id 可通过节点详情页 url 获取；或者管理端服务器列表表格中查看
      remark: &apos;DMIT.EB.WEE&apos;, // 节点名称仅用于自我分辨
      NodeQuality: {
          type: &apos;iframe&apos;, // iframe | image
          url: &apos;https://nodequality.com/r/rki21soaA2SlcnF7vBd2jIuDKPfiS6fr&apos;, // 测试结果
      },
      Speedtest: {
          type: &apos;image&apos;, // iframe | image
          url: &apos;https://www.speedtest.net/result/17721869331.png&apos;, // 测试结果
          desc: &apos;辽宁联通 1000M宽带测试&apos;, // 测试结果描述，可不填，仅在图片类型时有效
      },
      融合怪: {
          type: &apos;iframe&apos;, // iframe | image
          url: &apos;https://paste.spiritlhl.net/#/show/VKMWQ.txt&apos;, // 测试结果
      },
  },
  // 其他服务器节点...
};
&amp;lt;/script&amp;gt;
&amp;lt;script
  defer
  src=&quot;https://cdn.jsdelivr.net/gh/DevilC0822/config/script/nezhaAgentNodeTestResult.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/DevilC0822/config/blob/refs/heads/main/script/nezhaAgentNodeTestResult.js&quot;&gt;脚本源代码&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;p&gt;仅在哪吒探针 V1 版本测试可用，且只支持官方主题。&lt;/p&gt;
</content:encoded></item><item><title>macOS 网络调优</title><link>https://www.mihouo.com/posts/server/macos-network-optimization/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/macos-network-optimization/</guid><description>解决 macOS 连接远距离服务器单线程下载限速问题</description><pubDate>Mon, 12 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近购买了一些美国西部、德国等物理距离较远的服务器，使用 &lt;code&gt;iperf3&lt;/code&gt; 以及 &lt;code&gt;speedtest&lt;/code&gt; 测试，发现单线程下载速度限速严重(200Mbps左右,我发现限速规律大概随着延迟的增大呈现正相关，经过测试是160ms 延迟限速在200Mbps左右，150ms 延迟限速在220Mbps左右，随着延迟的增加，限速会逐渐增加，速率会逐渐降低)。&lt;/p&gt;
&lt;h3&gt;现在的默认值&lt;/h3&gt;
&lt;p&gt;从 macOS 10.5 开始，Apple 实施了“自我调整 TCP”，它会动态调整某些参数以优化性能。虽然默认设置随着时间推移有所改进，但仍需进行调整以充分利用高速连接。&lt;/p&gt;
&lt;p&gt;最近的 macOS 版本带有以下默认设置（具体数值可能会根据版本略有不同）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;net.inet.tcp.win_scale_factor: 3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net.inet.tcp.autorcvbufmax: 4194304&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net.inet.tcp.autosndbufmax: 4194304&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;高速连接的建议设置&lt;/h3&gt;
&lt;p&gt;为了在 1Gbps 或更快的网络上优化性能，请增加 TCP 窗口缩放因子和自动调整缓冲区的最大值。将以下设置添加到/etc/sysctl.conf 文件中（或使用 sysctl 命令动态应用）：&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;net.inet.tcp.win_scale_factor&lt;/code&gt; 和 &lt;code&gt;net.inet.tcp.autorcvbufmax&lt;/code&gt; 和 &lt;code&gt;net.inet.tcp.autosndbufmax&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;修改方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sudo nano /etc/sysctl.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;添加如下内容&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Increase TCP window scaling factor (default is 3-6; increase to 8)
net.inet.tcp.win_scale_factor=8

# Increase maximum receive buffer for TCP autotuning to 32MB
net.inet.tcp.autorcvbufmax=33554432

# Increase maximum send buffer for TCP autotuning to 32MB
net.inet.tcp.autosndbufmax=33554432
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;保存后执行 &lt;code&gt;sudo sysctl -f /etc/sysctl.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;h3&gt;默认情况下&lt;/h3&gt;
&lt;h4&gt;iperf3 测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./image-1.1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;speedtest 测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./image-1.2.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;修改后&lt;/h3&gt;
&lt;h4&gt;iperf3 测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./image-2.1.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;speedtest 测试&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./image-2.2.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这些设置非常适合高性能使用场景，例如文件服务器、高吞吐量应用程序或研究环境。&lt;/li&gt;
&lt;li&gt;如果这些设置导致不稳定或降低了您特定使用场景的性能，请恢复设置。&lt;/li&gt;
&lt;li&gt;如果设置后发现 iperf3 Retr 数据包丢失严重，请调 macOS 整缓冲区大小，或者&lt;a href=&quot;https://omnitt.com/&quot;&gt;调整 VPS TCP 缓冲区大小&lt;/a&gt;。
:::&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用代理访问各AI平台的API</title><link>https://www.mihouo.com/posts/tool-share/llm-api-proxy/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/llm-api-proxy/</guid><description>使用代理访问各 AI 平台的 API</description><pubDate>Mon, 28 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://llm-proxy.mihouo.com/&quot;&gt;LLM Proxy&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;由于各个 AI 公司对于中国大陆限制了访问，导致我们无法直接访问这些地区的 API，所以需要使用代理来访问这些地区的 API。&lt;/p&gt;
&lt;p&gt;比如 OpenAI 的 API 访问限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中国大陆地区无法访问&lt;/li&gt;
&lt;li&gt;香港地区无法访问&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;在一台美国服务器上部署一个代理，然后使用这个代理来访问 AI API。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;我这里添加了 mongoose 来存储请求记录，所以需要安装 mongoose，如果你不需要则跳过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 初始化项目
npm init -y
# 安装 mongoose
npm install mongoose
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 pm2 来启动项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动项目
pm2 start llm-proxy.js --name llm-proxy
# 使用环境变量
MONGODB_URI=mongodb://localhost:27017/llm-proxy PORT=8088 HOST=0.0.0.0 pm2 start llm-proxy.js --name llm-proxy
# 查看项目
pm2 list
# 停止项目
pm2 stop llm-proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;h3&gt;代理代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// llm-proxy.js
// 导入 Node.js 内置模块
const http = require(&apos;http&apos;);
const https = require(&apos;https&apos;); // 需要根据目标URL的协议选择 http 或 https
const fs = require(&apos;fs&apos;);
const path = require(&apos;path&apos;);
const { URL } = require(&apos;url&apos;);
const mongoose = require(&apos;mongoose&apos;);

// 获取环境变量
const PORT = process.env.PORT || 8088;
const HOST = process.env.HOST || &apos;0.0.0.0&apos;;
const MONGODB_URI = process.env.MONGODB_URI || &apos;&apos;;

// 如果 MONGODB_URI 存在，则使用 MongoDB 存储请求记录
let AiProxyUsage = null;
if (MONGODB_URI) {
  try {
    mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true });
    const AiProxyUsageSchema = new mongoose.Schema({
      type: {
        type: String,
        required: true,
      },
      createTime: {
        type: String,
        required: true,
      },
    });
    AiProxyUsage = mongoose?.models?.AiProxyUsage || mongoose.model(&apos;AiProxyUsage&apos;, AiProxyUsageSchema);
  } catch (error) {
    console.error(`Error connecting to MongoDB: ${error}`);
  }
}

// API 路由映射
const apiMapping = {
  &apos;/discord&apos;: &apos;https://discord.com/api&apos;,
  &apos;/telegram&apos;: &apos;https://api.telegram.org&apos;,
  &apos;/openai&apos;: &apos;https://api.openai.com&apos;,
  &apos;/claude&apos;: &apos;https://api.anthropic.com&apos;,
  &apos;/gemini&apos;: &apos;https://generativelanguage.googleapis.com&apos;,
  &apos;/meta&apos;: &apos;https://www.meta.ai/api&apos;, // 注意：实际的Meta API可能不同
  &apos;/groq&apos;: &apos;https://api.groq.com/openai&apos;,
  &apos;/xai&apos;: &apos;https://api.x.ai&apos;,
  &apos;/cohere&apos;: &apos;https://api.cohere.ai&apos;,
  &apos;/huggingface&apos;: &apos;https://api-inference.huggingface.co&apos;,
  &apos;/together&apos;: &apos;https://api.together.xyz&apos;,
  &apos;/novita&apos;: &apos;https://api.novita.ai&apos;,
  &apos;/portkey&apos;: &apos;https://api.portkey.ai&apos;,
  &apos;/fireworks&apos;: &apos;https://api.fireworks.ai&apos;,
  &apos;/openrouter&apos;: &apos;https://openrouter.ai/api&apos;
};

// 允许转发到目标 API 的请求头列表（小写）
const allowedHeaders = [&apos;accept&apos;, &apos;content-type&apos;, &apos;authorization&apos;];

// 辅助函数：从路径名中提取匹配的前缀和剩余部分
function extractPrefixAndRest(pathname, prefixes) {
  for (const prefix of prefixes) {
    if (pathname.startsWith(prefix)) {
      return [prefix, pathname.slice(prefix.length)];
    }
  }
  return [null, null];
}

// 创建 HTTP 服务器
const server = http.createServer(async (req, res) =&amp;gt; {
  // 解析请求 URL
  // Node.js 的 req.url 只包含路径和查询参数，需要结合 host 来构造完整 URL
  const requestUrl = new URL(req.url, `http://${req.headers.host}`);
  const pathname = requestUrl.pathname;

  // 1. 处理根路径和 index.html -&amp;gt; 提供本地 HTML 文件
  if (pathname === &apos;/&apos; || pathname === &apos;/index.html&apos;) {
    const filePath = path.join(__dirname, &apos;index.html&apos;); // 假设 index.html 在脚本同级目录下
    fs.readFile(filePath, (err, data) =&amp;gt; {
      if (err) {
        console.error(`Error reading index.html: ${err}`);
        res.writeHead(500, { &apos;Content-Type&apos;: &apos;text/plain&apos; });
        res.end(&apos;Internal Server Error - Could not read index.html&apos;);
      } else {
        res.writeHead(200, { &apos;Content-Type&apos;: &apos;text/html&apos; });
        res.end(data);
      }
    });
    return; // 结束请求处理
  }

  // 2. 处理 robots.txt
  if (pathname === &apos;/robots.txt&apos;) {
    res.writeHead(200, { &apos;Content-Type&apos;: &apos;text/plain&apos; });
    res.end(&apos;User-agent: *\nDisallow: /&apos;);
    return; // 结束请求处理
  }

  // 3. 处理 API 代理
  const [prefix, rest] = extractPrefixAndRest(pathname, Object.keys(apiMapping));

  if (!prefix) {
    // 如果路径不匹配任何已知前缀，返回 404
    res.writeHead(404, { &apos;Content-Type&apos;: &apos;text/plain&apos; });
    res.end(&apos;Not Found&apos;);
    return; // 结束请求处理
  }

  // 构建目标 API 的 URL
  const targetBaseUrl = apiMapping[prefix];
  const targetUrl = `${targetBaseUrl}${rest}${requestUrl.search}`; // 包含查询参数

  // 准备转发请求的选项
  const options = {
    method: req.method,
    headers: {},
    // agent: targetBaseUrl.startsWith(&apos;https&apos;) ? new https.Agent({ keepAlive: true }) : new http.Agent({ keepAlive: true }) // 可选：使用 agent 进行连接复用
  };

  // 复制允许的请求头
  for (const key in req.headers) {
    if (allowedHeaders.includes(key.toLowerCase())) {
      options.headers[key] = req.headers[key];
    }
  }
  // 确保 host 头部设置为目标主机，而不是代理服务器的主机
  options.headers[&apos;host&apos;] = new URL(targetBaseUrl).host;


  // 选择 http 或 https 模块进行请求
  const protocol = targetBaseUrl.startsWith(&apos;https&apos;) ? https : http;

  // 创建代理请求
  const proxyReq = protocol.request(targetUrl, options, (proxyRes) =&amp;gt; {
    // 设置响应头
    // 复制目标服务器的响应头到客户端响应
    const responseHeaders = { ...proxyRes.headers }; // 复制一份

    // 添加额外的安全响应头
    responseHeaders[&apos;X-Content-Type-Options&apos;] = &apos;nosniff&apos;;
    responseHeaders[&apos;X-Frame-Options&apos;] = &apos;DENY&apos;;
    responseHeaders[&apos;Referrer-Policy&apos;] = &apos;no-referrer&apos;;
    // 可以选择性地删除或修改某些从目标服务器传来的头，例如 &apos;transfer-encoding&apos;
    // delete responseHeaders[&apos;transfer-encoding&apos;];

    // 将目标服务器的状态码和处理后的响应头写入客户端响应
    res.writeHead(proxyRes.statusCode, responseHeaders);

    // 将目标服务器的响应体流式传输到客户端响应
    proxyRes.pipe(res, { end: true });
  });

  // 处理代理请求错误
  proxyReq.on(&apos;error&apos;, (err) =&amp;gt; {
    console.error(`Proxy request error: ${err}`);
    if (!res.headersSent) {
      res.writeHead(502, { &apos;Content-Type&apos;: &apos;text/plain&apos; }); // 502 Bad Gateway
    }
    res.end(&apos;Bad Gateway&apos;);
  });

  // 使用 MongoDB 存储请求记录
  if (MONGODB_URI &amp;amp;&amp;amp; AiProxyUsage) {
    const usage = new AiProxyUsage({
      type: prefix,
      createTime: new Date().toISOString(),
    });
    usage.save();
  }

  // 将客户端请求体流式传输到代理请求
  // 对于 GET, HEAD 等没有 body 的请求，这不会做任何事
  req.pipe(proxyReq, { end: true });

});

// 启动服务器
server.listen(PORT, HOST, () =&amp;gt; {
  console.log(`Node.js proxy server running on http://${HOST}:${PORT}`);
  console.log(`Root path serves &apos;./index.html&apos;`);
  console.log(&apos;Proxying the following prefixes:&apos;);
  Object.keys(apiMapping).forEach(prefix =&amp;gt; {
    console.log(`  ${prefix} -&amp;gt; ${apiMapping[prefix]}`);
  });
});

// 可选：更优雅地处理关闭信号，以便 pm2 reload/stop
process.on(&apos;SIGINT&apos;, () =&amp;gt; {
  console.log(&apos;SIGINT signal received: closing HTTP server&apos;);
  server.close(() =&amp;gt; {
    console.log(&apos;HTTP server closed&apos;);
    process.exit(0);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;前端页面&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- index.html --&amp;gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;zh-CN&quot;&amp;gt;

&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
  &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
  &amp;lt;link rel=&quot;icon&quot;
    href=&quot;data:image/svg+xml,&amp;lt;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22&amp;gt;&amp;lt;text y=%22.9em%22 font-size=%2290%22&amp;gt;🚀&amp;lt;/text&amp;gt;&amp;lt;/svg&amp;gt;&quot;&amp;gt;
  &amp;lt;title&amp;gt;LLM Proxy - 简单、安全、高效&amp;lt;/title&amp;gt;
  &amp;lt;!-- seo --&amp;gt;
  &amp;lt;meta name=&quot;description&quot; content=&quot;LLM Proxy - 提供简单、安全、高效、免费的 API 代理服务，让您的 AI 应用更加稳定、快速。&quot;&amp;gt;
  &amp;lt;meta name=&quot;keywords&quot; content=&quot;LLM Proxy, API Proxy, AI, 代理, 简单, 安全, 高效&quot;&amp;gt;
  &amp;lt;meta name=&quot;robots&quot; content=&quot;index, follow&quot;&amp;gt;
  &amp;lt;meta name=&quot;googlebot&quot; content=&quot;index, follow&quot;&amp;gt;
  &amp;lt;meta name=&quot;bingbot&quot; content=&quot;index, follow&quot;&amp;gt;
  &amp;lt;script src=&quot;https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;script src=&quot;https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/lib/languages/javascript.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/atom-one-light.min.css&quot;&amp;gt;
  &amp;lt;script&amp;gt;
    // Tailwind 配置
    tailwind.config = {
      theme: {
        extend: {
          fontFamily: {
            sans: [&apos;-apple-system&apos;, &apos;BlinkMacSystemFont&apos;, &apos;SF Pro Display&apos;, &apos;SF Pro Text&apos;, &apos;Helvetica Neue&apos;, &apos;Arial&apos;, &apos;sans-serif&apos;],
            mono: [&apos;SF Mono&apos;, &apos;SFMono-Regular&apos;, &apos;Menlo&apos;, &apos;Monaco&apos;, &apos;Consolas&apos;, &apos;&quot;Liberation Mono&quot;&apos;, &apos;&quot;Courier New&quot;&apos;, &apos;monospace&apos;],
          },
          maxWidth: {
            &apos;screen-xl&apos;: &apos;1280px&apos;,
          },
          colors: {
            &apos;apple-blue&apos;: {
              50: &apos;#E8F2FF&apos;,
              100: &apos;#C9E0FF&apos;,
              200: &apos;#A6CDFF&apos;,
              300: &apos;#83BAFF&apos;,
              400: &apos;#5FA7FF&apos;,
              500: &apos;#3C94FF&apos;,
              600: &apos;#0071E3&apos;, // Apple 按钮蓝
              700: &apos;#0051A5&apos;,
              800: &apos;#00356B&apos;,
              900: &apos;#001A36&apos;,
            },
            &apos;apple-gray&apos;: {
              50: &apos;#F9F9F9&apos;, // 浅灰背景
              100: &apos;#F5F5F7&apos;, // Apple 官网背景灰
              200: &apos;#E8E8ED&apos;,
              300: &apos;#D2D2D7&apos;,
              400: &apos;#B1B1B6&apos;,
              500: &apos;#86868B&apos;, // Apple 官网文字灰
              600: &apos;#6E6E73&apos;,
              700: &apos;#414144&apos;,
              800: &apos;#2D2D2F&apos;,
              900: &apos;#1D1D1F&apos;, // Apple 官网深色文字
            }
          },
          boxShadow: {
            &apos;apple&apos;: &apos;0 2px 5px -1px rgba(0, 0, 0, 0.1), 0 2px 5px -1px rgba(0, 0, 0, 0.06)&apos;,
            &apos;apple-md&apos;: &apos;0 4px 12px -2px rgba(0, 0, 0, 0.08), 0 2px 6px -1px rgba(0, 0, 0, 0.05)&apos;,
            &apos;apple-lg&apos;: &apos;0 10px 20px -5px rgba(0, 0, 0, 0.1), 0 5px 10px -5px rgba(0, 0, 0, 0.04)&apos;,
          },
          borderRadius: {
            &apos;apple&apos;: &apos;10px&apos;,
            &apos;apple-sm&apos;: &apos;6px&apos;
          }
        }
      }
    }
  &amp;lt;/script&amp;gt;
  &amp;lt;style type=&quot;text/tailwindcss&quot;&amp;gt;
    body {
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      letter-spacing: -0.2px;
    }
    .copy-btn:disabled svg {
      color: theme(&apos;colors.green.500&apos;);
    }
    /* 苹果风格滚动条 */
    ::-webkit-scrollbar {
      width: 8px;
      height: 8px;
    }
    ::-webkit-scrollbar-track {
      background: #f3f4f6; /* gray-100 instead of apple-gray.100 */
      border-radius: 100px;
    }
    ::-webkit-scrollbar-thumb {
      background: #d1d5db; /* gray-300 instead of apple-gray.300 */
      border-radius: 100px;
    }
    ::-webkit-scrollbar-thumb:hover {
      background: #9ca3af; /* gray-400 instead of apple-gray.400 */
    }
    
    /* 代码高亮自定义样式 */
    pre {
      position: relative;
      background: #f9fafb; /* gray-50 instead of apple-gray.50 */
      border-radius: 0.375rem; /* rounded-md instead of borderRadius.apple-sm */
      margin: 0;
      padding: 1rem;
      overflow: auto;
    }
    pre code {
      font-family: theme(&apos;fontFamily.mono&apos;);
      font-size: 0.9rem;
      line-height: 1.5;
      display: block;
      padding: 0;
    }
    /* 关键词高亮样式 */
    .hljs-keyword {
      color: #0033cc;
      font-weight: 500;
    }
    /* 字符串高亮样式 */
    .hljs-string {
      color: #067d17;
    }
    /* 注释高亮样式 */
    .hljs-comment {
      color: #888888;
      font-style: italic;
    }
    /* 函数高亮样式 */
    .hljs-function {
      color: #6f42c1;
    }
    /* 数字高亮样式 */
    .hljs-number {
      color: #986801;
    }
    /* 属性高亮样式 */
    .hljs-attr, .hljs-property {
      color: #e36209;
    }
    .code-container {
      position: relative;
      border-radius: 0.375rem; /* rounded-md instead of borderRadius.apple-sm */
      box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* shadow-sm instead of boxShadow.apple */
      overflow: hidden;
      width: 100%;
      max-width: 100%;
      margin: 0 auto;
    }
    .code-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0.5rem 1rem;
      background: #f3f4f6; /* gray-100 instead of apple-gray.100 */
      border-bottom: 1px solid #e5e7eb; /* gray-200 instead of apple-gray.200 */
    }
    .code-title {
      font-size: 0.85rem;
      font-weight: 500;
      color: #4b5563; /* gray-600 instead of apple-gray.600 */
    }
    .code-copy-btn {
      background: transparent;
      border: none;
      cursor: pointer;
      padding: 0.25rem;
      border-radius: 4px;
      color: #6b7280; /* gray-500 instead of apple-gray.500 */
      transition: all 0.2s;
    }
    .code-copy-btn:hover {
      color: #2563eb; /* blue-600 instead of apple-blue.600 */
      background: rgba(0, 0, 0, 0.05);
    }
  &amp;lt;/style&amp;gt;
  &amp;lt;script&amp;gt;
    // 确保highlight.js在页面加载完成后立即执行高亮
    window.onload = function () {
      hljs.configure({
        languages: [&apos;javascript&apos;]
      });
      hljs.highlightAll();
    };
  &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;

&amp;lt;body class=&quot;bg-apple-gray-50 text-apple-gray-900 font-sans flex flex-col items-center min-h-screen py-8 px-4&quot;&amp;gt;

  &amp;lt;div class=&quot;w-full max-w-screen-xl space-y-6 md:space-y-8&quot;&amp;gt;

    &amp;lt;div class=&quot;bg-gradient-to-br from-blue-600 to-blue-400 rounded-apple shadow-apple-lg text-center p-8 md:p-12&quot;&amp;gt;
      &amp;lt;h1 class=&quot;text-4xl md:text-6xl font-medium tracking-tight mb-2 md:mb-3 text-white&quot;&amp;gt;LLM Proxy&amp;lt;/h1&amp;gt;
      &amp;lt;h2 class=&quot;text-xl md:text-2xl font-light mb-6 md:mb-8 text-white/90&quot;&amp;gt;简单、安全、高效&amp;lt;/h2&amp;gt;
      &amp;lt;p class=&quot;text-base md:text-xl leading-relaxed font-light text-white/90 mx-auto&quot;&amp;gt;
        使用美国西部服务器，免费提供简单、安全、高效的 API 代理服务，让您的 AI 应用更加稳定、快速。
      &amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;bg-white rounded-apple shadow-apple-md p-6 md:p-8&quot;&amp;gt;
      &amp;lt;h2 class=&quot;text-xl md:text-2xl font-medium text-apple-gray-900 mb-3 text-left&quot;&amp;gt;可用端点&amp;lt;/h2&amp;gt;
      &amp;lt;p class=&quot;text-sm md:text-base font-light text-apple-gray-600 mb-6&quot;&amp;gt;选择并配置您需要的 AI 服务接口&amp;lt;/p&amp;gt;
      &amp;lt;div class=&quot;overflow-x-auto rounded-apple-sm&quot;&amp;gt;
        &amp;lt;table class=&quot;w-full text-left text-sm md:text-base table-fixed&quot;&amp;gt;
          &amp;lt;thead&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;th class=&quot;w-[18%] pb-3 pt-2 px-3 md:px-4 font-medium text-apple-gray-600 border-b border-apple-gray-200&quot;&amp;gt;
                服务
              &amp;lt;/th&amp;gt;
              &amp;lt;th class=&quot;w-[42%] pb-3 pt-2 px-3 md:px-4 font-medium text-apple-gray-600 border-b border-apple-gray-200&quot;&amp;gt;
                代理地址
              &amp;lt;/th&amp;gt;
              &amp;lt;th class=&quot;w-[40%] pb-3 pt-2 px-3 md:px-4 font-medium text-apple-gray-600 border-b border-apple-gray-200&quot;&amp;gt;
                原始地址&amp;lt;/th&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/thead&amp;gt;
          &amp;lt;tbody class=&quot;divide-y divide-apple-gray-200&quot;&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;OpenAI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/openai&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/openai&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://platform.openai.com/docs/api-reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.openai.com&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Anthropic Claude&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/claude&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/claude&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://docs.anthropic.com/claude/reference/getting-started-with-the-api&quot; target=&quot;_blank&quot;
                  rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.anthropic.com&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Google Gemini&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/gemini&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/gemini&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://ai.google.dev/api/rest&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://generativelanguage.googleapis.com&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Groq&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/groq&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/groq&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://console.groq.com/docs/api-reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.groq.com&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Meta AI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/meta&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/meta&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://developers.facebook.com/docs/graph-api&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://developers.facebook.com/docs/graph-api&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Cohere&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/cohere&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/cohere&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://docs.cohere.com/reference/about&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.cohere.ai&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Together AI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/together&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/together&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://docs.together.ai/reference/inference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.together.xyz&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;X.ai (Grok)&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/xai&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/xai&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://docs.x.ai/&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.x.ai&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Hugging Face&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/huggingface&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/huggingface&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://huggingface.co/docs/api-inference/index&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api-inference.huggingface.co&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;OpenRouter&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/openrouter&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/openrouter&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://openrouter.ai/docs&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://openrouter.ai/api&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Fireworks AI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/fireworks&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/fireworks&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://readme.fireworks.ai/reference/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.fireworks.ai&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Portkey AI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/portkey&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/portkey&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://portkey.ai/docs/api-reference/introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.portkey.ai&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Novita AI&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/novita&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/novita&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://docs.novita.ai/api-reference&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://api.novita.ai&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Discord&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/discord&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/discord&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://discord.com/developers/docs/intro&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://discord.com/api&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
            &amp;lt;tr&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 font-medium text-apple-gray-800 align-middle&quot;&amp;gt;Telegram&amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;div class=&quot;flex items-center gap-1&quot;&amp;gt;
                  &amp;lt;span
                    class=&quot;font-mono text-apple-gray-600 break-words min-w-0 text-sm&quot;&amp;gt;https://llm-proxy.mihouo.com/telegram&amp;lt;/span&amp;gt;
                  &amp;lt;button
                    class=&quot;copy-btn flex-shrink-0 p-1.5 rounded-full text-apple-gray-500 hover:bg-apple-gray-100 hover:text-apple-blue-600 focus:outline-none focus:ring-1 focus:ring-apple-blue-500 transition-colors duration-150&quot;
                    data-copy-text=&quot;https://llm-proxy.mihouo.com/telegram&quot; aria-label=&quot;复制地址&quot;&amp;gt;
                  &amp;lt;/button&amp;gt;
                &amp;lt;/div&amp;gt;
              &amp;lt;/td&amp;gt;
              &amp;lt;td class=&quot;py-3.5 px-3 md:px-4 align-middle&quot;&amp;gt;
                &amp;lt;a href=&quot;https://core.telegram.org/bots/api&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;
                  class=&quot;font-mono text-apple-blue-600 hover:underline break-words text-sm&quot;&amp;gt;https://core.telegram.org/bots/api&amp;lt;/a&amp;gt;
              &amp;lt;/td&amp;gt;
            &amp;lt;/tr&amp;gt;
          &amp;lt;/tbody&amp;gt;
        &amp;lt;/table&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;!-- 使用示例 --&amp;gt;
    &amp;lt;div class=&quot;bg-white rounded-apple shadow-apple-md p-6 md:p-8 text-center&quot;&amp;gt;
      &amp;lt;h2 class=&quot;text-xl md:text-2xl font-medium mb-4 text-apple-gray-800&quot;&amp;gt;使用示例&amp;lt;/h2&amp;gt;
      &amp;lt;p class=&quot;text-base md:text-lg font-light text-apple-gray-600 mb-6&quot;&amp;gt;通过几行简单的代码，即可轻松连接到 API 服务&amp;lt;/p&amp;gt;

      &amp;lt;div class=&quot;code-container mx-auto text-left&quot;&amp;gt;
        &amp;lt;div class=&quot;code-header&quot;&amp;gt;
          &amp;lt;span class=&quot;code-title&quot;&amp;gt;JavaScript&amp;lt;/span&amp;gt;
          &amp;lt;button class=&quot;code-copy-btn&quot; aria-label=&quot;复制代码&quot;&amp;gt;
            &amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; fill=&quot;currentColor&quot; class=&quot;w-4 h-4&quot;
              viewBox=&quot;0 0 16 16&quot;&amp;gt;
              &amp;lt;path
                d=&quot;M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z&quot;&amp;gt;
              &amp;lt;/path&amp;gt;
              &amp;lt;path
                d=&quot;M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z&quot;&amp;gt;
              &amp;lt;/path&amp;gt;
            &amp;lt;/svg&amp;gt;
          &amp;lt;/button&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;pre&amp;gt;&amp;lt;code class=&quot;language-javascript&quot;&amp;gt;// 导入 OpenAI 库
const { OpenAI } = require(&apos;openai&apos;);

// 创建 API 客户端实例
const openai = new OpenAI({
  apiKey: &apos;sk-**************************************&apos;,
  baseURL: &apos;https://llm-proxy.mihouo.com/openai/v1&apos;,
});

// 发送请求示例
async function generateResponse() {
  const response = await openai.chat.completions.create({
    model: &quot;gpt-3.5-turbo&quot;,
    messages: [
      { role: &quot;system&quot;, content: &quot;你是一个专业的AI助手。&quot; },
      { role: &quot;user&quot;, content: &quot;你好，请介绍一下自己。&quot; }
    ],
    temperature: 0.7,
  });
  
  console.log(response.choices[0].message.content);
}&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;bg-white rounded-apple shadow-apple-md p-6 text-center&quot;&amp;gt;
      &amp;lt;div class=&quot;flex flex-col md:flex-row items-center justify-center space-y-2 md:space-y-0 md:space-x-8&quot;&amp;gt;
        &amp;lt;p class=&quot;flex items-center text-apple-gray-500 text-sm&quot;&amp;gt;
          &amp;lt;span class=&quot;inline-block w-2.5 h-2.5 bg-green-500 rounded-full mr-2&quot;&amp;gt;&amp;lt;/span&amp;gt;
          &amp;lt;span class=&quot;font-light&quot;&amp;gt;状态：&amp;lt;/span&amp;gt;
          &amp;lt;span class=&quot;text-apple-gray-700 font-medium ml-1&quot;&amp;gt;运行中&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
        &amp;lt;p class=&quot;text-sm text-apple-gray-500&quot;&amp;gt;
          &amp;lt;span class=&quot;font-light&quot;&amp;gt;北京时间：&amp;lt;/span&amp;gt;
          &amp;lt;span id=&quot;live-time&quot; class=&quot;font-medium text-apple-gray-700 ml-1&quot;&amp;gt;加载中...&amp;lt;/span&amp;gt;
        &amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

  &amp;lt;/div&amp;gt;
  &amp;lt;script&amp;gt;
    // 时间更新脚本
    function updateLiveTime() {
      const timeElement = document.getElementById(&apos;live-time&apos;);
      if (timeElement) {
        const now = new Date();
        const beijingTime = now.toLocaleString(&apos;zh-CN&apos;, {
          timeZone: &apos;Asia/Shanghai&apos;,
          hour12: false,
          year: &apos;numeric&apos;, month: &apos;2-digit&apos;, day: &apos;2-digit&apos;,
          hour: &apos;2-digit&apos;, minute: &apos;2-digit&apos;, second: &apos;2-digit&apos;
        });
        timeElement.textContent = beijingTime.replace(/[\/]/g, &apos;-&apos;).replace(/\s+/g, &apos; &apos;).trim();
      }
    }
    updateLiveTime();
    setInterval(updateLiveTime, 1000);

    // --- 复制按钮脚本 ---
    document.addEventListener(&apos;DOMContentLoaded&apos;, () =&amp;gt; {
      const copyButtons = document.querySelectorAll(&apos;.copy-btn&apos;);

      // 确保在DOM加载完成后再次尝试高亮
      hljs.configure({
        languages: [&apos;javascript&apos;]
      });
      hljs.highlightAll();

      // 代码块复制功能
      const codeBlock = document.querySelector(&apos;pre code&apos;);
      const codeCopyBtn = document.querySelector(&apos;.code-copy-btn&apos;);

      if (codeBlock &amp;amp;&amp;amp; codeCopyBtn) {
        codeCopyBtn.addEventListener(&apos;click&apos;, () =&amp;gt; {
          const code = codeBlock.textContent;

          navigator.clipboard.writeText(code).then(() =&amp;gt; {
            // 复制成功反馈
            const originalSvg = codeCopyBtn.innerHTML;

            // 替换为成功图标
            codeCopyBtn.innerHTML = `&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; fill=&quot;currentColor&quot; class=&quot;w-4 h-4 text-green-500&quot; viewBox=&quot;0 0 16 16&quot;&amp;gt;&amp;lt;path d=&quot;M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z&quot;/&amp;gt;&amp;lt;/svg&amp;gt;`;

            // 1.5秒后恢复原始图标
            setTimeout(() =&amp;gt; {
              codeCopyBtn.innerHTML = originalSvg;
            }, 1500);
          }).catch(err =&amp;gt; {
            console.error(&apos;无法复制代码: &apos;, err);
            alert(&apos;复制失败，请手动复制。&apos;);
          });
        });
      }

      // SVG Icons - 苹果风格图标
      const originalIconSvg = `&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; fill=&quot;currentColor&quot; class=&quot;w-4 h-4&quot; viewBox=&quot;0 0 16 16&quot;&amp;gt;&amp;lt;path d=&quot;M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z&quot;/&amp;gt;&amp;lt;path d=&quot;M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z&quot;/&amp;gt;&amp;lt;/svg&amp;gt;`;
      const successIconSvg = `&amp;lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; width=&quot;16&quot; height=&quot;16&quot; fill=&quot;currentColor&quot; class=&quot;w-4 h-4&quot; viewBox=&quot;0 0 16 16&quot;&amp;gt;&amp;lt;path d=&quot;M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425z&quot;/&amp;gt;&amp;lt;/svg&amp;gt;`;

      copyButtons.forEach(button =&amp;gt; {
        button.innerHTML = originalIconSvg; // 设置初始图标

        button.addEventListener(&apos;click&apos;, () =&amp;gt; {
          const textToCopy = button.dataset.copyText;

          if (!textToCopy) {
            console.error(&apos;无复制内容&apos;);
            return;
          }
          if (!navigator.clipboard) {
            console.error(&apos;剪贴板 API 不可用&apos;);
            alert(&apos;抱歉，您的浏览器不支持或未启用剪贴板功能。&apos;);
            return;
          }

          navigator.clipboard.writeText(textToCopy).then(() =&amp;gt; {
            // 成功反馈 - 苹果风格的平滑过渡
            button.innerHTML = successIconSvg;
            button.disabled = true;

            setTimeout(() =&amp;gt; {
              button.innerHTML = originalIconSvg; // 恢复图标
              button.disabled = false;
            }, 1500); // 1.5秒后恢复

          }).catch(err =&amp;gt; {
            console.error(&apos;无法复制文本: &apos;, err);
            alert(&apos;复制失败，请手动复制。&apos;);
          });
        });
      });
    });
  &amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;

&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Bage 德国 机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-bage-germany-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-bage-germany-hybrid-machine/</guid><description>Bage 德国机器留档测试</description><pubDate>Sun, 27 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 10 GiB&lt;/li&gt;
&lt;li&gt;带宽 2 Gbps&lt;/li&gt;
&lt;li&gt;流量 2 TB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                      IP质量体检报告：193.160.*.*
                    bash &amp;lt;(curl -sL Check.Place) -I
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-27 14:54:05 CST  脚本版本：v2025-04-24
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS63150
组织：                  BAGE
坐标：                  9°29′28″E, 51°17′57″N
地图：                  https://check.place/51.2993,9.491,15,cn
城市：                  N/A
使用地：                [DE]德国, [EU]欧洲
注册地：                [DE]德国
时区：                  Europe/Berlin
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     家宽        家宽        机房        家宽        机房    
公司类型：     家宽        家宽        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：   3|低风险
ipapi：    0.00%|极低风险
AbuseIPDB：    0|低风险
IPQS：         0|低风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]
代理：     否      否      否      否      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      否      否      否      否      无      否      否 
服务器：   是      是      否      无      否      否      否      否 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     解锁     解锁     解锁     解锁     解锁     屏蔽     解锁   
地区：     [DE]     [DE]     [DE]     [DE]     [DE]              [DE]   
方式：     原生     原生     原生     原生     原生              原生   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 423   已标记 16   黑名单 0
========================================================================
今日IP检测量：591；总检测量：210022。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/ip/1KTBKJG38.svg

########################################################################
                 IP质量体检报告：2602:f988:84:*:*:*:*:*
                    bash &amp;lt;(curl -sL Check.Place) -I
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-27 14:54:05 CST  脚本版本：v2025-04-24
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS63150
组织：                  BAGE
坐标：                  8°34′27″E, 50°2′18″N
地图：                  https://check.place/50.0383,8.5743,15,cn
城市：                  黑森州, 法兰克福, 60549
使用地：                [DE]德国, [EU]欧洲
注册地：                [US]美国
时区：                  Europe/Berlin
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     家宽        家宽        机房        家宽        机房    
公司类型：     家宽        家宽        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：   3|低风险
ipapi：    0.00%|极低风险
AbuseIPDB：    0|低风险
IPQS：         0|低风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]    [DE]
代理：     否      否      否      否      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      否      否      否      否      无      否      否 
服务器：   是      是      否      无      否      否      否      否 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     解锁     解锁     解锁     屏蔽     屏蔽     失败   
地区：              [DE]     [DE]     [DE]                              
方式：              原生     原生     原生                              
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：593；总检测量：210024。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/ip/2KEI8BGVI.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.04.12
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址 [推荐]：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : AMD Ryzen 9 7950X 16-Core Processor
 CPU 核心数        : 1
 CPU 频率          : 4491.540 MHz
 CPU 缓存          : L1: 64.00 KB / L2: 512.00 KB / L3: 16.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ✔ Enabled
 内存              : 190.27 MiB / 960.69 MiB
 Swap              : 268 KiB / 2.00 GiB
 硬盘空间          : 4.03 GiB / 9.76 GiB
 启动盘路径        : /dev/vda1
 系统在线时间      : 0 days, 4 hour 43 min
 负载              : 0.53, 0.28, 0.11
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-33-amd64
 TCP加速方式       : bbr
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS63150 BAGE CLOUD LLC
 IPV4 位置         : Frankfurt am Main / Hesse / DE
 IPV6 ASN          : AS63150 BAGE
 IPV6 位置         : Frankfurt am Main / Hesse / Germany
 IPV6 子网掩码     : 128
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          5812 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          70247.97 MB/s
 单线程写测试:          38884.14 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         55.7 MB/s (13.60 IOPS, 1.88s))          82.2 MB/s (20078 IOPS, 1.27s)
 1GB-1M Block           11.1 GB/s (10.57 IOPS, 0.09s))          17.1 GB/s (16303 IOPS, 0.06s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 198.20 MB/s  (49.5k) | 791.39 MB/s  (12.3k)
Write      | 198.72 MB/s  (49.6k) | 795.55 MB/s  (12.4k)
Total      | 396.92 MB/s  (99.2k) | 1.58 GB/s    (24.7k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 1.18 GB/s     (2.3k) | 1.16 GB/s     (1.1k)
Write      | 1.25 GB/s     (2.4k) | 1.24 GB/s     (1.2k)
Total      | 2.43 GB/s     (4.7k) | 2.40 GB/s     (2.3k)
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：德国
[IPV6]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：德国
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 德国法兰克福(FRA15S37)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 德国法兰克福(FRA16S31)
---------------DisneyPlus---------------
[IPV4]
当前IPv4出口所在地区即将开通DisneyPlus
[IPV6]
当前IPv4出口所在地区即将开通DisneyPlus
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Yes (Region: DE)
 Disney+:                               Yes (Region: DE)
 Netflix:                               Yes (Region: DE)
 YouTube Premium:                       Yes (Region: DE)
 Amazon Prime Video:                    Yes (Region: DE)
 TVBAnywhere+:                          Yes
 Spotify Registration:                  No
 OneTrust Region:                       DE [Unknown]
 iQyi Oversea Region:                   DE
 Bing Region:                           DE
 Apple Region:                          DE
 YouTube CDN:                           Frankfurt
 Netflix Preferred CDN:                 Frankfurt
 ChatGPT:                               Yes
 Google Gemini:                         No
 Claude:                                No
 Wikipedia Editability:                 Yes
 Google Play Store:                     Germany 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        EUR
 ---Forum---
 Reddit:                                Yes
 ---Game---
 SD Gundam G Generation Eternal:        Yes
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Yes (Region: DE)
 YouTube Premium:                       Yes (Region: DE)
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  No
 OneTrust Region:                       DE [Hesse]
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           DE
 Apple Region:                          DE
 YouTube CDN:                           Frankfurt
 Netflix Preferred CDN:                 Frankfurt
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         No
 Claude:                                No
 Wikipedia Editability:                 Yes
 Google Play Store:                     Germany 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
 ---Game---
 SD Gundam G Generation Eternal:        Failed (Network Connection)
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         Failed
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 92 [8] 
VPN得分(越低越好): 4 [8] 
代理得分(越低越好): 18 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 2 [8] 
欺诈得分(越低越好): 3 [1] 0 [E]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0017 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
威胁级别: low [9] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: isp [0 7] FixedLineISP [3] hosting [A] unknown [C] consumer [9]
公司类型: hosting [A] isp [0 7]
是否云提供商: No [7 D] 
是否数据中心: No [0 5 6 8 C] Yes [1 A]
是否移动设备: No [A C] Yes [5 E]
是否代理: No [0 1 4 5 6 7 8 9 A C D E] 
是否VPN: No [0 1 6 7 A C D E] 
是否Tor: No [0 1 3 6 7 8 A C D E] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A E] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D E] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 0(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 3 [1] 
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0017 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
安全信息:
使用类型: hosting [A] FixedLineISP [3]
公司类型: hosting [A] 
是否云提供商: No [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A D] 
是否VPN: No [1 A D] 
是否Tor: No [1 3 A D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 313(Other) 
Google搜索可行性：NO
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✘     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✔     ✔     ✘     ✔     ✘    
Sohu      ✘     ✔     ✔     ✘     ✔     ✘    
Yandex    ✘     ✔     ✔     ✘     ✔     ✘    
Gmail     ✘     ✔     ✘     ✘     ✘     ✘    
Outlook   ✘     ✘     ✔     ✘     ✔     ✘    
Office365 ✘     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘    
MailRU    ✘     ✔     ✘     ✘     ✔     ✘    
AOL       ✘     ✔     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✔     ✔     ✘     ✔     ✘    
Apple     ✘     ✔     ✘     ✘     ✘     ✘    
FastMail  ✘     ✔     ✘     ✘     ✘     ✘    
ProtonMail✘     ✘     ✘     ✘     ✘     ✘    
MXRoute   ✘     ✘     ✔     ✘     ✔     ✘    
Namecrane ✘     ✔     ✔     ✘     ✔     ✘    
XYAMail   ✘     ✘     ✘     ✘     ✘     ✘    
ZohoMail  ✘     ✔     ✘     ✘     ✘     ✘    
Inbox_eu  ✘     ✔     ✔     ✘     ✘     ✘    
Free_fr   ✘     ✔     ✔     ✘     ✔     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信v4 219.141.140.10           电信163    [普通线路] 
北京联通v4 202.106.195.68           联通4837   [普通线路] 
北京移动v4 221.179.155.161          移动CMI    [普通线路] 
上海电信v4 202.96.209.133           电信163    [普通线路] 
上海联通v4 210.22.97.1              联通4837   [普通线路] 
上海移动v4 211.136.112.200          移动CMI    [普通线路] 
广州电信v4 58.60.188.222            电信163    [普通线路] 
广州联通v4 210.21.196.6             联通4837   [普通线路] 
广州移动v4 120.196.165.24           移动CMI    [普通线路] 
成都电信v4 61.139.2.69              电信163    [普通线路] 
成都联通v4 119.6.6.6                联通4837   [普通线路] 
成都移动v4 211.137.96.205           移动CMI    [普通线路] 移动CMIN2  [精品线路] 
北京电信v6 2400:89c0:1053:3::69     电信163    [普通线路] 
北京联通v6 2400:89c0:1013:3::54     联通4837   [普通线路] 
北京移动v6 2409:8c00:8421:1303::55  移动CMIN2  [精品线路] 移动CMI    [普通线路] 
上海电信v6 240e:e1:aa00:4000::24    电信163    [普通线路] 
上海联通v6 2408:80f1:21:5003::a     联通4837   [普通线路] 
上海移动v6 2409:8c1e:75b0:3003::26  移动CMIN2  [精品线路] 移动CMI    [普通线路] 
广州电信v6 240e:97c:2f:3000::44     电信163    [普通线路] 
广州联通v6 2408:8756:f50:1001::c    联通4837   [普通线路] 
广州移动v6 2409:8c54:871:1001::12   移动CMI    [普通线路] 
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自nexttrace，请知悉!
广州电信 58.60.188.222
0.46 ms         AS63150 德国 图林根州 埃尔福特 bagevm.com
0.79 ms         AS58212 德国 黑森 美因河畔法兰克福 dataforest.net
1.81 ms         AS4134 德国 黑森州 美因河畔法兰克福 www.chinatelecom.com.cn
230.01 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn
235.42 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
238.49 ms       AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
0.43 ms         AS63150 德国 图林根州 埃尔福特 bagevm.com
0.65 ms         AS58212 德国 黑森 美因河畔法兰克福 dataforest.net
1.14 ms         AS4837 [CU169-BACKBONE] 德国 黑森州 美因河畔法兰克福 chinaunicom.cn
135.39 ms       AS4837 [CU169-BACKBONE] 中国 北京 X-I chinaunicom.cn 联通
154.08 ms       AS4837 [CU169-BACKBONE] 中国 北京 chinaunicom.cn 联通
156.50 ms       AS4837 [CU169-BACKBONE] 中国 北京 chinaunicom.cn 联通
182.91 ms       AS17816 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
194.24 ms       AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
189.54 ms       AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
0.44 ms         AS63150 德国 图林根州 埃尔福特 bagevm.com
0.58 ms         AS58212 德国 黑森 美因河畔法兰克福 dataforest.net
51.36 ms        AS6453 德国 黑森 美因河畔法兰克福 tatacommunications.com
2.34 ms         AS6453 [TATA-R] 德国 黑森 美因河畔法兰克福 tatacommunications.com
18.74 ms        AS58453 [CMI-INT] 德国 黑森 美茵河畔法兰克福 cmi.chinamobile.com 移动
216.19 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
* ms    AS9808 [CMNET] 中国 广东 广州 I-C chinamobileltd.com 移动
213.70 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
215.81 ms       AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟
Speedtest.net    1271.67Mbps     1715.19Mbps     1.194.003.00ms
法兰克福         1329.86Mbps     1681.26Mbps     1.694.004.00ms
洛杉矶           36.24Mbps       66.25Mbps       151.09140.00139.00ms
联通上海5G       31.53Mbps       59.91Mbps       168.67165.00167.00ms
电信Zhenjiang5G  1.66Mbps        36.56Mbps       236.06520.00319.00ms
电信Suzhou5G     23.81Mbps       63.92Mbps       226.78356.00244.00ms
移动Chengdu      12.49Mbps       41.88Mbps       225.50248.00229.00ms
移动杭州5G       27.15Mbps       41.52Mbps       242.95193.00248.00ms
------------------------------------------------------------------------
 总共花费      : 6 分 12 秒
 时间          : Sun Apr 27 15:01:32 CST 2025
------------------------------------------------------------------------
  短链:
    https://paste.spiritlhl.net/#/show/BF4RE.txt
    http://hpaste.spiritlhl.net/#/show/BF4RE.txt
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Emby 302 直链播放</title><link>https://www.mihouo.com/posts/docker/emby-direct-link-playback/</link><guid isPermaLink="true">https://www.mihouo.com/posts/docker/emby-direct-link-playback/</guid><description>使用 Emby 302 直链播放 Alist 挂载的网盘内容</description><pubDate>Fri, 18 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前分享过&lt;a href=&quot;/posts/docker/emby-cracked-version-installation-and-usage/&quot;&gt;Emby 开心版安装和使用&lt;/a&gt;，那里使用的是使用 rclone 将网盘挂载本地，然后通过 Emby 添加媒体库播放。这种不单配置较为麻烦，而且还会在播放时占用服务器带宽。&lt;/p&gt;
&lt;p&gt;相较于上篇，这里的配置更为简单，且对低配置服务器更为友好。&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一台服务器&lt;/li&gt;
&lt;li&gt;一个域名（可选）&lt;/li&gt;
&lt;li&gt;网盘&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;大致流程&lt;/h2&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://alist.nn.ci/zh/&quot;&gt;Alist&lt;/a&gt; 挂载网盘，然后通过 &lt;a href=&quot;https://github.com/Akimio521/AutoFilm&quot;&gt;AutoFilm&lt;/a&gt; 项目将 Alist 中的内容遍历生成 Strm 文件，在通过 Emby 添加本地媒体库，最后再通过 &lt;a href=&quot;https://nginx.org/&quot;&gt;Nginx&lt;/a&gt; 转发实现 302 播放。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;docker-compose&lt;/code&gt; 安装，&lt;code&gt;docker-compose.yml&lt;/code&gt; 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  embyserver:
    image: amilys/embyserver
    platform: linux/amd64   # 强制使用 amd64 架构（我这里使用的是 arm64 版本，如果使用 amd64 版本，请删除此行）
    container_name: emby-happy
    privileged: true
    network_mode: bridge
    volumes:
      - ./config:/config
      - ./Movies:/movies:rslave
    ports:
      - 8096:8096
    restart: unless-stopped

  alist:
    image: xhofe/alist:latest
    container_name: alist
    volumes:
        - ./alist:/opt/alist/data
    network_mode: bridge
    ports:
        - 5244:5244
    environment:
        - PUID=0
        - PGID=0
        - UMASK=022
    restart: unless-stopped

  nginx:
    image: nginx:latest
    container_name: nginx
    network_mode: host
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/embyCache:/var/cache/nginx/emby
      - ./nginx/logs:/var/log/nginx
    restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时可以在浏览器中访问 &lt;a href=&quot;http://localhost:8096&quot;&gt;http://localhost:8096&lt;/a&gt; 即可访问 Emby。&lt;/p&gt;
&lt;h3&gt;Alist 挂载&lt;/h3&gt;
&lt;p&gt;访问 &lt;a href=&quot;http://127.0.0.1:5244&quot;&gt;http://127.0.0.1:5244&lt;/a&gt;,默认账号为 &lt;code&gt;admin&lt;/code&gt;，密码需要通过 &lt;code&gt;docker logs alist&lt;/code&gt; 查看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也可以通过 &lt;code&gt;docker exec -it alist /bin/bash&lt;/code&gt; 进入容器手动设置 &lt;code&gt;./alist admin set password&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;挂载参考&lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/aliyundrive_open.html&quot;&gt;官方教程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果你使用的是阿里云盘可以参考&lt;a href=&quot;/posts/apple-tv/using-alist-on-apple-tv/#%E8%8E%B7%E5%8F%96%E9%98%BF%E9%87%8C%E4%BA%91%E7%9B%98-tv-%E7%89%88%E7%9A%84-token&quot;&gt;获取阿里云盘 TV 版的 token&lt;/a&gt;实现不限速挂载。&lt;/p&gt;
&lt;p&gt;需要记录内容:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Alist 地址 &lt;code&gt;http://192.168.2.3:5244&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Alist Token 进入管理后台，&lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;其他&lt;/code&gt; -&amp;gt; &lt;code&gt;令牌&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Alist 账号密码&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;AutoFilm 生成 Strm 文件&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;Akimio521/AutoFilm&quot;}&lt;/p&gt;
&lt;p&gt;AutoFilm 也可以通过 docker 部署，我这里需要修改配置文件，所以选择通过源码运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/Akimio521/AutoFilm.git
cd AutoFilm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考 &lt;code&gt;/config/config.example.yaml&lt;/code&gt; 文件，配置 &lt;code&gt;config/config.yaml&lt;/code&gt; 文件。&lt;/p&gt;
&lt;p&gt;:::important
我这里在使用 AutoFilm 时运行会报错，经过修改后可以正常运行。
:::&lt;/p&gt;
&lt;h3&gt;Emby 配置&lt;/h3&gt;
&lt;p&gt;将 AutoFilm 生成的 Strm 文件放入映射目录 &lt;code&gt;Movies&lt;/code&gt; 中。
然后进入 &lt;a href=&quot;http://127.0.0.1:8096&quot;&gt;http://127.0.0.1:8096&lt;/a&gt; ，点击 &lt;code&gt;媒体库&lt;/code&gt; -&amp;gt; &lt;code&gt;新增媒体库&lt;/code&gt; -&amp;gt; 文件夹选择 &lt;code&gt;movies&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;Nginx 配置&lt;/h3&gt;
&lt;p&gt;接下来下载此&lt;a href=&quot;https://nanako-1253183981.cos.ap-guangzhou.myqcloud.com/article/p%3D162/nginx.zip&quot;&gt;压缩包&lt;/a&gt;，解压后得到一个nginx文件夹，其中有conf.d文件夹及nginx.conf文件，此为nginx相关配置文件。配置文件来源&lt;a href=&quot;https://blog.nanako.vip/&quot;&gt;nanako&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;需要修改的文件有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;conf.d/constant.js&lt;/li&gt;
&lt;li&gt;conf.d/emby.conf (可选，修改 Emby反代端口)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;具体配置注释都有详细说明，这里不再赘述。配置完成后重启 Nginx 服务。&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;如果你未更改 &lt;code&gt;conf.d/emby.conf&lt;/code&gt; 文件，则直接访问 &lt;a href=&quot;http://127.0.0.1:8091&quot;&gt;http://127.0.0.1:8091&lt;/a&gt; 即可访问 Emby。&lt;/p&gt;
&lt;p&gt;Strm 暂时无法使用网页端播放，需要使用&lt;a href=&quot;/posts/apple-tv/my-apple-tv-movie-watching-setup/#%E8%A7%82%E5%BD%B1app&quot;&gt;客户端&lt;/a&gt;播放。&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;h3&gt;浏览测试&lt;/h3&gt;
&lt;p&gt;可以看到浏览时流量走的 emby.mihouo.com 域名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;播放测试&lt;/h3&gt;
&lt;p&gt;播放时的流量走的是 aliyundrive.cloud 域名。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;使用 Emby 302 直链播放可以避免服务器带宽被占用(只有浏览emby 时加载的一些影片信息会产生流量，播放时走的 302 直链)，也能大幅提升播放速度。&lt;/p&gt;
</content:encoded></item><item><title>DMIT EB WEE 美国机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-dmits-us-eb-wee-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-dmits-us-eb-wee-hybrid-machine/</guid><description>DMIT EB WEE 美国机器留档测试</description><pubDate>Fri, 11 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 20 GiB&lt;/li&gt;
&lt;li&gt;带宽 1000 Mbps&lt;/li&gt;
&lt;li&gt;流量 1024 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                    IP质量体检报告(Lite)：154.17.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:50:13 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS906
组织：                  DMIT Cloud Services
坐标：                  118°15′60″W, 34°3′21″N
地图：                  https://check.place/34.0559,-118.2666,12,cn
城市：                  Los Angeles, 90017
使用地：                [US]United States of America, Americas
注册地：                [US]United States of America
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     商业        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：             19|低风险
ipapi：        0.22%|低风险
Cloudflare：   0|低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [US]    [US]    [US]    [US]    [US]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      否      否      否      否 
服务器：   是      否      否      否      否 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     解锁     屏蔽     解锁     解锁     解锁     屏蔽     解锁   
地区：     [US]              [US]     [US]     [US]              [US]   
方式：     原生              原生     原生     原生              原生   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 391   已标记 48   黑名单 0
========================================================================
今日IP检测量：171；总检测量：175277。感谢使用xy系列脚本！ 

########################################################################
              IP质量体检报告(Lite)：2605:52c0:1:*:*:*:*:*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:50:13 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS906
组织：                  DMIT Cloud Services
坐标：                  118°15′60″W, 34°3′21″N
地图：                  https://check.place/34.0559,-118.2666,12,cn
城市：                  Los Angeles, 90017
使用地：                [US]United States of America, Americas
注册地：                [US]United States of America
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：             19|低风险
ipapi：    0.00%|极低风险
Cloudflare：   0|低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [US]    [US]    [US]    [US]    [US]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      否      否      否      否 
服务器：   是      是      否      是      否 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     屏蔽     解锁     解锁     屏蔽     屏蔽     失败   
地区：                       [US]     [US]                              
方式：                       原生     原生                              
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：175；总检测量：175281。感谢使用xy系列脚本！ 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.03.29
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : AMD EPYC 9654 96-Core Processor
 CPU 核心数        : 1
 CPU 频率          : 2396.398 MHz
 CPU 缓存          : L1: 64.00 KB / L2: 512.00 KB / L3: 16.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ❌ Disabled
 内存              : 214.25 MiB / 958.71 MiB
 Swap              : 0 KiB / 1024.00 MiB
 硬盘空间          : 3.49 GiB / 19.60 GiB
 启动盘路径        : /dev/vda1
 系统在线时间      : 0 days, 15 hour 43 min
 负载              : 0.94, 0.32, 0.12
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-21-amd64
 TCP加速方式       : bbr
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS906 DMIT Cloud Services
 IPV4 位置         : Los Angeles / California / US
 IPV6 ASN          : AS906 DMIT
 IPV6 位置         : Los Angeles / California / United States
 IPV6 子网掩码     : 64
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          4455 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          50454.01 MB/s
 单线程写测试:          29464.37 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         87.0 MB/s (21.24 IOPS, 1.21s))          24.5 MB/s (5987 IOPS, 4.28s)
 1GB-1M Block           1.2 GB/s (1111 IOPS, 0.90s)             1.2 GB/s (1111 IOPS, 0.90s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 62.54 MB/s   (15.6k) | 1.06 GB/s    (16.6k)
Write      | 62.65 MB/s   (15.6k) | 1.07 GB/s    (16.7k)
Total      | 125.20 MB/s  (31.2k) | 2.13 GB/s    (33.4k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 1.02 GB/s     (2.0k) | 1.01 GB/s      (987)
Write      | 1.07 GB/s     (2.1k) | 1.07 GB/s     (1.0k)
Total      | 2.10 GB/s     (4.1k) | 2.08 GB/s     (2.0k)
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：美国
[IPV6]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：美国
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  洛杉机(LAX31S13)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  洛杉机(LAX17S56)
---------------DisneyPlus---------------
[IPV4]
当前IPv4出口所在地区即将开通DisneyPlus
[IPV6]
当前IPv4出口所在地区即将开通DisneyPlus
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Yes (Region: US)
 Disney+:                               No (IP Banned By Disney+ 1)
 Netflix:                               Yes (Region: US)
 YouTube Premium:                       Yes (Region: US)
 Amazon Prime Video:                    Yes (Region: US)
 TVBAnywhere+:                          Yes
 Spotify Registration:                  No
 OneTrust Region:                       US [California]
 iQyi Oversea Region:                   US
 Bing Region:                           US (Risky)
 Apple Region:                          US
 YouTube CDN:                           Los Angeles, CA
 Netflix Preferred CDN:                 Los Angeles, CA
 ChatGPT:                               Yes
 Google Gemini:                         Yes (Region: USA)
 Claude:                                No
 Wikipedia Editability:                 No
 Google Play Store:                     United States 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        USD
 ---Forum---
 Reddit:                                No
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Yes (Region: US)
 YouTube Premium:                       Yes (Region: US)
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  No
 OneTrust Region:                       US [California]
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           US (Risky)
 Apple Region:                          US
 YouTube CDN:                           Los Angeles, CA
 Netflix Preferred CDN:                 Dallas, TX
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         Yes (Region: USA)
 Claude:                                No
 Wikipedia Editability:                 No
 Google Play Store:                     United States 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         【US】
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 0 [8] 
VPN得分(越低越好): 99 [8] 
代理得分(越低越好): 100 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 100 [8] 
欺诈得分(越低越好): 65 [E] 19 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0014 (Low) [A]
公司滥用得分(越低越好): 0.0022 (Low) [A] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: hosting [0 7 A] business [8] hosting - high probability [C] DataCenter/WebHosting/Transit [3]
公司类型: business [0 A] isp [7]
是否云提供商: Yes [7 D] 
是否数据中心: No [0 5 6 8 A] Yes [1 C]
是否移动设备: No [5 A C] Yes [E]
是否代理: Yes [E] No [0 1 4 5 6 7 8 A C D]
是否VPN: Yes [E] No [0 1 6 7 A C D]
是否Tor: No [0 1 3 6 7 8 A C D E] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [A E] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D E] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 313(Total_Check) 0(Clean) 6(Blacklisted) 22(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 19 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0014 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
安全信息:
使用类型: hosting [A] DataCenter/WebHosting/Transit [3]
公司类型: hosting [A] 
是否云提供商: Yes [D] 
是否数据中心: Yes [1 A]
是否移动设备: No [A] 
是否代理: No [1 A D] 
是否VPN: No [1 A D] 
是否TorExit: No [1 D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 313(Total_Check) 0(Clean) 0(Blacklisted) 313(Other) 
Google搜索可行性：NO
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✔     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✔     ✔     ✘     ✔     ✘    
Sohu      ✘     ✔     ✔     ✘     ✔     ✘    
Yandex    ✔     ✔     ✔     ✘     ✔     ✘    
Gmail     ✔     ✔     ✘     ✘     ✘     ✘    
Outlook   ✔     ✘     ✔     ✘     ✔     ✘    
Office365 ✔     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘    
MailRU    ✔     ✔     ✘     ✘     ✔     ✘    
AOL       ✘     ✔     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✔     ✔     ✘     ✔     ✘    
Apple     ✘     ✘     ✘     ✘     ✘     ✘    
FastMail  ✘     ✔     ✘     ✘     ✘     ✘    
ProtonMail✘     ✘     ✘     ✘     ✘     ✘    
MXRoute   ✘     ✘     ✔     ✘     ✔     ✘    
Namecrane ✘     ✔     ✔     ✘     ✔     ✘    
XYAMail   ✘     ✘     ✘     ✘     ✘     ✘    
ZohoMail  ✘     ✔     ✘     ✘     ✘     ✘    
Inbox_eu  ✘     ✔     ✔     ✘     ✘     ✘    
Free_fr   ✘     ✔     ✔     ✘     ✔     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信v4 219.141.140.10           移动CMIN2  [精品线路] 
北京联通v4 202.106.195.68           联通9929   [优质线路] 联通4837   [普通线路] 
北京移动v4 221.179.155.161          移动CMIN2  [精品线路] 
上海电信v4 202.96.209.133           移动CMIN2  [精品线路] 
上海联通v4 210.22.97.1              联通9929   [优质线路] 联通4837   [普通线路] 
上海移动v4 211.136.112.200          移动CMIN2  [精品线路] 
广州电信v4 58.60.188.222            移动CMIN2  [精品线路] 
广州联通v4 210.21.196.6             联通9929   [优质线路] 联通4837   [普通线路] 
广州移动v4 120.196.165.24           移动CMIN2  [精品线路] 
成都电信v4 61.139.2.69              移动CMIN2  [精品线路] 电信163    [普通线路] 
成都联通v4 119.6.6.6                联通9929   [优质线路] 联通4837   [普通线路] 
成都移动v4 211.137.96.205           移动CMIN2  [精品线路] 
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自nexttrace，请知悉!
广州电信 58.60.188.222
0.19 ms         AS906 [DMIT-BB] Anycast Anycast DMIT.com
0.57 ms         AS906 [DMIT-BB] 美国 加利福尼亚 洛杉矶 DMIT.com
0.65 ms         AS906 [DMIT-BB] 美国 加利福尼亚 洛杉矶 DMIT.com
126.64 ms       AS58807 [CMIN2-NET] 美国 加利福尼亚 洛杉矶 cmi.chinamobile.com 移动
126.04 ms       AS58807 [CMIN2-NET] 中国 上海 cmi.chinamobile.com 移动
126.19 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
126.31 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
127.76 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
154.36 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
157.80 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
207.12 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
159.14 ms       AS134774 [CHINANET-GD] 中国 广东 深圳 chinatelecom.cn 电信
158.41 ms       AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
0.28 ms         AS906 [DMIT-BB] Anycast Anycast DMIT.com
0.59 ms         AS906 [DMIT-BB] 美国 加利福尼亚 洛杉矶 DMIT.com
0.79 ms         * [CUG-ASIA] 美国 加利福尼亚 洛杉矶 中国联通（香港）
131.95 ms       AS10099 [CUG-BACKBONE] 中国 上海 chinaunicomglobal.com 联通
132.73 ms       AS9929 [CNC-BACKBONE] 中国 上海 chinaunicom.cn 联通 CUII
154.84 ms       AS9929 [CNC-BACKBONE] 中国 广东 广州 chinaunicom.cn 联通 CUII
156.85 ms       * [CNC-BACKBONE] 中国 广东 广州
159.59 ms       AS4837 [CU169-BACKBONE] 中国 广东 广州 chinaunicom.cn 联通
159.67 ms       AS17816 [UNICOM-GD] 中国 广东 深圳 chinaunicom.cn 联通
162.08 ms       AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
159.13 ms       AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
0.20 ms         AS906 [DMIT-BB] Anycast Anycast DMIT.com
0.52 ms         AS906 [DMIT-BB] 美国 加利福尼亚 洛杉矶 DMIT.com
0.72 ms         AS906 [DMIT-BB] 美国 加利福尼亚 洛杉矶 DMIT.com
127.19 ms       AS58807 [CMIN2-NET] 美国 加利福尼亚 洛杉矶 cmi.chinamobile.com 移动
126.29 ms       AS58807 [CMIN2-NET] 中国 上海 cmi.chinamobile.com 移动
127.58 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
139.62 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
127.25 ms       AS9808 [CMNET] 中国 上海 chinamobileltd.com
153.16 ms       AS9808 [CMNET] 中国 北京 chinamobileltd.com 移动
152.66 ms       AS9808 [CMNET] 中国 北京 chinamobileltd.com 移动
254.89 ms       AS9808 [CMNET] 中国 北京 chinamobileltd.com 移动
154.79 ms       AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟     丢包率
Speedtest.net    1045.13 Mbps    998.75 Mbps     0.48     0.0%
洛杉矶           1039.58 Mbps    987.83 Mbps     0.76     0.0%
日本东京         825.27 Mbps     641.68 Mbps     102.90   2.3%
联通上海5G       928.55 Mbps     441.58 Mbps     130.17   0.0%
电信浙江         656.77 Mbps     689.40 Mbps     140.71   NULL
电信浙江         646.85 Mbps     932.02 Mbps     138.04   NULL
移动Chengdu      740.06 Mbps     373.02 Mbps     160.52   NULL
------------------------------------------------------------------------
 总共花费      : 5 分 46 秒
 时间          : Fri Apr 11 10:57:30 CST 2025
------------------------------------------------------------------------
  短链:
    https://paste.spiritlhl.net/#/show/DRkoI.txt
    http://hpaste.spiritlhl.net/#/show/DRkoI.txt
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>LLM 结构化输出</title><link>https://www.mihouo.com/posts/front/llm-structured-outputs/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/llm-structured-outputs/</guid><description>让 LLM 输出结构化数据（JSON、XML），而非自然语言</description><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是结构化输出？&lt;/h2&gt;
&lt;p&gt;通常情况下，LLM 的输出是自然语言，但是有时候我们需要 LLM 输出特定的结构化数据，比如 JSON、XML 等。&lt;/p&gt;
&lt;p&gt;让大语言模型（LLM）输出格式清晰、结构明确的数据，而不是一大段自然语言描述。&lt;/p&gt;
&lt;h2&gt;如何使用结构化输出？&lt;/h2&gt;
&lt;h3&gt;1. Prompt 中指定输出格式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import OpenAI from &apos;openai&apos;;

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const completion = await openai.chat.completions.create({
  model: &apos;gpt-4o-mini&apos;,
  messages: [
    {
      role: &apos;system&apos;,
      content: &apos;你是一个专业的历史学家，你应该能够以简洁和准确的方式总结一个人的生活。&apos;,
    },
    {
      role: &apos;user&apos;,
      content: `请总结10个${name}生活中的重要事件。
      以 JSON 格式输出，字段包括：
      {
      &quot;name&quot;: &quot;string&quot;, // name
      &quot;gender&quot;: &quot;string&quot;, // gender
      &quot;type&quot;: &quot;string&quot;, // type
      &quot;profile&quot;: &quot;string&quot;, // profile 100 words
      &quot;timeline&quot;: [
        {
          &quot;year&quot;: &quot;string&quot;, // year
          &quot;title&quot;: &quot;string&quot;, // title
          &quot;description&quot;: &quot;string&quot;, // description 50 words
        }
      ]
    }
    `,
    },
  ],
});


// 期望输出
```json
{
  &quot;name&quot;: &quot;string&quot;, // name
  &quot;gender&quot;: &quot;string&quot;, // gender
  &quot;type&quot;: &quot;string&quot;, // type
  &quot;profile&quot;: &quot;string&quot;, // profile 100 words
  &quot;timeline&quot;: [
    {
      &quot;year&quot;: &quot;string&quot;, // year
      &quot;title&quot;: &quot;string&quot;, // title
      &quot;description&quot;: &quot;string&quot;, // description 50 words
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 使用 zod 定义结构，配合 OpenAI 的 &lt;code&gt;zodResponseFormat&lt;/code&gt; 函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import OpenAI from &apos;openai&apos;;
import { zodResponseFormat } from &apos;openai/helpers/zod&apos;;
import { z } from &apos;zod&apos;;

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const BiographySchema = z.object({
  name: z.string(),
  gender: z.string(),
  type: z.string(),
  profile: z.string(),
  timeline: z.array(z.object({ year: z.string(), title: z.string(), description: z.string() })),
});


const completion = await openai.chat.completions.create({
  model: &apos;gpt-4o-mini&apos;,
  messages: [
    {
      role: &apos;system&apos;,
      content: &apos;你是一个专业的历史学家，你应该能够以简洁和准确的方式总结一个人的生活。&apos;,
    },
    {
      role: &apos;user&apos;,
      content: `请总结10个${name}生活中的重要事件。`,
    },
  ],
  response_format: zodResponseFormat(BiographySchema, &apos;biography&apos;),
});

// 期望输出
{
  &quot;name&quot;: &quot;string&quot;, // name
  &quot;gender&quot;: &quot;string&quot;, // gender
  &quot;type&quot;: &quot;string&quot;, // type
  &quot;profile&quot;: &quot;string&quot;, // profile 100 words
  &quot;timeline&quot;: [
    {
      &quot;year&quot;: &quot;string&quot;, // year
      &quot;title&quot;: &quot;string&quot;, // title
      &quot;description&quot;: &quot;string&quot;, // description 50 words
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;h3&gt;部分兼容 openai sdk 的服务不支持 &lt;code&gt;zodResponseFormat&lt;/code&gt; 函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;deepseek-chat 模型不支持 &lt;code&gt;zodResponseFormat&lt;/code&gt; 函数&lt;/p&gt;
&lt;p&gt;解决方法：&lt;code&gt;response_format: model.includes(&apos;deepseek-chat&apos;) ? { type: &apos;json_object&apos; } : zodResponseFormat(BiographySchema, &apos;biography&apos;),&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deepseek-reasoner 模型不支持 &lt;code&gt;json_object&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;解决方法：&lt;code&gt;response_format: model.includes(&apos;deepseek-reasoner&apos;) ? { type: &apos;text&apos; } : zodResponseFormat(BiographySchema, &apos;biography&apos;),&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;最终处理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;const response_format = model.includes(&apos;deepseek&apos;) ? 
  { type: model.includes(&apos;chat&apos;) ? &apos;json_object&apos; : &apos;text&apos;, }
  : zodResponseFormat(BiographySchema, &apos;biography&apos;),
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方法&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prompt 中指定输出格式&lt;/td&gt;
&lt;td&gt;简单易用，自然语言输入&lt;/td&gt;
&lt;td&gt;1. 如果输出格式复杂，可能需要多次调整 &amp;lt;br/&amp;gt;2. 不同的模型可能对于输出格式的理解不同导致输出格式错误 &amp;lt;br/&amp;gt;3. 输出的文本一般被 ```jsonn ``` 包裹，需要自行匹配真正的 JSON 文本[^1]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zod&lt;/code&gt; +  &lt;code&gt;zodResponseFormat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;结构清晰，易于维护，支持复杂类型，高可控&lt;/td&gt;
&lt;td&gt;需要额外引入 &lt;code&gt;zod&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;[^1]: 匹配方法 &lt;code&gt;text.match(/```json\s*([\s\S]*?)\s*```/)?.[1] || &apos;&apos;&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>SSE 在大语言模型中的流式数据处理简单封装</title><link>https://www.mihouo.com/posts/front/sse-for-streaming-data-processing-in-large-language-models/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/sse-for-streaming-data-processing-in-large-language-models/</guid><description>SSE 在 LLM 应用中的流式数据传输封装，实现逐步返回 Token</description><pubDate>Tue, 01 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;效果展示&lt;/h2&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/videos/preview-sse-2025-04-01.mp4&quot; type=&quot;video/mp4&quot;&amp;gt;
Your browser does not support the video tag.
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近我在使用大语言模型做一些简单、好玩的工具应用，比如 &lt;a href=&quot;https://tools.mihouo.com/tools/cyber-fortune-telling&quot;&gt;算命&lt;/a&gt;、&lt;a href=&quot;https://tools.mihouo.com/tools/cyber-biography&quot;&gt;生成名人大事记&lt;/a&gt;等，然而大语言模型返回的数据量比较大，如果一次性返回，那么用户体验会比较差，所以需要使用 SSE 来实现 流式数据传输，提升用户体验。&lt;/p&gt;
&lt;h2&gt;SSE 是什么？&lt;/h2&gt;
&lt;p&gt;SSE（Server-Sent Events）是一种 单向服务器推送技术，即：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;服务器 → 客户端&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;•	长连接（保持 HTTP 连接）
•	逐步推送数据，适用于 流式响应（如大模型返回 Token）&lt;/p&gt;
&lt;p&gt;与 WebSocket 的区别&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;SSE&lt;/th&gt;
&lt;th&gt;WebSocket&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;连接方向&lt;/td&gt;
&lt;td&gt;单向(服务器到客户端)&lt;/td&gt;
&lt;td&gt;双向&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;适用场景&lt;/td&gt;
&lt;td&gt;流式数据&lt;/td&gt;
&lt;td&gt;实时聊天、多人协作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;兼容性&lt;/td&gt;
&lt;td&gt;所有现代浏览器&lt;/td&gt;
&lt;td&gt;需要服务器和客户端都支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;连接数&lt;/td&gt;
&lt;td&gt;受浏览器限制&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;LLM 中的 SSE 工作流程&lt;/h2&gt;
&lt;p&gt;应用场景：ChatGPT、DeepSeek Chat、Gemini 等对话模型返回数据 逐步传输，而不是一次性返回整个 JSON。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端发起请求
•	使用 EventSource（浏览器）或 fetch + ReadableStream（Node.js）创建 SSE 连接&lt;/li&gt;
&lt;li&gt;服务器逐步返回数据
•	服务器端不断向 SSE 连接推送 Token&lt;/li&gt;
&lt;li&gt;客户端逐步解析数据
•	浏览器或前端代码监听 SSE 事件，逐步渲染消息&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;SSE 代码示例&lt;/h2&gt;
&lt;p&gt;服务端 Node.js 代码示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import OpenAI from &apos;openai&apos;;

const openai = new OpenAI({
  baseURL: &apos;https://api.deepseek.com&apos;,
  apiKey: process.env.DEEPSEEK_KEY,
});

const stream = await openai.chat.completions.create({
  model: &quot;deepseek-chat&quot;,
  messages: [{ role: &quot;user&quot;, content: &quot;Hello, world!&quot; }],
  stream: true,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端代码示例 JavaScript&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const stream = await fetch(&apos;http://localhost:3000/sse&apos;, {
  method: &apos;GET&apos;,
}).then(res =&amp;gt; res.body);

const reader = stream?.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader!.read();
  if (done) break;
  const text = decoder.decode(value);
  console.log(text);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;简单封装&lt;/h2&gt;
&lt;p&gt;基于大模型返回的 SSE 数据，进行简单封装，实现流式数据传输以及页面渲染，包含思考模型处理。&lt;/p&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;h4&gt;函数定义&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// @/utils/server.ts

/**
 * 获取大模型响应流
 * @param completion 大模型响应流 await service.chat.completions.create 返回的流式数据 create({ stream: true })
 * @param model 模型名称
 * @param type 类型
 * @returns 流式数据
 */
export const getStreamData = (completion: Stream&amp;lt;ChatCompletionChunk&amp;gt;) =&amp;gt; {
  let count = 0;
  let thinkingCount = 0;
  const stream = new ReadableStream({
    async start(controller) {
      try {
        console.log(&apos;Starting stream processing...&apos;);
        for await (const chunk of completion) {
          const { choices } = chunk;
          const delta = choices[0]?.delta as Delta ?? {};
          
          if (!delta) continue;

          const { reasoning_content = null, content = null } = delta;
          // 添加 thinking 标签
          if (count === 0 &amp;amp;&amp;amp; reasoning_content) {
            controller.enqueue(new TextEncoder().encode(&apos;&amp;lt;thinking&amp;gt;&apos;));
            // 等待 100ms 后避免批处理
            await new Promise(resolve =&amp;gt; setTimeout(resolve, 100));
          }
          if (reasoning_content) {
            count++;
            thinkingCount++;
            controller.enqueue(new TextEncoder().encode(reasoning_content));
          }
          if (count - thinkingCount === 0 &amp;amp;&amp;amp; thinkingCount !== 0 &amp;amp;&amp;amp; content) {
            controller.enqueue(new TextEncoder().encode(&apos;&amp;lt;/thinking&amp;gt;&apos;));
            // 等待 100ms 后避免批处理
            await new Promise(resolve =&amp;gt; setTimeout(resolve, 100));
          }
          if (content) {
            count++;
            controller.enqueue(new TextEncoder().encode(content));
          }
        }
      } catch (error) {
        console.error(&apos;Stream processing error:&apos;, error);
        controller.error(error);
      } finally {
        if (count === 0) {
          controller.enqueue(new TextEncoder().encode(&apos;[服务器繁忙，请稍后再试。]&apos;));
        } else {
          controller.enqueue(new TextEncoder().encode(&apos;[DONE]&apos;));
        }
        controller.close();
      }
    },
  });
  return stream;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import OpenAI from &apos;openai&apos;;
import { getStreamData } from &apos;@/utils/server&apos;;

const openai = new OpenAI({
  baseURL: &apos;https://api.deepseek.com&apos;,
  apiKey: process.env.DEEPSEEK_KEY,
});

const completion = await openai.chat.completions.create({
  model: &quot;deepseek-chat&quot;,
  messages: [{ role: &quot;user&quot;, content: &quot;Hello, world!&quot; }],
  stream: true,
});
const stream = await getStreamData(completion);

// 返回流式数据
return new Response(stream, {
  headers: {
    &apos;Content-Type&apos;: &apos;text/event-stream&apos;,
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;客户端&lt;/h3&gt;
&lt;h4&gt;函数定义&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// @/utils/client.ts

/**
 * 解析流式数据
 * @param stream 流式数据 ReadableStream
 * @param options 选项
 * @param options.output 输出类型 默认 text 如果 output 为 json 则onResult 的 result 返回解析后的 json 对象
 * @param options.onStart 流式数据开始
 * @param options.onEnd 流式数据结束 形参 thinking 为思考内容，result 为结果
 * @param options.onchange 实时更新最新内容 形参 thinking 为思考内容，result 为结果
 * @param options.onThinkingStart 如果是思考模型，则 onThinkingStart 会触发
 * @param options.onThinkingEnd 如果是思考模型，则 onThinkingEnd 会触发
 */
export const parseReadableStream = async (stream: ReadableStream&amp;lt;Uint8Array&amp;lt;ArrayBufferLike&amp;gt;&amp;gt;, options: {
  output?: &apos;text&apos; | &apos;json&apos;;
  onStart?: () =&amp;gt; void;
  onEnd?: (thinking: string, result: string | Record&amp;lt;string, unknown&amp;gt;) =&amp;gt; void;
  onchange?: (thinking: string, result: string) =&amp;gt; void;
  onThinkingStart?: () =&amp;gt; void;
  onThinkingEnd?: () =&amp;gt; void;
}) =&amp;gt; {
  const { output = &apos;text&apos;, onStart = () =&amp;gt; {}, onEnd = () =&amp;gt; {}, onThinkingStart = () =&amp;gt; {}, onThinkingEnd = () =&amp;gt; {}, onchange = () =&amp;gt; {} } = options;
  const reader = stream?.getReader();
  const decoder = new TextDecoder();
  let isReasoning = false;
  let thinking = &apos;&apos;;
  let content = &apos;&apos;;
  onStart();
  while (true) {
    const { done, value } = await reader!.read();
    if (done) break;
    const text = decoder.decode(value);
    if (text.includes(&apos;&amp;lt;thinking&amp;gt;&apos;)) {
      isReasoning = true;
      onThinkingStart();
      continue;
    }
    if (text.includes(&apos;&amp;lt;/thinking&amp;gt;&apos;)) {
      isReasoning = false;
      onThinkingEnd();
      continue;
    }
    if (text.includes(&apos;[DONE]&apos;)) {
      let result = content;
      if (output === &apos;json&apos;) {
        // 取出 ```json 和 ``` 之间的内容
        const jsonContent = result.match(/```json\s*([\s\S]*?)\s*```/)?.[1] || &apos;&apos;;
        try {
          result = JSON.parse(jsonContent);
        } catch (error) {
          console.error(error);
        }
      }
      onEnd(thinking, result);
      break;
    }
    if (isReasoning) {
      thinking += text;
    } else {
      content += text;
    }
    onchange(thinking, content);
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;import { parseReadableStream } from &apos;@/utils/client&apos;;

const stream = await fetch(&apos;http://localhost:3000/sse&apos;, {
  method: &apos;GET&apos;,
}).then(res =&amp;gt; res.body);

try {
  parseReadableStream(stream, {
  output: &apos;json&apos;,
  onStart: () =&amp;gt; {
    console.log(&apos;开始&apos;);
  },
  onEnd: (thinking, result) =&amp;gt; {
    console.log(&apos;结束&apos;, thinking, result);
  },
  onchange: (thinking, result) =&amp;gt; {
    // 如果 output 为 json 则 result 为 已经解析后对象
    console.log(&apos;实时更新&apos;, thinking, result);
  },
  onThinkingStart: () =&amp;gt; {
    console.log(&apos;思考开始&apos;);
  },
    onThinkingEnd: () =&amp;gt; {
      console.log(&apos;思考结束&apos;);
    },
  });
} catch (error) {
  console.error(error);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Emby 开心版安装、使用</title><link>https://www.mihouo.com/posts/docker/emby-cracked-version-installation-and-usage/</link><guid isPermaLink="true">https://www.mihouo.com/posts/docker/emby-cracked-version-installation-and-usage/</guid><description>使用 docker-compose 安装 Emby 开心版</description><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;docker-compose&lt;/code&gt; 安装，&lt;code&gt;docker-compose.yml&lt;/code&gt; 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3.8&apos;
services:
  embyserver-happy:
    image: amilys/embyserver
    platform: linux/amd64   # 强制使用 amd64 架构（我这里使用的是 arm64 版本，如果使用 amd64 版本，请删除此行）
    container_name: emby-happy
    privileged: true
    volumes:
      - ./emby/config:/config # 配置文件
      - ./emby/webdav:/docker/webdav # 挂载 webdav 目录
      - ./emby/local:/docker/local # 本机影音文件目录
    ports:
      - 8096:8096
    restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中访问 &lt;code&gt;http://localhost:8096&lt;/code&gt; 即可访问 Emby。&lt;/p&gt;
&lt;h2&gt;挂载目录&lt;/h2&gt;
&lt;p&gt;挂载目录为 &lt;code&gt;./emby/local&lt;/code&gt;，目录结构如下：&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;./emby/local&lt;/code&gt; 目录下存放你的影音文件，然后在浏览器访问 emby 服务，进入 &lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;服务器&lt;/code&gt; -&amp;gt; &lt;code&gt;媒体库&lt;/code&gt; -&amp;gt; &lt;code&gt;添加媒体库&lt;/code&gt; -&amp;gt; 选择 &lt;code&gt;从文件夹添加&lt;/code&gt;，然后选择 &lt;code&gt;/docker/local&lt;/code&gt; 目录，然后扫描，等待扫描完成后，你就可以在首页中看到你的影音文件了。&lt;/p&gt;
&lt;h2&gt;第三方播放&lt;/h2&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://apps.apple.com/cn/app/infuse-%E6%99%BA%E8%83%BD%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8/id1136220934&quot;&gt;Infuse&lt;/a&gt;、&lt;a href=&quot;https://apps.apple.com/cn/app/forward-%E6%96%B0%E8%A7%86%E7%95%8C/id6503940939&quot;&gt;Forward&lt;/a&gt;、&lt;a href=&quot;https://apps.apple.com/cn/app/vidhub-%E9%AB%98%E6%B8%85%E5%BD%B1%E7%89%87%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8-%E5%BF%AB%E9%80%9F%E6%92%AD%E6%94%BE%E4%BA%91%E7%9B%98%E7%BD%91%E7%9B%98/id1659622164&quot;&gt;VidHub&lt;/a&gt;、&lt;a href=&quot;https://apps.apple.com/cn/app/senplayer-%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8-8%E5%80%8D%E9%80%9F/id6443975850&quot;&gt;Senplay&lt;/a&gt; 等第三方播放器，可以播放 emby 中的影音文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;IMG_B433E50FE3C4-1.jpeg&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;局限&lt;/h2&gt;
&lt;p&gt;随着影视资源库的不断扩充，对于本地存储的容量要求也越来越高，此时有两条路可以选择&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用 NAS 存储&lt;/li&gt;
&lt;li&gt;使用云盘挂载&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 NAS 存储，这需要一笔不小的开支，而且需要考虑电费、散热、噪音等问题。&lt;/p&gt;
&lt;p&gt;而我之前使用的是阿里云盘，且开通了它的三年会员，所以就自然而然选择了云盘挂载。&lt;/p&gt;
&lt;h2&gt;云盘挂载&lt;/h2&gt;
&lt;p&gt;总体思路是将云盘转换成 webdav 服务，然后在将 webdav 挂载为本地目录。&lt;/p&gt;
&lt;h3&gt;1.云盘转 webdav&lt;/h3&gt;
&lt;p&gt;使用 alist 很方便的将云盘转换成 webdav 服务，只需要在 alist 中添加阿里云盘即可。&lt;/p&gt;
&lt;p&gt;:::tip
如果你没有阿里云盘的第三方权益会员，可以参考 &lt;a href=&quot;/posts/apple-tv/using-alist-on-apple-tv/#%E4%BD%BF%E7%94%A8&quot;&gt;alist 挂载阿里云盘&lt;/a&gt; 实现获取 阿里云盘 &lt;code&gt;TV token&lt;/code&gt; 解除限速。
:::&lt;/p&gt;
&lt;h3&gt;2.1 使用 Rclone 挂载为本地目录&lt;/h3&gt;
&lt;p&gt;可以使用 Rclone 挂载 webdav 为本地目录，然后使用 emby 添加此目录。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 Rclone&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;macos 不能使用brew 安装，需要使用源码安装。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://rclone.org/install.sh | sudo bash
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;配置 Rclone&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;rclone config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据提示创建 remote -&amp;gt; 输入名称 -&amp;gt; 选择 webdav -&amp;gt; 输入 url -&amp;gt; 选择 Other site/service or software -&amp;gt; 输入用户名 -&amp;gt; 选择 Yes, type in my own password -&amp;gt; 输入两次密码 -&amp;gt; 回车完成配置&lt;/p&gt;
&lt;p&gt;:::tip
url 为 alist 的 webdav 地址，例如：&lt;code&gt;https://example.com/dav/&lt;/code&gt;，用户名密码为你的 alist 用户名密码。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;挂载 webdav&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 直接后台挂载
rclone mount mywebdav:/ ~/docker/emby/webdav --network-mode --cache-dir cache --vfs-cache-mode full --header &quot;Referer:&quot; --daemon

# 使用 tmux 挂载
tmux new-session -d -s mount-rclone rclone mount mywebdav:/ ~/docker/emby/webdav --network-mode --cache-dir cache --vfs-cache-mode full --header &quot;Referer:&quot; --daemon

# 关闭挂载
#macos 使用
umount ~/docker/emby/webdav

#windows、linux 使用
fusermount -u ~/docker/emby/webdav
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
&lt;code&gt;rclone mount&lt;/code&gt;: 挂载命令&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mywebdav:&lt;/code&gt;: 刚刚配置的 remote 名称&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/docker/emby/webdav&lt;/code&gt;: 你的挂载目录&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--network-mode&lt;/code&gt;: 启用网络模式，该模式会让 rclone mount 使用网络流量来访问和同步文件，而不是使用本地缓存。这可以有效减少磁盘占用，但会使文件访问速度依赖于网络连接。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--cache-dir cache&lt;/code&gt;: 设置本地缓存目录。在此命令中，cache 是一个相对路径或绝对路径，指定 rclone 存储本地缓存的地方。缓存的文件可以加速后续访问操作，尤其是针对大量小文件或频繁访问的情况。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--vfs-cache-mode full&lt;/code&gt;: FS（虚拟文件系统）缓存模式。在 rclone mount 中，full 模式表示 rclone 将缓存文件的全部内容。这对于大型文件（例如视频文件）的操作尤其重要，因为它将所有文件数据保存在本地缓存中，从而提高性能。该模式确保每个文件都完全缓存并能够进行读取和修改。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--header &quot;Referer:&quot; &lt;/code&gt;: 必须加上，否则无法播放&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--daemon&lt;/code&gt;: 后台运行
:::&lt;/p&gt;
&lt;p&gt;:::warning
由于缓存的原因，可能导致挂载后再网盘处编辑文件，挂载的目录不会同步更新，此时需要手动关闭挂载，重新挂载。
:::&lt;/p&gt;
&lt;h3&gt;2.2 使用 strm 文件模拟本地目录&lt;/h3&gt;
&lt;p&gt;思路来自于：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;xtxt19931207/emby-alist&quot;}&lt;/p&gt;
&lt;p&gt;大体思路就是在 emby 的挂载目录中存放和 alist 1:1 一样的目录结构，但是文件内容由 mp4 -&amp;gt; strm 文件。&lt;/p&gt;
&lt;p&gt;且 strm 文件内容为 webdav 的 url 地址，emby 会自动识别 strm 文件，并进行播放。&lt;/p&gt;
&lt;p&gt;我在使用 emby-alist 仓库的代码时发现它存在一个 bug 无法识别所有的文件目录，因此我对它进行了修改，并添加了下载字幕的功能。&lt;/p&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 替换 webdav_url、save_mulu、username、password 为你自己的参数
webdav_url = &apos;https://example.com/dav/folder/&apos;  # alist webdav 地址 folder 为你的文件夹名称
save_mulu = &apos;./&apos;  # 输出路径
username = &apos;&apos;  # 用户名 alist 用户名
password = &apos;&apos;  # 密码 alist 密码
download_subtitle = False  # 是否下载字幕文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from webdav3.client import Client
import os, time, requests

def list_files(webdav_url, username, password):
	# 创建WebDAV客户端
	options = {
		&apos;webdav_hostname&apos;: webdav_url,
		&apos;webdav_login&apos;: username,
		&apos;webdav_password&apos;: password
	}
	
	client = Client(options)
	mulu = []
	wenjian = []
	q = 1
	while q &amp;lt; 15:
		try:		   
		  # 获取WebDAV服务器上的文件列表
			files = client.list()
		except:
			q += 1
			print(&apos;连接失败，1秒后重试...&apos;)
			time.sleep(1)
		else:
			if q &amp;gt; 1:
				print(&apos;重连成功...&apos;)
			break

	if q &amp;gt;= 15:
		print(f&quot;连接 {webdav_url} 失败，已达到最大重试次数&quot;)
		return [], []
		
	for file in files[1:]:
		if file[-1] == &apos;/&apos;:
			mulu.append(file)
		else:
			wenjian.append(file)
	return mulu, wenjian

def traverse_webdav(base_url, username, password, max_depth=10, current_depth=0):
	&quot;&quot;&quot;
	递归遍历WebDAV目录，获取所有目录和文件
	
	Args:
		base_url: WebDAV基础URL
		username: 用户名
		password: 密码
		max_depth: 最大递归深度，防止无限递归
		current_depth: 当前递归深度
		
	Returns:
		tuple: (所有目录列表, 所有文件URL列表)
	&quot;&quot;&quot;
	if current_depth &amp;gt;= max_depth:
		print(f&quot;警告: 达到最大递归深度 {max_depth}，停止在 {base_url} 的递归&quot;)
		return [], []
	
	print(f&quot;扫描目录: {base_url}&quot;)
	
	# 获取当前目录下的文件和子目录
	directories, files = list_files(base_url, username, password)
	
	all_directories = [base_url]
	all_files = [base_url + f for f in files]
	
	# 递归处理子目录
	for directory in directories:
		dir_url = base_url + directory
		sub_dirs, sub_files = traverse_webdav(dir_url, username, password, max_depth, current_depth + 1)
		all_directories.extend(sub_dirs)
		all_files.extend(sub_files)
	
	return all_directories, all_files

def process_media_file(file_url, save_path, webdav_url):
	&quot;&quot;&quot;处理媒体文件，创建.strm文件&quot;&quot;&quot;
	if not os.path.exists(save_path + file_url.replace(webdav_url, &apos;&apos;)[:-3] + &apos;strm&apos;):
		print(&apos;正在处理：&apos; + file_url.replace(webdav_url, &apos;&apos;))
		try:
			os.makedirs(os.path.dirname(save_path + file_url.replace(webdav_url, &apos;&apos;)[:-3] + &apos;strm&apos;), exist_ok=True)
			with open(save_path + file_url.replace(webdav_url, &apos;&apos;)[:-3] + &apos;strm&apos;, &quot;w&quot;, encoding=&apos;utf-8&apos;) as f:
				f.write(file_url.replace(&apos;/dav&apos;, &apos;/d&apos;))
		except:
			try:
				# 处理包含冒号等特殊字符的文件名
				os.makedirs(os.path.dirname(save_path + file_url.replace(webdav_url, &apos;&apos;).replace(&apos;：&apos;, &apos;.&apos;)[:-3] + &apos;strm&apos;), exist_ok=True)
				with open(save_path + file_url.replace(webdav_url, &apos;&apos;).replace(&apos;：&apos;, &apos;.&apos;)[:-3] + &apos;strm&apos;, &quot;w&quot;, encoding=&apos;utf-8&apos;) as f:
					f.write(file_url.replace(&apos;/dav&apos;, &apos;/d&apos;))
			except Exception as e:
				print(f&quot;处理失败: {file_url.replace(webdav_url, &apos;&apos;)} - 错误: {str(e)}&quot;)
				print(f&quot;{file_url.replace(webdav_url, &apos;&apos;)}处理失败，文件名包含特殊符号，建议重命名！&quot;)

def download_subtitle_file(file_url, save_path, webdav_url):
	&quot;&quot;&quot;下载字幕文件&quot;&quot;&quot;
	if not os.path.exists(save_path + file_url.replace(webdav_url, &apos;&apos;)):
		p = 1
		while p &amp;lt; 10:
			try:
				print(&apos;正在下载：&apos; + save_path + file_url.replace(webdav_url, &apos;&apos;))
				r = requests.get(file_url.replace(&apos;/dav&apos;, &apos;/d&apos;))
				os.makedirs(os.path.dirname(save_path + file_url.replace(webdav_url, &apos;&apos;)), exist_ok=True)
				with open(save_path + file_url.replace(webdav_url, &apos;&apos;), &apos;wb&apos;) as f:
					f.write(r.content)
			except Exception as e:
				p += 1
				print(f&apos;下载失败，1秒后重试... 错误: {str(e)}&apos;)
				time.sleep(1)
			else:
				if p &amp;gt; 1:
					print(&apos;重新下载成功！&apos;)
				break
		if p &amp;gt;= 10:
			print(f&quot;下载失败: {file_url.replace(webdav_url, &apos;&apos;)} - 已达到最大重试次数&quot;)

# 主程序
if __name__ == &quot;__main__&quot;:
	# 输入WebDAV地址、用户名和密码
	webdav_url = &apos;https://example.com/dav/folder/&apos;  # alist webdav 地址 folder 为你的文件夹名称
	save_mulu = &apos;./&apos;  # 输出路径
	username = &apos;&apos;  # 用户名 alist 用户名
	password = &apos;&apos;  # 密码 alist 密码
	
	# 控制是否下载字幕文件
	download_subtitle = False  # 设置为 False 将不会下载字幕文件
	
	# 设置最大递归深度
	max_depth = 10  # 可以根据实际需求调整
	
	print(f&quot;开始遍历 WebDAV 目录: {webdav_url}&quot;)
	print(f&quot;字幕下载功能: {&apos;已启用&apos; if download_subtitle else &apos;已禁用&apos;}&quot;)
	start_time = time.time()
	
	# 递归遍历所有目录和文件
	all_directories, all_files = traverse_webdav(webdav_url, username, password, max_depth)
	
	print(f&quot;遍历完成，共发现 {len(all_directories)} 个目录，{len(all_files)} 个文件&quot;)
	print(f&quot;遍历耗时: {time.time() - start_time:.2f} 秒&quot;)
	
	# 处理所有文件
	processed_count = 0
	media_count = 0
	subtitle_count = 0
	
	for file_url in all_files:
		extension = file_url.split(&apos;.&apos;)[-1].upper() if &apos;.&apos; in file_url else &apos;&apos;
		
		if extension in [&apos;MP4&apos;, &apos;MKV&apos;, &apos;FLV&apos;, &apos;AVI&apos;]:
			process_media_file(file_url, save_mulu, webdav_url)
			processed_count += 1
			media_count += 1
		elif extension in [&apos;ASS&apos;, &apos;SRT&apos;, &apos;SSA&apos;] and download_subtitle:
			# 只有当 download_subtitle 为 True 时才下载字幕文件
			download_subtitle_file(file_url, save_mulu, webdav_url)
			processed_count += 1
			subtitle_count += 1
	
	print(f&apos;处理完毕！共处理 {processed_count} 个文件（媒体文件: {media_count}, 字幕文件: {subtitle_count}）&apos;)
	print(f&apos;字幕下载功能: {&quot;已启用&quot; if download_subtitle else &quot;已禁用&quot;}&apos;)
	input(&apos;按任意键退出...&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
经测试无法通过 emby 播放 strm 文件，但是通过第三方播放器则可以正常播放。
:::&lt;/p&gt;
&lt;h3&gt;emby 添加资源库&lt;/h3&gt;
&lt;p&gt;在 emby 中添加资源库，选择 &lt;code&gt;从文件夹添加&lt;/code&gt;，然后选择 &lt;code&gt;/docker/emby&lt;/code&gt; 目录，然后扫描，等待扫描完成后，你就可以在首页中看到你的影音文件了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Next.js 在 Safari 浏览器中异常的水合错误</title><link>https://www.mihouo.com/posts/front/nextjs-hydration-error-in-safari-browser/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/nextjs-hydration-error-in-safari-browser/</guid><description>Next.js 在 Safari 浏览器中使用 HeroUI ListBox 组件时遇到的水合错误</description><pubDate>Tue, 04 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;问题描述&lt;/h2&gt;
&lt;p&gt;我在使用 Next.js(&lt;code&gt;v15.1.7&lt;/code&gt;) 开发时，使用 HeroUI 组件库的 ListBox 组件时，遇到了水合错误。&lt;/p&gt;
&lt;p&gt;一开始在 Chromium 内核的浏览器中一切正常，但是当我在开发者工具中切换到移动端(iphone系列)时，发现组件使用正常但是控制台报水合错误，我一开始的第一反应是移动端代码有问题，经过排查发现并不是，而是 Next.js 在 Safari 浏览器中对于标签嵌套的渲染有问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;水合错误&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;问题排查&lt;/h2&gt;
&lt;p&gt;经过排查，发现问题出在 ListBox 和 ListItem 的标签渲染上，ListBox 默认渲染为 ul 标签，ListItem 默认渲染为 li 标签，虽然这一切看着正常，但是可能是 Next.js 在 Safari 浏览器中对于组件的渲染有问题，导致水合错误。&lt;/p&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Sidebar.tsx
import { Listbox, ListboxItem } from &quot;@heroui/react&quot;;

export default function Sidebar() {
  return (
    &amp;lt;Listbox as=&quot;ul&quot;&amp;gt;
      &amp;lt;ListboxItem as=&quot;li&quot; /&amp;gt;
    &amp;lt;/Listbox&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;按照如下组合尝试修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ListBox -&amp;gt; ul &amp;amp;&amp;amp; ListboxItem -&amp;gt; li&lt;/li&gt;
&lt;li&gt;ListBox -&amp;gt; ol &amp;amp;&amp;amp; ListboxItem -&amp;gt; li&lt;/li&gt;
&lt;li&gt;ListBox -&amp;gt; div &amp;amp;&amp;amp; ListboxItem -&amp;gt; div&lt;/li&gt;
&lt;li&gt;ListBox -&amp;gt; div &amp;amp;&amp;amp; ListboxItem -&amp;gt; p&lt;/li&gt;
&lt;li&gt;ListBox -&amp;gt; p &amp;amp;&amp;amp; ListboxItem -&amp;gt; p&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 chromium 内核的浏览器只有使用 p 标签嵌套 p 标签才会出现水合错误，其他标签组合则不会出现水合错误。&lt;/p&gt;
&lt;p&gt;而对于 Safari 浏览器，则所有标签组合都会出现水合错误。&lt;/p&gt;
&lt;p&gt;因此，可见 Next.js 在 Safari 浏览器中对于组件的渲染有问题，为了解决这个问题(在应对水合错误时,官方推荐的&lt;a href=&quot;https://nextjs.org/docs/messages/react-hydration-error#solution-2-disabling-ssr-on-specific-components&quot;&gt;解决方案&lt;/a&gt;之一)，可以使用 &lt;code&gt;dynamic&lt;/code&gt; 组件来动态渲染组件，跳过 Next.js 的预渲染。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Layout.tsx
import dynamic from &quot;next/dynamic&quot;;

const Sidebar = dynamic(() =&amp;gt; import(&quot;@/components/Sidebar&quot;), { ssr: false });
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Grok API 充值 $5, 获取每月 $150 的 API 额度</title><link>https://www.mihouo.com/posts/tool-share/grok-api---get-150-in-free-api-credits-each-month/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/grok-api---get-150-in-free-api-credits-each-month/</guid><description>Grok API 充值 $5 获取每月 $150 额度</description><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近马斯克发布了 Grok 模型，但是 Grok3 的api 还未发布就已经出了 5 刀的充值活动，虽然如此但是我体验过后网页端的 Grok3 后发现回答速度以及质量都非常不错，因此我还是比较愿意给马斯克贡献这 5 刀的。&lt;/p&gt;
&lt;h2&gt;活动&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.x.ai/docs/data-sharing#how-it-works&quot;&gt;活动地址&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;需要使用信用卡充值&lt;/li&gt;
&lt;li&gt;需要数据共享，也就是使用数据换取 API 额度&lt;/li&gt;
&lt;li&gt;账单地址非欧盟地区&lt;/li&gt;
&lt;li&gt;每月可以获取 $150 的 API 额度，毕竟活动最终解释权归马斯克所有&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;充值&lt;/h2&gt;
&lt;p&gt;先填写账单地址，然后绑定信用卡，然后充值 $5 即可。&lt;/p&gt;
&lt;p&gt;以下是网友分享的充值经验：&lt;/p&gt;
&lt;p&gt;银行卡：
用的国内中行的2张，Visa和Master，都可以。一张付了2次可以。&lt;/p&gt;
&lt;p&gt;地址选择：&lt;/p&gt;
&lt;p&gt;账号选美区，付款账单地址美区，成功&lt;/p&gt;
&lt;p&gt;账号选美区，付款账单地址中国，成功&lt;/p&gt;
&lt;p&gt;账号选中国，付款账单地址中国，成功&lt;/p&gt;
&lt;p&gt;:::tip
绑定信用卡有延迟，请稍作等待后再进行查看充值。(我大概等了 10 分钟)
&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;领取额度&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;确认共享数据后，即可领取额度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;我这里使用的是本地部署的 open web ui 进行测试的，使用方式如下：&lt;/p&gt;
&lt;p&gt;在 open web ui 管理员界面，进入设置 -&amp;gt; 外部连接 -&amp;gt; 管理 OpenAI API 处添加一个即可。&lt;/p&gt;
&lt;p&gt;URL 填写：https://api.x.ai/v1&lt;/p&gt;
&lt;p&gt;API Key 生成后填写，然后保存进入首页测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-6.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-7.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>FRP 内网穿透</title><link>https://www.mihouo.com/posts/network/frp-intranet-penetration/</link><guid isPermaLink="true">https://www.mihouo.com/posts/network/frp-intranet-penetration/</guid><description>使用 FRP 实现内网穿透，将局域网服务暴露到公网</description><pubDate>Wed, 19 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;我的需求&lt;/h2&gt;
&lt;p&gt;我家里的服务器上部署了很多的服务，比如 alist、openwebui、以及一些我的小项目。&lt;/p&gt;
&lt;p&gt;我之前才用的方案是使用 DDNS 将我的动态公网 IP 地址固定下来，然后使用 Nginx Proxy Manager 来反向代理到我的内网服务。&lt;/p&gt;
&lt;p&gt;但是由于家庭宽带的一些端口的限制，比如 http(80)、https(443) 等端口被封锁，导致我只能使用 https://alist.mihouo.com:8443 来访问我的 alist 服务。&lt;/p&gt;
&lt;p&gt;因为 443 端口被封锁了，所以我在使用 Nginx Proxy Manager 的时候，使用 8443 端口代理 443 端口。&lt;/p&gt;
&lt;p&gt;我的最终需求也很简单，就是消灭掉 8443 端口，让我可以像 https://alist.mihouo.com 一样访问我的 alist 服务。&lt;/p&gt;
&lt;h2&gt;什么是 FRP&lt;/h2&gt;
&lt;p&gt;FRP (Fast Reverse Proxy) 是一个高性能的反向代理应用，主要用于内网穿透。它能够帮助用户将局域网内的服务暴露到公网上，解决 NAT 和防火墙限制的问题。FRP 由两个部分组成：frps（服务端）和 frpc（客户端）。其原理类似于将一个内网服务通过代理的方式暴露到外网，让外部网络能够访问内网服务。&lt;/p&gt;
&lt;h2&gt;FRP 的基本工作原理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;客户端 (frpc)：通常在内网运行，负责将本地服务的请求通过代理转发到公网。&lt;/li&gt;
&lt;li&gt;服务端 (frps)：通常运行在有公网 IP 的服务器上，负责接收来自客户端的请求，并将请求转发到对应的本地服务。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;frpc 和 frps 之间通过一个 TCP 连接进行通信，frps 作为代理服务器，接收来自外部的请求，转发到内网的对应服务上，而 frpc 则接收来自 frps 的请求并将其传递给本地服务。&lt;/p&gt;
&lt;h2&gt;FRP 配置&lt;/h2&gt;
&lt;p&gt;FRP 版本：&lt;strong&gt;v0.61.1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先需要在两个服务器上分别下载 &lt;a href=&quot;https://github.com/fatedier/frp/releases&quot;&gt;FRP&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解压下载的文件，并进入解压后的文件夹。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar -zxvf frp_0.61.1_linux_amd64.tar.gz
cd frp_0.61.1_linux_amd64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
注意自己的服务器是 windows、linux、macos(darwin) 等，然后选择对应的版本下载。&lt;/p&gt;
&lt;p&gt;也需要注意自己的 CPU 架构，比如 arm64、amd64 等，然后选择对应的版本下载。
:::&lt;/p&gt;
&lt;h3&gt;配置 FRPS&lt;/h3&gt;
&lt;p&gt;使用 nano 命令编辑 &lt;code&gt;frps.toml&lt;/code&gt; 文件，并添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[common]
# frp监听的端口，默认是7000，可以改成其他的
bind_port = 7000
# 授权码，请改成更复杂的 # 这个token之后在客户端会用到
token = token

# frp管理后台端口，请按自己需求更改
dashboard_port = 7500
# frp管理后台用户名和密码，请改成自己的
dashboard_user = user
dashboard_pwd = password
enable_prometheus = true

# frp日志配置
log_file = ./frps.log
log_level = info
log_max_days = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;可选项: 创建 systemd 服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/systemd/system/frps.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=FRP Server
After=network.target

[Service]
Type=simple
ExecStart=/opt/frp_0.61.1_linux_amd64/frps -c /opt/frp_0.61.1_linux_amd64/frps.toml
Restart=on-failure
User=root

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;启动服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start frps
sudo systemctl stop frps
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查看服务状态&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status frps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果启动失败可能是没有权限，使用 &lt;code&gt;sudo chmod +x /opt/frp_0.61.1_linux_amd64/frps&lt;/code&gt; 赋予权限。&lt;/p&gt;
&lt;p&gt;:::tip
启动状态为 &lt;code&gt;active (running)&lt;/code&gt;，则表示服务启动成功。启动失败可使用 &lt;code&gt;journalctl -u frps.service&lt;/code&gt; 查看详细日志。&lt;/p&gt;
&lt;p&gt;想要开机自启动，可以执行 &lt;code&gt;sudo systemctl enable frps&lt;/code&gt;。
:::&lt;/p&gt;
&lt;h4&gt;进入 frp Dashboard&lt;/h4&gt;
&lt;p&gt;通过你的公网 IP 和 dashboard_port 访问 frp Dashboard。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://8.218.x.x:7500
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;FRPC 配置&lt;/h3&gt;
&lt;p&gt;在你的另一台需要进行穿透的服务器上，使用 nano 命令编辑 &lt;code&gt;frpc.toml&lt;/code&gt; 文件，并添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#【公网服务端地址】
serverAddr = &quot;8.218.x.x&quot;
#【公网服务端端口】
serverPort = 7000
#【令牌，与公网服务端保持一致】
auth.token = &quot;token&quot;

[[proxies]]
name = &quot;alist&quot;
type = &quot;tcp&quot;
# 本地服务地址
localIP = &quot;127.0.0.1&quot;
# 本地服务端口
localPort = 5244
# 穿透的端口
remotePort = 5244
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;可选项: 创建服务&lt;/h4&gt;
&lt;p&gt;根据 frps 的配置，创建 frpc 的 systemd 服务。(基本只需要将 frps 更改为 frpc 即可)&lt;/p&gt;
&lt;p&gt;我这里的终端设备是 macos，因此我使用的是 launchd 来创建用户级服务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nano ~/Library/LaunchAgents/frpc.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs$
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;com.frpc&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
      &amp;lt;string&amp;gt;~/opt/frp_0.61.1_darwin_arm64/frpc&amp;lt;/string&amp;gt;
      &amp;lt;string&amp;gt;-c&amp;lt;/string&amp;gt;
      &amp;lt;string&amp;gt;~/opt/frp_0.61.1_darwin_arm64/frpc.toml&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;
    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;KeepAlive&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/tmp/frpc.err&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/tmp/frpc.out&amp;lt;/string&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;加载服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;launchctl load ~/Library/LaunchAgents/frpc.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;卸载服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;launchctl unload ~/Library/LaunchAgents/frpc.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;启动服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;launchctl start com.frp.client
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;停止服务&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;launchctl stop com.frp.client
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;查看服务状态&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;launchctl list | grep frpc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
不知为何，我使用 launchctl 启动服务后，总是提示 &lt;code&gt;failed: 5: Input/output error&lt;/code&gt;，但是我的配置文件并无问题，最终我使用 tmux 来启动服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;frp error log&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tmux new-session -d -s frpc &apos;~/opt/frp_0.61.1_darwin_arm64/frpc -c ~/opt/frp_0.61.1_darwin_arm64/frpc.toml&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;如果 frpc 启动正常，则可以通过 Dashboard 查看服务状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;frp Dashboard&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这时应该可以通过 8.218.x.x:5244 访问我的 alist 服务了。&lt;/p&gt;
&lt;p&gt;但是到这一步还没有到达我的最终需求，还需要在使用 Nginx Proxy Manager 将 5244 端口反向代理到 https://alist.mihouo.com。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/posts/network/nginx-proxy-manager-a-comprehensive-user-guide/&quot;&gt;Nginx Proxy Manager 配置参考&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>DeepSeek 本地部署</title><link>https://www.mihouo.com/posts/tool-share/deepseek-local-deploy/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/deepseek-local-deploy/</guid><description>DeepSeek R1 模型本地部署方案</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在 2025 的新年中，DeepSeek 推出了R1 模型，对标 openai 的 o1 模型，并提供了本地部署方案，本文主要介绍本地部署方案。&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;我这里是使用的 macmini 部署的，配置如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;处理器：M4&lt;/li&gt;
&lt;li&gt;内存：16GB&lt;/li&gt;
&lt;li&gt;硬盘：512GB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主要用到以下工具&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker: 容器化部署&lt;/li&gt;
&lt;li&gt;Ollama: 本地部署模型&lt;/li&gt;
&lt;li&gt;Open Web UI: 本地部署 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安装 Docker&lt;/h3&gt;
&lt;h4&gt;官网安装&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker 官网&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;使用 Homebrew 安装&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;brew install docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;验证&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 Ollama&lt;/h3&gt;
&lt;h4&gt;官网安装&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama 官网&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;使用 Homebrew 安装&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;brew install ollama
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;验证&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ollama --version
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;创建并编辑配置文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;nano ~/Library/LaunchAgents/com.user.ollama.service.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;添加以下内容： 配置文件中写入下面内容；定义了服务方式运行，以及做了跨域设置。使得内网其他的网络访问被允许&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
&amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;com.user.ollama.service&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
        &amp;lt;string&amp;gt;/usr/local/bin/ollama&amp;lt;/string&amp;gt;
        &amp;lt;string&amp;gt;serve&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;
    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;KeepAlive&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/tmp/ollama.err&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/tmp/ollama.out&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;/key&amp;gt;
    &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;OLLAMA_HOST&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;0.0.0.0&amp;lt;/string&amp;gt;
        &amp;lt;key&amp;gt;Ollama_ORIGINS&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;*&amp;lt;/string&amp;gt;
    &amp;lt;/dict&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;赋予权限，启动服务&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;chmod 644 ~/Library/LaunchAgents/com.user.ollama.service.plist
launchctl load ~/Library/LaunchAgents/com.user.ollama.service.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 Open Web UI&lt;/h3&gt;
&lt;p&gt;docker-compose.yml 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    restart: always
    ports:
      - &quot;3000:8080&quot;
    volumes:
      - ./open-webui:/app/backend/data

  watchtower:
    image: containrrr/watchtower
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 300 open-webui
    depends_on:
      - open-webui
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;启动&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;验证&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;docker-compose ps
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装模型&lt;/h2&gt;
&lt;p&gt;如果上述内容都安装成功，那么进入浏览器输入 &lt;code&gt;http://localhost:3000&lt;/code&gt; 即可看到 Open Web UI 的 UI 界面。&lt;/p&gt;
&lt;h3&gt;设置 open web ui 关联 ollama&lt;/h3&gt;
&lt;p&gt;进入 open web ui 的界面，点击左下角头像，进入设置界面，选择 &lt;code&gt;管理员设置&lt;/code&gt; -&amp;gt; &lt;code&gt;外部链接&lt;/code&gt; 选项，在&lt;code&gt;管理Ollama API连接&lt;/code&gt;选项下填写地址 &lt;code&gt;http://host.docker.internal:11434&lt;/code&gt;，确保按钮启用，点击 &lt;code&gt;保存&lt;/code&gt; 按钮，即可完成设置。&lt;/p&gt;
&lt;h3&gt;拉取模型&lt;/h3&gt;
&lt;p&gt;进入&lt;a href=&quot;https://ollama.com/search&quot;&gt;ollama 模型官网&lt;/a&gt;，搜索 &lt;code&gt;deepseek&lt;/code&gt; 模型，选择 &lt;code&gt;deepseek-r1&lt;/code&gt; 模型，选择模型大小，根据你的机器性能选择，我这里选择 &lt;code&gt;14b&lt;/code&gt; 模型，点击复制命令，进入终端执行命令，即可拉取模型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama pull deepseek-r1:14b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;模型大小选择，根据你机器的可用内存，选择合适的模型。通常情况下可以这样计算，你机器的可用内存需要大于模型占用的硬盘存储空间。&lt;em&gt;&lt;strong&gt;注意：这里计算的是机器内存和占用硬盘存储空间&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;测试模型&lt;/h3&gt;
&lt;p&gt;进入 open web ui 的界面，左上角选择模型，选择 &lt;code&gt;deepseek-r1&lt;/code&gt; 模型，进行对话测试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>使用 Realm 转发流量，实现落地代理</title><link>https://www.mihouo.com/posts/server/using-realm-for-traffic-forwarding-to-achieve-proxy-landing/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/using-realm-for-traffic-forwarding-to-achieve-proxy-landing/</guid><description>使用 Realm 转发流量实现落地代理，解决公共 DNS 不稳定问题</description><pubDate>Mon, 20 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;虽然在 使用 &lt;a href=&quot;/posts/server/use-alice-dns-unlock-network/&quot;&gt;使用 Alice DNS 解锁服务器访问限制&lt;/a&gt; 中，已经实现了使用 Alice DNS 解锁服务器访问限制，但是，由于 Alice DNS 是公共 DNS，存在高峰期访问不稳定的问题，因此我这次使用的是 Alice 香港的机器作为落地机。&lt;/p&gt;
&lt;h2&gt;准备材料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;线路优化的机器，但 ip 脏的机器，作为中转，通过 GFW，例如 &lt;a href=&quot;/posts/server/review-of-claw-hongkong-hybrid-machine/&quot;&gt;Claw 香港的机器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;一台 Alice 香港的机器，作为落地机，不通过 GFW，但是需要 IP 干净，例如 &lt;a href=&quot;/posts/server/review-of-alice-hongkong-hybrid-machine/&quot;&gt;Alice 香港的机器&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;示意图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;中转机&lt;/h3&gt;
&lt;p&gt;中转机使用 Realm 转发流量，实现落地代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一键 Realm 脚本
wget -N https://raw.githubusercontent.com/qqrrooty/EZrealm/main/realm.sh &amp;amp;&amp;amp; chmod +x realm.sh &amp;amp;&amp;amp; ./realm.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入面板后，先安装 realm，然后添加 realm 转发规则&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地监听端口: 1-65535，随便输入一个&lt;/li&gt;
&lt;li&gt;需要转发的IP: 落地机 IP&lt;/li&gt;
&lt;li&gt;需要转发端口: 1-65535，这个端口需要是落地机接收流量的端口(稍后配置落地机代理协议时生成的端口，例如 &lt;code&gt;28175&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;落地机&lt;/h3&gt;
&lt;p&gt;正常使用脚本搭建代理协议即可，例如我使用 ss 协议，则使用以下脚本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一键 ss 脚本
wget -O ss-rust.sh --no-check-certificate https://raw.githubusercontent.com/xOS/Shadowsocks-Rust/master/ss-rust.sh &amp;amp;&amp;amp; chmod +x ss-rust.sh &amp;amp;&amp;amp; ./ss-rust.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;如果使用 SS 协议，请选择加密方式为 2022 的，否则 ip 会被 GFW 封锁。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;添加好代理协议后会返回信息，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 以下信息已经做了脱敏处理，请不要尝试直接使用
Shadowsocks Rust 配置：
——————————————————————————————————
 地址：173.152.108.142
 地址：2a12:2813:321::69
 端口：28175 # 中转机转发端口
 密码：QiHQ2B0j6dsya1982xE1uVbElK6pn5x8HNfmm+Mzky394=
 加密：2022-blake3-aes-256-gcm
 TFO ：true
——————————————————————————————————
 链接  [IPv4]：ss://MjAyMi1ibGFrZTMtYWV23hsjda8213HUS12nd1oWHkreEUxdVZiRWxLNnBuNXg4SE5mbW0rTXpreTM5ND1AMTU3LjI1NC4yMS4xNDI6MjgxNzU 
 二维码[IPv4]：https://cli.im/api/qrcode/code?text=ss://MjAyMi1ibGFrZTMtYWV23hsjda8213HUS12nd1oWHkreEUxdVZiRWxLNnBuNXg4SE5mbW0rTXpreTM5ND1AMTU3LjI1NC4yMS4xNDI6MjgxNzU
 链接  [IPv6]：ss://MjAyMi1ibGFrZTMtYWV23hsjda8213HUS12nd1oWHkreEUxdVZiRWxLNnBuNXg4SE5mbW0rTXpreTM5ND1AMmExNDo2N2MwOjMwMzo6Njk6MjgxNzU 
 二维码[IPv6]：https://cli.im/api/qrcode/code?text=ss://MjAyMi1ibGFrZTMtYWV23hsjda8213HUS12nd1oWHkreEUxdVZiRWxLNnBuNXg4SE5mbW0rTXpreTM5ND1AMmExNDo2N2MwOjMwMzo6Njk6MjgxNzU
—————————————————————————
[信息] Surge 配置：
Const0822.alice.ws = ss,173.152.108.142,28175,encrypt-method=2022-blake3-aes-256-gcm,password=QiHQ2B0j6dsya1982xE1uVbElK6pn5x8HNfmm+Mzky394=,tfo=true,udp-relay=true,ecn=true
—————————————————————————
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;首先测试直接通过链接落地机的代理协议访问。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;新建一个代理，信息和落地机代理一样，然后将 ip 和端口修改为中转机的 ip 和端口，进行测试，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;我这里使用的测试工具是 Surge 的 &lt;a href=&quot;https://raw.githubusercontent.com/xream/scripts/main/surge/modules/network-info/net-lsp-x.sgmodule&quot;&gt;网络信息模块&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>Alice 香港落地机融合怪测试</title><link>https://www.mihouo.com/posts/server/review-of-alice-hongkong-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-alice-hongkong-hybrid-machine/</guid><description>Alice 香港落地机融合怪测试留档</description><pubDate>Thu, 16 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 512 MiB&lt;/li&gt;
&lt;li&gt;存储 5 GiB&lt;/li&gt;
&lt;li&gt;带宽 1000 Mbps&lt;/li&gt;
&lt;li&gt;流量 1024 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                      IP质量体检报告：157.254.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:32:31 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS202662
组织：                  Hytron Network Services Limited
坐标：                  114°10′33″E, 22°17′3″N
地图：                  https://check.place/22.2842,114.1759,15,cn
城市：                  N/A, 香港
使用地：                [HK]香港, [AS]亚洲
注册地：                [US]美国
时区：                  Asia/Hong_Kong
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     机房        机房        机房        机房        机房    
公司类型：     机房        机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：  0|低风险
ipapi：      0.11%|低风险
AbuseIPDB：    0|低风险
IPQS：                                          87|存在风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [HK]    [HK]    [HK]    [HK]    [HK]    [HK]    [HK]    [HK]
代理：     否      否      否      是      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      否      否      是      否      无      否      否 
服务器：   是      是      是      无      否      否      是      否 
滥用：     否      否      否      是      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     解锁     解锁     解锁     解锁     解锁     屏蔽     解锁   
地区：     [SG]     [HK]     [HK]     [HK]     [HK]              [SG]   
方式：      DNS      DNS      DNS     原生      DNS               DNS   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 428   已标记 11   黑名单 0
========================================================================
今日IP检测量：137；总检测量：175243。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/IP/3DPKHP3QH.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.01.02
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel Core Processor (Broadwell, IBRS)
 CPU 核心数        : 1
 CPU 频率          : 2399.996 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 4.00 MB / L3: 16.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ✔ Enabled
 内存              : 186.70 MiB / 422.59 MiB
 Swap              : [ no swap partition or swap file detected ]
 硬盘空间          : 1.74 GiB / 4.82 GiB
 启动盘路径        : /dev/vda3
 系统在线时间      : 0 days, 0 hour 14 min
 负载              : 0.70, 0.18, 0.06
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-30-amd64
 TCP加速方式       : bbr
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS202662 Hytron Network Services Limited
 IPV4 位置         : Hong Kong / Hong Kong / HK
 IPV6 ASN          : AS215355 Alice Networks
 IPV6 位置         : Kai Yi Wan / Hong Kong
 IPV6 子网掩码     : 128
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          899 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          18953.98 MB/s
 单线程写测试:          14265.00 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         17.0 MB/s (4158 IOPS, 6.16s)            22.7 MB/s (5535 IOPS, 4.62s)
 1GB-1M Block           160 MB/s (152 IOPS, 6.57s)              159 MB/s (151 IOPS, 6.59s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 52.08 MB/s   (13.0k) | 153.94 MB/s   (2.4k)
Write      | 52.16 MB/s   (13.0k) | 154.75 MB/s   (2.4k)
Total      | 104.25 MB/s  (26.0k) | 308.69 MB/s   (4.8k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 146.93 MB/s    (286) | 145.07 MB/s    (141)
Write      | 154.74 MB/s    (302) | 154.73 MB/s    (151)
Total      | 301.68 MB/s    (588) | 299.80 MB/s    (292)
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：中国香港
[IPV6]
您的网络可能没有正常配置IPv6，或者没有IPv6网络接入
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 中国香港(HKG07S42)
Youtube识别地域: 中国香港(HK)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 中国香港(HKG07S42)
Youtube识别地域: 中国香港(HK)
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：SG 区
[IPV6]
当前出口所在地区解锁DisneyPlus
区域：SG 区
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Yes (Region: SG)
 Disney+:                               Yes (Region: SG)
 Netflix:                               Yes (Region: HK)
 YouTube Premium:                       Yes (Region: HK)
 Amazon Prime Video:                    Yes (Region: HK)
 TVBAnywhere+:                          No
 Spotify Registration:                  No
 OneTrust Region:                       HK [Unknown]
 iQyi Oversea Region:                   HK
 Bing Region:                           HK (Risky)
 YouTube CDN:                           Hong Kong
 Netflix Preferred CDN:                 [Alice Networks] in [Hong Kong]
 ChatGPT:                               Yes
 Google Gemini:                         Yes (Region: SGP)
 Wikipedia Editability:                 No
 Google Play Store:                     Hong Kong 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        HKD
 ---Forum---
 Reddit:                                No
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Failed (Network Connection)
 YouTube Premium:                       Yes (Region: HK)
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  No
 OneTrust Region:                       Failed (Network Connection)
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           HK (Risky)
 YouTube CDN:                           Hong Kong
 Netflix Preferred CDN:                 Failed (CDN IP Not Found)
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         No
 Wikipedia Editability:                 Yes
 Google Play Store:                     Hong Kong 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         【SG】
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 1 [8] 
VPN得分(越低越好): 99 [8] 
代理得分(越低越好): 100 [8]
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 99 [8] 
欺诈得分(越低越好): 93 [E] 0 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0019 (Low) [A] 
公司滥用得分(越低越好): 0.0014 (Low) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 60 [2]  恶意记录数: 2 [2]  可疑记录数: 0 [2]  无记录数: 32 [2]  
安全信息:
使用类型: business [8] hosting - high probability [C] hosting [0 7 A] DataCenter/WebHosting/Transit [3] corporate [9]
公司类型: hosting [0 7] business [A]
是否云提供商: No [D] Yes [7]
是否数据中心: Yes [0 1 A C] No [5 6 8]
是否移动设备: No [5 A C] Yes [E]
是否代理: No [0 1 4 5 6 7 8 9 A B C D] Yes [E]
是否VPN: Yes [E] No [0 1 6 7 A C D]
是否Tor: No [0 1 3 6 7 8 A B C D E] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B E] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: Yes [E] No [7 8 A C D]
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 314(Total_Check) 0(Clean) 6(Blacklisted) 18(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 17 [1] 
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0021 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
威胁级别: low [B] 
安全信息:
使用类型: business [A] DataCenter/WebHosting/Transit [3]
公司类型: business [A] 
是否云提供商: No [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A B D] 
是否VPN: No [1 A D] 
是否TorExit: No [1 D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A B] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 0(Blacklisted) 314(Other) 
Google搜索可行性：YES
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✔     ✔     ✔     ✘     ✔     ✘    
163       ✔     ✔     ✔     ✘     ✔     ✘    
Sohu      ✔     ✔     ✔     ✘     ✔     ✘    
Yandex    ✔     ✔     ✔     ✘     ✔     ✘    
Gmail     ✔     ✔     ✘     ✘     ✘     ✘    
Outlook   ✔     ✘     ✔     ✘     ✔     ✘    
Office365 ✔     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✔     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✔     ✔     ✔     ✘     ✔     ✘    
MailRU    ✔     ✔     ✘     ✘     ✔     ✘    
AOL       ✔     ✔     ✘     ✘     ✘     ✘    
GMX       ✔     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✘     ✔     ✘     ✔     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信 219.141.140.10  电信163    [普通线路] 
北京联通 202.106.195.68  检测不到回程路由节点的IP地址
北京移动 221.179.155.161 检测不到回程路由节点的IP地址
上海电信 202.96.209.133  电信163    [普通线路] 
上海联通 210.22.97.1     检测不到回程路由节点的IP地址
上海移动 211.136.112.200 检测不到回程路由节点的IP地址
广州电信 58.60.188.222   电信163    [普通线路] 
广州联通 210.21.196.6    检测不到回程路由节点的IP地址
广州移动 120.196.165.24  检测不到回程路由节点的IP地址
成都电信 61.139.2.69     电信163    [普通线路] 
成都联通 119.6.6.6       联通4837   [普通线路] 
成都移动 211.137.96.205  检测不到回程路由节点的IP地址
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自ipip.net或nexttrace，请知悉!
广州电信 58.60.188.222
0.49 ms         AS202662 美国 加利福尼亚州 洛杉矶 hytron.io
0.50 ms         * RFC1918
149.45 ms       * [NSFNET-T3] 美国 加利福尼亚 圣何塞
258.27 ms       AS701 [UU-152] 美国 加利福尼亚 圣何塞 verizon.com
407.04 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
* ms    AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
* ms    AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
* ms    AS4134 [APNIC-AP] 中国 广东 深圳 www.chinatelecom.com.cn 电信
406.77 ms       AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
0.48 ms         AS202662 美国 加利福尼亚州 洛杉矶 hytron.io
3.82 ms         * RFC1918
10.82 ms        * RFC1918
214.02 ms       AS17816 [APNIC-AP] 中国 广东 茂名市 chinaunicom.cn 联通
207.98 ms       AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
256.05 ms       AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
0.35 ms         AS202662 美国 加利福尼亚州 洛杉矶 hytron.io
0.73 ms         * RFC1918
0.98 ms         * RFC1918
59.95 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
63.88 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
62.99 ms        AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟     丢包率
Speedtest.net    1039.11 Mbps    931.86 Mbps     0.53     0.0%
中国香港         1023.97 Mbps    943.80 Mbps     0.60     0.0%
新加坡           1036.78 Mbps    668.36 Mbps     34.53    0.0%
联通WuXi         1118.52 Mbps    96.38 Mbps      67.28    10.7%
联通成都         1.72 Mbps       3.32 Mbps       955.25   NULL
电信浙江         20.23 Mbps      501.61 Mbps     395.99   NULL
电信Suzhou5G     60.58 Mbps      346.14 Mbps     404.59   NULL
移动Beijing      11.82 Mbps      342.64 Mbps     591.90   20.4%
------------------------------------------------------------------------
 总共花费      : 8 分 45 秒
 时间          : Wed Jan 15 03:14:35 GMT 2025
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>关闭哪吒探针 Agent 的 SSH 登录</title><link>https://www.mihouo.com/posts/server/close-nezha-agent-ssh-login/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/close-nezha-agent-ssh-login/</guid><description>关闭哪吒探针 Agent 的 SSH 登录，防止被攻击</description><pubDate>Tue, 14 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;哪吒探针 Agent 的 SSH 登录默认是开启的，但是有时候我们需要关闭它，比如在某些情况下，我们需要关闭 SSH 登录，以防止被攻击。&lt;/p&gt;
&lt;h2&gt;关闭哪吒探针 Agent 的 SSH 登录&lt;/h2&gt;
&lt;h3&gt;V0&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;编辑 /etc/systemd/system/nezha-agent.service&lt;pre&gt;&lt;code&gt;nano /etc/systemd/system/nezha-agent.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;ExecStart=&lt;/code&gt; 后加上 &lt;code&gt;--disable-command-execute&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启Agent&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart nezha-agent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;V1&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;编辑 /opt/nezha/agent/config.yml&lt;pre&gt;&lt;code&gt;nano /opt/nezha/agent/config.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;disable_command_execute&lt;/code&gt; 参数改为 &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启Agent&lt;pre&gt;&lt;code&gt;systemctl restart nezha-agent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>使用 Alice DNS 解锁服务器访问限制</title><link>https://www.mihouo.com/posts/server/use-alice-dns-unlock-network/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/use-alice-dns-unlock-network/</guid><description>使用 Alice DNS 解锁香港等服务器的访问限制</description><pubDate>Mon, 13 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;由于某些国家或者地区的服务器(比如&lt;code&gt;香港地区&lt;/code&gt;)有很多服务无法正常访问，可以通过使用 Alice DNS 解锁。&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;登录 &lt;a href=&quot;https://app.alice.ws/dashboard&quot;&gt;Alice DNS&lt;/a&gt;，在 &lt;code&gt;DNS Service&lt;/code&gt; 申请，通过后绑定你的服务器 IP，然后它会提供给你 DNS IP，然后你要将你的服务器 DNS 设置为它提供的 DNS IP。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看当前 DNS
cat /etc/resolv.conf

# 设置 DNS 为 Alice DNS 的 DNS IP
echo -e &quot;nameserver 154.12.177.22\nnameserver 1.1.1.1&quot; | sudo tee /etc/resolv.conf &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置优化&lt;/h2&gt;
&lt;p&gt;默认情况下，所有网站都走了 Alice DNS 解析，但是我们只需要访问我们服务器未解锁的服务时才走 Alice DNS 解析，其他网站还是走默认的 DNS 解析。所以需要配置 DNS 分流。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一键分流脚本
wget https://raw.githubusercontent.com/Jimmyzxk/DNS-Alice-Unlock/refs/heads/main/dns-unlock.sh &amp;amp;&amp;amp; bash dns-unlock.sh

# 检测脚本
bash &amp;lt;(curl -L -s media.ispvps.com)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解锁前&lt;/h2&gt;
&lt;h3&gt;ipv4&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; ** 正在测试IPv4解锁情况 
--------------------------------
 ** 您的网络为: Alibaba (8.218.*.*) 


============[ Multination ]============
 Dazn:                                  Unsupport
 TikTok:                                Failed
 Disney+:                               No
 Netflix:               原生解锁        Originals Only (Region: SG)
 YouTube Premium:       原生解锁        Yes (Region: HK)
 Amazon Prime Video:    原生解锁        Yes (Region: HK)
 TVBAnywhere+:                          No
 iQyi Oversea Region:   原生解锁        HK
 YouTube CDN:                           Hong Kong 
 Netflix Preferred CDN:                 Hong Kong  
 Spotify Registration:                  No
 Steam Currency:                        HKD
 ChatGPT:               原生解锁        APP Only (Region: HK)
 Google Gemini:                         No
 Bing Region:                           HK
 Wikipedia Editability:                 No
 Instagram Licensed Audio:              Failed
 ---Forum---
 Reddit:                                No
=======================================
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ipv6&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;** 正在测试IPv6解锁情况 
--------------------------------
 ** 您的网络为: Alibaba (240b:4001:278:*:*) 


============[ Multination ]============
 Dazn:                                  Failed (Network Connection)
 TikTok:                                Failed
 Disney+:                               No
 Netflix:               原生解锁        Originals Only (Region: SG)
 YouTube Premium:       原生解锁        Yes (Region: HK)
 Amazon Prime Video:                    Unsupported
 TVBAnywhere+:                          Failed (Network Connection)
 iQyi Oversea Region:                   Failed
 YouTube CDN:                           Hong Kong 
 Netflix Preferred CDN:                 Hong Kong  
 Spotify Registration:                  No
 Steam Currency:                        Failed (Network Connection)
 ChatGPT:                               Failed
 Google Gemini:                         No
 Bing Region:                           HK
 Wikipedia Editability:                 No
 Instagram Licensed Audio:              Failed
 ---Forum---
 Reddit:                                No
=======================================
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解锁后&lt;/h2&gt;
&lt;h3&gt;ipv4&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; ** 正在测试IPv4解锁情况 
--------------------------------
 ** 您的网络为: Alibaba (8.218.*.*) 


============[ Multination ]============
 Dazn:                                  No
 TikTok:                DNS 解锁        Yes (Region: SG)
 Disney+:               DNS 解锁        Yes (Region: SG)
 Netflix:               DNS 解锁        Yes (Region: SG)
 YouTube Premium:       原生解锁        Yes (Region: HK)
 Amazon Prime Video:    原生解锁        Yes (Region: HK)
 TVBAnywhere+:                          No
 iQyi Oversea Region:   原生解锁        HK
 YouTube CDN:                           Hong Kong 
 Netflix Preferred CDN:                 Associated with [Alice Networks] in [Hong Kong ]
 Spotify Registration:                  No
 Steam Currency:                        HKD
 ChatGPT:               DNS 解锁        Yes (Region: SG)
 Google Gemini:         DNS 解锁        Yes (Region: SGP)
 Bing Region:                           HK
 Wikipedia Editability:                 No
 Instagram Licensed Audio:              Failed
 ---Forum---
 Reddit:                                No
=======================================
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ipv6&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; ** 正在测试IPv6解锁情况 
--------------------------------
 ** 您的网络为: Alibaba (240b:4001:278:*:*) 


============[ Multination ]============
 Dazn:                                  Failed (Network Connection)
 TikTok:                DNS 解锁        Yes (Region: SG)
 Disney+:                               No
 Netflix:                               Failed (Network Connection)
 YouTube Premium:       原生解锁        Yes (Region: HK)
 Amazon Prime Video:                    Unsupported
 TVBAnywhere+:                          Failed (Network Connection)
 iQyi Oversea Region:                   Failed
 YouTube CDN:                           Hong Kong 
 Netflix Preferred CDN:                 Associated with [] in [Hong Kong ]
 Spotify Registration:                  No
 Steam Currency:                        Failed (Network Connection)
 ChatGPT:                               Failed
 Google Gemini:                         No
 Bing Region:                           HK
 Wikipedia Editability:                 No
 Instagram Licensed Audio:              Failed
 ---Forum---
 Reddit:                                Failed (Network Connection)
=======================================
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>ClawCloud 香港 机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-clawclouds-hongkong-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-clawclouds-hongkong-hybrid-machine/</guid><description>留档-ClawCloud 香港机器留档测试</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 20 GiB&lt;/li&gt;
&lt;li&gt;带宽 1000 Mbps&lt;/li&gt;
&lt;li&gt;流量 500 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                    IP质量体检报告(Lite)：8.218.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:24:12 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS45102
组织：                  Alibaba (US) Technology Co., Ltd.
坐标：                  114°10′29″E, 22°16′42″N
地图：                  https://check.place/22.2783,114.1747,12,cn
城市：                  Hong Kong, 999077
使用地：                [HK]Hong Kong, Asia
注册地：                [SG]Singapore
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：      7|低风险
ipapi：               0.62%|低风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [HK]    [HK]    [HK]    [HK]    [HK]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      是      否      否      否 
服务器：   是      是      否      是      是 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     屏蔽     解锁     解锁     解锁     屏蔽     仅APP  
地区：                       [SG]     [HK]     [HK]              [HK]   
方式：                       原生     原生     原生              原生   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 425   已标记 13   黑名单 1
========================================================================
今日IP检测量：124；总检测量：175230。感谢使用xy系列脚本！ 

########################################################################
             IP质量体检报告(Lite)：240b:4001:278:*:*:*:*:*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:24:12 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS45102
组织：                  Alibaba (US) Technology Co., Ltd.
坐标：                  114°10′29″E, 22°16′42″N
地图：                  https://check.place/22.2783,114.1747,12,cn
城市：                  Hong Kong, 999077
使用地：                [HK]Hong Kong, Asia
注册地：                [SG]Singapore
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：                 25|中风险
ipapi：    0.00%|极低风险
Cloudflare：   0|低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [SG]    [HK]    [HK]    [HK]    [HK]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      否      否      否      否 
服务器：   是      是      否      是      否 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     屏蔽     解锁     解锁     屏蔽     屏蔽     失败   
地区：                       [SG]     [HK]                              
方式：                       原生     原生                              
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：125；总检测量：175231。感谢使用xy系列脚本！ 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.01.02
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel(R) Xeon(R) Platinum
 CPU 核心数        : 1
 CPU 频率          : 2500.002 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 1.00 MB / L3: 33.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ❌ Disabled
 内存              : 122.68 MiB / 923.82 MiB
 Swap              : [ no swap partition or swap file detected ]
 硬盘空间          : 908.56 MiB / 19990.38 MiB
 启动盘路径        : /dev/vda1
 系统在线时间      : 0 days, 0 hour 3 min
 负载              : 0.37, 0.17, 0.06
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-28-cloud-amd64
 TCP加速方式       : cubic
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS45102 Alibaba (US) Technology Co., Ltd.
 IPV4 位置         : Hong Kong / Hong Kong / HK
 IPV6 ASN          : AS45102 Alibaba
 IPV6 位置         : Hong Kong / Hong Kong
 IPV6 子网掩码     : 128
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          1056 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          4976.04 MB/s
 单线程写测试:          4631.54 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         26.4 MB/s (6450 IOPS, 3.97s)            37.7 MB/s (9208 IOPS, 2.78s)
 1GB-1M Block           232 MB/s (221 IOPS, 4.52s)              209 MB/s (199 IOPS, 5.02s)
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 0 [8] 
VPN得分(越低越好): 100 [8] 
代理得分(越低越好): 100 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 100 [8]
欺诈得分(越低越好): 65 [E] 4 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0011 (Low) [A] 
公司滥用得分(越低越好): 0.0045 (Low) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: hosting [0 7 9 A] DataCenter/WebHosting/Transit [3] hosting - high probability [C] business [8]
公司类型: hosting [0 7 A] 
是否云提供商: Yes [7 D] 
是否数据中心: Yes [0 1 5 6 A C] No [8]
是否移动设备: No [5 A C] Yes [E]
是否代理: Yes [E] No [0 1 4 5 6 7 8 9 A B C D]
是否VPN: No [0 1 6 7 C D] Yes [A E]
是否TorExit: No [1 7 D] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B E] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D E] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 314(Total_Check) 0(Clean) 7(Blacklisted) 23(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 4 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0011 (Low) [A] 
公司滥用得分(越低越好): 0.0045 (Low) [A] 
威胁级别: low [B] 
安全信息:
使用类型: hosting [A] DataCenter/WebHosting/Transit [3]
公司类型: hosting [A] 
是否云提供商: Yes [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A B D] 
是否VPN: Yes [A] No [1 D]
是否Tor: No [1 3 A B D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A B] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 0(Blacklisted) 314(Other) 
Google搜索可行性：YES
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✘     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✔     ✔     ✘     ✔     ✘    
Sohu      ✘     ✔     ✘     ✘     ✔     ✘    
Yandex    ✘     ✔     ✔     ✘     ✔     ✘    
Gmail     ✘     ✔     ✘     ✘     ✘     ✘    
Outlook   ✘     ✘     ✔     ✘     ✔     ✘    
Office365 ✘     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘    
MailRU    ✘     ✔     ✘     ✘     ✔     ✘    
AOL       ✘     ✔     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✔     ✔     ✘     ✔     ✘    
-----------------------全国延迟检测--本脚本原创-------------------------
 联通福州          21  | 联通南充          45  | 联通天津          38  |
 联通太原          43  | 联通大连          53  | 联通上海          27  |
 联通WuXi          32  | 电信长沙          26  | 电信扬州          31  |
 电信杭州          31  | 电信宁波          34  | 电信苏州          44  |
 电信武汉          47  | 电信南京          44  | 电信Zhenjiang     47  |
 移动杭州          34  | 移动成都          58  | 移动福州          57  |
 移动Beijing       45  |
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟
Speedtest.net    188.01Mbps      1109.82Mbps     2.06ms
中国香港         207.89Mbps      1105.75Mbps     3.87ms
联通WuXi         12.79Mbps       87.57Mbps       32.43ms
电信浙江         8.80Mbps        97.76Mbps       35.76ms
电信浙江         7.20Mbps        135.07Mbps      32.30ms
移动杭州5G       17.59Mbps       217.70Mbps      34.52ms
移动Beijing      19.32Mbps       222.46Mbps      47.91ms
------------------------------------------------------------------------
 总共花费      : 5 分 24 秒
 时间          : Fri Jan 10 09:49:21 UTC 2025
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>ClawCloud 加利福尼亚机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-clawclouds-california-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-clawclouds-california-hybrid-machine/</guid><description>留档-ClawCloud 加利福尼亚机器留档测试</description><pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 20 GiB&lt;/li&gt;
&lt;li&gt;带宽 200 Mbps&lt;/li&gt;
&lt;li&gt;流量 500 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                       IP质量体检报告：47.251.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:30:42 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS45102
组织：                  Alibaba US Technology Co., Ltd.
坐标：                  121°57′16″W, 37°21′11″N
地图：                  https://check.place/37.353,-121.9544,15,cn
城市：                  加州, 圣克拉拉, 95052
使用地：                [US]美国, [NA]北美洲
注册地：                [US]美国
时区：                  America/Los_Angeles
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     机房        机房        机房        机房        机房    
公司类型：     机房        机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：                 25|中风险
ipapi：                            1.94%|较高风险
AbuseIPDB：    0|低风险
IPQS：                        75|可疑IP
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [US]    [US]    [US]    [US]    [US]    [US]    [US]    [US]
代理：     否      否      否      是      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      是      否      是      否      无      否      否 
服务器：   是      是      是      无      否      否      是      是 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     解锁     屏蔽     解锁     解锁     解锁     屏蔽     解锁   
地区：     [US]              [US]     [US]     [US]              [US]   
方式：     原生              原生     原生     原生              原生   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 425   已标记 14   黑名单 0
========================================================================
今日IP检测量：133；总检测量：175239。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/IP/7LKO1GB7S.svg

########################################################################
                IP质量体检报告：240b:4004:108:*:*:*:*:*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:30:42 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS45102
组织：                  Alibaba US Technology Co., Ltd.
坐标：                  118°14′38″W, 34°3′16″N
地图：                  https://check.place/34.0544,-118.244,13,cn
城市：                  加州
使用地：                [US]美国, [NA]北美洲
注册地：                [SG]新加坡
时区：                  America/Los_Angeles
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     机房        机房        机房        机房        机房    
公司类型：     机房        机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：                 25|中风险
ipapi：    0.00%|极低风险
AbuseIPDB：    0|低风险
IPQS：                        75|可疑IP
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [US]    [US]    [US]    [US]    [US]    [US]    [US]    [US]
代理：     否      否      否      是      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      否      否      是      否      无      否      否 
服务器：   是      是      是      无      否      否      是      否 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     屏蔽     解锁     解锁     屏蔽     屏蔽     失败   
地区：                       [SG]     [US]                              
方式：                       原生     原生                              
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：135；总检测量：175241。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/IP/3VYFB1EIX.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.01.02
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel(R) Xeon(R) Platinum
 CPU 核心数        : 1
 CPU 频率          : 2499.998 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 1.00 MB / L3: 33.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ❌ Disabled
 内存              : 136.08 MiB / 923.82 MiB
 Swap              : [ no swap partition or swap file detected ]
 硬盘空间          : 911.32 MiB / 19990.38 MiB
 启动盘路径        : /dev/vda1
 系统在线时间      : 0 days, 0 hour 18 min
 负载              : 0.79, 0.30, 0.13
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-28-cloud-amd64
 TCP加速方式       : bbr
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS45102 Alibaba (US) Technology Co., Ltd.
 IPV4 位置         : San Jose / California / US
 IPV6 ASN          : AS45102 Alibaba
 IPV6 位置         : California / United States
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          1058 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          21311.86 MB/s
 单线程写测试:          13920.52 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         23.5 MB/s (5744 IOPS, 4.46s)            30.9 MB/s (7543 IOPS, 3.39s)
 1GB-1M Block           232 MB/s (221 IOPS, 4.52s)              209 MB/s (199 IOPS, 5.02s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 20.67 MB/s    (5.1k) | 96.32 MB/s    (1.5k)
Write      | 20.69 MB/s    (5.1k) | 96.82 MB/s    (1.5k)
Total      | 41.37 MB/s   (10.3k) | 193.15 MB/s   (3.0k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 91.71 MB/s     (179) | 90.84 MB/s      (88)
Write      | 96.58 MB/s     (188) | 96.89 MB/s      (94)
Total      | 188.30 MB/s    (367) | 187.74 MB/s    (182)
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：美国
[IPV6]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：新加坡
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  旧金山(SFO03S22)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: NUQ(NUQ04S38)
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：US 区
[IPV6]
当前出口所在地区解锁DisneyPlus
区域：US 区
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Failed (Error: )
 Disney+:                               No (IP Banned By Disney+ 1)
 Netflix:                               Originals Only
 YouTube Premium:                       Yes (Region: US)
 Amazon Prime Video:                    Yes (Region: US)
 TVBAnywhere+:                          Yes
 Spotify Registration:                  Yes (Region: US)
 OneTrust Region:                       US [California]
 iQyi Oversea Region:                   US
 Bing Region:                           US
 YouTube CDN:                           San Francisco, CA
 Netflix Preferred CDN:                 San Jose, CA
 ChatGPT:                               Yes
 Google Gemini:                         Yes (Region: USA)
 Wikipedia Editability:                 No
 Google Play Store:                     United States 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        USD
 ---Forum---
 Reddit:                                No
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Originals Only
 YouTube Premium:                       Yes (Region: US)
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  Yes (Region: US)
 OneTrust Region:                       US [California]
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           US
 YouTube CDN:                           Mountain View
 Netflix Preferred CDN:                 Seattle, WA
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         Yes (Region: USA)
 Wikipedia Editability:                 No
 Google Play Store:                     United States 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         【US】
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 0 [8] 
VPN得分(越低越好): 100 [8] 
代理得分(越低越好): 100 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 100 [8] 
欺诈得分(越低越好): 65 [E] 17 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0011 (Low) [A] 
公司滥用得分(越低越好): 0.0183 (Elevated) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2] 可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] business [8] hosting [0 7 9 A] hosting - high probability [C]
公司类型: hosting [0 7] business [A]
是否云提供商: Yes [7 D] 
是否数据中心: No [8] Yes [0 1 5 6 A C]
是否移动设备: Yes [E] No [5 A C]
是否代理: No [0 1 4 5 6 7 8 9 A B C D] Yes [E]
是否VPN: No [0 1 6 7 C D] Yes [A E]
是否Tor: No [0 1 3 6 7 8 A B C D E] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B E]
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D E] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 314(Total_Check) 0(Clean) 5(Blacklisted) 18(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 13 [1] 
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0011 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
威胁级别: low [B] 
安全信息:
使用类型: hosting [A] DataCenter/WebHosting/Transit [3]
公司类型: hosting [A] 
是否云提供商: Yes [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A B D] 
是否VPN: No [1 A D] 
是否Tor: No [1 3 A B D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A B] 
是否匿名: No [1 D] 
是否攻击者: No [D]
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 0(Blacklisted) 314(Other) 
Google搜索可行性：NO
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✘     ✔     ✔     ✘     ✘     ✘    
163       ✘     ✘     ✘     ✘     ✘     ✘    
Sohu      ✘     ✔     ✘     ✘     ✘     ✘    
Yandex    ✘     ✘     ✘     ✘     ✘     ✘    
Gmail     ✘     ✘     ✘     ✘     ✘     ✘    
Outlook   ✘     ✘     ✔     ✘     ✘     ✘    
Office365 ✘     ✘     ✘     ✘     ✘     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✘     ✘     ✘     ✘    
MailRU    ✘     ✔     ✘     ✘     ✘     ✘    
AOL       ✘     ✘     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✘     ✘     ✘     ✘    
Sina      ✘     ✘     ✘     ✘     ✘     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信 219.141.140.10  电信163    [普通线路] 
北京联通 202.106.195.68  联通4837   [普通线路] 
北京移动 221.179.155.161 移动CMI    [普通线路] 
上海电信 202.96.209.133  检测不到回程路由节点的IP地址
上海联通 210.22.97.1     联通4837   [普通线路] 
上海移动 211.136.112.200 移动CMI    [普通线路] 
广州电信 58.60.188.222   检测不到回程路由节点的IP地址
广州联通 210.21.196.6    联通4837   [普通线路] 
广州移动 120.196.165.24  移动CMI    [普通线路] 
成都电信 61.139.2.69     检测不到回程路由节点的IP地址
成都联通 119.6.6.6       联通4837   [普通线路] 
成都移动 211.137.96.205  移动CMI    [普通线路] 
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自ipip.net或nexttrace，请知悉!
广州电信 58.60.188.222
1.21 ms         * DOD
2.83 ms         * DOD
1.17 ms         * DOD
3.84 ms         * 新加坡 阿里云
3.36 ms         AS4134 [CHINANET-US] 美国 加利福尼亚 山景城 www.chinatelecom.com.cn 电信
160.74 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
159.20 ms       AS4134 [CHINANET-BB] 中国 广东 广州 www.chinatelecom.com.cn 电信
154.34 ms       AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
1.59 ms         * DOD
2.71 ms         * DOD
1.93 ms         * RFC1918
2.31 ms         * 美国 加利福尼亚 圣何塞 阿里云
103.08 ms       AS6453 [SQNC3-TATAC] 美国 加利福尼亚 圣何塞 tatacommunications.com
2.51 ms         AS6453 美国 加利福尼亚 圣何塞 tatacommunications.com
174.74 ms       AS4837 [CU169-BACKBONE] 中国 北京 chinaunicom.cn 联通
171.81 ms       AS4837 [CU169-BACKBONE] 中国 北京 chinaunicom.cn 联通
172.92 ms       AS4837 [CU169-BACKBONE] 中国 北京 chinaunicom.cn 联通
207.49 ms       AS17816 [UNICOM-GD] 中国 广东 深圳 chinaunicom.cn 联通
423.62 ms       AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
208.22 ms       AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
1.28 ms         * DOD
2.52 ms         * DOD
1.32 ms         * RFC1918
2.42 ms         * RFC1918
1.52 ms         AS2914 [NTTA-128] 美国 加利福尼亚 圣何塞 gin.ntt.net
1.43 ms         AS2914 [NTT-BACKBONE] 美国 加利福尼亚 圣何塞 gin.ntt.net
159.68 ms       AS2914 [NTT-BACKBONE] 中国 香港 gin.ntt.net
169.85 ms       AS58453 [CMI-INT] 中国 广东 广州 cmi.chinamobile.com 移动
169.41 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
169.73 ms       AS9808 [CMNET] 中国 广东 广州 I-C chinamobileltd.com 移动
276.74 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
180.81 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
177.66 ms       AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
181.94 ms       AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟     丢包率
Speedtest.net    194.57 Mbps     194.89 Mbps     2.06     0.0%
洛杉矶           202.11 Mbps     194.83 Mbps     13.75    0.0%
日本东京         209.88 Mbps     23.52 Mbps      114.72   0.0%
联通WuXi         223.27 Mbps     225.79 Mbps     168.04   0.0%
电信浙江         224.60 Mbps     215.60 Mbps     153.56   NULL
电信Suzhou5G     127.19 Mbps     212.02 Mbps     142.70   NULL
------------------------------------------------------------------------
 总共花费      : 14 分 56 秒
 时间          : Thu Jan  9 15:42:45 CST 2025
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>野草云香港 机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-yecaoyun-hongkong-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-yecaoyun-hongkong-hybrid-machine/</guid><description>野草云香港机器留档测试</description><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 15 GiB&lt;/li&gt;
&lt;li&gt;带宽 100 Mbps&lt;/li&gt;
&lt;li&gt;流量 600 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                    IP质量体检报告(Lite)：83.229.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:34:19 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS139659
组织：                  LUCIDACLOUD LIMITED
坐标：                  114°14′60″E, 22°19′40″N
地图：                  https://check.place/22.3279,114.2499,12,cn
城市：                  Tseung Kwan O, 999077
使用地：                [HK]Hong Kong, Asia
注册地：                [HK]Hong Kong
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     机房        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：  0|低风险
ipapi：                            1.95%|较高风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [SG]    [HK]    [HK]    [HK]    [HK]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      否      否      否      否 
服务器：   是      否      否      是      是 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     解锁     解锁     中国     解锁     屏蔽     仅APP  
地区：              [SG]     [SG]     [CN]     [SG]              [HK]   
方式：              原生     原生              原生              原生   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 366   已标记 73   黑名单 0
========================================================================
今日IP检测量：141；总检测量：175247。感谢使用xy系列脚本！ 

########################################################################
             IP质量体检报告(Lite)：2001:df1:7880:*:*:*:*:*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:34:19 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（IPinfo 数据库）
自治系统号：            ASAS139659
组织：                  LUCIDACLOUD LIMITED
坐标：                  114°14′55″E, 22°15′57″N
地图：                  https://check.place/22.2657,114.2487,12,cn
城市：                  Harmony Garden, 999077
使用地：                [HK]Hong Kong, Asia
注册地：                [HK]Hong Kong
IP类型：                 原生IP 
二、IP类型属性
数据库：      IPinfo      ipapi    IP2LOCATION 
使用类型：     机房        机房        机房    
公司类型：     家宽        机房    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：  0|低风险
ipapi：    0.00%|极低风险
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi SCAMALYTICS IPinfo IPWHOIS
地区：    [HK]    [HK]    [HK]    [HK]    [HK]
代理：     否      否      否      否      否 
Tor：      否      否      否      否      否 
VPN：      否      否      否      否      否 
服务器：   是      是      否      是      否 
滥用：     否      否      无      无      无 
机器人：   否      否      否      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     解锁     解锁     中国     屏蔽     屏蔽     失败   
地区：              [HK]     [HK]     [CN]                              
方式：              原生     原生                                       
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：144；总检测量：175250。感谢使用xy系列脚本！ 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.01.02
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel(R) Xeon(R) CPU E5-2698 v4 @ 2.20GHz
 CPU 核心数        : 1
 CPU 频率          : 2199.998 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 4.00 MB / L3: 16.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ✔ Enabled
 内存              : 465.76 MiB / 973.27 MiB
 Swap              : 371.25 MiB / 1.00 GiB
 硬盘空间          : 12.58 GiB / 14.73 GiB
 启动盘路径        : /dev/sda1
 系统在线时间      : 75 days, 6 hour 29 min
 负载              : 1.54, 0.40, 0.14
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-23-cloud-amd64
 TCP加速方式       : bbr
 虚拟化架构        : KVM
 NAT类型           : Port Restricted Cone
 IPV4 ASN          : AS139659 LUCIDACLOUD LIMITED
 IPV4 位置         : Hong Kong / Hong Kong / HK
 IPV6 ASN          : AS139659 Lucidacloud
 IPV6 位置         : Hong Kong
 IPV6 子网掩码     : 128
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          772 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          16317.83 MB/s
 单线程写测试:          11380.68 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         24.7 MB/s (6036 IOPS, 4.24s)            34.2 MB/s (8341 IOPS, 3.07s)
 1GB-1M Block           726 MB/s (692 IOPS, 1.44s)              1.2 GB/s (1139 IOPS, 0.88s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
测试失败请替换另一种方式
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP完整解锁Netflix，支持非自制剧的观看
NF所识别的IP地域信息：新加坡
[IPV6]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：中国香港
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 美国  洛杉机(LAX17S56)
Youtube识别地域: 中国香港(HK)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 中国香港(HKG33S01)
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：SG 区
[IPV6]
当前出口所在地区解锁DisneyPlus
区域：HK 区
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Yes (Region: HK)
 Disney+:                               Yes (Region: SG)
 Netflix:                               Yes (Region: SG)
 YouTube Premium:                       Yes (Region: HK)
 Amazon Prime Video:                    Yes (Region: SG)
 TVBAnywhere+:                          No
 Spotify Registration:                  Yes (Region: HK)
 OneTrust Region:                       HK [Unknown]
 iQyi Oversea Region:                   HK
 Bing Region:                           SG (Risky)
 YouTube CDN:                           Los Angeles, CA
 Netflix Preferred CDN:                 Hong Kong
 ChatGPT:                               No (Only Available with Mobile APP)
 Google Gemini:                         No
 Wikipedia Editability:                 Yes
 Google Play Store:                     Hong Kong 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        HKD
 ---Forum---
 Reddit:                                Yes
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Originals Only
 YouTube Premium:                       No
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  Yes (Region: HK)
 OneTrust Region:                       HK [Unknown]
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           HK (Risky)
 YouTube CDN:                           Hong Kong
 Netflix Preferred CDN:                 Hong Kong
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         No
 Wikipedia Editability:                 Yes
 Google Play Store:                     China 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         Failed
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 97 [8]
VPN得分(越低越好): 1 [8] 
代理得分(越低越好): 6 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 2 [8] 
欺诈得分(越低越好): 65 [E] 0 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0063 (Low) [A]
公司滥用得分(越低越好): 0.0234 (Elevated) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: hosting [0 7 9 A] DataCenter/WebHosting/Transit [3] hosting - high probability [C]
公司类型: hosting [0 7] business [A]
是否云提供商: Yes [7 D] 
是否数据中心: Yes [0 1 5 6 C] No [8 A]
是否移动设备: Yes [E] No [5 A C]
是否代理: Yes [E] No [0 1 4 5 6 7 8 9 A B C D]
是否VPN: Yes [E] No [0 1 6 7 A C D]
是否Tor: No [0 1 3 6 7 8 A B C D E] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B E] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D E] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
是否机器人: No [E] 
DNS-黑名单: 314(Total_Check) 0(Clean) 5(Blacklisted) 23(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 0 [1]
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0063 (Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
威胁级别: low [B] 
安全信息:
使用类型: hosting [A] DataCenter/WebHosting/Transit [3]
公司类型: hosting [A] 
是否云提供商: Yes [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A B D] 
是否VPN: No [1 A D] 
是否Tor: No [1 3 A B D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A B] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D]
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 0(Blacklisted) 314(Other) 
Google搜索可行性：YES
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✘     ✔     ✔     ✔     ✔     ✔    
QQ        ✔     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✘     ✔     ✘     ✘     ✘    
Sohu      ✔     ✔     ✔     ✘     ✘     ✘    
Yandex    ✘     ✘     ✔     ✘     ✘     ✘    
Gmail     ✔     ✔     ✘     ✘     ✘     ✘    
Outlook   ✔     ✘     ✘     ✘     ✔     ✘    
Office365 ✔     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✔     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✔     ✔     ✔     ✘     ✘     ✘    
MailRU    ✘     ✘     ✘     ✘     ✔     ✘    
AOL       ✔     ✔     ✘     ✘     ✘     ✘    
GMX       ✔     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✘     ✘     ✘     ✔     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信 219.141.140.10  电信163    [普通线路] 
北京联通 202.106.195.68  移动CMI    [普通线路] 
北京移动 221.179.155.161 移动CMI    [普通线路] 
上海电信 202.96.209.133  电信163    [普通线路] 
上海联通 210.22.97.1     移动CMI    [普通线路] 
上海移动 211.136.112.200 移动CMI    [普通线路] 
广州电信 58.60.188.222   检测不到回程路由节点的IP地址
广州联通 210.21.196.6    移动CMI    [普通线路] 
广州移动 120.196.165.24  移动CMI    [普通线路] 
成都电信 61.139.2.69     检测不到回程路由节点的IP地址
成都联通 119.6.6.6       移动CMI    [普通线路] 
成都移动 211.137.96.205  移动CMI    [普通线路] 
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自ipip.net或nexttrace，请知悉!
广州电信 58.60.188.222
0.79 ms         AS139659 中国 香港 yecaoyun.com
0.39 ms         * RFC1918
0.84 ms         * RFC1918
2.23 ms         AS10099 中国 香港 chinaunicomglobal.com
2.59 ms         * 中国 香港
50.33 ms        AS4134 [CHINANET-HK] 中国 香港 www.chinatelecom.com.cn
60.92 ms        AS4134 [CHINANET-GD] 中国 广东 深圳 www.chinatelecom.com.cn 电信
61.45 ms        AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
0.84 ms         AS139659 中国 香港 yecaoyun.com
0.67 ms         * RFC1918
2.85 ms         AS40065 [DATA-CENTRE] 中国 香港 cnservers.com
15.40 ms        AS58453 [CMI-INT] 中国 香港 cmi.chinamobile.com 移动
27.42 ms        AS58453 [CMI-INT] 中国 上海 cmi.chinamobile.com 移动
28.55 ms        AS9808 [CMNET] 中国 上海 X-I chinamobileltd.com 移动
28.83 ms        AS9808 [CMNET] 中国 上海 I-C chinamobileltd.com 移动
29.80 ms        AS9808 [CMNET] 中国 上海 chinamobileltd.com 移动
59.89 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
104.37 ms       AS4837 [CU169-BACKBONE] 中国 广东 广州 chinaunicom.cn
108.42 ms       AS17816 [UNICOM-GD] 中国 广东 深圳 chinaunicom.cn 联通
114.33 ms       AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
109.99 ms       AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
0.68 ms         AS139659 中国 香港 yecaoyun.com
0.77 ms         * RFC1918
0.66 ms         AS7578 [STREAMLINESERVERS-AU] 澳大利亚 维多利亚州 墨尔本 globalsecurelayer.com
1.26 ms         AS136510 中国 香港 Streamline Servers Pty Ltd
4.85 ms         AS3356 中国 香港 lumen.com
2.94 ms         AS58453 [CMI-INT] 中国 香港 cmi.chinamobile.com 移动
2.53 ms         AS58453 [CMI-INT] 中国 香港 cmi.chinamobile.com 移动
7.64 ms         AS58453 [CMI-INT] 中国 广东 广州 cmi.chinamobile.com 移动
135.43 ms       AS9808 [CMNET] 中国 广东 广州 X-I chinamobileltd.com 移动
33.36 ms        AS9808 [CMNET] 中国 广东 广州 I-C chinamobileltd.com 移动
30.81 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
36.17 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
39.45 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
35.75 ms        AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟     丢包率
新加坡           101.39 Mbps     99.02 Mbps      32.80    NULL
联通WuXi         105.58 Mbps     107.34 Mbps     87.03    0.0%
联通上海5G       105.94 Mbps     99.10 Mbps      136.88   65.0%
电信Zhenjiang5G  106.33 Mbps     101.32 Mbps     92.13    NULL
电信Suzhou5G     104.12 Mbps     100.99 Mbps     80.58    NULL
移动杭州5G       104.37 Mbps     101.00 Mbps     31.92    0.0%
移动Fujian       102.88 Mbps     99.59 Mbps      45.39    NULL
------------------------------------------------------------------------
 总共花费      : 6 分 53 秒
 时间          : Tue Jan  7 22:29:12 EST 2025
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>MikyHost 日本东京机器购买与留档测试</title><link>https://www.mihouo.com/posts/server/review-of-mikyhost-aws-japan-tokyo-machines-and-hybrid-systems/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-mikyhost-aws-japan-tokyo-machines-and-hybrid-systems/</guid><description>MikyHost 日本东京机器购买与留档测试</description><pubDate>Tue, 07 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 2 vCPU&lt;/li&gt;
&lt;li&gt;内存 512 Mib&lt;/li&gt;
&lt;li&gt;存储 20 GiB&lt;/li&gt;
&lt;li&gt;带宽 5 5Gbps&lt;/li&gt;
&lt;li&gt;流量 1 T&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;优惠购买&lt;/h2&gt;
&lt;h3&gt;购买&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://mikyhost.com/aff.php?aff=241&quot;&gt;官网(含Aff)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;VPS&lt;/code&gt; -&amp;gt; &lt;code&gt;Cheap VPS&lt;/code&gt; -&amp;gt; &lt;code&gt;Order Now (Package 1 512 RAM、 2vCPU、 20GB SSD、 1T 流量)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认 &lt;code&gt;Annually&lt;/code&gt; 订阅，&lt;code&gt;Region&lt;/code&gt; 根据需要选择，&lt;a href=&quot;http://ec2-reachability.amazonaws.com/&quot;&gt;AWS ip 测试地址&lt;/a&gt;，&lt;code&gt;Operating System&lt;/code&gt; 推荐选择 &lt;code&gt;Debian&lt;/code&gt;(毕竟就 512MB 内存)，&lt;code&gt;Server Management&lt;/code&gt; 默认，&lt;code&gt;Hostname&lt;/code&gt; 唯一，填写完成后点击右侧 &lt;code&gt;Continue&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然后进行创建账户，根据个人信息填写&lt;/p&gt;
&lt;p&gt;:::important
&lt;code&gt;Additional Information&lt;/code&gt;，&lt;code&gt;Choose Currency&lt;/code&gt; 一定要选择 &lt;code&gt;IDR&lt;/code&gt;，能省 Paypal 手续费(2.99$)
&lt;img src=&quot;1.png&quot; alt=&quot;alt text&quot; /&gt;
:::&lt;/p&gt;
&lt;p&gt;全部填写完成后点击 &lt;code&gt;Register&lt;/code&gt; 进行注册，然后输入优惠码进行付款，支持 &lt;code&gt;Paypal&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;4 折 优惠码：&lt;code&gt;BlackFriday24&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::tip
截止今日 2025-01-07 此优惠码依然有效
:::&lt;/p&gt;
&lt;h3&gt;关闭防火墙&lt;/h3&gt;
&lt;p&gt;创建完成后，会收到邮件，进入控制台，打开防火墙。&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;firewall&lt;/code&gt; -&amp;gt; &lt;code&gt;Add Rule&lt;/code&gt; -&amp;gt; &lt;code&gt;Application&lt;/code&gt; 选择 &lt;code&gt;All TCP + UDP&lt;/code&gt; -&amp;gt; &lt;code&gt;Add Rule&lt;/code&gt;。
&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;
&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;测试 IP 是否被墙&lt;/h3&gt;
&lt;p&gt;:::tip
AWS 机器可以通过关机在开机更换 IP，因此不怕 IP 被墙，但是可能分配你的 IP 默认是被墙的需要手动开关机更换。
:::&lt;/p&gt;
&lt;p&gt;拿到 &lt;code&gt;Public IP Address&lt;/code&gt;， 测试 IP 是否被墙，&lt;a href=&quot;http://ec2-reachability.amazonaws.com/&quot;&gt;ITDOG&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全红: 未关闭防火墙&lt;/li&gt;
&lt;li&gt;部分红: 已关闭防火墙，但 ip 被墙&lt;/li&gt;
&lt;li&gt;全绿: 已关闭防火墙，且 ip 未被墙&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;关闭 Paypal 自动续费&lt;/h3&gt;
&lt;p&gt;由于此机器只有第一年优惠，所以需要关闭 Paypal 自动续费，否则第二年续费会自动原价扣款。&lt;/p&gt;
&lt;p&gt;登录 Paypal 账户，找到刚刚的付款记录，点击&lt;code&gt;管理 MikyHost.com 付款&lt;/code&gt;，然后在状态栏点击 &lt;code&gt;取消&lt;/code&gt;，然后同意即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-5.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;关闭弹窗后状态栏会显示 &lt;code&gt;无效&lt;/code&gt;，即取消成功。&lt;/p&gt;
&lt;h2&gt;切换 root 用户&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo -s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;开启 ssh 账号密码登录&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 将代码中的第一句中的 Passwd 改为自己将要设置的密码，否则默认 root 密码为 Passwd。
echo root:Passwd |sudo chpasswd root
sudo sed -i &apos;s/^#\?PermitRootLogin.*/PermitRootLogin yes/g&apos; /etc/ssh/sshd_config;
sudo sed -i &apos;s/^#\?PasswordAuthentication.*/PasswordAuthentication yes/g&apos; /etc/ssh/sshd_config;
sudo systemctl restart sshd.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;IP Check&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bash &amp;lt;(curl -Ls IP.Check.Place)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;########################################################################
                       IP质量体检报告：57.180.*.*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:36:53 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS16509
组织：                  AMAZON-02
坐标：                  139°41′24″E, 35°41′21″N
地图：                  https://check.place/35.6893,139.6899,14,cn
城市：                  东京都, 东京, 151-0053
使用地：                [JP]日本, [AS]亚洲
注册地：                [US]美国
时区：                  Asia/Tokyo
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     机房        机房        商业        机房        机房    
公司类型：     机房        机房        商业    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：  0|低风险
ipapi：    0.01%|极低风险
AbuseIPDB：    0|低风险
IPQS：                        75|可疑IP
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [JP]    [JP]    [JP]    [JP]    [JP]    [JP]    [JP]    [JP]
代理：     否      否      否      是      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      是      否      是      否      无      否      否 
服务器：   是      是      是      无      否      否      是      是 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     解锁     解锁     解锁     解锁     屏蔽     解锁   
地区：              [HK]     [HK]     [JP]     [JP]              [SG]   
方式：               DNS      DNS     原生     原生               DNS   
六、邮局连通性及黑名单检测
本地25端口：阻断
IP地址黑名单数据库：  有效 439   正常 425   已标记 14   黑名单 0
========================================================================
今日IP检测量：147；总检测量：175253。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/IP/19NT6BIL2.svg

########################################################################
                IP质量体检报告：2406:da14:c29:*:*:*:*:*
                    bash &amp;lt;(curl -sL IP.Check.Place)
                   https://github.com/xykt/IPQuality
        报告时间：2025-04-11 10:36:53 CST  脚本版本：v2025-03-25
########################################################################
一、基础信息（Maxmind 数据库）
自治系统号：            AS16509
组织：                  AMAZON-02
坐标：                  139°41′24″E, 35°41′21″N
地图：                  https://check.place/35.6893,139.6899,15,cn
城市：                  东京都, 东京, 151-0053
使用地：                [JP]日本, [AS]亚洲
注册地：                [US]美国
时区：                  Asia/Tokyo
IP类型：                 广播IP 
二、IP类型属性
数据库：      IPinfo    ipregistry    ipapi     AbuseIPDB  IP2LOCATION 
使用类型：     机房        机房        商业        机房        机房    
公司类型：     机房        机房        商业    
三、风险评分
风险等级：      极低         低       中等       高         极高
SCAMALYTICS：                         38|中风险
ipapi：    0.00%|极低风险
AbuseIPDB：    0|低风险
IPQS：                        75|可疑IP
Cloudflare：   0|低风险
DB-IP：         |低风险
四、风险因子
库： IP2LOCATION ipapi ipregistry IPQS SCAMALYTICS ipdata IPinfo IPWHOIS
地区：    [US]    [JP]    [JP]    [JP]    [JP]    [JP]    [JP]    [JP]
代理：     否      否      否      是      否      否      否      否 
Tor：      否      否      否      否      否      否      否      否 
VPN：      否      否      否      是      否      无      否      否 
服务器：   是      是      是      无      否      否      是      否 
滥用：     否      否      否      否      无      否      无      无 
机器人：   否      否      无      否      否      无      无      无 
五、流媒体及AI服务解锁检测
服务商：  TikTok   Disney+  Netflix Youtube  AmazonPV  Spotify  ChatGPT 
状态：     失败     失败     失败     解锁     屏蔽     屏蔽     失败   
地区：                                [JP]                              
方式：                                原生                              
六、邮局连通性及黑名单检测
本地25端口：阻断
========================================================================
今日IP检测量：148；总检测量：175254。感谢使用xy系列脚本！ 
报告链接：https://Report.Check.Place/IP/PNFC1T9O8.svg
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;融合怪测试&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;由于此商家机器仅支持 ssh 密钥登录，我在开启 ssh 可以使用账号密码登录时已修改 ssh 配置文件后还未进行设置密码就与服务器断了开链接，导致现在无法使用密钥登录，也无法使用账号密码登录。只能重装系统，但是重装系统需要工单申请(工单回复较慢一般 24 小时起步)，所以暂时无法进行测试，后续补上融合怪测试。&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已补上融合怪测试&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;详情&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2025.01.02
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz
 CPU 核心数        : 2
 CPU 频率          : 2499.998 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 1.00 MB / L3: 35.75 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ❌ Disabled
 内存              : 106.25 MiB / 447.84 MiB
 Swap              : [ no swap partition or swap file detected ]
 硬盘空间          : 1.03 GiB / 19.46 GiB
 启动盘路径        : /dev/nvme0n1p1
 系统在线时间      : 0 days, 2 hour 28 min
 负载              : 0.55, 0.15, 0.05
 系统              : Debian GNU/Linux 12 (bookworm) (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 6.1.0-28-cloud-amd64
 TCP加速方式       : cubic
 虚拟化架构        : Amazon Virtualization
 NAT类型           : Full Cone
 IPV4 ASN          : AS16509 Amazon.com, Inc.
 IPV4 位置         : Tokyo / Tokyo / JP
 IPV6 ASN          : AS16509 Amazon.com
 IPV6 位置         : Tokyo / Tokyo / Japan
 IPV6 子网掩码     : 128
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          881 Scores
 2 线程测试(多核)得分:          1652 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          19843.65 MB/s
 单线程写测试:          16095.18 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         4.3 MB/s (1062 IOPS, 24.11s)            6.5 MB/s (1599 IOPS, 16.01s)
 1GB-1M Block           150 MB/s (143 IOPS, 6.99s)              141 MB/s (134 IOPS, 7.45s)
---------------------磁盘fio读写测试--感谢yabs开源----------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 6.20 MB/s     (1.5k) | 66.01 MB/s    (1.0k)
Write      | 6.19 MB/s     (1.5k) | 66.43 MB/s    (1.0k)
Total      | 12.39 MB/s    (3.0k) | 132.44 MB/s   (2.0k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 63.21 MB/s     (123) | 62.78 MB/s      (61)
Write      | 66.60 MB/s     (130) | 67.25 MB/s      (65)
Total      | 129.82 MB/s    (253) | 130.03 MB/s    (126)
------------流媒体解锁--基于oneclickvirt/CommonMediaTests开源-----------
以下测试的解锁地区是准确的，但是不是完整解锁的判断可能有误，这方面仅作参考使用
----------------Netflix-----------------
[IPV4]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：日本
[IPV6]
您的出口IP可以使用Netflix，但仅可看Netflix自制剧
NF所识别的IP地域信息：日本
----------------Youtube-----------------
[IPV4]
连接方式: Youtube Video Server
视频缓存节点地域: 日本 东京(NRT12S24)
Youtube识别地域: 日本(JP)
[IPV6]
连接方式: Youtube Video Server
视频缓存节点地域: 日本 东京(NRT12S24)
Youtube识别地域: 日本(JP)
---------------DisneyPlus---------------
[IPV4]
当前出口所在地区解锁DisneyPlus
区域：JP 区
[IPV6]
当前出口所在地区解锁DisneyPlus
区域：JP 区
解锁Netflix，Youtube，DisneyPlus上面和下面进行比较，不同之处自行判断
----------------流媒体解锁--感谢RegionRestrictionCheck开源--------------
 以下为IPV4网络测试，若无IPV4网络则无输出
============[ Multination ]============
 Dazn:                                  Yes (Region: JP)
 Disney+:                               No (IP Banned By Disney+ 1)
 Netflix:                               Originals Only
 YouTube Premium:                       Yes (Region: JP)
 Amazon Prime Video:                    Yes (Region: JP)
 TVBAnywhere+:                          Yes
 Spotify Registration:                  Yes (Region: JP)
 OneTrust Region:                       JP [Tokyo]
 iQyi Oversea Region:                   JP
 Bing Region:                           JP (Risky)
 YouTube CDN:                           Tokyo
 Netflix Preferred CDN:                 Mumbai (Bombay)
 ChatGPT:                               Yes
 Google Gemini:                         Yes (Region: JPN)
 Wikipedia Editability:                 No
 Google Play Store:                     Japan 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        JPY
 ---Forum---
 Reddit:                                No
=======================================
 以下为IPV6网络测试，若无IPV6网络则无输出
============[ Multination ]============
 Dazn:                                  IPv6 Is Not Currently Supported
 Disney+:                               IPv6 Is Not Currently Supported
 Netflix:                               Originals Only
 YouTube Premium:                       Yes (Region: JP)
 Amazon Prime Video:                    IPv6 Is Not Currently Supported
 TVBAnywhere+:                          IPv6 Is Not Currently Supported
 Spotify Registration:                  Yes (Region: JP)
 OneTrust Region:                       JP [Tokyo]
 iQyi Oversea Region:                   IPv6 Is Not Currently Supported
 Bing Region:                           JP (Risky)
 YouTube CDN:                           Tokyo
 Netflix Preferred CDN:                 Seattle, WA
 ChatGPT:                               Failed (Network Connection)
 Google Gemini:                         Yes (Region: JPN)
 Wikipedia Editability:                 No
 Google Play Store:                     Japan 
 Google Search CAPTCHA Free:            Yes
 Steam Currency:                        IPv6 Is Not Currently Supported
 ---Forum---
 Reddit:                                IPv6 Is Not Currently Supported
=======================================
---------------TikTok解锁--感谢lmc999的源脚本及fscarmen PR--------------
 Tiktok Region:         【JP】
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 0 [8] 
VPN得分(越低越好): 100 [8] 
代理得分(越低越好): 100 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2] 
威胁得分(越低越好): 100 [8] 
欺诈得分(越低越好): 15 [1] 
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0001 (Very Low) [A] 
公司滥用得分(越低越好): 0.0002 (Very Low) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] hosting [0 7 8 9] business [A] unknown [C]
公司类型: hosting [0 7 A] 
是否云提供商: Yes [7 D] 
是否数据中心: No [C] Yes [0 1 5 6 8 A]
是否移动设备: No [5 A C] 
是否代理: No [0 1 4 5 6 7 8 9 A B C D] 
是否VPN: No [0 1 6 7 C D] Yes [A]
是否TorExit: No [1 7 D] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 5(Blacklisted) 20(Other) 
IPV6:
安全得分:
欺诈得分(越低越好): 12 [1] 
滥用得分(越低越好): 0 [3]
ASN滥用得分(越低越好): 0.0001 (Very Low) [A] 
公司滥用得分(越低越好): 0 (Very Low) [A] 
威胁级别: low [B] 
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] business [A]
公司类型: business [A] 
是否云提供商: Yes [D] 
是否数据中心: Yes [1 A] 
是否移动设备: No [A] 
是否代理: No [1 A B D] 
是否VPN: No [1 A D] 
是否TorExit: No [1 D] 
是否Tor出口: No [1 D] 
是否网络爬虫: No [A B] 
是否匿名: No [1 D] 
是否攻击者: No [D] 
是否滥用者: No [A D] 
是否威胁: No [D] 
是否中继: No [D] 
是否Bogon: No [A D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 0(Blacklisted) 314(Other) 
Google搜索可行性：YES
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✘     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✔     ✔     ✘     ✔     ✘    
Sohu      ✘     ✔     ✔     ✘     ✔     ✘    
Yandex    ✘     ✔     ✔     ✘     ✔     ✘    
Gmail     ✘     ✔     ✘     ✘     ✘     ✘    
Outlook   ✘     ✘     ✔     ✘     ✔     ✘    
Office365 ✘     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘    
MailRU    ✘     ✔     ✘     ✘     ✔     ✘    
AOL       ✘     ✔     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✔     ✔     ✘     ✔     ✘    
----------------三网回程--基于oneclickvirt/backtrace开源----------------
北京电信 219.141.140.10  电信163    [普通线路] 
北京联通 202.106.195.68  联通4837   [普通线路] 
北京移动 221.179.155.161 移动CMI    [普通线路] 
上海电信 202.96.209.133  电信163    [普通线路] 
上海联通 210.22.97.1     联通4837   [普通线路] 
上海移动 211.136.112.200 移动CMI    [普通线路] 
广州电信 58.60.188.222   检测不到回程路由节点的IP地址
广州联通 210.21.196.6    联通4837   [普通线路] 
广州移动 120.196.165.24  移动CMI    [普通线路] 
成都电信 61.139.2.69     电信163    [普通线路] 
成都联通 119.6.6.6       联通4837   [普通线路] 
成都移动 211.137.96.205  移动CMI    [普通线路] 
准确线路自行查看详细路由，本测试结果仅作参考
同一目标地址多个线路时，可能检测已越过汇聚层，除了第一个线路外，后续信息可能无效
---------------------回程路由--感谢fscarmen开源及PR---------------------
依次测试电信/联通/移动经过的地区及线路，核心程序来自ipip.net或nexttrace，请知悉!
广州电信 58.60.188.222
6.22 ms         * RFC1112
0.36 ms         * RFC1112
1.69 ms         * RFC1112
1.41 ms         AS16509 [AT-88] 日本 东京都 东京 AMAZON amazon.com
2.73 ms         AS2914 日本 东京都 东京 gin.ntt.net
2.15 ms         AS2914 [NTT-BACKBONE] 日本 东京都 东京 gin.ntt.net
* ms    AS2914 [NTT-BACKBONE] 日本 东京都 东京 gin.ntt.net
* ms    AS4134 [CHINANET-BB] 中国 北京 www.chinatelecom.com.cn 电信
* ms    AS4134 中国 广东 深圳 福田区 www.chinatelecom.com.cn 电信
广州联通 210.21.196.6
2.49 ms         * RFC1112
0.97 ms         * RFC1112
0.43 ms         * RFC1112
2.29 ms         * RFC1112
3.01 ms         * RFC1112
2.07 ms         AS16509 日本 东京都 东京 AMAZON amazon.com
1.75 ms         * [AMAZO-4] AMAZON.COM
63.15 ms        AS4837 [CU169-BACKBONE] 中国 上海 chinaunicom.cn 联通
61.08 ms        AS4837 [CU169-BACKBONE] 中国 上海 chinaunicom.cn 联通
89.83 ms        AS17816 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
88.50 ms        AS17623 [APNIC-AP] 中国 广东 深圳 chinaunicom.cn 联通
85.25 ms        AS17623 中国 广东 深圳 宝安区 chinaunicom.cn 联通
广州移动 120.196.165.24
2.24 ms         * RFC1112
1.38 ms         * RFC1112
0.95 ms         * RFC1112
1.57 ms         * RFC6598
24.33 ms        * [AT-88] 日本 东京都 东京
2.59 ms         * [AMAZO-4] 日本 东京都 东京
3.45 ms         AS58453 [CMI-INT] 日本 东京都 东京 cmi.chinamobile.com 移动
58.09 ms        AS9808 [CMNET] 中国 广东 广州 I-C chinamobileltd.com 移动
* ms    AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
64.28 ms        AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
* ms    AS9808 [CMNET] 中国 广东 广州 chinamobileltd.com 移动
63.83 ms        AS56040 [APNIC-AP] 中国 广东 深圳 gd.10086.cn 移动
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟     丢包率
Speedtest.net    3934.75 Mbps    3509.57 Mbps    2.15     0.0%
日本东京         931.23 Mbps     750.65 Mbps     4.11     0.0%
中国香港         1503.24 Mbps    1974.84 Mbps    54.59    NULL
联通WuXi         2049.09 Mbps    3210.32 Mbps    64.08    0.0%
电信Suzhou5G     1279.96 Mbps    2891.50 Mbps    75.74    NULL
电信浙江         1500.05 Mbps    2511.40 Mbps    43.47    NULL
移动杭州5G       1125.56 Mbps    1766.46 Mbps    42.41    0.0%
移动Chengdu      1150.57 Mbps    1181.10 Mbps    106.13   NULL
------------------------------------------------------------------------
 总共花费      : 8 分 43 秒
 时间          : Wed Jan  8 01:54:56 UTC 2025
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>白嫖 Cursor Pro</title><link>https://www.mihouo.com/posts/tool-share/free-cursor/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/free-cursor/</guid><description>使用无限邮箱和刷新机器码无限试用 Cursor Pro</description><pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;s&gt;低成本稳定使用&lt;/s&gt;&lt;/h2&gt;
&lt;p&gt;2025年 5 月 7 日，Cursor 突然开放了教育优惠，只需要认证邮箱为学校邮箱，即可免费使用一年 Cursor Pro。&lt;/p&gt;
&lt;p&gt;:::note
&lt;s&gt;&lt;a href=&quot;https://www.cursor.com/cn/students&quot;&gt;申请教育优惠&lt;/a&gt;&lt;/s&gt;  2025-05-09 Cursor 开始大规模封杀使用教育优惠的账号，请谨慎使用，如果你是真学生，可以尝试使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;目前(2025-05-08)&lt;/code&gt;由于申请人数过多，已关闭中国大陆学生的申请通道，但是其他国家的学生仍然可以申请。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;如果不是学生身份，可以考虑闲鱼购买已认证的账号，价格在 100 元左右。&lt;/s&gt;
&lt;strong&gt;不推荐购买，因为现在申请教育优惠的账号，基本都被封杀了，即使是真学生也有很多账号被封杀。&lt;/strong&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;白嫖记录&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;2025 年 5 月 06 日&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::important
截止至 2025-05-06 日，此方式仍然可用。但是现在限制颇多，&lt;strong&gt;临时邮箱和别名邮箱基本不可用，需要购买 gmail、outlook、hotmail 等邮箱&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在最新版本的cursor 客户端中使用时，会使账号试用提前结束(只活了 1 天)，需要在旧的客户端中使用。&lt;a href=&quot;https://www.cursor.com/cn/downloads&quot;&gt;官网历史版本下载&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同一台机器上注册多个账号会提示 &lt;code&gt;too many free trials accounts used on this machine&lt;/code&gt;，这时需要更换机器码。
:::&lt;/p&gt;
&lt;p&gt;:::note
&lt;a href=&quot;https://github.com/settings/copilot&quot;&gt;Github Copilot&lt;/a&gt; 在 2024-12-06 宣布免费使用(每月 2000 次代码补全，以及 50次对话)，对于轻度用户来说，已经足够了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;
:::&lt;/p&gt;
&lt;h2&gt;账号注册&lt;/h2&gt;
&lt;p&gt;&lt;s&gt;### 方法一：别名邮箱&lt;/s&gt; (同一个邮箱注册多个别名后，会导致主域无法再次注册)&lt;/p&gt;
&lt;p&gt;由于 Cursor 试用只需要一个邮箱验证即可，并不像其他的很多订阅服务需要验证信用卡，这使得白嫖 Cursor 变得非常简单。&lt;/p&gt;
&lt;p&gt;我们只需要一个支持无限邮箱的邮箱，比如 Gmail、Outlook，就可以无限试用 Cursor Pro。如果你没有 Gmail，&lt;s&gt;也可以&lt;a href=&quot;https://www.2925.com&quot;&gt;2925无限邮&lt;/a&gt;&lt;/s&gt;(无法接收到邮件，推荐使用Gmail，或者使用临时邮箱)&lt;/p&gt;
&lt;p&gt;我这里以 Gmail 为例，演示如何无限试用 Cursor Pro。&lt;/p&gt;
&lt;p&gt;Gmail 支持别名,可以在** &lt;code&gt;+&lt;/code&gt; **号之后填写任意字符，因此我们可以创建一个无限邮箱。&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;xxx@gmail.com&lt;/code&gt; 与 &lt;code&gt;xxx+aaa@gmail.com&lt;/code&gt;、&lt;code&gt;xxx+bbb@gmail.com&lt;/code&gt;，可以看作同一个邮箱，给后二者别名邮箱发邮件 第一个主邮箱也可以收到。&lt;/p&gt;
&lt;p&gt;因此我们在注册 Cursor 时，只需要使用 &lt;code&gt;xxx+aaa@gmail.com&lt;/code&gt; 这个邮箱即可，然后随便注册一个密码，即可无限试用 Cursor Pro。&lt;/p&gt;
&lt;p&gt;&lt;s&gt;### 方法二：临时邮箱&lt;/s&gt; (基本无法通过注册)&lt;/p&gt;
&lt;p&gt;临时邮箱，顾名思义，就是临时使用的邮箱，一般用于注册一些需要邮箱验证的网站。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://temp-mail.org/zh&quot;&gt;temp-mail&lt;/a&gt; 是一个提供临时邮箱的网站，我们可以使用这个网站的邮箱来注册 Cursor。&lt;/p&gt;
&lt;h3&gt;方法三：购买邮箱 (目前最推荐的方式，需要配合低版本 Cursor 客户端使用)&lt;/h3&gt;
&lt;p&gt;google 搜索 gmail 邮箱购买，可以购买到很多便宜的邮箱&lt;/p&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gmail 邮箱 均价 3 元每个&lt;/li&gt;
&lt;li&gt;outlook 邮箱 均价 0.15 元每个&lt;/li&gt;
&lt;li&gt;hotmail 邮箱 均价 0.15 元每个 (2025-05-06日，实测注册可用)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;购买邮箱后，注册 Cursor 时，使用购买的邮箱即可。&lt;/p&gt;
&lt;h2&gt;更换机器码&lt;/h2&gt;
&lt;p&gt;:::tip
Cursor 最近有了新的限制，如果在同一台机器上注册多个账号，会提示 &lt;code&gt;too many free trials accounts used on this machine&lt;/code&gt;，因此需要切换机器码。
:::&lt;/p&gt;
&lt;h3&gt;Cursor Free Trial Reset Tool&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/yuaotian/go-cursor-help&quot;&gt;GO-Cursor-Help&lt;/a&gt;
::github{repo=&quot;yuaotian/go-cursor-help&quot;}&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zetaloop/cursor-shadow-patch&quot;&gt;Cursor Shadow Patch&lt;/a&gt;
::github{repo=&quot;zetaloop/cursor-shadow-patch&quot;}&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;让你的 Cursor 更智能&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/PRPz-qVkFJSgkuEKkTdzwg&quot;&gt;AI 通用开发助手提示词指南&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;让你的 Cursor 更稳定&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/pnJrH7Ifx4WZvseeP1fcEA&quot;&gt;Cursor异常问题收集和解决方案&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>在 Apple TV 中使用 AList</title><link>https://www.mihouo.com/posts/apple-tv/using-alist-on-apple-tv/</link><guid isPermaLink="true">https://www.mihouo.com/posts/apple-tv/using-alist-on-apple-tv/</guid><description>在 Apple TV 中配置 AList 实现流媒体播放</description><pubDate>Tue, 31 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;前提信息&lt;/h1&gt;
&lt;p&gt;Apple TV 局域网 ip: 192.168.2.2&lt;/p&gt;
&lt;p&gt;docker 宿主机局域网 ip: 192.168.2.3&lt;/p&gt;
&lt;h1&gt;基础使用&lt;/h1&gt;
&lt;p&gt;使用的是三方 token，因此需要开通阿里云盘的三方会员，否则会限速到 5 Mbps。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;AList 是一个支持多种存储，支持网页浏览和 WebDAV 的文件列表程序，由 gin 和 Solidjs 驱动。&lt;/p&gt;
&lt;p&gt;由于 Apple TV 中并不能直接安装 AList，因此需要使用 AListServer 来搭建 AList 服务。&lt;/p&gt;
&lt;p&gt;AListServer 是 Apple App Store 上的一个 AList 客户端，可以方便搭建 AList 服务，并提供了可视化管理界面。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 Apple TV 上打开 App Store，搜索 AListServer，并下载安装。&lt;/li&gt;
&lt;li&gt;安装完成后，打开 AListServer，启动服务。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;:::tip
如果在同一局域网下，可以输入运行 AListServer 的设备 IP 地址和端口号即可访问 AList 服务。&lt;/p&gt;
&lt;p&gt;如果不在同一局域网下，通过端口转发，将 AListServer 的端口转发到公网，即可在外网访问 AList 服务。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器端进入 AList 服务配置页面 http://192.168.2.2:5244/&lt;/li&gt;
&lt;li&gt;输入 AList 账号密码，点击登录&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;存储&lt;/code&gt; -&amp;gt; &lt;code&gt;添加&lt;/code&gt; -&amp;gt; &lt;code&gt;阿里云盘Open&lt;/code&gt;， 我这里以阿里云盘为例，其他云盘配置类似&lt;/li&gt;
&lt;li&gt;填写以下表单项 &lt;code&gt;挂载路径&lt;/code&gt;、&lt;code&gt;云盘类型&lt;/code&gt;、&lt;code&gt;根文件夹ID&lt;/code&gt;、&lt;code&gt;刷新令牌&lt;/code&gt;、&lt;code&gt;Oauth令牌链接&lt;/code&gt;、&lt;code&gt;移除方式&lt;/code&gt;，更多配置可以参考 &lt;a href=&quot;https://alist.nn.ci/zh/guide/drivers/aliyundrive_open.html&quot;&gt;AList 阿里云盘Open 配置&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;挂载路径：当前存储的入口，可以自定义，例如 &lt;code&gt;/ali&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;云盘类型：备份盘或者资源盘，我这里选择的是&lt;code&gt;资源盘&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;根文件夹ID：默认为root，展示全部云盘内容，若只想展示某文件夹內内容，可以改为file_id,通过阿里云盘网页端进入文件夹，网址中最后一段即为file_id，例如 &lt;code&gt;https://www.alipan.com/drive/folder/5fe01e1830601baf774e4827a9fb8fb2b5bf7940&lt;/code&gt; 中的 &lt;code&gt;5fe01e1830601baf774e4827a9fb8fb2b5bf7940&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;刷新令牌：阿里云盘的刷新令牌，可以在 https://alist.nn.ci/tool/aliyundrive/request.html 获取&lt;/li&gt;
&lt;li&gt;Oauth令牌链接：https://api-cf.nn.ci/alist/ali_open/token 或者 https://api.xhofe.top/alist/ali_open/token&lt;/li&gt;
&lt;li&gt;移除方式：回收站和删除，按需选择&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;保存&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;在浏览器中访问 http://192.168.2.2:5244/ ，即可看到阿里云盘中的文件列表。&lt;/p&gt;
&lt;h1&gt;进阶使用&lt;/h1&gt;
&lt;p&gt;由于阿里云盘对自家的阿里云盘 TV 版不限速，因此可以通过获取阿里云盘 TV 版的 token 来实现不限速挂载的阿里云盘。&lt;/p&gt;
&lt;p&gt;:::note[碎碎念]
作为阿里云盘内测用户，以及公测后的付费用户，看待阿里云盘限速，推出第三方权益包等操作，还是有点不爽的。&lt;/p&gt;
&lt;p&gt;尤其是第三方权益包。我开通阿里云盘 SVIP 后，可以正常使用 WebDAV 服务，但是在之后推出第三方权益包后，WebDAV 服务就对于已购买的 SVIP 用户仍然进行限速了(虽然有 10G 的不限速流量包，但是在我看来有点割韭菜的感觉)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;11.png&quot; alt=&quot;profile&quot; /&gt;
:::&lt;/p&gt;
&lt;h2&gt;获取阿里云盘 TV 版的 token&lt;/h2&gt;
&lt;p&gt;通过 docker 项目 &lt;a href=&quot;https://hub.docker.com/r/vscodev/alipan-tv-auth&quot;&gt;alipan-tv-auth&lt;/a&gt; 来获取阿里云盘 TV 版的 Refresh Token。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 alipan-tv-auth,使用&lt;code&gt;docker-compose&lt;/code&gt; 安装&lt;pre&gt;&lt;code&gt; version: &apos;3.8&apos;
 services:
   alipan-tv-auth:
     image: &apos;vscodev/alipan-tv-auth:latest&apos;
     container_name: alipan-tv-auth
     ports:
         - &apos;5245:5245&apos;
     restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;在浏览器中访问 http://192.168.2.3:5245/ ，授权获取阿里云盘 TV 版的 Refresh Token&lt;/li&gt;
&lt;li&gt;将获取到的 Refresh Token 填入到 AList 服务的刷新令牌表单项&lt;/li&gt;
&lt;li&gt;修改 AList 服务的 Oauth令牌链接为 http://192.168.2.3:5245/api/oauth/alipan/token&lt;/li&gt;
&lt;li&gt;保存配置，即可实现不限速挂载的阿里云盘&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第三方软件挂载&lt;/h2&gt;
&lt;p&gt;可以使用诸如 Infuse、Forward、Vidhub 等第三方软件，通过 WebDAV 协议挂载部署好的 AList 服务。&lt;/p&gt;
&lt;p&gt;我这里以 Infuse 为例，演示如何挂载 AList 服务。 我的 AList 地址为 http://192.168.2.2:5244/&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开 Infuse，&lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;新增文件来源&lt;/code&gt; -&amp;gt; &lt;code&gt;添加&lt;/code&gt; -&amp;gt; &lt;code&gt;添加 WebDAV&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;填写以下表单项，通信协议、位址、用户名、密码、高级-路径、高级-端口
&lt;ul&gt;
&lt;li&gt;通信协议：WebDAV 或者 WebDAV(HTTPS)&lt;/li&gt;
&lt;li&gt;位址：192.168.2.2&lt;/li&gt;
&lt;li&gt;用户名：admin&lt;/li&gt;
&lt;li&gt;密码：***&lt;/li&gt;
&lt;li&gt;路径：/dav&lt;/li&gt;
&lt;li&gt;端口：5244&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;保存&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;底部菜单栏选择文件，即可看到刚刚添加的文件来源了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip
我在首页中没有看到刮削的资源，不太确定 Infuse 是否会对 WebDAV 来源的资源进行刮削。&lt;/p&gt;
&lt;p&gt;我经常使用的 Forward 则可以正常刮削。
:::&lt;/p&gt;
&lt;h2&gt;速度测试&lt;/h2&gt;
&lt;p&gt;在已存储的共享中点击右侧修改标识 icon，选择网速测试，可以看到平均速度在 200 Mbps 左右。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;22.png&quot; alt=&quot;infuse 测试速度&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Docker 中安装 Home Assistant 并使用米家集成</title><link>https://www.mihouo.com/posts/docker/installing-home-assistant-in-docker-and-integrating-xiaomi-mi-home/</link><guid isPermaLink="true">https://www.mihouo.com/posts/docker/installing-home-assistant-in-docker-and-integrating-xiaomi-mi-home/</guid><description>在 Docker 中安装 Home Assistant 并集成米家设备</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Home Assistant 安装&lt;/h2&gt;
&lt;p&gt;我使用的是 &lt;code&gt;docker-compose&lt;/code&gt; 安装的，&lt;code&gt;docker-compose.yml&lt;/code&gt; 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3.8&apos;
services:
  homeassistant:
    image: homeassistant/home-assistant
    container_name: home-assistant
    restart: unless-stopped
    volumes:
      - ./home-assistant:/config
    ports:
      - &apos;8123:8123&apos;
    environment:
      - TZ=Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在浏览器中访问 &lt;code&gt;http://localhost:8123&lt;/code&gt; 即可访问 Home Assistant。&lt;/p&gt;
&lt;p&gt;:::tip
如果你想要在公网中访问 Home Assistant，可以使用 Nginx Proxy Manager 进行反向代理，可以参考 &lt;a href=&quot;/posts/network/nginx-proxy-manager-a-comprehensive-user-guide/&quot;&gt;Nginx Proxy Manager 使用指南&lt;/a&gt;。此时你应该仍然无法从公网访问 Home Assistant，查看 home-assistant 容器的日志，可以看到如下日志&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2024-12-26 14:59:40.393 ERROR (MainThread) [homeassistant.components.http.forwarded] A request from a reverse proxy was received from 192.168.97.1, but your HTTP integration is not set-up for reverse proxies
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时需要修改 &lt;code&gt;home-assistant&lt;/code&gt; 配置文件，在 &lt;code&gt;configuration.yaml&lt;/code&gt; 中添加如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http:
  use_x_forwarded_for: true
  trusted_proxies:
    - 192.168.97.0/24
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;启动 Home Assistant&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入浏览器，访问 &lt;code&gt;http://localhost:8123&lt;/code&gt; 即可访问 Home Assistant。根据页面提示，配置 Home Assistant。&lt;/p&gt;
&lt;h2&gt;米家集成&lt;/h2&gt;
&lt;p&gt;得益于小米官方的开放，Home Assistant 可以非常方便的集成小米的智能家居设备。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;XiaoMi/ha_xiaomi_home&quot;}&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 home-assistant 容器终端中&lt;/li&gt;
&lt;li&gt;clone 米家集成仓库&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/XiaoMi/ha_xiaomi_home.git
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;进入 ha_xiaomi_home 目录&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cd ha_xiaomi_home
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行安装脚本&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;./install.sh /config
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;浏览器中访问 Home Assistant，并重启 Home Assistant&lt;/li&gt;
&lt;li&gt;重启后，在 Home Assistant 中添加米家集成
&lt;ol&gt;
&lt;li&gt;在 Home Assistant 中点击 &lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;设备与服务&lt;/code&gt; -&amp;gt; &lt;code&gt;添加集成&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;搜索集成&lt;/code&gt; 中搜索 &lt;code&gt;Xiaomi Home&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;Xiaomi Home&lt;/code&gt; 集成，并按照提示进行配置&lt;/li&gt;
&lt;li&gt;配置完成后，在 Home Assistant 中可以看到米家设备，并进行控制&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果你使用的是 safari 浏览器，可能会出现无法添加集成的情况，此时可以尝试使用其他浏览器。&lt;/li&gt;
&lt;li&gt;如果验证小米账号中出现错误，可以修改 &lt;code&gt;http://homeassistant.local:8123/api/webhook/xxx&lt;/code&gt; 中的 &lt;code&gt;homeassistant.local&lt;/code&gt; 为你局域网 IP 地址。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;在苹果家庭App中添加设备&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;在 Home Assistant 中点击 &lt;code&gt;设置&lt;/code&gt; -&amp;gt; &lt;code&gt;设备与服务&lt;/code&gt; -&amp;gt; &lt;code&gt;添加集成&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;搜索集成&lt;/code&gt; 中搜索 &lt;code&gt;Apple&lt;/code&gt; -&amp;gt; &lt;code&gt;HomeKit Bridge&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;HomeKit Bridge&lt;/code&gt; 集成，并按照提示进行配置&lt;/li&gt;
&lt;li&gt;配置完成后，在左下角&lt;code&gt;通知&lt;/code&gt;处可以看到设备码和设备二维码&lt;/li&gt;
&lt;li&gt;打开苹果家庭App，点击 &lt;code&gt;+&lt;/code&gt; 按钮，选择 &lt;code&gt;添加或扫描配件&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;添加完成后，在苹果家庭App中可以看到设备，并进行控制&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;Home Assistant 是一个非常强大的智能家居平台，可以非常方便的集成小米的智能家居设备。且支持非常多的智能家居设备，可以参考 &lt;a href=&quot;https://www.home-assistant.io/docs/&quot;&gt;Home Assistant 官方文档&lt;/a&gt; 了解更多。&lt;/p&gt;
</content:encoded></item><item><title>使用 Surge Ponte 访问家庭网络</title><link>https://www.mihouo.com/posts/network/using-surge-proxy-to-access-home-network-via-public-internet/</link><guid isPermaLink="true">https://www.mihouo.com/posts/network/using-surge-proxy-to-access-home-network-via-public-internet/</guid><description>使用 Surge Ponte 建立私有 Mesh 网络，安全访问家庭内网</description><pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 Surge Ponte&lt;/h2&gt;
&lt;p&gt;Surge Ponte 是 Surge 提供的私有 Mesh 网络功能，可在多台设备之间建立加密隧道。借助这一特性，即使身处外网，也能像在家一样访问局域网内的 NAS、路由器管理页面、Home Assistant 等服务。&lt;/p&gt;
&lt;p&gt;:::tip
与传统内网穿透方案相比，Surge Ponte 无需额外服务器，配置简单，且通信全程加密。
:::&lt;/p&gt;
&lt;h2&gt;Ponte 服务端类型&lt;/h2&gt;
&lt;p&gt;Surge Mac 和 Apple TV 可作为服务端或客户端，而 iOS 设备仅支持客户端模式。&lt;/p&gt;
&lt;p&gt;配置服务端时，有三种穿透方式可选：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;直接 NAT 穿透&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full Cone NAT 网络&lt;/td&gt;
&lt;td&gt;依赖路由器和运营商，通常无法人为改变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代理 NAT 穿透&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;任意网络&lt;/td&gt;
&lt;td&gt;需借助支持 UDP 转发的代理，会消耗代理流量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;静态端口转发&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;有公网 IP&lt;/td&gt;
&lt;td&gt;需在路由器配置端口映射，适合高级用户&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;使用场景&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;外出时访问家中 NAS、监控摄像头&lt;/li&gt;
&lt;li&gt;远程调试智能家居设备&lt;/li&gt;
&lt;li&gt;使用家庭网络的代理策略上网&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;前提条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;正版 Surge 授权（Mac/iOS/tvOS）&lt;/li&gt;
&lt;li&gt;服务端与客户端登录同一 iCloud 账户&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置步骤&lt;/h2&gt;
&lt;p&gt;以 Apple TV 作为 Ponte 服务端为例：&lt;/p&gt;
&lt;h3&gt;1. 选择穿透类型&lt;/h3&gt;
&lt;p&gt;在局域网内打开 Apple TV 上的 Surge，进入 Ponte 设置，根据网络情况选择合适的穿透类型。&lt;/p&gt;
&lt;p&gt;我使用的是&lt;strong&gt;静态端口转发&lt;/strong&gt;，需在路由器放行 UDP 6208 端口，具体操作参考 &lt;a href=&quot;/posts/network/port-forwarding/&quot;&gt;端口转发配置指南&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;2. 设置设备名称&lt;/h3&gt;
&lt;p&gt;为 Ponte 服务端设置一个易于识别的名称，例如 &lt;code&gt;HOMETV&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3. 添加代理规则&lt;/h3&gt;
&lt;p&gt;在客户端 Surge 配置中添加规则，将家庭网段的流量指向 Ponte 设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Rule]
# 将 192.168.2.0/24 网段的请求通过 Ponte 设备转发
IP-CIDR,192.168.2.0/24,DEVICE:HOMETV
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
请将 &lt;code&gt;192.168.2.0/24&lt;/code&gt; 替换为你的实际家庭网段，&lt;code&gt;HOMETV&lt;/code&gt; 替换为你设置的设备名。
:::&lt;/p&gt;
&lt;h3&gt;4. 验证连接&lt;/h3&gt;
&lt;p&gt;断开 Wi-Fi，使用蜂窝网络测试访问家庭内网资源（如路由器管理页面 &lt;code&gt;192.168.2.1&lt;/code&gt;），确认 Ponte 连接正常。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.jpeg&quot; alt=&quot;Surge Ponte 连接成功&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Nginx Proxy Manager 使用指南</title><link>https://www.mihouo.com/posts/network/nginx-proxy-manager-a-comprehensive-user-guide/</link><guid isPermaLink="true">https://www.mihouo.com/posts/network/nginx-proxy-manager-a-comprehensive-user-guide/</guid><description>Nginx Proxy Manager 使用指南，Web 界面管理反向代理</description><pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 &lt;a href=&quot;https://github.com/NginxProxyManager/nginx-proxy-manager&quot;&gt;Nginx Proxy Manager&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Nginx Proxy Manager 是一个开源的 Nginx 管理工具，旨在简化 Nginx 的配置和管理。它提供了一个 Web 界面，允许用户轻松地管理多个 Nginx 虚拟主机和反向代理。&lt;/p&gt;
&lt;p&gt;:::tip
Nginx Proxy Manager 通常简称 npm，这与前端的包管理工具 Node Package Manager 缩写同名，因此在使用时需要注意区分。
:::&lt;/p&gt;
&lt;h2&gt;为什么需要 Nginx Proxy Manager&lt;/h2&gt;
&lt;p&gt;如果需要将内网服务暴露到外网，首先需要使用端口转发，将端口暴漏到公网中，如果我们有多个服务，就需要配置多个端口转发，这样不仅非常麻烦，而且还不方便管理，且多个端口暴漏还较为危险，因此我们可以使用 Nginx Proxy Manager 来管理多个反向代理。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;我使用的是 &lt;code&gt;docker-compose&lt;/code&gt; 安装的，&lt;code&gt;docker-compose.yml&lt;/code&gt; 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &apos;3.8&apos;
services:
nginx-proxy-manager:
    image: &apos;jc21/nginx-proxy-manager:latest&apos;
    restart: unless-stopped
    ports:
      - &apos;8880:80&apos;
      - &apos;81:81&apos;
      - &apos;8443:443&apos;
    volumes:
      - ./nginx-proxy-manager/data:/data
      - ./nginx-proxy-manager/letsencrypt:/etc/letsencrypt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;端口 81 是 NPM 的 Web 端口，80 是 HTTP 端口，443 是 HTTPS 端口。&lt;/li&gt;
&lt;li&gt;由于家庭网络(一般 80、443 端口被运营商屏蔽)中部署的 npm 服务，因此需要将端口映射为 80 -&amp;gt; 8880、443 -&amp;gt; 8443。&lt;/li&gt;
&lt;li&gt;数据和证书存储在 &lt;code&gt;./nginx-proxy-manager/data&lt;/code&gt; 和 &lt;code&gt;./nginx-proxy-manager/letsencrypt&lt;/code&gt; 目录中。&lt;/li&gt;
&lt;li&gt;请根据需要调整端口和路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;浏览器进入 &lt;code&gt;http://127.0.0.1:81&lt;/code&gt; ，默认账号密码是 &lt;code&gt;admin@example.com&lt;/code&gt; 和 &lt;code&gt;changeme&lt;/code&gt; ，登录后会提示你修改密码。&lt;/p&gt;
&lt;h3&gt;证书申请&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;首先需要添加域名并申请证书，点击 &lt;code&gt;SSL Certificates&lt;/code&gt; -&amp;gt; &lt;code&gt;Add SSL Certificate&lt;/code&gt; -&amp;gt; &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;Domain Names&lt;/code&gt; 处，添加你的域名，可以直接申请泛域名证书(例如 &lt;code&gt;*.mihouo.com&lt;/code&gt;)，&lt;code&gt;Email Address for Let&apos;s Encrypt&lt;/code&gt; 填写你的邮箱。&lt;/li&gt;
&lt;li&gt;开启 &lt;code&gt;Use a DNS Challenge&lt;/code&gt;，&lt;code&gt;DNS Provider&lt;/code&gt;处选择你的域名服务商，在 &lt;code&gt;Credentials File Content&lt;/code&gt; 处，将 &lt;code&gt;dns_cloudflare_api_token&lt;/code&gt; 修改为你的 DNS 令牌(这篇文章有申请指南&lt;a href=&quot;/posts/network/ddns-go/#%E9%85%8D%E7%BD%AE-ddns-go&quot;&gt;使用 ddns-go 进行动态域名解析&lt;/a&gt;)。&lt;/li&gt;
&lt;li&gt;同意 &lt;code&gt;Let&apos;s Encrypt&lt;/code&gt; 的条款，点击 &lt;code&gt;Save&lt;/code&gt; 按钮，等待完成证书申请。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;添加反向代理&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;点击 &lt;code&gt;Hosts&lt;/code&gt; -&amp;gt; &lt;code&gt;Proxy Hosts&lt;/code&gt; -&amp;gt; &lt;code&gt;Add Proxy Host&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;Domain Name&lt;/code&gt; 处，填写你的域名(需要携带二级域名)，我这里以 &lt;code&gt;alist&lt;/code&gt; 服务为例，填写 &lt;code&gt;alist.mihouo.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;Forward Hostname&lt;/code&gt; 处，填写你的内网服务地址，我这里以 &lt;code&gt;alist&lt;/code&gt; 服务为例，填写 &lt;code&gt;192.168.2.2&lt;/code&gt;，&lt;code&gt;Forward Port&lt;/code&gt; 填写你的服务端口，我这里是 &lt;code&gt;5244&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;选项 &lt;code&gt;Cache Assets&lt;/code&gt;、&lt;code&gt;Block Common Exploits&lt;/code&gt;、&lt;code&gt;Websockets Support&lt;/code&gt; 可以按需开启。&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;SSL&lt;/code&gt; 处，选择你刚刚申请的证书。开启 &lt;code&gt;Force SSL&lt;/code&gt; 选项，其余选择可以按需开启。点击 &lt;code&gt;Save&lt;/code&gt; 按钮，完成反向代理添加。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;浏览器打开 https://alist.mihouo.com:8443 ，如果配置正确，则可以看到 &lt;code&gt;alist&lt;/code&gt; 的页面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.png&quot; alt=&quot;alist&quot; /&gt;&lt;/p&gt;
&lt;p&gt;:::tip
需要放行对应端口，如果配置正确，但是无法访问，请检查防火墙和安全组配置。如果你也是家庭宽带，请参考 &lt;a href=&quot;/posts/network/port-forwarding&quot;&gt;端口转发&lt;/a&gt; 开启内网 &lt;code&gt;npm&lt;/code&gt; 映射的 &lt;code&gt;443&lt;/code&gt; 端口转发到公网任何你想作为后缀访问的端口。
:::&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Nginx Proxy Manager&lt;/code&gt; 是一个非常强大的反向代理工具，可以让你通过域名访问到内网中的服务，也可以方便的申请证书并使用。且只需要放行一个对外端口即可，非常方便。&lt;/p&gt;
</content:encoded></item><item><title>使用 ddns-go 进行动态域名解析</title><link>https://www.mihouo.com/posts/network/ddns-go/</link><guid isPermaLink="true">https://www.mihouo.com/posts/network/ddns-go/</guid><description>使用 ddns-go 实现动态公网 IP 域名解析</description><pubDate>Wed, 25 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 ddns-go&lt;/h2&gt;
&lt;p&gt;ddns-go 是一款开源的动态域名解析（DDNS）工具，旨在帮助用户将公网 IP 动态更新到 DNS 服务商，以便通过域名访问家庭网络、服务器或其他设备，即使公网 IP 是动态分配的。&lt;/p&gt;
&lt;h2&gt;为什么需要 ddns-go&lt;/h2&gt;
&lt;p&gt;公网 IP 地址大多是动态的，所以你需要定期更新公网 IP 地址，而使用 ddns-go 可以自动更新公网 IP 地址，从而实现公网 IP 地址的动态更新。&lt;/p&gt;
&lt;h2&gt;如何使用 ddns-go&lt;/h2&gt;
&lt;p&gt;我这里使用 docker 来安装 ddns-go，如果你没有使用 docker ，可以参考 &lt;a href=&quot;https://docs.docker.com/get-docker/&quot;&gt;docker 安装教程&lt;/a&gt; 来安装 docker。&lt;/p&gt;
&lt;p&gt;docker-compose.yml 文件如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  ddns-go:
    image: jeessy/ddns-go
    container_name: ddns-go
    restart: always
    ports:
      - &apos;9876:9876&apos;
    volumes:
      - ./ddns-go:/root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装好后，启动容器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 ddns-go&lt;/h2&gt;
&lt;p&gt;浏览器访问 &lt;code&gt;http://127.0.0.1:9876&lt;/code&gt;，设置初始账号密码，然后点击 &lt;code&gt;保存&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;p&gt;然后选择你的域名解析的服务商，我这里使用的是 &lt;code&gt;Cloudflare&lt;/code&gt;，输入你的 DNS Token。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登录 &lt;a href=&quot;https://dash.cloudflare.com/login&quot;&gt;Cloudflare&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;点击 左侧菜单 &lt;code&gt;管理账户&lt;/code&gt; -&amp;gt; &lt;code&gt;账户API令牌&lt;/code&gt; -&amp;gt; &lt;code&gt;创建令牌&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建 &lt;code&gt;编辑区域 DNS&lt;/code&gt; 令牌， 在区域资源处选择你的域名，在权限处选择 &lt;code&gt;编辑&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;创建好后，复制 &lt;code&gt;令牌&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将令牌粘贴到 ddns-go 的 &lt;code&gt;DNS Token&lt;/code&gt; 处&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 &lt;code&gt;Domains&lt;/code&gt; 处，添加你的域名，然后点击 &lt;code&gt;保存&lt;/code&gt; 按钮。&lt;/p&gt;
&lt;p&gt;如果需要可以配置 WebHook，进行变更通知。&lt;/p&gt;
&lt;p&gt;最后点击左上角 &lt;code&gt;保存&lt;/code&gt; 按钮，完成配置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;1.png&quot; alt=&quot;ddns-go&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;配置完成后，通过 http://&amp;lt;域名&amp;gt;:端口，即可访问到你的内网服务。&lt;/p&gt;
&lt;p&gt;例如 http://mihouo.com:5244 访问到我的 alist 服务。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;ddns-go 可以让你通过域名访问到内网服务，而无需担心公网 IP 地址的变化。&lt;/p&gt;
</content:encoded></item><item><title>端口转发</title><link>https://www.mihouo.com/posts/network/port-forwarding/</link><guid isPermaLink="true">https://www.mihouo.com/posts/network/port-forwarding/</guid><description>端口转发原理与配置，将外部访问引导到内网设备</description><pubDate>Tue, 24 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是端口转发&lt;/h2&gt;
&lt;p&gt;端口转发（Port Forwarding） 是一种将特定网络端口上的流量从一个设备或网络转发到另一个设备或网络的技术。通常，端口转发在路由器或防火墙上配置，用于将外部访问请求引导到内部网络的特定设备或服务。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;端口转发的工作原理如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;外部请求： 用户从互联网向路由器的公共 IP 地址发送请求，并指定特定端口（如 http://&amp;lt;公网IP&amp;gt;:8080）。&lt;/li&gt;
&lt;li&gt;路由器处理： 路由器收到该请求后，根据配置的端口转发规则，将该请求转发到内网中指定的 IP 地址和端口（如 192.168.2.2:5244）。&lt;/li&gt;
&lt;li&gt;服务响应： 内网设备处理请求并返回结果，通过路由器将响应数据发送回外部用户。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;前提条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;内网有部署服务&lt;/li&gt;
&lt;li&gt;拥有公网 IP 地址&lt;/li&gt;
&lt;li&gt;有光猫的超级密码，且光猫支持端口转发，或者光猫更改为桥接模式，使用路由器拨号上网，并配置端口转发&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;光猫设置&lt;/h2&gt;
&lt;p&gt;我这里使用的是光猫进行拨号上网，所以需要进入光猫后台进行设置。一般情况下光猫背面标签有配置地址，以及用户名密码。但是这里通常是普通用户，没有权限进行设置。需要使用超级用户进行设置。&lt;/p&gt;
&lt;p&gt;这里我是在闲鱼上购买服务，找人帮我获取到了光猫的超级密码，并将超级密码进行了固定。&lt;/p&gt;
&lt;p&gt;以我的光猫型号中兴 G7615 为例&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登录光猫后台 http://192.168.2.1/cu.html&lt;/li&gt;
&lt;li&gt;选择 “高级配置” -&amp;gt; “NAT 设置” -&amp;gt; “虚拟主机配置”&lt;/li&gt;
&lt;li&gt;填写 &lt;code&gt;名称&lt;/code&gt;、&lt;code&gt;协议&lt;/code&gt;、&lt;code&gt;广域网起始端口&lt;/code&gt;、&lt;code&gt;广域网结束端口&lt;/code&gt;、&lt;code&gt;虚拟主机IP地址&lt;/code&gt;、&lt;code&gt;虚拟主机端口	&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;名称：自定义，例如 AppleTV-alist&lt;/li&gt;
&lt;li&gt;协议：TCP 或 UDP,根据你部署的服务类型选择&lt;/li&gt;
&lt;li&gt;广域网起始端口：5244 公网访问的端口&lt;/li&gt;
&lt;li&gt;广域网结束端口：5244 公网访问的端口&lt;/li&gt;
&lt;li&gt;虚拟主机IP地址：192.168.2.2 内网服务部署的 IP 地址&lt;/li&gt;
&lt;li&gt;虚拟主机端口：5244 内网服务部署的端口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;点击 添加&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;添加示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上添加完成后，如果你拥有公网 IP 地址，则可以通过 &lt;code&gt;http://&amp;lt;公网IP&amp;gt;:5244&lt;/code&gt; 访问到内网中的服务。&lt;/p&gt;
&lt;p&gt;如下图，我这里部署的是一个 alist 服务&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;2.png&quot; alt=&quot;alist&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;端口转发的配置，可以让你通过公网 IP 地址访问到内网中的服务。但是它也有一些缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大多数家庭宽带没有公网 IP 地址，需要申请，且不一定能申请到&lt;/li&gt;
&lt;li&gt;公网 IP 地址大多是动态的，所以你需要定期更新公网 IP 地址。 (可以使用 &lt;a href=&quot;https://github.com/jeessy2/ddns-go&quot;&gt;ddns-go&lt;/a&gt; 来解决)
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/network/ddns-go/&quot;&gt;ddns-go配置教程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>我的 Apple TV 观影方案</title><link>https://www.mihouo.com/posts/apple-tv/my-apple-tv-movie-watching-setup/</link><guid isPermaLink="true">https://www.mihouo.com/posts/apple-tv/my-apple-tv-movie-watching-setup/</guid><description>Apple TV 观影解决方案，应用选择与体验优化</description><pubDate>Mon, 23 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;流媒体平台&lt;/h2&gt;
&lt;h3&gt;国内流媒体平台&lt;/h3&gt;
&lt;p&gt;因为Apple TV 并没有在大陆地区发售，因此国内流媒体平台也没有在 TV 端的 Apple Store 上架。因此只能选择爱优腾等国内流媒体的海外版观看，但是海外版资源较少，且会员订阅与国内不互通，且价格昂贵，非常不推荐购买使用。但是有 Apple 开发者开发的第三方流媒体平台 比如 Bilibili 可以完美解决这个问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BiliBili(需要使用第三方 App 如 Cheers、MiaoProject、弹幕播放器等)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;国外流媒体平台&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.netflix.com/&quot;&gt;Netflix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.disneyplus.com/&quot;&gt;Disney+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.apple.com/apple-tv-plus/&quot;&gt;Apple TV+&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hulu.com/&quot;&gt;Hulu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hbomax.com/&quot;&gt;HBO Max&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Emby&lt;/h2&gt;
&lt;h3&gt;介绍&lt;/h3&gt;
&lt;p&gt;Emby 是一款跨平台的多媒体服务器软件，旨在帮助用户管理和流式传输其个人媒体内容（如视频、音乐、照片等）。它能够通过 Web 浏览器、移动设备、智能电视等多种设备访问和播放本地媒体内容，为用户提供家庭媒体中心的解决方案。&lt;/p&gt;
&lt;h3&gt;服类型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;公益服：资源和稳定性看服主，我目前使用的公益服还可以，可直连，资源丰富度一般&lt;/li&gt;
&lt;li&gt;公费服：价格便宜(5左右/月)，国内看地区可以直连，非直连时需要使用代理服务&lt;/li&gt;
&lt;li&gt;机场服：购买机场附赠的 Emby 服，稳定性较好，资源丰富&lt;/li&gt;
&lt;li&gt;付费服：价格较贵(20左右/月)，可以国内直连，资源丰富，稳定性较好&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;优缺点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;价格便宜，免费版或者价格极低的公费服&lt;/li&gt;
&lt;li&gt;画质清晰，码率相较于国内流媒体平台较高&lt;/li&gt;
&lt;li&gt;支持任何操作系统，包括 Windows、Linux、macOS、Android、iOS 等&lt;/li&gt;
&lt;li&gt;内容丰富，包括电影、电视剧、动漫、综艺、纪录片等，且无删减&lt;/li&gt;
&lt;li&gt;无需自主管理片源，热播剧集、电影、综艺等更新及时&lt;/li&gt;
&lt;li&gt;资源丰富，包括国内、国外、港台、日韩、欧美等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通常来说公益服、公费服、机场服的稳定性较差，容易出现无法访问的情况&lt;/li&gt;
&lt;li&gt;服务器通常位于国外，初始加载速度较慢，不过有国内付费服，价格较贵(一般 20/月)&lt;/li&gt;
&lt;li&gt;每个服资源不同，需要自行甄别&lt;/li&gt;
&lt;li&gt;没有弹幕(因人而异，有人喜欢，有人不喜欢)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;我目前使用的 Emby 服&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://mooguu.xyz/&quot;&gt;蘑菇公费服&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ww1.12345.al/register?code=IDGuayX1&quot;&gt;ACA 机场服&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;@AlphaTVOverseaBoss_bot&quot;&gt;AlphaTV 公益服&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;@Nebula_Account_bot&quot;&gt;Nebula 公益服&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;网盘挂载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;阿里云盘: 使用infuse、forward、vidhub 等第三方 App 可以挂载阿里云盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
如果你使用的客户端不支持挂载你的网盘，如夸克、pikpak等，可以使用 alist 挂载网盘。
它可以将多个网盘挂载到一起，并提供 webdav 服务，支持 infuse、forward、vidhub 等第三方 App 挂载。
:::&lt;/p&gt;
&lt;h2&gt;观影App&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/cn/app/infuse-%E6%99%BA%E8%83%BD%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8/id1136220934&quot;&gt;Infuse&lt;/a&gt; 完成度较高，也是现在唯一一个支持真杜比视界的 App&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/cn/app/forward-%E6%96%B0%E8%A7%86%E7%95%8C/id6503940939&quot;&gt;Forward&lt;/a&gt; 完成度较低，但是UI非常好看&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/cn/app/vidhub-%E9%AB%98%E6%B8%85%E5%BD%B1%E7%89%87%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8-%E5%BF%AB%E9%80%9F%E6%92%AD%E6%94%BE%E4%BA%91%E7%9B%98%E7%BD%91%E7%9B%98/id1659622164&quot;&gt;VidHub&lt;/a&gt; 完成度很高，国内媒体平台知名度较高，但是由于某些原因被很多 Emby 服禁止，因此并不推荐购买&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/cn/app/senplayer-%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E5%99%A8-8%E5%80%8D%E9%80%9F/id6443975850&quot;&gt;Senplay&lt;/a&gt; 完成度较高&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/cn/app/conflux-video-player/id6450330892&quot;&gt;Conflux&lt;/a&gt; cheers 作者新开发的软件&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://t.me/ReflixApp&quot;&gt;Reflix&lt;/a&gt; 目前仅有TestFlight 版本&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Apple TV 的观影体验还是非常不错的，尤其是使用 Emby 服，可以非常方便的观看各种电影、电视剧、动漫、综艺、纪录片等。&lt;/p&gt;
&lt;p&gt;你所要支付的成本只有第三方 App 的买断费用，以及 Emby 服的订阅费用。&lt;/p&gt;
</content:encoded></item><item><title>ClawCloud 新加坡 机器留档测试</title><link>https://www.mihouo.com/posts/server/review-of-clawclouds-singapore-hybrid-machine/</link><guid isPermaLink="true">https://www.mihouo.com/posts/server/review-of-clawclouds-singapore-hybrid-machine/</guid><description>留档-ClawCloud 新加坡机器留档测试</description><pubDate>Mon, 23 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;机器详情&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CPU 1 vCPU&lt;/li&gt;
&lt;li&gt;内存 1 GiB&lt;/li&gt;
&lt;li&gt;存储 20 GiB&lt;/li&gt;
&lt;li&gt;带宽 1000 Mbps&lt;/li&gt;
&lt;li&gt;流量 500 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/spiritLHLS/ecs&quot;&gt;一键脚本&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;spiritLHLS/ecs&quot;}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://gitlab.com/spiritysdx/za/-/raw/main/ecs.sh -o ecs.sh &amp;amp;&amp;amp; chmod +x ecs.sh &amp;amp;&amp;amp; bash ecs.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;详情&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;--------------------- A Bench Script By spiritlhl ----------------------
                   测评频道: https://t.me/vps_reviews                    
VPS融合怪版本：2024.11.08
Shell项目地址：https://github.com/spiritLHLS/ecs
Go项目地址：https://github.com/oneclickvirt/ecs
---------------------基础信息查询--感谢所有开源项目---------------------
 CPU 型号          : Intel(R) Xeon(R) Platinum
 CPU 核心数        : 1
 CPU 频率          : 2500.002 MHz
 CPU 缓存          : L1: 32.00 KB / L2: 1.00 MB / L3: 33.00 MB
 AES-NI指令集      : ✔ Enabled
 VM-x/AMD-V支持    : ❌ Disabled
 内存              : 368.71 MiB / 907.96 MiB
 Swap              : [ no swap partition or swap file detected ]
 硬盘空间          : 3.22 GiB / 19.20 GiB
 启动盘路径        : /dev/vda1
 系统在线时间      : 23 days, 23 hour 37 min
 负载              : 0.53, 0.14, 0.05
 系统              : Ubuntu 22.04.5 LTS (x86_64)
 架构              : x86_64 (64 Bit)
 内核              : 5.15.0-122-generic
 TCP加速方式       : cubic
 虚拟化架构        : KVM
 NAT类型           : Full Cone
 IPV4 ASN          : AS45102 Alibaba (US) Technology Co., Ltd.
 IPV4 位置         : Singapore / Singapore / SG
----------------------CPU测试--通过sysbench测试-------------------------
 -&amp;gt; CPU 测试中 (Fast Mode, 1-Pass @ 5sec)
 1 线程测试(单核)得分:          1117 Scores
---------------------内存测试--感谢lemonbench开源-----------------------
 -&amp;gt; 内存测试 Test (Fast Mode, 1-Pass @ 5sec)
 单线程读测试:          3994.02 MB/s
 单线程写测试:          4090.81 MB/s
------------------磁盘dd读写测试--感谢lemonbench开源--------------------
 -&amp;gt; 磁盘IO测试中 (4K Block/1M Block, Direct Mode)
 测试操作               写速度                                  读速度
 100MB-4K Block         14.0 MB/s (3427 IOPS, 7.47s)            13.1 MB/s (3190 IOPS, 8.02s)
 1GB-1M Block           159 MB/s (151 IOPS, 6.61s)              147 MB/s (139 IOPS, 7.15s)
-------------IP质量检测--基于oneclickvirt/securityCheck使用-------------
数据仅作参考，不代表100%准确，如果和实际情况不一致请手动查询多个数据库比对
以下为各数据库编号，输出结果后将自带数据库来源对应的编号
ipinfo数据库  [0] | scamalytics数据库 [1] | virustotal数据库   [2] | abuseipdb数据库   [3] | ip2location数据库    [4]
ip-api数据库  [5] | ipwhois数据库     [6] | ipregistry数据库   [7] | ipdata数据库      [8] | db-ip数据库          [9]
ipapiis数据库 [A] | ipapicom数据库    [B] | bigdatacloud数据库 [C] | cheervision数据库 [D] | ipqualityscore数据库 [E]
IPV4:
安全得分:
声誉(越高越好): 0 [2] 
信任得分(越高越好): 0 [8] 
VPN得分(越低越好): 100 [8] 
代理得分(越低越好): 100 [8] 
社区投票-无害: 0 [2] 
社区投票-恶意: 0 [2]
威胁得分(越低越好): 100 [8] 
欺诈得分(越低越好): 4 [1] 
滥用得分(越低越好): 0 [3] 
ASN滥用得分(越低越好): 0.0012 (Low) [A] 
公司滥用得分(越低越好): 0.0054 (Low) [A] 
威胁级别: low [9 B] 
黑名单记录统计:(有多少黑名单网站有记录):
无害记录数: 0 [2]  恶意记录数: 0 [2]  可疑记录数: 0 [2]  无记录数: 94 [2]  
安全信息:
使用类型: DataCenter/WebHosting/Transit [3] hosting [0 7 9 A] hosting - high probability [C] business [8]
公司类型: hosting [0 7 A] 
是否云提供商: Yes [7] No [D]
是否数据中心: Yes [0 1 5 6 A C] No [8]
是否移动设备: No [5 A C] 
是否代理: No [0 1 4 5 6 7 8 9 A B C D] 
是否VPN: Yes [A] No [0 1 6 7 C D]
是否Tor: No [0 1 3 6 7 8 A B C D] 
是否Tor出口: No [1 7 D] 
是否网络爬虫: No [9 A B] 
是否匿名: No [1 6 7 8 D] 
是否攻击者: No [7 8 D] 
是否滥用者: No [7 8 A C D] 
是否威胁: No [7 8 C D] 
是否中继: No [0 7 8 C D] 
是否Bogon: No [7 8 A C D] 
DNS-黑名单: 314(Total_Check) 0(Clean) 7(Blacklisted) 17(Other) 
Google搜索可行性：YES
-------------邮件端口检测--基于oneclickvirt/portchecker开源-------------
Platform  SMTP  SMTPS POP3  POP3S IMAP  IMAPS
LocalPort ✔     ✔     ✔     ✔     ✔     ✔    
QQ        ✘     ✔     ✔     ✘     ✔     ✘    
163       ✘     ✔     ✔     ✘     ✔     ✘    
Sohu      ✘     ✔     ✔     ✘     ✔     ✘    
Yandex    ✘     ✔     ✔     ✘     ✔     ✘    
Gmail     ✘     ✔     ✘     ✘     ✘     ✘    
Outlook   ✘     ✘     ✔     ✘     ✔     ✘    
Office365 ✘     ✘     ✔     ✘     ✔     ✘    
Yahoo     ✘     ✔     ✘     ✘     ✘     ✘    
MailCOM   ✘     ✔     ✔     ✘     ✔     ✘    
MailRU    ✘     ✔     ✘     ✘     ✔     ✘    
AOL       ✘     ✔     ✘     ✘     ✘     ✘    
GMX       ✘     ✘     ✔     ✘     ✔     ✘    
Sina      ✘     ✘     ✔     ✘     ✔     ✘    
-----------------------全国延迟检测--本脚本原创-------------------------
 联通天津          73  | 联通福州          57  | 联通上海          66  |
 联通南充          76  | 联通太原          81  | 联通大连          90  |
 联通WuXi          64  | 电信长沙          185 | 电信扬州          203 |
 电信苏州          204 | 电信杭州          190 | 电信武汉          192 |
 电信宁波          198 | 电信兰州          203 | 电信南京          210 |
 电信Zhenjiang     200 | 移动福州          72  | 移动杭州          212 |
 移动成都          236 | 移动Beijing       214 | 移动杭州          218 |
--------------------自动更新测速节点列表--本脚本原创--------------------
位置             上传速度        下载速度        延迟
Speedtest.net    210.69Mbps      1089.98Mbps     2.61ms
新加坡           218.12Mbps      1106.24Mbps     2.40ms
联通WuXi         16.15Mbps       184.33Mbps      64.94ms
联通上海5G       11.18Mbps       135.05Mbps      69.97ms
电信浙江         5.68Mbps        6.36Mbps        195.56ms
电信浙江         5.91Mbps        66.74Mbps       191.94ms
移动Fujian       5.60Mbps        5.09Mbps        223.96ms
移动Beijing      0.71Mbps        41.31Mbps       208.81ms
------------------------------------------------------------------------
 总共花费      : 5 分 22 秒
 时间          : Mon Dec 23 06:46:46 UTC 2024
------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>在前端项目中使用 Mock 服务</title><link>https://www.mihouo.com/posts/front/using-mock-services-in-front-end-projects/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/using-mock-services-in-front-end-projects/</guid><description>使用 Mock 服务模拟后端接口，实现前后端分离开发</description><pubDate>Tue, 24 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;在前后端分离开发中，Mock服务是一种常用的技术手段，用于模拟后端接口，以便前端开发人员可以独立进行开发和测试。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;什么是 Mock 服务&lt;/h2&gt;
&lt;p&gt;Mock 服务是一种技术手段，用于模拟后端接口，以便前端开发人员可以独立进行开发和测试。&lt;/p&gt;
&lt;h2&gt;为什么需要 Mock 服务&lt;/h2&gt;
&lt;p&gt;提高开发效率，通常前端开发人员需要等待后端接口开发完毕才能进行开发，而使用 Mock 服务可以让我们在开发过程中使用模拟数据进行开发和测试。&lt;/p&gt;
&lt;h2&gt;如何在项目中使用 Mock 服务&lt;/h2&gt;
&lt;p&gt;通常情况下，我们使用框架进行开发，比如 &lt;a href=&quot;https://v3.ice.work/&quot;&gt;ice.js&lt;/a&gt;、&lt;a href=&quot;https://umijs.org/&quot;&gt;umijs&lt;/a&gt;、&lt;a href=&quot;https://nextjs.org/&quot;&gt;next.js&lt;/a&gt; 等，这些框架都提供了 Mock 服务的支持。&lt;/p&gt;
&lt;p&gt;但是也有一些项目没有使用框架，比如纯前端项目，这个时候我们就需要自己搭建 Mock 服务。这时我们可以使用 &lt;a href=&quot;https://apifox.cn/&quot;&gt;Apifox&lt;/a&gt; 工具来作为 Mock 服务。&lt;/p&gt;
&lt;h3&gt;使用 Apifox 作为 Mock 服务&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 Apifox 工具&lt;/li&gt;
&lt;li&gt;通常由后端提供接口文档，并启用本地 Mock 服务，拿到 Mock 服务地址（例如：&lt;code&gt;http://127.0.0.1:4523/m1/4945763-4603443-default/**/*&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;修改前端项目中接口请求地址为 Mock 服务地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;3.1 通常我们会在项目中配置一个环境变量，用于区分开发环境和生产环境，env.development 添加以下配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# apifox mock base url
REACT_APP_APIFOX_BASE_API=http://127.0.0.1:4523/m1/4945763-4603443-default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.2 在开发环境中，我们将接口请求地址配置为 Mock 服务地址, 请求拦截器中添加如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;service.interceptors.request.use(
  (config: InternalAxiosRequestConfig) =&amp;gt; {
    const { isApifoxMock = false } = config.customConfig || {};
    if (isApifoxMock) {
      config.baseURL = process.env.REACT_APP_APIFOX_BASE_API;
    }
    return config;
  },
  error =&amp;gt; {
    return Promise.reject(error);
  },
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3.3 配置接口自定义参数(&lt;code&gt;isApifxiMock === true&lt;/code&gt;)
&lt;img src=&quot;./1.png&quot; alt=&quot;example.png&quot; /&gt;
如果你是用的是 &lt;code&gt;typescript&lt;/code&gt;，需要在 &lt;code&gt;src&lt;/code&gt; 目录下的 &lt;code&gt;types.d.ts&lt;/code&gt; 文件中添加如下代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare module &apos;axios&apos; {
  export interface AxiosRequestConfig {
    customConfig?: {
      isApifoxMock?: boolean; // 是否是 apifox mock
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动项目，访问接口，查看 Mock 数据
&lt;img src=&quot;./2.png&quot; alt=&quot;example.png&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后端提供接口后，更改接口自定义参数 &lt;code&gt;isApifxiMock === false&lt;/code&gt;，即可访问真实接口数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Mock 服务在前端开发中扮演着非常重要的角色，它可以帮助我们快速实现前后端分离开发，提升开发效率和测试效果。&lt;/p&gt;
</content:encoded></item><item><title>Fusion 组件库 DatePicker2 组件选择异常行为</title><link>https://www.mihouo.com/posts/front/abnormal-behavior-in-fusions-datepicker2-component/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/abnormal-behavior-in-fusions-datepicker2-component/</guid><description>本文分析了Fusion组件库中DatePicker2组件出现的选择异常行为。通过具体问题的复现与调试，探讨了问题的成因，并提供了相应的解决方案，帮助开发者在使用该组件时避免类似问题。</description><pubDate>Thu, 05 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;今天下午，同事又遇到了一个奇怪的 Bug，这次是在 &lt;code&gt;Fusion&lt;/code&gt; 组件库中的 &lt;code&gt;DatePicker2&lt;/code&gt; 组件上。简单描述如下：有两个日期选择框，分别是开始日期和结束日期，先点击结束日期选择框，随便选择一个日期，提示选择失败，并提示“请先选择开始日期”，然后这时结束日期并不会成功选择，因为在 &lt;code&gt;onChange&lt;/code&gt; 事件中会判断开始日期是否已经选择，如果没有选择则不会更新结束日期。但是这个时候，开在选择开始日期，选择成功后，再次选择结束日期，重点是并不需要选择具体日期，而是将弹框打开，再关闭，这时结束日期就会成功选择了。而选择的日期是第一次选择失败的那个日期。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/videos/datepick-bug-example.mov&quot; type=&quot;video/mp4&quot;&amp;gt;
Your browser does not support the video tag.
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h2&gt;复现问题&lt;/h2&gt;
&lt;p&gt;打开 &lt;code&gt;Fusion&lt;/code&gt; 官网，找到 &lt;code&gt;DatePicker2&lt;/code&gt; 组件，然后在&lt;a href=&quot;https://fusion.design/pc/component/date-picker-2?themeid=2#size-container&quot;&gt;尺寸大小示例&lt;/a&gt;中修改代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Demo() {
	const [first, setFirst] = React.useState(&apos;&apos;);
	const [second, setSecond] = React.useState(&apos;&apos;);
	return (
		&amp;lt;div&amp;gt;
			&amp;lt;DatePicker2 value={first} onChange={(_, str) =&amp;gt; {
				setFirst(str);
			}} /&amp;gt;
			&amp;lt;DatePicker2 value={second} onChange={(_, str) =&amp;gt; {
				if (!first) {
					return;
				}
				setSecond(str);
			}} /&amp;gt;

		&amp;lt;/div&amp;gt;
	);
}
ReactDOM.render(&amp;lt;Demo /&amp;gt;, mountNode);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先选择第二个日期选择框，然后选择一个日期，会提示选择失败，然后选择第一个日期选择框，选择一个日期，再次选择第二个日期选择框，不选择具体日期，只是打开弹框，然后关闭，这时选择成功。&lt;/p&gt;
&lt;p&gt;这样就可以复现这个问题了。&lt;/p&gt;
&lt;h2&gt;分析问题&lt;/h2&gt;
&lt;p&gt;这个问题看起来很奇怪，第一次选择日期并不会成功，但是第二次选择日期就会成功，而且选择的日期是第一次选择失败的日期。这个问题看起来很像是一个状态更新的问题，因此我认为可能是因为 &lt;code&gt;DatePicker2&lt;/code&gt; 组件在第一次选择失败是内部保存了这个结果，导致第二次选择日期时尽管没有选择具体日期，但是内部直接使用了第一次选择失败的日期。而我在 &lt;code&gt;DatePicker2&lt;/code&gt; 组件文档中并没有重置状态的方法，因此我是用 key 来强制更新组件，这样就可以解决这个问题。&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;function Demo() {
	const [first, setFirst] = React.useState(&apos;&apos;);
	const [second, setSecond] = React.useState(&apos;&apos;);
	return (
		&amp;lt;div&amp;gt;
			&amp;lt;DatePicker2 value={first} onChange={(_, str) =&amp;gt; {
				setFirst(str);
			}} /&amp;gt;
            {/*  使用 key 强制刷新组件  */}
			&amp;lt;DatePicker2 key={Date.now()} value={second} onChange={(_, str) =&amp;gt; {
				if (!first) {
					return;
				}
				setSecond(str);
			}} /&amp;gt;

		&amp;lt;/div&amp;gt;
	);
}
ReactDOM.render(&amp;lt;Demo /&amp;gt;, mountNode);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>由于动画效果导致的页面渲染问题</title><link>https://www.mihouo.com/posts/front/page-rendering-issues-caused-by-animation-effects/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/page-rendering-issues-caused-by-animation-effects/</guid><description>动画效果导致容器宽度变化非实时引发的页面渲染异常</description><pubDate>Fri, 09 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;昨天下午，同事再写新项目页面时发现了一个问题，由于这个项目的框架是我搭建的，这个问题大概是折叠、打开二级菜单时主视图的 Echart 图表显示异常，宽度没有自适应。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;问题分析&lt;/h2&gt;
&lt;p&gt;在听他描述时，我第一想法是检查他的代码中是否在 &lt;code&gt;resize&lt;/code&gt; 事件中调用了 &lt;code&gt;chartInstance.resize&lt;/code&gt; 方法，因为 Echart 图表的宽度是根据容器的宽度来自适应的，所以在页面宽度发生变化时，需要调用 &lt;code&gt;chartInstance.resize&lt;/code&gt; 方法来重新计算图表的宽度。&lt;/p&gt;
&lt;p&gt;但是发现他调用了这个方法，但是 Echart 图表却没有任何变化，我就想到是否是因为切换二级菜单时，并没有触发 &lt;code&gt;resize&lt;/code&gt; 事件，因为页面整体的宽度并没有发生改变，只是二级菜单的宽度发生了改变，所以我就使用 &lt;code&gt;new Event(&apos;resize&apos;)&lt;/code&gt; 来手动触发 &lt;code&gt;resize&lt;/code&gt; 事件；&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const onChangeCollapse = () =&amp;gt; {
  const resizeEvent = new Event(&apos;resize&apos;);
  window.dispatchEvent(resizeEvent);
	// ... 其他逻辑代码
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时切换二级菜单是否折叠时 &lt;code&gt;chartInstance.resize&lt;/code&gt; 方法正常被调用了，图表的宽度也重新计算了，但是图表宽度确仍然异常，具体表现是，在二级菜单折叠时 Echart 图表却渲染的宽度较短，在二级菜单打开时 Echart 图表却渲染的宽度较长，超出屏幕范围。这一奇怪的现象起初令我很是疑惑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./collapsed.png&quot; alt=&quot;collapsed.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./open.png&quot; alt=&quot;open.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我将 &lt;code&gt;chartInstance.resize&lt;/code&gt; 调用放入 &lt;code&gt;setTimeout&lt;/code&gt; 中并设置 1s 的延迟执行，发现一切都正常了，这时也打消了我刚刚的疑惑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;刚刚这一异常渲染表现是因为容器的宽度改变并不是实时的，而是有延迟，所以导致每次 &lt;code&gt;chartInstance.resize&lt;/code&gt; 调用时都是以前一次的宽度进行渲染的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;既然找到了原因，问题就很容易解决了，只需要将 &lt;code&gt;chartInstance.resize&lt;/code&gt; 放入 &lt;code&gt;setTimeout&lt;/code&gt; 并以一个合理的延迟时间运行即可，我控制二级菜单折叠展开的动画持续时间是 0.3s，因此将&lt;code&gt;setTimeout&lt;/code&gt; 第二个参数设置为 300，就解决了这个问题。&lt;/p&gt;
&lt;h2&gt;完美解决方案&lt;/h2&gt;
&lt;p&gt;在记录这个问题时突然想到，这样修改的话需要在每个 &lt;code&gt;chartInstance.resize&lt;/code&gt; 都需要放入延时器中执行，有些麻烦，且未来如果更改了控制二级菜单折叠展开的动画持续时间就需要在每个页面都更改，不太方便，因此可以将手动触发 &lt;code&gt;resize&lt;/code&gt; 事件放入&lt;code&gt;setTimeout&lt;/code&gt; 中执行，那么动画已经结束，然后再触发 resize 事件，页面中接收到 &lt;code&gt;resize&lt;/code&gt; 事件时对应的容器宽度也已经正常改变完成了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const DEALY = 0.3;
const onChangeCollapse = () =&amp;gt; {
  setTimeout(() =&amp;gt; {
	  const resizeEvent = new Event(&apos;resize&apos;);
	  window.dispatchEvent(resizeEvent);
		// ... 其他逻辑代码
	}, DEALY * 100);
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Vercel KV 存储可视化管理页面</title><link>https://www.mihouo.com/posts/front/visual-management-interface-for-vercel-kv-storage/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/visual-management-interface-for-vercel-kv-storage/</guid><description>使用 Next.js 14 搭建可视化管理 Vercel KV 存储项目</description><pubDate>Fri, 09 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;前段时间将博客从 &lt;code&gt;Notion&lt;/code&gt; 迁移到了 &lt;code&gt;Vercel&lt;/code&gt;，发现了 &lt;code&gt;Vercel&lt;/code&gt; 新功能 &lt;code&gt;Storage&lt;/code&gt;，可以用来存储一些信息，我使用的是 &lt;code&gt;KV Storage&lt;/code&gt;，但是 &lt;code&gt;Vercel&lt;/code&gt; 并没有提供可视化的管理页面，这里就写了一个页面来管理 &lt;code&gt;KV Storage&lt;/code&gt;。正好也可以趁此机会实践 &lt;code&gt;Next.js 14&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;https://github.com/DevilC0822/vkvm&quot;&gt;项目地址&lt;/a&gt;&lt;/h2&gt;
&lt;h2&gt;功能&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;Next.js 14&lt;/code&gt; 全栈框架搭建，利用 &lt;code&gt;Vercel SDK&lt;/code&gt; 来操作 &lt;code&gt;KV Storage&lt;/code&gt;，实现&lt;code&gt;string&lt;/code&gt;、&lt;code&gt;hash&lt;/code&gt;、&lt;code&gt;list&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;zset&lt;/code&gt;五种数据结构的增删改查。&lt;/p&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;clone&lt;/code&gt; 项目&lt;/li&gt;
&lt;li&gt;安装依赖 &lt;code&gt;npm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;从 &lt;a href=&quot;https://vercel.com/devilc0822s-projects/~/stores&quot;&gt;Vercel Storage&lt;/a&gt; 获取你的 &lt;code&gt;secret key&lt;/code&gt;，将其添加到 &lt;code&gt;.env.development.local&lt;/code&gt; 文件中&lt;/li&gt;
&lt;li&gt;运行 &lt;code&gt;npm run dev&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;项目截图&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./databoard.png&quot; alt=&quot;databoard.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./views.png&quot; alt=&quot;views.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./create.png&quot; alt=&quot;create.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./edit.png&quot; alt=&quot;edit.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./view.png&quot; alt=&quot;view.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>VSCode 中一个很实用的 React 代码片段</title><link>https://www.mihouo.com/posts/front/a-handy-react-code-snippet-in-vscode/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/a-handy-react-code-snippet-in-vscode/</guid><description>VSCode 中实用的 React 代码片段，提高编码效率</description><pubDate>Tue, 06 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在 React 项目中，经常需要创建一个组件、页面。这个时候，我们可以使用代码片段来快速生成一个默认的模板。&lt;/p&gt;
&lt;p&gt;通常情况下，我们需要创建一个组件，需要创建一个文件夹，然后在文件夹中创建一个 &lt;code&gt;index.tsx&lt;/code&gt; 文件，然后在 &lt;code&gt;index.tsx&lt;/code&gt; 文件中写入组件的代码。而定义在 &lt;code&gt;index.tsx&lt;/code&gt; 中的组件名称通常情况下为文件夹的名称。但是 &lt;code&gt;vscdoe&lt;/code&gt; 并没有提供一个变量用于获取当前文件夹的名称，而是只有当前文件夹的路径，因此需要使用正则匹配得到当前文件夹的名称。&lt;/p&gt;
&lt;p&gt;这个时候，我们可以使用代码片段来快速生成一个默认的模板。&lt;/p&gt;
&lt;p&gt;首先，需要创建一个新的用户代码片段，打开 VSCode 的命令面板（&lt;strong&gt;&lt;code&gt;cmd + shift + p&lt;/code&gt;&lt;/strong&gt;），输入 “&lt;code&gt;Preferences: Configure User Snippets&lt;/code&gt;”，然后选择 “&lt;code&gt;New Global Snippets file…&lt;/code&gt;”。&lt;/p&gt;
&lt;p&gt;如果你使用了 &lt;code&gt;typescript&lt;/code&gt; 则直接替换成如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;React Functional Component&quot;: {
    &quot;prefix&quot;: &quot;rfc&quot;,
    &quot;body&quot;: [
      &quot;const ${1:${TM_DIRECTORY/^.+\\/(.*)$/$1/}}: React.FC = () =&amp;gt; {&quot;,
      &quot;  return (&quot;,
      &quot;    &amp;lt;div&amp;gt;$1&amp;lt;/div&amp;gt;&quot;,
      &quot;  );&quot;,
      &quot;};&quot;,
      &quot;&quot;,
      &quot;export default $1;&quot;
    ],
    &quot;description&quot;: &quot;Create a React functional component with folder name&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你使用的是 &lt;code&gt;javascript&lt;/code&gt; 则删除 &lt;code&gt;body&lt;/code&gt; 中第一行的 &lt;code&gt;: React.FC&lt;/code&gt; 即可。&lt;/p&gt;
&lt;p&gt;使用时只需要输入 rfc 然后按下 &lt;code&gt;tab&lt;/code&gt; 键即可生成一个默认的模板。&lt;/p&gt;
</content:encoded></item><item><title>使用 ReactDOM.createPortal 重写组件</title><link>https://www.mihouo.com/posts/front/rewriting-components-with-reactdomcreateportal/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/rewriting-components-with-reactdomcreateportal/</guid><description>使用 ReactDOM.createPortal 重写组件，解决组件嵌套和布局问题</description><pubDate>Wed, 17 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最近在做一个新项目，这个项目的有一个交互的规范是搜索条件默认是隐藏的然后点击一个过滤器 Icon，展示那些搜索条件，由于我们部门是后来加入的这个项目，在此基础上做一些定制开发，也要遵循这一规范，但是我看到已经有其他开发编写完成了这个组件，但是我认为这样编写的组件扩展性和可维护性比较差，因此我打算使用 &lt;em&gt;&lt;strong&gt;ReactDOM.createPortal 重写这个组件。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;组件预览&lt;/h2&gt;
&lt;p&gt;&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/videos/preview-filter.mp4&quot; type=&quot;video/mp4&quot;&amp;gt;
Your browser does not support the video tag.
&amp;lt;/video&amp;gt;&lt;/p&gt;
&lt;h2&gt;原组件&lt;/h2&gt;
&lt;p&gt;目前这个组件的使用方式，可以看到，实现这个组件需要引入两个组件，分别是 &lt;code&gt;FilterIcon&lt;/code&gt; 和 &lt;code&gt;FilterView&lt;/code&gt; ，其中 &lt;code&gt;FilterIcon&lt;/code&gt; 是那个过滤器的 Icon 主要作用是点击展示隐藏搜索条件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;FilterView&lt;/code&gt; 主要是定义一些搜索条件，它这里将那些条件 抽象了出来并传入 &lt;code&gt;searchList&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 示例伪代码
import FilterIcon from &quot;@/pages/LuZhiBI/components/FilterIcon/filterIcon&quot;;
import FilterView from &quot;@/pages/LuZhiBI/components/FilterView&quot;;

const searchList: any[] = [
  {
    searchType: &quot;string&quot;,
    searchComponentType: &quot;input&quot;,
    placeHolder: &quot;请输入指标名称&quot;,
    title: &quot;指标名称&quot;,
    key: &quot;indicatorName&quot;,
  },
  {
    searchType: &quot;string&quot;,
    searchComponentType: &quot;multipleSelect&quot;,
    placeHolder: &quot;请选择指标类型&quot;,
    title: &quot;指标类型&quot;,
    key: &quot;indicatorType&quot;,
    options: options.classificationOfIndicatorsOptions,
    mode: &quot;multiple&quot;,
    defaultValue: options.classificationOfIndicatorsOptions.map(item =&amp;gt; item.value),
  },
  {
    searchType: &quot;text&quot;,
    searchComponentType: &quot;dateRangeTime&quot;,
    showTime: true,
    title: &quot;创建时间&quot;,
    key: &quot;createdAt&quot;,
  },
];

&amp;lt;div className={styles.container}&amp;gt;
  &amp;lt;div className={styles.headBox}&amp;gt;
    ...
    &amp;lt;div className={styles.searchBox}&amp;gt;
      ...
      &amp;lt;FilterIcon handleFilterClick={handleFilterClick} filterTitle=&quot;过滤器&quot; /&amp;gt;
      ...
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;FilterView ref={filterViewRef} searchList={searchList} finish={onFinish} defaultFormValue={defaultSearchInfo} /&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从上可以看出，有几点不足之处&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个功能却需要引入两个组件配合&lt;/li&gt;
&lt;li&gt;需要手动获取 &lt;code&gt;FilterView&lt;/code&gt; ref，并使用 &lt;code&gt;handleFilterClick&lt;/code&gt; 控制展示隐藏&lt;/li&gt;
&lt;li&gt;&lt;code&gt;searchList&lt;/code&gt; 定义复杂，并且不利于自定义的一些配置（需要修改原组件，在内部做一些业务适配，随着项目的不断迭代，组件内部会变得相当庞大，且难以维护，之前做的一个项目就出现了类似的问题），在定义&lt;code&gt;searchList&lt;/code&gt; 时没有 &lt;code&gt;typescript&lt;/code&gt; 提示，失去了使用它的意义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;新组件&lt;/h2&gt;
&lt;p&gt;在打算写这个组件之前，我想到了 Vue 中的 &lt;a href=&quot;https://cn.vuejs.org/guide/built-ins/teleport&quot;&gt;**&lt;code&gt;Teleport&lt;/code&gt;&lt;/a&gt;** 内置组件，它可以将组件内部的元素传送到目标 dom 节点位置，因此我在想是否 React 中有一个类似的装件，然后我就发现了**&lt;code&gt;ReactDOM.createPortal&lt;/code&gt;**，它可以实现类似 &lt;code&gt;Teleport&lt;/code&gt; 的功能。&lt;/p&gt;
&lt;h3&gt;详细解释&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ReactDOM.createPortal&lt;/code&gt;&lt;/strong&gt;：该方法接受两个参数：第一个参数是渲染的子元素，第二个参数是目标 DOM 节点。&lt;/p&gt;
&lt;p&gt;对于现在的需求，这个组件需要实现两部分，一个是固定的过滤器 Icon，另一个是需要被传送到目前位置的 children，最终实现代码如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { type FC, useEffect, useState } from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import { Button, Tooltip } from &apos;antd&apos;;
import { FilterOutlined } from &apos;@ant-design/icons&apos;;
import { motion } from &apos;framer-motion&apos;;

interface IProps {
  hoverTitle: string;
  nodeID: string;
  children?: React.ReactNode;
}

const FilterIcon: FC&amp;lt;IProps&amp;gt; = props =&amp;gt; {
  const { hoverTitle = &apos;过滤器&apos;, nodeID, children } = props;
  const [node, setNode] = useState&amp;lt;any&amp;gt;(null);
  const [height, setHeight] = useState&amp;lt;string | number&amp;gt;(0);
  const filterClick = () =&amp;gt; {
    if (height === 0) {
      setHeight(&apos;auto&apos;);
    } else {
      setHeight(0);
    }
  };
  useEffect(() =&amp;gt; {
    const targetNode = document.getElementById(nodeID);
    setNode(targetNode);
  }, [nodeID]);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;Tooltip placement=&quot;bottom&quot; title={hoverTitle}&amp;gt;
        &amp;lt;Button
          onClick={filterClick}
          icon={
            &amp;lt;FilterOutlined
              style={{
                fontSize: 16,
              }}
            /&amp;gt;
          }
          type=&quot;text&quot;
        &amp;gt;&amp;lt;/Button&amp;gt;
      &amp;lt;/Tooltip&amp;gt;
      {node &amp;amp;&amp;amp;
        ReactDOM.createPortal(
          &amp;lt;motion.div
            animate={{ height }}
            className=&quot;filter-conditions&quot;
            style={{
              height: 0,
              overflow: &apos;hidden&apos;,
              width: &apos;100%&apos;,
              backgroundColor: &apos;#f5f5f5&apos;,
              borderRadius: 4,
              marginTop: 10,
              padding: &apos;0 16px&apos;,
            }}
          &amp;gt;
            {children}
          &amp;lt;/motion.div&amp;gt;,
          node,
        )}
    &amp;lt;/&amp;gt;
  );
};
export default FilterIcon;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用方式&lt;/h3&gt;
&lt;p&gt;使用时只需要引入 &lt;code&gt;Filter&lt;/code&gt; ，并在合适的位置建立一个空的 div 节点 &lt;code&gt;&amp;lt;div id=&quot;filter-main&quot; /&amp;gt;&lt;/code&gt;，并指定节点 id，并将它传入给 &lt;code&gt;Filter&lt;/code&gt; 组件即可，过滤条件则直接作为 children 写入 &lt;code&gt;Filter&lt;/code&gt; 组件内部即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 示例伪代码
import Filter from &apos;@/pages/LuZhiBI/components/Filter&apos;;

&amp;lt;div className={styles.DatasetView}&amp;gt;
  &amp;lt;div className={styles.titleBox}&amp;gt;
    &amp;lt;div style={{ display: &apos;flex&apos;, justifyContent: &apos;space-between&apos;, alignItems: &apos;center&apos; }}&amp;gt;
      &amp;lt;div className={styles.filterBox}&amp;gt;
        &amp;lt;Filter hoverTitle=&quot;过滤器&quot; nodeID=&quot;filter-main&quot;&amp;gt;
          &amp;lt;Form 
          name=&quot;filter&quot;
          onFinish={onFinish}
          autoComplete=&quot;off&quot;
          initialValues={{
            date: [],
            indicator: [&apos;&apos;],
          }}
          &amp;gt;
            &amp;lt;Row gutter={16} style={{ marginTop: 16 }}&amp;gt;
              &amp;lt;Col span={8}&amp;gt;
                &amp;lt;Form.Item&amp;lt;FieldType&amp;gt;
                  label=&quot;日期&quot;
                  name=&quot;date&quot;
                  rules={[{ required: true, message: &apos;请选择查询日期&apos; }]}
                &amp;gt;
                  &amp;lt;RangePicker style={{ width: &apos;100%&apos; }} /&amp;gt;
                &amp;lt;/Form.Item&amp;gt;
              &amp;lt;/Col&amp;gt;
              &amp;lt;Col span={8}&amp;gt;
                &amp;lt;Form.Item&amp;lt;FieldType&amp;gt;
                  label=&quot;指标&quot;
                  name=&quot;indicator&quot;
                  required
                  rules={[{
                    validator: (_rule, value) =&amp;gt; {
                      if (!value &amp;amp;&amp;amp; value !== &apos;&apos;) {
                        return Promise.reject(&apos;请选择指标&apos;);
                      }
                      return Promise.resolve();
                    }
                  }]}
                &amp;gt;
                  &amp;lt;Select mode=&apos;multiple&apos; options={indicatorOptions} /&amp;gt;
                &amp;lt;/Form.Item&amp;gt;
              &amp;lt;/Col&amp;gt;
              &amp;lt;Col span={8}&amp;gt;
                &amp;lt;Flex justify=&quot;flex-end&quot;&amp;gt;
                  &amp;lt;Form.Item&amp;gt;
                    &amp;lt;Space size=&quot;small&quot;&amp;gt;
                      &amp;lt;Button htmlType=&quot;reset&quot;&amp;gt;
                        重置
                      &amp;lt;/Button&amp;gt;
                      &amp;lt;Button type=&quot;primary&quot; htmlType=&quot;submit&quot;&amp;gt;
                        查询
                      &amp;lt;/Button&amp;gt;
                    &amp;lt;/Space&amp;gt;
                  &amp;lt;/Form.Item&amp;gt;
                &amp;lt;/Flex&amp;gt;
              &amp;lt;/Col&amp;gt;
            &amp;lt;/Row&amp;gt;
          &amp;lt;/Form&amp;gt;
        &amp;lt;/Filter&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div id=&quot;filter-main&quot; /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Steam 家庭共享库预览 - 部署</title><link>https://www.mihouo.com/posts/front/steamshared/deployment/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/deployment/</guid><description>Steam 家庭共享库预览项目部署过程记录</description><pubDate>Tue, 18 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;2024 年 7 月 31 日更新内容：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;今天新购买了台香港的服务器，因此这个项目又复活了，地址仍然是 &lt;a href=&quot;http://www.steamhome.xyz&quot;&gt;www.steamhome.xyz&lt;/a&gt;, 26号所说计划的也将继续进行。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;2024 年 7 月 26 日更新内容：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于 GCP 300 美金体验的账号到期机器停机，导致部署的服务挂了，目前手上没有国外的服务器，因此线上服务展示停止服务，但是我又发现使用 Vercel 的 KV Datebase 可以实现类似于数据库的功能，可以用于存储 steam key，因此我打算使用 &lt;strong&gt;Next.js 14（全栈框架）&lt;/strong&gt; 重写这个项目。但是由于 Vercel 的 KV Datebase 并没有提供可视化的管理页面，因此我计划如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先使用 Next.js 14 搭建 Vercel 的 KV Datebase 可视化面板并开源到 Github&lt;/li&gt;
&lt;li&gt;使用 Next.js 14（全栈框架） 重写 Steam 家庭共享库预览项目。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相比服务端开发和客户端开发遇到的问题来所，部署时遇到的问题更为复杂。&lt;/p&gt;
&lt;p&gt;首先遇到的就是服务器的网络环境，由于部署时只拥有一台国内的腾讯云服务器，我将后台服务部署上去后，发现请求总是失败，通过服务运行日志发现是服务请求 Steam Web API 时由于网络原因被拒绝，这时我通过 ChatGPT 询问解决方法，它给出使用代理访问，但是网上被没有免费可靠的代理，但是刚好那段时间我购买了 GCP 300 美金体验的账号，创建了 GCP 服务器，搭建了代理，但是由于未知原因导致搭建的代理仍然无法请求。这时我就改变了想法将服务直接部署到 GCP 服务器上。&lt;/p&gt;
&lt;h2&gt;GCP 部署&lt;/h2&gt;
&lt;p&gt;首先按照&lt;a href=&quot;https://www.mohouo.com/posts/steamshared/setting-up-a-debian-environment-a-step-by-step-guide/&quot;&gt;该文章&lt;/a&gt;安装所需要的环境&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装宝塔&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;GCP 服务器由于默认没有 hostname 导致安装失败，需要设置它然后就能正常安装。&lt;a href=&quot;https://www.bt.cn/bbs/thread-121180-1-1.html&quot;&gt;参考文章&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码移入服务器中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我选择的是将代码提交到 GitHub 私人仓库种，然后通过服务器生成 SSH，将公钥放到 GitHub SSH 管理中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动后台服务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将代码 clone 后安装 npm 依赖，通过宝塔的网站菜单管理项目&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;由于此项目还用到了数据库，我一开始并没有设置数据库账号密码，导致有人攻击给我数据库清空，并新建了个库，库名的大概意思是如果想要恢复数据需要支付 ustd。还好我这里的数据并不是重要的。但是为了后续再被攻击，需要开启数据库权限。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;启用访问控制&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要编辑 MongoDB 的配置文件（通常是 /etc/mongod.conf）并启用访问控制。找到 security 部分，并确保设置了以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;security:
  authorization: enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;进入 MongBD 数据库&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mongo
# 或者
mongosh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;创建用户&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use admin
db.createUser({
  user: &quot;adminUser&quot;,
  pwd: &quot;adminPassword&quot;,
  roles: [ { role: &quot;root&quot;, db: &quot;steam-shared&quot; } ] // 赋予这个账号访问 steam-shared 数据库的权限
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;重启 MongoDB 服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart mongod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;更改连接数据库&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;账号密码的配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  DB_HOST: &apos;localhost&apos;,
  DB_PORT: 27017,
  DB_NAME: &apos;steam-shared&apos;,
  DB_USER: &apos;adminUser&apos;,
  DB_PASS: &apos;adminPassword&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const mongoose = require(&apos;mongoose&apos;);
const config = require(&apos;../config/db&apos;);

module.exports = (success, error) =&amp;gt; {
  mongoose.connect(`mongodb://${config.DB_USER}:${encodeURIComponent(config.DB_PASS)}@${config.DB_HOST}:${config.DB_PORT}/${config.DB_NAME}?authSource=admin`, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
    .then(() =&amp;gt; {
      console.log(&apos;Database connection successful&apos;);
      success();
    })
    .catch((err) =&amp;gt; {
      console.error(&apos;Database connection error:&apos;, err);
      error(err);
    });
};

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;部署客户端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;客户端通过 npm run build:prod 命令打包后使用宝塔 PHP 项目部署，将网站目录设置成打包后生成的文件夹即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;经过上述部署这时应该可以通过 ip:port 访问项目了，但是这样十分难以记忆，这时就需要使用域名了，由于国内购买的域名需要备案使用，所以我在国外域名服务商购买了个便宜域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这时只需要进行域名解析和记录添加就可以通过 &lt;a href=&quot;https://www.steamhome.xyz&quot;&gt;www.steamhome.xyz&lt;/a&gt; 访问页面了。&lt;/p&gt;
&lt;p&gt;首先在 GCP 生成一个 DNS 解析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后在域名管理中添加解析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等待几分钟ping 这个域名直到能够 ping 通&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再在 GCP 处配置 A 记录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;项目也需要配置添加域名&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;比如请求后台服务没配置之前只能通过 http://35.220.167.240:3001/api/tracking/data 去请求，配置后则可以通过 http://api.steamhome.xyz/api/tracking/data 请求。&lt;/p&gt;
&lt;p&gt;页面也是同理可以通过 &lt;a href=&quot;http://www.steamhome.xyz&quot;&gt;www.steamhome.xyz&lt;/a&gt; 访问。&lt;/p&gt;
&lt;h2&gt;SSL&lt;/h2&gt;
&lt;p&gt;如果需要通过 https 访问则需要添加 SSL 证书，通过宝塔可以一键配置 Let&apos;s Encrypt 证书&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./9.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>记录一下装 Debian 环境</title><link>https://www.mihouo.com/posts/front/steamshared/setting-up-a-debian-environment-a-step-by-step-guide/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/setting-up-a-debian-environment-a-step-by-step-guide/</guid><description>Debian 环境安装配置记录，包括 SSH、软件安装等</description><pubDate>Wed, 05 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;我使用的是 Google Cloud 创建的实例，它默认是不开启 ssh 登录的，为了统一管理服务器，我使用的是 Termius。&lt;/p&gt;
&lt;h2&gt;开启 SSH 登录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;通过内置的 SSH 窗口进入终端&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;终端中设置 root 用户的密码&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 进入 root 用户
sudo -i
# 设置密码
passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改 sshd_config文件中的 PermitRootLogin 和 PasswordAuthentication&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重启服务器&lt;/li&gt;
&lt;li&gt;通过 Termius 登录&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;登录成功后如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装 nvm&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;1. 更新系统包&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;首先，确保你的系统包是最新的：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;2. 安装NVM&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;你可以使用以下命令从NVM的GitHub仓库安装NVM：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**请注意，**v0.39.7 &lt;strong&gt;是NVM的版本号，你可以检查 &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;NVM的GitHub页面&lt;/a&gt; 以获取最新版本。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;3. 加载NVM&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;安装完成后，你需要将NVM加载到你的shell会话中。你可以通过以下命令来实现：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export NVM_DIR=&quot;$([ -z &quot;${XDG_CONFIG_HOME-}&quot; ] &amp;amp;&amp;amp; printf %s &quot;${HOME}/.nvm&quot; || printf %s &quot;${XDG_CONFIG_HOME}/nvm&quot;)&quot;
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; \. &quot;$NVM_DIR/nvm.sh&quot; # This loads nvm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为了在每次启动shell时自动加载NVM，你可以将上述命令添加到你的shell配置文件中，例如~/.bashrc、~/.zshrc或~/.profile：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;export NVM_DIR=&quot;$([ -z &quot;${XDG_CONFIG_HOME-}&quot; ] &amp;amp;&amp;amp; printf %s &quot;${HOME}/.nvm&quot; || printf %s &quot;${XDG_CONFIG_HOME}/nvm&quot;)&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
echo &apos;[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; \. &quot;$NVM_DIR/nvm.sh&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;然后，重新加载shell配置文件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;4. 验证NVM安装&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;你可以通过以下命令验证NVM是否安装成功：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;5. 安装Node.js&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;使用NVM安装Node.js的最新LTS版本：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvm install --lts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;你也可以安装特定版本的Node.js，例如：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvm install 14.17.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6. 设置默认Node.js版本&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你可以使用以下命令设置默认的Node.js版本：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看安装的 node 版本列表
nvm list
# 设置 默认版本
nvm alias default 14.17.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;7. 验证Node.js和npm安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后，验证Node.js和npm是否安装成功：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装 &lt;strong&gt;MongoDB 6&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加针对 Ubuntu 的源：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-6.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/6.0 multiverse&quot; | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加签名&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://pgp.mongodb.com/server-6.0.asc | \
   sudo gpg -o /usr/share/keyrings/mongodb-server-6.0.gpg \
   --dearmor
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新apt 并安装 mongodb&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install mongodb-org -y
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;启动MongoDB服务&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;安装完成后，启动MongoDB服务&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start mongod
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;设置MongoDB服务开机自启&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;设置MongoDB服务在系统启动时自动启动&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable mongod
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;验证MongoDB安装&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;检查MongoDB服务的状态，确保它正在运行&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status mongod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;看到类似于以下的输出，表示MongoDB服务正在运行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装 Git&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新APT包数据库&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;首先，确保你的APT包数据库是最新的&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装Git&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;使用以下命令安装Git&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install git -y
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;验证Git安装&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;安装完成后，你可以使用以下命令验证Git是否安装成功&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git -v
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 PM2&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;已经安装了 Node.js 和 npm后，全局安装 PM2&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g pm2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;使用 PM2 启动你的 Node.js 项目。例如，如果你的项目入口文件是 main.js&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pm2 start main.js --name &quot;steam-shared-service&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;检验项目是否运行成功&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 apifox 请求服务项目中的接口&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Steam 家庭共享库预览 - 客户端设计与实现</title><link>https://www.mihouo.com/posts/front/steamshared/client-side-design-and-implementation/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/client-side-design-and-implementation/</guid><description>Steam 家庭共享库预览项目客户端设计与实现</description><pubDate>Fri, 31 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;2024 年 7 月 31 日更新内容：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;复活了&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;2024 年 7 月 26 日更新内容：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;暂时停止服务，&lt;a href=&quot;https://www.mihouo.com/posts/steamshared/deployment/&quot;&gt;原因见此&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;地址： &lt;a href=&quot;https://www.steamhome.xyz/&quot;&gt;https://www.steamhome.xyz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;客户端的开发较为简单，我在这里大致讲一下背后的流程。&lt;/p&gt;
&lt;p&gt;首先，进入页面先向后台发送请求看是否有可用的 Steam Key，如果没有的话，提示用户输入自己的 key，并且右上角状态图标发生改变，用于提示用户。&lt;/p&gt;
&lt;p&gt;如果用户输入了可用的 key 则判断 key 是否可用，如果可用则改变状态，提示用户可以填写 Steam id 进行查询。&lt;/p&gt;
&lt;p&gt;在用户填写了 steam id 后进行请求，将多个玩家的游戏库存进行并集操作，并记录每一个游戏被哪个玩家拥有（用一个字段记录&lt;code&gt;subordinate&lt;/code&gt;），然后右侧表格进行渲染，表格行为单个游戏，列为玩家，交叉出用刚刚记录的&lt;code&gt;subordinate&lt;/code&gt; 判断该玩家是否拥有这款游戏。游戏名称小眼睛 icon 点击后提供该玩家进入这个家庭后能玩到哪些不属于 Ta 的游戏，实现逻辑也是使用了&lt;code&gt;subordinate&lt;/code&gt; 字段。&lt;/p&gt;
&lt;h2&gt;预览&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;首页&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;添加成功信息提示&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查看玩家能游玩的新游戏列表&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;其他&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Steam 家庭共享库预览 - 服务端设计与实现</title><link>https://www.mihouo.com/posts/front/steamshared/server-side-design-and-implementation/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/server-side-design-and-implementation/</guid><description>基于 Node.js 和 Express 的服务端设计与实现</description><pubDate>Fri, 31 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这篇文章中，我将介绍如何设计和实现一个基于 Node.js 和 Express 框架的服务端项目，提供 Steam 家庭共享库的预览功能。我们将聚焦于一个名为 gamers 的接口，该接口接收 ids 参数，并返回对应的游戏信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术栈选择&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在构建服务端项目时，选择合适的技术栈是成功的关键。对于本项目，我们选择了 Node.js 和 Express 框架。选择这两者的原因如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt;：Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时，具有非阻塞、事件驱动的特点，非常适合构建高并发的应用程序。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Express&lt;/strong&gt;：Express 是一个快速、简洁的 Node.js Web 应用框架，提供了一系列强大的功能来帮助开发者快速构建 Web 应用和 API。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;接口设计&lt;/h2&gt;
&lt;p&gt;主要用到的 Steam Web API 接口如下&lt;/p&gt;
&lt;p&gt;• 获取用户信息: http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/&lt;/p&gt;
&lt;p&gt;• 获取用户拥有的游戏: http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/&lt;/p&gt;
&lt;p&gt;• 获取游戏详细信息: http://store.steampowered.com/api/appdetails&lt;/p&gt;
&lt;h2&gt;接口实现&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;app.post(&apos;/get/gamers&apos;, async (req, res) =&amp;gt; {
  try {
    const { ids, key: reqKey = &apos;&apos; } = req.body;
    if (!ids || ids?.length === 0) {
      res.send({
        code: 400,
        data: null,
        msg: &apos;请输入用户 id&apos;,
        success: false,
      });
    }
    const key = reqKey || await getKey();
    if (!key) {
      res.send({
        code: 500,
        data: null,
        msg: &apos;无可用key&apos;,
        success: false,
      });
    }
    const resPlayerSummaries = await service({
      url: &apos;/ISteamUser/GetPlayerSummaries/v0002/&apos;,
      params: {
        steamids: ids.join(&apos;,&apos;),
        format: &apos;json&apos;,
        key,
      },
      instance: &apos;IPlayerService&apos;,
      method: &apos;get&apos;,
    }).then(res =&amp;gt; {
      if (res.response.players) {
        return res.response.players;
      }
      return [];
    });
    if (resPlayerSummaries.length === 0) {
      res.send({
        code: 400,
        data: null,
        msg: &apos;输入的用户 id 均无效&apos;,
        success: false,
      })
    }
    const resAllGames= await Promise.allSettled(resPlayerSummaries.map(player =&amp;gt; {
      if (player.communityvisibilitystate === 1) {
        return Promise.resolve({games: [], steamid: player.steamid});
      }
      return service({
        url: &apos;/IPlayerService/GetOwnedGames/v0001/&apos;,
        params: {
          steamid: player.steamid, // 76561198391051438
          format: &apos;json&apos;,
          key,
        },
        instance: &apos;IPlayerService&apos;,
      }).then(res =&amp;gt; {
        if (res.response?.games) {
          return {games: res.response.games, steamid: player.steamid};
        }
        return {games: [], steamid: player.steamid};
      })
    })).then(res =&amp;gt; {
      return res.map(res =&amp;gt; res.value);
    });
    const allGamesIds = [...new Set(...[resAllGames.map(res =&amp;gt; res?.games.map(game =&amp;gt; game.appid))].map(ids =&amp;gt; ids.flat()))];
    const resGames = await Promise.allSettled(allGamesIds.map(appid =&amp;gt; {
      return service({
        url: &apos;/api/appdetails?appids&apos;,
        params: {
          appids: appid,
          l: &apos;schinese&apos;,
          cc: &apos;cn&apos;,
          format: &apos;json&apos;,
          filters: &apos;basic,price_overview,categories&apos;,
          key,
        },
        instance: &apos;Storefront&apos;,
      }).then(res =&amp;gt; {
        const data = Object.values(res)[0];
        if (data.success) {
          return data.data;
        }
        return {
          steam_appid: appid,
          success: false,
        };
      }).catch(err =&amp;gt; {
        return {
          steam_appid: appid,
          success: false,
        };
      });
    }));
    const successGames = resGames.filter(game =&amp;gt; (game.value?.success ?? true));
    const failGames = resGames.filter(game =&amp;gt; !(game.value?.success ?? true));
    const result = [];
    ids.forEach((id, index) =&amp;gt; {
      const cur = resPlayerSummaries.find(player =&amp;gt; player.steamid === id);
      if (!cur) {
        return;
      }
      if (cur.communityvisibilitystate === 1) {
        result.push({
          isPublic: false,
          user: cur,
          stock: [],
          failGameId: [],
        });
        return;
      }
      const curUserGameIds = resAllGames.find(res =&amp;gt; res?.steamid === id)?.games.map(game =&amp;gt; game.appid);
      result.push({
        isPublic: true,
        user: cur,
        stock: successGames.filter(game =&amp;gt; curUserGameIds?.includes(game.value.steam_appid)).map(game =&amp;gt; game.value),
        failGameId: failGames.filter(game =&amp;gt; curUserGameIds?.includes(game.value.steam_appid)).map(game =&amp;gt; game.value.steam_appid),
      });
    });
    res.send({
      code: 200,
      data: result,
      msg: &apos;success&apos;,
      success: true,
    });
  } catch (e) {
    console.log(e);
    res.send({
      code: 500,
      msg: e === &apos;无可用key&apos; ? e : &apos;服务器错误&apos;,
      success: false,
    });
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;接口测试&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;无入参&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随机输入的 steamid&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;真实存在的 steamid&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Steam 家庭共享库预览-原型设计</title><link>https://www.mihouo.com/posts/front/steamshared/prototype-design/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/prototype-design/</guid><description>Steam 家庭共享库预览项目原型设计</description><pubDate>Tue, 23 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://modao.cc/proto/0TuoMfIscdhftN58PWjuU/sharing?view_mode=read_only&quot;&gt;原型地址&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Steam 家庭共享库预览-前言</title><link>https://www.mihouo.com/posts/front/steamshared/introduction/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/steamshared/introduction/</guid><description>Steam 家庭共享库预览项目介绍，利用 Steam Web API 开发预览应用</description><pubDate>Fri, 19 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最近 Steam 推出了一种新的&lt;a href=&quot;https://store.steampowered.com/news/app/593110/view/4149575031735702628&quot;&gt;游戏库家庭共享方案&lt;/a&gt;，简单来说支持六个用户组建成新的家庭，而这五个人可以游玩所有成员的游戏（即并集）。同时相比过去老的家庭共享功能，新的Steam家庭中玩家可以在其他玩家在线的同时游玩家庭库列表中的共享游戏，也支持离线访问，但是需要注意的是同一款游戏不能共同访问，除非这款游戏在家庭库中有多个副本。这一方案刚推出引起了很多玩家的热烈讨论，并有很多玩家在社区内寻找优质成员组建成新的家庭。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;由于加入到家庭中再退出存在&lt;strong&gt;一年的冷却期（离开后席位也有一年的冷却期）&lt;/strong&gt;，所以为了避免玩家在加入家庭后失望不满，所以我想着利用 Steam Web API 开发出一个 Web 应用，方便各位玩家在加入前知晓自己加入后可以获得哪些新游戏的游玩权限而开发。&lt;/p&gt;
&lt;h2&gt;可行性&lt;/h2&gt;
&lt;p&gt;利用 Steam Web API 可以查看用户的库存，因此在理论上是可行的。&lt;/p&gt;
&lt;p&gt;通过 http://api.steampowered.com/ISteamWebAPIUtil/GetSupportedAPIList/v1/?key=xxx&amp;amp;steamids=xxx 接口可以获取该key可用的所有API信息，通过查询表明我的 key 可以使用一个名为 GetOwnedGames 的接口查询用户的库存信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;GetOwnedGames&quot;,
    &quot;version&quot;: 1,
    &quot;httpmethod&quot;: &quot;GET&quot;,
    &quot;description&quot;: &quot;Return a list of games owned by the player&quot;,
    &quot;parameters&quot;: [
        {
            &quot;name&quot;: &quot;key&quot;,
            &quot;type&quot;: &quot;string&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;Access key&quot;
        },
        {
            &quot;name&quot;: &quot;steamid&quot;,
            &quot;type&quot;: &quot;uint64&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;The player we&apos;re asking about&quot;
        },
        {
            &quot;name&quot;: &quot;include_appinfo&quot;,
            &quot;type&quot;: &quot;bool&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;true if we want additional details (name, icon) about each game&quot;
        },
        {
            &quot;name&quot;: &quot;include_played_free_games&quot;,
            &quot;type&quot;: &quot;bool&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;Free games are excluded by default.  If this is set, free games the user has played will be returned.&quot;
        },
        {
            &quot;name&quot;: &quot;appids_filter&quot;,
            &quot;type&quot;: &quot;uint32&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;if set, restricts result set to the passed in apps&quot;
        },
        {
            &quot;name&quot;: &quot;include_free_sub&quot;,
            &quot;type&quot;: &quot;bool&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;Some games are in the free sub, which are excluded by default.&quot;
        },
        {
            &quot;name&quot;: &quot;skip_unvetted_apps&quot;,
            &quot;type&quot;: &quot;bool&quot;,
            &quot;optional&quot;: true,
            &quot;description&quot;: &quot;if set, skip unvetted store apps&quot;
        },
        {
            &quot;name&quot;: &quot;language&quot;,
            &quot;type&quot;: &quot;string&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;Will return appinfo in this language&quot;
        },
        {
            &quot;name&quot;: &quot;include_extended_appinfo&quot;,
            &quot;type&quot;: &quot;bool&quot;,
            &quot;optional&quot;: false,
            &quot;description&quot;: &quot;true if we want even more details (capsule, sortas, and capabilities) about each game.  include_appinfo must also be true.&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;技术选型&lt;/h2&gt;
&lt;h3&gt;服务端&lt;/h3&gt;
&lt;p&gt;由于 Steam Web API 接口返回的数据格式对于我来说并不是很友好，因此我这里使用 Node.js 对接口预先进行处理封装，再将其提供给前端直接使用。&lt;/p&gt;
&lt;h2&gt;客户端&lt;/h2&gt;
&lt;p&gt;使用 React + Ant Design 进行开发。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;p&gt;获取 Steam Web API 的 key。&lt;/p&gt;
&lt;p&gt;访问 https://steamcommunity.com/dev/apikey 如果没有 key 则按照指示创建一个。&lt;/p&gt;
&lt;p&gt;如下如所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>使用 antv/g2 绘制&apos;旭日图&apos;</title><link>https://www.mihouo.com/posts/front/drawing-sunburst-charts-with-antv-g2/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/drawing-sunburst-charts-with-antv-g2/</guid><description>使用 AntV/G2 绘制旭日图和径向堆叠条形图，展示层级结构数据</description><pubDate>Mon, 15 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为什么标题中的旭日图打双引号呢，是因为虽然它的样式长得像是旭日图，但是数据结构却和旭日图所需要的相差远。&lt;/p&gt;
&lt;p&gt;在官方示例中，&lt;a href=&quot;https://g2.antv.antgroup.com/examples/general/sunburst/#sunburst-color&quot;&gt;旭日图&lt;/a&gt;的数据结构是父节点不设置值的大小，而是由它的所有子节点累加确认的。因此，旭日图是**父节点一定对应一个或者多个子节点，且这些子节点不能超过内圈父节点的范围。**而现在产品想要的效果是父节点和子节点没有关联，只是父节点的值需要等于子节点的累加值（这里的父节点由两部分组成），这样的话子节点一定会跨过这两个父节点的临界，产品原型如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;径向堆叠条形图&lt;/h2&gt;
&lt;p&gt;因此，旭日图并不满足这个需求，我和产品反映了这个问题，他说让我使用&lt;a href=&quot;https://g2.antv.antgroup.com/examples/general/radial/#radial-stacked&quot;&gt;径向堆叠条形图&lt;/a&gt;试试，在我一番调试下，发现这个虽然在外观上能实现这样的图形，但是在其他的很多方面都是不符合需求的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;使用径向堆叠图实现的效果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用径向堆叠图实现的效果&lt;/p&gt;
&lt;p&gt;由于径向堆叠图的数据结构通常是同一组类型在不同状态（时期）下的数据，而这个需求的数据结构是不同类型在在不同状态（时期）的数据，所以导致它有诸多问题&lt;/p&gt;
&lt;p&gt;主要就是 tooltip 展示问题&lt;/p&gt;
&lt;p&gt;内层展示还算是正常&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;外层展示就完全不对应了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我猜测这是由于径向堆叠图的数据结构设计是想要展示内外侧是相同的，但是这里我给出的数据确实不同的，而它展示的逻辑可能是，由 chart.data.transform 的 fields 数组中定义所有字段依次展示。&lt;/p&gt;
&lt;p&gt;这是会导致 tooltip 不对应的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data = [
  {
    State: &apos;&apos;,
    天猫: 25,
    天猫_购买: 15,
    京东: 20,
    京东_购买: 10,
    抖音: 10,
    抖音_购买: 5,
    微信: 10,
    微信_购买: 5,
  },
  {
    State: &apos; &apos;,
    百利: 5,
    百利_购买: 5,
    尊尼获加: 5,
    尊尼获加_购买: 5,
    帝亚吉欧: 10,
    帝亚吉欧_购买: 10,
  },
];

// chart.data 如下
{
  value: data,
  transform: [
	  {
		  type: &apos;fold&apos;,
		  fields: [&apos;天猫&apos;, &apos;天猫_购买&apos;, &apos;京东&apos;, &apos;京东_购买&apos;, &apos;抖音&apos;, &apos;抖音_购买&apos;, &apos;微信&apos;, &apos;微信_购买&apos;, &apos;百利&apos;, &apos;百利_购买&apos;, &apos;尊尼获加&apos;, &apos;尊尼获加_购买&apos;, &apos;帝亚吉欧&apos;, &apos;帝亚吉欧_购买&apos;],
		  key: &apos;key&apos;,
		  value: &apos;会员总数&apos;,
		  retains: [&apos;State&apos;],
		},
	],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最终解决方案&lt;/h2&gt;
&lt;p&gt;最后我想到 echart 中之前画过嵌套饼图，但是在 antv/g2（v5） 版本的图标示例中并没有嵌套饼图的例子，查看文档发现，实现双视图需要使用到&lt;a href=&quot;https://g2.antv.antgroup.com/manual/core/composition&quot;&gt;复合&lt;/a&gt;实现，我们这里的需求是视图的层叠，因此使用的是&lt;a href=&quot;https://g2.antv.antgroup.com/spec/composition/space-layer&quot;&gt;空间复合&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;按照官方示例很容易就绘制出了嵌套饼图，但是这里会出现图形大小层叠的问题，导致外层图形的 tooltip 无法正常使用。这是因为每个圆环实际上是在一个正方形的区域内绘制的，这个区域的大小是由圆环的直径决定的。如果你创建了两个嵌套的圆环，后一个圆环可能会在事件处理上覆盖前一个圆环，导致前一个圆环的&lt;code&gt;tooltip&lt;/code&gt;失效。具体表现图如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而我在文档中并没有发现能够改变这一规则的属性，因此我想如果改变内圈直径，使其大小不遮挡外层圆环，这样的话就不会影响外层 tooltip 触发。最后我使用 &lt;em&gt;&lt;strong&gt;attr&lt;/strong&gt;&lt;/em&gt; 修改圆环的宽高，然后再使用&lt;em&gt;&lt;strong&gt;coordinate.outerRadius&lt;/strong&gt; 和 &lt;strong&gt;coordinate.innerRadius&lt;/strong&gt;&lt;/em&gt; 修改圆环视觉上的大小，最终实现代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const data1 = [
  { item: &apos;天猫&apos;, bgColor: &apos;#e8b038&apos;, count: 20, percent: 0.2 },
  { item: &apos;天猫-purchase&apos;, bgColor: &apos;#e8b038&apos;, type: &apos;purchase&apos;, count: 10, percent: 0.1 },
  { item: &apos;京东&apos;, bgColor: &apos;#528df3&apos;, count: 10, percent: 0.1 },
  { item: &apos;京东-purchase&apos;, bgColor: &apos;#528df3&apos;, type: &apos;purchase&apos;, count: 10, percent: 0.1 },
  { item: &apos;抖音&apos;, bgColor: &apos;#ff3d72&apos;, count: 20, percent: 0.2 },
  { item: &apos;抖音-purchase&apos;, bgColor: &apos;#ff3d72&apos;, type: &apos;purchase&apos;, count: 10, percent: 0.1 },
  { item: &apos;微信&apos;, bgColor: &apos;#00a54e&apos;, count: 15, percent: 0.15 },
  { item: &apos;微信-purchase&apos;, bgColor: &apos;#00a54e&apos;, type: &apos;purchase&apos;, count: 5, percent: 0.05 },
];

const data2 = [
  { item: &apos;帝亚吉欧&apos;, bgColor: &apos;#9ecbfb&apos;, count: 10, percent: 0.1 },
  { item: &apos;帝亚吉欧-purchase&apos;, type: &apos;purchase&apos;, bgColor: &apos;#9ecbfb&apos;, count: 5, percent: 0.05 },
  { item: &apos;尊尼获加&apos;, bgColor: &apos;#add7fc&apos;, count: 7, percent: 0.07 },
  { item: &apos;尊尼获加-purchase&apos;, type: &apos;purchase&apos;, bgColor: &apos;#add7fc&apos;, count: 3, percent: 0.03 },
  { item: &apos;百利&apos;, bgColor: &apos;#bfe2fd&apos;, count: 3, percent: 0.03 },
  { item: &apos;百利-purchase&apos;, type: &apos;purchase&apos;, bgColor: &apos;#bfe2fd&apos;, count: 2, percent: 0.02 },
  { item: &apos;not-show&apos;, bgColor: &apos;transparent&apos;, count: 70, percent: 0.7 },
];

// 创建图表实例
const chart = new Chart({
  container: &apos;channel-members&apos;,
  height: 311,
});
const layer = chart.spaceLayer();

layer
  .interval()
  .data(data2)
  .coordinate({ type: &apos;theta&apos;, outerRadius: 1.1, innerRadius: 0.82 })
  .transform({ type: &apos;stackY&apos; })
  .attr(&apos;x&apos;, 640 / 2 - 150)
  .attr(&apos;y&apos;, 311 / 2 - 150)
  .attr(&apos;width&apos;, 300)
  .attr(&apos;height&apos;, 300)
  .encode(&apos;y&apos;, &apos;percent&apos;)
  .style(&apos;fill&apos;, (d: any) =&amp;gt; {
    const result = {
      image: lines({
        backgroundColor: d.bgColor,
        backgroundOpacity: 0.6,
        stroke: d.bgColor,
        lineWidth: 1,
        spacing: 0,
      }),
      repetition: &apos;repeat&apos;,
      transform: &apos;rotate(30deg)&apos;,
    }
    if (d.type === &apos;purchase&apos;) {
      result.image = lines({
        backgroundColor: &apos;transparent&apos;,
        backgroundOpacity: 0.6,
        stroke: d.bgColor,
        lineWidth: 1,
        spacing: 10,
      });
    }
    return result;
  })
  .interaction(&apos;tooltip&apos;, { disableNative: true })
  .tooltip((data) =&amp;gt; {
    if (data.item === &apos;&apos;) {
      return &apos;&apos;;
    }
    return {
      name: data.item,
      value: `${data.percent * 100}%`,
    }
  });

layer
  .interval()
  .data(data1)
  .coordinate({ type: &apos;theta&apos;, outerRadius: 3.2, innerRadius: 1.6 })
  .transform({ type: &apos;stackY&apos; })
  .attr(&apos;x&apos;, 640 / 2 - 50)
  .attr(&apos;y&apos;, 311 / 2 - 50)
  .attr(&apos;width&apos;, 100)
  .attr(&apos;height&apos;, 100)
  .encode(&apos;y&apos;, &apos;percent&apos;)
  .style(&apos;fill&apos;, (d: any) =&amp;gt; {
    const result = {
      image: lines({
        backgroundColor: d.bgColor,
        backgroundOpacity: 0.6,
        stroke: d.bgColor,
        lineWidth: 1,
        spacing: 0,
      }),
      repetition: &apos;repeat&apos;,
      transform: &apos;rotate(30deg)&apos;,
    }
    if (d.type === &apos;purchase&apos;) {
      result.image = lines({
        backgroundColor: &apos;transparent&apos;,
        backgroundOpacity: 0.6,
        stroke: d.bgColor,
        lineWidth: 1,
        spacing: 10,
      });
    }
    return result;
  })
  .tooltip((data) =&amp;gt; ({
    name: data.item,
    value: `${data.percent * 100}%`,
  }));

chart.on(`element:${ChartEvent.POINTER_OVER}`, ({ data }) =&amp;gt; {
  if (data.data.item === &apos;not-show&apos;) {
    return;
  }
  chart.emit(&apos;tooltip:show&apos;, { data })
});
chart.on(`element:${ChartEvent.POINTER_MOVE}`, ({ data }) =&amp;gt; {
  if (data.data.item === &apos;not-show&apos;) {
    return;
  }
  chart.emit(&apos;tooltip:show&apos;, { data })
});
chart.on(`plot:${ChartEvent.POINTER_OVER}`, () =&amp;gt; chart.emit(&apos;tooltip:hide&apos;));

chart.render();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于只有天猫拥有子店铺，所以外层圆环是不完整的，这里使用 data.item === &apos;not-show&apos; 控制是否显示，实现是通过style.fill 填充透明色。如果不设置这一项的话，剩下的项会填充整个圆环，不符合需求。这样设置后将鼠标放置到外层空白区域仍然会触发 tooltip，这里我的解决方案是使用自定义交互事件，判断是否 data.item === &apos;not-show&apos; 为 true，如果不是则触发 tooltip 。&lt;/p&gt;
&lt;p&gt;最终实现效果如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>使用 antv/g2 绘制双圈交集图形</title><link>https://www.mihouo.com/posts/front/drawing-venn-diagrams-with-antv-g2/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/drawing-venn-diagrams-with-antv-g2/</guid><description>使用 AntV/G2 绘制双圈交集韦恩图，实现数据对比可视化</description><pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;最近在开发新项目，但是这个项目由其他部门搭建的，使用了和我们原先不同的图表图。我们组原先使用的是 &lt;a href=&quot;https://echarts.apache.org/zh/index.html&quot;&gt;echarts&lt;/a&gt; 而这个项目使用的是 &lt;a href=&quot;https://g2.antv.antgroup.com/&quot;&gt;antv/g2&lt;/a&gt; ，由于之前并没有使用过这个图表图，特此记录一下&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;乍一看觉得这个实现起来并不复杂，使用韦恩图即可实现，随即上官网查看韦恩图示例，一步步修改配置，最终实现出来如下样子的图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实现代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chart
  .path()
  .data({
    type: &apos;inline&apos;,
    value: [
      { sets: [&apos;A&apos;], size: 15, label: &apos;购买人数&apos;, value: 19992 },
      { sets: [&apos;B&apos;], size: 15, label: &apos;会员总数&apos;, value: 29992 },
      { sets: [&apos;A&apos;, &apos;B&apos;], size: 6, label: &apos;会员购买人数&apos;, value: 9992 },
    ],
    transform: [
      {
        type: &apos;venn&apos;,
      },
    ],
  })
  .encode(&apos;d&apos;, &apos;path&apos;)
  .encode(&apos;color&apos;, &apos;key&apos;)
  .encode(&apos;shape&apos;, &apos;hollow&apos;)
  .label({
    position: &apos;inside&apos;,
    text: (a) =&amp;gt; `${a?.label}\n${formatNumOfOptions(a?.value, { digit: 0 })}`,
    style: {
      fontSize: 12,
      fontWeight: 500,
      fill: &apos;#1890FF&apos;,
    },
  })
  .style(&apos;lineWidth&apos;, 1)
  .style(&apos;lineDash&apos;, [7, 4])
  .style(&apos;fill&apos;, (d) =&amp;gt; {
    if (d.sets.length === 1) {
      if (d.sets[0] === &apos;A&apos;) {
        return &apos;#3BA0FF&apos;;
      }
      return &apos;#4DCB73&apos;;
    }
    return &apos;#cdecf2&apos;;
  })
  .style(&apos;fillOpacity&apos;, 0.1)
  .legend(false)
  .tooltip(false);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察上图很容易看出这与设计图有两处不同，分别是交汇处的线条颜色，以及 label 的位置&lt;/p&gt;
&lt;p&gt;首先解决交汇处线条颜色，让它们在交汇处仍然用自身原本的颜色，使用 style 实现，代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;style(&apos;stroke&apos;, (d) =&amp;gt; {
  if (d.key === &apos;A&apos;) {
    return &apos;#1890FF&apos;;
  }
  if (d.key === &apos;B&apos;) {
    return &apos;#4DCB73&apos;;
  }
  return &apos;transparent&apos;;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而解决 label 位置就稍微麻烦一些，需要自定义渲染 label ，查了下文档，发现自定义渲染使用 &lt;a href=&quot;https://g2.antv.antgroup.com/spec/label/overview#%E9%80%89%E9%A1%B9options&quot;&gt;label.render&lt;/a&gt; 函数&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用 render 函数自定义渲染 html 标签，配合 css 属性&lt;em&gt;transform: translate&lt;/em&gt; 进行偏移最终实现标签的自定义位置，具体实现代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;render: (_, datum) =&amp;gt; {
  if (datum.key === &apos;A&apos;) {
    return `
      &amp;lt;div style=&quot;font-weight: 500; color: #1890FF; transform: translate(-80px, -20px)&quot;&amp;gt;
        &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
        &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    `;
  }
  if (datum.key === &apos;B&apos;) {
    return `
      &amp;lt;div style=&quot;font-weight: 500; color: #4DCB73; transform: translate(30px, -20px);&quot;&amp;gt;
      &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
      &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
      &amp;lt;/div&amp;gt;
    `;
  }
  return `
    &amp;lt;div style=&quot;font-weight: 500; color: #00BBFF; transform: translate(-36px, -20px);&quot;&amp;gt;
    &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
    &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  `;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时你会发现虽然 label 标签正确的进行了偏移但是在偏移原点时会出现蓝色方块，如下图所示&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是因为&lt;strong&gt;使用了CSS来样式化标签，将 label.style.fill 属性删除即可，最终代码如下&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const chart = new Chart({
      container: &apos;user-assets&apos;,
      height: 311,
    });

    chart
      .path()
      .data({
        type: &apos;inline&apos;,
        value: [
          { sets: [&apos;A&apos;], size: 15, label: &apos;购买人数&apos;, value: 19992 },
          { sets: [&apos;B&apos;], size: 15, label: &apos;会员总数&apos;, value: 29992 },
          { sets: [&apos;A&apos;, &apos;B&apos;], size: 6, label: &apos;会员购买人数&apos;, value: 9992 },
        ],
        transform: [
          {
            type: &apos;venn&apos;,
          },
        ],
      })
      .encode(&apos;d&apos;, &apos;path&apos;)
      .encode(&apos;color&apos;, &apos;key&apos;)
      .encode(&apos;shape&apos;, &apos;hollow&apos;)
      .label({
        position: &apos;inside&apos;,
        text: (a) =&amp;gt; `${a?.label}\n${formatNumOfOptions(a?.value, { digit: 0 })}`,
        style: {
          fontSize: 12,
        },
        render: (_, datum) =&amp;gt; {
          if (datum.key === &apos;A&apos;) {
            return `
              &amp;lt;div style=&quot;font-weight: 500; color: #1890FF; transform: translate(-80px, -20px)&quot;&amp;gt;
                &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
                &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;
            `;
          }
          if (datum.key === &apos;B&apos;) {
            return `
              &amp;lt;div style=&quot;font-weight: 500; color: #4DCB73; transform: translate(30px, -20px);&quot;&amp;gt;
              &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
              &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
              &amp;lt;/div&amp;gt;
            `;
          }
          return `
            &amp;lt;div style=&quot;font-weight: 500; color: #00BBFF; transform: translate(-36px, -20px);&quot;&amp;gt;
            &amp;lt;p style=&quot;font-size: 12px&quot;&amp;gt;${datum.label}&amp;lt;/p&amp;gt;
            &amp;lt;p style=&quot;font-size: 14px&quot;&amp;gt;${formatNumOfOptions(datum.value, { digit: 0 })}&amp;lt;/p&amp;gt;
            &amp;lt;/div&amp;gt;
          `;
        },
      })
      .style(&apos;lineWidth&apos;, 1)
      .style(&apos;lineDash&apos;, [7, 4])
      .style(&apos;fill&apos;, (d) =&amp;gt; {
        if (d.sets.length === 1) {
          if (d.sets[0] === &apos;A&apos;) {
            return &apos;#3BA0FF&apos;;
          }
          return &apos;#4DCB73&apos;;
        }
        return &apos;#cdecf2&apos;;
      })
      // 交汇处描边的线条颜色维持它们各自的颜色
      .style(&apos;stroke&apos;, (d) =&amp;gt; {
        if (d.key === &apos;A&apos;) {
          return &apos;#1890FF&apos;;
        }
        if (d.key === &apos;B&apos;) {
          return &apos;#4DCB73&apos;;
        }
        return &apos;transparent&apos;;
      })
      .style(&apos;fillOpacity&apos;, 0.1)
      .legend(false)
      .tooltip(false);
    chart.render();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>记录分享我使用的一些提升效率的工具</title><link>https://www.mihouo.com/posts/tool-share/sharing-and-reviewing-productivity-tools-i-use/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/sharing-and-reviewing-productivity-tools-i-use/</guid><description>分享工作生活中使用的提升效率的工具</description><pubDate>Fri, 29 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;截图&lt;/h2&gt;
&lt;p&gt;目前在使用 &lt;a href=&quot;https://www.snipaste.com/&quot;&gt;Snipaste&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;可以很方便的截图、贴图，并在图片上添加一些简单的指示图案和文本。&lt;/p&gt;
&lt;p&gt;对于我来说也有些不足之处，比如不能长截图，不能截图提取文字。&lt;/p&gt;
&lt;p&gt;前段时间发现了另一款截图工具 &lt;a href=&quot;https://pixpinapp.com/&quot;&gt;PinPin&lt;/a&gt; 可以解决我的需求，但是由于我使用的是 mac ，而这个软件暂时未推出 mac 版本。官方预计 2024 年 3月推出 mac 测试版，但是截止 2024 年 3 月 29 日暂未推出。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;翻译&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tisfeng/Easydict&quot;&gt;Easydict&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GitHub 开源软件，一个简洁优雅的词典翻译 macOS App。开箱即用，支持离线 OCR 识别，支持有道词典，🍎 苹果系统词典，🍎 苹果系统翻译，ChatGPT，Gemini，DeepL，Google，Bing，腾讯，百度，阿里，小牛，彩云和火山翻译。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它支持划词翻译、截图翻译，并支持配置多个翻译源。由于它支持OCR截图翻译，所以在需要截图提取文字时也可以很方便提取文字。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>记录分享我使用的一些 Ai 相关的工具</title><link>https://www.mihouo.com/posts/tool-share/sharing-and-reviewing-ai-tools-i-use/</link><guid isPermaLink="true">https://www.mihouo.com/posts/tool-share/sharing-and-reviewing-ai-tools-i-use/</guid><description>分享工作生活中使用的 AI 相关工具</description><pubDate>Mon, 18 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;代码&lt;/h2&gt;
&lt;p&gt;由于 GitHub Copilot 是订阅制，且价格比较昂贵，所以我一般使用一些价格很低的方案使用，比如 Github 学生包（截止 当前 2024 年 03 月 18 日 基本不可用），目前替代方案是上淘宝上车（价格一般 5 块钱一个月 40 左右一年，&lt;s&gt;稳定性暂时未知&lt;/s&gt;（稳定性很差，基本不可用））。&lt;/p&gt;
&lt;p&gt;2024-03-07在 &lt;a href=&quot;http://linux.do/&quot;&gt;linux.do&lt;/a&gt; 论坛拼车板块上了付费拼车（2024-04-23翻车）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;GitHub Copilot 不可用时，我通常是临时使用 &lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=aminer.codegeex&quot;&gt;**&lt;code&gt;CodeGeeX&lt;/code&gt;&lt;/a&gt; （免费，且效果较好）。**&lt;/p&gt;
&lt;p&gt;翻车时拼车车主也有推荐，我在去年 8 月份左右时使用过这个插件，当时的体验效果和GitHub Copilot 还有些差距，如今使用体验还是不错的。&lt;/p&gt;
&lt;p&gt;目前（2024 年 06 月 18 日，已经稳定使用近一月）又切换到了使用 GitHub Copilot。使用的是 GitHub 学生包权益。&lt;a href=&quot;https://az100.top&quot;&gt;购买地址&lt;/a&gt;、&lt;a href=&quot;https://t.me/az100github&quot;&gt;补货通知TG频道&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;问答&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://chat.openai.com/&quot;&gt;&lt;code&gt;ChatGPT&lt;/code&gt;&lt;/a&gt; &lt;s&gt;（官网3.5）&lt;/s&gt; （macOS ChatGPT 客户端 4o、超出限制后使用 3.5）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://www.coze.com&quot;&gt;&lt;code&gt;Coze&lt;/code&gt;&lt;/a&gt; （字节海外平台，免费使用 GPT-4）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.plusaigpt.com/&quot;&gt;&lt;code&gt;PlusAI&lt;/code&gt;&lt;/a&gt; （低价使用 GPT-4 Turbo (128k) ）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://uu.ci/&quot;&gt;&lt;strong&gt;&lt;code&gt;UU 免费接口&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;（api 调用 &lt;s&gt;每天签到 5 $&lt;/s&gt;（目前已经不能签到领取额度了，听说也要关站了））&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;写作&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.notion.so/product/ai&quot;&gt;&lt;code&gt;Notion AI&lt;/code&gt;&lt;/a&gt;（淘宝 8 元一月）&lt;/p&gt;
</content:encoded></item><item><title>setCookie 异步导致的问题案例</title><link>https://www.mihouo.com/posts/front/case-studies-on-issues-caused-by-asynchronous-setcookie-operations/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/case-studies-on-issues-caused-by-asynchronous-setcookie-operations/</guid><description>前端异步 setCookie 操作导致的逻辑错误及解决方案</description><pubDate>Fri, 15 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近有个新需求，用户登录后如果是黑名单用户的话，则不允许访问页面，并弹出提示。&lt;/p&gt;
&lt;p&gt;由于这个项目的登录并非传统的用户名、密码登录，而是通过京东服务市场订阅软件，通过按钮重定向跳转并配合 &lt;code&gt;setCookie&lt;/code&gt; 操作登录。详情如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里一开始和后端商量的方案是判断 &lt;code&gt;cookie&lt;/code&gt; 的 &lt;code&gt;token&lt;/code&gt; 字段是否存在，如果不存在则为黑名单用户，然后前端再处理打开遮罩并提示用户。&lt;/p&gt;
&lt;p&gt;在这种情况下，由于&lt;code&gt;setCookie&lt;/code&gt;是异步操作，可能会导致以下问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;逻辑错误&lt;/strong&gt;：由于&lt;code&gt;setCookie&lt;/code&gt;是异步的，可能会导致在判断黑名单用户之前 &lt;code&gt;cookie&lt;/code&gt; 还未被设置，导致判断逻辑错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户体验问题&lt;/strong&gt;：用户可能会在 cookie 还未被设置的情况下就看到页面内容，然后才收到黑名单提示，这样会导致用户体验不佳。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不稳定性&lt;/strong&gt;：异步操作可能会导致不稳定的行为，因为异步操作的完成时间是不确定的，无法保证每次都能如期完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了解决这个问题，你可以考虑以下方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Loading 界面&lt;/strong&gt;：在设置 &lt;code&gt;cookie&lt;/code&gt; 期间显示一个 &lt;code&gt;Loading&lt;/code&gt; 界面，确保在 cookie 设置完毕前用户无法看到页面内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟执行&lt;/strong&gt;：设置一个定时器，在一定时间后再检查 &lt;code&gt;cookie&lt;/code&gt; 是否设置成功，如果还未设置成功，则提示用户。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;与后端沟通&lt;/strong&gt;：与后端进行合作，讨论是否可以在登录成功后返回一个标识，而不是依赖于 &lt;code&gt;cookie&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端检测&lt;/strong&gt;：后端先提前检测是否是黑名单用户，如果是的话则重定向到 &lt;code&gt;/black&lt;/code&gt; 路由&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终我们选择了方案 &lt;strong&gt;4&lt;/strong&gt;！通过在后端进行黑名单用户检测，可以避免前端异步操作导致的问题，确保用户体验和逻辑的准确性。&lt;/p&gt;
&lt;p&gt;在这个方案中，后端负责判断用户是否为黑名单用户，并且做出相应的跳转，前端则根据后端的响应进行重定向并传递参数，以此来实现对黑名单用户的处理。这样可以简化前端的处理逻辑，确保页面重定向和参数传递的准确性。&lt;/p&gt;
</content:encoded></item><item><title>吐槽项目中下载接口的请求封装</title><link>https://www.mihouo.com/posts/front/rants-on-wrapping-download-requests-in-projects/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/rants-on-wrapping-download-requests-in-projects/</guid><description>项目中下载接口请求封装的痛点与坑，提供优化建议和解决方案</description><pubDate>Fri, 15 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前两天有个需求，页面中有个导出的按钮，需要导出报表，一开始后端给出的接口请求方式是 get 方式，但是考虑到未来可能请求的入参条件会非常多，可能会超过 url 长度最大值，所以将请求方式修改成了 post 请求。&lt;/p&gt;
&lt;p&gt;但是将请求方式修改后发现，接口报了异常，查看原因是因为入参格式，在项目中发现这个项目将所有下载相关的接口的入参都改成了 formData 格式的，而后端并没有以此格式接收导致的。&lt;/p&gt;
&lt;p&gt;处理下载请求配置方法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const download = (requestUrl, method, instanceName, data) =&amp;gt; {
  const formData = new FormData();
  if (data.bodyParams) {
    Object.keys(data.bodyParams).forEach((key) =&amp;gt; {
      formData.append(key, data.bodyParams[key]);
    });
  } else {
    Object.keys(data).forEach((key) =&amp;gt; {
      formData.append(key, data[key]);
    });
  }

  const req = {
    url: formatDownloadRequestV2(requestUrl, data),
    method: method.toUpperCase(),
    data: formData,
    headers: {
      &apos;Content-Type&apos;: &apos;multipart/form-data&apos;,
      filename: &apos;utf-8&apos;,
    },
    responseType: &apos;blob&apos;,
    baseURL: config[`${instanceName ?? &apos;base&apos;}URL`],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在修改这个问题是又发现了另外一个可能存在的问题 就是 formatDownloadRequestV2 方法处理 url 时，如果是 get 请求并含有参数时，它会忽略它的参数，函数代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formatDownloadRequestV2 = (requestUrl, data) =&amp;gt; {
  // 组织请求参数
  let url = requestUrl;
  if (Object.prototype.hasOwnProperty.call(data, &apos;pathParams&apos;)) {
    Object.keys(data.pathParams).forEach((key) =&amp;gt; {
      url = requestUrl.replace(`{${key}}`, data.pathParams[key]);
    });
  }
  if (Object.prototype.hasOwnProperty.call(data, &apos;urlParams&apos;)) {
    Object.keys(data.urlParams).forEach((key, index) =&amp;gt; {
      url = index === 0 ? (url += `?${key}=${data.urlParams[key]}`) : (url += `&amp;amp;${key}=${data.urlParams[key]}`);
    });
  }
  if (Object.prototype.hasOwnProperty.call(data, &apos;bodyParams&apos;)) {
    return requestUrl;
  }
  return url;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总得来说有两处需要修改的&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不将所有的请求的参数都修改为 formData 格式&lt;/li&gt;
&lt;li&gt;修改 formatDownloadRequestV2 函数，使其正常处理 get 请求的 url&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于修改点 1 的修改如下：&lt;/p&gt;
&lt;p&gt;我们项目中的接口定义通常如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // 主播绩效-下载对比数据
  downloadCompareDate: {
    url: &apos;/anchorPerformance/comparison/export&apos;,
    method: &apos;POST&apos;,
    instanceName: &apos;live&apos;,
    isDownload: true,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先在接口定义中加入 noFormData 配置参数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  // 主播绩效-下载对比数据
  downloadCompareDate: {
    noFormData: true,
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后修改 download 方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 添加 形参 noFormData
const download = (requestUrl, method, instanceName, data, noFormData) =&amp;gt; {
  const req = {
    url: formatDownloadRequestV2(requestUrl, data, method.toUpperCase()),
    method: method.toUpperCase(),
    // 如果不需要转成 formData 格式 则直接 将 data 传入
    data: noFormData ? data : formData,
    headers: {
      &apos;Content-Type&apos;: noFormData ? &apos;application/json&apos; : &apos;multipart/form-data&apos;,
      filename: &apos;utf-8&apos;,
    },
    responseType: &apos;blob&apos;,
    baseURL: config[`${instanceName ?? &apos;base&apos;}URL`],
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于修改点 2 的修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const formatDownloadRequestV2 = (requestUrl, data, method) =&amp;gt; {
  // 组织请求参数
  let url = requestUrl;
  // 添加一个标识 判断是否走入下面三个 if 的逻辑中
  let flag = true;
  if (Object.prototype.hasOwnProperty.call(data, &apos;pathParams&apos;)) {
    flag = false;
    Object.keys(data.pathParams).forEach((key) =&amp;gt; {
      url = requestUrl.replace(`{${key}}`, data.pathParams[key]);
    });
  }
  if (Object.prototype.hasOwnProperty.call(data, &apos;urlParams&apos;)) {
    flag = false;
    Object.keys(data.urlParams).forEach((key, index) =&amp;gt; {
      url = index === 0 ? (url += `?${key}=${data.urlParams[key]}`) : (url += `&amp;amp;${key}=${data.urlParams[key]}`);
    });
  }
  if (Object.prototype.hasOwnProperty.call(data, &apos;bodyParams&apos;)) {
    flag = false;
    return requestUrl;
  }
  // 修改 get 请求的 url
  if (flag &amp;amp;&amp;amp; method === &apos;GET&apos;) {
    url += formatURL(data);
  }
  return url;
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>React setState 批处理机制引发的问题案例</title><link>https://www.mihouo.com/posts/front/case-studies-on-issues-caused-by-reacts-setstate-batching-mechanism/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/case-studies-on-issues-caused-by-reacts-setstate-batching-mechanism/</guid><description>本文分析了在React开发中，由于setState批处理机制引发的一些常见问题。通过具体案例展示这些问题的出现原因，并提供相应的解决方案，帮助开发者更好地理解和处理这些问题。</description><pubDate>Thu, 14 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;React 的 &lt;strong&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt; 钩子用于在函数组件中添加状态。当你调用由 &lt;strong&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt; 返回的设置状态函数（通常命名为 &lt;strong&gt;&lt;code&gt;setState&lt;/code&gt;&lt;/strong&gt;）时，React 会安排一次组件的更新。但是，这些更新并不总是立即执行。React 有一个批处理（batching）机制，用于合并多个状态更新，从而减少不必要的重新渲染，提高应用性能。这个机制的工作原理和其对开发的影响是开发者需要理解的重要概念。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;批处理机制的工作原理&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;事件处理中的批处理&lt;/strong&gt;：在 React 事件处理器内部触发的状态更新（如用户点击按钮时触发的事件）会自动批处理。这意味着如果你在一个事件处理函数中连续调用多次 &lt;strong&gt;&lt;code&gt;setState&lt;/code&gt;&lt;/strong&gt;，React 会将这些更新合并为一次更新，并只触发一次重新渲染。这种批处理发生在 React 控制的事件处理函数中，如 &lt;strong&gt;&lt;code&gt;onClick&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;onChange&lt;/code&gt;&lt;/strong&gt; 等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步操作中的批处理&lt;/strong&gt;：React 18 引入了自动批处理，这意味着即使在异步操作（如 &lt;strong&gt;&lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt;、&lt;strong&gt;&lt;code&gt;Promise.then&lt;/code&gt;&lt;/strong&gt; 或者任何不是 React 事件处理器触发的异步回调）中的状态更新也会被批处理。在 React 17 及之前版本，只有 React 控制的事件处理函数中的状态更新才会自动批处理，而异步操作中的状态更新不会被批处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动批处理&lt;/strong&gt;：如果你需要在不自动批处理的情况下强制批处理状态更新（例如，在 React 17 或更早版本的异步代码中），可以使用 &lt;strong&gt;&lt;code&gt;ReactDOM.unstable_batchedUpdates()&lt;/code&gt;&lt;/strong&gt; 函数。通过将更新封装在 &lt;strong&gt;&lt;code&gt;unstable_batchedUpdates&lt;/code&gt;&lt;/strong&gt; 的回调中，即使在异步操作中也可以实现批处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;批处理的影响&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能提升&lt;/strong&gt;：通过减少不必要的渲染次数，批处理机制可以显著提高应用的性能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态更新的合并&lt;/strong&gt;：在批处理期间，React 会合并状态更新。如果你多次更新同一个状态，React 会以最后一次更新为准。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步性质&lt;/strong&gt;：&lt;strong&gt;&lt;code&gt;setState&lt;/code&gt;&lt;/strong&gt; 调用是异步的，React 会在稍后某个时间点应用状态更新。因此，立即读取状态更新后的值可能不会反映最新的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;案例&lt;/h2&gt;
&lt;p&gt;假设有一个页面，它有两个子组件，在页面初始化时需要请求接口，其中两个入参分别是这两个子组件通过事件抛出来的，伪代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React, { useState, useEffect } from &apos;react&apos;;

const ChildrenOne = (props) =&amp;gt; {
  useEffect(() =&amp;gt; {
    setTimeout(() =&amp;gt; {
      props.onSendMsg(&apos;one&apos;);
    }, Math.random() * 3 * 1000);
  }, []);
  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;;
};
const ChildrenTwo = (props) =&amp;gt; {
  useEffect(() =&amp;gt; {
    setTimeout(() =&amp;gt; {
      props.onSendMsg(&apos;two&apos;);
    }, Math.random() * 3 * 1000);
  }, []);
  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;;
};

export default () =&amp;gt; {
  const [params, setParams] = useState({
    msg1: &apos;&apos;,
    msg2: &apos;&apos;,
  });
  const onSearch = () =&amp;gt; {
    console.log(params);
  };
  const onEmitOne = (msg: string) =&amp;gt; {
    setParams({
      ...params,
      msg1: msg,
    });
  };
  const onEmitTwo = (msg: string) =&amp;gt; {
    setParams({
      ...params,
      msg2: msg,
    });
  };
  useEffect(() =&amp;gt; {
    // 需要在 msg1 和 msg2 都 ready 之后再执行
    if (params.msg1 &amp;amp;&amp;amp; params.msg2) {
      onSearch();
    }
  }, [params]);
  return (
    &amp;lt;&amp;gt;
      &amp;lt;ChildrenOne onSendMsg={onEmitOne} /&amp;gt;
      &amp;lt;ChildrenTwo onSendMsg={onEmitTwo} /&amp;gt;
    &amp;lt;/&amp;gt;
  );
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时我们会发现&lt;code&gt;onSearch&lt;/code&gt; 函数始终不会触发，这是因为&lt;code&gt;setParams&lt;/code&gt;函数调用是异步的，而且每次调用都基于旧的&lt;code&gt;params&lt;/code&gt;状态。在&lt;code&gt;onEmitOne&lt;/code&gt;和&lt;code&gt;onEmitTwo&lt;/code&gt;函数中，&lt;code&gt;params&lt;/code&gt;的值在函数开始执行时就被确定了，不会因为状态更新而改变。因此，当&lt;code&gt;onEmitOne&lt;/code&gt;和&lt;code&gt;onEmitTwo&lt;/code&gt;函数几乎同时被调用时，它们获取到的&lt;code&gt;params&lt;/code&gt;状态都是初始状态。这就导致了即使两个函数都调用了&lt;code&gt;setParams&lt;/code&gt;，最后的结果也只会包含其中一个函数的更新。为了解决这个问题，你应该使用&lt;code&gt;setParams&lt;/code&gt;的函数形式来更新状态，这样每次更新都会基于最新的状态进行。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，你可以对 &lt;code&gt;setParams&lt;/code&gt; 使用函数形式的调用，这种形式的调用会包含最新的状态。如下代码所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const onEmitOne = (msg: string) =&amp;gt; {
  setParams(prevParams =&amp;gt; ({
    ...prevParams,
    msg1: msg,
  }));
};
const onEmitTwo = (msg: string) =&amp;gt; {
  setParams(prevParams =&amp;gt; ({
    ...prevParams,
    msg2: msg,
  }));
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，无论 &lt;code&gt;onEmitOne&lt;/code&gt; 和 &lt;code&gt;onEmitTwo&lt;/code&gt; 函数何时被调用，&lt;code&gt;setParams&lt;/code&gt; 都会基于最新的 &lt;code&gt;params&lt;/code&gt; 状态进行更新，从而确保 &lt;code&gt;onSearch&lt;/code&gt; 函数可以被正确触发。&lt;/p&gt;
&lt;p&gt;这时 &lt;code&gt;onSearch&lt;/code&gt; 函数虽然能正常触发，但是在之后还存在其他入参的改变导致频繁的接口请求，而我这里的需求是只在页面初始化时进行。&lt;/p&gt;
&lt;p&gt;这时我的解决方案是讲&lt;code&gt;onEmitOne&lt;/code&gt; 和 &lt;code&gt;onEmitTwo&lt;/code&gt; 改造成异步函数，使用 &lt;code&gt;Promise&lt;/code&gt; 将结果传递出去，这时在新创建一个&lt;code&gt;getInitData&lt;/code&gt; 函数只在页面初始化时调用，这个函数的主要功能是拿到&lt;code&gt;onEmitOne&lt;/code&gt; 和 &lt;code&gt;onEmitTwo&lt;/code&gt; 返回的两个参数，将参数传递给&lt;code&gt;onSearch&lt;/code&gt; 函数，并执行。这时也需要将 &lt;code&gt;onSearch&lt;/code&gt; 函数改造能能接收入参的形式，实现代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default () =&amp;gt; {
  const [params, setParams] = useState({
    msg1: &apos;&apos;,
    msg2: &apos;&apos;,
  });
  const onSearch = (_param?: { msg1: string; msg2: string }) =&amp;gt; {
    const { msg1, msg2 } = _param ?? {};
    const param = {
      msg1: msg1 ?? params.msg1,
      msg2: msg2 ?? params.msg2,
    };
    setParams({
      ...params,
      ...param,
    });
    console.log(param);
  };
  const onEmitOne = async (msg: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
    return new Promise((resolve) =&amp;gt; {
      resolve(msg);
    });
  };
  const onEmitTwo = async (msg: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
    return new Promise((resolve) =&amp;gt; {
      resolve(msg);
    });
  };
  const getInitData = async () =&amp;gt; {
    const res = await Promise.all([onEmitOne(&apos;one&apos;), onEmitTwo(&apos;two&apos;)]);
    onSearch({
      msg1: res[0],
      msg2: res[1],
    });
  };
  useEffect(() =&amp;gt; {
    getInitData();
  }, []);
  return &amp;lt;&amp;gt;&amp;lt;/&amp;gt;;
};

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>罗技项目作为子系统嵌入的实现</title><link>https://www.mihouo.com/posts/front/implementing-logitech-projects-as-embedded-subsystems/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/implementing-logitech-projects-as-embedded-subsystems/</guid><description>将 Angular 项目改造成子系统嵌入，使用微前端技术实现集成</description><pubDate>Fri, 12 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;为什么要改造成为子系统&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;由于现在的罗技项目采用Angular框架进行开发的，且该项目Bug非常多，不仅如此，考虑到未来罗技有开发二期的需求，加上目前前端团队对于Angular框架都不太熟悉。基于以上原因最合适的方案是把这个项目改造成子系统，未来二期在另个子系统使用前端团队都熟悉的React框架进行开发。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;微前端技术选型&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/ice-lab/icestark&quot;&gt;icestark&lt;/a&gt; 是一个面向大型系统的微前端解决方案，适用于以下业务场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后台比较分散，体验差别大，因为要频繁跳转导致操作效率低，希望能统一收口的一个系统内&lt;/li&gt;
&lt;li&gt;单页面应用非常庞大，多人协作成本高，开发/构建时间长，依赖升级回归成本高&lt;/li&gt;
&lt;li&gt;系统有二方/三方接入的需求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;icestark 在保证一个系统的操作体验基础上，实现各个微应用的独立开发和发版，主应用通过 icestark 管理微应用的注册和渲染，将整个系统彻底解耦。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;整体设计&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;罗技项目嵌入实现.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;主应用设计&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;使用2023年年中时重构的罗技项目将其改造成主应用，主要在这里实现登陆、权限、布局、菜单等一些全局配置&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;技术选型&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/learn&quot;&gt;React&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.typescriptlang.org/&quot;&gt;TypeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cn.vitejs.dev/&quot;&gt;Vite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ant.design/index-cn&quot;&gt;Ant Design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;接入&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;安装 &lt;strong&gt;@ice/stark&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;npm i --save @ice/stark
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注册子应用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import { AppRouter, AppRoute } from &apos;@ice/stark&apos;;

function Layout() {
  return (
    &amp;lt;div className=&quot;Layout&quot;&amp;gt;
      &amp;lt;AppRouter&amp;gt;
        &amp;lt;AppRoute
          activePath=&quot;/logitech&quot;
          title=&quot;罗技angular一期&quot;
          entry=&quot;http://127.0.0.1:4200&quot;
          /&amp;gt;
        &amp;lt;div style={{ padding: 20 }}&amp;gt;
          &amp;lt;Outlet /&amp;gt;
        &amp;lt;/div&amp;gt;
      &amp;lt;/AppRouter&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
export default Layout;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;子应用设计（一期Angular项目）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;使用 &lt;a href=&quot;https://github.com/ice-lab/icestark&quot;&gt;icestark&lt;/a&gt; 将罗技项目改造成子系统&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接入&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;根据官方给出的&lt;a href=&quot;https://micro-frontends.ice.work/docs/guide/use-child/others#angular-%E5%BA%94%E7%94%A8&quot;&gt;Angular项目接入流程&lt;/a&gt;操作&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;按照上方的操作正常接入并在主应用上添加对应的菜单，会发现有两个问题&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;图片、字体、图标等静态资源加载失败&lt;/li&gt;
&lt;li&gt;通过菜单跳转切换页面不成功&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;静态资源加载失败问题解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过浏览器控制台-&amp;gt;网络中可以看到，出现这个问题的原因是因为加载这些静态资源的路径不正确，它错误的以主应用为基准进行了请求&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.png&quot; alt=&quot;罗技项目嵌入.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;尝试的解决手段&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;配置webpack的publicPath属性（未解决）&lt;/li&gt;
&lt;li&gt;配置index.html的base标签的href属性（未解决）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以解决的方案&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将项目中的静态资源上传到OSS对象存储中，在修改引用方式（可解决，但是项目中的静态资源非常多，修改起来非常耗时）&lt;/li&gt;
&lt;li&gt;修改项目中的地址，改为绝对路径&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终选择方案2进行修改，具体修改如下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于模版中的静态资源使用自定义创建的管道进行修改&lt;/li&gt;
&lt;li&gt;对于组件中的静态资源使用自定义全局函数进行修改&lt;/li&gt;
&lt;li&gt;对于样式文件中的静态资源使用scss变量拼接控制修改&lt;/li&gt;
&lt;li&gt;对于使用到的 Ant Design of Angular 组件库的icon采用全局导入则能正常显示&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;模版&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建通道&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在src-&amp;gt;app文件夹下创建pipe文件夹，并创建 &lt;strong&gt;asset-url.pipe.ts&lt;/strong&gt; 文件，内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Pipe, PipeTransform } from &apos;@angular/core&apos;;
import { assetUrl } from &apos;@app/utils&apos;

@Pipe({
  name: &apos;assetUrl&apos;
})
export class AssetUrlPipe implements PipeTransform {

  transform(value: string, ...args: unknown[]): unknown {
    return assetUrl(value);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;导入的 assetUrl 函数在解决组件的静态资源问题时有介绍&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;创建 SharedModule&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在src-&amp;gt;app文件夹下创建shared文件夹，并创建 &lt;strong&gt;shared.module.ts&lt;/strong&gt; 文件，内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { NgModule } from &apos;@angular/core&apos;;
import { CommonModule } from &apos;@angular/common&apos;;
import { AssetUrlPipe } from &apos;../pipe/asset-url.pipe&apos;;  // 更新为 assetUrl 管道的实际路径

@NgModule({
  declarations: [AssetUrlPipe],
  imports: [
    CommonModule
  ],
  exports: [AssetUrlPipe]  // 别忘了导出
})
export class SharedModule { }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在模版用使用管道&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;进入需要使用管道的页面的module中，导入SharedModule&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { SharedModule } from &quot;@app/shared/shared.module&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在模版中使用管道&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img [src]=&quot;&apos;/assets/images/1520.png&apos; | assetUrl&quot; alt=&quot;&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;组件&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建全局函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在src-&amp;gt;app文件夹下创建utils文件夹，并创建 &lt;strong&gt;index.ts&lt;/strong&gt; 文件，内容如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function assetUrl(url: string): string {
    // @ts-ignore
    const publicPath = __webpack_public_path__;
    const publicPathSuffix = publicPath.endsWith(&apos;/&apos;) ? &apos;&apos; : &apos;/&apos;;
    const urlPrefix = url.startsWith(&apos;/&apos;) ? &apos;&apos; : &apos;/&apos;;
    return `${publicPath}${publicPathSuffix}${urlPrefix}${url}`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用全局函数&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;进入需要使用管道的页面的component中，导入这个assetUrl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { assetUrl } from &apos;@app/utils&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用assetUrl&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const list = [
  {
    icon: assetUrl(&apos;assets/images/salesYoy.png&apos;),
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;样式文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在样式文件下新增对应的地址，并使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$assets-dev: &apos;http://127.0.0.1:4200/assets&apos;;

.toFullScreen{
  background:url(#{$assets-content-dev}/images/fullscreen.jpeg);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Icon&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as AllIcons from &apos;@ant-design/icons-angular/icons&apos;;

const antDesignIcons = AllIcons as {
  [key: string]: IconDefinition;
};
const icons: IconDefinition[] = Object.keys(antDesignIcons).map(key =&amp;gt; antDesignIcons[key])

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    NzIconModule.forRoot(icons)
  ]
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;效果展示&lt;/h3&gt;
&lt;p&gt;图片都加载成功&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.png&quot; alt=&quot;罗技项目嵌入图.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;菜单跳转解决方案&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;未知原因导致的路由跳转失败调试中发现路由第一次跳转 NavigationCancel 时中断并保留在当前页面，报错信息如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;id&quot;: 3,
    &quot;url&quot;: &quot;/businessOverview&quot;,
    &quot;reason&quot;: &quot;Navigation ID 3 is not equal to the current navigation id 4&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路由跳转触发的完整事件流&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NavigationStart&lt;/strong&gt;: 在路由开始导航时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RouteConfigLoadStart&lt;/strong&gt;: 在 Router 对一个异步路由进行懒加载时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RouteConfigLoadEnd&lt;/strong&gt;: 在路由配置加载完成时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RoutesRecognized&lt;/strong&gt;: 在 Router 解析完 URL，并识别出了相应的一组针对该 URL 的路由配置时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GuardsCheckStart&lt;/strong&gt;: 在 Router 开始运行 CanActivate 和 CanDeactivate 守卫时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChildActivationStart&lt;/strong&gt;: 当 Router 开始激活某个路由的子路由时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ActivationStart&lt;/strong&gt;: 当 Router 开始激活某个路由时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GuardsCheckEnd&lt;/strong&gt;: 当 Router 完成运行 CanActivate 和 CanDeactivate 守卫时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ResolveStart&lt;/strong&gt;: 当 Router 开始解析路由时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ResolveEnd&lt;/strong&gt;: 当路由解析完成时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ChildActivationEnd&lt;/strong&gt;: 当 Router 完成激活某个路由的子路由时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ActivationEnd&lt;/strong&gt;: 当 Router 完成激活某个路由时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NavigationEnd&lt;/strong&gt;: 在路由导航结束时触发。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NavigationCancel&lt;/strong&gt;: 在路由导航被取消时触发（这可能是由于一个 guard 返回了 false）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NavigationError&lt;/strong&gt;: 在路由导航失败时触发。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果在此基础上重新进入这个路由则能正常进入，通过这个机制，可以在路由跳转时使用setTimeout延时再进入一次，这时路由正确进入，但是页面并没有正常渲染(前一页面的内容仍然在页面上，当前页面的内容在这下方渲染，或着干脆就没有渲染)，这时可以通过让页面刷新解决，但是会牺牲一些用户体验&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;navigate(`/${menuInfo.key}`);
// 如果路径以/logitech/开头，就加载logitech的子应用
if (menuInfo.key.startsWith(&apos;logitech/&apos;)) {
  setTimeout(() =&amp;gt; {
    navigate(`/${menuInfo.key}`);
    window.location.reload();
  }, 50);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;strong&gt;子应用设计（二期React项目）&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;直接新建React项目即可&lt;/p&gt;
&lt;h2&gt;最终效果展示&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./4.png&quot; alt=&quot;罗技项目嵌入 (1).png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5.png&quot; alt=&quot;罗技项目嵌入实现 (1).png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>利用font-face实现前端反爬虫</title><link>https://www.mihouo.com/posts/front/implementing-front-end-anti-scraping-with-font-face/</link><guid isPermaLink="true">https://www.mihouo.com/posts/front/implementing-front-end-anti-scraping-with-font-face/</guid><description>通过 font-face 技术实现前端反爬虫，保护网站内容</description><pubDate>Wed, 22 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;什么是爬虫&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;网络爬虫，是一个自动提取网页的程序，它为搜索引擎从万维网上下载网页，是搜索引擎的重要组成。&lt;/p&gt;
&lt;p&gt;爬取数据主要分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从渲染好的 html 页面直接找到感兴趣的节点，然后获取对应的文本&lt;/li&gt;
&lt;li&gt;去分析对应的接口数据，更加方便、精确地获取数据&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;为什么需要反爬虫？&lt;/strong&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;网络爬虫会根据特定策略尽可能多的“爬过”网站中的高价值信息，占用服务器带宽，增加服务器的负载&lt;/li&gt;
&lt;li&gt;恶意利用网络爬虫对Web服务发动DoS攻击，可能使Web服务资源耗尽而不能提供正常服务&lt;/li&gt;
&lt;li&gt;恶意利用网络爬虫将免费查询的资源批量抓走，各种敏感信息，造成网站的核心数据被窃取，导致公司丧失竞争力 。&lt;/li&gt;
&lt;li&gt;状告爬虫成功的几率小，爬虫在国内还是个擦边球，就是有可能可以起诉成功，也可能完全无效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;strong&gt;制定出 Web 端反爬技术方案&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;从这2个角度（网页所见非所得、查接口请求没用）出发，制定了下面的反爬方案。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 HTTPS 协议&lt;/li&gt;
&lt;li&gt;登陆态下，单位时间内限制掉请求次数过多（等级1），则降低频率给账号返回数据&lt;/li&gt;
&lt;li&gt;登陆态下，单位时间内限制掉请求次数过多（等级2），则返回错误的数据给该账号&lt;/li&gt;
&lt;li&gt;登陆态下，单位时间内限制掉请求次数过多（等级3），则封锁该账号&lt;/li&gt;
&lt;li&gt;前端技术限制 （接下来是核心技术）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该方案也可以覆盖 OCR 爬取场景。OCR 的前提是页面渲染完毕，页面所需业务数据需要通过接口获取。所以基于用户行为采集分析，基于日志分析用户在时间范围内的请求频次、用户行为是否正常，如果不正常，说明可能是爬虫程序，依据用户单位时间内情况恶略程度，可以采用降频、返回错误数据、封锁账号的策略。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;服务端加密&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;首先后端与前端约定好数据加密规则&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;随机映射表（比如 0 -&amp;gt; 3, 1 -&amp;gt; 7, 2 -&amp;gt; 4, 3 -&amp;gt; 5, 4 -&amp;gt; 1, 5 -&amp;gt; 0, 6 -&amp;gt; 6, 7 -&amp;gt; 9, 8 -&amp;gt; 2, 9 -&amp;gt; 8）&lt;/li&gt;
&lt;li&gt;约定好的规则加密（根据方程 y = kx + b 其中，k 为当前的月份，b 为当月的号数，线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2023-11-22”，那么线性变换的 k 为 11，b 为 22。）&lt;/li&gt;
&lt;li&gt;将数字转换为字符串，使用3.1415926拼接（比如 数字123 变为1 + &apos;3.1415926&apos; + 2 + &apos;3.1415926&apos; + 3，最终结果为&apos;13.141592623.141592623.1415926&apos;）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;&lt;strong&gt;客户端解密&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;将后端返回的数据以3.1415926为分隔符转换成数组&lt;/li&gt;
&lt;li&gt;将数据解密 &lt;em&gt;&lt;strong&gt;x = (y - b) / k&lt;/strong&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;将数组转换成字符串&lt;/li&gt;
&lt;li&gt;给数据添加对应的字体文件（根据服务端的映射制作的）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;&lt;strong&gt;实现效果&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;接口定义&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1.png&quot; alt=&quot;前端反爬虫.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接口返回的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2.webp&quot; alt=&quot;xx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dom结构中的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3.webp&quot; alt=&quot;xx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;页面中显示的数据&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./4.webp&quot; alt=&quot;xx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;实现过程&lt;/strong&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;服务端&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;以node.js为例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const encodeNumberRule1 = (number) =&amp;gt; {
  const numMap = {
    0: 3,
    1: 7,
    2: 4,
    3: 5,
    4: 1,
    5: 0,
    6: 6,
    7: 9,
    8: 2,
    9: 8,
  };
  const date = dayjs().format(&apos;YYYY-MM-DD&apos;);
  const k = Number(date.split(&apos;-&apos;)[1]);
  const b = Number(date.split(&apos;-&apos;)[2]);
  const result = String(number).split(&apos;&apos;).map((item) =&amp;gt; {
    if (isNaN(item)) {
      return item;
    }
    return numMap[item] * k + b;
  }).join(&apos;3.1415926&apos;);
  return result;
};

function encodeNumberRule2(number) {
  // numMap 为随机16进制映射表
  const numMap = {
    0: 0xe6a7,
    1: 0xf257,
    2: 0xa87e,
    3: 0xb2c1,
    4: 0xc7b2,
    5: 0xa9f2,
    6: 0xe2c7,
    7: 0xf82b,
    8: 0xc1b2,
    9: 0xc8f6,
  };

  const date = dayjs().format(&apos;YYYY-MM-DD&apos;);
  const k = Number(date.split(&apos;-&apos;)[1]);
  const b = Number(date.split(&apos;-&apos;)[2]);
  const result = String(number).split(&apos;&apos;).map((item) =&amp;gt; {
    if (item === &apos;.&apos; || item === &apos;-&apos;) {
      return item;
    }
    return (numMap[item] * k + b).toString(16);
  }).join(&apos;3.1415926&apos;);
  console.log(&apos;result&apos;, result);
  return result.toString();
}

app.get(&apos;/api/test&apos;, (req, res) =&amp;gt; {
  res.send({
    code: 200,
    msg: &apos;test&apos;,
    data: [
      {
        x: encodeNumberRule1(123),
        y: encodeNumberRule1(-123.2),
        rule: 1,
      },
      {
        x: encodeNumberRule2(123),
        y: encodeNumberRule2(-123.2),
        rule: 2,
      }
    ],
    success: true,
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;客户端&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;制作字体文件&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将字体文件转换成svg格式（&lt;a href=&quot;https://convertio.co/zh/&quot;&gt;ttf转svg&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;选取对应的字符，根据与后端约定的映射表修改unicode编码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./5.webp&quot; alt=&quot;xx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6.webp&quot; alt=&quot;xx&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载改完unicode后的字体并转为woff与woff2，在css中设置对应字体（&lt;a href=&quot;https://transfonter.org/&quot;&gt;ttf转woff&lt;/a&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@font-face {
  font-family: &apos;icomoon-num-1&apos;;
  src: url(&apos;./assets/font/icomoon-num-1/icomoon.woff2&apos;) format(&apos;woff2&apos;),
    url(&apos;./assets/font/icomoon-num-1/icomoon.woff&apos;) format(&apos;woff&apos;);
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: &apos;icomoon-num-2&apos;;
  src: url(&apos;./assets/font/icomoon-num-2/icomoon-num-2.woff2&apos;) format(&apos;woff2&apos;),
    url(&apos;./assets/font/icomoon-num-2/icomoon-num-2.woff&apos;) format(&apos;woff&apos;);
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

.num-font-rule-1 {
  font-family: &apos;icomoon-num-1&apos;, sans-serif;
}
.num-font-rule-2 {
  font-family: &apos;icomoon-num-2&apos;, sans-serif;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;编写解密函数&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const decryptNumRule1 = (num) =&amp;gt; {
  if (!num) {
    return &apos;&apos;;
  }
  // 1. 将字符串转换成数组以3.1415926为分隔符
  const arr = num.split(&apos;3.1415926&apos;);
  const date = dayjs().format(&apos;YYYY-MM-DD&apos;);
  const k = Number(date.split(&apos;-&apos;)[1]);
  const b = Number(date.split(&apos;-&apos;)[2]);
  // 2. 将数组中的每一项转换成数字 x = (y - b) / k
  const result = arr.map(item =&amp;gt; {
    if (isNaN(Number(item))) {
      return item;
    }
    const y = Number(item);
    return (y - b) / k;
  });
  // 3. 将数组转换成字符串
  return result.join(&apos;&apos;);
};

const decryptNumRule2 = (num) =&amp;gt; {
  if (!num) {
    return &apos;&apos;;
  }
  // 1. 将字符串转换成数组以3.1415926为分隔符
  const arr = num.split(&apos;3.1415926&apos;);
  const date = dayjs().format(&apos;YYYY-MM-DD&apos;);
  const k = Number(date.split(&apos;-&apos;)[1]);
  const b = Number(date.split(&apos;-&apos;)[2]);
  // 2. 将数组中的每一项转换成数字 x = (y - b) / k
  const result = arr.map(item =&amp;gt; {
    if (isNaN(parseInt(item, 16))) {
      console.log(item);
      return item;
    }
    const y = parseInt(item, 16);
    return `&amp;amp;#x${((y - b) / k).toString(16)};`;
  });
  // 3. 将数组转换成字符串
  return result.join(&apos;&apos;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;开发中使用&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意： 在使用四位数16进制的unicode编码时需要使用dangerouslySetInnerHTML渲染，否则将会在页面中直接显示对应的字符串，对应Vue中需要使用v-html指令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function Test() {
  return &amp;lt;&amp;gt;
    {
      testData.map((item, index) =&amp;gt; {
        return &amp;lt;div key={index}&amp;gt;
          {
            item.rule === 1 &amp;amp;&amp;amp; &amp;lt;&amp;gt;
              &amp;lt;p className=&apos;num-font-rule-1&apos;&amp;gt;{utils.decryptNumRule1(item.x)}&amp;lt;/p&amp;gt;
              &amp;lt;p className=&apos;num-font-rule-1&apos;&amp;gt;{utils.decryptNumRule1(item.y)}&amp;lt;/p&amp;gt;
            &amp;lt;/&amp;gt;
          }
          {
            item.rule === 2 &amp;amp;&amp;amp; &amp;lt;&amp;gt;
              &amp;lt;p className=&apos;num-font-rule-2&apos; dangerouslySetInnerHTML={{
                __html: utils.decryptNumRule2(item.x),
              }} /&amp;gt;
              &amp;lt;p className=&apos;num-font-rule-2&apos; dangerouslySetInnerHTML={{
                __html: utils.decryptNumRule2(item.y),
              }} /&amp;gt;
            &amp;lt;/&amp;gt;
          }
          &amp;lt;br /&amp;gt;
        &amp;lt;/div&amp;gt;;
      })
    }
  &amp;lt;/&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>