magic-agent 2.4.3 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLIENT-CONTRACT.md +467 -41
- package/README.md +15 -14
- package/client-ui/pc/app.css +74 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +40 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +247 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +72 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +217 -0
- package/client-ui/shared/conv/conv.css +329 -174
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +373 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +5554 -639
- package/client-ui-mobile/reqs.css +110 -1
- package/client-ui-mobile/reqs.js +533 -74
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +67 -0
- package/client-ui-mobile/scripts.js +599 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +69 -10
- package/cmd/magic-client/main.go +210 -32
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +157 -7
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +166 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +119 -7
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +168 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +15 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +157 -11
- package/internal/cli/session.go +75 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +297 -33
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +518 -24
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +207 -2
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +288 -0
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +290 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +31 -0
- package/internal/client/scripts_decode_test.go +130 -0
- package/internal/client/scripts_decode_windows.go +182 -0
- package/internal/client/scripts_test.go +739 -0
- package/internal/client/service.go +717 -97
- package/internal/client/service_test.go +217 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +175 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +6 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +14 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
// scripts_decode_implausible.go - 「解码结果像不像乱码」的判定,**全平台共用一份**。
|
|
4
|
+
//
|
|
5
|
+
// 只保留 !windows 时会怎样:scripts_decode_windows.go 里调用了它却没定义,
|
|
6
|
+
// Windows 目标直接编译不过(`undefined: hasImplausibleRune`)——
|
|
7
|
+
// 2026-10-26 打 2.6.1 时 win32-x64 就是这个错误挂掉的(另外 4 个目标照常出包,
|
|
8
|
+
// 很容易被「完成 4/5」这句糊过去)。
|
|
9
|
+
//
|
|
10
|
+
// 非 Windows 的解码路径本身不走它(UTF-8 解不出就原样保留字节,见
|
|
11
|
+
// scripts_decode_other.go),但 scripts_decode_test.go 不分平台编译、要引用它,
|
|
12
|
+
// 所以两个平台都得有这个符号。判定逻辑只有这一份,不要再往平台文件里抄第二份。
|
|
13
|
+
|
|
14
|
+
import "unicode/utf8"
|
|
15
|
+
|
|
16
|
+
// hasImplausibleRune 报告文本里有没有「控制台输出里不会出现」的字符。
|
|
17
|
+
//
|
|
18
|
+
// 这些字符是「字节编码猜错」的典型产物:GBK 字节被当 UTF-8 解会冒出
|
|
19
|
+
// 西里尔文(һ)、希伯来文、中文全角标点残留等。
|
|
20
|
+
func hasImplausibleRune(s string) bool {
|
|
21
|
+
for _, r := range s {
|
|
22
|
+
switch {
|
|
23
|
+
case r == utf8.RuneError: // 替换符:解码失败过
|
|
24
|
+
return true
|
|
25
|
+
case r < 0x20 && r != '\t' && r != '\n' && r != '\r':
|
|
26
|
+
return true // 控制符
|
|
27
|
+
case r >= 0x80 && r <= 0x9F:
|
|
28
|
+
return true // C1 控制区
|
|
29
|
+
case r >= 0xE000 && r <= 0xF8FF:
|
|
30
|
+
return true // 私用区
|
|
31
|
+
case r >= 0x0370 && r <= 0x04FF:
|
|
32
|
+
return true // 希腊文 + 西里尔文
|
|
33
|
+
case r >= 0x0590 && r <= 0x08FF:
|
|
34
|
+
return true // 希伯来文 / 阿拉伯文 / 叙利亚文 / 塔安文
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package client
|
|
4
|
+
|
|
5
|
+
// scripts_decode_other.go - 非 Windows 平台脚本输出的解码。
|
|
6
|
+
//
|
|
7
|
+
// unix 上没有「控制台代码页」这回事:脚本往管道写的就是**原始字节**,
|
|
8
|
+
// 绝大多数工具写 UTF-8,少量老工具写 Latin-1。按 UTF-8 解是唯一合理口径;
|
|
9
|
+
// 解不出就按字节保留(不猜别的编码 —— 猜错比乱码更难查)。
|
|
10
|
+
|
|
11
|
+
import "unicode/utf8"
|
|
12
|
+
|
|
13
|
+
// decodeScriptBytes 把脚本输出的一行字节解成文本。
|
|
14
|
+
func decodeScriptBytes(b []byte) string {
|
|
15
|
+
if len(b) == 0 {
|
|
16
|
+
return ""
|
|
17
|
+
}
|
|
18
|
+
if utf8.Valid(b) {
|
|
19
|
+
return string(b)
|
|
20
|
+
}
|
|
21
|
+
return string(b)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// encodeACP 非 Windows 平台不需要按代码页编码(unix 上就是原始字节)。
|
|
25
|
+
func encodeACP(s string, cp uint32) ([]byte, bool) {
|
|
26
|
+
return []byte(s), true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// scriptCodePage 非 Windows 平台没有「控制台代码页」:UTF-8(65001)就是它。
|
|
30
|
+
// 单独存在的理由是 scripts_decode_test.go 会引用它(测试文件不分平台编译)。
|
|
31
|
+
func scriptCodePage() uint32 { return 65001 }
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"runtime"
|
|
5
|
+
"testing"
|
|
6
|
+
"unicode/utf8"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
/* 脚本输出的解码(Windows 上是**控制台代码页**,不是 UTF-8 —— 见
|
|
10
|
+
* scripts_decode_windows.go 的说明)。
|
|
11
|
+
*
|
|
12
|
+
* 这一组测试钉住三件事:
|
|
13
|
+
* ① UTF-8 输出原样通过(node / python / go 写的 stdout);
|
|
14
|
+
* ② 代码页输出能正确还原(中文 Windows 上 .cmd/.bat 的常态);
|
|
15
|
+
* ③ **GBK 字节碰巧是合法 UTF-8** 时不能误判(这是纯 utf8.Valid 判不出的
|
|
16
|
+
* 情况 —— 「第一行」的 GBK 字节解成 UTF-8 是「һһࠀ」,看着像乱码但
|
|
17
|
+
* utf8.Valid 返回 true)。 */
|
|
18
|
+
|
|
19
|
+
func TestDecodeScriptBytesKeepsUTF8(t *testing.T) {
|
|
20
|
+
in := "hello 世界 · 正常输出"
|
|
21
|
+
if got := decodeScriptBytes([]byte(in)); got != in {
|
|
22
|
+
t.Fatalf("UTF-8 输出被改动了:%q != %q", got, in)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func TestDecodeScriptBytesRoundTripCodePage(t *testing.T) {
|
|
27
|
+
for _, in := range []string{"第一行", "中文", "hello", "混合 mixed 内容 123"} {
|
|
28
|
+
in := in
|
|
29
|
+
t.Run(in, func(t *testing.T) {
|
|
30
|
+
// 「代码页」只存在于 Windows;unix 上 encodeACP 就是原始字节,往返没有意义。
|
|
31
|
+
// 按用例跳过而不是整函数跳过:非 Windows 编译时才算这个函数(否则 vet 报未定义)。
|
|
32
|
+
if runtime.GOOS != "windows" {
|
|
33
|
+
t.Skip("只有 Windows 才有控制台代码页这回事")
|
|
34
|
+
}
|
|
35
|
+
enc, ok := encodeACP(in, scriptCodePage())
|
|
36
|
+
if !ok {
|
|
37
|
+
t.Fatalf("按代码页编码 %q 失败", in)
|
|
38
|
+
}
|
|
39
|
+
got := decodeScriptBytes(enc)
|
|
40
|
+
if got != in {
|
|
41
|
+
t.Fatalf("代码页往返不对:%q != %q(字节 % x)", got, in, enc)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// TestDecodeScriptBytesDetectsGBKAsUTF8 钉住那个「判别不出来」的坑:
|
|
48
|
+
//
|
|
49
|
+
// 单个汉字的 GBK 字节**常常**是合法 UTF-8 ——
|
|
50
|
+
// 「一」的 GBK 字节是 d2 bb,按 UTF-8 解是西里尔文「һ」(U+04BB)。
|
|
51
|
+
// utf8.Valid 返回 true,所以**只靠 utf8.Valid 判不出编码**,
|
|
52
|
+
// 必须同时看「解出来的文本里有没有不像话字符」。
|
|
53
|
+
//
|
|
54
|
+
// (实测:本机代码页 936 下,「一」「专」「业」「丫」等都是这种。)
|
|
55
|
+
func TestDecodeScriptBytesDetectsGBKAsUTF8(t *testing.T) {
|
|
56
|
+
if runtime.GOOS != "windows" {
|
|
57
|
+
t.Skip("只有 Windows 才有控制台代码页这回事")
|
|
58
|
+
}
|
|
59
|
+
// 「一」在这个代码页下的 GBK 字节恰好是合法 UTF-8(前提)。
|
|
60
|
+
enc, ok := encodeACP("一", scriptCodePage())
|
|
61
|
+
if !ok {
|
|
62
|
+
t.Fatal("编码失败")
|
|
63
|
+
}
|
|
64
|
+
if !utf8.Valid(enc) {
|
|
65
|
+
t.Skip("当前系统的代码页下这个字节恰好不是合法 UTF-8,本用例的前提不成立")
|
|
66
|
+
}
|
|
67
|
+
// 但按 UTF-8 解出来的文本里应该有「不像话字符」——
|
|
68
|
+
// 这正是解码器据以下判断的依据。
|
|
69
|
+
if !hasImplausibleRune(string(enc)) {
|
|
70
|
+
t.Skip("这串字节按 UTF-8 解出来没有异常字符,本用例的前提不成立")
|
|
71
|
+
}
|
|
72
|
+
// 解码器必须绕过 UTF-8 那条路,落到代码页上。
|
|
73
|
+
if got := decodeScriptBytes(enc); got != "一" {
|
|
74
|
+
t.Fatalf("解码结果 = %q,期望 %q", got, "一")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// TestDecodeScriptBytesCodePageLine 钉住整行的情形:
|
|
79
|
+
// 中文 Windows 上 .cmd 的一整行输出,多半是「ASCII + 若干双字节字符」,
|
|
80
|
+
// 整串通常不是合法 UTF-8 —— 但**不能只靠这一条**(上一条用例证明了
|
|
81
|
+
// 单个汉字就能是合法的)。两件事都要成立才算对。
|
|
82
|
+
func TestDecodeScriptBytesCodePageLine(t *testing.T) {
|
|
83
|
+
if runtime.GOOS != "windows" {
|
|
84
|
+
t.Skip("只有 Windows 才有控制台代码页这回事")
|
|
85
|
+
}
|
|
86
|
+
for _, in := range []string{
|
|
87
|
+
"第一行",
|
|
88
|
+
"INFO 启动完成",
|
|
89
|
+
"总计 3 项",
|
|
90
|
+
"路径 D:\\work\\x",
|
|
91
|
+
} {
|
|
92
|
+
enc, ok := encodeACP(in, scriptCodePage())
|
|
93
|
+
if !ok {
|
|
94
|
+
t.Fatalf("编码 %q 失败", in)
|
|
95
|
+
}
|
|
96
|
+
if got := decodeScriptBytes(enc); got != in {
|
|
97
|
+
t.Fatalf("解码结果 = %q,期望 %q", got, in)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestHasImplausibleRune(t *testing.T) {
|
|
103
|
+
// 正常输出不该被判成乱码。
|
|
104
|
+
for _, s := range []string{
|
|
105
|
+
"hello world",
|
|
106
|
+
"第一行",
|
|
107
|
+
"路径 D:\\workspace\\magic-agent",
|
|
108
|
+
"2026-10-24 12:00:00 INFO 启动完成",
|
|
109
|
+
"总计 3 项,耗时 1.2s",
|
|
110
|
+
} {
|
|
111
|
+
if hasImplausibleRune(s) {
|
|
112
|
+
t.Errorf("正常文本被误判成乱码:%q", s)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 这些是「编码猜错」的典型产物,必须被判出来。
|
|
116
|
+
for _, s := range []string{
|
|
117
|
+
"\uFFFD", // 替换符
|
|
118
|
+
"\u04BB", // 西里尔文(GBK 字节被当 UTF-8 解的产物)
|
|
119
|
+
"\u0400", // 西里尔文
|
|
120
|
+
"\u05D0", // 希伯来文
|
|
121
|
+
"\u0600", // 阿拉伯文
|
|
122
|
+
"\uE000", // 私用区
|
|
123
|
+
"\u0007", // 控制符(BEL)
|
|
124
|
+
"\u0085", // C1 控制区
|
|
125
|
+
} {
|
|
126
|
+
if !hasImplausibleRune(s) {
|
|
127
|
+
t.Errorf("乱码字符没被识别:%q", s)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package client
|
|
4
|
+
|
|
5
|
+
// scripts_decode_windows.go - Windows 上脚本输出的**字节 → 文本**解码。
|
|
6
|
+
//
|
|
7
|
+
// ## 为什么需要这一层(实测,2026-10-24)
|
|
8
|
+
//
|
|
9
|
+
// Windows 的控制台有**代码页**(codepage)概念,中文系统默认 936(GBK)。
|
|
10
|
+
// cmd.exe / PowerShell 往管道写文本时,编码的是**代码页**,不是 UTF-8:
|
|
11
|
+
//
|
|
12
|
+
// GBK 编码的 .cmd 里 echo 中文 → 管道里是 d6 d0 ce c4(4 字节)
|
|
13
|
+
// UTF-8 编码的 .cmd 里 echo 中文 → 管道里是 e4 b8 ad e6 96 87(6 字节)
|
|
14
|
+
//
|
|
15
|
+
// 服务端若一律按 UTF-8 解,日志区就是一串乱码 —— 用户看到一堆看不懂的字符,
|
|
16
|
+
// 还以为脚本坏了。
|
|
17
|
+
//
|
|
18
|
+
// ## 为什么不能一律按代码页解
|
|
19
|
+
//
|
|
20
|
+
// 现在越来越多的工具(node / python / go)**直接写 UTF-8 字节**到 stdout,
|
|
21
|
+
// 不经过控制台编码。那种脚本按 GBK 解就反过来会坏(经典 mojibake:
|
|
22
|
+
// 「中文」解成「涓」)。
|
|
23
|
+
//
|
|
24
|
+
// ## 判据:UTF-8 自校验 + 「不像话字符」排查
|
|
25
|
+
//
|
|
26
|
+
// UTF-8 是自校验的,合法字节序列解出来的文本一定是可信的 —— **但反过来不成立**:
|
|
27
|
+
// GBK 字节常常**碰巧**是合法 UTF-8。实测「第一行」的 GBK 字节
|
|
28
|
+
// (b5 da d2 bb d0 d0)解成 UTF-8 是「һһࠀ」—— 看着像乱码,但 utf8.Valid
|
|
29
|
+
// 返回 true。**只靠 utf8.Valid 判不出编码**,这是本文件的核心难点。
|
|
30
|
+
//
|
|
31
|
+
// 所以判据是两条一起看:
|
|
32
|
+
//
|
|
33
|
+
// 1. 字节是合法 UTF-8;**且**
|
|
34
|
+
// 2. 解出来的文本里**没有「不像话字符」**(见 implausibleRune)。
|
|
35
|
+
//
|
|
36
|
+
// 「不像话字符」= 控制符 / 替换符 / 私用区 / 希腊文 / 西里尔文 / 希伯来文 /
|
|
37
|
+
// 阿拉伯文 —— 这些字符**不会出现在控制台输出里**,但**恰好**是 GBK 字节被
|
|
38
|
+
// 当 UTF-8 解时的典型产物(上面那个「һ」就是西里尔文 U+04BB)。
|
|
39
|
+
//
|
|
40
|
+
// 两条都满足 → 按 UTF-8;否则 → 按控制台代码页解。
|
|
41
|
+
//
|
|
42
|
+
// ## 诚实边界
|
|
43
|
+
//
|
|
44
|
+
// 这仍是**启发式**,不是万能检测:
|
|
45
|
+
//
|
|
46
|
+
// - 脚本输出真的含希腊文/西里尔文时,会被误判成代码页(少见,可接受)。
|
|
47
|
+
// - 同一行里 UTF-8 与 GBK 混排(几乎不可能出现)不处理。
|
|
48
|
+
//
|
|
49
|
+
// 但它覆盖了真实场景的两个大头(中文 Windows 上的 .cmd/.bat/.ps1 走代码页;
|
|
50
|
+
// node/python 工具走 UTF-8),而且**不会崩、不会丢内容** —— 解不出来时保留
|
|
51
|
+
// 原始字节,好过乱码或空串。
|
|
52
|
+
|
|
53
|
+
import (
|
|
54
|
+
"errors"
|
|
55
|
+
"syscall"
|
|
56
|
+
"unicode/utf8"
|
|
57
|
+
"unsafe"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
var (
|
|
61
|
+
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
62
|
+
procGetACP = kernel32.NewProc("GetACP")
|
|
63
|
+
procMultiByteToWideChar = kernel32.NewProc("MultiByteToWideChar")
|
|
64
|
+
procWideCharToMultiByte = kernel32.NewProc("WideCharToMultiByte")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// scriptCodePage 当前系统的 ANSI 代码页(936=GBK / 1252=西欧 / 65001=UTF-8)。
|
|
68
|
+
func scriptCodePage() uint32 {
|
|
69
|
+
r, _, _ := procGetACP.Call()
|
|
70
|
+
cp := uint32(r)
|
|
71
|
+
if cp == 0 {
|
|
72
|
+
cp = 1252 // 兜底:拿不到就用西欧,宁可显示成问号也别崩
|
|
73
|
+
}
|
|
74
|
+
return cp
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// decodeScriptBytes 把脚本输出的一行字节解成文本。
|
|
78
|
+
func decodeScriptBytes(b []byte) string {
|
|
79
|
+
if len(b) == 0 {
|
|
80
|
+
return ""
|
|
81
|
+
}
|
|
82
|
+
// 先试 UTF-8:合法 + 解出来不像乱码 → 直接用。
|
|
83
|
+
if utf8.Valid(b) {
|
|
84
|
+
if s := string(b); !hasImplausibleRune(s) {
|
|
85
|
+
return s
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// 再试控制台代码页。
|
|
89
|
+
if s, ok := decodeACP(b, scriptCodePage()); ok && !hasImplausibleRune(s) {
|
|
90
|
+
return s
|
|
91
|
+
}
|
|
92
|
+
// 都不理想 → 保留原始字节(宁可显示成乱码,也不要静默丢内容)。
|
|
93
|
+
return string(b)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// errnoIsZero 判断 syscall 调用的错误码是否为 0(成功)。
|
|
97
|
+
//
|
|
98
|
+
// ⚠️ **不要直接比 err != nil**:Proc.Call 返回的 error **成功时也是非 nil**
|
|
99
|
+
// (它是 syscall.Errno(0) 装箱出来的接口值,接口本身不为 nil)。
|
|
100
|
+
// 直接比 err != nil 会把每次成功都判成失败 —— 这是本文件第一个版本的实际 bug。
|
|
101
|
+
//
|
|
102
|
+
// 正确口径:把 error 拆成 syscall.Errno 再比值。
|
|
103
|
+
func errnoIsZero(err error) bool {
|
|
104
|
+
if err == nil {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
var errno syscall.Errno
|
|
108
|
+
if errors.As(err, &errno) {
|
|
109
|
+
return errno == 0
|
|
110
|
+
}
|
|
111
|
+
// 不是 Errno(例如某些平台包装过)→ 保守当作失败。
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// decodeACP 用指定代码页解码(MultiByteToWideChar)。
|
|
116
|
+
func decodeACP(b []byte, cp uint32) (string, bool) {
|
|
117
|
+
if len(b) == 0 {
|
|
118
|
+
return "", true
|
|
119
|
+
}
|
|
120
|
+
// 第一次调用问长度(cbMultiByte = 0)。
|
|
121
|
+
n, _, errno := procMultiByteToWideChar.Call(
|
|
122
|
+
uintptr(cp), 0,
|
|
123
|
+
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)),
|
|
124
|
+
0, 0,
|
|
125
|
+
)
|
|
126
|
+
/* ⚠️ errno 是 syscall.Errno,**成功时也是非 nil 的接口值**(值是 0)——
|
|
127
|
+
必须比 errno != 0,不能比 err != nil,否则每次都判成失败。 */
|
|
128
|
+
if n == 0 || !errnoIsZero(errno) {
|
|
129
|
+
return "", false
|
|
130
|
+
}
|
|
131
|
+
wide := make([]uint16, n)
|
|
132
|
+
n2, _, errno := procMultiByteToWideChar.Call(
|
|
133
|
+
uintptr(cp), 0,
|
|
134
|
+
uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)),
|
|
135
|
+
uintptr(unsafe.Pointer(&wide[0])), uintptr(len(wide)),
|
|
136
|
+
)
|
|
137
|
+
if n2 == 0 || !errnoIsZero(errno) {
|
|
138
|
+
return "", false
|
|
139
|
+
}
|
|
140
|
+
return syscall.UTF16ToString(wide[:n2]), true
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* encodeACP 用指定代码页编码(WideCharToMultiByte)。
|
|
144
|
+
*
|
|
145
|
+
* 给测试桩用:Windows 上 cmd.exe 按代码页解释脚本文件里的字节,
|
|
146
|
+
* 测试桩必须按同样的编码存,否则「echo 中文」会变成乱码命令。
|
|
147
|
+
*/
|
|
148
|
+
func encodeACP(s string, cp uint32) ([]byte, bool) {
|
|
149
|
+
if s == "" {
|
|
150
|
+
return nil, true
|
|
151
|
+
}
|
|
152
|
+
wide, err := syscall.UTF16FromString(s)
|
|
153
|
+
if err != nil {
|
|
154
|
+
return nil, false
|
|
155
|
+
}
|
|
156
|
+
// UTF16FromString 末尾带一个 NUL,编码时不要它。
|
|
157
|
+
if len(wide) > 0 && wide[len(wide)-1] == 0 {
|
|
158
|
+
wide = wide[:len(wide)-1]
|
|
159
|
+
}
|
|
160
|
+
if len(wide) == 0 {
|
|
161
|
+
return nil, true
|
|
162
|
+
}
|
|
163
|
+
n, _, errno := procWideCharToMultiByte.Call(
|
|
164
|
+
uintptr(cp), 0,
|
|
165
|
+
uintptr(unsafe.Pointer(&wide[0])), uintptr(len(wide)),
|
|
166
|
+
0, 0, 0, 0,
|
|
167
|
+
)
|
|
168
|
+
if n == 0 || !errnoIsZero(errno) {
|
|
169
|
+
return nil, false
|
|
170
|
+
}
|
|
171
|
+
out := make([]byte, n)
|
|
172
|
+
n2, _, errno := procWideCharToMultiByte.Call(
|
|
173
|
+
uintptr(cp), 0,
|
|
174
|
+
uintptr(unsafe.Pointer(&wide[0])), uintptr(len(wide)),
|
|
175
|
+
uintptr(unsafe.Pointer(&out[0])), uintptr(len(out)),
|
|
176
|
+
0, 0,
|
|
177
|
+
)
|
|
178
|
+
if n2 == 0 || !errnoIsZero(errno) {
|
|
179
|
+
return nil, false
|
|
180
|
+
}
|
|
181
|
+
return out[:n2], true
|
|
182
|
+
}
|