Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Double-click `ModAVD.exe` in the package root: it starts a local server that lis

## Updating ModAVD

`modavd update` updates the whole ModAVD package in the root folder (the emulator, components and scripts, and also `ModAVD.exe` and `tools\modavd.exe` themselves). It does the same thing as AUTO-MAS's "ModAVD one-click update" (`魔改 AVD 一键更新`), and uses the same `<root>\.update\` folder and state file: if an update is interrupted partway on either side, the other can finish it or roll it back. Under the current integration agreement, AUTO-MAS keeps its own updater for now; whether and when it switches to calling this command is up to the AUTO-MAS project's own maintenance, and this is not a feature or promise already delivered by this repository.
`modavd update` updates the whole ModAVD package in the root folder (the emulator, components and scripts, and also `ModAVD.exe` and `tools\modavd.exe` themselves). It does the same thing as AUTO-MAS's ModAVD one-click update, and uses the same `<root>\.update\` folder and state file: if an update is interrupted partway on either side, the other can finish it or roll it back. Under the current integration agreement, AUTO-MAS keeps its own updater for now; whether and when it switches to calling this command is up to the AUTO-MAS project's own maintenance, and this is not a feature or promise already delivered by this repository.

- It does only the step you explicitly ask for; it never goes online automatically and never downloads in the background:
- `modavd update` (`status`) looks at local state only; `check` looks up the latest version;
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<!-- README.md 和 README.en.md 逐节对应:改了其中一份,请在同一个 PR 里把另一份也改了。 -->

[ModAVD(魔改 AVD)](https://github.com/ModAVD/ModAVD) 的管理程序:图形管理器 `ModAVD.exe`,以及命令行和 MCP 服务 `modavd.exe`。Go 写,只用标准库。目前只支持 Windows x64,平台相关的部分已经拆成平台层,其他平台的安排见 [ModAVD#27](https://github.com/ModAVD/ModAVD/issues/27)。
[ModAVD](https://github.com/ModAVD/ModAVD) 的管理程序:图形管理器 `ModAVD.exe`,以及命令行和 MCP 服务 `modavd.exe`。Go 写,只用标准库。目前只支持 Windows x64,平台相关的部分已经拆成平台层,其他平台的安排见 [ModAVD#27](https://github.com/ModAVD/ModAVD/issues/27)。

> **版本提示(`main` 与 Release)**:本文按仓库 `main` 的实现编写。当前 `main` 的版本是 `0.3.0`,最新独立 Release 是 `v0.1.0`(对应 ModAVD `0.36.0`),`v0.2.0` 已起草、还没发布;`0.2.0` 起的新建后台初始化、`update` 命令和退出码 9 都不在 `v0.1.0` 下载包里。使用已发布包时,请以对应 Release 的说明和包内版本为准。

Expand Down Expand Up @@ -263,7 +263,7 @@ claude mcp add --transport stdio --scope user modavd '--' D:\ModAVD\tools\modavd

## 更新 ModAVD

`modavd update` 更新根目录里的整个 ModAVD 包(模拟器、组件、脚本,也包括 `ModAVD.exe` 和 `tools\modavd.exe` 自己),和 AUTO-MAS 的「魔改 AVD 一键更新」做的是同一件事,用的也是同一个 `<根>\.update\` 目录和状态文件:哪边更新到一半被打断,另一边都能接着补完或退回。按当前对接约定,AUTO-MAS 目前保留自己的更新器;是否以及何时改成调用这个命令,由 AUTO-MAS 项目自己的维护决定,这不是本仓库已经交付的功能或承诺。
`modavd update` 更新根目录里的整个 ModAVD 包(模拟器、组件、脚本,也包括 `ModAVD.exe` 和 `tools\modavd.exe` 自己),和 AUTO-MAS 里的 ModAVD 一键更新做的是同一件事,用的也是同一个 `<根>\.update\` 目录和状态文件:哪边更新到一半被打断,另一边都能接着补完或退回。按当前对接约定,AUTO-MAS 目前保留自己的更新器;是否以及何时改成调用这个命令,由 AUTO-MAS 项目自己的维护决定,这不是本仓库已经交付的功能或承诺。

- 只做明确要求的那一步,不自动联网、不在后台下载:
- `modavd update`(`status`)只看本地;`check` 查最新版本;
Expand Down
4 changes: 2 additions & 2 deletions cmd/modavd-gui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func run(args []string) (code int) {

fs := flag.NewFlagSet("ModAVD", flag.ContinueOnError)
fs.SetOutput(io.Discard)
rootFlag := fs.String("root", "", "魔改 AVD 根目录")
rootFlag := fs.String("root", "", "ModAVD 根目录")
port := fs.Int("port", 0, "监听端口(0 = 随机)")
noWindow := fs.Bool("no-window", false, "不打开窗口")
dev := fs.Bool("dev", false, "开发模式:提供 /dev/ 的模拟后端页面,没有窗口也不自动退出")
Expand All @@ -134,7 +134,7 @@ func run(args []string) (code int) {
}
r, err := root.Resolve(*rootFlag)
if err != nil {
fatalErr("找不到魔改 AVD 根目录", err)
fatalErr("找不到 ModAVD 根目录", err)
return errs.ExitCode(err)
}

Expand Down
6 changes: 3 additions & 3 deletions internal/adb/adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ var (

func (c *Client) portConflict(k serverKind, port int, pid uint32, name string) *errs.Error {
return errs.Newf(errs.PortConflict,
"魔改 AVD 的%s adb 端口 %d 被 %s(PID %d)占用,"+serverConflictReason+"。请在 %s 里把 %s 改成一个没被占用的端口"+
"ModAVD 的%s adb 端口 %d 被 %s(PID %d)占用,"+serverConflictReason+"。请在 %s 里把 %s 改成一个没被占用的端口"+
"(不能用 5037 和各实例的控制台 / adb / gRPC 端口,也不能和 %s 相同),或者先关掉占用它的程序",
k.label, port, name, pid, c.Root.MetadataPath(), k.key, k.otherKey)
}
Expand Down Expand Up @@ -325,15 +325,15 @@ func (c *Client) ensure(ctx context.Context, k serverKind, port int, timeout tim
return false, err
}
prepareServerLog(c.Root)
c.Log.Infof("启动魔改 AVD %s adb server(端口 %d)", k.label, port)
c.Log.Infof("启动 ModAVD %s adb server(端口 %d)", k.label, port)
// StartOrphan rather than StartDetached: an MCP client ends modavd's whole process tree when it disconnects, and
// while the start-server client (our child for ~2 s) is alive the server it forks is part of that tree. Losing
// the server is not harmless: the emulator registers with it only once, at boot. Without a configured orphan
// helper (unit tests) StartOrphan is StartDetached. Output goes nowhere, like AUTO-MAS (stdout=DEVNULL).
client, err := host.StartOrphan(c.Root.AdbExe(), []string{"-P", strconv.Itoa(port), "start-server"},
host.StartOptions{Env: c.Env()})
if err != nil {
return false, errs.Wrap(errs.Internal, err, fmt.Sprintf("启动魔改 AVD %s adb server(端口 %d)失败: %s",
return false, errs.Wrap(errs.Internal, err, fmt.Sprintf("启动 ModAVD %s adb server(端口 %d)失败: %s",
k.label, port, errs.As(err).Message))
}
up, err = c.waitListening(ctx, k, port, timeout, client)
Expand Down
4 changes: 2 additions & 2 deletions internal/adb/adb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,14 +351,14 @@ func TestPortConflictMessage(t *testing.T) {
dir := tempDir(t)
c := New(&root.Root{Dir: dir}, nil)
e := c.portConflict(privateServer, 20050, 1234, "foo.exe")
want := "魔改 AVD 的私有 adb 端口 20050 被 foo.exe(PID 1234)占用,它不是 adb。请在 " + c.Root.MetadataPath() +
want := "ModAVD 的私有 adb 端口 20050 被 foo.exe(PID 1234)占用,它不是 adb。请在 " + c.Root.MetadataPath() +
" 里把 adbServerPort 改成一个没被占用的端口(不能用 5037 和各实例的控制台 / adb / gRPC 端口,也不能和 " +
"scriptAdbServerPort 相同),或者先关掉占用它的程序"
if e.Code != errs.PortConflict || e.Error() != want {
t.Errorf("private conflict:\n got %s %q\nwant %q", e.Code, e.Error(), want)
}
e = c.portConflict(scriptServer, 20049, 1, "x.exe")
if !strings.HasPrefix(e.Message, "魔改 AVD 的脚本专用 adb 端口 20049 被 x.exe(PID 1)占用") ||
if !strings.HasPrefix(e.Message, "ModAVD 的脚本专用 adb 端口 20049 被 x.exe(PID 1)占用") ||
!strings.Contains(e.Message, "里把 scriptAdbServerPort 改成") || !strings.Contains(e.Message, "也不能和 adbServerPort 相同") {
t.Errorf("script conflict: %q", e.Message)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/adb/real_adb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func TestRealAdb(t *testing.T) {
t.Fatalf("EnsureServer = %v, %v", ok, err)
}
t.Logf("EnsureServer took %v", time.Since(start))
if n := logs.count(fmt.Sprintf("启动魔改 AVD 私有 adb server(端口 %d)", testServerPort)); n != 1 {
if n := logs.count(fmt.Sprintf("启动 ModAVD 私有 adb server(端口 %d)", testServerPort)); n != 1 {
t.Errorf("start log lines = %d", n)
}
pid, up, err := win.ListenerPID(testServerPort)
Expand All @@ -205,7 +205,7 @@ func TestRealAdb(t *testing.T) {
if err != nil || !ok {
t.Errorf("second EnsureServer = %v, %v", ok, err)
}
if n := logs.count("启动魔改 AVD 私有 adb server"); n != 1 {
if n := logs.count("启动 ModAVD 私有 adb server"); n != 1 {
t.Errorf("second EnsureServer started again (%d log lines)", n)
}
// PrepareForClose with the server up asks it first; it does not know emulator-20080, and no qemu of this
Expand Down Expand Up @@ -277,7 +277,7 @@ func TestRealAdb(t *testing.T) {
if err != nil || !ok {
t.Fatalf("EnsureScriptServer = %v, %v", ok, err)
}
if n := logs.count(fmt.Sprintf("启动魔改 AVD 脚本专用 adb server(端口 %d)", testScriptPort)); n != 1 {
if n := logs.count(fmt.Sprintf("启动 ModAVD 脚本专用 adb server(端口 %d)", testScriptPort)); n != 1 {
t.Errorf("script start log lines = %d", n)
}
pid, _, _ := win.ListenerPID(testScriptPort)
Expand Down
4 changes: 2 additions & 2 deletions internal/adb/serverlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func (c *Client) reportGoneServer(k serverKind, port int) {
if found && (s.created == 0 || p.Created == s.created) {
what = "还在,但不再监听"
}
c.Log.Warnf("魔改 AVD %s adb server(端口 %d,pid %d,%s 起来的)%s%s", k.label, port, s.pid,
c.Log.Warnf("ModAVD %s adb server(端口 %d,pid %d,%s 起来的)%s%s", k.label, port, s.pid,
s.at.Format("15:04:05"), what, logTailText(ServerLogTail(c.Root)))
}

Expand All @@ -159,7 +159,7 @@ func (c *Client) reportServerLog(k serverKind, port int, timeout time.Duration)
if same {
return
}
c.Log.Warnf("魔改 AVD %s adb server(端口 %d)%.0f 秒内没监听上%s", k.label, port, timeout.Seconds(),
c.Log.Warnf("ModAVD %s adb server(端口 %d)%.0f 秒内没监听上%s", k.label, port, timeout.Seconds(),
logTailText(lines))
}

Expand Down
12 changes: 6 additions & 6 deletions internal/avd/avd.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func Get(r *root.Root, i int) (*Instance, error) {
return nil, notFound(i)
}

func notFound(i int) error { return errs.Newf(errs.NotFound, "魔改 AVD 实例 %d 不存在", i) }
func notFound(i int) error { return errs.Newf(errs.NotFound, "ModAVD 实例 %d 不存在", i) }

// lockInstance takes root.LockInstanceConfig of i. noDir: the avd directory does not exist (no lock taken, nothing on
// disk to guard); err is nil then.
Expand Down Expand Up @@ -509,10 +509,10 @@ func ValidateOptions(memoryMB, cpu, dataGB *int) (int, int, int, error) {
data = *dataGB
}
if !slices.Contains(MemoryChoicesMB, memory) {
return 0, 0, 0, errs.New(errs.InvalidArgument, "魔改 AVD 的内存只能选 "+joinInts(MemoryChoicesMB, "%d GB", 1024))
return 0, 0, 0, errs.New(errs.InvalidArgument, "ModAVD 的内存只能选 "+joinInts(MemoryChoicesMB, "%d GB", 1024))
}
if !slices.Contains(CPUChoices, cores) {
return 0, 0, 0, errs.New(errs.InvalidArgument, "魔改 AVD 的 CPU 核数只能选 "+joinInts(CPUChoices, "%d", 1))
return 0, 0, 0, errs.New(errs.InvalidArgument, "ModAVD 的 CPU 核数只能选 "+joinInts(CPUChoices, "%d", 1))
}
if data < MinDataPartitionGB || data > MaxDataPartitionGB {
return 0, 0, 0, errs.Newf(errs.InvalidArgument, "数据盘上限只能在 %d–%d GB 之间", MinDataPartitionGB,
Expand All @@ -528,7 +528,7 @@ func ValidateFPS(fps *int) (int, error) {
value = *fps
}
if !slices.Contains(FPSChoices, value) {
return 0, errs.New(errs.InvalidArgument, "魔改 AVD 的帧率只能选 "+joinInts(FPSChoices, "%d", 1))
return 0, errs.New(errs.InvalidArgument, "ModAVD 的帧率只能选 "+joinInts(FPSChoices, "%d", 1))
}
return value, nil
}
Expand Down Expand Up @@ -714,7 +714,7 @@ func pathExists(path string) bool {
}

// FreeIndex is the smallest valid index that has no instance and whose three ports in plan busy() reports free.
// errs.Busy "魔改 AVD 实例已满(端口段 20000–20100 最多 9 台)" (the root's plan) when there is none. A nil busy skips
// errs.Busy "ModAVD 实例已满(端口段 20000–20100 最多 9 台)" (the root's plan) when there is none. A nil busy skips
// the port check.
func FreeIndex(plan root.PortPlan, existing map[int]bool, busy func(ports []int) []int) (int, error) {
for i := 0; i <= root.MaxIndex; i++ {
Expand All @@ -727,7 +727,7 @@ func FreeIndex(plan root.PortPlan, existing map[int]bool, busy func(ports []int)
}
return i, nil
}
return 0, errs.Newf(errs.Busy, "魔改 AVD 实例已满(端口段 %d–%d 最多 9 台)", plan.Base, plan.Base+100)
return 0, errs.Newf(errs.Busy, "ModAVD 实例已满(端口段 %d–%d 最多 9 台)", plan.Base, plan.Base+100)
}

type UpdateOptions struct {
Expand Down
14 changes: 7 additions & 7 deletions internal/avd/avd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ func TestListAndGet(t *testing.T) {
os.Remove(r.AvdHome())
_, err := Get(r, 0)
wantCode(t, err, errs.NotFound)
if err.Error() != "魔改 AVD 实例 0 不存在" {
if err.Error() != "ModAVD 实例 0 不存在" {
t.Fatalf("message: %v", err)
}
for _, i := range []int{4, 0, 9} {
Expand Down Expand Up @@ -727,9 +727,9 @@ func TestValidate(t *testing.T) {
memory, cpu, data *int
msg string
}{
{ip(1000), nil, nil, "魔改 AVD 的内存只能选 3 GB / 4 GB / 5 GB / 6 GB"},
{ip(0), ip(3), ip(1), "魔改 AVD 的内存只能选 3 GB / 4 GB / 5 GB / 6 GB"},
{nil, ip(3), ip(1), "魔改 AVD 的 CPU 核数只能选 2 / 4 / 6"},
{ip(1000), nil, nil, "ModAVD 的内存只能选 3 GB / 4 GB / 5 GB / 6 GB"},
{ip(0), ip(3), ip(1), "ModAVD 的内存只能选 3 GB / 4 GB / 5 GB / 6 GB"},
{nil, ip(3), ip(1), "ModAVD 的 CPU 核数只能选 2 / 4 / 6"},
{nil, nil, ip(15), "数据盘上限只能在 16–512 GB 之间"},
{nil, nil, ip(513), "数据盘上限只能在 16–512 GB 之间"},
} {
Expand All @@ -753,7 +753,7 @@ func TestValidate(t *testing.T) {
}
_, err = ValidateFPS(ip(0))
wantCode(t, err, errs.InvalidArgument)
if err.Error() != "魔改 AVD 的帧率只能选 30 / 60" {
if err.Error() != "ModAVD 的帧率只能选 30 / 60" {
t.Errorf("fps message %q", err.Error())
}
}
Expand Down Expand Up @@ -805,7 +805,7 @@ func TestFreeIndex(t *testing.T) {
}
_, err = FreeIndex(def, all, func(ports []int) []int { return ports })
wantCode(t, err, errs.Busy)
if err.Error() != "魔改 AVD 实例已满(端口段 20000–20100 最多 9 台)" {
if err.Error() != "ModAVD 实例已满(端口段 20000–20100 最多 9 台)" {
t.Fatalf("message %v", err)
}
// another plan: the ports asked about and the message are the plan's
Expand All @@ -815,7 +815,7 @@ func TestFreeIndex(t *testing.T) {
t.Fatalf("plan 21000: FreeIndex = %d %v, asked %v", i, err, asked)
}
_, err = FreeIndex(root.PortPlan{Base: 21000}, all, func(ports []int) []int { return ports })
if err == nil || err.Error() != "魔改 AVD 实例已满(端口段 21000–21100 最多 9 台)" {
if err == nil || err.Error() != "ModAVD 实例已满(端口段 21000–21100 最多 9 台)" {
t.Fatalf("plan 21000: message %v", err)
}
all[7] = true
Expand Down
8 changes: 4 additions & 4 deletions internal/avd/testdata/instances_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,12 @@ def validate_options(memory_mb, cpu, data_partition_gb):
)
if memory not in MEMORY_CHOICES_MB:
raise ValueError(
"魔改 AVD 的内存只能选 "
"ModAVD 的内存只能选 "
+ " / ".join(f"{value // 1024} GB" for value in MEMORY_CHOICES_MB)
)
if cores not in CPU_CHOICES:
raise ValueError(
"魔改 AVD 的 CPU 核数只能选 " + " / ".join(str(v) for v in CPU_CHOICES)
"ModAVD 的 CPU 核数只能选 " + " / ".join(str(v) for v in CPU_CHOICES)
)
low, high = DATA_PARTITION_RANGE_GB
if not low <= data_gb <= high:
Expand All @@ -345,7 +345,7 @@ def validate_fps(fps):
value = DEFAULT_FPS if fps is None else int(fps)
if value not in FPS_CHOICES:
raise ValueError(
"魔改 AVD 的帧率只能选 " + " / ".join(str(v) for v in FPS_CHOICES)
"ModAVD 的帧率只能选 " + " / ".join(str(v) for v in FPS_CHOICES)
)
return value

Expand Down Expand Up @@ -432,7 +432,7 @@ def write_instance_settings(root, idx, changes):
"""AvdManager.write_instance_settings for cpu / memoryMb / fps (no display keys, no expected values)."""
instance = list_instances(root).get(str(idx))
if instance is None:
raise RuntimeError(f"魔改 AVD 实例 {idx} 不存在")
raise RuntimeError(f"ModAVD 实例 {idx} 不存在")
fps = validate_fps(changes["fps"]) if "fps" in changes else None
memory, cpu, _ = validate_options(
changes.get("memoryMb", memory_for(instance_meta(root, idx))),
Expand Down
10 changes: 5 additions & 5 deletions internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func init() {
"status 列出这五个文件在不在、从哪来(download / import),不把它算进必需组件。" +
"不带参数时默认查看组件;source 查看或修改记住的下载源(auto、googledownloads_cn、tencent、google)。" +
"--json 时必须给 --accept-license(仅 fetch)。只有 Windows 有组件清单,darwin、linux 返回 unsupported。" +
"退出码:没同意许可协议是 2(license_declined),路径有中文、空间不够、不是魔改 AVD 的文件夹是 4," +
"退出码:没同意许可协议是 2(license_declined),路径有中文、空间不够、不是 ModAVD 的文件夹是 4," +
"正在补全是 5,下不成是 9(components_unavailable),下载或解压后没通过校验、导入的文件被拒收也是 9(components_rejected," +
"--json 的 error.details.reason 是 unknown_file、mismatch 或 incomplete)。"
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
Expand Down Expand Up @@ -236,7 +236,7 @@ func init() {
{
name: "collect-info", args: "[--out-dir 目录] [--stage-dir 目录] [--keep-stage] [--no-explorer]",
summary: "收集问题信息,打成 zip",
details: "打成「魔改AVD问题信息-<时间>.zip」,默认放桌面,再打开资源管理器选中它(--no-explorer 不打开)。" +
details: "打成「ModAVD-问题信息-<时间>.zip」,默认放桌面,再打开资源管理器选中它(--no-explorer 不打开)。" +
"--stage-dir 是暂存目录的父目录(默认 %TEMP%),--keep-stage 保留暂存目录。每一节单独收,出错写进 summary.txt 接着收下一节。" +
"只读:不收磁盘镜像,不收 AUTO-MAS 的配置和账号数据,不起 adb server、不碰 5037。目前只支持 Windows。",
flags: []flagSpec{optOutDir, optStageDir, optKeepStage, optNoExplorer}, final: true,
Expand Down Expand Up @@ -805,7 +805,7 @@ func stopAll(c *cmdCtx, m manager, opt emu.StopOptions) (*StopAllResult, error)
c.log.Infof("没有正在运行的实例")
return res, nil
}
c.log.Infof("同时正常关闭 %d 台魔改 AVD 实例(安卓正常关机):%s", len(targets), joinInts(targets, "、"))
c.log.Infof("同时正常关闭 %d 台 ModAVD 实例(安卓正常关机):%s", len(targets), joinInts(targets, "、"))
type outcome struct {
r *emu.StopResult
err error
Expand All @@ -830,7 +830,7 @@ func stopAll(c *cmdCtx, m manager, opt emu.StopOptions) (*StopAllResult, error)
if c.ctx.Err() != nil && errs.CodeOf(e) != errs.Canceled {
e = errs.Wrap(errs.Canceled, e, "已取消")
}
c.log.Warnf("魔改 AVD 实例 %d 关机失败: %s", targets[k], errs.As(e).Error())
c.log.Warnf("ModAVD 实例 %d 关机失败: %s", targets[k], errs.As(e).Error())
res.Failed = append(res.Failed, StopFailure{Index: targets[k], Error: errorInfo(e)})
continue
}
Expand Down Expand Up @@ -933,7 +933,7 @@ func humanCollectInfo(w io.Writer, data any) {
fmt.Fprintln(w, l)
}
if !r.MasFound {
fmt.Fprintln(w, "没找到 AUTO-MAS 安装目录,反馈时请附上 AUTO-MAS 的 debug 文件夹里的日志")
fmt.Fprintln(w, report.MasMissingNote)
}
if len(r.Problems) > 0 {
fmt.Fprintf(w, "有 %d 节没收集成功,原因写在 zip 里的 summary.txt,照样提交就行。\n", len(r.Problems))
Expand Down
Loading
Loading