agentworkshop 0.7.50 → 0.7.51
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/912eb5bb-670e-4cc8-b21d-7fc097723466.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +957 -957
- package/.output/server/package.json +1 -1
- package/README-zh.md +829 -829
- package/README.md +839 -839
- package/config.yml +1 -1
- package/docs/cli.en.md +447 -447
- package/docs/cli.md +412 -412
- package/docs/mcp.en.md +27 -0
- package/docs/mcp.md +108 -0
- package/docs/plugins.en.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/mcp/aw-mcp-server.mjs +796 -0
- package/mcp/discovery.mjs +227 -0
- package/package.json +4 -1
- package/.output/public/_nuxt/builds/meta/08ba670b-7512-426b-a0ae-b10cbeb1c985.json +0 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/discovery.mjs —— AgentWorkShop 运行实例的端口自动发现。
|
|
3
|
+
*
|
|
4
|
+
* 发现优先级(逐级探测,GET /api/health 返回 data.status==='ok' 才算命中):
|
|
5
|
+
* 1. env AW_BASE_URL 显式指定平台地址(如 http://127.0.0.1:3005)
|
|
6
|
+
* 2. env AW_PORT 显式钉住端口(多实例并存时消歧)
|
|
7
|
+
* 3. 锁文件 .runtime/aw.lock { pid, port, mode }——scripts/start.mjs 端口顺延后回写,
|
|
8
|
+
* 是"实例真实监听端口"的唯一权威记录。依次找:
|
|
9
|
+
* a. cwd 向上逐级找 .AgentWorkShop/.runtime/aw.lock(repo 级)
|
|
10
|
+
* b. env AW_HOME/.runtime/aw.lock 与 AW_HOME/.AgentWorkShop/.runtime/aw.lock
|
|
11
|
+
* c. ~/.AgentWorkShop/.runtime/aw.lock(home 级)
|
|
12
|
+
* 4. env PORT / NUXT_PORT 启动惯例变量(scripts/start.mjs 同款优先级)
|
|
13
|
+
* 5. config.yml server.dev.port / server.prod.port(轻量正则,不引 YAML 依赖)
|
|
14
|
+
* 6. 默认端口 3000(dev)/ 3001(prod)
|
|
15
|
+
*
|
|
16
|
+
* 刻意零依赖:本模块被 stdio MCP server(cli 子进程)与 CLI 命令共享,
|
|
17
|
+
* 不得假设仓库 node_modules 的解析路径。
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
|
+
import { homedir } from 'node:os'
|
|
21
|
+
import { dirname, join, resolve } from 'node:path'
|
|
22
|
+
|
|
23
|
+
const LOCK_REL = join('.AgentWorkShop', '.runtime', 'aw.lock')
|
|
24
|
+
const HEALTH_PATH = '/api/health'
|
|
25
|
+
const HEALTH_TIMEOUT_MS = 800
|
|
26
|
+
|
|
27
|
+
/** cwd 向上逐级查找含 target 相对路径的目录;找不到返回 null */
|
|
28
|
+
export function walkUpFind(cwd, target) {
|
|
29
|
+
let dir = resolve(cwd)
|
|
30
|
+
for (;;) {
|
|
31
|
+
const hit = join(dir, target)
|
|
32
|
+
if (existsSync(hit)) return hit
|
|
33
|
+
const parent = dirname(dir)
|
|
34
|
+
if (parent === dir) return null
|
|
35
|
+
dir = parent
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 读锁文件 → { pid, port, mode, startedAt } | null(损坏/越界端口一律忽略) */
|
|
40
|
+
export function readLock(lockPath) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(readFileSync(lockPath, 'utf8'))
|
|
43
|
+
const port = Number(raw?.port)
|
|
44
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) return null
|
|
45
|
+
return { pid: raw?.pid ?? null, port, mode: raw?.mode ?? null, startedAt: raw?.startedAt ?? null }
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** GET /api/health 探活;命中返回 data(含 status/app/version/mode),未命中返回 null */
|
|
53
|
+
export async function probeHealth(base, timeoutMs = HEALTH_TIMEOUT_MS) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(`${base.replace(/\/$/, '')}${HEALTH_PATH}`, { signal: AbortSignal.timeout(timeoutMs) })
|
|
56
|
+
if (!res.ok) return null
|
|
57
|
+
const json = await res.json().catch(() => null)
|
|
58
|
+
const data = json?.data
|
|
59
|
+
return data?.status === 'ok' ? data : null
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pushCandidate(list, seen, cand) {
|
|
67
|
+
const key = cand.base ?? `port:${cand.port}`
|
|
68
|
+
if (seen.has(key)) return
|
|
69
|
+
seen.add(key)
|
|
70
|
+
list.push(cand)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 轻量解析 config.yml 的 server.dev.port / server.prod.port(嵌套格式,零依赖)。
|
|
75
|
+
* 只关心 server: 块下 dev:/prod: 子块的 port:,不引 YAML 依赖。
|
|
76
|
+
*/
|
|
77
|
+
export function parseServerPorts(text) {
|
|
78
|
+
const ports = {}
|
|
79
|
+
let inServer = false
|
|
80
|
+
let sub = null
|
|
81
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
82
|
+
const t = line.trim()
|
|
83
|
+
if (!t || t.startsWith('#')) continue
|
|
84
|
+
const indent = line.length - line.trimStart().length
|
|
85
|
+
if (indent === 0) {
|
|
86
|
+
inServer = t === 'server:'
|
|
87
|
+
sub = null
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (!inServer) continue
|
|
91
|
+
if (indent <= 2) {
|
|
92
|
+
sub = /^(dev|prod):$/.test(t) ? t.slice(0, -1) : null
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (sub && t.startsWith('port:')) {
|
|
96
|
+
const p = Number(t.slice(5).trim())
|
|
97
|
+
if (Number.isInteger(p) && p > 0) ports[sub] = p
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return ports
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 组装候选(按优先级,不去重探测)。返回 [{ kind, source, base?, port? }]
|
|
105
|
+
* kind: 'url' | 'lock' | 'envport' | 'config' | 'default'
|
|
106
|
+
*/
|
|
107
|
+
export function buildCandidates({ cwd = process.cwd(), env = process.env, defaults } = {}) {
|
|
108
|
+
const cands = []
|
|
109
|
+
const seen = new Set()
|
|
110
|
+
const envUrl = env.AW_BASE_URL?.trim()
|
|
111
|
+
if (envUrl) pushCandidate(cands, seen, { kind: 'url', source: 'env:AW_BASE_URL', base: envUrl.replace(/\/$/, '') })
|
|
112
|
+
|
|
113
|
+
const pinPort = Number(env.AW_PORT)
|
|
114
|
+
if (Number.isInteger(pinPort) && pinPort > 0) pushCandidate(cands, seen, { kind: 'envport', source: 'env:AW_PORT', port: pinPort })
|
|
115
|
+
|
|
116
|
+
const lockPaths = []
|
|
117
|
+
const awHome = env.AW_HOME?.trim()
|
|
118
|
+
const repoLock = walkUpFind(cwd, LOCK_REL)
|
|
119
|
+
if (repoLock) lockPaths.push({ path: repoLock, source: `lock:${repoLock}` })
|
|
120
|
+
if (awHome) {
|
|
121
|
+
// 显式 AW_HOME = 操作者指定的配置根:优先于隐式的 cwd 向上发现,
|
|
122
|
+
// 且不再回落全局 ~/.AgentWorkShop(多实例隔离语义)
|
|
123
|
+
lockPaths.unshift(
|
|
124
|
+
{ path: join(resolve(awHome), '.runtime', 'aw.lock'), source: `lock(AW_HOME):${awHome}` },
|
|
125
|
+
{ path: join(resolve(awHome), LOCK_REL), source: `lock(AW_HOME):${join(awHome, '.AgentWorkShop')}` },
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const homeLock = join(homedir(), LOCK_REL)
|
|
130
|
+
lockPaths.push({ path: homeLock, source: `lock:${homeLock}` })
|
|
131
|
+
}
|
|
132
|
+
for (const { path, source } of lockPaths) {
|
|
133
|
+
const lock = existsSync(path) ? readLock(path) : null
|
|
134
|
+
if (lock) pushCandidate(cands, seen, { kind: 'lock', source, port: lock.port, lock })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const key of ['PORT', 'NUXT_PORT']) {
|
|
138
|
+
const p = Number(env[key])
|
|
139
|
+
if (Number.isInteger(p) && p > 0) pushCandidate(cands, seen, { kind: 'envport', source: `env:${key}`, port: p })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const configYaml = walkUpFind(cwd, 'config.yml')
|
|
143
|
+
if (configYaml) {
|
|
144
|
+
try {
|
|
145
|
+
const ports = parseServerPorts(readFileSync(configYaml, 'utf8'))
|
|
146
|
+
if (ports.dev) pushCandidate(cands, seen, { kind: 'config', source: `config:server.dev.port(${configYaml})`, port: ports.dev })
|
|
147
|
+
if (ports.prod) pushCandidate(cands, seen, { kind: 'config', source: `config:server.prod.port(${configYaml})`, port: ports.prod })
|
|
148
|
+
}
|
|
149
|
+
catch { /* 读不了就跳过 */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const p of defaults ?? ['3000', '3001']) {
|
|
153
|
+
const port = Number(p)
|
|
154
|
+
if (Number.isInteger(port) && port > 0) pushCandidate(cands, seen, { kind: 'default', source: `default:${port}`, port })
|
|
155
|
+
}
|
|
156
|
+
return cands
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 自动发现运行中的 AgentWorkShop 实例。
|
|
161
|
+
* 返回 { base, port|null, source, health, candidates } | null。
|
|
162
|
+
* candidates 里每项附 ok 与 health,便于 aw_status/诊断展示"为什么没连上"。
|
|
163
|
+
*/
|
|
164
|
+
export async function discoverInstance(opts = {}) {
|
|
165
|
+
const timeoutMs = opts.timeoutMs ?? HEALTH_TIMEOUT_MS
|
|
166
|
+
const cands = buildCandidates(opts)
|
|
167
|
+
const probed = []
|
|
168
|
+
for (const cand of cands) {
|
|
169
|
+
const base = cand.base ?? `http://127.0.0.1:${cand.port}`
|
|
170
|
+
const health = await probeHealth(base, timeoutMs)
|
|
171
|
+
probed.push({ ...cand, base, ok: health != null, health })
|
|
172
|
+
if (health) {
|
|
173
|
+
return { base, port: cand.port ?? (Number(new URL(base).port) || null), source: cand.source, health, candidates: probed }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 供 aw_status 展示:只探测不命中也算信息(列出全部候选与失败原因形态) */
|
|
180
|
+
export async function discoverWithReport(opts = {}) {
|
|
181
|
+
const cands = buildCandidates(opts)
|
|
182
|
+
const probed = []
|
|
183
|
+
for (const cand of cands) {
|
|
184
|
+
const base = cand.base ?? `http://127.0.0.1:${cand.port}`
|
|
185
|
+
const health = await probeHealth(base, opts.timeoutMs ?? HEALTH_TIMEOUT_MS)
|
|
186
|
+
probed.push({ source: cand.source, base, ok: health != null, health: health ?? null })
|
|
187
|
+
}
|
|
188
|
+
const hit = probed.find(p => p.ok)
|
|
189
|
+
return { hit: hit ?? null, candidates: probed }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 解析本实例的配置根(与锁文件发现同优先级):
|
|
194
|
+
* env AW_HOME > cwd 向上找 .AgentWorkShop > ~/.AgentWorkShop
|
|
195
|
+
*/
|
|
196
|
+
export function findConfigRoot(opts = {}) {
|
|
197
|
+
const env = opts.env ?? process.env
|
|
198
|
+
const cwd = opts.cwd ?? process.cwd()
|
|
199
|
+
const awHome = env.AW_HOME?.trim()
|
|
200
|
+
if (awHome) return resolve(awHome)
|
|
201
|
+
const repoRoot = walkUpFind(cwd, join('.AgentWorkShop', '.runtime', 'aw.lock'))
|
|
202
|
+
if (repoRoot) return dirname(dirname(repoRoot))
|
|
203
|
+
const repoRootDir = walkUpFind(cwd, join('.AgentWorkShop', 'runtime-settings.json'))
|
|
204
|
+
if (repoRootDir) return dirname(repoRootDir)
|
|
205
|
+
return join(homedir(), '.AgentWorkShop')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 读 MCP 集成开关(系统设置 → MCP 集成 → 启动 MCP 集成)。
|
|
210
|
+
* 优先级与平台一致:env AW_MCP_ENABLED > <configRoot>/runtime-settings.json 的 overrides["mcp.enabled"] > 默认 false。
|
|
211
|
+
* 文件缺失/损坏一律视为 false(fail-closed:开关语义宁可拒绝)。
|
|
212
|
+
*/
|
|
213
|
+
export function readMcpEnabled(opts = {}) {
|
|
214
|
+
const env = opts.env ?? process.env
|
|
215
|
+
const envVal = env.AW_MCP_ENABLED?.trim()
|
|
216
|
+
if (envVal !== undefined && envVal !== '') {
|
|
217
|
+
return /^(1|true|on|yes)$/i.test(envVal)
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const root = opts.configRoot ?? findConfigRoot(opts)
|
|
221
|
+
const raw = JSON.parse(readFileSync(join(root, 'runtime-settings.json'), 'utf8'))
|
|
222
|
+
return raw?.overrides?.['mcp.enabled'] === true
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentworkshop",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgentWorkShop \u8f6f\u4ef6\u5f00\u53d1\u7cfb\u7edf - \u57fa\u4e8e Nuxt 4 \u7684\u914d\u7f6e\u9a71\u52a8\u8fd0\u884c\u8303\u5f0f",
|
|
6
6
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"scripts",
|
|
37
37
|
".AgentWorkShop/prompts",
|
|
38
38
|
"server/plugins-builtin",
|
|
39
|
+
"mcp",
|
|
39
40
|
"config.yml",
|
|
40
41
|
"nuxt.config.ts",
|
|
41
42
|
"tsconfig.json",
|
|
@@ -50,6 +51,8 @@
|
|
|
50
51
|
"docs/sdk.md",
|
|
51
52
|
"docs/sdk.en.md",
|
|
52
53
|
"docs/aml.md",
|
|
54
|
+
"docs/mcp.md",
|
|
55
|
+
"docs/mcp.en.md",
|
|
53
56
|
"docs/tui.md",
|
|
54
57
|
"docs/full-test-plan.md",
|
|
55
58
|
"docs/multi-harness-architecture.md",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"id":"08ba670b-7512-426b-a0ae-b10cbeb1c985","timestamp":1790570910812,"prerendered":[]}
|