magic-agent 1.0.0
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/LICENSE +21 -0
- package/README.md +392 -0
- package/cmd/magic-agent/main.go +12 -0
- package/cmd/magic-client/main.go +144 -0
- package/go.mod +11 -0
- package/go.sum +12 -0
- package/internal/agent/arkclaw.go +587 -0
- package/internal/agent/arkclaw_stream.go +312 -0
- package/internal/agent/arkclaw_stream_test.go +428 -0
- package/internal/agent/arkclaw_test.go +827 -0
- package/internal/agent/ask.go +694 -0
- package/internal/agent/ask_test.go +587 -0
- package/internal/agent/attachments.go +200 -0
- package/internal/agent/attachments_test.go +481 -0
- package/internal/agent/capability.go +154 -0
- package/internal/agent/capability_test.go +190 -0
- package/internal/agent/claude.go +406 -0
- package/internal/agent/codebuddy.go +1044 -0
- package/internal/agent/codebuddy_account_test.go +128 -0
- package/internal/agent/codebuddy_gateway.go +479 -0
- package/internal/agent/codebuddy_gateway_stream.go +358 -0
- package/internal/agent/codebuddy_gateway_test.go +673 -0
- package/internal/agent/codex.go +599 -0
- package/internal/agent/codex_home.go +206 -0
- package/internal/agent/codex_home_test.go +130 -0
- package/internal/agent/codex_stream_test.go +97 -0
- package/internal/agent/codex_test.go +280 -0
- package/internal/agent/custom_models.go +197 -0
- package/internal/agent/custom_models_test.go +283 -0
- package/internal/agent/dsh.go +754 -0
- package/internal/agent/dsh_sdk.go +679 -0
- package/internal/agent/dsh_sdk_test.go +726 -0
- package/internal/agent/dsh_test.go +759 -0
- package/internal/agent/engine.go +469 -0
- package/internal/agent/engine_base.go +497 -0
- package/internal/agent/engine_base_test.go +308 -0
- package/internal/agent/engine_test.go +858 -0
- package/internal/agent/env.go +88 -0
- package/internal/agent/env_test.go +106 -0
- package/internal/agent/jsonextract.go +128 -0
- package/internal/agent/jsonextract_test.go +110 -0
- package/internal/agent/keepalive_test.go +216 -0
- package/internal/agent/llmengine.go +470 -0
- package/internal/agent/llmengine_test.go +551 -0
- package/internal/agent/llmhttp.go +437 -0
- package/internal/agent/llmhttp_test.go +304 -0
- package/internal/agent/llmmodels.go +205 -0
- package/internal/agent/login.go +27 -0
- package/internal/agent/main_test.go +23 -0
- package/internal/agent/models.go +686 -0
- package/internal/agent/models_test.go +877 -0
- package/internal/agent/openclaw.go +543 -0
- package/internal/agent/openclaw_acp.go +949 -0
- package/internal/agent/openclaw_acp_test.go +406 -0
- package/internal/agent/openclaw_test.go +451 -0
- package/internal/agent/output.go +191 -0
- package/internal/agent/permission.go +362 -0
- package/internal/agent/permission_test.go +526 -0
- package/internal/agent/prompt.go +65 -0
- package/internal/agent/runcmd.go +259 -0
- package/internal/agent/runcmd_test.go +81 -0
- package/internal/agent/runcmd_unix.go +56 -0
- package/internal/agent/runcmd_windows.go +66 -0
- package/internal/agent/runner.go +180 -0
- package/internal/agent/runner_test.go +263 -0
- package/internal/agent/spawnhook_test.go +96 -0
- package/internal/agent/stream.go +882 -0
- package/internal/agent/stream_test.go +565 -0
- package/internal/agent/streamjson.go +185 -0
- package/internal/agent/tags.go +52 -0
- package/internal/agent/toolkind.go +96 -0
- package/internal/agent/toolkind_test.go +70 -0
- package/internal/agent/tools_suffix_test.go +261 -0
- package/internal/agent/trae.go +342 -0
- package/internal/agent/workspace_test.go +166 -0
- package/internal/boundary/boundary_test.go +165 -0
- package/internal/cli/ask.go +1113 -0
- package/internal/cli/ask_test.go +1088 -0
- package/internal/cli/attach_test.go +239 -0
- package/internal/cli/contract_test.go +157 -0
- package/internal/cli/control.go +149 -0
- package/internal/cli/control_test.go +192 -0
- package/internal/cli/engines_test.go +205 -0
- package/internal/cli/events.go +198 -0
- package/internal/cli/events_test.go +166 -0
- package/internal/cli/keepalive_test.go +391 -0
- package/internal/cli/login.go +78 -0
- package/internal/cli/permission_test.go +295 -0
- package/internal/cli/prompt_source_test.go +222 -0
- package/internal/cli/root.go +752 -0
- package/internal/cli/session.go +486 -0
- package/internal/cli/session_test.go +202 -0
- package/internal/cli/sessionlog.go +325 -0
- package/internal/cli/sessionlog_command_test.go +204 -0
- package/internal/cli/sessionlog_test.go +262 -0
- package/internal/cli/sessionlog_user_test.go +129 -0
- package/internal/cli/toolkind_wire_test.go +84 -0
- package/internal/cli/workspace_test.go +188 -0
- package/internal/client/cors_test.go +136 -0
- package/internal/client/lan.go +68 -0
- package/internal/client/lan_test.go +53 -0
- package/internal/client/pair.go +163 -0
- package/internal/client/pair_test.go +109 -0
- package/internal/client/profile.go +184 -0
- package/internal/client/service.go +888 -0
- package/internal/client/service_test.go +654 -0
- package/internal/client/session_events_test.go +186 -0
- package/internal/client/session_messages_test.go +150 -0
- package/internal/config/config.go +439 -0
- package/internal/config/config_test.go +448 -0
- package/internal/session/session.go +447 -0
- package/internal/session/session_append.go +241 -0
- package/internal/session/session_append_test.go +161 -0
- package/internal/session/session_kill_unix_test.go +90 -0
- package/internal/session/session_mark_test.go +95 -0
- package/internal/session/session_test.go +237 -0
- package/internal/session/sessionlog.go +128 -0
- package/internal/session/sessionlog_read_test.go +157 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/install.js +121 -0
- package/npm/lib/platform.js +78 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# magic-agent
|
|
2
|
+
|
|
3
|
+
统一的 agent CLI 代理 —— 把 **claude / codebuddy / trae / llm / codex / openclaw / dsh** 七家 CLI 与 **arkclaw**(A2A JSON-RPC 网关)、**codebuddy-gateway**(CodeBuddy Code HTTP 网关)的非交互调用收成一条命令,提供一致的引擎/模型切换、超时与重试、固定输出格式与稳定退出码。适合脚本化编排与上层工具集成。
|
|
4
|
+
|
|
5
|
+
## 能做什么
|
|
6
|
+
|
|
7
|
+
| 能力 | 说明 |
|
|
8
|
+
|------|------|
|
|
9
|
+
| **多引擎统一调用** | 一条命令串起 claude / codebuddy / trae / llm / codex / openclaw / dsh / arkclaw / codebuddy-gateway 这十种后端,参数语义统一 |
|
|
10
|
+
| **引擎与模型清单** | `--engines` 给出当前可用的引擎、模型列表与积分倍率;`install` 字段给出一键装/升级命令 |
|
|
11
|
+
| **流式 + 追加需求** | `--stream` 走各引擎原生协议;`--keep-alive` / `--append` 支持常驻会话里中途补需求 |
|
|
12
|
+
| **统一 wire** | 多模态附件、用户选择(`AskUserQuestion`)、四档权限模型都收成同一套协议,CLI 端与上层 UI 共用收口 |
|
|
13
|
+
| **客户端契约面** | `--contract` 给固定 schema(`contractVersion` + `engines[].capabilities`),桌面 / 移动端插件启动时按它做能力降级 |
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g magic-agent # 从 npmjs 全局安装(无需 Go)
|
|
19
|
+
npm install -g ./magic-agent-<version>.tgz # 从本地 tgz 安装(离线 / 内网)
|
|
20
|
+
go build -o bin/magic-agent ./cmd/magic-agent # 从源码构建(Go 1.26+)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
> ⚠️ 截至 2026-09-28,npmjs 上还查不到 `magic-agent`(首次正式发布尚未发生),
|
|
24
|
+
> 所以 `npm install -g magic-agent` 暂时装不上 —— 先用本地 tgz 或源码构建。
|
|
25
|
+
|
|
26
|
+
平台产物缺失时 `postinstall` 会用本机 Go 现场编译;两者都没有只告警,不阻断安装。同一个 `postinstall` 还会按需安装 **llm CLI**(`-e llm` 的依赖,隔离在 `~/.llm-venv`)。
|
|
27
|
+
|
|
28
|
+
发布由 **GitHub Actions** 完成:推 `v*.*.*` tag 触发 5 平台交叉编译并 `npm publish` 到 npmjs;手动触发走 dry-run。流程细节、`NPM_TOKEN` 配置与本地打包见 [docs/development.md](docs/development.md)。
|
|
29
|
+
|
|
30
|
+
## 引擎依赖
|
|
31
|
+
|
|
32
|
+
七个 CLI 按需安装,未安装的引擎自动探测失败但不影响其他引擎:
|
|
33
|
+
|
|
34
|
+
| 引擎 | CLI | 探测路径 / 环境变量 |
|
|
35
|
+
|------|-----|---------------------|
|
|
36
|
+
| claude | Claude Code | `MAGIC_AGENT_CLAUDE_BIN` → `/opt/homebrew/bin/claude` → PATH |
|
|
37
|
+
| codebuddy | CodeBuddy Code CLI(`npm i -g @tencent-ai/codebuddy-code`) | `MAGIC_AGENT_CODEBUDDY_BIN` → PATH → npm 全局前缀 |
|
|
38
|
+
| codebuddy-ai | 同上(**同一个 CLI 的第二个账号**) | `MAGIC_AGENT_CODEBUDDY_AI_BIN` → PATH → npm 全局前缀 |
|
|
39
|
+
| trae | trae-cli | `MAGIC_AGENT_TRAE_BIN` → `~/.local/bin/trae-cli` → PATH |
|
|
40
|
+
| llm | [simonw/LLM](https://github.com/simonw/LLM) | `MAGIC_AGENT_LLM_BIN` → `~/.llm-venv/bin/llm` → `/opt/homebrew/bin/llm` → PATH |
|
|
41
|
+
| codex | `@openai/codex` | `MAGIC_AGENT_CODEX_BIN` → `/opt/homebrew/bin/codex` → PATH |
|
|
42
|
+
| openclaw | OpenClaw | `MAGIC_AGENT_OPENCLAW_BIN` → `/opt/homebrew/bin/openclaw` → PATH |
|
|
43
|
+
| dsh | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | `MAGIC_AGENT_DSH_BIN` → `~/.local/bin/dsh` → PATH → npm 全局前缀 |
|
|
44
|
+
| arkclaw | 无(远端 A2A 网关) | 配置文件的 `arkclaw` 节,或 `MAGIC_AGENT_ARKCLAW_URL` / `_KEY` / `_CLAW_ID` |
|
|
45
|
+
| codebuddy-gateway | 无(已在跑的 CodeBuddy Code HTTP 网关) | 配置文件的 `codebuddyGateway` 节,或 `MAGIC_AGENT_CBGW_URL` / `_PASSWORD` |
|
|
46
|
+
|
|
47
|
+
探测链最后一跳是 **npm 全局前缀** `<NPM_CONFIG_PREFIX>/bin`,专治「CLI 装好了、探测却说 not found」(容器常把 `npm i -g` 的落点设成不在 PATH 里的私有前缀)。优先级不变:显式参数 > 环境变量 > 候选路径 > PATH > npm 前缀。
|
|
48
|
+
|
|
49
|
+
`codebuddy` 与 `codebuddy-ai` 是**一个 CLI、两个账号**:跑同一个二进制,靠
|
|
50
|
+
`authentication.id` 分开票据(`sharedDataPath/auth/<id>.info`),互不顶号。
|
|
51
|
+
各自登录一次即可,登录态落进自己的票据:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
magic-agent --login codebuddy # 拉起交互式会话,在里面执行 /login
|
|
55
|
+
magic-agent --login codebuddy-ai # 同上;两个账号各自独立
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> 不再使用 WorkBuddy / WorkBuddy AI 桌面 App 内置的那份 CLI —— 那份的凭据由 App
|
|
59
|
+
> 用受管密钥静态加密,独立进程解不开(`missing-key`),只会拿到空输出。
|
|
60
|
+
|
|
61
|
+
## 引擎与模型清单(`--engines`)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
magic-agent --engines # JSON 数组:引擎 + 可用性 + 模型 + 版本 + 一键安装命令
|
|
65
|
+
magic-agent --engines --no-models # 跳过模型与版本探测(不启动任何 CLI,快)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
每行的字段:
|
|
69
|
+
|
|
70
|
+
| 字段 | 含义 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `engine` | 引擎名 |
|
|
73
|
+
| `ok` | 该引擎是否可用(CLI 存在 / 网关配置齐备) |
|
|
74
|
+
| `bin` | 可用时给二进制路径;无本机 CLI 的引擎给端点 URL |
|
|
75
|
+
| `note` | 不可用原因(`ok:false` 时) |
|
|
76
|
+
| `install` | 一键安装 / 升级命令(shell 一行)。`ok:false` 时拿它安装,已装的引擎重跑就是升级到最新版。桌面端 GUI 应用与网关类引擎没有该字段 |
|
|
77
|
+
| `version` | 引擎当前版本号(尽力而为,拿不到就不给)。是「升级有没有生效」的依据,**不是**可用性判据 |
|
|
78
|
+
| `models` | 该引擎当前支持的模型(动态探测,来源见下) |
|
|
79
|
+
| `models_note` | 拿不到 `models` 时的原因 |
|
|
80
|
+
| `model_credits` | 积分倍率表(model → `"0.34"`)。`codebuddy` / `codebuddy-ai` 给出;客户端按它给模型加「限免 / 夜间免费」角标与结算 |
|
|
81
|
+
| `workspace` | 指定工作目录的落地方式(`flag:-C` / `cwd` / `none`) |
|
|
82
|
+
| `streaming` | 是否支持 `--stream` |
|
|
83
|
+
| `attachments` | 收附件的落地方式(原生图片 / 路径写进提示词) |
|
|
84
|
+
| `append` | 是否支持常驻会话 + 追加消息 |
|
|
85
|
+
| `ask` | 「需要用户选择」的落地方式 |
|
|
86
|
+
| `permission` | 四档权限模型的落地方式 |
|
|
87
|
+
| `capabilities` | 该引擎的**静态能力 id 集合**,回答「能做什么」;与 `ok` / `models` 那些回答「此刻如何」的字段严格分开。取值见 `internal/agent/capability.go` |
|
|
88
|
+
|
|
89
|
+
`--json` 为兼容旧调用保留(行为相同)。
|
|
90
|
+
|
|
91
|
+
### 一键安装 / 升级(`install` 字段)
|
|
92
|
+
|
|
93
|
+
| 引擎 | `install` |
|
|
94
|
+
|------|-----------|
|
|
95
|
+
| claude | `npm install -g @anthropic-ai/claude-code` |
|
|
96
|
+
| codex | `npm install -g @openai/codex` |
|
|
97
|
+
| openclaw | `npm install -g openclaw@latest` |
|
|
98
|
+
| dsh | `npm i -g @deepseek-ai/dsh` |
|
|
99
|
+
| trae | `sh -c "$(curl -L https://trae.cn/trae-cli/install.sh)"` |
|
|
100
|
+
| llm | `python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm` |
|
|
101
|
+
| codebuddy / codebuddy-ai / arkclaw / codebuddy-gateway / 具名 A2A agent | **无该字段**(桌面端 GUI 应用 / 网关,没有可执行的安装命令),怎么才能用见同行 `note` |
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# 装齐本机缺的引擎
|
|
105
|
+
magic-agent --engines --no-models | jq -r '.[] | select(.ok|not) | "\(.engine)\t\(.install // .note)"'
|
|
106
|
+
# 升级:看版本号 → 跑命令 → 再看版本号(变了才算升上去)
|
|
107
|
+
magic-agent --engines --no-models | jq -r '.[] | select(.install) | "\(.engine)\t\(.version // "-")\t\(.install)"'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 模型清单来源(不硬编码)
|
|
111
|
+
|
|
112
|
+
`models` 一律从各引擎自己的权威入口现取现算,代理侧不维护任何模型名常量:
|
|
113
|
+
|
|
114
|
+
| 引擎 | 动态来源 |
|
|
115
|
+
|------|---------|
|
|
116
|
+
| claude | `~/.claude/settings.json`(顶层 `model` + env 里 `ANTHROPIC_*MODEL[*_NAME]`) |
|
|
117
|
+
| codebuddy | 客户端模型选择器清单 ∩ 客户端合并配置缓存;回退 `codebuddy --help` 的 `Currently supported` |
|
|
118
|
+
| codebuddy-ai | 同上(独立配置目录 `~/.workbuddy-ai`) |
|
|
119
|
+
| trae | `trae-cli models --json` |
|
|
120
|
+
| llm | `llm models` |
|
|
121
|
+
| codex | `codex debug models` |
|
|
122
|
+
| openclaw | `openclaw models list --json` |
|
|
123
|
+
| dsh | `$DSH_HOME/settings.yaml` 里已配置的模型,输出 `route/model` |
|
|
124
|
+
| arkclaw / codebuddy-gateway | 无清单(模型由网关侧决定),改用 `models_note` 说明 |
|
|
125
|
+
|
|
126
|
+
探测只跑**只读子命令**(不发起推理、不消耗额度),但会真的启动进程,因此并发执行、单引擎上限 30s;任一引擎失败只写进该行 `models_note`。
|
|
127
|
+
|
|
128
|
+
### 客户端契约(`--contract`)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
magic-agent --contract # {"contractVersion":1,"engines":[...]}
|
|
132
|
+
magic-agent --contract | jq -r '.contractVersion' # 启动时先校验这个
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| 字段 | 含义 |
|
|
136
|
+
|------|------|
|
|
137
|
+
| `contractVersion` | 契约面版本(当前 `1`)。客户端超出自己支持的范围就明确报错并提示升级,不要带着不匹配的能力继续跑 |
|
|
138
|
+
| `engines` | 与 `--engines` 的行完全同构,每行另含 `capabilities` |
|
|
139
|
+
|
|
140
|
+
版本规则:**只增字段不升版本**;字段改名 / 语义变化 / 删除时必须升 `contractVersion`。`--contract` 默认**不探测模型**(模型探测要起真 CLI,30 秒级,客户端启动路径等不起),要连模型一起拿就写 `--no-models=false`。
|
|
141
|
+
|
|
142
|
+
## 用法
|
|
143
|
+
|
|
144
|
+
无子命令设计:所有功能都走根命令 + flags。
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# 基本提问(默认 json 输出;位置参数等价于 -p)
|
|
148
|
+
magic-agent -p "用一句话解释什么是熵"
|
|
149
|
+
magic-agent -o text -p "用一句话解释什么是熵"
|
|
150
|
+
|
|
151
|
+
# 切引擎、切模型
|
|
152
|
+
magic-agent -e codebuddy "写一首俳句" # codebuddy 默认 hy3
|
|
153
|
+
magic-agent -e codebuddy -m glm-5.3 "写一首俳句"
|
|
154
|
+
magic-agent -e trae -m My-MiniMax-M3 "..." # trae 无 --model,内部转 -c model.name=
|
|
155
|
+
magic-agent -e llm -m minimax-m3 "问题" # llm 用自己注册的模型
|
|
156
|
+
|
|
157
|
+
# 工具与权限(--tools 默认 off = 纯 chat)
|
|
158
|
+
magic-agent -e claude --tools on -p "看看当前目录有什么"
|
|
159
|
+
magic-agent -e claude --tools Bash,Read -p "读一下 README"
|
|
160
|
+
magic-agent -e claude --tools on --permission auto -p "重构这个函数"
|
|
161
|
+
|
|
162
|
+
# 超时 + 重试 + 系统提示词
|
|
163
|
+
magic-agent -e claude -t 3m -r 2 --verbose -o text "复杂的分析任务"
|
|
164
|
+
magic-agent -e claude -s "你是严谨的翻译官,只输出译文" -o text "Hello, world"
|
|
165
|
+
|
|
166
|
+
# 管道输入 / 文件
|
|
167
|
+
cat doc.md | magic-agent -e claude -o text -f - "总结上文"
|
|
168
|
+
magic-agent -e claude -p ./prompt.md "补充一句" # 提示词给文件路径
|
|
169
|
+
|
|
170
|
+
# 附件(截图 + 提示词)
|
|
171
|
+
magic-agent -p "这张图什么颜色" -a shot.png
|
|
172
|
+
magic-agent -p "对比两张图" -a a.png -a b.png
|
|
173
|
+
|
|
174
|
+
# 流式输出
|
|
175
|
+
magic-agent --stream -e claude -o text "复杂问题"
|
|
176
|
+
magic-agent --stream --no-thinking -e claude "问题"
|
|
177
|
+
|
|
178
|
+
# 会话
|
|
179
|
+
magic-agent --sessions # 会话登记表
|
|
180
|
+
magic-agent --session-log <id> # 读这段会话的事件历史({session, events})
|
|
181
|
+
magic-agent --session-log <id> --after 20 # 增量续读:只回 seq > 20 的部分
|
|
182
|
+
magic-agent --stop <session_id> # 停掉这条会话的引擎进程组
|
|
183
|
+
magic-agent --append <run_id> -p "追加:顺便把 lint 也跑了"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Flags
|
|
187
|
+
|
|
188
|
+
| Flag | 默认 | 说明 |
|
|
189
|
+
|------|------|------|
|
|
190
|
+
| `-e, --engine` | `codebuddy` | 引擎:`claude` \| `codebuddy` \| `codebuddy-ai` \| `codebuddy-gateway` \| `trae` \| `llm` \| `codex` \| `openclaw` \| `dsh` \| `arkclaw`,外加配置里 `agents` 声明的具名 agent |
|
|
191
|
+
| `-m, --model` | 空 | 模型(空 = 引擎默认;codebuddy 默认 `hy3`) |
|
|
192
|
+
| `-s, --system` | 空 | 系统提示词;空 = 用配置里的 `systemPrompt`。值可以是文件路径或 `@文件` |
|
|
193
|
+
| `-p, --prompt` | 空 | 提示词;值可以是文件路径或 `@文件` |
|
|
194
|
+
| `-f, --file` | 空 | 从文件读 prompt(`-` = stdin) |
|
|
195
|
+
| `-a, --attach` | 空 | 附件(截图 / 图片),可重复或逗号分隔 |
|
|
196
|
+
| `--tools` | `off` | `off`(纯 chat)\| `on`(agent 模式)\| 逗号分隔白名单 |
|
|
197
|
+
| `-t, --timeout` | `600s` | 单次尝试超时(如 `90s` / `3m`) |
|
|
198
|
+
| `-r, --retries` | `0` | 失败重试次数(总尝试 = 1 + retries) |
|
|
199
|
+
| `--backoff` | `2s` | 首次重试退避(指数翻倍,上限 30s,带抖动) |
|
|
200
|
+
| `-o, --output` | `json` | 输出格式:`json` \| `text` |
|
|
201
|
+
| `--stream` | 关 | 流式输出(text 模式思考走 stderr) |
|
|
202
|
+
| `--no-thinking` | 关 | 流式模式下不转发思考过程增量 |
|
|
203
|
+
| `--engines` | 关 | 列出引擎、可用性、模型、版本与一键安装命令 |
|
|
204
|
+
| `--no-models` | 关 | 配合 `--engines`:跳过模型与版本探测 |
|
|
205
|
+
| `--json` | 关 | 兼容保留(行为同 `--engines` 的默认 JSON 输出) |
|
|
206
|
+
| `--stop` | 空 | 停止指定会话 / 运行(传 `session_id` 或 `run_id`) |
|
|
207
|
+
| `--sessions` | 关 | 列出会话登记表 |
|
|
208
|
+
| `--session-log` | 空 | 读一段会话的事件历史(传 `session_id` 或 `run_id`);输出 `{session, events}` |
|
|
209
|
+
| `--after` | `0` | 配合 `--session-log` 增量续读:只回 `seq > N` 的事件(另附 `count/after/lastSeq/snapshotRequired`) |
|
|
210
|
+
| `--keep-alive` | **开** | 常驻会话(仅 `claude`/`codebuddy` 的 `--stream` 调用生效) |
|
|
211
|
+
| `--append` | 空 | 向常驻会话追加一条消息(传 `session_id` 或 `run_id`) |
|
|
212
|
+
| `--idle` | `5m` | 常驻会话空闲收工时长(`0` = 本轮结束就收工) |
|
|
213
|
+
| `-v, --verbose` | `false` | 重试过程打印到 stderr |
|
|
214
|
+
|
|
215
|
+
prompt 输入优先级:`-p/--prompt` > 位置参数 > `--file` > stdin 管道(stdin 非 TTY 且无其他输入时自动读)。
|
|
216
|
+
|
|
217
|
+
## 流式模式(`--stream`)
|
|
218
|
+
|
|
219
|
+
`--stream` 走各引擎的原生协议:claude / codebuddy / trae 走 CLI 的原生 `stream-json` NDJSON,llm 走纯文本流式,openclaw 走 ACP 桥,arkclaw 与 codebuddy-gateway 走 SSE。增量实时转发,无缓冲等待。
|
|
220
|
+
|
|
221
|
+
思考通道与正文分开:`-o text` 下正文进 stdout、思考进 stderr;`-o json` 下是 NDJSON 事件流(`thinking` / `text` / `tool_use` / `tool_result` / `ask` / `turn_end`),末尾一条 `result` 收尾行。
|
|
222
|
+
|
|
223
|
+
各引擎的流式成色不同(其中 openclaw 的 ACP 桥、arkclaw 的 A2A SSE、dsh 的 SDK 通道差异最大),细节见 [docs/streaming.md](docs/streaming.md)。
|
|
224
|
+
|
|
225
|
+
## 常驻会话与追加需求(`--keep-alive` 默认开)
|
|
226
|
+
|
|
227
|
+
`claude` / `codebuddy` 的 `--stream` 调用**默认即常驻**:长任务跑着的时候,可以在另一个进程追加需求。
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
magic-agent -e claude --stream -p "先把仓库跑一遍测试" -o text # ① 起会话
|
|
231
|
+
magic-agent --append <run_id> -p "追加:顺便把 lint 也跑了" # ② 中途追加
|
|
232
|
+
magic-agent --stop <run_id> # ③ 收工(或空闲 --idle 自动退出)
|
|
233
|
+
|
|
234
|
+
magic-agent -e claude --stream --keep-alive=false -p "..." # 要「一轮结束就退出」的老行为
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
| 维度 | 行为 |
|
|
238
|
+
|---|---|
|
|
239
|
+
| 支持范围 | 只有 `claude` / `codebuddy` 的 `--stream` 调用;其它情况自动忽略(显式传 `--keep-alive` 却不满足条件 → `exit 2` 报错) |
|
|
240
|
+
| 追加语义 | 消息写进引擎 stdin 成为**下一轮 user 消息**:本轮跑完后接着处理,不打断当前轮 |
|
|
241
|
+
| 收工 | 最后一轮结束后 `--idle`(默认 5m)内无人追加则优雅收尾;`--idle 0` = 本轮结束就收工 |
|
|
242
|
+
| 超时 | 常驻生效期间不套 `-t` 的默认 600s(长任务 + 追加不该被砍),显式给 `-t` 才生效 |
|
|
243
|
+
|
|
244
|
+
> ⚠️ **默认常驻带来的行为变化**:进程会在最后一轮结束后继续存活 `--idle` 等待追加。调用方若要「一轮完成」的信号,请读事件流里的 **`turn_end`** 事件(而不是等进程退出),或启动时传 `--keep-alive=false`。
|
|
245
|
+
|
|
246
|
+
## 停止指定会话
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
magic-agent --sessions # 列出会话登记表(新→旧)
|
|
250
|
+
magic-agent --stop <session_id> # 按会话 id 停
|
|
251
|
+
magic-agent --stop <run_id> # 按运行 id 停(新会话还没拿到 session_id 时)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
引擎 CLI 在**独立进程组**里跑,所以调用方对 magic-agent 发信号带不走它 —— 界面显示「已停止」而引擎还在后台跑。因此每次调用都落一条记录(`~/.magic-agent/sessions/<run_id>.json`,`MAGIC_AGENT_SESSIONS` 可改目录,记录保留 24 小时),`--stop` 按记录里的 pid 杀整个进程组(先 `SIGTERM`,2s 内不退再 `SIGKILL`)。
|
|
255
|
+
|
|
256
|
+
| 情况 | 退出码 |
|
|
257
|
+
|---|---|
|
|
258
|
+
| 在跑 → 已停 / 进程已不在 / 会话已结束(幂等,重复点「停止」不算错) | 0 |
|
|
259
|
+
| id 不存在 | 1 |
|
|
260
|
+
| `--stop` 后没给 id | 2 |
|
|
261
|
+
|
|
262
|
+
## 附件(文件+提示词)
|
|
263
|
+
|
|
264
|
+
**提示词照旧,附件是并列的第二份输入**:「截图 + 提示词」是主要用法。
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
magic-agent -p "这张图什么颜色" -a ~/Desktop/shot.png
|
|
268
|
+
magic-agent -p "对比这两张图的差异" -a a.png -a b.png # 可重复,也可逗号分隔
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
每个引擎尽量走**自己的原生附件通道**(`--engines` 的 `attachments` 字段给出机器可读答案):
|
|
272
|
+
|
|
273
|
+
| 引擎 | 落地方式 |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `claude` / `codebuddy` | 原生 `stream-json`:图片作为 image content block 经 stdin 传入 |
|
|
276
|
+
| `codex` | 原生 `-i/--image`(只收图片) |
|
|
277
|
+
| `llm` | 原生 `-a`(委托 llm CLI 时)/ OpenAI 兼容 content parts(直连配置模型时) |
|
|
278
|
+
| `arkclaw` | A2A 原生 `file` part(base64 inline) |
|
|
279
|
+
| `trae` / `openclaw` / `dsh` / `codebuddy-gateway` | 无原生通道 → 把**绝对路径**写进提示词清单,靠引擎自己的读文件工具 |
|
|
280
|
+
|
|
281
|
+
校验:路径必须存在、是普通文件、≤32MB;重复路径自动去重;类型按**魔数**嗅探(不信扩展名)。无原生通道的引擎会**明确提示**不静默;提示词在有原生通道时原样不改写。
|
|
282
|
+
|
|
283
|
+
> 能力上限取决于模型:链路只保证把附件按协议送到位,模型是否真能看图要看后端。实测同一张纯红 PNG,`-e llm` 直连 MiniMax-M3 答对,而走某网关的 claude CLI 把图片块吞掉了。
|
|
284
|
+
|
|
285
|
+
## 需要用户选择(`AskUserQuestion`)
|
|
286
|
+
|
|
287
|
+
模型发起 `AskUserQuestion`(或工具调用待授权)时,claude 是普通 `tool_use` 块、codebuddy 族是 `control_request`——wire 上两种完全不同的形状。magic-agent 把它们归一化成一份统一格式,`--stream` 输出里多一类 `ask` 事件(消息里带 `questions` / `options`),上层 UI 拿到后渲染给用户,再用 `--append` 把选择传回去。
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
magic-agent --engines --no-models | jq -r '.[] | "\(.engine)\t\(.ask)"'
|
|
291
|
+
# claude → "tool:AskUserQuestion";其余(含 codebuddy 族)→ "none"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
> ⚠️ **codebuddy / codebuddy-ai 报 `none` 是实测结论(2026-09-28)**:官方文档把 `AskUserQuestion` 列为内置工具,但 CLI 只把它交给第一方宿主(官方 App / IDE)——第三方宿主驱动的通道下模型**根本看不到该工具**。七种配置实测全部拿不到(`-p` / `--tools default` / 白名单 / `--allowedTools` / `-y` / `auto` / SDK 声明 `capabilities.elicitation.form` / `CODEBUDDY_HOST_CAPABILITIES` / `--acp` ± `elicitation.form`),模型一律搜索不到、退化成文字提问。此前按"同族协议"推断成支持,会让上层 UI 承诺永远不出现的决策卡。**需要决策卡请用 `claude` 引擎。** 详见 [docs/engines.md](docs/engines.md)。
|
|
295
|
+
|
|
296
|
+
wire 形状、三条通道与接住回答的写法见 [docs/notes.md](docs/notes.md)。
|
|
297
|
+
|
|
298
|
+
## 提示词可以是文件路径
|
|
299
|
+
|
|
300
|
+
长提示词写在文件里,不必每次 `-f` 或 `cat |`:`-p` / 位置参数 / `-s` 的值**命中一个已存在的普通文件**时,直接按文件内容用。
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
magic-agent -p ./prompt.md # 按文件内容提问
|
|
304
|
+
magic-agent -p ./prompt.md "补充一句" # 文件内容在前、追加文本在后
|
|
305
|
+
magic-agent -p @notes/task.md # @ 强制按文件读(读不到 → exit 2)
|
|
306
|
+
magic-agent -s ./reviewer.md "审一下这段" # 系统提示词也能给文件
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
自动识别的判据刻意保守,避免把正常提示词误判成路径:必须是**单行**、≤4096 字节、`stat` 出来是**普通文件**。所以多行提示词、粘贴的长文都不受影响。`~` 前缀会展开。
|
|
310
|
+
|
|
311
|
+
## 默认系统提示词与配置
|
|
312
|
+
|
|
313
|
+
不想每次调用都敲 `-s`,就把默认系统提示词写进配置文件 —— 对所有引擎生效:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
mkdir -p ~/.config/magic-agent && cat > ~/.config/magic-agent/config.json <<'EOF'
|
|
317
|
+
{
|
|
318
|
+
"systemPrompt": "你是一个中文助手,始终用中文回答所有问题。",
|
|
319
|
+
"arkclaw": { "url": "…", "key": "…", "claw_id": "…" }
|
|
320
|
+
}
|
|
321
|
+
EOF
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
| 维度 | 行为 |
|
|
325
|
+
|---|---|
|
|
326
|
+
| 生效条件 | 调用时未给 `-s` → 注入配置里的 `systemPrompt`;给了 `-s` 则 `-s` 优先,不叠加 |
|
|
327
|
+
| 作用范围 | 全部引擎 |
|
|
328
|
+
| 键名兼容 | `systemPrompt` \| `system_prompt` \| `system` 任选其一 |
|
|
329
|
+
| 临时覆盖 | 环境变量 `MAGIC_AGENT_SYSTEM_PROMPT` 优先于文件值 |
|
|
330
|
+
| 配置文件路径 | 默认 `~/.config/magic-agent/config.json`(`MAGIC_AGENT_CONFIG` 覆盖,`XDG_CONFIG_HOME` 优先于 `~/.config`) |
|
|
331
|
+
| 配置写坏时 | **不阻断调用**(本次不注入),`-v` 打印原因,不静默 |
|
|
332
|
+
|
|
333
|
+
完整配置项见 [config.example.json](config.example.json)。
|
|
334
|
+
|
|
335
|
+
## 输出格式
|
|
336
|
+
|
|
337
|
+
**json**(默认):stdout 单行 envelope。
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
{"engine":"claude","model":"claude-sonnet-4-6","session_id":"...","attempts":1,"latency_ms":534,"text":"..."}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**text**(`-o text`):stdout 只含模型正文 + 尾换行。
|
|
344
|
+
|
|
345
|
+
**失败时**:stdout 恒为空(不产生半截内容),错误打到 stderr,格式与 `-o` 联动。json 模式下 stderr 也是一行 envelope,可直接 `jq -r .reason` 取根因。
|
|
346
|
+
|
|
347
|
+
| 字段 | 说明 |
|
|
348
|
+
|------|------|
|
|
349
|
+
| `error` | 完整错误链(含引擎 / 重试包装),面向人排查 |
|
|
350
|
+
| `reason` | 最内层根因(stderr 摘要 / 超时 / 非零退出码),面向程序分支判断 |
|
|
351
|
+
| `attempts` | 实际执行次数;`0` = 参数校验阶段即失败 |
|
|
352
|
+
|
|
353
|
+
## 退出码
|
|
354
|
+
|
|
355
|
+
| 码 | 含义 |
|
|
356
|
+
|----|------|
|
|
357
|
+
| 0 | 成功 |
|
|
358
|
+
| 1 | 调用失败(引擎错误 / 超时耗尽 / 重试耗尽) |
|
|
359
|
+
| 2 | 参数或输入错误(未知引擎、非法格式、空 prompt 等) |
|
|
360
|
+
|
|
361
|
+
## 重试语义
|
|
362
|
+
|
|
363
|
+
- **可重试**:网络类(connection refused / reset、EOF、timeout、signal killed)、限流类(429、rate limit、503 overloaded)、单次尝试超时。
|
|
364
|
+
- **快速失败**:参数错、鉴权错、空输出、CLI 明确报错等不可恢复错误 —— 不浪费重试。
|
|
365
|
+
- 退避:`backoff * 2^(n-1)` + 抖动,上限 30s;整体 context 取消会立即打断等待。
|
|
366
|
+
|
|
367
|
+
所有引擎都以独立**进程组**运行:超时 / 取消时 `kill(-pgid)` 杀掉整个进程树,CLI 内部 spawn 的 node worker 不会残留。
|
|
368
|
+
|
|
369
|
+
## 平台支持
|
|
370
|
+
|
|
371
|
+
三档平台覆盖 npm 包内自带的 5 个预编译产物,进程组隔离在各平台均生效:
|
|
372
|
+
|
|
373
|
+
| 平台 | 产物目录 |
|
|
374
|
+
|------|---------|
|
|
375
|
+
| macOS arm64 / x64 | `npm/dist/darwin-arm64` / `darwin-x64` |
|
|
376
|
+
| Linux x64 / arm64 | `npm/dist/linux-x64` / `linux-arm64` |
|
|
377
|
+
| Windows x64 | `npm/dist/win32-x64` |
|
|
378
|
+
|
|
379
|
+
## 更多文档
|
|
380
|
+
|
|
381
|
+
| 文档 | 内容 |
|
|
382
|
+
|------|------|
|
|
383
|
+
| [docs/engines.md](docs/engines.md) | arkclaw / codebuddy-gateway / llm / dsh / codex 五个引擎的配置、协议与行为细节 |
|
|
384
|
+
| [docs/streaming.md](docs/streaming.md) | 各引擎流式的实现路径与实测差异(openclaw ACP 桥、arkclaw A2A SSE、dsh SDK 通道) |
|
|
385
|
+
| [docs/tools-and-permissions.md](docs/tools-and-permissions.md) | `--tools` 工具白名单与四档权限模型的落地细节 |
|
|
386
|
+
| [docs/notes.md](docs/notes.md) | 需要用户选择的统一格式、在 WorkBuddy / CodeBuddy 会话内使用时的注意事项 |
|
|
387
|
+
| [docs/development.md](docs/development.md) | CI 发版(GitHub Actions → npmjs)、本地打包流程、改动落地四步曲、架构与真机验证记录 |
|
|
388
|
+
| [CLIENT-CONTRACT.md](CLIENT-CONTRACT.md) | 客户端契约面(magic-client 插件对接用) |
|
|
389
|
+
|
|
390
|
+
## License
|
|
391
|
+
|
|
392
|
+
MIT
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// magic-client 是 magic-agent 的**可选**桌面 / 移动端插件:提供基础版 Agent UI
|
|
2
|
+
// 与一组扩展接口,让业务项目(magic-test 等)只写 profile 与业务模块。
|
|
3
|
+
//
|
|
4
|
+
// 它不替代 core:引擎调用、事件归一、会话与控制通道全在 magic-agent 里。
|
|
5
|
+
// 本进程只做三件事 —— 启动时校验 core 契约、把 core 的 NDJSON 事件原样中继给客户端、
|
|
6
|
+
// 提供静态资源与扩展接口的落点。
|
|
7
|
+
//
|
|
8
|
+
// 用法:
|
|
9
|
+
//
|
|
10
|
+
// magic-client serve --profile ./profiles/zhantianping.json --ui-dir ./client-ui
|
|
11
|
+
// magic-client serve --print-url # 只打印带令牌的地址,不启动(脚本用)
|
|
12
|
+
//
|
|
13
|
+
// 默认**只绑 127.0.0.1**,并需要 X-Magic-Token:同机任意进程都能访问本地端口,
|
|
14
|
+
// 不开鉴权等于把引擎执行权交给它们。
|
|
15
|
+
package main
|
|
16
|
+
|
|
17
|
+
import (
|
|
18
|
+
"crypto/rand"
|
|
19
|
+
"encoding/hex"
|
|
20
|
+
"flag"
|
|
21
|
+
"fmt"
|
|
22
|
+
"net"
|
|
23
|
+
"net/http"
|
|
24
|
+
"os"
|
|
25
|
+
"os/signal"
|
|
26
|
+
"strings"
|
|
27
|
+
"syscall"
|
|
28
|
+
"time"
|
|
29
|
+
|
|
30
|
+
"github.com/darren/magic-agent/internal/client"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
func main() {
|
|
34
|
+
if len(os.Args) < 2 || os.Args[1] != "serve" {
|
|
35
|
+
fmt.Fprintln(os.Stderr, "用法: magic-client serve [--profile <path>] [--ui-dir <dir>] [--addr 127.0.0.1:0] [--token <t>]")
|
|
36
|
+
os.Exit(2)
|
|
37
|
+
}
|
|
38
|
+
fs := flag.NewFlagSet("serve", flag.ExitOnError)
|
|
39
|
+
var (
|
|
40
|
+
profilePath = fs.String("profile", "", "profile JSON 路径(空 = 内置默认)")
|
|
41
|
+
uiDir = fs.String("ui-dir", "", "静态资源目录(基础版 UI 与业务模块)")
|
|
42
|
+
addr = fs.String("addr", "", "监听地址(默认 127.0.0.1:0;带 --lan 时是 0.0.0.0:0)")
|
|
43
|
+
lan = fs.Bool("lan", false, "绑到局域网(0.0.0.0),让手机 / 平板连得上。⚠️ 开了之后令牌是唯一门锁")
|
|
44
|
+
token = fs.String("token", "", "访问令牌(空 = 随机生成并打印;--lan 时至少要 24 个字符)")
|
|
45
|
+
magicAgent = fs.String("magic-agent", os.Getenv("MAGIC_AGENT_BIN"), "core 可执行文件(空 = 从 PATH 找 magic-agent)")
|
|
46
|
+
printURL = fs.Bool("print-url", false, "只打印带令牌的访问地址后退出(不启动服务)")
|
|
47
|
+
)
|
|
48
|
+
_ = fs.Parse(os.Args[2:])
|
|
49
|
+
|
|
50
|
+
prof, err := client.LoadProfile(*profilePath)
|
|
51
|
+
if err != nil {
|
|
52
|
+
fmt.Fprintln(os.Stderr, "magic-client:", err)
|
|
53
|
+
os.Exit(1)
|
|
54
|
+
}
|
|
55
|
+
tok := strings.TrimSpace(*token)
|
|
56
|
+
if tok == "" {
|
|
57
|
+
tok = randToken() // 16 字节 = 32 位十六进制,远强于局域网那条线
|
|
58
|
+
}
|
|
59
|
+
/* 绑到局域网时令牌是**唯一**门锁:内网能被扫到,短令牌等于没有。
|
|
60
|
+
默认生成的那个够强,所以这条只在用户显式给了弱令牌时报错。 */
|
|
61
|
+
if *lan {
|
|
62
|
+
if err := client.ValidateLanToken(tok); err != nil {
|
|
63
|
+
fmt.Fprintln(os.Stderr, "magic-client:", err)
|
|
64
|
+
os.Exit(2)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
svc, err := client.New(client.Options{
|
|
68
|
+
MagicAgent: *magicAgent,
|
|
69
|
+
Profile: prof,
|
|
70
|
+
UIRoot: *uiDir,
|
|
71
|
+
Token: tok,
|
|
72
|
+
})
|
|
73
|
+
if err != nil {
|
|
74
|
+
// 契约不匹配是**要立刻停**的错误:带着不匹配的能力跑起来只会让人以为是界面 bug。
|
|
75
|
+
fmt.Fprintln(os.Stderr, "magic-client: 启动前检查失败:", err)
|
|
76
|
+
os.Exit(1)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
listenAddr := strings.TrimSpace(*addr)
|
|
80
|
+
if listenAddr == "" {
|
|
81
|
+
if *lan {
|
|
82
|
+
listenAddr = "0.0.0.0:0"
|
|
83
|
+
} else {
|
|
84
|
+
listenAddr = "127.0.0.1:0"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
ln, err := net.Listen("tcp", listenAddr)
|
|
88
|
+
if err != nil {
|
|
89
|
+
fmt.Fprintln(os.Stderr, "magic-client: 监听失败:", err)
|
|
90
|
+
os.Exit(1)
|
|
91
|
+
}
|
|
92
|
+
port := ln.Addr().(*net.TCPAddr).Port
|
|
93
|
+
url := fmt.Sprintf("http://127.0.0.1:%d/?token=%s", port, tok)
|
|
94
|
+
if *printURL {
|
|
95
|
+
// 只打印本机地址:脚本拿它做探活,局域网地址在 --lan 下另有输出。
|
|
96
|
+
fmt.Println(url)
|
|
97
|
+
_ = ln.Close()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
c := svc.Contract()
|
|
102
|
+
fmt.Fprintf(os.Stderr, "magic-client: %s | core=%s 契约 v%d | 引擎 %d 个 | 数据目录 %s\n",
|
|
103
|
+
prof.App.Name, svc.MagicAgentPath(), c.ContractVersion, len(c.Engines), prof.DataDir())
|
|
104
|
+
fmt.Fprintf(os.Stderr, "magic-client: 打开 %s\n", url)
|
|
105
|
+
fmt.Fprintf(os.Stderr, "magic-client: 接口 /desk/{health,contract,engines,modules,sessions,profile,asks,ask,ask/{id}/control} + 业务转发 /api/*(都要 X-Magic-Token)\n")
|
|
106
|
+
if *lan {
|
|
107
|
+
// 绑到局域网就把可用地址全打出来:一台机器常有多个网卡,猜错比多打两行麻烦。
|
|
108
|
+
fmt.Fprintln(os.Stderr, "magic-client: ⚠️ 已绑到局域网(0.0.0.0):同网络内的设备都能访问,令牌是唯一门锁")
|
|
109
|
+
addrs := client.LanAddresses(port)
|
|
110
|
+
if len(addrs) == 0 {
|
|
111
|
+
fmt.Fprintln(os.Stderr, "magic-client: 没找到局域网地址(只连着回环?用 ipconfig getifaddr en0 查一下本机 IP)")
|
|
112
|
+
}
|
|
113
|
+
for _, a := range addrs {
|
|
114
|
+
fmt.Fprintf(os.Stderr, "magic-client: 手机 / 平板打开 http://%s/?token=%s\n", a, tok)
|
|
115
|
+
// 配对二维码页:要在**电脑上**打开这个链接(手机扫屏幕上的码)。
|
|
116
|
+
// 用某个网卡地址打开,码里就是那个地址 —— 所以每个地址各给一条。
|
|
117
|
+
fmt.Fprintf(os.Stderr, "magic-client: 配对二维码(在电脑上开,手机扫屏幕) http://%s/pair?token=%s\n", a, tok)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
srv := &http.Server{
|
|
122
|
+
Handler: svc.Handler(),
|
|
123
|
+
ReadHeaderTimeout: 10 * time.Second,
|
|
124
|
+
}
|
|
125
|
+
go func() {
|
|
126
|
+
stop := make(chan os.Signal, 1)
|
|
127
|
+
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
|
128
|
+
<-stop
|
|
129
|
+
fmt.Fprintln(os.Stderr, "\nmagic-client: 收到退出信号,关闭中")
|
|
130
|
+
_ = srv.Close()
|
|
131
|
+
}()
|
|
132
|
+
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
|
|
133
|
+
fmt.Fprintln(os.Stderr, "magic-client: 服务退出:", err)
|
|
134
|
+
os.Exit(1)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func randToken() string {
|
|
139
|
+
b := make([]byte, 16)
|
|
140
|
+
if _, err := rand.Read(b); err != nil {
|
|
141
|
+
return fmt.Sprintf("%d", time.Now().UnixNano())
|
|
142
|
+
}
|
|
143
|
+
return hex.EncodeToString(b)
|
|
144
|
+
}
|
package/go.mod
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module github.com/darren/magic-agent
|
|
2
|
+
|
|
3
|
+
go 1.26.5
|
|
4
|
+
|
|
5
|
+
require github.com/spf13/cobra v1.10.2
|
|
6
|
+
|
|
7
|
+
require (
|
|
8
|
+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
9
|
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
|
10
|
+
github.com/spf13/pflag v1.0.9 // indirect
|
|
11
|
+
)
|
package/go.sum
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
2
|
+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
3
|
+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
4
|
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
5
|
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
|
6
|
+
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
|
7
|
+
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
|
8
|
+
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
|
9
|
+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
|
10
|
+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
11
|
+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
12
|
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|