<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-04-22T14:23:17+00:00</updated><id>/feed.xml</id><title type="html">lxmghct’s blog</title><subtitle>学习、实践、分享&lt;br&gt; Learning, Practicing, Sharing</subtitle><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><entry><title type="html">OpenClaw 新建会话异常</title><link href="/troubleshooting/2026/04/20/OpenClaw-%E6%96%B0%E5%BB%BA%E4%BC%9A%E8%AF%9D%E5%BC%82%E5%B8%B8.html" rel="alternate" type="text/html" title="OpenClaw 新建会话异常" /><published>2026-04-20T16:30:00+00:00</published><updated>2026-04-20T16:30:00+00:00</updated><id>/troubleshooting/2026/04/20/OpenClaw%20%E6%96%B0%E5%BB%BA%E4%BC%9A%E8%AF%9D%E5%BC%82%E5%B8%B8</id><content type="html" xml:base="/troubleshooting/2026/04/20/OpenClaw-%E6%96%B0%E5%BB%BA%E4%BC%9A%E8%AF%9D%E5%BC%82%E5%B8%B8.html"><![CDATA[<h1 id="问题描述">问题描述</h1>
<p>我在使用 openclaw 时，本来用的好好的，突然发现无论在浏览器还是在 tui 里，输入<code class="language-plaintext highlighter-rouge">/reset</code>或者<code class="language-plaintext highlighter-rouge">/new</code>命令来新建会话时，都会出现异常。以浏览器为例，具体表现为：新建会话时，前面突然加了一堆系统提示：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>System: [2026-04-21 00:12:25 GMT+8] Exec completed (calm-she, code 0) :: exec-1 done-1
System: [2026-04-21 00:12:27 GMT+8] Exec completed (warm-cru, code 0) :: exec-2 done-2
System: [2026-04-21 00:12:34 GMT+8] [Post-compaction context refresh]
System:
System: Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. Execute your Session Startup sequence now — read the required files before responding to the user.
System:
System: Critical rules from AGENTS.md:
System:
System: ## Session Startup
System:
System: Before doing anything else:
System:
System: 1. Read SOUL.md — this is who you are
System: 2. Read USER.md — this is who you're helping
System: 3. Read memory/2026-04-21.md (today + yesterday) for recent context
System: 4. If in MAIN SESSION (direct chat with your human): Also read MEMORY.md
System:
System: Don't ask permission. Just do it.
System:
System: ## Red Lines
System:
System: - Don't exfiltrate private data. Ever.
System: - Don't run destructive commands without asking.
System: - trash &gt; rm (recoverable beats gone forever)
System: - When in doubt, ask.
System:
System: Current time: Tuesday, April 21st, 2026 — 12:12 AM (Asia/Shanghai) / 2026-04-20 16:12 UTC

A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.
Current time: Tuesday, April 21st, 2026 — 12:16 AM (Asia/Shanghai) / 2026-04-20 16:16 UTC
</code></pre></div></div>

<p>此时继续对话，比如我发一个 hello，发送出去的消息会被自动加上系统提示的内容，变成：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
System: [2026-04-21 00:11:31 GMT+8] [Post-compaction context refresh]
System:
System: Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. Execute your Session Startup sequence now — read the required files before responding to the user.
System:
System: Critical rules from AGENTS.md:
System:
System: ## Session Startup
System:
System: Before doing anything else:
System:
System: 1. Read SOUL.md — this is who you are
System: 2. Read USER.md — this is who you're helping
System: 3. Read memory/2026-04-21.md (today + yesterday) for recent context
System: 4. If in MAIN SESSION (direct chat with your human): Also read MEMORY.md
System:
System: Don't ask permission. Just do it.
System:
System: ## Red Lines
System:
System: - Don't exfiltrate private data. Ever.
System: - Don't run destructive commands without asking.
System: - trash &gt; rm (recoverable beats gone forever)
System: - When in doubt, ask.
System:
System: Current time: Tuesday, April 21st, 2026 — 12:11 AM (Asia/Shanghai) / 2026-04-20 16:11 UTC

[Tue 2026-04-21 00:11 GMT+8] hello
</code></pre></div></div>
<p>这些提示导致每次都会调用工具去读取文件，有时甚至会直接忽略我发的内容。</p>

<p>而正常的对话虽然也会加一点前缀，但并不是上面这样的：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"type":"message","id":"b2f4300d","parentId":"d45d8da2","timestamp":"2026-03-16T11:23:53.797Z","message":{"role":"user","content":[{"type":"text","text":"Sender (untrusted metadata):\n```json\n{\n  \"label\": \"openclaw-tui (gateway-client)\",\n  \"id\": \"gateway-client\",\n  \"name\": \"openclaw-tui\",\n  \"username\": \"openclaw-tui\"\n}\n```\n\n[Mon 2026-03-16 19:23 GMT+8] 你好"}],"timestamp":1773660233795}}
</code></pre></div></div>
<p>格式整理一下就是：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sender (untrusted metadata):
```json
{
  "label": "openclaw-tui (gateway-client)",
  "id": "gateway-client",
  "name": "openclaw-tui",
   "username": "openclaw-tui"
}
```
[Mon 2026-03-16 19:23 GMT+8] 你好
</code></pre></div></div>

<h1 id="解决方案">解决方案</h1>
<p>目前没有找到问题所在，即使删除工作空间的所有内容，或者重启 openclaw 之后，问题依然存在。</p>

<p>但是在某些情况下会短暂的恢复正常。比如新建的 session 里开头不会有这个问题：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openclaw tui --session agent:gpt:test
</code></pre></div></div>
<p>但继续对话之后又会出现这个问题。</p>

<p>以及明确告知它”你叫XXX”时，它会恢复正常一下，但继续对话之后又会出现这个问题。</p>

<p><code class="language-plaintext highlighter-rouge">openclaw gateway restart</code>无法解决问题。</p>

<p>最后尝试了一种方式：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>openclaw gateway stop
openclaw gateway uninstall
openclaw gateway install
openclaw gateway start
</code></pre></div></div>
<p>执行完之后问题就解决了。不过目前无法确定是等了一段时间自行恢复了，还是卸载重装之后就解决了。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="openclaw" /><summary type="html"><![CDATA[问题描述 我在使用 openclaw 时，本来用的好好的，突然发现无论在浏览器还是在 tui 里，输入/reset或者/new命令来新建会话时，都会出现异常。以浏览器为例，具体表现为：新建会话时，前面突然加了一堆系统提示： ``` System: [2026-04-21 00:12:25 GMT+8] Exec completed (calm-she, code 0) :: exec-1 done-1 System: [2026-04-21 00:12:27 GMT+8] Exec completed (warm-cru, code 0) :: exec-2 done-2 System: [2026-04-21 00:12:34 GMT+8] [Post-compaction context refresh] System: System: Session was just compacted. The conversation summary above is a hint, NOT a substitute for your startup sequence. Execute your Session Startup sequence now — read the required files before responding to the user. System: System: Critical rules from AGENTS.md: System: System: ## Session Startup System: System: Before doing anything else: System: System: 1. Read SOUL.md — this is who you are System: 2. Read USER.md — this is who you’re helping System: 3. Read memory/2026-04-21.md (today + yesterday) for recent context System: 4. If in MAIN SESSION (direct chat with your human): Also read MEMORY.md System: System: Don’t ask permission. Just do it. System: System: ## Red Lines System: System: - Don’t exfiltrate private data. Ever. System: - Don’t run destructive commands without asking. System: - trash &gt; rm (recoverable beats gone forever) System: - When in doubt, ask. System: System: Current time: Tuesday, April 21st, 2026 — 12:12 AM (Asia/Shanghai) / 2026-04-20 16:12 UTC]]></summary></entry><entry><title type="html">将hiagent转为openai api格式以便给openclaw使用</title><link href="/project-logs/tech-exploration/2026/04/20/%E5%B0%86hiagent%E8%BD%AC%E4%B8%BAopenai-api%E6%A0%BC%E5%BC%8F%E4%BB%A5%E4%BE%BF%E7%BB%99openclaw%E4%BD%BF%E7%94%A8.html" rel="alternate" type="text/html" title="将hiagent转为openai api格式以便给openclaw使用" /><published>2026-04-20T02:30:00+00:00</published><updated>2026-04-20T02:30:00+00:00</updated><id>/project-logs/tech-exploration/2026/04/20/%E5%B0%86hiagent%E8%BD%AC%E4%B8%BAopenai%20api%E6%A0%BC%E5%BC%8F%E4%BB%A5%E4%BE%BF%E7%BB%99openclaw%E4%BD%BF%E7%94%A8</id><content type="html" xml:base="/project-logs/tech-exploration/2026/04/20/%E5%B0%86hiagent%E8%BD%AC%E4%B8%BAopenai-api%E6%A0%BC%E5%BC%8F%E4%BB%A5%E4%BE%BF%E7%BB%99openclaw%E4%BD%BF%E7%94%A8.html"><![CDATA[<p>最近想把南开的 hiagent 放到 openclaw 里用，从而节约一些 token。但这个 hiagent 是一个会话形式的智能体，而 openclaw 目前并不支持会话形式的智能体，只支持几种主流的调用格式比如 openai api 格式，所以需要先把把 hiagent 转换成 openai api 的格式。</p>

<p>注意：本文提到的 hiagent 都特指南开提供的这个 hiagent，而并非火山引擎或者其他的 hiagent。</p>

<p>项目地址：<a href="https://github.com/lxmghct/nankai-hiagent-proxy">https://github.com/lxmghct/nankai-hiagent-proxy</a></p>

<h1 id="1-核心问题及解决方案">1. 核心问题及解决方案</h1>
<h2 id="11-会话形式转为无状态的调用格式">1.1. 会话形式转为无状态的调用格式</h2>
<p>hiagent 的会话形式意味着它会在内部维护一个状态，包括之前的对话。在对话前需要获取<code class="language-plaintext highlighter-rouge">AppConversationId</code>，并在后续的对话中持续使用这个 ID 来维持会话状态。要想将这种形式转为无状态的调用格式，需要建立一个映射关系，一个比较简单粗暴的做法就是维护一个前几轮历史对话到<code class="language-plaintext highlighter-rouge">AppConversationId</code>的映射表，每次新的对话请求时，先根据当前的历史对话去查这个映射表，如果找到了对应的<code class="language-plaintext highlighter-rouge">AppConversationId</code>，就直接使用它；如果没有找到，就创建一个新的会话并获取新的<code class="language-plaintext highlighter-rouge">AppConversationId</code>，然后把这个新的 ID 和当前的历史对话一起存到映射表里。</p>

<p>不过考虑到映射表性能的问题，所以还是给前几轮对话建立一个哈希值，直接保存这个哈希值到<code class="language-plaintext highlighter-rouge">AppConversationId</code>的映射，这样就可以快速地通过哈希值来查找对应的会话 ID。</p>

<p>还有个问题是前几轮对话到底多少轮比较合适。如果轮数太多，前几轮对话默认没法确定唯一的<code class="language-plaintext highlighter-rouge">AppConversationId</code>，那么前几轮就会反复的去创建会话，虽然可行但没那么优雅。如果只有一轮那么重复概率很高，更何况 openclaw 的首轮对话往往是工具调用，重复概率就更大了。不过好在 openclaw 会把时间信息和用户问题一起发送过来：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[{'type': 'text', 'text': 'Sender (untrusted metadata):\n```json\n{\n  "label": "openclaw-tui (gateway-client)",\n  "id": "gateway-client",\n  "name": "openclaw-tui",\n  "username": "openclaw-tui"\n}\n```\n\n[Wed 2026-04-22 01:54 GMT+8] 你好'}]
</code></pre></div></div>
<p>这样基本一轮对话都不需要，只要一个prompt就可以唯一确定一个会话了。不过考虑到并发问题，而且这里的时间戳是到秒的，所以还是拿到该问题的回答后，再把这个问题和回答一起存到映射表里，这样就更保险了。</p>

<h2 id="12-工具调用问题">1.2. 工具调用问题</h2>
<p>hiagent 并不提供标准的工具调用接口，输入和输出都只有纯文本格式。一般来说这种情况只要在输入输出里约定好一个格式就可以了，比如输入里约定好工具调用的格式，输出里约定好工具调用结果的格式，这样就可以通过解析输入输出的文本来实现工具调用了。但是在实际使用过程中，经常会出现下面的情况，比如我约的了用<code class="language-plaintext highlighter-rouge">[[[HIAGENT_TOOL_CALL]]]</code>和<code class="language-plaintext highlighter-rouge">[[[END_HIAGENT_TOOL_CALL]]]</code>：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;tool_call&gt;[HIAGENT_TOOL_CALL]]]
{
"name": "read",
"arguments": {
"path": "/home/..."
}
}
[[[END_HIAGENT_TOOL_CALL]]]
</code></pre></div></div>
<p>可以看到上面的标签被不知道从哪出来的<code class="language-plaintext highlighter-rouge">&lt;tool_call&gt;</code>标签给打乱了，导致工具调用的格式被破坏了，无法正确解析工具调用的内容了。</p>

<p>此时有两个解决方法:</p>

<ol>
  <li>继续兼容这种混合标签的解析方式，增加对这种情况的处理逻辑（优先级较低，因为不规范且不稳定）</li>
  <li>取消新约定的<code class="language-plaintext highlighter-rouge">[[[HIAGENT_XXX]]]</code>标签，统一使用hiagent内部约定的<code class="language-plaintext highlighter-rouge">&lt;tool_call&gt;</code>标签。目前经过测试可能的格式为：</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;tool_call&gt;{"name": "read", "arguments": {"path": "/home/..."}}

&lt;tool_call&gt;read{"path": "/home/..."}

&lt;tool_call&gt;[{"name": "read", "arguments": {"path": "/home/..."}}, {"name": "write", "arguments": {"path": "/home/...", "content": "hello world"}}]
</code></pre></div></div>
<p>这里还会有一个潜在问题就是json格式不规范，目前遇到最多的就是arguments字段格式是有异常的，变成了：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"arguments": "{"path": "/home/..."}"
</code></pre></div></div>
<p>给一个本来没问题的 json 对象加了一层引号，导致解析失败了。目前仅发现了这个问题，在代码里已做了兼容处理。</p>

<h1 id="2-实现思路与关键代码示例">2. 实现思路与关键代码示例</h1>
<p>使用 FastAPI 来实现一个适配器服务，用 <code class="language-plaintext highlighter-rouge">@app.post("/v1/chat/completions")</code> 来接收 openclaw 的请求，在这个接口里先把 openclaw 的请求转换成 hiagent 的输入格式，然后调用 hiagent 来获取回答，最后再把 hiagent 的回答转换成 openclaw 需要的输出格式返回给 openclaw 就可以了。</p>

<h2 id="21-工具格式约定">2.1. 工具格式约定</h2>
<p>在 prompt 里约定工具调用的格式:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">TOOL_INSTRUCTION</span> <span class="o">=</span> <span class="sh">"""</span><span class="s">
## Tool calling rules

When you need to call a tool, you MUST use the following format.

&lt;tool_call&gt;
{
  </span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">tool_name</span><span class="sh">"</span><span class="s">,
  </span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="s">: { JSON }
}

Rules:
- Output ONLY the &lt;tool_call&gt; block
- Do NOT explain anything
- Do NOT use markdown
- Do NOT add extra text
- Do NOT wrap in code blocks
- arguments MUST be valid JSON object
- If multiple tools are needed, output multiple &lt;tool_call&gt; blocks sequentially
- If no tool is needed, answer normally

Example:

User: read file
Assistant:
&lt;tool_call&gt;
{
  </span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">read</span><span class="sh">"</span><span class="s">,
  </span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="s">: {
    </span><span class="sh">"</span><span class="s">path</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">/home/test.txt</span><span class="sh">"</span><span class="s">
  }
}

For multiple tool calls, you may also output:

&lt;tool_call&gt;
[
  {
    </span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">tool1</span><span class="sh">"</span><span class="s">,
    </span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="s">: {...}
  },
  {
    </span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="s">: </span><span class="sh">"</span><span class="s">tool2</span><span class="sh">"</span><span class="s">,
    </span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="s">: {...}
  }
]

</span><span class="sh">"""</span>
</code></pre></div></div>

<h2 id="22-构造-hiagent-输入格式">2.2. 构造 hiagent 输入格式</h2>
<p>将 openclaw 的请求转换成 hiagent 的输入格式:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>

<span class="k">def</span> <span class="nf">build_prompt_from_messages</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="n">create_new</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="n">parts</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="c1"># ---------- messages ----------
</span>    <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">messages</span><span class="p">:</span>
        <span class="n">role</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">content</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">create_new</span> <span class="ow">and</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">HIAGENT_HAS_OPENCLAW_PROMPT</span> <span class="ow">and</span> <span class="n">content</span><span class="p">:</span>
            <span class="c1"># if HIAGENT_HAS_OPENCLAW_PROMPT and content.startswith("You are a personal assistant running inside OpenClaw."):
</span>            <span class="c1">#     continue
</span>            <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[system]</span><span class="se">\n</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
            <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="se">\n</span><span class="si">{</span><span class="n">TOOL_INSTRUCTION</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

            <span class="c1"># ---------- tools schema ----------
</span>            <span class="k">if</span> <span class="n">tools</span><span class="p">:</span>
                <span class="n">tool_desc</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Available tools:</span><span class="se">\n</span><span class="sh">"</span><span class="p">]</span>

                <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">tools</span><span class="p">:</span>
                    <span class="n">func</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>
                    <span class="n">name</span> <span class="o">=</span> <span class="n">func</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">)</span>
                    <span class="n">desc</span> <span class="o">=</span> <span class="n">func</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">)</span>
                    <span class="n">params</span> <span class="o">=</span> <span class="n">func</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">parameters</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>

                    <span class="n">tool_desc</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Tool: </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>
                    <span class="k">if</span> <span class="n">desc</span><span class="p">:</span>
                        <span class="n">tool_desc</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Description: </span><span class="si">{</span><span class="n">desc</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

                    <span class="k">if</span> <span class="n">params</span><span class="p">:</span>
                        <span class="n">tool_desc</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="s">Parameters JSON schema:</span><span class="sh">"</span><span class="p">)</span>
                        <span class="n">tool_desc</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>

                    <span class="n">tool_desc</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">""</span><span class="p">)</span>

                <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tool_desc</span><span class="p">))</span>

        <span class="k">elif</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span> <span class="ow">and</span> <span class="n">content</span><span class="p">:</span>
            <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[user]</span><span class="se">\n</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">elif</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">assistant</span><span class="sh">"</span><span class="p">:</span>
            <span class="n">tool_calls</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">tool_calls</span><span class="sh">"</span><span class="p">)</span>

            <span class="k">if</span> <span class="n">tool_calls</span><span class="p">:</span>
                <span class="c1"># {"role": "assistant", "content": null, "tool_calls": [{"id": "call08690af73f584d3386d63b10509f55d9", "type": "function", "function": {"name": "read", "arguments": "{\"path\":\"/home/devuser/.openclaw/workspace/SOUL.md\"}"}}, {"id": "call937287b1a26e4c83aed8a9fdd261ec3c", "type": "function", "function": {"name": "read", "arguments": "{\"path\":\"/home/devuser/.openclaw/workspace/USER.md\"}"}}]}
</span>                <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="s">[assistant]</span><span class="se">\n</span><span class="sh">"</span> <span class="o">+</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">tool_calls</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>
            <span class="k">elif</span> <span class="n">content</span><span class="p">:</span>
                <span class="c1"># {"role": "assistant", "content": [{"type": "text", "text": "Hey. I'm back online. \n\nI see we're still getting to know each other — your profile's pretty blank. What's on your mind? "}]}
</span>                <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[assistant]</span><span class="se">\n</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">elif</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">tool</span><span class="sh">"</span><span class="p">:</span>
            <span class="c1">#  {"role": "tool", "content": "Successfully wrote 269 bytes to /home/xxx", "tool_call_id": "call76e5d5301407447f833d9b71b973ecc7"}
</span>            <span class="n">call_id</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">tool_call_id</span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">)</span>
            <span class="n">parts</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[tool:</span><span class="si">{</span><span class="n">call_id</span><span class="si">}</span><span class="s">]</span><span class="se">\n</span><span class="si">{</span><span class="n">content</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># parts.append("[assistant]")
</span>
    <span class="k">return</span> <span class="sh">"</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="23-解析-hiagent-输出格式">2.3. 解析 hiagent 输出格式</h2>
<p>然后是处理 hiagent 的回答并转换成 openclaw 需要的输出格式，先进行工具提取：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">parse_hiagent_tool_calls</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">tool_calls</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="c1"># ---------- &lt;tool_call&gt; ----------
</span>    <span class="n">idx</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">start</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="sh">"</span><span class="s">&lt;tool_call&gt;</span><span class="sh">"</span><span class="p">,</span> <span class="n">idx</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">start</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">:</span>
            <span class="k">break</span>

        <span class="n">cursor</span> <span class="o">=</span> <span class="n">start</span> <span class="o">+</span> <span class="nf">len</span><span class="p">(</span><span class="sh">"</span><span class="s">&lt;tool_call&gt;</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># 跳过空白
</span>        <span class="k">while</span> <span class="n">cursor</span> <span class="o">&lt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">text</span><span class="p">)</span> <span class="ow">and</span> <span class="n">text</span><span class="p">[</span><span class="n">cursor</span><span class="p">].</span><span class="nf">isspace</span><span class="p">():</span>
            <span class="n">cursor</span> <span class="o">+=</span> <span class="mi">1</span>

        <span class="k">if</span> <span class="n">cursor</span> <span class="o">&gt;=</span> <span class="nf">len</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
            <span class="k">break</span>

        <span class="c1"># ---------- 情况 A: 直接 JSON 对象 ----------
</span>        <span class="k">if</span> <span class="n">text</span><span class="p">[</span><span class="n">cursor</span><span class="p">]</span> <span class="ow">in</span> <span class="sh">"</span><span class="s">{[</span><span class="sh">"</span><span class="p">:</span>
            <span class="n">json_str</span><span class="p">,</span> <span class="n">end</span> <span class="o">=</span> <span class="nf">_extract_balanced_json</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">cursor</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">json_str</span><span class="p">:</span>
                <span class="k">try</span><span class="p">:</span>
                    <span class="n">data</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">json_str</span><span class="p">)</span>

                    <span class="c1"># 如果是数组
</span>                    <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
                        <span class="k">for</span> <span class="n">item</span> <span class="ow">in</span> <span class="n">data</span><span class="p">:</span>
                            <span class="n">func</span> <span class="o">=</span> <span class="n">item</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">function</span><span class="sh">"</span><span class="p">,</span> <span class="n">item</span><span class="p">)</span>
                            <span class="n">tool_calls</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span>
                                <span class="nc">HiAgentToolCall</span><span class="p">(</span>
                                    <span class="nb">id</span><span class="o">=</span><span class="n">item</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="sa">f</span><span class="sh">"</span><span class="s">call_</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">().</span><span class="nb">hex</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
                                    <span class="n">name</span><span class="o">=</span><span class="n">func</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">),</span>
                                    <span class="n">arguments</span><span class="o">=</span><span class="nf">normalize_arguments</span><span class="p">(</span>
                                        <span class="n">func</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>
                                    <span class="p">),</span>
                                <span class="p">)</span>
                            <span class="p">)</span>
                    <span class="k">else</span><span class="p">:</span>
                        <span class="n">tool_calls</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span>
                            <span class="nc">HiAgentToolCall</span><span class="p">(</span>
                                <span class="nb">id</span><span class="o">=</span><span class="n">data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="sa">f</span><span class="sh">"</span><span class="s">call_</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">().</span><span class="nb">hex</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
                                <span class="n">name</span><span class="o">=</span><span class="n">data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">),</span>
                                <span class="n">arguments</span><span class="o">=</span><span class="nf">normalize_arguments</span><span class="p">(</span>
                                    <span class="n">data</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">arguments</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>
                                <span class="p">),</span>
                            <span class="p">)</span>
                        <span class="p">)</span>

                    <span class="n">idx</span> <span class="o">=</span> <span class="n">end</span>
                    <span class="k">continue</span>
                <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
                    <span class="k">pass</span>

        <span class="c1"># ---------- 情况 B: name{...} ----------
</span>        <span class="n">name_match</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">([a-zA-Z_][a-zA-Z0-9_]*)</span><span class="sh">"</span><span class="p">,</span> <span class="n">text</span><span class="p">[</span><span class="n">cursor</span><span class="p">:])</span>
        <span class="k">if</span> <span class="n">name_match</span><span class="p">:</span>
            <span class="n">name</span> <span class="o">=</span> <span class="n">name_match</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
            <span class="n">cursor</span> <span class="o">+=</span> <span class="nf">len</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>

            <span class="k">while</span> <span class="n">cursor</span> <span class="o">&lt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">text</span><span class="p">)</span> <span class="ow">and</span> <span class="n">text</span><span class="p">[</span><span class="n">cursor</span><span class="p">].</span><span class="nf">isspace</span><span class="p">():</span>
                <span class="n">cursor</span> <span class="o">+=</span> <span class="mi">1</span>

            <span class="k">if</span> <span class="n">cursor</span> <span class="o">&lt;</span> <span class="nf">len</span><span class="p">(</span><span class="n">text</span><span class="p">)</span> <span class="ow">and</span> <span class="n">text</span><span class="p">[</span><span class="n">cursor</span><span class="p">]</span> <span class="o">==</span> <span class="sh">"</span><span class="s">{</span><span class="sh">"</span><span class="p">:</span>
                <span class="n">json_str</span><span class="p">,</span> <span class="n">end</span> <span class="o">=</span> <span class="nf">_extract_balanced_json</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">cursor</span><span class="p">)</span>
                <span class="k">if</span> <span class="n">json_str</span><span class="p">:</span>
                    <span class="k">try</span><span class="p">:</span>
                        <span class="n">args</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">json_str</span><span class="p">)</span>
                        <span class="n">tool_calls</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span>
                            <span class="nc">HiAgentToolCall</span><span class="p">(</span>
                                <span class="nb">id</span><span class="o">=</span><span class="sa">f</span><span class="sh">"</span><span class="s">call_</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">().</span><span class="nb">hex</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
                                <span class="n">name</span><span class="o">=</span><span class="n">name</span><span class="p">,</span>
                                <span class="n">arguments</span><span class="o">=</span><span class="n">args</span><span class="p">,</span>
                            <span class="p">)</span>
                        <span class="p">)</span>
                        <span class="n">idx</span> <span class="o">=</span> <span class="n">end</span>
                        <span class="k">continue</span>
                    <span class="k">except</span> <span class="nb">Exception</span><span class="p">:</span>
                        <span class="k">pass</span>

        <span class="n">idx</span> <span class="o">=</span> <span class="n">cursor</span> <span class="o">+</span> <span class="mi">1</span>

    <span class="k">if</span> <span class="n">tool_calls</span><span class="p">:</span>
        <span class="k">return</span> <span class="n">tool_calls</span><span class="p">,</span> <span class="bp">None</span>

    <span class="k">return</span> <span class="bp">None</span><span class="p">,</span> <span class="n">text</span>
</code></pre></div></div>

<p>最后是把提取到的工具调用转换成 openclaw 需要的格式，注意 openclaw 默认是 streaming 的，所以需要把工具调用的结果也以流式的格式返回。虽然 hiagent 的接口也支持 steaming 参数，但是经过实践发现这个流式是假的，虽然是流式的返回格式，但是它并不会在工具调用结果出来后就立刻返回，而是等到整个回答结束才一次性返回，而且流式的格式也是 hiagent 自己定义的，并不完全符合 openclaw 的流式格式，所以不如就调用它的 blocking 接口再自己转成流式的格式了。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">fastapi.responses</span> <span class="kn">import</span> <span class="n">StreamingResponse</span>


<span class="k">def</span> <span class="nf">sse_pack</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
    <span class="k">return</span> <span class="sa">f</span><span class="sh">"</span><span class="s">data: </span><span class="si">{</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span><span class="si">}</span><span class="se">\n\n</span><span class="sh">"</span>


<span class="k">def</span> <span class="nf">build_stream_text_chunks</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">custom-model</span><span class="sh">"</span><span class="p">,</span> <span class="n">chunk_size</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">text</span><span class="p">),</span> <span class="n">chunk_size</span><span class="p">):</span>
        <span class="n">chunk</span> <span class="o">=</span> <span class="n">text</span><span class="p">[</span><span class="n">i</span><span class="p">:</span><span class="n">i</span><span class="o">+</span><span class="n">chunk_size</span><span class="p">]</span>
        <span class="k">yield</span> <span class="nf">sse_pack</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">chatcmpl-</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">object</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">chat.completion.chunk</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">created</span><span class="sh">"</span><span class="p">:</span> <span class="nf">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()),</span>
            <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">:</span> <span class="p">[{</span>
                <span class="sh">"</span><span class="s">index</span><span class="sh">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">delta</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">chunk</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">finish_reason</span><span class="sh">"</span><span class="p">:</span> <span class="bp">None</span>
            <span class="p">}]</span>
        <span class="p">})</span>


<span class="k">def</span> <span class="nf">build_stream_tool_calls</span><span class="p">(</span><span class="n">tool_calls</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">custom-model</span><span class="sh">"</span><span class="p">):</span>

    <span class="k">yield</span> <span class="nf">sse_pack</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">chatcmpl-</span><span class="si">{</span><span class="n">uuid</span><span class="p">.</span><span class="nf">uuid4</span><span class="p">()</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">object</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">chat.completion.chunk</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">created</span><span class="sh">"</span><span class="p">:</span> <span class="nf">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()),</span>
        <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span>
                <span class="sh">"</span><span class="s">index</span><span class="sh">"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">delta</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="sh">"</span><span class="s">tool_calls</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
                        <span class="n">tc</span><span class="p">.</span><span class="nf">to_openai_tool_call</span><span class="p">()</span> <span class="k">for</span> <span class="n">tc</span> <span class="ow">in</span> <span class="n">tool_calls</span>
                    <span class="p">]</span>
                <span class="p">},</span>
                <span class="sh">"</span><span class="s">finish_reason</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">tool_calls</span><span class="sh">"</span>
            <span class="p">}</span>
        <span class="p">]</span>
    <span class="p">})</span>


<span class="k">def</span> <span class="nf">stream_done</span><span class="p">():</span>
    <span class="k">return</span> <span class="sh">"</span><span class="s">data: [DONE]</span><span class="se">\n\n</span><span class="sh">"</span>


<span class="k">def</span> <span class="nf">build_openai_stream_response</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">tool_calls</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">generator</span><span class="p">():</span>

        <span class="c1"># role chunk (recommended)
</span>        <span class="k">yield</span> <span class="nf">sse_pack</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">:</span> <span class="p">[{</span>
                <span class="sh">"</span><span class="s">delta</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">assistant</span><span class="sh">"</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">index</span><span class="sh">"</span><span class="p">:</span> <span class="mi">0</span>
            <span class="p">}]</span>
        <span class="p">})</span>

        <span class="c1"># 1. content 文本
</span>        <span class="k">if</span> <span class="n">content</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="nf">build_stream_text_chunks</span><span class="p">(</span><span class="n">content</span><span class="p">):</span>
                <span class="k">yield</span> <span class="n">chunk</span>

        <span class="c1"># 2. tool calls
</span>        <span class="k">if</span> <span class="n">tool_calls</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="nf">build_stream_tool_calls</span><span class="p">(</span><span class="n">tool_calls</span><span class="p">):</span>
                <span class="k">yield</span> <span class="n">chunk</span>

        <span class="c1"># finish
</span>        <span class="k">yield</span> <span class="nf">sse_pack</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">:</span> <span class="p">[{</span>
                <span class="sh">"</span><span class="s">delta</span><span class="sh">"</span><span class="p">:</span> <span class="p">{},</span>
                <span class="sh">"</span><span class="s">finish_reason</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">stop</span><span class="sh">"</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">index</span><span class="sh">"</span><span class="p">:</span> <span class="mi">0</span>
            <span class="p">}]</span>
        <span class="p">})</span>

        <span class="k">yield</span> <span class="nf">stream_done</span><span class="p">()</span>

    <span class="k">return</span> <span class="nc">StreamingResponse</span><span class="p">(</span>
        <span class="nf">generator</span><span class="p">(),</span>
        <span class="n">media_type</span><span class="o">=</span><span class="sh">"</span><span class="s">text/event-stream</span><span class="sh">"</span>
    <span class="p">)</span>
</code></pre></div></div>

<h2 id="24-关于历史对话到appconversationid的映射关系">2.4. 关于历史对话到<code class="language-plaintext highlighter-rouge">AppConversationId</code>的映射关系</h2>
<p>目前使用了 sqlite 来维护这个映射关系，主要的两列为：一列是历史对话的哈希值，一列是对应的<code class="language-plaintext highlighter-rouge">AppConversationId</code>。每次新的对话请求过来时，先把当前的历史对话计算出哈希值，然后去这个表里查有没有对应的<code class="language-plaintext highlighter-rouge">AppConversationId</code>，如果有就直接用它，如果没有就创建一个新的会话获取新的<code class="language-plaintext highlighter-rouge">AppConversationId</code>，然后把这个新的 ID 和当前的历史对话的哈希值一起存到表里。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">compute_conversation_key</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">max_assistant_turns</span><span class="o">=</span><span class="mi">1</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">messages</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">None</span>
    
    <span class="k">if</span> <span class="nf">count_assistant_turns</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">max_assistant_turns</span><span class="p">:</span>
        <span class="c1"># 如果 assistant 轮数还不够，就不计算 key，强制新建会话
</span>        <span class="k">return</span> <span class="bp">None</span>

    <span class="n">text_list</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">assistant_turns</span> <span class="o">=</span> <span class="mi">0</span>

    <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">messages</span><span class="p">:</span>
        <span class="n">role</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">:</span>
            <span class="k">continue</span>
        <span class="n">content</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">text</span> <span class="o">=</span> <span class="sh">""</span>
        <span class="k">if</span> <span class="n">content</span><span class="p">:</span>
            <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
                <span class="n">text</span> <span class="o">=</span> <span class="n">content</span>
            <span class="k">elif</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
                <span class="n">text</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">text</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">content</span><span class="p">)</span>
        <span class="n">text_list</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">[</span><span class="si">{</span><span class="n">role</span><span class="si">}</span><span class="s">]</span><span class="se">\n</span><span class="si">{</span><span class="n">text</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">role</span> <span class="o">==</span> <span class="sh">"</span><span class="s">assistant</span><span class="sh">"</span><span class="p">:</span>
            <span class="n">assistant_turns</span> <span class="o">+=</span> <span class="mi">1</span>
            <span class="k">if</span> <span class="n">assistant_turns</span> <span class="o">&gt;=</span> <span class="n">max_assistant_turns</span><span class="p">:</span>
                <span class="k">break</span>

    <span class="n">key_material</span> <span class="o">=</span> <span class="sh">"</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">text_list</span><span class="p">)</span>
    <span class="n">h</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">key_material</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)).</span><span class="nf">hexdigest</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">h</span>
</code></pre></div></div>

<h2 id="25-完整流程代码">2.5. 完整流程代码</h2>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/v1/chat/completions</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">chat</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>

    <span class="n">stream</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">stream</span><span class="sh">"</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
    <span class="n">messages</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[]</span>
    <span class="n">tools</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">tools</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 计算会话 key（忽略 system）
</span>    <span class="n">conv_key</span> <span class="o">=</span> <span class="nf">compute_conversation_key</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
    <span class="n">conversation_id</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="k">if</span> <span class="n">conv_key</span><span class="p">:</span>
        <span class="n">obj</span> <span class="o">=</span> <span class="nf">get_conversation_by_key</span><span class="p">(</span><span class="n">conv_key</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">obj</span><span class="p">:</span>
            <span class="n">conversation_id</span> <span class="o">=</span> <span class="n">obj</span><span class="p">[</span><span class="sh">"</span><span class="s">app_conversation_id</span><span class="sh">"</span><span class="p">]</span>
            <span class="n">idx</span> <span class="o">=</span> <span class="nf">find_last_assistant_index</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
            <span class="n">prompt</span> <span class="o">=</span> <span class="nf">build_prompt_from_messages</span><span class="p">(</span><span class="n">messages</span><span class="p">[</span><span class="n">idx</span><span class="o">+</span><span class="mi">1</span><span class="p">:],</span> <span class="n">tools</span><span class="o">=</span><span class="n">tools</span><span class="p">,</span> <span class="n">create_new</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">conversation_id</span><span class="p">:</span>
        <span class="n">conversation_id</span> <span class="o">=</span> <span class="nf">create_conversation</span><span class="p">()</span>
        <span class="n">prompt</span> <span class="o">=</span> <span class="nf">build_prompt_from_messages</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="n">tools</span><span class="p">,</span> <span class="n">create_new</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">regenerate_conversation</span> <span class="o">=</span> <span class="bp">False</span>
    <span class="n">content</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">tool_calls</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">token_usage</span> <span class="o">=</span> <span class="bp">None</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="n">content</span><span class="p">,</span> <span class="n">tool_calls</span><span class="p">,</span> <span class="n">token_usage</span> <span class="o">=</span> <span class="nf">chat_query</span><span class="p">(</span><span class="n">conversation_id</span><span class="p">,</span> <span class="n">prompt</span><span class="p">)</span>
    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error during chat query:</span><span class="sh">"</span><span class="p">,</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
        <span class="c1"># 400
</span>        <span class="k">if</span> <span class="sh">"</span><span class="s">Conversation expired or invalid</span><span class="sh">"</span> <span class="ow">in</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">):</span>
            <span class="k">if</span> <span class="n">conv_key</span><span class="p">:</span>
                <span class="nf">delete_conversation</span><span class="p">(</span><span class="n">conv_key</span><span class="p">)</span>
            <span class="n">conversation_id</span> <span class="o">=</span> <span class="nf">create_conversation</span><span class="p">()</span>
            <span class="n">prompt</span> <span class="o">=</span> <span class="nf">build_prompt_from_messages</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="n">tools</span><span class="p">,</span> <span class="n">create_new</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
            <span class="n">regenerate_conversation</span> <span class="o">=</span> <span class="bp">True</span>

            <span class="c1"># 重新用完整消息构建 prompt，会导致一大段文本塞到同一次会话里，极有可能消耗大量上下文，但目前没有更好的选择了
</span>            <span class="k">try</span><span class="p">:</span>
                <span class="n">content</span><span class="p">,</span> <span class="n">tool_calls</span><span class="p">,</span> <span class="n">token_usage</span> <span class="o">=</span> <span class="nf">chat_query</span><span class="p">(</span><span class="n">conversation_id</span><span class="p">,</span> <span class="n">prompt</span><span class="p">)</span>
            <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
                <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error during chat query for new conversation:</span><span class="sh">"</span><span class="p">,</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
                <span class="k">return</span> <span class="nf">build_openai_response</span><span class="p">(</span><span class="sh">"</span><span class="s">hiagent 请求失败: </span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span> <span class="n">stream</span><span class="o">=</span><span class="n">stream</span><span class="p">)</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">build_openai_response</span><span class="p">(</span><span class="sh">"</span><span class="s">hiagent 请求失败: </span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">),</span> <span class="n">stream</span><span class="o">=</span><span class="n">stream</span><span class="p">)</span>

    <span class="n">openclaw_message</span> <span class="o">=</span> <span class="nf">build_openclaw_assistant_format</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">tool_calls</span><span class="p">)</span>
    <span class="n">messages</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">openclaw_message</span><span class="p">)</span>
    <span class="n">assistant_turns</span> <span class="o">=</span> <span class="nf">count_assistant_turns</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">conv_key</span><span class="p">:</span>
        <span class="n">new_conv_key</span> <span class="o">=</span> <span class="nf">compute_conversation_key</span><span class="p">(</span><span class="n">messages</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">new_conv_key</span><span class="p">:</span>
            <span class="nf">save_conversation</span><span class="p">(</span><span class="n">new_conv_key</span><span class="p">,</span> <span class="n">conversation_id</span><span class="p">,</span> <span class="nf">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()),</span> <span class="n">turn_count</span><span class="o">=</span><span class="n">assistant_turns</span><span class="p">)</span>
    <span class="k">elif</span> <span class="ow">not</span> <span class="n">regenerate_conversation</span><span class="p">:</span>
        <span class="nf">update_turn_count</span><span class="p">(</span><span class="n">conv_key</span><span class="p">,</span> <span class="n">assistant_turns</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="nf">save_conversation</span><span class="p">(</span><span class="n">conv_key</span><span class="p">,</span> <span class="n">conversation_id</span><span class="p">,</span> <span class="nf">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="nf">time</span><span class="p">()),</span> <span class="n">turn_count</span><span class="o">=</span><span class="n">assistant_turns</span><span class="p">)</span>

    <span class="n">resp</span> <span class="o">=</span> <span class="nf">build_openai_response</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">tool_calls</span><span class="o">=</span><span class="n">tool_calls</span><span class="p">,</span> <span class="n">stream</span><span class="o">=</span><span class="n">stream</span><span class="p">,</span> <span class="n">token_usage</span><span class="o">=</span><span class="n">token_usage</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">resp</span>
</code></pre></div></div>

<h1 id="3-实现思路尝试1">3. 实现思路尝试1</h1>
<p>一开始我并没有尝试将历史对话和<code class="language-plaintext highlighter-rouge">AppConversationId</code>建立映射关系，主要是考虑到两点：</p>

<ol>
  <li>直接将所有的历史对话都发到一次对话里，直接把 hiagent 当成无状态的模型来用更简单一些</li>
  <li>OpenClaw 和 hiagent 各自都会有压缩上下文的机制，映射关系可能会随着上下文的变化而失效了，维护映射关系可能会比较麻烦</li>
</ol>

<p>这样的实现确实更简单一些：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@app.post</span><span class="p">(</span><span class="sh">"</span><span class="s">/v1/chat/completions</span><span class="sh">"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">chat</span><span class="p">(</span><span class="n">body</span><span class="p">:</span> <span class="nb">dict</span><span class="p">):</span>

    <span class="n">user_id</span> <span class="o">=</span> <span class="sh">"</span><span class="s">default_user</span><span class="sh">"</span>
    <span class="n">stream</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">stream</span><span class="sh">"</span><span class="p">,</span> <span class="bp">False</span><span class="p">)</span>
    <span class="n">messages</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">)</span> <span class="ow">or</span> <span class="p">[]</span>
    <span class="n">tools</span> <span class="o">=</span> <span class="n">body</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">tools</span><span class="sh">"</span><span class="p">)</span>

    <span class="c1"># 构造完整历史 prompt
</span>    <span class="n">prompt</span> <span class="o">=</span> <span class="nf">build_prompt_from_messages</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="n">tools</span><span class="p">)</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="c1"># 每次创建新的 hiagent 会话（不复用）
</span>        <span class="n">conversation_id</span> <span class="o">=</span> <span class="nf">create_conversation</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
        <span class="n">answer</span> <span class="o">=</span> <span class="nf">chat_query</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">conversation_id</span><span class="p">,</span> <span class="n">prompt</span><span class="p">)</span>

    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Error during chat query:</span><span class="sh">"</span><span class="p">,</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
        <span class="k">if</span> <span class="n">stream</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">build_openai_stream_response</span><span class="p">(</span><span class="sh">"</span><span class="s">hiagent 请求失败: </span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">build_openai_response_text</span><span class="p">(</span><span class="sh">"</span><span class="s">hiagent 请求失败: </span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">e</span><span class="p">))</span>
    
    <span class="c1"># 解析 tool call
</span>    <span class="n">tool_calls</span><span class="p">,</span> <span class="n">prefix_text</span> <span class="o">=</span> <span class="nf">parse_hiagent_tool_calls</span><span class="p">(</span><span class="n">answer</span><span class="p">)</span>

    <span class="c1"># ---------- STREAM ----------
</span>    <span class="k">if</span> <span class="n">stream</span><span class="p">:</span>
        <span class="c1"># return build_openai_stream_response(answer, tool_calls=tool_calls, prefix_text=prefix_text)
</span>        <span class="k">return</span> <span class="nf">build_openai_stream_response</span><span class="p">(</span><span class="n">answer</span><span class="p">,</span> <span class="n">tool_calls</span><span class="o">=</span><span class="n">tool_calls</span><span class="p">,</span> <span class="n">prefix_text</span><span class="o">=</span><span class="bp">None</span><span class="p">)</span>

    <span class="c1"># ---------- NON STREAM ----------
</span>
    <span class="k">if</span> <span class="n">tool_calls</span><span class="p">:</span>
        <span class="n">resp</span> <span class="o">=</span> <span class="nf">build_openai_response_tool_calls</span><span class="p">(</span><span class="n">tool_calls</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="n">prefix_text</span><span class="p">)</span>

    <span class="k">else</span><span class="p">:</span>
        <span class="n">resp</span> <span class="o">=</span> <span class="nf">build_openai_response_text</span><span class="p">(</span><span class="n">answer</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">resp</span>
</code></pre></div></div>

<p>不过这种方式有个问题，上下文消耗的速度会比预想的要快，可能是因为把所有的历史对话都塞到了一次对话的输入里了，导致 hiagent 的内部的上下文压缩机制并没有发挥作用。不过这只是个推测，从结果来看，实际使用时经常几轮对话就开始超时了。</p>

<p>后面经过验证发现虽然 openclaw 确实会有上下文的优化，但它发送给 api 的内容也确实是完整的历史，所以实际上不会存在上下文因压缩而频繁变换导致无法维护映射关系的问题了。</p>

<h1 id="4-其他问题">4. 其他问题</h1>
<h2 id="41-超时问题">4.1. 超时问题</h2>
<p>最初调试的时候发现有时 hiagent 会出现超时的情况，此时会返回 504 gateway timeout 的错误。所以我就给代码加了个超时的判断，只要是 504 错误，就反复再发送几次请求，直到超过最大次数。</p>

<p>不过使用过程中发现超时十分频繁，且基本固定为一分钟左右，且只要第一次超时，后面反复再发送的几次也基本超时了，感觉不像是 hiagent 真正的处理超时了。所以我查看了 api 文档，找到 <code class="language-plaintext highlighter-rouge">get_conversation_messages</code> 这个接口，查看一下频繁超时的对话的消息记录，发现了多次重复的问题和回答，也就是说，虽然我接收到了超时，但大模型内部仍在处理我的请求，并处理完之后保存到了上下文里。所以超时实际上来自于网关而非大模型。</p>

<p>原有思路肯定会影响上下文质量且频繁超时，所以采用拿到 504 错误后不再反复发送请求了，而是接着调用 <code class="language-plaintext highlighter-rouge">get_conversation_messages</code> 这个接口去轮询消息记录，直到拿到新的回答了再继续后续的流程，这样就避免了频繁超时的问题了。</p>

<p><code class="language-plaintext highlighter-rouge">get_conversation_messages</code> 示例返回值：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "Messages": [
		{
			"ConversationID": "01KPNYPAS92ZZC51J3309FR47S",
			"QueryID": "d7j624elvnde66pa0a9g",
			"Query": "[user]\n[{'type': 'text', 'text': 'Sender (untrusted metadata):\\n```json\\n{\\n  \"label\": \"openclaw-tui (gateway-client)\",\\n  \"id\": \"gateway-client\",\\n  \"name\": \"openclaw-tui\",\\n  \"username\": \"openclaw-tui\"\\n}\\n```\\n\\n[Tue 2026-04-21 01:23 GMT+8] 你好'}]\n\n[assistant]",
			"AnswerInfo": {
				"Answer": "你好！很高兴见到你。😊\n\n我是你的 OpenClaw 助手。看起来这是一个全新的会话，我刚刚上线。\n\n让我先了解一下你的情况：&lt;tool_call&gt;read\n{\"path\": \"/home/devuser/.openclaw/workspace/IDENTITY.md\"}",
				"MessageID": "01KPNYPAX7NK5QMKN6MCG56CB0",
				"CreatedTime": 1776705809,
				"TaskID": "01KPNYPAX7NK5QMKN6MCG56CB0",
				"Like": 0,
				"TotalTokens": 10470,
				"Latency": 5.459,
				"TracingJsonStr": "",
				"RetrieverResource": false
			},
			"OtherAnswers": [],
			"Inputs": {},
			"SendByTrigger": false,
			"SendByAsyncWorkflow": false,
			"UserMessage": {
				"Messages": [
					{
						"type": "text",
						"text": "[user]\n[{'type': 'text', 'text': 'Sender (untrusted metadata):\\n```json\\n{\\n  \"label\": \"openclaw-tui (gateway-client)\",\\n  \"id\": \"gateway-client\",\\n  \"name\": \"openclaw-tui\",\\n  \"username\": \"openclaw-tui\"\\n}\\n```\\n\\n[Tue 2026-04-21 01:23 GMT+8] 你好'}]\n\n[assistant]"
					}
				]
			}
		}
	],
	"BaseResp": null
}
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="project-logs" /><category term="tech-exploration" /><category term="openclaw" /><category term="agent" /><summary type="html"><![CDATA[最近想把南开的 hiagent 放到 openclaw 里用，从而节约一些 token。但这个 hiagent 是一个会话形式的智能体，而 openclaw 目前并不支持会话形式的智能体，只支持几种主流的调用格式比如 openai api 格式，所以需要先把把 hiagent 转换成 openai api 的格式。]]></summary></entry><entry><title type="html">笔记本电脑键盘间歇失灵问题</title><link href="/platforms/2026/04/14/%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91%E9%94%AE%E7%9B%98%E9%97%B4%E6%AD%87%E5%A4%B1%E7%81%B5%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="笔记本电脑键盘间歇失灵问题" /><published>2026-04-14T13:30:00+00:00</published><updated>2026-04-14T13:30:00+00:00</updated><id>/platforms/2026/04/14/%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91%E9%94%AE%E7%9B%98%E9%97%B4%E6%AD%87%E5%A4%B1%E7%81%B5%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/platforms/2026/04/14/%E7%AC%94%E8%AE%B0%E6%9C%AC%E7%94%B5%E8%84%91%E9%94%AE%E7%9B%98%E9%97%B4%E6%AD%87%E5%A4%B1%E7%81%B5%E9%97%AE%E9%A2%98.html"><![CDATA[<h1 id="问题描述">问题描述</h1>
<p>最近在使用笔记本电脑时，当系统负载较高时，键盘会出现间歇性失灵的情况。表现为大概只要五秒没有按下键盘，键盘就会失灵。此时按下键盘是没有反应的，需要过将近十秒左右键盘才能恢复正常使用。而此后如果超过五秒没有按下键盘，键盘又会再次失灵。这个问题非常影响使用体验。</p>

<p>在失灵期间，打开设备管理器，可以发现键盘展开后，其中的 <code class="language-plaintext highlighter-rouge">HID Keyboard Device</code> 有时会有异常。具体表现为：在设备管理器里右键-&gt;扫描检测硬件改动，此时 <code class="language-plaintext highlighter-rouge">HID Keyboard Device</code> 会消失，然后过一会（和前面描述的十秒左右时间一致）又会重新出现。也就是扫描硬件改动和按下键盘一样会触发键盘的重新连接。</p>

<p>原因初步推测可能和电源管理策略有关。</p>

<h1 id="尝试的解决方案">尝试的解决方案</h1>
<ol>
  <li>
    <p>进入设备管理器，找到 <code class="language-plaintext highlighter-rouge">HID Keyboard Device</code>，右键选择属性，在电源管理选项卡中取消勾选“允许计算机关闭此设备以节约电源”。不过实际操作下来，这里的两个选项：允许计算机关闭此设备以节约电源（默认未勾选）、允许此设备唤醒计算机（默认未勾选）均无法改变勾选状态。前者无法点击，后者点击了重新打开后又变回去了。</p>
  </li>
  <li>
    <p>控制面板-&gt;电源选项-&gt;更改计划设置-&gt;更改高级电源设置-&gt;USB设置-&gt;USB选择性暂停设置，设置为禁用。这个选项是针对 USB 设备的，笔记本键盘一般是通过 USB 接口连接的，所以禁用这个选项可以防止系统在高负载时关闭 USB 设备来节约电源。但更改这个选项之后仍然会出现键盘间歇失灵的问题。</p>
  </li>
</ol>

<h1 id="最终解决方案">最终解决方案</h1>
<p>实际上设备管理器里的”允许计算机关闭此设备以节约电源”选项是有效的，只是找错了设备。不应该去找 键盘-&gt;HID Keyboard Device，而应该去找 通用串行总线控制器-&gt;USB根集线器 (USB 3.0)，在这个设备的属性-&gt;电源管理选项卡中取消勾选“允许计算机关闭此设备以节约电源”。更改这个选项之后，键盘间歇失灵的问题就解决了。一般电脑里会有好几个USB根集线器设备，可以都去修改一下。</p>

<p>如果只想修改键盘的 USB 电源策略，可以点击菜单栏-&gt;查看-&gt;按连接列出设备，以此来寻找<code class="language-plaintext highlighter-rouge">HID Keyboard Device</code>位于哪个USB根集线器下。在我的电脑上是这样的：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 基于 ACPI x64 的电脑 -&gt; Microsoft ACPI-Compliant System -&gt; PCI Express 根复合体 -&gt; PCI Express 根端口 -&gt; AMD USB 2.0 可扩展主机控制器 -&gt; USB 根集线器 (USB 3.0) -&gt; USB输入设备 -&gt; HID Keyboard Device
</code></pre></div></div>
<p>如图：
<img src="/post_assets/images/2026/04/16-device-manager.png" alt="alt text" /></p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="platforms" /><category term="windows" /><summary type="html"><![CDATA[问题描述 最近在使用笔记本电脑时，当系统负载较高时，键盘会出现间歇性失灵的情况。表现为大概只要五秒没有按下键盘，键盘就会失灵。此时按下键盘是没有反应的，需要过将近十秒左右键盘才能恢复正常使用。而此后如果超过五秒没有按下键盘，键盘又会再次失灵。这个问题非常影响使用体验。]]></summary></entry><entry><title type="html">Windows 10 Microsoft Compatibility Telemetry 占用 CPU 过高</title><link href="/platforms/2026/04/13/Windows-10-Microsoft-Compatibility-Telemetry-%E5%8D%A0%E7%94%A8-CPU-%E8%BF%87%E9%AB%98.html" rel="alternate" type="text/html" title="Windows 10 Microsoft Compatibility Telemetry 占用 CPU 过高" /><published>2026-04-13T11:30:00+00:00</published><updated>2026-04-13T11:30:00+00:00</updated><id>/platforms/2026/04/13/Windows%2010%20Microsoft%20Compatibility%20Telemetry%20%E5%8D%A0%E7%94%A8%20CPU%20%E8%BF%87%E9%AB%98</id><content type="html" xml:base="/platforms/2026/04/13/Windows-10-Microsoft-Compatibility-Telemetry-%E5%8D%A0%E7%94%A8-CPU-%E8%BF%87%E9%AB%98.html"><![CDATA[<p>Windows 10 使用中，发现有时 Microsoft Compatibility Telemetry 进程占用 CPU 过高，直接占用高达 99%，导致机器卡顿。可以参考以下步骤来禁用 Microsoft Compatibility Telemetry：</p>

<p>https://www.cnblogs.com/ls1519/p/12867055.html#:~:text=Win10%E4%BD%BF%E7%94%A8%E4%B8%AD%EF%BC%8C%E5%8F%91%E7%8E%B0Microsoft%20Compatibility%20Telemetry%E8%BF%9B%E7%A8%8B%E5%8D%A0%E7%94%A8CPU%E8%BF%87%E9%AB%98%EF%BC%8C%E5%AF%BC%E8%87%B4%E6%9C%BA%E5%99%A8%E5%8D%A1%E9%A1%BF%E3%80%82%20<em>%20Microsoft%20Compatibility%20Telemetry%E6%98%AF%E5%BE%AE%E8%BD%AF%E4%B8%8B%E7%9A%84%E4%B8%80%E4%B8%AA%E7%9B%91%E6%B5%8B%E6%95%B0%E6%8D%AE%E6%94%B6%E9%9B%86%E6%9C%8D%E5%8A%A1%EF%BC%8C%E5%A6%82%E6%9E%9C%E5%8A%A0%E5%85%A5microsoft,Experience%EF%BC%9B%20</em>%203.%E5%9C%A8%E5%8F%B3%E4%BE%A7%E6%89%93%E5%BC%80%E7%9A%84%E7%AA%97%E5%8F%A3%E4%B8%AD%EF%BC%8C%E6%89%BE%E5%88%B0Microsoft%20Compatibility%20Appraiser%E8%AE%A1%E5%88%92%E4%BB%BB%E5%8A%A1%EF%BC%8C%E7%82%B9%E5%87%BB%E9%BC%A0%E6%A0%87%E5%8F%B3%E9%94%AE%2D%E9%80%89%E6%8B%A9%E2%80%9C%E7%A6%81%E7%94%A8%E2%80%9D%EF%BC%9B%20*%204.%E9%87%8D%E5%90%AF%E7%94%B5%E8%84%91%E5%8D%B3%E5%8F%AF%E3%80%82</p>

<p>右键 <code class="language-plaintext highlighter-rouge">此电脑</code> -&gt; <code class="language-plaintext highlighter-rouge">管理</code> -&gt; <code class="language-plaintext highlighter-rouge">系统工具</code> -&gt; <code class="language-plaintext highlighter-rouge">任务计划程序库</code> -&gt; <code class="language-plaintext highlighter-rouge">Microsoft</code> -&gt; <code class="language-plaintext highlighter-rouge">Windows</code> -&gt; <code class="language-plaintext highlighter-rouge">Application Experience</code>，找到 <code class="language-plaintext highlighter-rouge">Microsoft Compatibility Appraiser</code> 计划任务，右键选择“禁用”，重启电脑即可。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="platforms" /><category term="windows" /><summary type="html"><![CDATA[Windows 10 使用中，发现有时 Microsoft Compatibility Telemetry 进程占用 CPU 过高，直接占用高达 99%，导致机器卡顿。可以参考以下步骤来禁用 Microsoft Compatibility Telemetry：]]></summary></entry><entry><title type="html">Python fails to output ANSI color codes for the terminal</title><link href="/troubleshooting/2026/04/13/Python-fails-to-output-ANSI-color-codes-for-the-terminal.html" rel="alternate" type="text/html" title="Python fails to output ANSI color codes for the terminal" /><published>2026-04-13T06:30:00+00:00</published><updated>2026-04-13T06:30:00+00:00</updated><id>/troubleshooting/2026/04/13/Python%20fails%20to%20output%20ANSI%20color%20codes%20for%20the%20terminal</id><content type="html" xml:base="/troubleshooting/2026/04/13/Python-fails-to-output-ANSI-color-codes-for-the-terminal.html"><![CDATA[<p>在 python 里使用 ANSI 转义序列来输出彩色文本时，可能会遇到在 Windows 终端上无效的问题。
参考：
https://stackoverflow.com/questions/40754673/python-fails-to-output-ansi-color-codes-for-the-terminal</p>

<p>可以使用以下代码来启用 Windows 终端对 ANSI 转义序列的支持：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">os</span>
<span class="kn">import</span> <span class="n">sys</span>

<span class="k">if</span> <span class="n">sys</span><span class="p">.</span><span class="n">platform</span> <span class="o">==</span> <span class="sh">"</span><span class="s">win32</span><span class="sh">"</span><span class="p">:</span>
    <span class="n">os</span><span class="p">.</span><span class="nf">system</span><span class="p">(</span><span class="sh">''</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="python" /><summary type="html"><![CDATA[在 python 里使用 ANSI 转义序列来输出彩色文本时，可能会遇到在 Windows 终端上无效的问题。 参考： https://stackoverflow.com/questions/40754673/python-fails-to-output-ansi-color-codes-for-the-terminal]]></summary></entry><entry><title type="html">联想 R9000P 安装 windows 10 并迁移旧系统数据</title><link href="/platforms/2026/04/09/%E8%81%94%E6%83%B3R9000P%E5%AE%89%E8%A3%85win10%E5%B9%B6%E8%BF%81%E7%A7%BB%E6%97%A7%E7%B3%BB%E7%BB%9F%E6%95%B0%E6%8D%AE.html" rel="alternate" type="text/html" title="联想 R9000P 安装 windows 10 并迁移旧系统数据" /><published>2026-04-09T11:30:00+00:00</published><updated>2026-04-09T11:30:00+00:00</updated><id>/platforms/2026/04/09/%E8%81%94%E6%83%B3R9000P%E5%AE%89%E8%A3%85win10%E5%B9%B6%E8%BF%81%E7%A7%BB%E6%97%A7%E7%B3%BB%E7%BB%9F%E6%95%B0%E6%8D%AE</id><content type="html" xml:base="/platforms/2026/04/09/%E8%81%94%E6%83%B3R9000P%E5%AE%89%E8%A3%85win10%E5%B9%B6%E8%BF%81%E7%A7%BB%E6%97%A7%E7%B3%BB%E7%BB%9F%E6%95%B0%E6%8D%AE.html"><![CDATA[<p>我想给联想 R9000P ARX8 安装 windows 10 专业版系统，不过联想官网只提供了 windows 11 的驱动程序。这里记录一下安装过程和遇到的问题。</p>

<h2 id="1-数据迁移">1. 数据迁移</h2>
<p>我使用的是 macrium reflect 进行数据迁移，可以免费试用30天，如果只用一次的话完全够了。也可考虑旧版本的免费版：</p>

<p><a href="https://archive.org/details/reflect_setup_free_x64_202402">https://archive.org/details/reflect_setup_free_x64_202402</a></p>

<p>在旧系统上（windows 10 专业版）先用 macrium reflect 分别给每个盘创建一个镜像文件到外置硬盘上。然后在新系统上安装 macrium reflect，先把非系统盘的数据恢复到新系统的非系统盘上，注意在恢复时，手动拖到未分配的空间上，不要直接恢复，不然会覆盖掉新系统的分区表。所以需要先在新系统上删除非系统盘的分区，变成未分配的空间，然后再恢复数据。完成非系统盘的恢复后，再恢复系统盘，这时 macrium reflect 会自动创建 PE 启动盘，按照提示重启电脑，进入 PE 环境后继续恢复系统盘。恢复完成后重启电脑，就可以进入新的 windows 10 系统了。</p>

<h2 id="2-驱动安装">2. 驱动安装</h2>
<p>可以考虑使用驱动精灵等第三方驱动安装工具，不过非会员下载速度极慢。</p>

<p>也可以打开设备管理器，在其他设备里找到那些未知设备，以及所有有叹号的设备，点开属性，查看详细信息，找到设备的硬件 ID，然后在网上搜索这个 ID，找到对应的驱动程序进行安装。某些驱动比如 AMD 芯片组、英伟达显卡等，可以去官网上下载对应型号的驱动程序进行安装。其他可以去例如：<a href="https://www.catalog.update.microsoft.com/Search.aspx">https://www.catalog.update.microsoft.com/Search.aspx</a> 等网站上搜索对应的驱动程序进行安装。</p>

<h2 id="3-其他问题">3. 其他问题</h2>
<h3 id="31-启动项问题">3.1. 启动项问题</h3>
<p>第一次备份恢复时，不小心把原有的系统盘给覆盖了。只能重新安装 windows 10 专业版系统。此时在启动电脑时，会出现三个选项：</p>
<ul>
  <li>Windows 10 （目前的系统）</li>
  <li>Windows 10 （之前的系统）</li>
  <li>Windows 10 PE （macrium reflect 的 PE 启动盘）</li>
</ul>

<p>上面的第二和第三个选项都是无法进入系统的。此时需要进入系统后，用管理员打开命令提示符，输入 <code class="language-plaintext highlighter-rouge">bcdedit</code> 命令，查看当前的启动项。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bcdedit /enum
</code></pre></div></div>
<p>找到之前的系统和 PE 启动盘的启动项，记下它们的标识符（identifier），然后使用以下命令删除它们：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bcdedit /delete {identifier}
# 比如
bcdedit /delete {9f123abc-xxxx-xxxx-xxxx-xxxxxxxx}
</code></pre></div></div>

<h3 id="32-多个恢复分区问题">3.2. 多个恢复分区问题</h3>
<p>原电脑本身有一个恢复分区，现在电脑也有一个，数据迁移后变成了：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EFI系统分区(260MB) | C (237.23GB) | 恢复分区(1000MB) | 未分配(250.02GB) | D(472.51GB) | E(472.66GB) | F(302.73GB) | 未分配(170.55GB) | 恢复分区(797MB) |
</code></pre></div></div>
<p>可以考虑删除原来的恢复分区，或者把它合并到系统盘里。删除恢复分区后，系统盘就会变成未分配的空间，可以使用磁盘管理工具把它合并到系统盘里。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>diskpart
list disk
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 磁盘 ###  状态           大小     可用     Dyn  Gpt
  --------  -------------  -------  -------  ---  ---
  磁盘 0    联机             1907 GB   420 GB        *
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select disk 0
list partition
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
  分区 ###       类型              大小     偏移量
  -------------  ----------------  -------  -------
  分区      1    系统                 260 MB  1024 KB
  分区      2    保留                  16 MB   261 MB
  分区      3    主要                 237 GB   277 MB
  分区      8    恢复                1000 MB   237 GB
  分区      5    主要                 472 GB   488 GB
  分区      6    主要                 472 GB   961 GB
  分区      7    主要                 302 GB  1433 GB
  分区      4    恢复                 797 MB  1906 GB
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select partition 8
delete partition override
</code></pre></div></div>

<p>然后就可以在磁盘管理工具里把系统盘和未分配的空间合并了。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="platforms" /><category term="windows" /><summary type="html"><![CDATA[我想给联想 R9000P ARX8 安装 windows 10 专业版系统，不过联想官网只提供了 windows 11 的驱动程序。这里记录一下安装过程和遇到的问题。]]></summary></entry><entry><title type="html">Rust extern “C”找不到__stderrp, __stdinp</title><link href="/troubleshooting/2026/03/27/Rust-Rust-extern-C%E6%89%BE%E4%B8%8D%E5%88%B0__stderrp-__stdinp.html" rel="alternate" type="text/html" title="Rust extern “C”找不到__stderrp, __stdinp" /><published>2026-03-27T07:30:00+00:00</published><updated>2026-03-27T07:30:00+00:00</updated><id>/troubleshooting/2026/03/27/Rust%20Rust%20extern%20C%E6%89%BE%E4%B8%8D%E5%88%B0__stderrp%20__stdinp</id><content type="html" xml:base="/troubleshooting/2026/03/27/Rust-Rust-extern-C%E6%89%BE%E4%B8%8D%E5%88%B0__stderrp-__stdinp.html"><![CDATA[<p>在运行 Rust 程序时，遇到了以下错误：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>extern "C"找不到__stderrp, __stdinp
</code></pre></div></div>

<p>这个错误提示是说 Rust 程序在链接时找不到 <code class="language-plaintext highlighter-rouge">__stderrp</code> 和 <code class="language-plaintext highlighter-rouge">__stdinp</code> 这两个符号。这通常是因为 Rust 程序在链接时没有正确链接到 C 标准库。而这些 <code class="language-plaintext highlighter-rouge">_stderrp</code> 和 <code class="language-plaintext highlighter-rouge">__stdinp</code> 是 Darwin 系统内部变量，在 Windows 或 Linux 上是不存在的。所以会出现这个错误。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="rust" /><summary type="html"><![CDATA[在运行 Rust 程序时，遇到了以下错误：]]></summary></entry><entry><title type="html">使用 Clippy 统计 Rust 项目中的 lint</title><link href="/troubleshooting/2026/03/27/%E4%BD%BF%E7%94%A8Clippy%E7%BB%9F%E8%AE%A1Rust%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84lint.html" rel="alternate" type="text/html" title="使用 Clippy 统计 Rust 项目中的 lint" /><published>2026-03-27T03:30:00+00:00</published><updated>2026-03-27T03:30:00+00:00</updated><id>/troubleshooting/2026/03/27/%E4%BD%BF%E7%94%A8Clippy%E7%BB%9F%E8%AE%A1Rust%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84lint</id><content type="html" xml:base="/troubleshooting/2026/03/27/%E4%BD%BF%E7%94%A8Clippy%E7%BB%9F%E8%AE%A1Rust%E9%A1%B9%E7%9B%AE%E4%B8%AD%E7%9A%84lint.html"><![CDATA[<p>使用 Clippy 来检查 Rust 项目中的 lint 是一个很好的实践，可以帮助我们发现代码中的潜在问题和改进点。下面是一个示例脚本，展示了如何使用 Python 来运行 Clippy 并统计 lint 的数量。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">import</span> <span class="n">json</span>
<span class="kn">import</span> <span class="n">csv</span>
<span class="kn">from</span> <span class="n">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">from</span> <span class="n">collections</span> <span class="kn">import</span> <span class="n">Counter</span>

<span class="n">ROOT_DIR</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/path/to/rust/projects</span><span class="sh">"</span>

<span class="n">output_name</span> <span class="o">=</span> <span class="n">ROOT_DIR</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s">/</span><span class="sh">"</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">].</span><span class="nf">replace</span><span class="p">(</span><span class="sh">"</span><span class="s">dataset-</span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">)</span>

<span class="n">OUTPUT_CSV</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="s">clippy-output/</span><span class="si">{</span><span class="n">output_name</span><span class="si">}</span><span class="s">_clippy.csv</span><span class="sh">"</span>


<span class="k">def</span> <span class="nf">find_rust_projects</span><span class="p">(</span><span class="n">root</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">查找 Rust 项目目录，兼容 translation_raw 结构</span><span class="sh">"""</span>
    <span class="n">projects</span> <span class="o">=</span> <span class="nf">set</span><span class="p">()</span>

    <span class="n">root</span> <span class="o">=</span> <span class="nc">Path</span><span class="p">(</span><span class="n">root</span><span class="p">)</span>

    <span class="c1"># 原有逻辑：查找 Cargo.toml
</span>    <span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">root</span><span class="p">.</span><span class="nf">rglob</span><span class="p">(</span><span class="sh">"</span><span class="s">Cargo.toml</span><span class="sh">"</span><span class="p">):</span>
        <span class="n">projects</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">path</span><span class="p">.</span><span class="n">parent</span><span class="p">)</span>

    <span class="c1"># 新增逻辑：查找 translation_raw / translation_raw_WIP
</span>    <span class="k">for</span> <span class="n">dir_path</span> <span class="ow">in</span> <span class="n">root</span><span class="p">.</span><span class="nf">rglob</span><span class="p">(</span><span class="sh">"</span><span class="s">*</span><span class="sh">"</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">dir_path</span><span class="p">.</span><span class="nf">is_dir</span><span class="p">():</span>
            <span class="k">continue</span>

        <span class="n">cargo</span> <span class="o">=</span> <span class="n">dir_path</span> <span class="o">/</span> <span class="sh">"</span><span class="s">Cargo.toml</span><span class="sh">"</span>
        <span class="k">if</span> <span class="n">cargo</span><span class="p">.</span><span class="nf">exists</span><span class="p">():</span>
            <span class="k">continue</span>

        <span class="n">translation_raw</span> <span class="o">=</span> <span class="n">dir_path</span> <span class="o">/</span> <span class="sh">"</span><span class="s">translation_raw</span><span class="sh">"</span>
        <span class="n">translation_raw_wip</span> <span class="o">=</span> <span class="n">dir_path</span> <span class="o">/</span> <span class="sh">"</span><span class="s">translation_raw_WIP</span><span class="sh">"</span>

        <span class="k">if</span> <span class="n">translation_raw</span><span class="p">.</span><span class="nf">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="n">translation_raw</span><span class="p">.</span><span class="nf">is_dir</span><span class="p">():</span>
            <span class="n">projects</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">translation_raw</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">translation_raw_wip</span><span class="p">.</span><span class="nf">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="n">translation_raw_wip</span><span class="p">.</span><span class="nf">is_dir</span><span class="p">():</span>
            <span class="n">projects</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">translation_raw_wip</span><span class="p">)</span>

    <span class="k">return</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">projects</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">run_clippy</span><span class="p">(</span><span class="n">project_path</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">运行 clippy 并统计 lint</span><span class="sh">"""</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span>
        <span class="sh">"</span><span class="s">cargo</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">clippy</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">--message-format=json</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">--all-targets</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">--all-features</span><span class="sh">"</span>
    <span class="p">]</span>

    <span class="n">process</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nc">Popen</span><span class="p">(</span>
        <span class="n">cmd</span><span class="p">,</span>
        <span class="n">cwd</span><span class="o">=</span><span class="n">project_path</span><span class="p">,</span>
        <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">PIPE</span><span class="p">,</span>
        <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">PIPE</span><span class="p">,</span>
        <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>

    <span class="n">lint_counter</span> <span class="o">=</span> <span class="nc">Counter</span><span class="p">()</span>
    <span class="n">level_counter</span> <span class="o">=</span> <span class="nc">Counter</span><span class="p">()</span>

    <span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">process</span><span class="p">.</span><span class="n">stdout</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">msg</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">json</span><span class="p">.</span><span class="n">JSONDecodeError</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="k">if</span> <span class="n">msg</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">reason</span><span class="sh">"</span><span class="p">)</span> <span class="o">!=</span> <span class="sh">"</span><span class="s">compiler-message</span><span class="sh">"</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">message</span> <span class="o">=</span> <span class="n">msg</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">message</span><span class="sh">"</span><span class="p">,</span> <span class="p">{})</span>
        <span class="n">code</span> <span class="o">=</span> <span class="n">message</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">code</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">level</span> <span class="o">=</span> <span class="n">message</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">if</span> <span class="ow">not</span> <span class="n">code</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">lint_name</span> <span class="o">=</span> <span class="n">code</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">code</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">lint_name</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="c1"># 只统计 clippy
</span>        <span class="k">if</span> <span class="ow">not</span> <span class="n">lint_name</span><span class="p">.</span><span class="nf">startswith</span><span class="p">(</span><span class="sh">"</span><span class="s">clippy::</span><span class="sh">"</span><span class="p">):</span>
            <span class="k">continue</span>

        <span class="n">lint_counter</span><span class="p">[</span><span class="n">lint_name</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">level_counter</span><span class="p">[</span><span class="n">level</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>

    <span class="n">process</span><span class="p">.</span><span class="nf">wait</span><span class="p">()</span>

    <span class="k">return</span> <span class="n">lint_counter</span><span class="p">,</span> <span class="n">level_counter</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">projects</span> <span class="o">=</span> <span class="nf">find_rust_projects</span><span class="p">(</span><span class="n">ROOT_DIR</span><span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Found </span><span class="si">{</span><span class="nf">len</span><span class="p">(</span><span class="n">projects</span><span class="p">)</span><span class="si">}</span><span class="s"> Rust projects</span><span class="sh">"</span><span class="p">)</span>

    <span class="n">rows</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">project</span> <span class="ow">in</span> <span class="n">projects</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Running clippy: </span><span class="si">{</span><span class="n">project</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="n">lint_counter</span><span class="p">,</span> <span class="n">level_counter</span> <span class="o">=</span> <span class="nf">run_clippy</span><span class="p">(</span><span class="n">project</span><span class="p">)</span>

        <span class="c1"># 保留最后一段路径
</span>        <span class="n">project_path</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">project</span><span class="p">)</span>
        <span class="n">project_name</span> <span class="o">=</span> <span class="n">project_path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s">/</span><span class="sh">"</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>
        <span class="k">if</span> <span class="n">project_name</span> <span class="o">==</span> <span class="sh">"</span><span class="s">translation_raw</span><span class="sh">"</span> <span class="ow">or</span> <span class="n">project_name</span> <span class="o">==</span> <span class="sh">"</span><span class="s">translation_raw_WIP</span><span class="sh">"</span><span class="p">:</span>
            <span class="n">suffix</span> <span class="o">=</span> <span class="sh">"</span><span class="s">_raw</span><span class="sh">"</span> <span class="k">if</span> <span class="n">project_name</span> <span class="o">==</span> <span class="sh">"</span><span class="s">translation_raw</span><span class="sh">"</span> <span class="k">else</span> <span class="sh">"</span><span class="s">_opt</span><span class="sh">"</span>
            <span class="n">project_name</span> <span class="o">=</span> <span class="n">project_path</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s">/</span><span class="sh">"</span><span class="p">)[</span><span class="o">-</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="n">suffix</span>


        <span class="n">row</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">project</span><span class="sh">"</span><span class="p">:</span> <span class="n">project_name</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">warnings</span><span class="sh">"</span><span class="p">:</span> <span class="n">level_counter</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">warning</span><span class="sh">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
            <span class="sh">"</span><span class="s">errors</span><span class="sh">"</span><span class="p">:</span> <span class="n">level_counter</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
            <span class="sh">"</span><span class="s">total</span><span class="sh">"</span><span class="p">:</span> <span class="nf">sum</span><span class="p">(</span><span class="n">level_counter</span><span class="p">.</span><span class="nf">values</span><span class="p">())</span>
        <span class="p">}</span>

        <span class="c1"># 可选：加入常见 lint
</span>        <span class="k">for</span> <span class="n">lint</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">lint_counter</span><span class="p">.</span><span class="nf">items</span><span class="p">():</span>
            <span class="n">row</span><span class="p">[</span><span class="n">lint</span><span class="p">]</span> <span class="o">=</span> <span class="n">count</span>

        <span class="n">rows</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">row</span><span class="p">)</span>

    <span class="c1"># 收集所有列名
</span>    <span class="n">fieldnames</span> <span class="o">=</span> <span class="nf">set</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
        <span class="n">fieldnames</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">r</span><span class="p">.</span><span class="nf">keys</span><span class="p">())</span>

    <span class="n">fieldnames</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">fieldnames</span><span class="p">)</span>
    <span class="c1"># 把 project, warnings, errors, total 放在前面
</span>    <span class="n">fieldnames</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">project</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">warnings</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">errors</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">total</span><span class="sh">"</span><span class="p">]</span> <span class="o">+</span> <span class="p">[</span><span class="n">f</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">fieldnames</span> <span class="k">if</span> <span class="n">f</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">{</span><span class="sh">"</span><span class="s">project</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">warnings</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">errors</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">total</span><span class="sh">"</span><span class="p">}]</span>

    <span class="c1"># 写入 CSV
</span>    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">OUTPUT_CSV</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">,</span> <span class="n">newline</span><span class="o">=</span><span class="sh">""</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="nc">DictWriter</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">fieldnames</span><span class="o">=</span><span class="n">fieldnames</span><span class="p">)</span>
        <span class="n">writer</span><span class="p">.</span><span class="nf">writeheader</span><span class="p">()</span>
        <span class="n">writer</span><span class="p">.</span><span class="nf">writerows</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Saved to </span><span class="si">{</span><span class="n">OUTPUT_CSV</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="rust" /><summary type="html"><![CDATA[使用 Clippy 来检查 Rust 项目中的 lint 是一个很好的实践，可以帮助我们发现代码中的潜在问题和改进点。下面是一个示例脚本，展示了如何使用 Python 来运行 Clippy 并统计 lint 的数量。]]></summary></entry><entry><title type="html">OpenClaw exec host not allowed</title><link href="/troubleshooting/2026/03/12/OpenClaw-exec-host-not-allowed.html" rel="alternate" type="text/html" title="OpenClaw exec host not allowed" /><published>2026-03-12T08:30:00+00:00</published><updated>2026-03-12T08:30:00+00:00</updated><id>/troubleshooting/2026/03/12/OpenClaw%20exec%20host%20not%20allowed</id><content type="html" xml:base="/troubleshooting/2026/03/12/OpenClaw-exec-host-not-allowed.html"><![CDATA[<p>在使用 OpenClaw 时，遇到了以下错误：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>exec host not allowed (requested sandbox; configure tools.exec.host=gateway to allow)
</code></pre></div></div>

<p>这个错误提示是说当前的 OpenClaw 配置不允许执行主机上的命令。要解决这个问题，需要在 OpenClaw 的配置文件中添加以下配置：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tools.exec.host=gateway
</code></pre></div></div>

<p>然后在 OpenClaw 中执行以下命令：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/exec host=gateway
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="openclaw" /><category term="ai" /><summary type="html"><![CDATA[在使用 OpenClaw 时，遇到了以下错误：]]></summary></entry><entry><title type="html">Markdown文件拆分</title><link href="/troubleshooting/2026/03/12/Markdown%E6%96%87%E4%BB%B6%E6%8B%86%E5%88%86.html" rel="alternate" type="text/html" title="Markdown文件拆分" /><published>2026-03-12T07:00:00+00:00</published><updated>2026-03-12T07:00:00+00:00</updated><id>/troubleshooting/2026/03/12/Markdown%E6%96%87%E4%BB%B6%E6%8B%86%E5%88%86</id><content type="html" xml:base="/troubleshooting/2026/03/12/Markdown%E6%96%87%E4%BB%B6%E6%8B%86%E5%88%86.html"><![CDATA[<p>将一个 Markdown 文件根据标题结构拆分成多个文件，可以使用以下 Python 脚本来实现。这个脚本支持两种模式：一种是将每个标题对应的内容拆分成单独的文件，另一种是根据标题层级创建目录结构。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">os</span>
<span class="kn">import</span> <span class="n">re</span>
<span class="kn">import</span> <span class="n">argparse</span>
<span class="kn">from</span> <span class="n">collections</span> <span class="kn">import</span> <span class="n">defaultdict</span>


<span class="k">def</span> <span class="nf">clean_title</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">去除markdown格式并清理非法文件名字符</span><span class="sh">"""</span>
    <span class="c1"># 去掉markdown格式
</span>    <span class="n">title</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">[*_`~]</span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">,</span> <span class="n">title</span><span class="p">)</span>

    <span class="c1"># 去掉链接
</span>    <span class="n">title</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">\[(.*?)\]\(.*?\)</span><span class="sh">"</span><span class="p">,</span> <span class="sa">r</span><span class="sh">"</span><span class="s">\1</span><span class="sh">"</span><span class="p">,</span> <span class="n">title</span><span class="p">)</span>

    <span class="c1"># 去掉图片
</span>    <span class="n">title</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">"</span><span class="s">!\[(.*?)\]\(.*?\)</span><span class="sh">"</span><span class="p">,</span> <span class="sa">r</span><span class="sh">"</span><span class="s">\1</span><span class="sh">"</span><span class="p">,</span> <span class="n">title</span><span class="p">)</span>

    <span class="c1"># 清理非法字符
</span>    <span class="n">title</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">'</span><span class="s">[\\/:</span><span class="sh">"</span><span class="s">*?&lt;&gt;|]+</span><span class="sh">'</span><span class="p">,</span> <span class="sh">""</span><span class="p">,</span> <span class="n">title</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">title</span><span class="p">.</span><span class="nf">strip</span><span class="p">()</span>


<span class="k">def</span> <span class="nf">parse_markdown</span><span class="p">(</span><span class="n">file_path</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">解析markdown标题结构</span><span class="sh">"""</span>
    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="sh">"</span><span class="s">r</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">lines</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">readlines</span><span class="p">()</span>

    <span class="n">headers</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">line</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">lines</span><span class="p">):</span>
        <span class="n">m</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sa">r</span><span class="sh">'</span><span class="s">^(#+)\s+(.*)</span><span class="sh">'</span><span class="p">,</span> <span class="n">line</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">m</span><span class="p">:</span>
            <span class="n">level</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
            <span class="n">title</span> <span class="o">=</span> <span class="n">m</span><span class="p">.</span><span class="nf">group</span><span class="p">(</span><span class="mi">2</span><span class="p">).</span><span class="nf">strip</span><span class="p">()</span>
            <span class="n">headers</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
                <span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">:</span> <span class="n">level</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">:</span> <span class="n">title</span><span class="p">,</span>
                <span class="sh">"</span><span class="s">line</span><span class="sh">"</span><span class="p">:</span> <span class="n">i</span>
            <span class="p">})</span>

    <span class="k">return</span> <span class="n">lines</span><span class="p">,</span> <span class="n">headers</span>


<span class="k">def</span> <span class="nf">find_base_level</span><span class="p">(</span><span class="n">headers</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">找到文档中最高级标题</span><span class="sh">"""</span>
    <span class="k">return</span> <span class="nf">min</span><span class="p">(</span><span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="n">headers</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">ensure_unique</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">counter</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">同级重名处理</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="n">counter</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="n">counter</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="k">return</span> <span class="n">name</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">new_name</span> <span class="o">=</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">(</span><span class="si">{</span><span class="n">counter</span><span class="p">[</span><span class="n">name</span><span class="p">]</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span>
        <span class="n">counter</span><span class="p">[</span><span class="n">name</span><span class="p">]</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="k">return</span> <span class="n">new_name</span>


<span class="k">def</span> <span class="nf">split_markdown</span><span class="p">(</span><span class="n">lines</span><span class="p">,</span> <span class="n">headers</span><span class="p">,</span> <span class="n">base_level</span><span class="p">,</span> <span class="n">max_level</span><span class="p">,</span> <span class="n">min_level</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">根据标题切片</span><span class="sh">"""</span>
    <span class="n">sections</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">h</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">headers</span><span class="p">):</span>

        <span class="n">rel_level</span> <span class="o">=</span> <span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]</span> <span class="o">-</span> <span class="n">base_level</span> <span class="o">+</span> <span class="mi">1</span>

        <span class="k">if</span> <span class="n">rel_level</span> <span class="o">&lt;</span> <span class="n">max_level</span> <span class="ow">or</span> <span class="n">rel_level</span> <span class="o">&gt;</span> <span class="n">min_level</span><span class="p">:</span>
            <span class="k">continue</span>

        <span class="n">start</span> <span class="o">=</span> <span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">line</span><span class="sh">"</span><span class="p">]</span>
        <span class="n">end</span> <span class="o">=</span> <span class="nf">len</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">headers</span><span class="p">)):</span>
            <span class="k">if</span> <span class="n">headers</span><span class="p">[</span><span class="n">j</span><span class="p">][</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]</span> <span class="o">&lt;=</span> <span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]:</span>
                <span class="n">end</span> <span class="o">=</span> <span class="n">headers</span><span class="p">[</span><span class="n">j</span><span class="p">][</span><span class="sh">"</span><span class="s">line</span><span class="sh">"</span><span class="p">]</span>
                <span class="k">break</span>

        <span class="n">sections</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">:</span> <span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">:</span> <span class="n">rel_level</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">""</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">lines</span><span class="p">[</span><span class="n">start</span><span class="p">:</span><span class="n">end</span><span class="p">])</span>
        <span class="p">})</span>

    <span class="k">return</span> <span class="n">sections</span>


<span class="k">def</span> <span class="nf">write_file_mode</span><span class="p">(</span><span class="n">sections</span><span class="p">,</span> <span class="n">output_dir</span><span class="p">):</span>
    <span class="n">os</span><span class="p">.</span><span class="nf">makedirs</span><span class="p">(</span><span class="n">output_dir</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">counter</span> <span class="o">=</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="nb">int</span><span class="p">)</span>

    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">sections</span><span class="p">:</span>
        <span class="n">name</span> <span class="o">=</span> <span class="nf">clean_title</span><span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">])</span>
        <span class="n">name</span> <span class="o">=</span> <span class="nf">ensure_unique</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">counter</span><span class="p">)</span>

        <span class="n">path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">output_dir</span><span class="p">,</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">.md</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">write_tree_mode</span><span class="p">(</span><span class="n">sections</span><span class="p">,</span> <span class="n">output_dir</span><span class="p">,</span> <span class="n">max_level</span><span class="p">):</span>
    <span class="n">stack</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">counters</span> <span class="o">=</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="nb">int</span><span class="p">))</span>

    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">sections</span><span class="p">:</span>

        <span class="n">level</span> <span class="o">=</span> <span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]</span>

        <span class="k">while</span> <span class="nf">len</span><span class="p">(</span><span class="n">stack</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">level</span><span class="p">:</span>
            <span class="n">stack</span><span class="p">.</span><span class="nf">pop</span><span class="p">()</span>

        <span class="n">title</span> <span class="o">=</span> <span class="nf">clean_title</span><span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">title</span><span class="sh">"</span><span class="p">])</span>

        <span class="n">parent_key</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/</span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">stack</span><span class="p">)</span>

        <span class="n">title</span> <span class="o">=</span> <span class="nf">ensure_unique</span><span class="p">(</span><span class="n">title</span><span class="p">,</span> <span class="n">counters</span><span class="p">[</span><span class="n">parent_key</span><span class="p">])</span>

        <span class="n">stack</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">level</span> <span class="o">==</span> <span class="n">max_level</span><span class="p">:</span>
            <span class="n">dir_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">output_dir</span><span class="p">,</span> <span class="o">*</span><span class="n">stack</span><span class="p">)</span>
            <span class="n">os</span><span class="p">.</span><span class="nf">makedirs</span><span class="p">(</span><span class="n">dir_path</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

        <span class="k">else</span><span class="p">:</span>
            <span class="n">dir_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">output_dir</span><span class="p">,</span> <span class="o">*</span><span class="n">stack</span><span class="p">[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
            <span class="n">os</span><span class="p">.</span><span class="nf">makedirs</span><span class="p">(</span><span class="n">dir_path</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

            <span class="n">path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">dir_path</span><span class="p">,</span> <span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s">.md</span><span class="sh">"</span><span class="p">)</span>

            <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">])</span>


<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="p">.</span><span class="nc">ArgumentParser</span><span class="p">()</span>

    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">input_file</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">--output</span><span class="sh">"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="sh">"</span><span class="s">output</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">--mode</span><span class="sh">"</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">file</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">tree</span><span class="sh">"</span><span class="p">],</span> <span class="n">default</span><span class="o">=</span><span class="sh">"</span><span class="s">file</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">--max_level</span><span class="sh">"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">--min_level</span><span class="sh">"</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>

    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_args</span><span class="p">()</span>

    <span class="n">lines</span><span class="p">,</span> <span class="n">headers</span> <span class="o">=</span> <span class="nf">parse_markdown</span><span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">input_file</span><span class="p">)</span>

    <span class="k">if</span> <span class="ow">not</span> <span class="n">headers</span><span class="p">:</span>
        <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">No headers found.</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">return</span>

    <span class="n">base_level</span> <span class="o">=</span> <span class="nf">find_base_level</span><span class="p">(</span><span class="n">headers</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">file</span><span class="sh">"</span><span class="p">:</span>
        <span class="n">min_level</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">max_level</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">min_level</span><span class="p">:</span>
            <span class="n">min_level</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">min_level</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">min_level</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="n">h</span><span class="p">[</span><span class="sh">"</span><span class="s">level</span><span class="sh">"</span><span class="p">]</span> <span class="o">-</span> <span class="n">base_level</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">for</span> <span class="n">h</span> <span class="ow">in</span> <span class="n">headers</span><span class="p">)</span>

    <span class="n">sections</span> <span class="o">=</span> <span class="nf">split_markdown</span><span class="p">(</span>
        <span class="n">lines</span><span class="p">,</span>
        <span class="n">headers</span><span class="p">,</span>
        <span class="n">base_level</span><span class="p">,</span>
        <span class="n">args</span><span class="p">.</span><span class="n">max_level</span><span class="p">,</span>
        <span class="n">min_level</span>
    <span class="p">)</span>

    <span class="k">if</span> <span class="n">args</span><span class="p">.</span><span class="n">mode</span> <span class="o">==</span> <span class="sh">"</span><span class="s">file</span><span class="sh">"</span><span class="p">:</span>
        <span class="nf">write_file_mode</span><span class="p">(</span><span class="n">sections</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">output</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="nf">write_tree_mode</span><span class="p">(</span><span class="n">sections</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">output</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">max_level</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="python" /><summary type="html"><![CDATA[将一个 Markdown 文件根据标题结构拆分成多个文件，可以使用以下 Python 脚本来实现。这个脚本支持两种模式：一种是将每个标题对应的内容拆分成单独的文件，另一种是根据标题层级创建目录结构。]]></summary></entry><entry><title type="html">vscode ssh权限混乱问题.</title><link href="/troubleshooting/2026/02/23/vscode-ssh%E6%9D%83%E9%99%90%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="vscode ssh权限混乱问题." /><published>2026-02-23T08:00:00+00:00</published><updated>2026-02-23T08:00:00+00:00</updated><id>/troubleshooting/2026/02/23/vscode%20ssh%E6%9D%83%E9%99%90%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2026/02/23/vscode-ssh%E6%9D%83%E9%99%90%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98.html"><![CDATA[<p>使用vscode ssh连接服务器时，如果同时以多个用户连接了同一个服务器，有时会出现权限混乱的问题。比如先以user1连接服务器，再打开一个新窗口以root连接服务器，可以发现root这个窗口任何操作都没有权限，也就是似乎以user1的权限去打开了root的目录。</p>

<p>这个原因是因为在vscode ssh的配置文件里使用了相同的 Host，导致vscode ssh无法区分不同用户的连接，从而出现权限混乱的问题。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host 123.123.xxx.xxx
  HostName 123.123.xxx.xxx
  User devuser

Host 123.123.xxx.xxx
  HostName 123.123.xxx.xxx
  User root
</code></pre></div></div>

<p>解决这个问题的方法是给每个用户的连接配置不同的 Host，比如：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host devuser_123.123.xxx.xxx
  HostName 123.123.xxx.xxx
  User devuser
Host root_123.123.xxx.xxx
  HostName 123.123.xxx.xxx
  User root
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="vscode" /><category term="ssh" /><summary type="html"><![CDATA[使用vscode ssh连接服务器时，如果同时以多个用户连接了同一个服务器，有时会出现权限混乱的问题。比如先以user1连接服务器，再打开一个新窗口以root连接服务器，可以发现root这个窗口任何操作都没有权限，也就是似乎以user1的权限去打开了root的目录。]]></summary></entry><entry><title type="html">搭建简易的unturned游戏服务器管理界面</title><link href="/project-logs/games/2026/01/07/%E6%90%AD%E5%BB%BA%E7%AE%80%E6%98%93%E7%9A%84unturned%E6%B8%B8%E6%88%8F%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2.html" rel="alternate" type="text/html" title="搭建简易的unturned游戏服务器管理界面" /><published>2026-01-07T11:30:00+00:00</published><updated>2026-01-07T11:30:00+00:00</updated><id>/project-logs/games/2026/01/07/%E6%90%AD%E5%BB%BA%E7%AE%80%E6%98%93%E7%9A%84unturned%E6%B8%B8%E6%88%8F%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2</id><content type="html" xml:base="/project-logs/games/2026/01/07/%E6%90%AD%E5%BB%BA%E7%AE%80%E6%98%93%E7%9A%84unturned%E6%B8%B8%E6%88%8F%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AE%A1%E7%90%86%E7%95%8C%E9%9D%A2.html"><![CDATA[<h1 id="1-前言">1. 前言</h1>
<p>我想做一个简易的unturned游戏服务器管理界面，方便自己和朋友们管理游戏服务器。借助 AI 工具和自己的一些前后端知识，最终实现了这个目标。本文将分享我的开发过程和一些关键代码。</p>

<p>项目地址: <a href="https://github.com/lxmghct/unturned-server">https://github.com/lxmghct/unturned-server</a></p>

<h1 id="2-技术栈">2. 技术栈</h1>
<ul>
  <li>后端: Flask + Flask-SocketIO</li>
  <li>前端: HTML + CSS + JavaScript (axios + socket.io)</li>
</ul>

<h1 id="3-整体架构设计">3. 整体架构设计</h1>

<p>这个管理后台的核心目标很简单：通过 Web 界面控制一个在 Linux 上运行的 Unturned 专用服务器。</p>

<p>整体架构如下：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>浏览器
   ↓ (HTTP + Password Header)
Flask 后端
   ↓
tmux 会话
   ↓
Unturned 服务端进程
</code></pre></div></div>

<p>核心思想是：</p>
<ul>
  <li>Web 不直接管理进程</li>
  <li>所有服务器生命周期都交给 tmux</li>
  <li>后端只是作为一个“控制层”和“状态解析层”</li>
</ul>

<p>这样做的好处是：</p>
<ul>
  <li>即使 Flask 崩溃，服务器仍然在 tmux 中运行</li>
  <li>服务器和 Web 管理面板完全解耦</li>
  <li>不需要写复杂的进程守护逻辑</li>
</ul>

<p>使用 tmux 的好处在于它天然支持交互式控制台程序，可以方便地向 unturned 开服脚本打开的寄生终端发送命令，并且可以随时获取日志输出。</p>

<h1 id="4-服务器状态管理">4. 服务器状态管理</h1>

<h2 id="41-服务器状态识别机制">4.1. 服务器状态识别机制</h2>
<p>主要需要获取的信息有两个，即服务器的 Server Code 和当前激活的存档（Active Save）。
Unturned 启动后会在控制台输出：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server Code: XXXXX
Workshop install folder: /home/.../Servers/xxx/
</code></pre></div></div>
<p>不过考虑到获取当前存档并不依赖于日志，实际上在启动脚本时就可以知道当前存档名称。所以后续我在启动脚本中加入了一行：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"Using active_save: </span><span class="nv">$active_save</span><span class="s2">"</span>
</code></pre></div></div>
<p>这样直接读取日志第一行就能获取当前存档。</p>

<h2 id="42-状态分类">4.2. 状态分类</h2>

<p>服务器状态定义为三种：</p>

<table>
  <tbody>
    <tr>
      <td>状态</td>
      <td>含义</td>
    </tr>
    <tr>
      <td>0</td>
      <td>未运行</td>
    </tr>
    <tr>
      <td>1</td>
      <td>启动中（tmux存在但未解析到Server Code）</td>
    </tr>
    <tr>
      <td>2</td>
      <td>运行中（tmux存在且已解析到Server Code）</td>
    </tr>
  </tbody>
</table>

<p>判断逻辑：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if not session_exists:
    status = 0
elif session_exists and not server_code:
    status = 1
else:
    status = 2
</code></pre></div></div>

<h2 id="43-状态获取与持久化">4.3. 状态获取与持久化</h2>

<p>一个关键问题：服务器启动需要时间，Server Code 不是立即出现。</p>

<h3 id="431-第一版解决方案">4.3.1. 第一版解决方案</h3>

<p>启动服务器后，开一个后台线程，每秒抓取一次 tmux 输出，直到解析到 Server Code。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tmux_capture</span><span class="p">():</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">tmux</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">capture-pane</span><span class="sh">"</span><span class="p">,</span><span class="sh">"</span><span class="s">-S</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-32767</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-t</span><span class="sh">"</span><span class="p">,</span> <span class="n">TMUX_SESSION</span><span class="p">,</span> <span class="sh">"</span><span class="s">-p</span><span class="sh">"</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="n">stdout</span>

<span class="k">def</span> <span class="nf">wait_for_server_code</span><span class="p">():</span>
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="n">output</span> <span class="o">=</span> <span class="nf">tmux_capture</span><span class="p">()</span>
        <span class="n">server_code</span> <span class="o">=</span> <span class="nf">extract_server_code</span><span class="p">(</span><span class="n">output</span><span class="p">)</span>
        <span class="bp">...</span>
        <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<p>线程只在需要时启动：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">start_waiting_thread</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">waiting_thread</span> <span class="ow">is</span> <span class="bp">None</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">waiting_thread</span><span class="p">.</span><span class="nf">is_alive</span><span class="p">():</span>
        <span class="bp">...</span>
</code></pre></div></div>

<p>最后存储到全局变量：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">server_status</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">server_code</span><span class="sh">"</span><span class="p">:</span> <span class="bp">None</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">active_save</span><span class="sh">"</span><span class="p">:</span> <span class="bp">None</span>
<span class="p">}</span>
</code></pre></div></div>

<p>由于后端和 tmux 是解耦的，Flask 重启会丢失这个状态，所以需要存储到文件里以便下次恢复。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">STATUS_FILE</span> <span class="o">=</span> <span class="sh">"</span><span class="s">data/server-status.txt</span><span class="sh">"</span>

<span class="c1"># 当解析到 Server Code 后：
</span><span class="nf">save_status_to_file</span><span class="p">(</span><span class="n">server_code</span><span class="p">,</span> <span class="n">active_save</span><span class="p">)</span>

<span class="c1"># Flask 启动时：
</span><span class="k">if</span> <span class="nf">tmux_session_exists</span><span class="p">():</span>
    <span class="nf">load_status_from_file</span><span class="p">()</span>
</code></pre></div></div>

<p>缺点:
日志文件较大时会丢失部分重要信息，导致无法正确解析 Server Code。且日志较大时性能较差。前端不能过于频请求状态，否则会影响性能，因此前端状态不能及时更新。</p>

<h3 id="432-第二版改进方案">4.3.2. 第二版改进方案</h3>
<p>主要有两点改进：</p>

<ol>
  <li>tmux 的输出重定向到一个单独的日志文件，Flask 只读取这个日志文件，避免读取大量无关日志。</li>
  <li>使用 Flask-SocketIO 实现状态的实时推送，避免前端频繁轮询。</li>
</ol>

<p>启动脚本中添加：</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tmux pipe-pane <span class="nt">-o</span> <span class="s2">"cat &gt;&gt; </span><span class="nv">$log_path</span><span class="s2">"</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_read_log_pipe_thread</span><span class="p">():</span>
    <span class="sh">"""</span><span class="s">读取日志管道内容并写入日志文件</span><span class="sh">"""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">exists</span><span class="p">(</span><span class="n">LOG_FILE</span><span class="p">):</span>
        <span class="nf">open</span><span class="p">(</span><span class="n">LOG_FILE</span><span class="p">,</span> <span class="sh">"</span><span class="s">w</span><span class="sh">"</span><span class="p">).</span><span class="nf">close</span><span class="p">()</span>

    <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">LOG_FILE</span><span class="p">,</span> <span class="sh">"</span><span class="s">r</span><span class="sh">"</span><span class="p">)</span> <span class="k">as</span> <span class="n">log_file</span><span class="p">:</span>
        <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">stop_reading_flag</span><span class="p">:</span>
                <span class="k">break</span>
            <span class="n">line</span> <span class="o">=</span> <span class="n">log_file</span><span class="p">.</span><span class="nf">readline</span><span class="p">()</span>
            <span class="k">if</span> <span class="ow">not</span> <span class="n">line</span><span class="p">:</span>
                <span class="n">socketio</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="nf">load_status_from_string</span><span class="p">([</span><span class="n">line</span><span class="p">])</span>
                <span class="n">socketio</span><span class="p">.</span><span class="nf">emit</span><span class="p">(</span><span class="sh">"</span><span class="s">log_append</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
                    <span class="sh">"</span><span class="s">line</span><span class="sh">"</span><span class="p">:</span> <span class="n">line</span>
                <span class="p">})</span>
</code></pre></div></div>

<p>未实现的内容：</p>

<ul>
  <li>想采用 mkfifo 创建管道的方式来替代原有的读取日志文件的方式，但由于 pipe 需要持续监听，在设计上 tmux 和后端是解耦的，一旦后端终止，pipe 也会断开，导致无法持续监听日志输出。因此最终还是采用了定时读取日志文件的方式。</li>
</ul>

<h1 id="5-服务器终端交互">5. 服务器终端交互</h1>
<h2 id="51-判断是否存在会话">5.1. 判断是否存在会话</h2>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tmux_session_exists</span><span class="p">():</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">tmux</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">has-session</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-t</span><span class="sh">"</span><span class="p">,</span> <span class="n">TMUX_SESSION</span><span class="p">],</span>
        <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span><span class="p">,</span>
        <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">DEVNULL</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="n">returncode</span> <span class="o">==</span> <span class="mi">0</span>
</code></pre></div></div>

<h2 id="52-启动与退出">5.2. 启动与退出</h2>
<p>启动是使用<code class="language-plaintext highlighter-rouge">tmux new-session</code>而不是<code class="language-plaintext highlighter-rouge">tmux new</code>，这样可以保证当 unturned 的寄生终端退出后，tmux 会话也会自动关闭。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">cmd</span> <span class="o">=</span> <span class="n">RUN_SCRIPT</span> <span class="o">+</span> <span class="sa">f</span><span class="sh">"</span><span class="s"> active_save=</span><span class="si">{</span><span class="n">active_save</span><span class="si">}</span><span class="sh">"</span>
    <span class="k">if</span> <span class="n">update</span><span class="p">:</span>
        <span class="n">cmd</span> <span class="o">+=</span> <span class="sh">"</span><span class="s"> update=1</span><span class="sh">"</span>

    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">tmux</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">new-session</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-s</span><span class="sh">"</span><span class="p">,</span> <span class="n">TMUX_SESSION</span><span class="p">,</span> <span class="sh">"</span><span class="s">-d</span><span class="sh">"</span><span class="p">,</span> <span class="n">cmd</span><span class="p">]</span>
    <span class="p">)</span>
</code></pre></div></div>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tmux_send</span><span class="p">(</span><span class="n">cmd</span><span class="p">):</span>
    <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">tmux</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">send-keys</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-t</span><span class="sh">"</span><span class="p">,</span> <span class="n">TMUX_SESSION</span><span class="p">,</span> <span class="n">cmd</span><span class="p">,</span> <span class="sh">"</span><span class="s">Enter</span><span class="sh">"</span><span class="p">],</span>
        <span class="n">check</span><span class="o">=</span><span class="bp">False</span>
    <span class="p">)</span>

<span class="c1"># 退出
</span><span class="nf">send_tmux_command</span><span class="p">(</span><span class="sh">"</span><span class="s">shutdown</span><span class="sh">"</span><span class="p">)</span> 
</code></pre></div></div>

<h1 id="6-存档管理">6. 存档管理</h1>
<p>主要分为上传、下载、删除、备份、回档。存档其基本结构为：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Servers/
  ├── SaveA/
  │   ├── Bundles
  │   ├── Config.json
  │   ├── Config.txt
  │   ├── Level
  │   ├── Maps
  │   ├── Server
  │   ├── Workshop
  │   └── WorkshopDownloadConfig.json
  ├── SaveB/
</code></pre></div></div>

<h2 id="61-上传">6.1. 上传</h2>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@api.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/upload-save</span><span class="sh">"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">POST</span><span class="sh">"</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">upload_save</span><span class="p">():</span>
    <span class="k">if</span> <span class="sh">"</span><span class="s">file</span><span class="sh">"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">request</span><span class="p">.</span><span class="n">files</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">No file</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

    <span class="nb">file</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">files</span><span class="p">[</span><span class="sh">"</span><span class="s">file</span><span class="sh">"</span><span class="p">]</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">.</span><span class="nf">endswith</span><span class="p">(</span><span class="sh">"</span><span class="s">.zip</span><span class="sh">"</span><span class="p">):</span>
        <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Only zip allowed</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>
    
    <span class="nb">file</span><span class="p">.</span><span class="nf">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">os</span><span class="p">.</span><span class="n">SEEK_END</span><span class="p">)</span>
    <span class="n">size</span> <span class="o">=</span> <span class="nb">file</span><span class="p">.</span><span class="nf">tell</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">size</span> <span class="o">&gt;</span> <span class="mi">4</span> <span class="o">*</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">File too large</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>
    <span class="nb">file</span><span class="p">.</span><span class="nf">seek</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="k">with</span> <span class="n">tempfile</span><span class="p">.</span><span class="nc">TemporaryDirectory</span><span class="p">()</span> <span class="k">as</span> <span class="n">tmpdir</span><span class="p">:</span>
        <span class="n">zip_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">,</span> <span class="sh">"</span><span class="s">upload.zip</span><span class="sh">"</span><span class="p">)</span>
        <span class="nb">file</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">zip_path</span><span class="p">)</span>

        <span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="nc">ZipFile</span><span class="p">(</span><span class="n">zip_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">z</span><span class="p">:</span>
            <span class="n">z</span><span class="p">.</span><span class="nf">extractall</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">)</span>

        <span class="n">entries</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="nf">listdir</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">)</span>
        <span class="n">entries</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="sh">"</span><span class="s">upload.zip</span><span class="sh">"</span><span class="p">)</span>

        <span class="c1"># 确保 zip 中只有一个根目录
</span>        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">entries</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Zip must contain exactly one root directory</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

        <span class="n">root_dir_name</span> <span class="o">=</span> <span class="n">entries</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
        <span class="n">extracted_dir</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">,</span> <span class="n">root_dir_name</span><span class="p">)</span>

        <span class="c1"># 处理 zip 名称与文件夹名不同的情况
</span>        <span class="k">if</span> <span class="n">root_dir_name</span> <span class="o">!=</span> <span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">]:</span>
            <span class="n">shutil</span><span class="p">.</span><span class="nf">move</span><span class="p">(</span><span class="n">extracted_dir</span><span class="p">,</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">,</span> <span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">]))</span>
            <span class="n">extracted_dir</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">tmpdir</span><span class="p">,</span> <span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">])</span>

        <span class="c1"># 确保文件夹中有 Config.txt
</span>        <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">exists</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">extracted_dir</span><span class="p">,</span> <span class="sh">"</span><span class="s">Config.txt</span><span class="sh">"</span><span class="p">)):</span>
            <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Missing Config.txt in root directory</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

        <span class="n">save_name</span> <span class="o">=</span> <span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">[:</span><span class="o">-</span><span class="mi">4</span><span class="p">]</span>  <span class="c1"># 不带扩展名的文件名
</span>        <span class="n">dst</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">SAVES_DIR</span><span class="p">,</span> <span class="n">save_name</span><span class="p">)</span>

        <span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">exists</span><span class="p">(</span><span class="n">dst</span><span class="p">):</span>
            <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Save already exists</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

        <span class="c1"># 保存文件
</span>        <span class="n">shutil</span><span class="p">.</span><span class="nf">move</span><span class="p">(</span><span class="n">extracted_dir</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span>

    <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">message</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">Save </span><span class="si">{</span><span class="n">save_name</span><span class="si">}</span><span class="s"> uploaded successfully</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">save_name</span><span class="sh">"</span><span class="p">:</span> <span class="n">save_name</span>
    <span class="p">})</span>
</code></pre></div></div>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nf">uploadSave</span><span class="p">()</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">file</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">uploadFile</span><span class="dl">"</span><span class="p">).</span><span class="nx">files</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
    <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">file</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">请选择文件</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">file</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nf">endsWith</span><span class="p">(</span><span class="dl">"</span><span class="s2">.zip</span><span class="dl">"</span><span class="p">))</span> <span class="p">{</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">仅支持上传.zip文件</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">file</span><span class="p">.</span><span class="nx">size</span> <span class="o">&gt;</span> <span class="mi">4</span> <span class="o">*</span> <span class="mi">1024</span> <span class="o">*</span> <span class="mi">1024</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">文件过大，最大支持4MB</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">const</span> <span class="nx">formData</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FormData</span><span class="p">();</span>
    <span class="nx">formData</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="dl">"</span><span class="s2">file</span><span class="dl">"</span><span class="p">,</span> <span class="nx">file</span><span class="p">);</span>

    <span class="c1">// 检查存档是否重复</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">serverStatus</span><span class="p">.</span><span class="nx">saves</span><span class="p">.</span><span class="nf">includes</span><span class="p">(</span><span class="nx">file</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">.</span><span class="dl">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">]))</span> <span class="p">{</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">存档名重复，请选择其他存档名</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">try</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">request</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">"</span><span class="s2">/upload-save</span><span class="dl">"</span><span class="p">,</span> <span class="nx">formData</span><span class="p">,</span> <span class="p">{</span>
            <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
                <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">multipart/form-data</span><span class="dl">"</span><span class="p">,</span>
            <span class="p">},</span>
        <span class="p">});</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">上传成功</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">success</span><span class="dl">"</span><span class="p">);</span>
        <span class="nf">refreshSaves</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">save_name</span><span class="p">);</span> <span class="c1">// 刷新存档列表</span>
    <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">toast</span><span class="p">(</span><span class="dl">"</span><span class="s2">上传失败</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">error</span><span class="dl">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="62-下载">6.2. 下载</h2>
<p>这里的下载直接从备份里下载。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@api.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/download-backup</span><span class="sh">"</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">GET</span><span class="sh">"</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">download_backup</span><span class="p">():</span>
    <span class="n">save_name</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">args</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">save_name</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">backup_name</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">args</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">backup_name</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">save_name</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">backup_name</span><span class="p">:</span>
        <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Save name and backup name are required</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

    <span class="n">backup_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">SAVES_DIR</span><span class="p">,</span> <span class="n">save_name</span><span class="p">,</span> <span class="sh">"</span><span class="s">Backup</span><span class="sh">"</span><span class="p">,</span> <span class="n">backup_name</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">exists</span><span class="p">(</span><span class="n">backup_path</span><span class="p">):</span>
        <span class="k">return</span> <span class="nf">jsonify</span><span class="p">({</span><span class="sh">"</span><span class="s">error</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Backup not found</span><span class="sh">"</span><span class="p">}),</span> <span class="mi">400</span>

    <span class="c1"># 创建临时zip文件
</span>    <span class="n">temp_zip</span> <span class="o">=</span> <span class="n">tempfile</span><span class="p">.</span><span class="nc">NamedTemporaryFile</span><span class="p">(</span><span class="n">delete</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">suffix</span><span class="o">=</span><span class="sh">"</span><span class="s">.zip</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">with</span> <span class="n">zipfile</span><span class="p">.</span><span class="nc">ZipFile</span><span class="p">(</span><span class="n">temp_zip</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">,</span> <span class="n">zipfile</span><span class="p">.</span><span class="n">ZIP_DEFLATED</span><span class="p">)</span> <span class="k">as</span> <span class="n">zipf</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">root</span><span class="p">,</span> <span class="n">dirs</span><span class="p">,</span> <span class="n">files</span> <span class="ow">in</span> <span class="n">os</span><span class="p">.</span><span class="nf">walk</span><span class="p">(</span><span class="n">backup_path</span><span class="p">):</span>
            <span class="k">for</span> <span class="nb">file</span> <span class="ow">in</span> <span class="n">files</span><span class="p">:</span>
                <span class="n">file_path</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">root</span><span class="p">,</span> <span class="nb">file</span><span class="p">)</span>
                <span class="n">arcname</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">relpath</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">backup_path</span><span class="p">)</span>
                <span class="n">zipf</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="n">arcname</span><span class="p">)</span>

    <span class="n">temp_zip</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">generate</span><span class="p">():</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">temp_zip</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="sh">'</span><span class="s">rb</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
                <span class="n">data</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="mi">4096</span><span class="p">)</span>
                <span class="k">if</span> <span class="ow">not</span> <span class="n">data</span><span class="p">:</span>
                    <span class="k">break</span>
                <span class="k">yield</span> <span class="n">data</span>
        <span class="n">os</span><span class="p">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">temp_zip</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>

    <span class="n">response</span> <span class="o">=</span> <span class="n">app</span><span class="p">.</span><span class="nf">response_class</span><span class="p">(</span><span class="nf">generate</span><span class="p">(),</span> <span class="n">mimetype</span><span class="o">=</span><span class="sh">'</span><span class="s">application/zip</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">response</span><span class="p">.</span><span class="n">headers</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="sh">'</span><span class="s">Content-Disposition</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">attachment</span><span class="sh">'</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="sa">f</span><span class="sh">"</span><span class="si">{</span><span class="n">backup_name</span><span class="si">}</span><span class="s">.zip</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">response</span>
</code></pre></div></div>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">downloadBackup</span><span class="p">(</span><span class="nx">backupName</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">request</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/download-backup</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">params</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">save_name</span><span class="p">:</span> <span class="nx">backupTableData</span><span class="p">.</span><span class="nx">currentSave</span><span class="p">,</span>
            <span class="na">backup_name</span><span class="p">:</span> <span class="nx">backupName</span>
        <span class="p">},</span>
        <span class="na">responseType</span><span class="p">:</span> <span class="dl">'</span><span class="s1">blob</span><span class="dl">'</span>
    <span class="p">})</span>
        <span class="p">.</span><span class="nf">then</span><span class="p">(</span><span class="nx">res</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="c1">// 默认文件名</span>
            <span class="kd">let</span> <span class="nx">filename</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">backupName</span><span class="p">}</span><span class="s2">.zip`</span><span class="p">;</span>

            <span class="c1">// 尝试从 Content-Disposition 解析</span>
            <span class="kd">const</span> <span class="nx">disposition</span> <span class="o">=</span> <span class="nx">res</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="dl">'</span><span class="s1">content-disposition</span><span class="dl">'</span><span class="p">];</span>
            <span class="k">if </span><span class="p">(</span><span class="nx">disposition</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">const</span> <span class="nx">match</span> <span class="o">=</span> <span class="nx">disposition</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="sr">/filename="</span><span class="se">?([^</span><span class="sr">"</span><span class="se">]</span><span class="sr">+</span><span class="se">)</span><span class="sr">"</span><span class="se">?</span><span class="sr">/</span><span class="p">);</span>
                <span class="k">if </span><span class="p">(</span><span class="nx">match</span><span class="p">)</span> <span class="p">{</span>
                    <span class="nx">filename</span> <span class="o">=</span> <span class="nf">decodeURIComponent</span><span class="p">(</span><span class="nx">match</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="c1">// 创建下载</span>
            <span class="kd">const</span> <span class="nx">blob</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Blob</span><span class="p">([</span><span class="nx">res</span><span class="p">.</span><span class="nx">data</span><span class="p">],</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/zip</span><span class="dl">'</span> <span class="p">});</span>
            <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="nb">window</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nf">createObjectURL</span><span class="p">(</span><span class="nx">blob</span><span class="p">);</span>

            <span class="kd">const</span> <span class="nx">a</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">);</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">href</span> <span class="o">=</span> <span class="nx">url</span><span class="p">;</span>
            <span class="nx">a</span><span class="p">.</span><span class="nx">download</span> <span class="o">=</span> <span class="nx">filename</span><span class="p">;</span>
            <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nf">appendChild</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
            <span class="nx">a</span><span class="p">.</span><span class="nf">click</span><span class="p">();</span>

            <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nf">removeChild</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span>
            <span class="nb">window</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nf">revokeObjectURL</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
        <span class="p">})</span>
        <span class="p">.</span><span class="k">catch</span><span class="p">(</span><span class="nx">err</span> <span class="o">=&gt;</span> <span class="p">{</span>
            <span class="nf">toast</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">response</span><span class="p">?.</span><span class="nx">data</span><span class="p">?.</span><span class="nx">error</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">下载失败</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">);</span>
        <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="63-删除">6.3. 删除</h2>
<p>删除直接使用 <code class="language-plaintext highlighter-rouge">shutil.rmtree</code> 删除存档目录。</p>

<h2 id="64-备份">6.4. 备份</h2>
<p>unturned 本身并没有提供存档备份功能，所以需要自己实现。主要思路是将当前存档目录下的内容复制到一个以时间命名的备份目录中。我之前这篇<a href="/project-logs/games/2024/09/20/Unturned%E8%87%AA%E5%8A%A8%E5%A4%87%E4%BB%BD%E8%84%9A%E6%9C%AC.html">Unturned自动备份脚本</a>写过使用纯 Bash 脚本实现的备份功能，这里直接借鉴了其中的思路，并用 Python 重写。</p>

<p>备份需要排除一些不必要的文件夹，不备份Steam Workshop 内容（体积巨大）以及Backup 目录本身（避免递归）。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">exclude_dirs</span> <span class="o">=</span> <span class="p">{</span><span class="sh">"</span><span class="s">Workshop</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Backup</span><span class="sh">"</span><span class="p">}</span>
</code></pre></div></div>
<p>备份时，直接在存档目录下创建一个 Backup 目录，然后在该目录下创建以时间命名的备份文件夹，最后将存档内容复制进去。备份时需要限制最大备份数量，超过后删除最早的备份。</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">backup_save</span><span class="p">():</span>
    <span class="n">save_name</span> <span class="o">=</span> <span class="n">server_status</span><span class="p">[</span><span class="sh">"</span><span class="s">active_save</span><span class="sh">"</span><span class="p">]</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">save_name</span><span class="p">:</span>
        <span class="k">return</span>
    
    <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">Backing up </span><span class="si">{</span><span class="n">save_name</span><span class="si">}</span><span class="s">...</span><span class="sh">"</span><span class="p">)</span>
    
    <span class="c1"># 游戏目录
</span>    <span class="n">source_dir</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">SAVES_DIR</span><span class="p">,</span> <span class="n">save_name</span><span class="p">)</span>
    <span class="n">timestamp</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="nf">strftime</span><span class="p">(</span><span class="sh">"</span><span class="s">%Y-%m-%d_%H-%M-%S</span><span class="sh">"</span><span class="p">,</span> <span class="n">time</span><span class="p">.</span><span class="nf">localtime</span><span class="p">())</span>
    
    <span class="c1"># 创建备份目录
</span>    <span class="n">backup_dest</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">SAVES_DIR</span><span class="p">,</span> <span class="n">save_name</span><span class="p">,</span> <span class="sh">"</span><span class="s">Backup</span><span class="sh">"</span><span class="p">,</span> <span class="n">timestamp</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="nf">makedirs</span><span class="p">(</span><span class="n">backup_dest</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    
    <span class="c1"># 执行备份
</span>    <span class="n">exclude_dirs</span> <span class="o">=</span> <span class="p">{</span><span class="sh">"</span><span class="s">Workshop</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">Backup</span><span class="sh">"</span><span class="p">}</span>
    <span class="nf">copytree_with_exclusions</span><span class="p">(</span><span class="n">source_dir</span><span class="p">,</span> <span class="n">backup_dest</span><span class="p">,</span> <span class="n">exclude_dirs</span><span class="p">)</span>
    
    <span class="c1"># 删除超过最大数量的备份
</span>    <span class="nf">clean_old_backups</span><span class="p">(</span><span class="n">save_name</span><span class="p">)</span>
</code></pre></div></div>
<p>定时自动备份：</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="n">apscheduler.schedulers.background</span> <span class="kn">import</span> <span class="n">BackgroundScheduler</span>

<span class="n">backup_scheduler</span> <span class="o">=</span> <span class="nc">BackgroundScheduler</span><span class="p">()</span>
<span class="n">backup_scheduler</span><span class="p">.</span><span class="nf">add_job</span><span class="p">(</span><span class="n">backup_save_scheduler</span><span class="p">,</span> <span class="sh">'</span><span class="s">interval</span><span class="sh">'</span><span class="p">,</span> <span class="n">minutes</span><span class="o">=</span><span class="n">BACKUP_INTERVAL_MINUTES</span><span class="p">)</span>
<span class="c1"># 如果服务器正在运行，则启动自动备份
</span><span class="n">backup_scheduler</span><span class="p">.</span><span class="nf">start</span><span class="p">()</span>
</code></pre></div></div>

<h1 id="65-回档">6.5. 回档</h1>
<p>回档则是将某个备份目录的内容复制回存档目录，覆盖当前存档内容。在复制前需要确保该存档未被使用（服务器未运行）。</p>

<h1 id="7-其他待改进的功能">7. 其他待改进的功能</h1>
<p>由于本项目仅用于个人和朋友的使用，功能相对简陋，有些安全方面的都简化了，比如：</p>
<ol>
  <li>身份验证仅用一个简单的密码，用于简单防止网上某些恶意扫描器。</li>
  <li>上传文件没有做严格的校验，<code class="language-plaintext highlighter-rouge">z.extractall</code> 直接解压到存档目录，存在被构造路径穿越攻击的风险。</li>
</ol>

<h1 id="8-界面演示">8. 界面演示</h1>
<p><img src="/post_assets/images/2026/01/07-page.png" alt="Alt text" /></p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="project-logs" /><category term="games" /><category term="游戏" /><category term="web" /><category term="flask" /><summary type="html"><![CDATA[1. 前言 我想做一个简易的unturned游戏服务器管理界面，方便自己和朋友们管理游戏服务器。借助 AI 工具和自己的一些前后端知识，最终实现了这个目标。本文将分享我的开发过程和一些关键代码。]]></summary></entry><entry><title type="html">原生html的table固定表头滚动</title><link href="/learning-notes/2026/01/06/%E5%8E%9F%E7%94%9Fhtml%E7%9A%84table%E5%9B%BA%E5%AE%9A%E8%A1%A8%E5%A4%B4%E6%BB%9A%E5%8A%A8.html" rel="alternate" type="text/html" title="原生html的table固定表头滚动" /><published>2026-01-06T13:00:00+00:00</published><updated>2026-01-06T13:00:00+00:00</updated><id>/learning-notes/2026/01/06/%E5%8E%9F%E7%94%9Fhtml%E7%9A%84table%E5%9B%BA%E5%AE%9A%E8%A1%A8%E5%A4%B4%E6%BB%9A%E5%8A%A8</id><content type="html" xml:base="/learning-notes/2026/01/06/%E5%8E%9F%E7%94%9Fhtml%E7%9A%84table%E5%9B%BA%E5%AE%9A%E8%A1%A8%E5%A4%B4%E6%BB%9A%E5%8A%A8.html"><![CDATA[<p>参考：<a href="https://www.cnblogs.com/itjeff/p/16205938.html">https://www.cnblogs.com/itjeff/p/16205938.html</a></p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">table</span> <span class="nt">tbody</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="nb">block</span><span class="p">;</span>
    <span class="nl">height</span><span class="p">:</span> <span class="m">240px</span><span class="p">;</span>
    <span class="nl">overflow-y</span><span class="p">:</span> <span class="nb">scroll</span><span class="p">;</span>
<span class="p">}</span>

<span class="nt">table</span> <span class="nt">thead</span><span class="o">,</span>
<span class="nt">tbody</span> <span class="nt">tr</span> <span class="p">{</span>
    <span class="nl">display</span><span class="p">:</span> <span class="n">table</span><span class="p">;</span>
    <span class="nl">width</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span>
    <span class="nl">table-layout</span><span class="p">:</span> <span class="nb">fixed</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="learning-notes" /><category term="html" /><summary type="html"><![CDATA[参考：https://www.cnblogs.com/itjeff/p/16205938.html]]></summary></entry><entry><title type="html">vscode连接wsl2失败</title><link href="/troubleshooting/2026/01/06/vscode%E8%BF%9E%E6%8E%A5wsl2%E5%A4%B1%E8%B4%A5.html" rel="alternate" type="text/html" title="vscode连接wsl2失败" /><published>2026-01-06T12:00:00+00:00</published><updated>2026-01-06T12:00:00+00:00</updated><id>/troubleshooting/2026/01/06/vscode%E8%BF%9E%E6%8E%A5wsl2%E5%A4%B1%E8%B4%A5</id><content type="html" xml:base="/troubleshooting/2026/01/06/vscode%E8%BF%9E%E6%8E%A5wsl2%E5%A4%B1%E8%B4%A5.html"><![CDATA[<h1 id="问题描述">问题描述</h1>
<p>在使用 vscode 连接 wsl2/docker 时，出现如下错误：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>failed: Connection refused.
</code></pre></div></div>

<p>wsl2 上的 vscode-server 早在之前就已经安装好了，但就是无法连接上。</p>

<h1 id="解决方法">解决方法</h1>
<p>参考: <a href="https://github.com/microsoft/vscode-remote-release/issues/2388">https://github.com/microsoft/vscode-remote-release/issues/2388</a></p>

<p>在 vscode 中打开命令面板 (Ctrl+Shift+P)，输入 <code class="language-plaintext highlighter-rouge">Remote</code>，选择 <code class="language-plaintext highlighter-rouge">Remote-SSH: 在主机上终止 VS Code 服务器</code>（<code class="language-plaintext highlighter-rouge">Remote-SSH: Kill VS Code Server on Host</code>），然后选择对应的 wsl2 主机，终止掉 vscode-server 进程。重新连接 wsl2 即可.</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="vscode" /><summary type="html"><![CDATA[问题描述 在使用 vscode 连接 wsl2/docker 时，出现如下错误：]]></summary></entry><entry><title type="html">flask-socketio与threading模块冲突问题</title><link href="/troubleshooting/2026/01/06/flask-socketio%E4%B8%8Ethreading%E6%A8%A1%E5%9D%97%E5%86%B2%E7%AA%81%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="flask-socketio与threading模块冲突问题" /><published>2026-01-06T11:00:00+00:00</published><updated>2026-01-06T11:00:00+00:00</updated><id>/troubleshooting/2026/01/06/flask-socketio%E4%B8%8Ethreading%E6%A8%A1%E5%9D%97%E5%86%B2%E7%AA%81%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2026/01/06/flask-socketio%E4%B8%8Ethreading%E6%A8%A1%E5%9D%97%E5%86%B2%E7%AA%81%E9%97%AE%E9%A2%98.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">flask-socketio</code>与<code class="language-plaintext highlighter-rouge">threading</code>模块冲突问题</p>

<p>如果直接在<code class="language-plaintext highlighter-rouge">threading</code>模块中使用<code class="language-plaintext highlighter-rouge">emit</code>等功能，会导致无法获取当前的<code class="language-plaintext highlighter-rouge">SocketIO</code>上下文，从而无法正确发送消息。</p>

<p>为了解决这个问题，可以使用<code class="language-plaintext highlighter-rouge">SocketIO</code>提供的<code class="language-plaintext highlighter-rouge">start_background_task</code>方法来创建后台任务。而<code class="language-plaintext highlighter-rouge">time.sleep</code>等阻塞操作也应避免使用，可以使用<code class="language-plaintext highlighter-rouge">SocketIO</code>的<code class="language-plaintext highlighter-rouge">sleep</code>方法来替代。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="python" /><summary type="html"><![CDATA[flask-socketio与threading模块冲突问题]]></summary></entry><entry><title type="html">spring boot项目多配置文件无法启动问题</title><link href="/troubleshooting/2025/12/25/spring-boot%E9%A1%B9%E7%9B%AE%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="spring boot项目多配置文件无法启动问题" /><published>2025-12-25T10:00:00+00:00</published><updated>2025-12-25T10:00:00+00:00</updated><id>/troubleshooting/2025/12/25/spring%20boot%E9%A1%B9%E7%9B%AE%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2025/12/25/spring-boot%E9%A1%B9%E7%9B%AE%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E6%97%A0%E6%B3%95%E5%90%AF%E5%8A%A8%E9%97%AE%E9%A2%98.html"><![CDATA[<p>我在运行spring boot项目报了下面的错:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>我在运行spring boot项目报了下面的错，该项目曾经是我开发的，不过我已经离开一年，现在回来运行不了了，架构几乎没有变化，数据库密码也是对的。请问可能是什么原因？

Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2025-12-29 13:56:33.650 ERROR --- [ restartedMain] org.springframework.boot.SpringApplication : Application run failed
org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:156)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:543)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:141)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:743)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:390)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:312)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1214)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1203)
	at com.hc.display.DisplayApplication.main(DisplayApplication.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:124)
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.&lt;init&gt;(TomcatWebServer.java:86)
	at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:416)
	at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:180)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:180)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:153)
	... 13 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'servletEndpointRegistrar' defined in class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/web/ServletEndpointManagementContextConfiguration$WebMvcServletEndpointManagementContextConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar]: Factory method 'servletEndpointRegistrar' threw exception; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'healthEndpoint' defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthEndpointConfiguration.class]: Unsatisfied dependency expressed through method 'healthEndpoint' parameter 1; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'healthIndicatorRegistry' defined in class path resource [org/springframework/boot/actuate/autoconfigure/health/HealthIndicatorAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.actuate.health.HealthIndicatorRegistry]: Factory method 'healthIndicatorRegistry' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dbHealthIndicator' defined in class path resource [org/springframework/boot/actuate/autoconfigure/jdbc/DataSourceHealthIndicatorAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.actuate.health.HealthIndicator]: Factory method 'dbHealthIndicator' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is java.lang.IllegalStateException: Cannot load driver class: org.mariadb.jdbc.Driver
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:627)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:607)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1321)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1160)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204)
	at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:211)
	at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:202)
	at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:96)
	at org.springframework.boot.web.servlet.ServletContextInitializerBeans.&lt;init&gt;(ServletContextInitializerBeans.java:85)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:253)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:227)
	at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53)
	at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5132)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909)
	at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:841)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1384)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1374)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:909)
	at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:262)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.StandardService.startInternal(StandardService.java:421)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:932)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
	at org.apache.catalina.startup.Tomcat.start(Tomcat.java:456)
	at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:105)
	... 18 common frames omitted
</code></pre></div></div>

<p>尝试了下面方案：</p>

<ol>
  <li>检查依赖，发现 <code class="language-plaintext highlighter-rouge">C:\Users\用户名\.m2\repository\</code> 中是存在 <code class="language-plaintext highlighter-rouge">mariadb</code>这个依赖的。</li>
  <li>移除 <code class="language-plaintext highlighter-rouge">.idea</code> 目录，重新加载项目，问题依旧。</li>
  <li>idea清除缓存，问题依旧。</li>
</ol>

<p><strong>最终解决方案</strong>：</p>

<p>由于我在项目的<code class="language-plaintext highlighter-rouge">pom.xml</code>中存在配置文件切换操作。</p>

<p>根目录的<code class="language-plaintext highlighter-rouge">pom.xml</code>中有如下配置：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    <span class="nt">&lt;profiles&gt;</span>

        <span class="nt">&lt;profile&gt;</span>
            <span class="nt">&lt;id&gt;</span>prod-mariadb<span class="nt">&lt;/id&gt;</span>
            <span class="nt">&lt;properties&gt;</span>
                <span class="nt">&lt;profileActive&gt;</span>prod-mariadb<span class="nt">&lt;/profileActive&gt;</span>
                <span class="nt">&lt;projectEnv&gt;</span>prod<span class="nt">&lt;/projectEnv&gt;</span>
                <span class="nt">&lt;spring.datasource.driver-class-name&gt;</span>org.mariadb.jdbc.Driver<span class="nt">&lt;/spring.datasource.driver-class-name&gt;</span>
                <span class="nt">&lt;spring.datasource.url&gt;</span>jdbc:mariadb://localhost:3306/honest_culture?useUnicode=true<span class="ni">&amp;amp;</span>characterEncoding=utf-8<span class="nt">&lt;/spring.datasource.url&gt;</span>
                <span class="nt">&lt;spring.datasource.username&gt;</span>root<span class="nt">&lt;/spring.datasource.username&gt;</span>
                <span class="nt">&lt;spring.datasource.password&gt;</span>root-password<span class="nt">&lt;/spring.datasource.password&gt;</span>
            <span class="nt">&lt;/properties&gt;</span>
        <span class="nt">&lt;/profile&gt;</span>

        <span class="nt">&lt;profile&gt;</span>
            <span class="nt">&lt;id&gt;</span>prod-dm<span class="nt">&lt;/id&gt;</span>
            <span class="nt">&lt;properties&gt;</span>
                <span class="nt">&lt;profileActive&gt;</span>prod-dm<span class="nt">&lt;/profileActive&gt;</span>
                <span class="nt">&lt;projectEnv&gt;</span>prod<span class="nt">&lt;/projectEnv&gt;</span>
                <span class="nt">&lt;spring.datasource.driver-class-name&gt;</span>dm.jdbc.driver.DmDriver<span class="nt">&lt;/spring.datasource.driver-class-name&gt;</span>
                <span class="nt">&lt;spring.datasource.url&gt;</span>jdbc:dm://localhost:5236?schema=""honest_culture""<span class="ni">&amp;amp;</span>clobAsString=1<span class="nt">&lt;/spring.datasource.url&gt;</span>
                <span class="nt">&lt;spring.datasource.username&gt;</span>SYSDBA<span class="nt">&lt;/spring.datasource.username&gt;</span>
                <span class="nt">&lt;spring.datasource.password&gt;</span>SYSDBA<span class="nt">&lt;/spring.datasource.password&gt;</span>
            <span class="nt">&lt;/properties&gt;</span>
        <span class="nt">&lt;/profile&gt;</span>
    <span class="nt">&lt;/profiles&gt;</span>
</code></pre></div></div>

<p>子目录的<code class="language-plaintext highlighter-rouge">pom.xml</code>：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">&lt;profiles&gt;</span>
        <span class="nt">&lt;profile&gt;</span>
            <span class="nt">&lt;id&gt;</span>prod-mariadb<span class="nt">&lt;/id&gt;</span>
            <span class="nt">&lt;dependencies&gt;</span>
                <span class="nt">&lt;dependency&gt;</span>
                    <span class="nt">&lt;groupId&gt;</span>org.mariadb.jdbc<span class="nt">&lt;/groupId&gt;</span>
                    <span class="nt">&lt;artifactId&gt;</span>mariadb-java-client<span class="nt">&lt;/artifactId&gt;</span>
                <span class="nt">&lt;/dependency&gt;</span>
            <span class="nt">&lt;/dependencies&gt;</span>
        <span class="nt">&lt;/profile&gt;</span>

        <span class="nt">&lt;profile&gt;</span>
            <span class="nt">&lt;id&gt;</span>prod-dm<span class="nt">&lt;/id&gt;</span>
            <span class="nt">&lt;dependencies&gt;</span>
                <span class="nt">&lt;dependency&gt;</span>
                    <span class="nt">&lt;groupId&gt;</span>com.dameng<span class="nt">&lt;/groupId&gt;</span>
                    <span class="nt">&lt;artifactId&gt;</span>DmJdbcDriver18<span class="nt">&lt;/artifactId&gt;</span>
                <span class="nt">&lt;/dependency&gt;</span>
            <span class="nt">&lt;/dependencies&gt;</span>
        <span class="nt">&lt;/profile&gt;</span>
    <span class="nt">&lt;/profiles&gt;</span>
</code></pre></div></div>

<p>最终通过两次切换maven配置文件并重新加载项目，问题解决。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="java" /><category term="spring-boot" /><summary type="html"><![CDATA[我在运行spring boot项目报了下面的错:]]></summary></entry><entry><title type="html">安卓EditText焦点问题</title><link href="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93EditText%E7%84%A6%E7%82%B9%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="安卓EditText焦点问题" /><published>2025-12-23T08:30:00+00:00</published><updated>2025-12-23T08:30:00+00:00</updated><id>/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93EditText%E7%84%A6%E7%82%B9%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93EditText%E7%84%A6%E7%82%B9%E9%97%AE%E9%A2%98.html"><![CDATA[<p>EditText点击输入后，再点击旁边空白处时，该EditText仍然没有失去焦点。</p>

<p>解决方案：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">hideKeyboardAndClearFocus</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">View</span> <span class="n">view</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">InputMethodManager</span> <span class="n">imm</span> <span class="o">=</span> <span class="o">(</span><span class="nc">InputMethodManager</span><span class="o">)</span> <span class="n">context</span><span class="o">.</span><span class="na">getSystemService</span><span class="o">(</span><span class="nc">Context</span><span class="o">.</span><span class="na">INPUT_METHOD_SERVICE</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">imm</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">imm</span><span class="o">.</span><span class="na">hideSoftInputFromWindow</span><span class="o">(</span><span class="n">view</span><span class="o">.</span><span class="na">getWindowToken</span><span class="o">(),</span> <span class="mi">0</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">View</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">view</span><span class="o">.</span><span class="na">findFocus</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">temp</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">temp</span><span class="o">.</span><span class="na">clearFocus</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nc">View</span> <span class="n">rootView</span> <span class="o">=</span> <span class="n">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">root_layout</span><span class="o">);</span>
    <span class="n">rootView</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="n">view</span> <span class="o">-&gt;</span> <span class="o">{</span>
        <span class="n">hideKeyboardAndClearFocus</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="n">rootView</span><span class="o">);</span>
    <span class="o">});</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="android" /><summary type="html"><![CDATA[EditText点击输入后，再点击旁边空白处时，该EditText仍然没有失去焦点。]]></summary></entry><entry><title type="html">安卓LinearLayout最后一个按钮宽度异常问题</title><link href="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93LinearLayout%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E6%8C%89%E9%92%AE%E5%AE%BD%E5%BA%A6%E5%BC%82%E5%B8%B8%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="安卓LinearLayout最后一个按钮宽度异常问题" /><published>2025-12-23T08:00:00+00:00</published><updated>2025-12-23T08:00:00+00:00</updated><id>/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93LinearLayout%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E6%8C%89%E9%92%AE%E5%AE%BD%E5%BA%A6%E5%BC%82%E5%B8%B8%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93LinearLayout%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E6%8C%89%E9%92%AE%E5%AE%BD%E5%BA%A6%E5%BC%82%E5%B8%B8%E9%97%AE%E9%A2%98.html"><![CDATA[<p>有如下安卓代码：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;LinearLayout</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
    <span class="na">android:orientation=</span><span class="s">"horizontal"</span>
    <span class="na">android:padding=</span><span class="s">"8dp"</span>
<span class="nt">&gt;</span>
    <span class="nt">&lt;Button</span>
        <span class="na">android:id=</span><span class="s">"@+id/btnSaveOriginal"</span>
        <span class="na">style=</span><span class="s">"@style/ButtonStyle"</span>
        <span class="na">android:text=</span><span class="s">"@string/save_original"</span>
    <span class="nt">/&gt;</span>
    <span class="nt">&lt;Button</span>
        <span class="na">android:id=</span><span class="s">"@+id/btnSaveModified"</span>
        <span class="na">style=</span><span class="s">"@style/ButtonStyle"</span>
        <span class="na">android:text=</span><span class="s">"@string/save_updated"</span>
    <span class="nt">/&gt;</span>
    <span class="nt">&lt;Button</span>
        <span class="na">android:id=</span><span class="s">"@+id/btnImport"</span>
        <span class="na">style=</span><span class="s">"@style/ButtonStyle"</span>
        <span class="na">android:text=</span><span class="s">"@string/load_updated"</span>
    <span class="nt">/&gt;</span>
    <span class="nt">&lt;Button</span>
        <span class="na">android:id=</span><span class="s">"@+id/btnClear"</span>
        <span class="na">style=</span><span class="s">"@style/ButtonStyle"</span>
        <span class="na">android:text=</span><span class="s">"@string/clear_updated"</span>
    <span class="nt">/&gt;</span>
<span class="nt">&lt;/LinearLayout&gt;</span>

<span class="nt">&lt;style</span> <span class="na">name=</span><span class="s">"ButtonStyle"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:layout_width"</span><span class="nt">&gt;</span>wrap_content<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:layout_height"</span><span class="nt">&gt;</span>wrap_content<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:backgroundTint"</span><span class="nt">&gt;</span>#265468<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:textColor"</span><span class="nt">&gt;</span>#FFFFFF<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:paddingHorizontal"</span><span class="nt">&gt;</span>12dp<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:paddingVertical"</span><span class="nt">&gt;</span>8dp<span class="nt">&lt;/item&gt;</span>
    <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:layout_marginHorizontal"</span><span class="nt">&gt;</span>5dp<span class="nt">&lt;/item&gt;</span>
<span class="nt">&lt;/style&gt;</span>
</code></pre></div></div>

<p>这里按钮的宽度非常奇怪，前两个按钮里面有5个字，宽度刚好盖过这些字没问题。但第三个按钮只有两个字，但宽度却远超过两个字的宽度，比前两个按钮宽度略微小一些。最后一个按钮则是因为宽度不够被挤扁了，宽度不足一个字的宽度，高度则是超过了前三个按钮。</p>

<p>第三个按钮宽度比较宽是因为 Button 默认有最小宽度（minWidth），两个字不足以撑开内容宽度，于是被拉到最小宽度。</p>

<p>最后一个按钮则是因为几个按钮宽度总和超过了父布局的宽度，导致最后一个按钮被压缩到非常窄。</p>

<p>解决方法：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Button</span>
    <span class="na">android:layout_width=</span><span class="s">"0dp"</span>
    <span class="na">android:layout_weight=</span><span class="s">"1"</span>
    <span class="err">...</span> <span class="nt">/&gt;</span>
</code></pre></div></div>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="android" /><summary type="html"><![CDATA[有如下安卓代码： ```xml]]></summary></entry><entry><title type="html">安卓Fragment按钮绑定事件混乱问题</title><link href="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93Fragment%E6%8C%89%E9%92%AE%E7%BB%91%E5%AE%9A%E4%BA%8B%E4%BB%B6%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98.html" rel="alternate" type="text/html" title="安卓Fragment按钮绑定事件混乱问题" /><published>2025-12-23T08:00:00+00:00</published><updated>2025-12-23T08:00:00+00:00</updated><id>/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93Fragment%E6%8C%89%E9%92%AE%E7%BB%91%E5%AE%9A%E4%BA%8B%E4%BB%B6%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98</id><content type="html" xml:base="/troubleshooting/2025/12/23/%E5%AE%89%E5%8D%93Fragment%E6%8C%89%E9%92%AE%E7%BB%91%E5%AE%9A%E4%BA%8B%E4%BB%B6%E6%B7%B7%E4%B9%B1%E9%97%AE%E9%A2%98.html"><![CDATA[<h1 id="问题描述">问题描述</h1>
<p>我在安卓开发中出现了按钮绑定事件混乱的问题。页面结构是：</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MainActivity:
   tab1:
       Fragment1(包含一个ConfigEditFragment)
   tab2:
       Fragment2(包含一个ConfigEditFragment)
</code></pre></div></div>

<p>每个 ConfigEditFragment 都有一个刷新按钮，点击后调用从父级元素传过来的回调函数。现在的问题是无论我点击的是 Fragment1 的刷新按钮还是 Fragment2 的刷新按钮，触发的都是 test2()。而这两个 Fragment 里包含的 ConfigEditFragment 也确实是不同的内容。请问这是什么原因？</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Fragment1</span> <span class="kd">extends</span> <span class="nc">Fragment</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">ConfigEditorFragment</span> <span class="n">configFragment1</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">View</span> <span class="nf">onCreateView</span><span class="o">(</span><span class="nc">LayoutInflater</span> <span class="n">inflater</span><span class="o">,</span> <span class="nc">ViewGroup</span> <span class="n">container</span><span class="o">,</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">View</span> <span class="n">rootView</span> <span class="o">=</span> <span class="n">inflater</span><span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">fragment1_page</span><span class="o">,</span> <span class="n">container</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>

        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span> <span class="n">requireContext</span><span class="o">();</span>

        <span class="nc">FragmentTransaction</span> <span class="n">ft</span> <span class="o">=</span> <span class="n">getParentFragmentManager</span><span class="o">().</span><span class="na">beginTransaction</span><span class="o">();</span>
        <span class="n">configFragment1</span> <span class="o">=</span> <span class="nc">ConfigEditorFragment</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="k">new</span> <span class="nc">Data1</span><span class="o">(),</span> <span class="k">this</span><span class="o">::</span><span class="n">test1</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">fragment1_container</span><span class="o">,</span> <span class="n">configFragment1</span><span class="o">,</span> <span class="s">"config_fragment_1"</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">commit</span><span class="o">();</span>

        <span class="k">return</span> <span class="n">rootView</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onViewCreated</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">View</span> <span class="n">view</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onViewCreated</span><span class="o">(</span><span class="n">view</span><span class="o">,</span> <span class="n">savedInstanceState</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">test1</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="n">requireContext</span><span class="o">(),</span> <span class="s">"Fragment1 test1 called"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Fragment2</span> <span class="kd">extends</span> <span class="nc">Fragment</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">ConfigEditorFragment</span> <span class="n">configFragment2</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">View</span> <span class="nf">onCreateView</span><span class="o">(</span><span class="nc">LayoutInflater</span> <span class="n">inflater</span><span class="o">,</span> <span class="nc">ViewGroup</span> <span class="n">container</span><span class="o">,</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">View</span> <span class="n">rootView</span> <span class="o">=</span> <span class="n">inflater</span><span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">fragment2_page</span><span class="o">,</span> <span class="n">container</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>

        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span> <span class="n">requireContext</span><span class="o">();</span>

        <span class="nc">FragmentTransaction</span> <span class="n">ft</span> <span class="o">=</span> <span class="n">getParentFragmentManager</span><span class="o">().</span><span class="na">beginTransaction</span><span class="o">();</span>
        <span class="n">configFragment2</span> <span class="o">=</span> <span class="nc">ConfigEditorFragment</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="k">new</span> <span class="nc">Data2</span><span class="o">(),</span> <span class="k">this</span><span class="o">::</span><span class="n">test2</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">fragment2_container</span><span class="o">,</span> <span class="n">configFragment2</span><span class="o">,</span> <span class="s">"config_fragment_2"</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">commit</span><span class="o">();</span>

        <span class="k">return</span> <span class="n">rootView</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onViewCreated</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">View</span> <span class="n">view</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onViewCreated</span><span class="o">(</span><span class="n">view</span><span class="o">,</span> <span class="n">savedInstanceState</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">test2</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="n">requireContext</span><span class="o">(),</span> <span class="s">"Fragment2 test2 called"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
    <span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MainPagerAdapter</span> <span class="kd">extends</span> <span class="nc">FragmentStateAdapter</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="nf">MainPagerAdapter</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">FragmentActivity</span> <span class="n">fragmentActivity</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">(</span><span class="n">fragmentActivity</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@NonNull</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Fragment</span> <span class="nf">createFragment</span><span class="o">(</span><span class="kt">int</span> <span class="n">position</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">switch</span> <span class="o">(</span><span class="n">position</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">case</span> <span class="mi">0</span><span class="o">:</span>
                <span class="k">return</span> <span class="k">new</span> <span class="nf">SettingsFragment</span><span class="o">();</span>
            <span class="k">case</span> <span class="mi">1</span><span class="o">:</span>
                <span class="k">return</span> <span class="k">new</span> <span class="nf">Fragment1</span><span class="o">();</span>
            <span class="k">default</span><span class="o">:</span>
                <span class="k">return</span> <span class="k">new</span> <span class="nf">Fragment2</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getItemCount</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="mi">3</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h1 id="原因">原因</h1>
<p>Fragment 管理层级 + FragmentManager 用错了，导致同一个 ConfigEditorFragment 实例被复用 / 覆盖问题，从而按钮事件指向了最后一次绑定的回调（Location 的）。</p>

<h1 id="解决方案">解决方案</h1>
<p>将 <code class="language-plaintext highlighter-rouge">getParentFragmentManager()</code> 改为 <code class="language-plaintext highlighter-rouge">getChildFragmentManager()</code>。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="troubleshooting" /><category term="android" /><summary type="html"><![CDATA[问题描述 我在安卓开发中出现了按钮绑定事件混乱的问题。页面结构是： MainActivity: tab1: Fragment1(包含一个ConfigEditFragment) tab2: Fragment2(包含一个ConfigEditFragment)]]></summary></entry><entry><title type="html">Xposed模块DeviceInfo开发</title><link href="/project-logs/platforms/2025/12/18/Xposed%E6%A8%A1%E5%9D%97DeviceInfo%E5%BC%80%E5%8F%91.html" rel="alternate" type="text/html" title="Xposed模块DeviceInfo开发" /><published>2025-12-18T02:00:00+00:00</published><updated>2025-12-18T02:00:00+00:00</updated><id>/project-logs/platforms/2025/12/18/Xposed%E6%A8%A1%E5%9D%97DeviceInfo%E5%BC%80%E5%8F%91</id><content type="html" xml:base="/project-logs/platforms/2025/12/18/Xposed%E6%A8%A1%E5%9D%97DeviceInfo%E5%BC%80%E5%8F%91.html"><![CDATA[<h1 id="1-需求描述">1. 需求描述</h1>
<p>本来是想开发一个用于伪造 wifi 和定位信息的用于打卡的 Xposed 模块。后面想逐步完善成一个伪造设备信息的模块，所以命名为 DeviceInfo。目前已实现功能：Wifi 信息伪造（已完成）、定位信息伪造（进行中）、其他尚未实现。</p>

<h1 id="2-开发环境">2. 开发环境</h1>
<p>开发环境：Android Studio + Xposed 模块开发环境</p>

<h1 id="3-项目结构">3. 项目结构</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src
  └─main
    ├─assets
    ├─java
    │  └─com
    │      └─example
    │          └─deviceinfo
    │              ├─adapter
    │              ├─config
    │              ├─content_provider
    │              ├─fragment
    │              │  └─config_editor
    │              │      ├─model
    │              │      └─ui
    │              ├─pojo
    │              └─util
    └─res
        ├─layout
        ├─values
        └─xml
</code></pre></div></div>

<h1 id="4-主要页面框架">4. 主要页面框架</h1>
<h2 id="41-主要页面设计">4.1 主要页面设计</h2>
<p>打算设计成这样的页面：</p>
<ul>
  <li>顶部：几个功能模块的 Tab（Wifi、定位、设备信息等）</li>
  <li>其余部分：各个模块的设置页面</li>
</ul>

<p>每个页面均为三列的表格，左侧是字段名，中间是当前值，右侧是伪造值。</p>

<p>最终完成的界面如图所示：
<img src="/post_assets/images/2025/12/18-app.jpg" alt="DeviceInfo主界面" /></p>

<h2 id="42-页面实现">4.2. 页面实现</h2>
<p>考虑到每个页面的布局是类似的，所以我打算使用 Fragment 来封装每个页面的布局。每个 Fragment 都包含一个 RecyclerView 用于显示三列的表格。</p>

<h3 id="421-数据模型">4.2.1. 数据模型</h3>
<p>先创建一个基本数据模型，后续其他配置类均继承自该模型：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">BaseConfig</span> <span class="kd">implements</span> <span class="nc">Serializable</span> <span class="o">{</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="n">configId</span><span class="o">;</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="n">configName</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kt">long</span> <span class="n">createdAt</span><span class="o">;</span>
    <span class="kd">public</span> <span class="kt">long</span> <span class="n">updatedAt</span><span class="o">;</span>

    <span class="kd">public</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">data</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="kd">abstract</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="nf">getConfigItems</span><span class="o">();</span>

    <span class="kd">public</span> <span class="kd">abstract</span> <span class="nc">String</span> <span class="nf">getKeyOfDefaultName</span><span class="o">();</span>

    <span class="kd">public</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">getKeyType</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">ConfigItem</span> <span class="n">item</span> <span class="o">:</span> <span class="n">getConfigItems</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">item</span><span class="o">.</span><span class="na">key</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">key</span><span class="o">))</span> <span class="o">{</span>
                <span class="k">return</span> <span class="n">item</span><span class="o">.</span><span class="na">type</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getDefaultConfigName</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">getKeyOfDefaultName</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">key</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">key</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">Object</span> <span class="n">value</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="k">return</span> <span class="n">value</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="s">""</span> <span class="o">:</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">value</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">BaseConfig</span> <span class="nf">copy</span><span class="o">()</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">BaseConfig</span> <span class="nf">newInstance</span><span class="o">(</span><span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="n">cls</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">cls</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"BaseConfig"</span><span class="o">,</span> <span class="s">"Failed to create new instance"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="nc">JSONObject</span> <span class="nf">toJsonObject</span><span class="o">()</span> <span class="kd">throws</span> <span class="nc">JSONException</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="o">&lt;</span><span class="no">T</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">fromJsonObject</span><span class="o">(</span><span class="nc">JSONObject</span> <span class="n">obj</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">cls</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">ConfigItem</span> <span class="kd">implements</span> <span class="nc">Serializable</span> <span class="o">{</span>
        <span class="kd">public</span> <span class="nc">String</span> <span class="n">key</span><span class="o">;</span>
        <span class="kd">public</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">type</span><span class="o">;</span>
        <span class="kd">public</span> <span class="nc">String</span> <span class="n">description</span><span class="o">;</span>

        <span class="kd">public</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">type</span><span class="o">,</span> <span class="nc">String</span> <span class="n">description</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">this</span><span class="o">.</span><span class="na">key</span> <span class="o">=</span> <span class="n">key</span><span class="o">;</span>
            <span class="k">this</span><span class="o">.</span><span class="na">type</span> <span class="o">=</span> <span class="n">type</span><span class="o">;</span>
            <span class="k">this</span><span class="o">.</span><span class="na">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>一开始我采用的方案是反射获取字段，也就是每个配置类定义一堆字段，然后通过反射获取字段名和字段值进行显示和修改。而 BaseConfig 则提供了通用的反射读写方法。后来写其他代码时反复创建示例以及反射操作十分繁琐，所以我改成了使用 Map 来存储字段名和字段值。这样每个配置类只需要定义字段的描述信息即可，读写时直接操作 Map 即可，代码复杂度大大减小。</p>

<p>数据主要存储在 data 字段中，key 为字段名，value 为字段值。每个子类需要实现 getConfigItems 方法返回该配置类的所有字段信息。</p>

<h3 id="422-列表适配器">4.2.2. 列表适配器</h3>
<p>中间的表格采用 RecyclerView 来实现，先定义每一行的显示布局 item_row.xml：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
    <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
    <span class="na">android:orientation=</span><span class="s">"horizontal"</span>
    <span class="na">android:padding=</span><span class="s">"8dp"</span><span class="nt">&gt;</span>

    <span class="nt">&lt;LinearLayout</span>
        <span class="na">android:layout_width=</span><span class="s">"0dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
        <span class="na">android:layout_weight=</span><span class="s">"1"</span>
        <span class="na">android:orientation=</span><span class="s">"vertical"</span><span class="nt">&gt;</span>

        <span class="nt">&lt;TextView</span>
            <span class="na">android:id=</span><span class="s">"@+id/tvKey"</span>
            <span class="na">android:layout_width=</span><span class="s">"wrap_content"</span>
            <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
            <span class="na">android:textStyle=</span><span class="s">"bold"</span>
            <span class="na">android:textSize=</span><span class="s">"16sp"</span> <span class="nt">/&gt;</span>

        <span class="nt">&lt;TextView</span>
            <span class="na">android:id=</span><span class="s">"@+id/tvKeyDescription"</span>
            <span class="na">android:layout_width=</span><span class="s">"wrap_content"</span>
            <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
            <span class="na">android:textSize=</span><span class="s">"12sp"</span>
            <span class="na">android:textColor=</span><span class="s">"#888888"</span>
            <span class="na">android:paddingTop=</span><span class="s">"2dp"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;/LinearLayout&gt;</span>

    <span class="nt">&lt;TextView</span>
        <span class="na">android:id=</span><span class="s">"@+id/tvOriginalValue"</span>
        <span class="na">android:layout_width=</span><span class="s">"0dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
        <span class="na">android:layout_weight=</span><span class="s">"1"</span><span class="nt">/&gt;</span>

    <span class="nt">&lt;EditText</span>
        <span class="na">android:id=</span><span class="s">"@+id/etModifiedValue"</span>
        <span class="na">android:importantForAutofill=</span><span class="s">"no"</span>
        <span class="na">android:layout_width=</span><span class="s">"0dp"</span>
        <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
        <span class="na">android:layout_weight=</span><span class="s">"1"</span>
        <span class="na">android:inputType=</span><span class="s">"text"</span>
        <span class="na">tools:ignore=</span><span class="s">"LabelFor"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/LinearLayout&gt;</span>
</code></pre></div></div>

<p>实现 RecyclerView 适配器 ConfigAdapter：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConfigTableAdapter</span> <span class="kd">extends</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">Adapter</span><span class="o">&lt;</span><span class="nc">ConfigTableAdapter</span><span class="o">.</span><span class="na">ViewHolder</span><span class="o">&gt;</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">BaseConfig</span> <span class="n">originalObject</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">BaseConfig</span> <span class="n">updatedObject</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">BaseConfig</span> <span class="n">tempNewObject</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="n">keyDescriptions</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">modifiedValues</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>

    <span class="c1">// 保存 ViewHolder，便于批量 UI 更新</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">ViewHolder</span><span class="o">&gt;</span> <span class="n">holderMap</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>

    <span class="kd">public</span> <span class="nf">ConfigTableAdapter</span><span class="o">(</span><span class="nc">BaseConfig</span> <span class="n">originalObject</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">originalObject</span> <span class="o">=</span> <span class="n">originalObject</span><span class="o">;</span>
        <span class="k">this</span><span class="o">.</span><span class="na">keyDescriptions</span> <span class="o">=</span> <span class="n">originalObject</span><span class="o">.</span><span class="na">getConfigItems</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="nd">@NonNull</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">ViewHolder</span> <span class="nf">onCreateViewHolder</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">ViewGroup</span> <span class="n">parent</span><span class="o">,</span> <span class="kt">int</span> <span class="n">viewType</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">View</span> <span class="n">view</span> <span class="o">=</span> <span class="nc">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">parent</span><span class="o">.</span><span class="na">getContext</span><span class="o">())</span>
                <span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">item_row</span><span class="o">,</span> <span class="n">parent</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
        <span class="k">return</span> <span class="k">new</span> <span class="nf">ViewHolder</span><span class="o">(</span><span class="n">view</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onBindViewHolder</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">ViewHolder</span> <span class="n">holder</span><span class="o">,</span> <span class="kt">int</span> <span class="n">position</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">ConfigItem</span> <span class="n">item</span> <span class="o">=</span> <span class="n">keyDescriptions</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">position</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">item</span><span class="o">.</span><span class="na">key</span><span class="o">;</span>

        <span class="c1">// 第一列：键名和描述</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">tvKey</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">tvKeyDescription</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">item</span><span class="o">.</span><span class="na">description</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">item</span><span class="o">.</span><span class="na">description</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">item</span><span class="o">.</span><span class="na">description</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">holder</span><span class="o">.</span><span class="na">tvKeyDescription</span><span class="o">.</span><span class="na">setVisibility</span><span class="o">(</span><span class="nc">View</span><span class="o">.</span><span class="na">VISIBLE</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">holder</span><span class="o">.</span><span class="na">tvKeyDescription</span><span class="o">.</span><span class="na">setVisibility</span><span class="o">(</span><span class="nc">View</span><span class="o">.</span><span class="na">GONE</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">holderMap</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">holder</span><span class="o">);</span>

        <span class="c1">// 第二列：原始值</span>
        <span class="nc">Object</span> <span class="n">value</span> <span class="o">=</span> <span class="n">originalObject</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">tvOriginalValue</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">value</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="s">""</span> <span class="o">:</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">value</span><span class="o">));</span>

        <span class="c1">// 第三列：修改值</span>
        <span class="nc">Object</span> <span class="n">modified</span> <span class="o">=</span> <span class="n">modifiedValues</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="n">holder</span><span class="o">.</span><span class="na">etModifiedValue</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">modified</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="s">""</span> <span class="o">:</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">modified</span><span class="o">));</span>

        <span class="n">holder</span><span class="o">.</span><span class="na">etModifiedValue</span><span class="o">.</span><span class="na">addTextChangedListener</span><span class="o">(</span><span class="k">new</span> <span class="n">android</span><span class="o">.</span><span class="na">text</span><span class="o">.</span><span class="na">TextWatcher</span><span class="o">()</span> <span class="o">{</span>
            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">beforeTextChanged</span><span class="o">(</span><span class="nc">CharSequence</span> <span class="n">s</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">,</span> <span class="kt">int</span> <span class="n">count</span><span class="o">,</span> <span class="kt">int</span> <span class="n">after</span><span class="o">)</span> <span class="o">{</span>
            <span class="o">}</span>

            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onTextChanged</span><span class="o">(</span><span class="nc">CharSequence</span> <span class="n">s</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">,</span> <span class="kt">int</span> <span class="n">before</span><span class="o">,</span> <span class="kt">int</span> <span class="n">count</span><span class="o">)</span> <span class="o">{</span>
            <span class="o">}</span>

            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">afterTextChanged</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">text</span><span class="o">.</span><span class="na">Editable</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
                <span class="nc">String</span> <span class="n">newValueStr</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="na">toString</span><span class="o">();</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">newValueStr</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
                    <span class="n">modifiedValues</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                    <span class="n">modifiedValues</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="n">newValueStr</span><span class="o">);</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">});</span>

    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">getItemCount</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">keyDescriptions</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">ViewHolder</span> <span class="kd">extends</span> <span class="nc">RecyclerView</span><span class="o">.</span><span class="na">ViewHolder</span> <span class="o">{</span>
        <span class="nc">TextView</span> <span class="n">tvKey</span><span class="o">;</span>
        <span class="nc">TextView</span> <span class="n">tvKeyDescription</span><span class="o">;</span>
        <span class="nc">TextView</span> <span class="n">tvOriginalValue</span><span class="o">;</span>
        <span class="nc">EditText</span> <span class="n">etModifiedValue</span><span class="o">;</span>

        <span class="nc">ViewHolder</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">View</span> <span class="n">itemView</span><span class="o">)</span> <span class="o">{</span>
            <span class="kd">super</span><span class="o">(</span><span class="n">itemView</span><span class="o">);</span>
            <span class="n">tvKey</span> <span class="o">=</span> <span class="n">itemView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tvKey</span><span class="o">);</span>
            <span class="n">tvKeyDescription</span> <span class="o">=</span> <span class="n">itemView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tvKeyDescription</span><span class="o">);</span>
            <span class="n">tvOriginalValue</span> <span class="o">=</span> <span class="n">itemView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tvOriginalValue</span><span class="o">);</span>
            <span class="n">etModifiedValue</span> <span class="o">=</span> <span class="n">itemView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">etModifiedValue</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="423-fragment-实现">4.2.3. Fragment 实现</h3>
<p>先实现一个简单的框架：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConfigEditorFragment</span> <span class="kd">extends</span> <span class="nc">Fragment</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">ARG_CONFIG</span> <span class="o">=</span> <span class="s">"config"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">ConfigEditorController</span> <span class="n">controller</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">Runnable</span> <span class="n">refreshCallback</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">ConfigEditorFragment</span> <span class="nf">newInstance</span><span class="o">(</span><span class="nc">BaseConfig</span> <span class="n">config</span><span class="o">,</span> <span class="nc">Runnable</span> <span class="n">refreshCallback</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Bundle</span> <span class="n">b</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Bundle</span><span class="o">();</span>
        <span class="n">b</span><span class="o">.</span><span class="na">putSerializable</span><span class="o">(</span><span class="no">ARG_CONFIG</span><span class="o">,</span> <span class="n">config</span><span class="o">);</span>

        <span class="nc">ConfigEditorFragment</span> <span class="n">f</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConfigEditorFragment</span><span class="o">();</span>
        <span class="n">f</span><span class="o">.</span><span class="na">setArguments</span><span class="o">(</span><span class="n">b</span><span class="o">);</span>

        <span class="nc">ConfigEditorFragment</span><span class="o">.</span><span class="na">refreshCallback</span> <span class="o">=</span> <span class="n">refreshCallback</span><span class="o">;</span>
        <span class="k">return</span> <span class="n">f</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">View</span> <span class="nf">onCreateView</span><span class="o">(</span><span class="nc">LayoutInflater</span> <span class="n">i</span><span class="o">,</span> <span class="nc">ViewGroup</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Bundle</span> <span class="n">b</span><span class="o">)</span> <span class="o">{</span>

        <span class="nc">View</span> <span class="n">v</span> <span class="o">=</span> <span class="n">i</span><span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">fragment_config_editor</span><span class="o">,</span> <span class="n">c</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>

        <span class="c1">// 添加全局点击事件：点击后关闭键盘并移除焦点</span>
        <span class="n">v</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="n">view</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="nc">UiUtils</span><span class="o">.</span><span class="na">hideKeyboardAndClearFocus</span><span class="o">(</span><span class="n">requireContext</span><span class="o">(),</span> <span class="n">view</span><span class="o">);</span>
        <span class="o">});</span>

        <span class="nc">Bundle</span> <span class="n">args</span> <span class="o">=</span> <span class="n">getArguments</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">args</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Arguments cannot be null"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="nc">BaseConfig</span> <span class="n">config</span> <span class="o">=</span> <span class="o">(</span><span class="nc">BaseConfig</span><span class="o">)</span> <span class="n">args</span><span class="o">.</span><span class="na">getSerializable</span><span class="o">(</span><span class="no">ARG_CONFIG</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">config</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalStateException</span><span class="o">(</span><span class="s">"Config cannot be null"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ConfigEditorController</span><span class="o">(</span><span class="n">requireContext</span><span class="o">(),</span> <span class="n">v</span><span class="o">,</span> <span class="n">config</span><span class="o">,</span> <span class="n">refreshCallback</span><span class="o">);</span>

        <span class="k">return</span> <span class="n">v</span><span class="o">;</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>这里使用了一个控制器类 ConfigEditorController 来处理具体的逻辑，以便于将业务逻辑和 UI 逻辑分离。这里需要处理的逻辑较多，主要有：</p>
<ul>
  <li>初始化 RecyclerView 列表</li>
  <li>处理保存按钮点击事件</li>
  <li>加载和保存配置数据</li>
  <li>同步 UI 内容和变量</li>
  <li>刷新等方法接口的实现、上下文在 Fragment 中传递</li>
  <li>导入时弹出新的配置选择列表</li>
</ul>

<p>以及其他一些细节的功能，实现过程较为繁琐，这里就不一一展开了。导入时弹出的新的配置选择列表新建了一个继承自<code class="language-plaintext highlighter-rouge">android.app.Dialog</code>的类 <code class="language-plaintext highlighter-rouge">ConfigSelectDialog</code>，用于显示可供选择的配置列表，用户选择后返回所选配置。代码可以参考 GitHub 仓库。下面是该对话框的界面截图：</p>

<p><img src="/post_assets/images/2025/12/18-dialog.jpg" alt="配置选择对话框" /></p>

<h2 id="424-持久化">4.2.4. 持久化</h2>
<p>配置文件持久化保存在应用的私有存储空间内的 files 目录下。配置文件以 JSON 格式保存，使用 JSONObject 进行读写。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConfigStorage</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">ROOT_DIR</span> <span class="o">=</span> <span class="s">"configs"</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">CURRENT_CONFIG_FILE</span> <span class="o">=</span> <span class="s">"current_config.json"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Set</span><span class="o">&lt;</span><span class="nc">Character</span><span class="o">&gt;</span> <span class="n">illegalChars</span> <span class="o">=</span> <span class="nc">Set</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="sc">'\\'</span><span class="o">,</span> <span class="sc">'/'</span><span class="o">,</span> <span class="sc">':'</span><span class="o">,</span> <span class="sc">'*'</span><span class="o">,</span> <span class="sc">'?'</span><span class="o">,</span> <span class="sc">'\"'</span><span class="o">,</span> <span class="sc">'&lt;'</span><span class="o">,</span> <span class="sc">'&gt;'</span><span class="o">,</span> <span class="sc">'|'</span><span class="o">,</span> <span class="sc">'\''</span><span class="o">);</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">File</span> <span class="nf">getConfigDir</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">c</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="no">ROOT_DIR</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="n">cls</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">());</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">dir</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="n">dir</span><span class="o">.</span><span class="na">mkdirs</span><span class="o">();</span>
        <span class="k">return</span> <span class="n">dir</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">saveConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">BaseConfig</span> <span class="n">obj</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">overwrite</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">obj</span><span class="o">.</span><span class="na">configName</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">obj</span><span class="o">.</span><span class="na">configName</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Config name cannot be empty"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">overwrite</span> <span class="o">||</span> <span class="n">obj</span><span class="o">.</span><span class="na">configId</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">obj</span><span class="o">.</span><span class="na">configId</span> <span class="o">=</span> <span class="n">generateId</span><span class="o">();</span>
            <span class="n">obj</span><span class="o">.</span><span class="na">createdAt</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>
        <span class="o">}</span>
        <span class="n">obj</span><span class="o">.</span><span class="na">updatedAt</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>

        <span class="nc">JSONObject</span> <span class="n">root</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="na">toJsonObject</span><span class="o">();</span>
        <span class="n">createConfigFile</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">obj</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="n">obj</span><span class="o">.</span><span class="na">configId</span><span class="o">,</span> <span class="n">root</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">saveConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">JSONObject</span> <span class="n">obj</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">overwrite</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">configId</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"configId"</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">configName</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"configName"</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">();</span>

        <span class="k">if</span> <span class="o">(</span><span class="n">configName</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">IllegalArgumentException</span><span class="o">(</span><span class="s">"Config name cannot be empty"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">overwrite</span> <span class="o">||</span> <span class="n">configId</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">configId</span> <span class="o">=</span> <span class="n">generateId</span><span class="o">();</span>
            <span class="n">obj</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"configId"</span><span class="o">,</span> <span class="n">configId</span><span class="o">);</span>
            <span class="n">obj</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"createdAt"</span><span class="o">,</span> <span class="n">now</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">obj</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"updatedAt"</span><span class="o">,</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">());</span>
        <span class="n">createConfigFile</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">,</span> <span class="n">configId</span><span class="o">,</span> <span class="n">obj</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">JSONObject</span><span class="o">&gt;</span> <span class="nf">loadConfigList</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">JSONObject</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
        <span class="nc">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">getConfigDir</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
        <span class="k">for</span> <span class="o">(</span><span class="nc">File</span> <span class="n">f</span> <span class="o">:</span> <span class="n">dir</span><span class="o">.</span><span class="na">listFiles</span><span class="o">())</span> <span class="o">{</span>
            <span class="nc">JSONObject</span> <span class="n">obj</span> <span class="o">=</span> <span class="n">readJSONObject</span><span class="o">(</span><span class="n">f</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">obj</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                <span class="n">list</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">obj</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">list</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">BaseConfig</span> <span class="nf">loadConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">getConfigDir</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
        <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="n">id</span> <span class="o">+</span> <span class="s">".json"</span><span class="o">);</span>
        <span class="nc">JSONObject</span> <span class="n">obj</span> <span class="o">=</span> <span class="n">readJSONObject</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">obj</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">try</span> <span class="o">{</span>
                <span class="k">return</span> <span class="nc">BaseConfig</span><span class="o">.</span><span class="na">fromJsonObject</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
            <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"ConfigStorage"</span><span class="o">,</span> <span class="s">"loadConfig: "</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">boolean</span> <span class="nf">configNameExists</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">String</span> <span class="n">configName</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">List</span><span class="o">&lt;</span><span class="nc">JSONObject</span><span class="o">&gt;</span> <span class="n">list</span> <span class="o">=</span> <span class="nc">ConfigStorage</span><span class="o">.</span><span class="na">loadConfigList</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
            <span class="k">for</span> <span class="o">(</span><span class="nc">JSONObject</span> <span class="n">obj</span> <span class="o">:</span> <span class="n">list</span><span class="o">)</span> <span class="o">{</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">configName</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">obj</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"configName"</span><span class="o">)))</span> <span class="o">{</span>
                    <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">ignored</span><span class="o">)</span> <span class="o">{</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">deleteConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">String</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">getConfigDir</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
        <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="n">id</span> <span class="o">+</span> <span class="s">".json"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">file</span><span class="o">.</span><span class="na">delete</span><span class="o">();</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">checkFileName</span><span class="o">(</span><span class="nc">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">name</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">name</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="s">"配置名称不能为空"</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">char</span> <span class="n">c</span> <span class="o">:</span> <span class="n">name</span><span class="o">.</span><span class="na">toCharArray</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">illegalChars</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">c</span><span class="o">))</span> <span class="o">{</span>
                <span class="k">return</span> <span class="s">"配置名称包含非法字符"</span><span class="o">;</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">name</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">&gt;</span> <span class="mi">100</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="s">"配置名称过长"</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">applyCurrentConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">String</span> <span class="n">configId</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">rootDir</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">c</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="no">ROOT_DIR</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">rootDir</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="n">rootDir</span><span class="o">.</span><span class="na">mkdirs</span><span class="o">();</span>
        <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">rootDir</span><span class="o">,</span> <span class="no">CURRENT_CONFIG_FILE</span><span class="o">);</span>
        <span class="nc">JSONObject</span> <span class="n">root</span> <span class="o">=</span> <span class="n">readJSONObject</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">root</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">root</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">JSONObject</span><span class="o">();</span>
        <span class="o">}</span>

        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">cls</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">();</span>

        <span class="n">root</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="nc">Objects</span><span class="o">.</span><span class="na">requireNonNullElse</span><span class="o">(</span><span class="n">configId</span><span class="o">,</span> <span class="nc">JSONObject</span><span class="o">.</span><span class="na">NULL</span><span class="o">));</span>

        <span class="nc">FileWriter</span> <span class="n">fw</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FileWriter</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
        <span class="n">fw</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">root</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="mi">2</span><span class="o">));</span>
        <span class="n">fw</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="o">&lt;</span><span class="no">T</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="no">T</span> <span class="nf">getCurrentConfig</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;</span><span class="no">T</span><span class="o">&gt;</span> <span class="n">cls</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">c</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="no">ROOT_DIR</span> <span class="o">+</span> <span class="s">"/"</span> <span class="o">+</span> <span class="no">CURRENT_CONFIG_FILE</span><span class="o">);</span>
        <span class="nc">JSONObject</span> <span class="n">root</span> <span class="o">=</span> <span class="n">readJSONObject</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">root</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="n">cls</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">root</span><span class="o">.</span><span class="na">has</span><span class="o">(</span><span class="n">key</span><span class="o">)</span> <span class="o">||</span> <span class="n">root</span><span class="o">.</span><span class="na">isNull</span><span class="o">(</span><span class="n">key</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">String</span> <span class="n">configId</span> <span class="o">=</span> <span class="n">root</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="n">key</span><span class="o">);</span>
        <span class="nc">File</span> <span class="n">configFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">getConfigDir</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">),</span> <span class="n">configId</span> <span class="o">+</span> <span class="s">".json"</span><span class="o">);</span>
        <span class="nc">JSONObject</span> <span class="n">obj</span> <span class="o">=</span> <span class="n">readJSONObject</span><span class="o">(</span><span class="n">configFile</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">obj</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="k">return</span> <span class="nc">BaseConfig</span><span class="o">.</span><span class="na">fromJsonObject</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">createConfigFile</span><span class="o">(</span><span class="nc">Context</span> <span class="n">c</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cls</span><span class="o">,</span> <span class="nc">String</span> <span class="n">id</span><span class="o">,</span> <span class="nc">JSONObject</span> <span class="n">obj</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>
        <span class="nc">File</span> <span class="n">file</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">File</span><span class="o">(</span><span class="n">getConfigDir</span><span class="o">(</span><span class="n">c</span><span class="o">,</span> <span class="n">cls</span><span class="o">),</span> <span class="n">id</span> <span class="o">+</span> <span class="s">".json"</span><span class="o">);</span>
        <span class="nc">FileWriter</span> <span class="n">fw</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">FileWriter</span><span class="o">(</span><span class="n">file</span><span class="o">);</span>
        <span class="n">fw</span><span class="o">.</span><span class="na">write</span><span class="o">(</span><span class="n">obj</span><span class="o">.</span><span class="na">toString</span><span class="o">(</span><span class="mi">2</span><span class="o">));</span>
        <span class="n">fw</span><span class="o">.</span><span class="na">close</span><span class="o">();</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">generateId</span><span class="o">()</span> <span class="o">{</span>
        <span class="c1">// 使用时间戳 + 随机数生成唯一ID</span>
        <span class="kt">int</span> <span class="n">randomPart</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="o">(</span><span class="nc">Math</span><span class="o">.</span><span class="na">random</span><span class="o">()</span> <span class="o">*</span> <span class="mi">100000</span><span class="o">);</span>
        <span class="k">return</span> <span class="nc">System</span><span class="o">.</span><span class="na">currentTimeMillis</span><span class="o">()</span> <span class="o">+</span> <span class="s">"-"</span> <span class="o">+</span> <span class="n">randomPart</span><span class="o">;</span>
    <span class="o">}</span>


    <span class="kd">private</span> <span class="kd">static</span> <span class="nc">JSONObject</span> <span class="nf">readJSONObject</span><span class="o">(</span><span class="nc">File</span> <span class="n">f</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="n">f</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">json</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="nc">Files</span><span class="o">.</span><span class="na">readAllBytes</span><span class="o">(</span><span class="n">f</span><span class="o">.</span><span class="na">toPath</span><span class="o">()));</span>
            <span class="k">return</span> <span class="k">new</span> <span class="nf">JSONObject</span><span class="o">(</span><span class="n">json</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"ConfigStorage"</span><span class="o">,</span> <span class="s">"readJSONObject: "</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>存储方式比较简单，直接将每个配置对象序列化为 JSON 格式保存为单独的文件，文件名为配置 ID 加 .json 后缀。配置文件保存在应用的私有存储空间内的 files/configs/配置类名/ 目录下。而当前使用的配置 ID 则保存在 files/configs/current_config.json 文件中，以便每次启动时加载和 Xposed 模块注入时使用。</p>

<h1 id="5-各模块设计">5. 各模块设计</h1>
<h2 id="51-wifi模块设计">5.1. Wifi模块设计</h2>
<p>首先是继承自 BaseConfig 的 WifiData 类，用于存储 Wifi 相关的信息：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">WifiData</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span> <span class="kd">implements</span> <span class="nc">Serializable</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="n">keyDescriptions</span> <span class="o">=</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"networkType"</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"网络类型: 1: WIFI, 0: MOBILE, -1: 无网络"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"ssid"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"Wi-Fi 名称 (SSID)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"bssid"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"AP 的 MAC 地址 (BSSID)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"securityType"</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"加密类型(安卓12及以上)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"frequency"</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"频率 (MHz)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"wifiStandard"</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"Wi-Fi 标准(安卓11及以上)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"rssi"</span><span class="o">,</span> <span class="nc">Integer</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"信号强度 (RSSI)"</span><span class="o">)</span>
    <span class="o">);</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">KEY_OF_DEFAULT_NAME</span> <span class="o">=</span> <span class="s">"ssid"</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="nf">getConfigItems</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">keyDescriptions</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getKeyOfDefaultName</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="no">KEY_OF_DEFAULT_NAME</span><span class="o">;</span>
    <span class="o">}</span>

    
    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">WifiData</span> <span class="nf">fromWifiInfo</span><span class="o">(</span><span class="nc">WifiInfo</span> <span class="n">wifiInfo</span><span class="o">,</span> <span class="kt">int</span> <span class="n">networkType</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">WifiData</span> <span class="n">wifiData</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">WifiData</span><span class="o">();</span>
        <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"networkType"</span><span class="o">,</span> <span class="n">networkType</span><span class="o">);</span>
        <span class="nc">String</span> <span class="n">ssid</span> <span class="o">=</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getSSID</span><span class="o">();</span>
        <span class="c1">// wifi info 返回的 ssid 有时会带引号，需要去掉</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">ssid</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="n">ssid</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">&gt;=</span> <span class="mi">2</span> <span class="o">&amp;&amp;</span> <span class="n">ssid</span><span class="o">.</span><span class="na">startsWith</span><span class="o">(</span><span class="s">"\""</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">ssid</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="s">"\""</span><span class="o">))</span> <span class="o">{</span>
            <span class="n">ssid</span> <span class="o">=</span> <span class="n">ssid</span><span class="o">.</span><span class="na">substring</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">ssid</span><span class="o">.</span><span class="na">length</span><span class="o">()</span> <span class="o">-</span> <span class="mi">1</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"ssid"</span><span class="o">,</span> <span class="n">ssid</span><span class="o">);</span>
        <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"bssid"</span><span class="o">,</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getBSSID</span><span class="o">());</span>
        <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"frequency"</span><span class="o">,</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getFrequency</span><span class="o">());</span>
        <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"rssi"</span><span class="o">,</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getRssi</span><span class="o">());</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">S</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// 安卓12 及以上</span>
            <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"securityType"</span><span class="o">,</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getCurrentSecurityType</span><span class="o">());</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">R</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// 安卓11 及以上</span>
            <span class="n">wifiData</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"wifiStandard"</span><span class="o">,</span> <span class="n">wifiInfo</span><span class="o">.</span><span class="na">getWifiStandard</span><span class="o">());</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">wifiData</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>获取方式目前使用的方法比较简单，网络类型使用 ConnectivityManager.getActiveNetworkInfo() 获取，Wifi 信息使用 WifiManager.getConnectionInfo() 获取。后续可以考虑使用更复杂的方式获取，比如从扫描结果中获取等。而存储的内容则基本是所有 <code class="language-plaintext highlighter-rouge">android.net.wifi.WifiInfo</code> 对象可以直接用<code class="language-plaintext highlighter-rouge">get</code>方法拿到的所有值。加载网络和 Wifi 信息的代码如下：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">loadNetworkInfo</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">NetworkInfo</span> <span class="n">info</span> <span class="o">=</span> <span class="n">connectivityManager</span><span class="o">.</span><span class="na">getActiveNetworkInfo</span><span class="o">();</span>
        <span class="nc">Integer</span> <span class="n">type</span> <span class="o">=</span> <span class="o">(</span><span class="n">info</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">?</span> <span class="n">info</span><span class="o">.</span><span class="na">getType</span><span class="o">()</span> <span class="o">:</span> <span class="kc">null</span><span class="o">;</span>
        <span class="c1">// 1: WIFI, 0: MOBILE, null: 无网络</span>
        <span class="nc">Log</span><span class="o">.</span><span class="na">d</span><span class="o">(</span><span class="s">"WifiFragment"</span><span class="o">,</span> <span class="s">"Active Network Type: "</span> <span class="o">+</span> <span class="n">type</span><span class="o">);</span>
        <span class="nc">WifiInfo</span> <span class="n">wifiInfo</span> <span class="o">=</span> <span class="n">wifiManager</span><span class="o">.</span><span class="na">getConnectionInfo</span><span class="o">();</span>
        <span class="nc">WifiData</span> <span class="n">data</span> <span class="o">=</span> <span class="nc">WifiData</span><span class="o">.</span><span class="na">fromWifiInfo</span><span class="o">(</span><span class="n">wifiInfo</span><span class="o">,</span> <span class="n">type</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="o">-</span><span class="mi">1</span> <span class="o">:</span> <span class="n">type</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">wifiConfigFragment</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">wifiConfigFragment</span><span class="o">.</span><span class="na">setTargetConfig</span><span class="o">(</span><span class="n">data</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>

<h2 id="52-定位模块设计">5.2. 定位模块设计</h2>
<p>定位模块的设计思路和 Wifi 模块类似，先定义一个 LocationData 类继承自 BaseConfig，用于存储定位相关的信息。然后在定位模块的 Fragment 中加载当前的位置信息并显示在界面上。定位信息的获取可以使用 LocationManager 或 FusedLocationProviderClient 等方式获取当前位置信息。存储的内容则是 <code class="language-plaintext highlighter-rouge">android.location.Location</code> 对象可以直接用 <code class="language-plaintext highlighter-rouge">get</code> 方法拿到的所有值。LocationData 类的实现如下：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LocationData</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span> <span class="kd">implements</span> <span class="nc">Serializable</span> <span class="o">{</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="n">keyDescriptions</span> <span class="o">=</span> <span class="nc">List</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"latitude"</span><span class="o">,</span> <span class="nc">Double</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"纬度"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"longitude"</span><span class="o">,</span> <span class="nc">Double</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"经度"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"horizontalAccuracy"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"水平精度，误差范围(米)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"altitude"</span><span class="o">,</span> <span class="nc">Double</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"GPS原始高度，WGS84(米)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"verticalAccuracy"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"垂直方向的误差范围(米)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"mslAltitude"</span><span class="o">,</span> <span class="nc">Double</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"相对于平均海平面的高度(米)(Android 14+)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"mslAltitudeAccuracy"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"mslAltitude的误差范围(米)(Android 14+)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"speed"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"速度(米/秒)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"speedAccuracy"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"speed的误差范围(米/秒)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"bearing"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"方位角，正北方向顺时针计(度)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"bearingAccuracy"</span><span class="o">,</span> <span class="nc">Float</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"bearing的误差范围(度)"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"isMock"</span><span class="o">,</span> <span class="nc">Boolean</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"是否为模拟位置"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">ConfigItem</span><span class="o">(</span><span class="s">"elapsedRealtimeMillis"</span><span class="o">,</span> <span class="nc">Long</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="s">"位置被测量时的系统运行时间(毫秒)(Android 13+)"</span><span class="o">)</span>
    <span class="o">);</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">List</span><span class="o">&lt;</span><span class="nc">ConfigItem</span><span class="o">&gt;</span> <span class="nf">getConfigItems</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">keyDescriptions</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getKeyOfDefaultName</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kd">static</span> <span class="nc">LocationData</span> <span class="nf">fromLocation</span><span class="o">(</span><span class="nc">Location</span> <span class="n">location</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">LocationData</span> <span class="n">obj</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LocationData</span><span class="o">();</span>
        <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">map</span> <span class="o">=</span> <span class="n">obj</span><span class="o">.</span><span class="na">data</span><span class="o">;</span>
        <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"latitude"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getLatitude</span><span class="o">());</span>
        <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"longitude"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getLongitude</span><span class="o">());</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasAccuracy</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"horizontalAccuracy"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getAccuracy</span><span class="o">());</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasAltitude</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"altitude"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getAltitude</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasVerticalAccuracy</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"verticalAccuracy"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getVerticalAccuracyMeters</span><span class="o">());</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">UPSIDE_DOWN_CAKE</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// Android 14+</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasMslAltitude</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">obj</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"mslAltitude"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getMslAltitudeMeters</span><span class="o">());</span>
                <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasMslAltitudeAccuracy</span><span class="o">())</span> <span class="o">{</span>
                    <span class="n">obj</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"mslAltitudeAccuracy"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getMslAltitudeAccuracyMeters</span><span class="o">());</span>
                <span class="o">}</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasSpeed</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"speed"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getSpeed</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasSpeedAccuracy</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"speedAccuracy"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getSpeedAccuracyMetersPerSecond</span><span class="o">());</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasBearing</span><span class="o">())</span> <span class="o">{</span>
            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"bearing"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getBearing</span><span class="o">());</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">location</span><span class="o">.</span><span class="na">hasBearingAccuracy</span><span class="o">())</span> <span class="o">{</span>
                <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"bearingAccuracy"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getBearingAccuracyDegrees</span><span class="o">());</span>
            <span class="o">}</span>
        <span class="o">}</span>
        <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"isMock"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">isFromMockProvider</span><span class="o">());</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">TIRAMISU</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// Android 13+</span>
            <span class="n">map</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="s">"elapsedRealtimeMillis"</span><span class="o">,</span> <span class="n">location</span><span class="o">.</span><span class="na">getElapsedRealtimeMillis</span><span class="o">());</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="n">obj</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>获取定位信息参考了：<a href="https://developer.aliyun.com/article/1308376">https://developer.aliyun.com/article/1308376</a></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">LocationFragment</span> <span class="kd">extends</span> <span class="nc">Fragment</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="nc">LocationManager</span> <span class="n">locationManager</span><span class="o">;</span>
    <span class="kd">private</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">LOCATION_FRAGMENT_TAG</span> <span class="o">=</span> <span class="s">"location_fragment"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">ConfigEditorFragment</span> <span class="n">locationConfigFragment</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span>

    <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">isRequestingLocation</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">View</span> <span class="nf">onCreateView</span><span class="o">(</span><span class="nc">LayoutInflater</span> <span class="n">inflater</span><span class="o">,</span> <span class="nc">ViewGroup</span> <span class="n">container</span><span class="o">,</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">View</span> <span class="n">rootView</span> <span class="o">=</span> <span class="n">inflater</span><span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">fragment_location_page</span><span class="o">,</span> <span class="n">container</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span>
        <span class="n">context</span> <span class="o">=</span> <span class="n">requireContext</span><span class="o">();</span>

        <span class="n">locationManager</span> <span class="o">=</span> <span class="o">(</span><span class="nc">LocationManager</span><span class="o">)</span> <span class="n">context</span><span class="o">.</span><span class="na">getSystemService</span><span class="o">(</span><span class="nc">Context</span><span class="o">.</span><span class="na">LOCATION_SERVICE</span><span class="o">);</span>

        <span class="nc">FragmentTransaction</span> <span class="n">ft</span> <span class="o">=</span> <span class="n">getChildFragmentManager</span><span class="o">().</span><span class="na">beginTransaction</span><span class="o">();</span>
        <span class="n">locationConfigFragment</span> <span class="o">=</span> <span class="nc">ConfigEditorFragment</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="k">new</span> <span class="nc">LocationData</span><span class="o">(),</span> <span class="k">this</span><span class="o">::</span><span class="n">loadLocationInfo</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">replace</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">location_fragment_container</span><span class="o">,</span> <span class="n">locationConfigFragment</span><span class="o">,</span> <span class="no">LOCATION_FRAGMENT_TAG</span><span class="o">);</span>
        <span class="n">ft</span><span class="o">.</span><span class="na">commit</span><span class="o">();</span>

        <span class="n">rootView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">btnOpenMap</span><span class="o">).</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="n">v</span> <span class="o">-&gt;</span> <span class="nc">UiUtils</span><span class="o">.</span><span class="na">toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"功能开发中，敬请期待！"</span><span class="o">));</span>

        <span class="k">return</span> <span class="n">rootView</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onViewCreated</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">View</span> <span class="n">view</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
        <span class="kd">super</span><span class="o">.</span><span class="na">onViewCreated</span><span class="o">(</span><span class="n">view</span><span class="o">,</span> <span class="n">savedInstanceState</span><span class="o">);</span>
        <span class="n">view</span><span class="o">.</span><span class="na">post</span><span class="o">(</span><span class="k">this</span><span class="o">::</span><span class="n">getLastKnownLocation</span><span class="o">);</span>
        <span class="n">view</span><span class="o">.</span><span class="na">post</span><span class="o">(</span><span class="nl">locationConfigFragment:</span><span class="o">:</span><span class="n">getCurrentConfig</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">getLastKnownLocation</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">ActivityCompat</span><span class="o">.</span><span class="na">checkSelfPermission</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">android</span><span class="o">.</span><span class="na">Manifest</span><span class="o">.</span><span class="na">permission</span><span class="o">.</span><span class="na">ACCESS_FINE_LOCATION</span><span class="o">)</span>
                <span class="o">!=</span> <span class="nc">PackageManager</span><span class="o">.</span><span class="na">PERMISSION_GRANTED</span>
                <span class="o">&amp;&amp;</span> <span class="nc">ActivityCompat</span><span class="o">.</span><span class="na">checkSelfPermission</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="nc">Manifest</span><span class="o">.</span><span class="na">permission</span><span class="o">.</span><span class="na">ACCESS_COARSE_LOCATION</span><span class="o">)</span>
                <span class="o">!=</span> <span class="nc">PackageManager</span><span class="o">.</span><span class="na">PERMISSION_GRANTED</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">UiUtils</span><span class="o">.</span><span class="na">toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"无定位权限，无法获取位置信息"</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">Location</span> <span class="n">last</span> <span class="o">=</span> <span class="n">locationManager</span><span class="o">.</span><span class="na">getLastKnownLocation</span><span class="o">(</span><span class="nc">LocationManager</span><span class="o">.</span><span class="na">GPS_PROVIDER</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">last</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">LocationData</span> <span class="n">locationData</span> <span class="o">=</span> <span class="nc">LocationData</span><span class="o">.</span><span class="na">fromLocation</span><span class="o">(</span><span class="n">last</span><span class="o">);</span>
            <span class="n">locationConfigFragment</span><span class="o">.</span><span class="na">setTargetConfig</span><span class="o">(</span><span class="n">locationData</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">loadLocationInfo</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">isRequestingLocation</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">ActivityCompat</span><span class="o">.</span><span class="na">checkSelfPermission</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">android</span><span class="o">.</span><span class="na">Manifest</span><span class="o">.</span><span class="na">permission</span><span class="o">.</span><span class="na">ACCESS_FINE_LOCATION</span><span class="o">)</span>
                <span class="o">!=</span> <span class="nc">PackageManager</span><span class="o">.</span><span class="na">PERMISSION_GRANTED</span>
                <span class="o">&amp;&amp;</span> <span class="nc">ActivityCompat</span><span class="o">.</span><span class="na">checkSelfPermission</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="nc">Manifest</span><span class="o">.</span><span class="na">permission</span><span class="o">.</span><span class="na">ACCESS_COARSE_LOCATION</span><span class="o">)</span>
                <span class="o">!=</span> <span class="nc">PackageManager</span><span class="o">.</span><span class="na">PERMISSION_GRANTED</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">UiUtils</span><span class="o">.</span><span class="na">toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"无定位权限，无法获取位置信息"</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">LocationListener</span> <span class="n">listener</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LocationListener</span><span class="o">()</span> <span class="o">{</span>
            <span class="nd">@Override</span>
            <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onLocationChanged</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Location</span> <span class="n">location</span><span class="o">)</span> <span class="o">{</span>
                <span class="c1">// 拿到最新位置</span>
                <span class="nc">LocationData</span> <span class="n">locationData</span> <span class="o">=</span> <span class="nc">LocationData</span><span class="o">.</span><span class="na">fromLocation</span><span class="o">(</span><span class="n">location</span><span class="o">);</span>
                <span class="n">locationConfigFragment</span><span class="o">.</span><span class="na">setTargetConfig</span><span class="o">(</span><span class="n">locationData</span><span class="o">);</span>
                <span class="c1">// 用完立刻停止定位</span>
                <span class="n">locationManager</span><span class="o">.</span><span class="na">removeUpdates</span><span class="o">(</span><span class="k">this</span><span class="o">);</span>
                <span class="n">isRequestingLocation</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span>
                <span class="nc">UiUtils</span><span class="o">.</span><span class="na">toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"位置信息已更新"</span><span class="o">);</span>
            <span class="o">}</span>
        <span class="o">};</span>
        <span class="n">locationManager</span><span class="o">.</span><span class="na">requestLocationUpdates</span><span class="o">(</span>
                <span class="nc">LocationManager</span><span class="o">.</span><span class="na">GPS_PROVIDER</span><span class="o">,</span>
                <span class="mi">0</span><span class="o">,</span>
                <span class="mi">0</span><span class="o">,</span>
                <span class="n">listener</span>
        <span class="o">);</span>
        <span class="n">isRequestingLocation</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span>
        <span class="nc">UiUtils</span><span class="o">.</span><span class="na">toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"正在获取定位"</span><span class="o">);</span>
    <span class="o">}</span>

<span class="o">}</span>
</code></pre></div></div>

<p>我没有使用上面网站里提到的监听方式：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">locationManager</span><span class="o">.</span><span class="na">requestLocationUpdates</span><span class="o">(</span>
                <span class="nc">LocationManager</span><span class="o">.</span><span class="na">GPS_PROVIDER</span><span class="o">,</span><span class="c1">//指定GPS定位的提供者</span>
                <span class="mi">1000</span><span class="o">,</span><span class="c1">//间隔时间</span>
                <span class="mi">1</span><span class="o">,</span><span class="c1">//位置更新之间的最小距离</span>
                <span class="k">new</span> <span class="nf">LocationListener</span><span class="o">()</span> <span class="o">{</span> <span class="c1">//监听GPS定位信息是否改变</span>
                    <span class="nd">@Override</span>
                    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onLocationChanged</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Location</span> <span class="n">location</span><span class="o">)</span> <span class="o">{</span> <span class="c1">//GPS信息发生改变时回调</span>
                    <span class="o">}</span>
                <span class="o">}</span>
        <span class="o">);</span>
</code></pre></div></div>
<p>因为这种方式一直监听位置变化，比较耗电，而且我只需要获取一次最新位置即可。所以改成了上面的方式，初始化和点击按钮时再获取一次最新位置，拿到后立刻停止监听。</p>

<h1 id="6-添加-xposed-注入代码">6. 添加 Xposed 注入代码</h1>
<h2 id="61-配置-xposed-环境">6.1. 配置 Xposed 环境</h2>
<ol>
  <li>在根目录的 settings.gradle 文件中添加 Xposed 仓库：
    <div class="language-gradle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dependencyResolutionManagement</span> <span class="o">{</span>
 <span class="n">repositoriesMode</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">RepositoriesMode</span><span class="o">.</span><span class="na">FAIL_ON_PROJECT_REPOS</span><span class="o">)</span>
 <span class="k">repositories</span> <span class="o">{</span>
     <span class="n">google</span><span class="o">()</span>
     <span class="n">mavenCentral</span><span class="o">()</span>
     <span class="n">maven</span> <span class="o">{</span> <span class="n">url</span> <span class="s1">'https://api.xposed.info/'</span> <span class="o">}</span>
 <span class="o">}</span>
<span class="o">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>在 app 模块的 build.gradle 文件中添加 Xposed 依赖：
```gradle
dependencies {
 compileOnly ‘de.robv.android.xposed:api:82’
}</p>
  </li>
  <li>在 AndroidManifest.xml 中添加 Xposed 模块声明：
    <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
     <span class="nt">&lt;meta-data</span>
         <span class="na">android:name=</span><span class="s">"xposedmodule"</span>
         <span class="na">android:value=</span><span class="s">"true"</span><span class="nt">/&gt;</span>

     <span class="nt">&lt;meta-data</span>
         <span class="na">android:name=</span><span class="s">"xposeddescription"</span>
         <span class="na">android:value=</span><span class="s">"@string/xposed_description"</span><span class="nt">/&gt;</span>

     <span class="nt">&lt;meta-data</span>
         <span class="na">android:name=</span><span class="s">"xposedminversion"</span>
         <span class="na">android:value=</span><span class="s">"54"</span><span class="nt">/&gt;</span>

     <span class="nt">&lt;meta-data</span>
         <span class="na">android:name=</span><span class="s">"xposedscope"</span>
         <span class="na">android:resource=</span><span class="s">"@array/xposedscope"</span><span class="nt">/&gt;</span>
</code></pre></div>    </div>
    <p>xposedscope 定义了模块可以注入的应用范围。</p>
  </li>
  <li>在 assets 目录下创建 xposed_init 文件，内容为模块的入口类全名：
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>com.example.deviceinfo.MainHook
</code></pre></div>    </div>
  </li>
</ol>

<h2 id="62-编写-xposed-注入代码">6.2. 编写 Xposed 注入代码</h2>
<p>由于我没有 Xposed 的开发经验，安卓开发经验也不多，基本依靠能找到的有限的资料以及 AI 辅助完成了注入代码的编写。</p>

<h3 id="621-数据共享探索">6.2.1. 数据共享探索</h3>
<p>对我来说比较困难的地方在于数据共享，由于 Xposed 模块是注入到其他应用进程中的，而配置数据保存在模块应用的私有存储空间内，所以不能直接访问配置文件。</p>

<p>在旧版本安卓有使用 Xposed 提供的 XSharedPreferences 类来实现跨进程数据共享的方式:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 在应用进程中</span>
<span class="n">sharedPreferences</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getSharedPreferences</span><span class="o">(</span><span class="n">prefName</span><span class="o">,</span> <span class="nc">Context</span><span class="o">.</span><span class="na">MODE_WORLD_READABLE</span><span class="o">)</span>

<span class="c1">// 在 Xposed 模块中</span>
<span class="nc">XSharedPreferences</span> <span class="n">xSharedPreferences</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">XSharedPreferences</span><span class="o">(</span><span class="s">"com.example.deviceinfo"</span><span class="o">,</span> <span class="s">"config_prefs"</span><span class="o">);</span>
</code></pre></div></div>
<p>但在较新版本里，<code class="language-plaintext highlighter-rouge">MODE_WORLD_READABLE</code> 已被废弃无法使用。</p>

<p>我在网上找到了一个新的方式：New XSharedPreference
<a href="https://github.com/LSPosed/LSPosed/wiki/New-XSharedPreferences">https://github.com/LSPosed/LSPosed/wiki/New-XSharedPreferences</a></p>

<p>这种方式需要在 AndroidManifest.xml 中将 <code class="language-plaintext highlighter-rouge">xposedminversion</code> 设置为 93 及以上版本，这种情况下，如果模块被激活，<code class="language-plaintext highlighter-rouge">MODE_WORLD_READABLE</code>就不会报错了。</p>

<p>但是在我实际使用过程中，XSharedPreferences 获取到的对象总是 null，无法读取到数据。各个地方的代码都和官方 demo 以及网上其他资料一致，不知道是什么原因导致的。</p>

<p>最终我打算使用 ContentProvider 来实现数据共享。ContentProvider 是安卓提供的跨进程数据共享机制，可以通过 URI 访问其他应用的数据。模块应用中创建一个 ContentProvider 用于提供配置数据的访问接口，Xposed 模块通过 ContentResolver 访问该 ContentProvider 获取配置数据。</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">ConfigProvider</span> <span class="kd">extends</span> <span class="nc">ContentProvider</span> <span class="o">{</span>

    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;&gt;</span> <span class="n">configClassMap</span> <span class="o">=</span> <span class="nc">Map</span><span class="o">.</span><span class="na">of</span><span class="o">(</span>
            <span class="nc">WifiData</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">(),</span> <span class="nc">WifiData</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
            <span class="nc">LocationData</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">(),</span> <span class="nc">LocationData</span><span class="o">.</span><span class="na">class</span>
    <span class="o">);</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">onCreate</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Bundle</span> <span class="nf">call</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">String</span> <span class="n">method</span><span class="o">,</span> <span class="nc">String</span> <span class="n">arg</span><span class="o">,</span> <span class="nc">Bundle</span> <span class="n">extras</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(!</span><span class="nc">ProviderConstants</span><span class="o">.</span><span class="na">METHOD_GET_CURRENT_CONFIG</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">method</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">extras</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>

        <span class="nc">String</span> <span class="n">className</span> <span class="o">=</span> <span class="n">extras</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="nc">ProviderConstants</span><span class="o">.</span><span class="na">EXTRA_CLASS_NAME</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">className</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>

        <span class="nc">Class</span><span class="o">&lt;?</span> <span class="kd">extends</span> <span class="nc">BaseConfig</span><span class="o">&gt;</span> <span class="n">cls</span> <span class="o">=</span> <span class="n">configClassMap</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">className</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">cls</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span> <span class="n">getContext</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">context</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>

        <span class="k">try</span> <span class="o">{</span>
            <span class="nc">BaseConfig</span> <span class="n">config</span> <span class="o">=</span> <span class="nc">ConfigStorage</span><span class="o">.</span><span class="na">getCurrentConfig</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">cls</span><span class="o">);</span>
            <span class="k">if</span> <span class="o">(</span><span class="n">config</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>

            <span class="nc">Bundle</span> <span class="n">bundle</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Bundle</span><span class="o">();</span>

            <span class="k">for</span> <span class="o">(</span><span class="nc">Map</span><span class="o">.</span><span class="na">Entry</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">e</span> <span class="o">:</span> <span class="n">config</span><span class="o">.</span><span class="na">data</span><span class="o">.</span><span class="na">entrySet</span><span class="o">())</span> <span class="o">{</span>
                <span class="nc">Object</span> <span class="n">v</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="na">getValue</span><span class="o">();</span>
                <span class="nc">String</span> <span class="n">k</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="na">getKey</span><span class="o">();</span>

                <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">String</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putString</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">String</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">Integer</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putInt</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">Integer</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">Boolean</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putBoolean</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">Boolean</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">Long</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putLong</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">Long</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">Float</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putFloat</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">Float</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">v</span> <span class="k">instanceof</span> <span class="nc">Double</span><span class="o">)</span> <span class="o">{</span>
                    <span class="n">bundle</span><span class="o">.</span><span class="na">putDouble</span><span class="o">(</span><span class="n">k</span><span class="o">,</span> <span class="o">(</span><span class="nc">Double</span><span class="o">)</span> <span class="n">v</span><span class="o">);</span>
                <span class="o">}</span>
            <span class="o">}</span>
            <span class="k">return</span> <span class="n">bundle</span><span class="o">;</span>

        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="s">"ConfigProvider"</span><span class="o">,</span> <span class="s">"getCurrentConfig failed"</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Cursor</span> <span class="nf">query</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Uri</span> <span class="n">uri</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">projection</span><span class="o">,</span> <span class="nc">String</span> <span class="n">selection</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">selectionArgs</span><span class="o">,</span> <span class="nc">String</span> <span class="n">sortOrder</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">String</span> <span class="nf">getType</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Uri</span> <span class="n">uri</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="nc">Uri</span> <span class="nf">insert</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Uri</span> <span class="n">uri</span><span class="o">,</span> <span class="nc">ContentValues</span> <span class="n">values</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">delete</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Uri</span> <span class="n">uri</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">as</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="mi">0</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">int</span> <span class="nf">update</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Uri</span> <span class="n">uri</span><span class="o">,</span> <span class="nc">ContentValues</span> <span class="n">v</span><span class="o">,</span> <span class="nc">String</span> <span class="n">s</span><span class="o">,</span> <span class="nc">String</span><span class="o">[]</span> <span class="n">as</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="mi">0</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>然后在 AndroidManifest.xml 中注册该 ContentProvider：</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nt">&lt;provider</span>
        <span class="na">android:name=</span><span class="s">".content_provider.ConfigProvider"</span>
        <span class="na">android:authorities=</span><span class="s">"com.example.deviceinfo.configprovider"</span>
        <span class="na">android:exported=</span><span class="s">"true"</span>
        <span class="na">android:grantUriPermissions=</span><span class="s">"true"</span> <span class="nt">/&gt;</span>
</code></pre></div></div>

<p>在 MainHook.java 中通过 ContentResolver 访问该 ContentProvider 获取配置数据：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">PACKAGE_NAME</span> <span class="o">=</span> <span class="s">"com.example.deviceinfo"</span><span class="o">;</span>

    <span class="kd">private</span> <span class="nc">Bundle</span> <span class="nf">getDataFromProvider</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">String</span> <span class="n">className</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Uri</span> <span class="n">uri</span> <span class="o">=</span> <span class="nc">Uri</span><span class="o">.</span><span class="na">parse</span><span class="o">(</span><span class="s">"content://"</span> <span class="o">+</span> <span class="no">PACKAGE_NAME</span> <span class="o">+</span> <span class="s">".configprovider"</span><span class="o">);</span>
        <span class="nc">Bundle</span> <span class="n">extras</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Bundle</span><span class="o">();</span>
        <span class="n">extras</span><span class="o">.</span><span class="na">putString</span><span class="o">(</span><span class="nc">ProviderConstants</span><span class="o">.</span><span class="na">EXTRA_CLASS_NAME</span><span class="o">,</span> <span class="n">className</span><span class="o">);</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="k">return</span> <span class="n">context</span><span class="o">.</span><span class="na">getContentResolver</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">call</span><span class="o">(</span><span class="n">uri</span><span class="o">,</span> <span class="nc">ProviderConstants</span><span class="o">.</span><span class="na">METHOD_GET_CURRENT_CONFIG</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">extras</span><span class="o">);</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Throwable</span> <span class="n">t</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">XposedBridge</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Error calling content provider: "</span> <span class="o">+</span> <span class="n">t</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
            <span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>所需的 Context 对象可以通过 hook Application.attach() 方法获取。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span>
                <span class="nc">Application</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="s">"attach"</span><span class="o">,</span>
                <span class="nc">Context</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="k">new</span> <span class="nf">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">afterHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Context</span><span class="o">)</span> <span class="n">param</span><span class="o">.</span><span class="na">args</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
                        <span class="nc">Context</span> <span class="n">providerContext</span><span class="o">;</span>
                        <span class="k">try</span> <span class="o">{</span>
                            <span class="n">providerContext</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">createPackageContext</span><span class="o">(</span>
                                    <span class="s">"com.example.deviceinfo"</span><span class="o">,</span>
                                    <span class="nc">Context</span><span class="o">.</span><span class="na">CONTEXT_IGNORE_SECURITY</span> <span class="o">|</span> <span class="nc">Context</span><span class="o">.</span><span class="na">CONTEXT_INCLUDE_CODE</span>
                            <span class="o">);</span>
                        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                            <span class="nc">XposedBridge</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Failed to create provider context: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
                            <span class="k">return</span><span class="o">;</span>
                        <span class="o">}</span>
                        <span class="nc">XposedBridge</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Context acquired: "</span> <span class="o">+</span> <span class="n">providerContext</span><span class="o">);</span>
                    <span class="o">}</span>
                <span class="o">}</span>
        <span class="o">);</span>
</code></pre></div></div>

<h3 id="622-注入-wifi-和定位信息">6.2.2. 注入 Wifi 和定位信息</h3>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleLoadPackage</span><span class="o">(</span><span class="kd">final</span> <span class="n">XC_LoadPackage</span><span class="o">.</span><span class="na">LoadPackageParam</span> <span class="n">lpparam</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">lpparam</span><span class="o">.</span><span class="na">packageName</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="no">PACKAGE_NAME</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span>
                <span class="nc">Application</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="s">"attach"</span><span class="o">,</span>
                <span class="nc">Context</span><span class="o">.</span><span class="na">class</span><span class="o">,</span>
                <span class="k">new</span> <span class="nf">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">afterHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="nc">Context</span> <span class="n">context</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Context</span><span class="o">)</span> <span class="n">param</span><span class="o">.</span><span class="na">args</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
                        <span class="nc">Context</span> <span class="n">providerContext</span><span class="o">;</span>
                        <span class="k">try</span> <span class="o">{</span>
                            <span class="n">providerContext</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">createPackageContext</span><span class="o">(</span>
                                    <span class="s">"com.example.deviceinfo"</span><span class="o">,</span>
                                    <span class="nc">Context</span><span class="o">.</span><span class="na">CONTEXT_IGNORE_SECURITY</span> <span class="o">|</span> <span class="nc">Context</span><span class="o">.</span><span class="na">CONTEXT_INCLUDE_CODE</span>
                            <span class="o">);</span>
                        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                            <span class="nc">XposedBridge</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Failed to create provider context: "</span> <span class="o">+</span> <span class="n">e</span><span class="o">.</span><span class="na">getMessage</span><span class="o">());</span>
                            <span class="k">return</span><span class="o">;</span>
                        <span class="o">}</span>
                        <span class="nc">XposedBridge</span><span class="o">.</span><span class="na">log</span><span class="o">(</span><span class="s">"Context acquired: "</span> <span class="o">+</span> <span class="n">providerContext</span><span class="o">);</span>

                        <span class="n">hookNetwork</span><span class="o">(</span><span class="n">lpparam</span><span class="o">,</span> <span class="n">providerContext</span><span class="o">);</span>
                        <span class="n">hookLocation</span><span class="o">(</span><span class="n">lpparam</span><span class="o">,</span> <span class="n">providerContext</span><span class="o">);</span>
                    <span class="o">}</span>
                <span class="o">}</span>
        <span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookNetwork</span><span class="o">(</span><span class="n">XC_LoadPackage</span><span class="o">.</span><span class="na">LoadPackageParam</span> <span class="n">lpparam</span><span class="o">,</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Bundle</span> <span class="n">bundle</span> <span class="o">=</span> <span class="n">getDataFromProvider</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"WifiData"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">bundle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="c1">// 1. 网络类型</span>
        <span class="kt">int</span> <span class="n">networkType</span> <span class="o">=</span> <span class="n">bundle</span><span class="o">.</span><span class="na">getInt</span><span class="o">(</span><span class="s">"networkType"</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span>
        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span><span class="s">"android.net.NetworkInfo"</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">.</span><span class="na">classLoader</span><span class="o">,</span>
                <span class="s">"getType"</span><span class="o">,</span> <span class="k">new</span> <span class="n">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">beforeHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
                        <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">==</span> <span class="nc">ConnectivityManager</span><span class="o">.</span><span class="na">TYPE_MOBILE</span> <span class="o">||</span> <span class="n">networkType</span> <span class="o">==</span> <span class="nc">ConnectivityManager</span><span class="o">.</span><span class="na">TYPE_WIFI</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="n">networkType</span><span class="o">);</span>
                        <span class="o">}</span>
                    <span class="o">}</span>
                <span class="o">});</span>
        <span class="c1">// TODO: NetworkInfo.getTypeName, NetworkInfo.getSubType</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span><span class="s">"android.net.ConnectivityManager"</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">.</span><span class="na">classLoader</span><span class="o">,</span>
                    <span class="s">"getNetworkCapabilities"</span><span class="o">,</span> <span class="k">new</span> <span class="n">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                        <span class="nd">@Override</span>
                        <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">beforeHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span>
                        <span class="o">}</span>
                    <span class="o">});</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span>
                <span class="s">"android.net.NetworkCapabilities"</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">.</span><span class="na">classLoader</span><span class="o">,</span>
                <span class="s">"hasTransport"</span><span class="o">,</span> <span class="kt">int</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="k">new</span> <span class="n">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">beforeHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="kt">int</span> <span class="n">transportType</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">param</span><span class="o">.</span><span class="na">args</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span>
                        <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">==</span> <span class="nc">ConnectivityManager</span><span class="o">.</span><span class="na">TYPE_WIFI</span> <span class="o">&amp;&amp;</span> <span class="n">transportType</span> <span class="o">==</span> <span class="nc">NetworkCapabilities</span><span class="o">.</span><span class="na">TRANSPORT_WIFI</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
                        <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">==</span> <span class="nc">ConnectivityManager</span><span class="o">.</span><span class="na">TYPE_MOBILE</span> <span class="o">&amp;&amp;</span> <span class="n">transportType</span> <span class="o">==</span> <span class="nc">NetworkCapabilities</span><span class="o">.</span><span class="na">TRANSPORT_CELLULAR</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
                        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span>
                        <span class="o">}</span>
                    <span class="o">}</span>
                <span class="o">});</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">networkType</span> <span class="o">!=</span> <span class="nc">ConnectivityManager</span><span class="o">.</span><span class="na">TYPE_WIFI</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span><span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">.</span><span class="na">classLoader</span><span class="o">,</span>
                <span class="s">"getSSID"</span><span class="o">,</span> <span class="k">new</span> <span class="n">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">afterHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="nc">String</span> <span class="n">ssid</span> <span class="o">=</span> <span class="n">bundle</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="s">"ssid"</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
                        <span class="k">if</span> <span class="o">(</span><span class="n">ssid</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="s">"\""</span> <span class="o">+</span> <span class="n">ssid</span> <span class="o">+</span> <span class="s">"\""</span><span class="o">);</span>
                        <span class="o">}</span>
                    <span class="o">}</span>
                <span class="o">});</span>

        <span class="n">hookMethodOfString</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="s">"getBSSID"</span><span class="o">,</span> <span class="s">"bssid"</span><span class="o">);</span>

        <span class="n">hookMethodOfInt</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="s">"getFrequency"</span><span class="o">,</span> <span class="s">"frequency"</span><span class="o">);</span>

        <span class="n">hookMethodOfInt</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="s">"getRssi"</span><span class="o">,</span> <span class="s">"rssi"</span><span class="o">);</span>

        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">S</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">hookMethodOfInt</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="s">"getCurrentSecurityType"</span><span class="o">,</span> <span class="s">"securityType"</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">R</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">hookMethodOfInt</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.net.wifi.WifiInfo"</span><span class="o">,</span> <span class="s">"getWifiStandard"</span><span class="o">,</span> <span class="s">"wifiStandard"</span><span class="o">);</span>
        <span class="o">}</span>

        <span class="c1">// TODO: Hook ScanResult</span>

    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookLocation</span><span class="o">(</span><span class="n">XC_LoadPackage</span><span class="o">.</span><span class="na">LoadPackageParam</span> <span class="n">lpparam</span><span class="o">,</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Bundle</span> <span class="n">bundle</span> <span class="o">=</span> <span class="n">getDataFromProvider</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="s">"LocationData"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">bundle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>

        <span class="n">hookMethodOfDouble</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getLatitude"</span><span class="o">,</span> <span class="s">"latitude"</span><span class="o">);</span>
        <span class="n">hookMethodOfDouble</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getLongitude"</span><span class="o">,</span> <span class="s">"longitude"</span><span class="o">);</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getAccuracy"</span><span class="o">,</span> <span class="s">"horizontalAccuracy"</span><span class="o">);</span>
        <span class="n">hookMethodOfDouble</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getAltitude"</span><span class="o">,</span> <span class="s">"altitude"</span><span class="o">);</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getVerticalAccuracyMeters"</span><span class="o">,</span> <span class="s">"verticalAccuracy"</span><span class="o">);</span>
        <span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="nc">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">UPSIDE_DOWN_CAKE</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">hookMethodOfDouble</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getMslAltitudeMeters"</span><span class="o">,</span> <span class="s">"mslAltitude"</span><span class="o">);</span>
            <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getMslAltitudeAccuracyMeters"</span><span class="o">,</span> <span class="s">"mslAltitudeAccuracy"</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getSpeed"</span><span class="o">,</span> <span class="s">"speed"</span><span class="o">);</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getSpeedAccuracyMetersPerSecond"</span><span class="o">,</span> <span class="s">"speedAccuracy"</span><span class="o">);</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getBearing"</span><span class="o">,</span> <span class="s">"bearing"</span><span class="o">);</span>
        <span class="n">hookMethodOfFloat</span><span class="o">(</span><span class="n">bundle</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">,</span> <span class="s">"android.location.Location"</span><span class="o">,</span> <span class="s">"getBearingAccuracyDegrees"</span><span class="o">,</span> <span class="s">"bearingAccuracy"</span><span class="o">);</span>

        <span class="c1">// Hook 其他方法...</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookMethodOfString</span><span class="o">(</span><span class="nc">Bundle</span> <span class="n">bundle</span><span class="o">,</span> <span class="n">XC_LoadPackage</span><span class="o">.</span><span class="na">LoadPackageParam</span> <span class="n">lpparam</span><span class="o">,</span> <span class="nc">String</span> <span class="n">className</span><span class="o">,</span> <span class="nc">String</span> <span class="n">methodName</span><span class="o">,</span> <span class="nc">String</span> <span class="n">key</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findAndHookMethod</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="n">lpparam</span><span class="o">.</span><span class="na">classLoader</span><span class="o">,</span>
                <span class="n">methodName</span><span class="o">,</span> <span class="k">new</span> <span class="n">XC_MethodHook</span><span class="o">()</span> <span class="o">{</span>
                    <span class="nd">@Override</span>
                    <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">afterHookedMethod</span><span class="o">(</span><span class="nc">MethodHookParam</span> <span class="n">param</span><span class="o">)</span> <span class="o">{</span>
                        <span class="nc">String</span> <span class="n">val</span> <span class="o">=</span> <span class="n">bundle</span><span class="o">.</span><span class="na">getString</span><span class="o">(</span><span class="n">key</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
                        <span class="k">if</span> <span class="o">(</span><span class="n">val</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
                            <span class="n">param</span><span class="o">.</span><span class="na">setResult</span><span class="o">(</span><span class="n">val</span><span class="o">);</span>
                        <span class="o">}</span>
                    <span class="o">}</span>
                <span class="o">});</span>
    <span class="o">}</span>

    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookMethodOfInt</span><span class="o">(...)</span> <span class="o">{</span> <span class="cm">/* 类似上面的 hookMethodOfString */</span> <span class="o">}</span>
    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookMethodOfDouble</span><span class="o">(...)</span> <span class="o">{</span> <span class="cm">/* 类似上面的 hookMethodOfString */</span> <span class="o">}</span>
    <span class="kd">private</span> <span class="kt">void</span> <span class="nf">hookMethodOfFloat</span><span class="o">(...)</span> <span class="o">{</span> <span class="cm">/* 类似上面的 hookMethodOfString */</span> <span class="o">}</span>
</code></pre></div></div>

<p>目前 Hook 的内容比较草率，Wifi 方面只是简单地 Hook 了 WifiInfo 的几个方法，不过足够应付某些 app 比如飞书的 wifi 打卡，比如老版本 QQ 8.2.11 无法在移动网络下使用的问题。定位方面我一开始以为只要 Hook Location 的 get 方法就行了，后来发现实际上很多 app 都不会从 Location 对象里获取数据，而是用一些第三方 SDK 来获取位置，这些目前我都没有处理，后续可以继续完善。</p>

<h3 id="623-检测-xposed-模块是否激活">6.2.3. 检测 Xposed 模块是否激活</h3>
<p>我看到网上有方法以及 AI 也会推荐下面的代码来检测 Xposed 模块是否激活：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isModuleActive</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">getBooleanField</span><span class="o">(</span>
                <span class="nc">XposedHelpers</span><span class="o">.</span><span class="na">findClass</span><span class="o">(</span><span class="s">"de.robv.android.xposed.XposedBridge"</span><span class="o">,</span> <span class="kc">null</span><span class="o">),</span>
                <span class="s">"isXposedModuleActive"</span>
        <span class="o">);</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>但是我实际测试时发现并没有效果，原理大概是因为 Xposed 进程位于不同的类加载器中，导致无法访问到该字段。</p>

<p>我在上面测试 New XSharedPreference 时发现当模块未激活时，MODE_WORLD_READABLE 会报错，而激活后则不会报错。所以我打算用这种方式来检测模块是否激活：</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">isModuleActive</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">try</span> <span class="o">{</span>
            <span class="n">context</span><span class="o">.</span><span class="na">getSharedPreferences</span><span class="o">(</span><span class="s">"test_prefs"</span><span class="o">,</span> <span class="nc">Context</span><span class="o">.</span><span class="na">MODE_WORLD_READABLE</span><span class="o">);</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">SecurityException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>
</code></pre></div></div>
<p>这种方式有时会失效，不知道是否和初始化有关，目前没有找到原因。</p>

<p>我还尝试了其他方式，比如通过 ContentProvider + findClass 的方式来检测模块是否激活，但是仍然没有成功。</p>]]></content><author><name>Long, Xiaoming</name><email>lxmghct@gmail.com</email></author><category term="project-logs" /><category term="platforms" /><category term="android" /><category term="xposed" /><summary type="html"><![CDATA[1. 需求描述 本来是想开发一个用于伪造 wifi 和定位信息的用于打卡的 Xposed 模块。后面想逐步完善成一个伪造设备信息的模块，所以命名为 DeviceInfo。目前已实现功能：Wifi 信息伪造（已完成）、定位信息伪造（进行中）、其他尚未实现。]]></summary></entry></feed>