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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
// sessionlog.go - 「看历史」会话持久化的纯读写路径。
|
|
4
|
+
//
|
|
5
|
+
// 谁用:core CLI 在流式跑时 append 事件到 `<dir>/<session_id>.jsonl`;
|
|
6
|
+
// plugin (`magic-client`) 在 `GET /desk/session/{id}/messages` 路由上读这个文件。
|
|
7
|
+
// 双方都在 `~/.magic-agent/sessions/`(同一目录)。
|
|
8
|
+
//
|
|
9
|
+
// 与 `Begin/Finish` 的关系:这里只管「事件落盘」的内容(流式输出每条事件);
|
|
10
|
+
// 登记表(`run-*.json`)只记元数据(提示词、状态、pid)。两条线相互独立。
|
|
11
|
+
//
|
|
12
|
+
// 读写路径都放在 `session` 包,而不是 `cli`:因为 plugin (`internal/client`) 也要读。
|
|
13
|
+
// 跨包依赖方向:`internal/client -> internal/session`,避免 `internal/client -> internal/cli`。
|
|
14
|
+
//
|
|
15
|
+
// 设计约束:
|
|
16
|
+
// - 文件不存在 ≠ 错:返回空 slice + nil error(UI 上「空会话」也是合理状态)。
|
|
17
|
+
// - 坏行不阻塞:NDJSON 解析失败的行 skip 一次(stderr 打 warning);
|
|
18
|
+
// 文件里其它正常行仍能取到。
|
|
19
|
+
// - 大文件:16MB 单行上限(result 行可能很大);调用方按 seq 翻页可另写。
|
|
20
|
+
|
|
21
|
+
import (
|
|
22
|
+
"bufio"
|
|
23
|
+
"errors"
|
|
24
|
+
"fmt"
|
|
25
|
+
"io"
|
|
26
|
+
"os"
|
|
27
|
+
"path/filepath"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// DefaultLogDir 返回 ~/.magic-agent/sessions/(与 core CLI 用的目录一致)。
|
|
31
|
+
//
|
|
32
|
+
// 谁用:plugin (`internal/client`) 在 plugin 进程里直接读历史时 —— core CLI 是另一
|
|
33
|
+
// 个进程,得在 plugin 这边再做一次解析。
|
|
34
|
+
func DefaultLogDir() (string, error) {
|
|
35
|
+
home, err := os.UserHomeDir()
|
|
36
|
+
if err != nil {
|
|
37
|
+
return "", fmt.Errorf("找不到家目录:%w", err)
|
|
38
|
+
}
|
|
39
|
+
return filepath.Join(home, ".magic-agent", "sessions"), nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// SeqOf 从**已解析**的一条事件里取出 seq。
|
|
43
|
+
//
|
|
44
|
+
// 为什么要回调而不是要求 T 带字段:本包被 CLI(`sessionEvent` 结构体)与 plugin
|
|
45
|
+
// (`map[string]any`,它不想依赖 cli 的 schema)两边共用,两者取 seq 的方式不同。
|
|
46
|
+
type SeqOf[T any] func(T) uint64
|
|
47
|
+
|
|
48
|
+
// ReadSessionLog 读整个 jsonl 到 `[]T`(每条事件一行 JSON)。
|
|
49
|
+
//
|
|
50
|
+
// dir 为空时报错(避免「默认进 home 写错了路径」这种难追的 bug,调用方必须显式传)。
|
|
51
|
+
// 这是**泛型**,因为 events 的具体类型由调用方决定(CLI 用 `sessionEvent`、
|
|
52
|
+
// plugin 也用同一类型 —— 所以 plugin 这边不需要再写一份)。
|
|
53
|
+
//
|
|
54
|
+
// 顺序:seq 升序;空文件返回 nil。损坏行跳过(warn 一行)。
|
|
55
|
+
func ReadSessionLog[T any](dir, sessionID string, parse func([]byte) (T, error)) ([]T, error) {
|
|
56
|
+
evs, _, err := ReadSessionLogAfter(dir, sessionID, 0, nil, parse)
|
|
57
|
+
return evs, err
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ReadSessionLogAfter 只读 seq > after 的事件(**断线续读**用)。
|
|
61
|
+
// 返回 (事件, 文件里的最大 seq, error)。after = 0 等价于全量。
|
|
62
|
+
//
|
|
63
|
+
// 语义对齐 agents-anywhere 的 `GET /sessions/{id}/events?after=seq:N`:
|
|
64
|
+
// 客户端报「我已经看到 seq=N」,服务端就从 N+1 开始给。
|
|
65
|
+
//
|
|
66
|
+
// ⚠️ after 是**排他**上界:不回 seq == after 的那条。重复回一条会让客户端的去重
|
|
67
|
+
//
|
|
68
|
+
// 逻辑从「优化」变成「必需」—— 而续读的全部意义就是让客户端不必再关心去重。
|
|
69
|
+
//
|
|
70
|
+
// ⚠️ 返回的 maxSeq 覆盖**整个文件**(含被 after 滤掉的部分):调用方要用它判断
|
|
71
|
+
//
|
|
72
|
+
// 「客户端的游标是不是超前了」(after > maxSeq ⇒ 客户端状态比磁盘还新,
|
|
73
|
+
// 只可能是日志被重置过、或游标来自另一个会话)。只统计过滤后的事件得不到这个信息。
|
|
74
|
+
//
|
|
75
|
+
// ⚠️ 仍然顺序读完整文件后过滤,没有按 seq 做**定位**(文件是 NDJSON、seq 靠解析才拿到,
|
|
76
|
+
//
|
|
77
|
+
// 无法 seek)。代价是每次续读都要解析整个文件;收益是**网络只传增量**。
|
|
78
|
+
// 本项目的会话规模下这个取舍是划算的(本地解析毫秒级,跨网传全量是百 KB 级)。
|
|
79
|
+
func ReadSessionLogAfter[T any](dir, sessionID string, after uint64, seqOf SeqOf[T], parse func([]byte) (T, error)) ([]T, uint64, error) {
|
|
80
|
+
if dir == "" {
|
|
81
|
+
return nil, 0, fmt.Errorf("ReadSessionLog: dir 不能为空(调用方需用 DefaultLogDir() 解析)")
|
|
82
|
+
}
|
|
83
|
+
f, err := os.Open(filepath.Join(dir, sessionID+".jsonl"))
|
|
84
|
+
if err != nil {
|
|
85
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
86
|
+
return nil, 0, nil // 没历史 ≠ 错
|
|
87
|
+
}
|
|
88
|
+
return nil, 0, err
|
|
89
|
+
}
|
|
90
|
+
defer f.Close()
|
|
91
|
+
return readNDJSON(f, after, seqOf, parse)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// readNDJSON 流式读 NDJSON。空行 / 坏行跳过(warn 一次)。
|
|
95
|
+
//
|
|
96
|
+
// after / seqOf 都可选:seqOf 为 nil 时不按 seq 过滤(此时 after 无意义),
|
|
97
|
+
// 返回的 maxSeq 也就恒为 0 —— 这两个参数是配套的,调用方别只给一半。
|
|
98
|
+
func readNDJSON[T any](r io.Reader, after uint64, seqOf SeqOf[T], parse func([]byte) (T, error)) ([]T, uint64, error) {
|
|
99
|
+
var out []T
|
|
100
|
+
var maxSeq uint64
|
|
101
|
+
sc := bufio.NewScanner(r)
|
|
102
|
+
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
|
103
|
+
for sc.Scan() {
|
|
104
|
+
line := sc.Bytes()
|
|
105
|
+
if len(line) == 0 {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
v, err := parse(line)
|
|
109
|
+
if err != nil {
|
|
110
|
+
fmt.Fprintf(os.Stderr, "magic-agent: 会话历史行解析失败:%v(跳过该行)\n", err)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
if seqOf == nil {
|
|
114
|
+
out = append(out, v)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
s := seqOf(v)
|
|
118
|
+
// maxSeq 先更新、后过滤:它要反映整个文件,而不只是这一页。
|
|
119
|
+
if s > maxSeq {
|
|
120
|
+
maxSeq = s
|
|
121
|
+
}
|
|
122
|
+
if s <= after {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
out = append(out, v)
|
|
126
|
+
}
|
|
127
|
+
return out, maxSeq, sc.Err()
|
|
128
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// 会话日志的增量读(断线续读)。这一组钉住三件最容易写错的事:
|
|
11
|
+
//
|
|
12
|
+
// 1. after 是**排他**上界(不回 seq == after 那条)——写成 >= 会让客户端每次续读
|
|
13
|
+
// 都重收一条,去重逻辑就被迫成为必需;
|
|
14
|
+
// 2. 返回的 maxSeq 覆盖**整个文件**而不只是这一页 —— 它是「客户端游标是否超前」的
|
|
15
|
+
// 唯一判据,只统计过滤后的事件就永远判不出超前;
|
|
16
|
+
// 3. 文件不存在时不是错误(新会话没有历史是正常状态)。
|
|
17
|
+
|
|
18
|
+
// seqEvent 测试用的事件类型:带 seq 与 text 两个字段。
|
|
19
|
+
type seqEvent struct {
|
|
20
|
+
Seq uint64 `json:"seq"`
|
|
21
|
+
Text string `json:"text"`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func parseSeqEvent(line []byte) (seqEvent, error) {
|
|
25
|
+
var ev seqEvent
|
|
26
|
+
err := json.Unmarshal(line, &ev)
|
|
27
|
+
return ev, err
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// writeLog 写一份 fixture 会话日志,返回目录。
|
|
31
|
+
func writeLog(t *testing.T, id string, lines ...string) string {
|
|
32
|
+
t.Helper()
|
|
33
|
+
dir := t.TempDir()
|
|
34
|
+
if len(lines) > 0 {
|
|
35
|
+
body := ""
|
|
36
|
+
for _, l := range lines {
|
|
37
|
+
body += l + "\n"
|
|
38
|
+
}
|
|
39
|
+
if err := os.WriteFile(filepath.Join(dir, id+".jsonl"), []byte(body), 0o600); err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return dir
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestReadSessionLogAfterFiltersBySeq(t *testing.T) {
|
|
47
|
+
dir := writeLog(t, "s", `{"seq":1,"text":"a"}`, `{"seq":2,"text":"b"}`, `{"seq":3,"text":"c"}`)
|
|
48
|
+
|
|
49
|
+
// after=1 → 只回 2、3(排他)。
|
|
50
|
+
evs, maxSeq, err := ReadSessionLogAfter(dir, "s", 1, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
51
|
+
if err != nil {
|
|
52
|
+
t.Fatal(err)
|
|
53
|
+
}
|
|
54
|
+
if len(evs) != 2 || evs[0].Seq != 2 || evs[1].Seq != 3 {
|
|
55
|
+
t.Fatalf("after=1 期望 [2 3],实得 %+v", evs)
|
|
56
|
+
}
|
|
57
|
+
if maxSeq != 3 {
|
|
58
|
+
t.Fatalf("maxSeq 期望 3,实得 %d", maxSeq)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// after=0 → 全量(seq 从 1 起,所以 0 等价于「没过滤」)。
|
|
62
|
+
evs, _, err = ReadSessionLogAfter(dir, "s", 0, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
63
|
+
if err != nil {
|
|
64
|
+
t.Fatal(err)
|
|
65
|
+
}
|
|
66
|
+
if len(evs) != 3 {
|
|
67
|
+
t.Fatalf("after=0 期望 3 条,实得 %d", len(evs))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// TestReadSessionLogAfterMaxSeqCoversWholeFile 是这一组里最要紧的一条:
|
|
72
|
+
// maxSeq 必须反映**整个文件**,不能只统计返回的那一页。
|
|
73
|
+
//
|
|
74
|
+
// 为什么:调用方用 `after > maxSeq` 判「客户端游标超前了(日志被重置 / 游标来自
|
|
75
|
+
// 别的会话)」,超时时得以全量重建。若 maxSeq 只统计过滤结果,after=99 这类超前
|
|
76
|
+
// 情形会得到 maxSeq=0,判据恒为真 —— 客户端会被永久判成「超前」,每次进会话都
|
|
77
|
+
// 重拉全量,增量能力形同虚设。
|
|
78
|
+
func TestReadSessionLogAfterMaxSeqCoversWholeFile(t *testing.T) {
|
|
79
|
+
dir := writeLog(t, "s", `{"seq":1,"text":"a"}`, `{"seq":7,"text":"b"}`)
|
|
80
|
+
|
|
81
|
+
// after 超前(99 > 7):这一页是空的,但 maxSeq 仍须是 7。
|
|
82
|
+
evs, maxSeq, err := ReadSessionLogAfter(dir, "s", 99, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
83
|
+
if err != nil {
|
|
84
|
+
t.Fatal(err)
|
|
85
|
+
}
|
|
86
|
+
if len(evs) != 0 {
|
|
87
|
+
t.Fatalf("after=99 期望空页,实得 %+v", evs)
|
|
88
|
+
}
|
|
89
|
+
if maxSeq != 7 {
|
|
90
|
+
t.Fatalf("超前时 maxSeq 仍须覆盖整个文件(期望 7,实得 %d)", maxSeq)
|
|
91
|
+
}
|
|
92
|
+
if !(99 > maxSeq) {
|
|
93
|
+
t.Fatalf("判据 `after > maxSeq` 应为 true(客户端超前),maxSeq=%d", maxSeq)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 追平(after == maxSeq):空页,且判据为 false(这是正常的「没有新事件」)。
|
|
97
|
+
evs, maxSeq, err = ReadSessionLogAfter(dir, "s", 7, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
98
|
+
if err != nil {
|
|
99
|
+
t.Fatal(err)
|
|
100
|
+
}
|
|
101
|
+
if len(evs) != 0 {
|
|
102
|
+
t.Fatalf("已追平期望空页,实得 %+v", evs)
|
|
103
|
+
}
|
|
104
|
+
if 7 > maxSeq {
|
|
105
|
+
t.Fatalf("追平时不该判成超前(maxSeq=%d)", maxSeq)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// TestReadSessionLogAfterMissingFile 没历史不是错(新会话 / 从未落盘)。
|
|
110
|
+
func TestReadSessionLogAfterMissingFile(t *testing.T) {
|
|
111
|
+
dir := t.TempDir()
|
|
112
|
+
evs, maxSeq, err := ReadSessionLogAfter(dir, "nope", 3, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
113
|
+
if err != nil {
|
|
114
|
+
t.Fatalf("文件不存在不该报错,实得 %v", err)
|
|
115
|
+
}
|
|
116
|
+
if evs != nil || maxSeq != 0 {
|
|
117
|
+
t.Fatalf("期望 (nil, 0),实得 (%+v, %d)", evs, maxSeq)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// TestReadSessionLogAfterNilSeqOf 不给 seqOf 时退化成全量(且 maxSeq 恒 0)。
|
|
122
|
+
//
|
|
123
|
+
// 这两个参数是配套的:seqOf 为 nil 就没法过滤,after 无意义。
|
|
124
|
+
func TestReadSessionLogAfterNilSeqOf(t *testing.T) {
|
|
125
|
+
dir := writeLog(t, "s", `{"seq":1,"text":"a"}`, `{"seq":2,"text":"b"}`)
|
|
126
|
+
evs, maxSeq, err := ReadSessionLogAfter(dir, "s", 5, nil, parseSeqEvent)
|
|
127
|
+
if err != nil {
|
|
128
|
+
t.Fatal(err)
|
|
129
|
+
}
|
|
130
|
+
if len(evs) != 2 || maxSeq != 0 {
|
|
131
|
+
t.Fatalf("seqOf=nil 期望全量 2 条 + maxSeq=0,实得 %d 条 / maxSeq=%d", len(evs), maxSeq)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// TestReadSessionLogAfterSkipsCorruptLines 坏行不该让整次续读失败。
|
|
136
|
+
func TestReadSessionLogAfterSkipsCorruptLines(t *testing.T) {
|
|
137
|
+
dir := writeLog(t, "s", `{"seq":1,"text":"a"}`, `{ this is not json`, `{"seq":3,"text":"c"}`)
|
|
138
|
+
evs, maxSeq, err := ReadSessionLogAfter(dir, "s", 1, func(e seqEvent) uint64 { return e.Seq }, parseSeqEvent)
|
|
139
|
+
if err != nil {
|
|
140
|
+
t.Fatalf("坏行不该报错,实得 %v", err)
|
|
141
|
+
}
|
|
142
|
+
if len(evs) != 1 || evs[0].Seq != 3 {
|
|
143
|
+
t.Fatalf("期望只回 seq=3,实得 %+v", evs)
|
|
144
|
+
}
|
|
145
|
+
if maxSeq != 3 {
|
|
146
|
+
t.Fatalf("maxSeq 期望 3(坏行跳过但正常行仍算),实得 %d", maxSeq)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// TestReadSessionLogAfterEmptyDirRejected 空 dir 必须报错。
|
|
151
|
+
//
|
|
152
|
+
// 这是防「默认进 home 写/读错路径」那类难追的 bug:调用方必须显式给目录。
|
|
153
|
+
func TestReadSessionLogAfterEmptyDirRejected(t *testing.T) {
|
|
154
|
+
if _, _, err := ReadSessionLogAfter("", "s", 0, nil, parseSeqEvent); err == nil {
|
|
155
|
+
t.Fatal("空 dir 期望报错,实得 nil")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/npm/install.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// install.js - postinstall 两件事:
|
|
5
|
+
//
|
|
6
|
+
// 1) magic-agent 二进制兜底:
|
|
7
|
+
// 1a) 当前平台已有预编译二进制 -> 直接通过(正常路径)
|
|
8
|
+
// 1b) 没有但有 go -> 现场编译(首次 npm install 于非预打包平台时有用)
|
|
9
|
+
// 1c) 都没有 -> 只告警,不让安装失败(用户仍可自行 go build)
|
|
10
|
+
//
|
|
11
|
+
// 2) llm 引擎依赖:安装 simonw/LLM CLI(https://github.com/simonw/LLM)
|
|
12
|
+
// 探测顺序(找到就跳过安装):
|
|
13
|
+
// MAGIC_AGENT_LLM_BIN > ~/.llm-venv/bin/llm > PATH(llm) > brew
|
|
14
|
+
// 都没有时建 ~/.llm-venv(pipx 风格隔离,不污染系统 Python):
|
|
15
|
+
// python3 -m venv ~/.llm-venv && ~/.llm-venv/bin/pip install llm
|
|
16
|
+
// 安装失败只告警(-e llm 不可用,其他引擎不受影响)。
|
|
17
|
+
|
|
18
|
+
const { execFileSync, execSync } = require("child_process");
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const os = require("os");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
|
|
23
|
+
const { currentTarget, binaryPath, pkgRoot, findGo } = require("./lib/platform");
|
|
24
|
+
|
|
25
|
+
// ── 1) magic-agent 二进制 ──────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const target = currentTarget();
|
|
28
|
+
const out = binaryPath(target);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(out)) {
|
|
31
|
+
const go = findGo();
|
|
32
|
+
if (go) {
|
|
33
|
+
const pkg = require(path.join(pkgRoot, "package.json"));
|
|
34
|
+
process.stdout.write(`magic-agent: 当前平台 ${target.dir} 无预编译产物,尝试用 go 现场编译 ... `);
|
|
35
|
+
try {
|
|
36
|
+
execFileSync(
|
|
37
|
+
go,
|
|
38
|
+
["build", "-trimpath", "-ldflags", `-s -w -X github.com/darren/magic-agent/internal/cli.Version=${pkg.version}`, "-o", out, "./cmd/magic-agent"],
|
|
39
|
+
{
|
|
40
|
+
cwd: pkgRoot,
|
|
41
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
42
|
+
env: { ...process.env, CGO_ENABLED: "0", GOOS: target.goos, GOARCH: target.goarch },
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
fs.chmodSync(out, 0o755);
|
|
46
|
+
console.log("ok");
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log(`失败: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
process.stderr.write(
|
|
52
|
+
`magic-agent: 警告 - 当前平台 ${target.dir} 缺少预编译二进制,且本机未找到 go,命令可能无法运行。\n`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 2) llm CLI(simonw/LLM)────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function haveLlm() {
|
|
60
|
+
if (process.env.MAGIC_AGENT_LLM_BIN) return process.env.MAGIC_AGENT_LLM_BIN;
|
|
61
|
+
const venv = path.join(os.homedir(), ".llm-venv", "bin", "llm");
|
|
62
|
+
if (fs.existsSync(venv)) return venv;
|
|
63
|
+
try {
|
|
64
|
+
return execFileSync("llm", ["--version"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim() && "llm";
|
|
65
|
+
} catch (_) {
|
|
66
|
+
/* PATH 里没有 */
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function installLlm() {
|
|
72
|
+
const home = os.homedir();
|
|
73
|
+
const venvBin = path.join(home, ".llm-venv", "bin");
|
|
74
|
+
|
|
75
|
+
// 找一个能用的 python3(python3 > python;都不行时放弃)。
|
|
76
|
+
let py = null;
|
|
77
|
+
for (const c of ["python3", "python"]) {
|
|
78
|
+
try {
|
|
79
|
+
execFileSync(c, ["--version"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
80
|
+
py = c;
|
|
81
|
+
break;
|
|
82
|
+
} catch (_) {
|
|
83
|
+
/* 下一个 */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!py) return "未找到可用的 python3";
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
process.stdout.write("magic-agent: 安装 llm CLI (simonw/LLM) 到 ~/.llm-venv ... ");
|
|
90
|
+
const env = { ...process.env };
|
|
91
|
+
// Homebrew Python pip 可能因 PIP_BREAK_SYSTEM_PACKAGES / truststore 报错,
|
|
92
|
+
// venv 内不需要这些;静音升级提示。
|
|
93
|
+
delete env.PIP_BREAK_SYSTEM_PACKAGES;
|
|
94
|
+
execFileSync(py, ["-m", "venv", path.join(home, ".llm-venv")], { stdio: ["ignore", "ignore", "pipe"] });
|
|
95
|
+
execFileSync(
|
|
96
|
+
path.join(venvBin, "pip"),
|
|
97
|
+
["install", "--quiet", "--disable-pip-version-check", "llm"],
|
|
98
|
+
{ stdio: ["ignore", "ignore", "pipe"], env }
|
|
99
|
+
);
|
|
100
|
+
console.log("ok");
|
|
101
|
+
return null;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.log(`失败: ${err.message.split("\n")[0]}`);
|
|
104
|
+
return err.message.split("\n")[0];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!haveLlm()) {
|
|
109
|
+
const why = installLlm();
|
|
110
|
+
if (why) {
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`magic-agent: 警告 - llm CLI 安装失败(${why})。-e llm 引擎不可用;claude/codebuddy/trae 不受影响。\n` +
|
|
113
|
+
`手动安装:pip install llm 或 pipx install llm,然后用 MAGIC_AGENT_LLM_BIN 指向可执行文件。\n`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// 已有 llm:静默通过(安装输出越少越好)。
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// postinstall 永不阻断安装。
|
|
121
|
+
process.exit(0);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// platform.js - node 平台标识 -> Go 交叉编译目标 / 产物路径 的统一映射。
|
|
4
|
+
//
|
|
5
|
+
// 约定:dist/<goos>-<goarch>/magic-agent[.exe]
|
|
6
|
+
// darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64
|
|
7
|
+
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
// 构建目标表(顺序即 build.js 的输出顺序)。
|
|
11
|
+
const TARGETS = [
|
|
12
|
+
{ dir: "darwin-arm64", goos: "darwin", goarch: "arm64" },
|
|
13
|
+
{ dir: "darwin-x64", goos: "darwin", goarch: "amd64" },
|
|
14
|
+
{ dir: "linux-x64", goos: "linux", goarch: "amd64" },
|
|
15
|
+
{ dir: "linux-arm64", goos: "linux", goarch: "arm64" },
|
|
16
|
+
{ dir: "win32-x64", goos: "windows", goarch: "amd64" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const GOOS_BY_NODE = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
20
|
+
|
|
21
|
+
/** 当前进程对应的 Go 目标 */
|
|
22
|
+
function currentTarget() {
|
|
23
|
+
return {
|
|
24
|
+
dir: `${process.platform}-${process.arch}`,
|
|
25
|
+
goos: GOOS_BY_NODE[process.platform],
|
|
26
|
+
goarch: process.arch === "x64" ? "amd64" : process.arch,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function exeName(goos) {
|
|
31
|
+
return goos === "windows" ? "magic-agent.exe" : "magic-agent";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 包根目录(npm/ 的上一级) */
|
|
35
|
+
const pkgRoot = path.resolve(__dirname, "..", "..");
|
|
36
|
+
|
|
37
|
+
/** 二进制输出目录 */
|
|
38
|
+
const distDir = path.join(pkgRoot, "npm", "dist");
|
|
39
|
+
|
|
40
|
+
/** 某个目标产物的绝对路径 */
|
|
41
|
+
function binaryPath(target) {
|
|
42
|
+
return path.join(distDir, target.dir, exeName(target.goos));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 定位可用的 go 可执行文件。
|
|
47
|
+
* 顺序:MAGIC_AGENT_GO_BIN > PATH > homebrew > /usr/local/go > 常见版本目录。
|
|
48
|
+
*/
|
|
49
|
+
function findGo() {
|
|
50
|
+
const fs = require("fs");
|
|
51
|
+
const candidates = [];
|
|
52
|
+
if (process.env.MAGIC_AGENT_GO_BIN) candidates.push(process.env.MAGIC_AGENT_GO_BIN);
|
|
53
|
+
candidates.push("go");
|
|
54
|
+
candidates.push("/opt/homebrew/bin/go", "/usr/local/go/bin/go", "/usr/local/bin/go");
|
|
55
|
+
for (const c of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
if (c === "go") {
|
|
58
|
+
const { execFileSync } = require("child_process");
|
|
59
|
+
execFileSync("go", ["version"], { stdio: "ignore" });
|
|
60
|
+
return "go";
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(c)) return c;
|
|
63
|
+
} catch (_) {
|
|
64
|
+
/* 继续找下一个 */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
TARGETS,
|
|
72
|
+
currentTarget,
|
|
73
|
+
exeName,
|
|
74
|
+
pkgRoot,
|
|
75
|
+
distDir,
|
|
76
|
+
binaryPath,
|
|
77
|
+
findGo,
|
|
78
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "magic-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw 引擎",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"magic-agent": "npm/bin/magic-agent.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node npm/build.js",
|
|
11
|
+
"build:current": "node npm/build.js --current",
|
|
12
|
+
"bump": "node npm/bump.js",
|
|
13
|
+
"prepack": "node npm/build.js",
|
|
14
|
+
"postinstall": "node npm/install.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"npm/bin/",
|
|
18
|
+
"npm/lib/",
|
|
19
|
+
"npm/dist/",
|
|
20
|
+
"npm/install.js",
|
|
21
|
+
"cmd/",
|
|
22
|
+
"internal/",
|
|
23
|
+
"go.mod",
|
|
24
|
+
"go.sum",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"os": [
|
|
32
|
+
"darwin",
|
|
33
|
+
"linux",
|
|
34
|
+
"win32"
|
|
35
|
+
]
|
|
36
|
+
}
|