Repository navigation
bridge: 复制 server config 时输出单行 JSON(Android 端粘贴报「非法 JSON」) - #3
Open
std-external wants to merge 1 commit into
Open
std-external wants to merge 1 commit into
std-external wants to merge 1 commit into
Conversation
…ients The accounts page copied JSON.stringify(config, null, 2), so the clipboard held a pretty-printed document. On phones that value is pasted into the client's single-line import path and rejected as invalid JSON; the compact document imports fine. Crossgram's own client parsers already tolerate whitespace (crossgram-android ServerSwitchConfig.Server.parseUserInput, telegram-x CrossgramServerConfiguration.parse and the Dart/TDLib equivalents all skip whitespace), so no parser change is needed: the failure is in the text transport, and a compact document carries neither line breaks nor indentation whitespace to be mangled or truncated. The page still renders the readable, indented document; only the clipboard text changes. Also covers the hidden-textarea fallback copy that phones use when navigator.clipboard is unavailable over plain http.
std-external
force-pushed
the
fix/server-config-single-line
branch
from
October 9, 2026 16:11
5dec6f6 to
46dd0a1
Compare
std-external
pushed a commit
to std-external/crossgram
that referenced
this pull request
Oct 9, 2026
…r any endpoint Account list management: - Every account card gets a select box and a `Delete account` button; selected entries can be deleted together. Deletion always goes through a confirmation dialog that lists the exact entries, how many Telegram clients are signed in through them (they are signed out), and refuses entries the configuration file does not own. - `findDuplicateAccounts()` reports entries that describe the same platform account: the adapter resolved both to the same platform user (platformKind + userId), or the entry lost its deterministic virtual phone to another entry (`VirtualPhoneClaimedError`). The entry clients are already signed in through keeps the account, with the stable id order as a tie-breaker. The page offers `Select duplicates`, which only selects those entries; nothing is deleted without the confirmation above. - `deleteAccounts()` validates every target first, then stops the platform subscription, removes the platform session, virtual phone, TOTP secret, two-step password, auth bindings and client authorizations, revokes the matching auth keys, and finally deletes the Cordis plugin entry through the tree that owns it, so app.yml is written back and the account cannot return on the next start. Message and conversation history is kept: it is scoped by platform session and stays reusable if the entry id is configured again. - `VirtualPhoneClaimedError` replaces the anonymous phone-collision error so the dashboard can name the entry that already serves the account. Copying the server configuration: - The dashboard publishes `serverEndpoints`: the configured main endpoint first, then every `altEndpoints` entry in configuration order, deduplicated. - An `Endpoint` picker next to `Copy server configuration` (only shown when a backup endpoint exists, main selected by default) rewrites host/port and the `dcs` entries of the document. The clipboard form stays the single-line JSON from std-microblock#3, and the mobile fallback path without the Clipboard API is covered by the same end-to-end test. Tests: unit tests for duplicate grouping, account removal against SQLite, entry removal against the real Cordis loader (qualified entry keys, app.yml write-back) and the endpoint document rewrite; the browser end-to-end test now covers the endpoint picker with and without the Clipboard API, the duplicate banner, the unmanaged and signed-in confirmations, and deleting the selected duplicate.
Author
这个 PR 还需要吗?#4 已经独立基于
我这边没有强倾向,按你的判断走就行。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
账号列表页(Platform accounts)的 Copy server configuration 按钮把
JSON.stringify(serverConfig, null, 2)直接写进剪贴板,复制出来是 26 行、带缩进的 pretty JSON。在 Android 端把它粘贴进客户端,会被判为非法 JSON;把换行全部删掉变成一行后就能正常导入。
报告来源:QQ 群「Koishi On Topic」的群管理员。复现步骤:打开
/platform-accounts→点「Copy server configuration」→ 粘贴到 Android 客户端。
根因:解析方在我们自己的客户端,但解析器本身不挑换行
先确认了各环节谁在解析:
makeCrossGramServerConfig(packages/bridge/src/account-dashboard.ts)、WebUI 复制、deploy/generate-client-config.mjsServerSwitchConfig.Server.parseUserInput→new JSONObject(value)(Androidorg.json)CrossgramServerConfiguration.parse→new JSONObject(json)CrossgramServerConfiguration.parse→jsonDecodeCrossgramServerConfig::parse→json_decode(Parser::skip_whitespaces()是skip_till_not(" \t\r\n"))也就是说:服务端只是产出文本,解析全在 Crossgram 自己的客户端里,本来不存在「第三方客户端我们改不了」的问题。
但实测表明这些解析器本来就吃换行:把
crossgram-android里那份真实的ServerSwitchConfig.java(未改动)配 Android libcore 的
org.json源码编译起来,直接调Server.parseUserInput:复现脚本与全部源码桩在
reports/crossgram-62-android-parse-probe/(公钥用占位符)。结论分两层:
所以不需要(也不应该)为它改解析器,改了是空转。
缩进空格被换成 U+00A0/NBSP 这类非 JSON 空白)。报告人「回车全部删掉就正常了」正说明坏点在换行/空白,
而不在字段内容。
因此修在产出侧:剪贴板只写紧凑单行 JSON——它既没有换行、也没有缩进空白,任何「按行读 / 单行输入框 /
空白归一化」的搬运都不会再破坏它,而字段名、字段顺序、值(含 PEM 的
\n转义)与可读版本逐字等价。页面上的展示(
<details><pre>)仍保持格式化排版,没有改展示层。改动
packages/bridge/client/bridge-model.ts:新增两个纯函数serializeServerConfig(config)→JSON.stringify(config):剪贴板用,单行formatServerConfig(config)→JSON.stringify(config, null, 2):页面展示用,行为不变packages/bridge/client/accounts.tsx:复制按钮改用serializeServerConfig();<pre>展示改用formatServerConfig(),排版未动packages/bridge/client/bridge-model.test.ts:3 个新用例(单行、无首尾空白、round-trip 深相等、PEM 转义、展示仍是多行、undefined 处理)
packages/cordis-webui-solidjs/src/bridge.browser.e2e.test.ts:手机视口下真实剪贴板断言,并覆盖手机实际走的那条回退路径(明文 http 下没有
navigator.clipboard,走隐藏 textarea +execCommand('copy')),确认两条路径写出的都是同一份单行文本复制文本对照(host/公钥均为占位符)
改动前(594 字符、26 行):
{ "name": "CrossGram", "enable_special_config": false, "host": "203.0.113.10", "port": 4430, "rsa_key": "-----BEGIN RSA PUBLIC KEY-----\n<PLACEHOLDER_PUBLIC_KEY>\n-----END RSA PUBLIC KEY-----", "dcs": [ { "id": 1, "ip": "203.0.113.10", "port": 4430 }, { "id": 2, "ip": "203.0.113.10", "port": 4430 }, { "id": 3, "ip": "203.0.113.10", "port": 4430 }, { "id": 4, "ip": "203.0.113.10", "port": 4430 }, { "id": 5, "ip": "203.0.113.10", "port": 4430 } ] }(上面 dcs 为了可读性写成一行;实际是 5 个各占 5 行的对象,共 26 行。)
改动后(396 字符、0 换行):
{"name":"CrossGram","enable_special_config":false,"host":"203.0.113.10","port":4430,"rsa_key":"-----BEGIN RSA PUBLIC KEY-----\n<PLACEHOLDER_PUBLIC_KEY>\n-----END RSA PUBLIC KEY-----","dcs":[{"id":1,"ip":"203.0.113.10","port":4430},{"id":2,"ip":"203.0.113.10","port":4430},{"id":3,"ip":"203.0.113.10","port":4430},{"id":4,"ip":"203.0.113.10","port":4430},{"id":5,"ip":"203.0.113.10","port":4430}]}验证
e2e(390×844 手机视口、真实剪贴板)断言:剪贴板文本不含
\r\n\u2028\u2029、无首尾空白、JSON.parse后与serverConfig深相等(fixture 的rsa_key改成多行 PEM 以覆盖转义)、回退复制路径写出的文本与主路径逐字相同,并且页面
<pre>仍含换行、与剪贴板内容解析结果等价。回归验证:把按钮改回
copyText(configuration()!)并重新 build 后,该 e2e 失败并打印出多行的旧文本 ——说明断言确实卡住这个 bug。
yarn typecheck:webui:solid通过。全量yarn test:webui:solid:79 passed / 1 failed,失败的是packages/cordis-webui-solidjs/src/loader.browser.e2e.test.ts:131(loader 页横向溢出),已在未修改的main上复现同样失败,与本次改动无关。未纳入本次改动
deploy/generate-client-config.mjs生成的是文件(Android/Desktop 走文件导入),保留格式化 JSON。packages/mtproto-debug/client/page.tsx:412复制的是调试事件 payload,与客户端导入无关。packages/platform-admin-bot/src/index.ts:311)仍是 pretty JSON 消息,从 Telegram 里长按复制会走同一条搬运路径。要给它在保留可读展示的同时加「单行复制」入口,需要给
IMInlineKeyboardButton(packages/bridge/src/platform.ts:440,目前只有url/callback)新增copy_text按钮类型并改 platform-crossgram 渲染,属于跨包 feature,可以另开 PR。出了问题;只凭「多行失败」去改解析器会改错地方。