@ai-zen/agents-host 0.1.0-alpha.1 → 0.1.0-alpha.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/dist/app.d.ts +8 -1
- package/dist/app.js +51 -4
- package/dist/config.d.ts +6 -3
- package/dist/config.js +42 -19
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/zh.d.ts +1 -0
- package/dist/i18n/zh.js +1 -0
- package/dist/runtime/ChatRunner.js +2 -2
- package/dist/services/ConfigService.js +6 -1
- package/dist/services/ConversationService.d.ts +2 -2
- package/dist/services/ConversationService.js +7 -16
- package/dist/services/DiscoveryService.d.ts +18 -5
- package/dist/services/DiscoveryService.js +43 -27
- package/dist/services/ModelService.js +4 -4
- package/dist/services/QuestionService.d.ts +3 -0
- package/dist/services/QuestionService.js +3 -35
- package/package.json +2 -2
package/dist/app.d.ts
CHANGED
|
@@ -211,6 +211,13 @@ export declare class HostApp {
|
|
|
211
211
|
/** R50 的那条警告只说一次(描述的是装配能力,不是某一轮的错) */
|
|
212
212
|
private handoffWarningSent;
|
|
213
213
|
private constructor();
|
|
214
|
+
/**
|
|
215
|
+
* 报一条「配置文件坏了、已恢复出厂」(D32)。
|
|
216
|
+
*
|
|
217
|
+
* 恢复动作本身在读到坏文件那一刻就做完了(写回 + 用出厂配置),这里只负责让用户知道
|
|
218
|
+
* 自己被重置过 —— warning 是 PRD F-J2 要求上通知栏的那一档。
|
|
219
|
+
*/
|
|
220
|
+
private noticeRecoveredConfig;
|
|
214
221
|
/**
|
|
215
222
|
* 造一个 `HostApp`(不监听)。
|
|
216
223
|
*
|
|
@@ -327,7 +334,7 @@ export declare class HostApp {
|
|
|
327
334
|
private createConversationRuntime;
|
|
328
335
|
/** 素材转换要的实发模型与端点(F-H7 的判定依据:模型能不能看图、端点有没有 Files API) */
|
|
329
336
|
private resolveModelContext;
|
|
330
|
-
/**
|
|
337
|
+
/** 读配置(`config.json` 坏了由 SDK 恢复出厂,D32) */
|
|
331
338
|
private readConfig;
|
|
332
339
|
/** 凭证校验:顺手记下「这条进程被用过」(握手成功即算) */
|
|
333
340
|
private buildVerifier;
|
package/dist/app.js
CHANGED
|
@@ -86,6 +86,32 @@ export function readDiskVersion() {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
// ---------------------------------------------------------------------------
|
|
89
|
+
// 配置文件恢复出厂的通知口
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
/**
|
|
92
|
+
* 配置文件坏了被恢复出厂时的通知口(D32)。
|
|
93
|
+
*
|
|
94
|
+
* `host.json` 与 `config.json` 的首次读都在通知服务之前(端口要在开库之前定下来,
|
|
95
|
+
* `bootstrap()` 在造对象之前),装配期发生的恢复先攒在这里;`HostApp` 造好之后 `attach()`
|
|
96
|
+
* 接上真正的通知,攒下的补发完,此后每恢复一次当场发一次。
|
|
97
|
+
*/
|
|
98
|
+
class ConfigRecoverySpool {
|
|
99
|
+
queued = [];
|
|
100
|
+
sink = null;
|
|
101
|
+
/** 交给 `readHostSettings` / `ConfigManager` 的那个回调 */
|
|
102
|
+
onRecovered = (path) => {
|
|
103
|
+
if (this.sink !== null)
|
|
104
|
+
return this.sink(path);
|
|
105
|
+
this.queued.push(path);
|
|
106
|
+
};
|
|
107
|
+
/** 接上通知口,并把攒下的那些补发完 */
|
|
108
|
+
async attach(sink) {
|
|
109
|
+
this.sink = sink;
|
|
110
|
+
for (const path of this.queued.splice(0))
|
|
111
|
+
await sink(path);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
89
115
|
// HostApp
|
|
90
116
|
// ---------------------------------------------------------------------------
|
|
91
117
|
export class HostApp {
|
|
@@ -425,6 +451,20 @@ export class HostApp {
|
|
|
425
451
|
});
|
|
426
452
|
this.hubForPush = this.hub;
|
|
427
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* 报一条「配置文件坏了、已恢复出厂」(D32)。
|
|
456
|
+
*
|
|
457
|
+
* 恢复动作本身在读到坏文件那一刻就做完了(写回 + 用出厂配置),这里只负责让用户知道
|
|
458
|
+
* 自己被重置过 —— warning 是 PRD F-J2 要求上通知栏的那一档。
|
|
459
|
+
*/
|
|
460
|
+
async noticeRecoveredConfig(path) {
|
|
461
|
+
await this.noticeService.create({
|
|
462
|
+
level: "warning",
|
|
463
|
+
code: "notice.configRecovered",
|
|
464
|
+
params: { path },
|
|
465
|
+
duration: null,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
428
468
|
/**
|
|
429
469
|
* 造一个 `HostApp`(不监听)。
|
|
430
470
|
*
|
|
@@ -434,8 +474,11 @@ export class HostApp {
|
|
|
434
474
|
*/
|
|
435
475
|
static async create(options = {}) {
|
|
436
476
|
const paths = resolvePaths(options.aiZenDir);
|
|
477
|
+
// 配置文件坏了就恢复出厂(D32):恢复过的文件名先攒着,等通知服务起来再补一条 warning
|
|
478
|
+
const recoveries = new ConfigRecoverySpool();
|
|
437
479
|
// D25 的中间那一档:只在这一档真的要被用到时才读(参数给了就不必读,坏文件也不该拦住显式端口)
|
|
438
|
-
const configPort = options.configPort ??
|
|
480
|
+
const configPort = options.configPort ??
|
|
481
|
+
(options.port === undefined ? (await readHostSettings(paths, recoveries.onRecovered)).port : undefined);
|
|
439
482
|
await mkdir(paths.dataRoot, { recursive: true });
|
|
440
483
|
await mkdir(paths.runDir, { recursive: true });
|
|
441
484
|
await mkdir(paths.assetsDir, { recursive: true });
|
|
@@ -447,13 +490,14 @@ export class HostApp {
|
|
|
447
490
|
await db.close();
|
|
448
491
|
throw error;
|
|
449
492
|
}
|
|
450
|
-
const configManager = new ConfigManager(paths.configFile);
|
|
493
|
+
const configManager = new ConfigManager(paths.configFile, recoveries.onRecovered);
|
|
451
494
|
// SDK 口径的首次初始化:目录 + config.json + 默认助手 + 子 Agent + mcp.json + 出厂 Skill
|
|
452
495
|
// + 思考策略表。已有文件不覆盖(用户自定义的助手定义 `custom: true` 原样保留)。
|
|
453
496
|
await configManager.bootstrap();
|
|
454
497
|
const logging = resolveLogger(options, paths);
|
|
498
|
+
let app;
|
|
455
499
|
try {
|
|
456
|
-
|
|
500
|
+
app = new HostApp(options, paths, db, configManager, configPort, logging.logger);
|
|
457
501
|
}
|
|
458
502
|
catch (error) {
|
|
459
503
|
// 构造途中的抛出(端口不合法、接线造不出来)也要把库关掉:worker 线程不该被漏下
|
|
@@ -461,6 +505,9 @@ export class HostApp {
|
|
|
461
505
|
await logging.own?.close();
|
|
462
506
|
throw error;
|
|
463
507
|
}
|
|
508
|
+
// 通知服务就位了:装配期恢复过出厂的那几份配置各补一条 warning
|
|
509
|
+
await recoveries.attach((path) => app.noticeRecoveredConfig(path));
|
|
510
|
+
return app;
|
|
464
511
|
}
|
|
465
512
|
// -------------------------------------------------------------------------
|
|
466
513
|
// 启停
|
|
@@ -919,7 +966,7 @@ export class HostApp {
|
|
|
919
966
|
: undefined;
|
|
920
967
|
return { model: model ?? null, endpoint: endpoint ?? null };
|
|
921
968
|
}
|
|
922
|
-
/**
|
|
969
|
+
/** 读配置(`config.json` 坏了由 SDK 恢复出厂,D32) */
|
|
923
970
|
async readConfig() {
|
|
924
971
|
return await this.configManager.read();
|
|
925
972
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConfigRecoveredHandler } from "@ai-zen/agents-sdk";
|
|
1
2
|
/**
|
|
2
3
|
* 路径与监听的来源(plan §1 的 `config.ts` 契约)。
|
|
3
4
|
*
|
|
@@ -90,8 +91,10 @@ export interface HostSettings {
|
|
|
90
91
|
/**
|
|
91
92
|
* 读 `<dataRoot>/host.json`。
|
|
92
93
|
*
|
|
93
|
-
* 三种情况分开:文件不在 →
|
|
94
|
-
*
|
|
94
|
+
* 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
|
|
95
|
+
* 就是坏文件,恢复出厂:先把坏文件改名留档为 `host.json.broken`、再写回一份合法的 `{}`、
|
|
96
|
+
* 再回调一次(D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件;留档是数据,不是兜底 ——
|
|
97
|
+
* 用户手写的端口设置得能查回来);`port` 在但不是可用端口 →
|
|
95
98
|
* `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
|
|
96
99
|
*/
|
|
97
|
-
export declare function readHostSettings(paths: HostPaths): Promise<HostSettings>;
|
|
100
|
+
export declare function readHostSettings(paths: HostPaths, onRecovered?: ConfigRecoveredHandler): Promise<HostSettings>;
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { access, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { HostError } from "./wire/errors.js";
|
|
@@ -119,11 +119,13 @@ function invalidPort(field, value, source) {
|
|
|
119
119
|
/**
|
|
120
120
|
* 读 `<dataRoot>/host.json`。
|
|
121
121
|
*
|
|
122
|
-
* 三种情况分开:文件不在 →
|
|
123
|
-
*
|
|
122
|
+
* 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
|
|
123
|
+
* 就是坏文件,恢复出厂:先把坏文件改名留档为 `host.json.broken`、再写回一份合法的 `{}`、
|
|
124
|
+
* 再回调一次(D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件;留档是数据,不是兜底 ——
|
|
125
|
+
* 用户手写的端口设置得能查回来);`port` 在但不是可用端口 →
|
|
124
126
|
* `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
|
|
125
127
|
*/
|
|
126
|
-
export async function readHostSettings(paths) {
|
|
128
|
+
export async function readHostSettings(paths, onRecovered) {
|
|
127
129
|
let raw;
|
|
128
130
|
try {
|
|
129
131
|
raw = await readFile(paths.hostConfigFile, "utf8");
|
|
@@ -132,29 +134,17 @@ export async function readHostSettings(paths) {
|
|
|
132
134
|
// 文件不在是正常情况(绝大多数据根上就没有这份文件)
|
|
133
135
|
if (isMissingFile(error))
|
|
134
136
|
return {};
|
|
135
|
-
|
|
136
|
-
messageKey: "config.fileBroken",
|
|
137
|
-
params: { path: paths.hostConfigFile, reason: "unreadable" },
|
|
138
|
-
detail: { path: paths.hostConfigFile, reason: "unreadable" },
|
|
139
|
-
});
|
|
137
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
140
138
|
}
|
|
141
139
|
let parsed;
|
|
142
140
|
try {
|
|
143
141
|
parsed = JSON.parse(raw);
|
|
144
142
|
}
|
|
145
143
|
catch {
|
|
146
|
-
|
|
147
|
-
messageKey: "config.fileBroken",
|
|
148
|
-
params: { path: paths.hostConfigFile, reason: "not_json" },
|
|
149
|
-
detail: { path: paths.hostConfigFile, reason: "not_json" },
|
|
150
|
-
});
|
|
144
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
151
145
|
}
|
|
152
146
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
153
|
-
|
|
154
|
-
messageKey: "config.fileBroken",
|
|
155
|
-
params: { path: paths.hostConfigFile, reason: "not_an_object" },
|
|
156
|
-
detail: { path: paths.hostConfigFile, reason: "not_an_object" },
|
|
157
|
-
});
|
|
147
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
158
148
|
}
|
|
159
149
|
const record = parsed;
|
|
160
150
|
if (!Object.prototype.hasOwnProperty.call(record, "port") || record.port === null)
|
|
@@ -164,6 +154,39 @@ export async function readHostSettings(paths) {
|
|
|
164
154
|
throw invalidPort("port", port, "host.json");
|
|
165
155
|
return { port };
|
|
166
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* 坏掉之后恢复出厂:先把坏文件留档,再写回一份合法的 `{}`(=默认端口、默认语言),再回调一次。
|
|
159
|
+
*/
|
|
160
|
+
async function recoverHostSettings(paths, onRecovered) {
|
|
161
|
+
await archiveBroken(paths.hostConfigFile);
|
|
162
|
+
await writeFile(paths.hostConfigFile, "{}\n", "utf8");
|
|
163
|
+
await onRecovered?.(paths.hostConfigFile);
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 把坏掉的设置文件改名留档为 `host.json.broken`。
|
|
168
|
+
*
|
|
169
|
+
* 与 SDK 的 `ConfigManager.archiveBroken()` 同一条规则(三个配置文件同一口径):同名残骸
|
|
170
|
+
* 已经在就带上时间戳(同一毫秒再撞上就补序号),不覆盖上一份残骸。
|
|
171
|
+
*/
|
|
172
|
+
async function archiveBroken(path) {
|
|
173
|
+
const base = `${path}.broken`;
|
|
174
|
+
let target = base;
|
|
175
|
+
for (let index = 0; await exists(target); index += 1) {
|
|
176
|
+
target = index === 0 ? `${base}.${Date.now()}` : `${base}.${Date.now()}-${index}`;
|
|
177
|
+
}
|
|
178
|
+
await rename(path, target);
|
|
179
|
+
}
|
|
180
|
+
/** 这个路径在不在(`access` 不抛就是在) */
|
|
181
|
+
async function exists(path) {
|
|
182
|
+
try {
|
|
183
|
+
await access(path);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
167
190
|
function isMissingFile(error) {
|
|
168
191
|
return (typeof error === "object" &&
|
|
169
192
|
error !== null &&
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export declare const en: {
|
|
|
34
34
|
readonly "notice.upgradeRestarting": "Updated to {version}. Restarting now.";
|
|
35
35
|
readonly "notice.logWarning": "Host warning: {summary}";
|
|
36
36
|
readonly "notice.logError": "Host error: {summary}";
|
|
37
|
+
readonly "notice.configRecovered": "Config file {path} was broken; factory settings restored.";
|
|
37
38
|
readonly "notice.handoffWritten": "Handoff document written to {path}.";
|
|
38
39
|
readonly "notice.autoNamed": "Named this conversation \"{name}\".";
|
|
39
40
|
readonly "notice.languageChanged": "This connection now speaks {language}.";
|
package/dist/i18n/en.js
CHANGED
|
@@ -39,6 +39,7 @@ export const en = {
|
|
|
39
39
|
"notice.upgradeRestarting": "Updated to {version}. Restarting now.",
|
|
40
40
|
"notice.logWarning": "Host warning: {summary}",
|
|
41
41
|
"notice.logError": "Host error: {summary}",
|
|
42
|
+
"notice.configRecovered": "Config file {path} was broken; factory settings restored.",
|
|
42
43
|
"notice.handoffWritten": "Handoff document written to {path}.",
|
|
43
44
|
"notice.autoNamed": "Named this conversation \"{name}\".",
|
|
44
45
|
"notice.languageChanged": "This connection now speaks {language}.",
|
package/dist/i18n/zh.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare const zh: {
|
|
|
31
31
|
"notice.upgradeRestarting": string;
|
|
32
32
|
"notice.logWarning": string;
|
|
33
33
|
"notice.logError": string;
|
|
34
|
+
"notice.configRecovered": string;
|
|
34
35
|
"notice.handoffWritten": string;
|
|
35
36
|
"notice.autoNamed": string;
|
|
36
37
|
"notice.languageChanged": string;
|
package/dist/i18n/zh.js
CHANGED
|
@@ -35,6 +35,7 @@ export const zh = {
|
|
|
35
35
|
"notice.upgradeRestarting": "已更新到 {version},现在重启。",
|
|
36
36
|
"notice.logWarning": "宿主警告:{summary}",
|
|
37
37
|
"notice.logError": "宿主错误:{summary}",
|
|
38
|
+
"notice.configRecovered": "配置文件 {path} 坏了,已恢复出厂设置。",
|
|
38
39
|
"notice.handoffWritten": "交接文档已写到 {path}。",
|
|
39
40
|
"notice.autoNamed": "给这段对话起名叫「{name}」。",
|
|
40
41
|
"notice.languageChanged": "这条连接现在说{language}。",
|
|
@@ -332,7 +332,7 @@ class TurnSession {
|
|
|
332
332
|
const agent = await this.createAgent({
|
|
333
333
|
conversationId: this.conversationId,
|
|
334
334
|
workspaceId: this.conversation.workspaceId,
|
|
335
|
-
agentId: this.conversation.agentId
|
|
335
|
+
agentId: this.conversation.agentId,
|
|
336
336
|
modelId: this.conversation.modelId,
|
|
337
337
|
thinkingLevel: this.conversation.thinkingLevel,
|
|
338
338
|
scope: this.scope,
|
|
@@ -1089,7 +1089,7 @@ export class ChatRunner {
|
|
|
1089
1089
|
}
|
|
1090
1090
|
return prepare({
|
|
1091
1091
|
conversationId: conversation.id,
|
|
1092
|
-
agentId: conversation.agentId
|
|
1092
|
+
agentId: conversation.agentId,
|
|
1093
1093
|
modelId: conversation.modelId,
|
|
1094
1094
|
input: params.input,
|
|
1095
1095
|
});
|
|
@@ -601,7 +601,12 @@ export class ConfigService {
|
|
|
601
601
|
// ---------------------------------------------------------------------------
|
|
602
602
|
// 纯函数
|
|
603
603
|
// ---------------------------------------------------------------------------
|
|
604
|
-
/**
|
|
604
|
+
/**
|
|
605
|
+
* 端点定义 → 对外形状(密钥只给掩码,F-F2)。
|
|
606
|
+
*
|
|
607
|
+
* `apiKey` 在类型上是必填,但 `config.json` 是手改得了的:SDK 的读法是 `JSON.parse as AppConfig`,
|
|
608
|
+
* 不补字段。少写这一行的配置会走到 `?? ""`(界面显示「未设置密钥」,而不是整页读不出来)。
|
|
609
|
+
*/
|
|
605
610
|
function toEndpointView(endpoint) {
|
|
606
611
|
const apiKey = endpoint.apiKey ?? "";
|
|
607
612
|
return {
|
|
@@ -217,8 +217,8 @@ export declare class ConversationService {
|
|
|
217
217
|
/**
|
|
218
218
|
* 读配置。
|
|
219
219
|
*
|
|
220
|
-
* `config.json`
|
|
221
|
-
*
|
|
220
|
+
* `config.json` 是手改得了的文件,它坏了由 SDK 当场恢复出厂(D32),所以这里不再把
|
|
221
|
+
* 解析失败折成别的错——读到的要么是磁盘上那份,要么是已经写回磁盘的出厂配置。
|
|
222
222
|
*/
|
|
223
223
|
private readConfig;
|
|
224
224
|
private viewContext;
|
|
@@ -578,8 +578,8 @@ export class ConversationService {
|
|
|
578
578
|
name: row.name,
|
|
579
579
|
nameIsAuto: row.nameIsAuto,
|
|
580
580
|
pinned: row.pinned,
|
|
581
|
-
agentId: row.agentId
|
|
582
|
-
modelId: row.modelId
|
|
581
|
+
agentId: row.agentId,
|
|
582
|
+
modelId: row.modelId,
|
|
583
583
|
createdAt: iso(row.createdAt),
|
|
584
584
|
updatedAt: iso(row.updatedAt),
|
|
585
585
|
// 这个数来自 `listByConversation` 的长度:只有**主线**消息(不含子 Agent 的,那些在同一个
|
|
@@ -599,9 +599,9 @@ export class ConversationService {
|
|
|
599
599
|
const agent = await this.loadAgent(row.agentId, context);
|
|
600
600
|
const model = this.effectiveModel(row, context.config);
|
|
601
601
|
const modelState = {
|
|
602
|
-
modelId: model?.id ?? row.modelId
|
|
602
|
+
modelId: model?.id ?? row.modelId,
|
|
603
603
|
// 模型可能在配置里被删掉了:行上的 id 仍然有效(它是这段会话现在用的那个),显示名退回 id
|
|
604
|
-
name: model?.name ?? row.modelId
|
|
604
|
+
name: model?.name ?? row.modelId,
|
|
605
605
|
vision: model?.vision === true,
|
|
606
606
|
source: this.modelSource(row, agent, context.config),
|
|
607
607
|
};
|
|
@@ -636,20 +636,11 @@ export class ConversationService {
|
|
|
636
636
|
/**
|
|
637
637
|
* 读配置。
|
|
638
638
|
*
|
|
639
|
-
* `config.json`
|
|
640
|
-
*
|
|
639
|
+
* `config.json` 是手改得了的文件,它坏了由 SDK 当场恢复出厂(D32),所以这里不再把
|
|
640
|
+
* 解析失败折成别的错——读到的要么是磁盘上那份,要么是已经写回磁盘的出厂配置。
|
|
641
641
|
*/
|
|
642
642
|
async readConfig() {
|
|
643
|
-
|
|
644
|
-
return await this.configManager.read();
|
|
645
|
-
}
|
|
646
|
-
catch (error) {
|
|
647
|
-
throw new HostError("config_invalid", {
|
|
648
|
-
params: { reason: error instanceof Error ? error.message : String(error) },
|
|
649
|
-
detail: { path: this.configManager.configPath },
|
|
650
|
-
cause: error,
|
|
651
|
-
});
|
|
652
|
-
}
|
|
643
|
+
return await this.configManager.read();
|
|
653
644
|
}
|
|
654
645
|
async viewContext(config, workspaces = [], agents = []) {
|
|
655
646
|
return {
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* 能力发现服务:把「这个项目现在能用哪些 MCP / Skill / 工具 / 子 Agent」摊成分层清单。
|
|
3
3
|
*
|
|
4
4
|
* 对应能力编号:F-G1(MCP 5 层链,可编辑的只有 2 个作用域)、F-G2(Skill 4 层、工具与子 Agent
|
|
5
|
-
* 各 2 层,只发现真实存在的目录,离项目越近越优先)、F-F6
|
|
5
|
+
* 各 2 层,只发现真实存在的目录,离项目越近越优先)、F-F6(坏文件按层分两种处置,见下)、
|
|
6
6
|
* L9(MCP 的 5 层链)、R54(5 层链与各能力目录由本服务算好交给 ScopeRegistry)。
|
|
7
7
|
*
|
|
8
|
+
* **F-F6 里「坏文件」的那条口径不是一句话,按文件在哪分**:用户级那份
|
|
9
|
+
* (`<数据根>/mcp.json`)坏了由 SDK 恢复出厂并留残骸(PRD D32),本服务因此看到的是一个
|
|
10
|
+
* 好文件(除非文件是运行期才坏的,那时下一次启动才恢复);项目侧
|
|
11
|
+
* (`<cwd>/.mcp.json`、`<cwd>/.ai-zen/mcp.json`、`<cwd>/.agents/mcp.json`)与
|
|
12
|
+
* `~/.agents/mcp.json` 坏了仍然是看得见的 `broken` 状态、能修 —— 那些文件在用户的仓库里,
|
|
13
|
+
* 恢复出厂等于改人家的 git 内容。
|
|
14
|
+
*
|
|
8
15
|
* 三件事分开:
|
|
9
16
|
*
|
|
10
17
|
* 1. **路径**({@link DiscoveryService.scopeAbilityPaths} 等):只有「按 cwd 推出各能力应该去
|
|
@@ -12,7 +19,8 @@
|
|
|
12
19
|
* `createWorkspaceScopeFactory`)拿的就是这些路径。
|
|
13
20
|
* 2. **读**({@link DiscoveryService.discover}):把每一层的 `missing | ok | broken` 与条目
|
|
14
21
|
* 报出来。`missing` 与 `broken` 必须分开(F-F6):以前两者都当空配置,一保存就把用户的
|
|
15
|
-
*
|
|
22
|
+
* 文件覆盖掉了;`broken` 在项目侧与 `~/.agents` 各层是「看得见、能修」,用户级那层由
|
|
23
|
+
* SDK 恢复出厂(D32)。
|
|
16
24
|
* 3. **写**({@link DiscoveryService.writeMcpEntry} / {@link DiscoveryService.deleteMcpEntry}):
|
|
17
25
|
* 只写 MCP 的两个可编辑作用域,一次只动一条;写回时保留这一版不认识的字段与 `oauth`
|
|
18
26
|
* (F-F6),其中 `oauth.clientSecret` 省略就按磁盘原文保留(F-F2:读出去的是掩码,
|
|
@@ -85,6 +93,13 @@ export declare class DiscoveryService {
|
|
|
85
93
|
* `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
|
|
86
94
|
*/
|
|
87
95
|
mcpLayers(cwd: string | null): DiscoveryLayerPath[];
|
|
96
|
+
/**
|
|
97
|
+
* 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
|
|
98
|
+
*
|
|
99
|
+
* 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
|
|
100
|
+
* 是同一份定义。
|
|
101
|
+
*/
|
|
102
|
+
private editableMcpLayers;
|
|
88
103
|
/** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
|
|
89
104
|
skillLayers(cwd: string | null): DiscoveryLayerPath[];
|
|
90
105
|
/** 用户工具的 2 层目录:`<ws>/.agents/tools` → `<aiZenDir>/tools` */
|
|
@@ -100,8 +115,6 @@ export declare class DiscoveryService {
|
|
|
100
115
|
scopeAbilityPaths(cwd: string): ScopeAbilityPaths;
|
|
101
116
|
/** 只取 MCP 的 5 层路径(喂给 SDK 的 `discoverMcpServers` / `McpScopePlugin`) */
|
|
102
117
|
mcpPaths(cwd: string): string[];
|
|
103
|
-
/** 按作用域找那一层的路径(写入时用) */
|
|
104
|
-
mcpLayer(scope: McpEditableScope, cwd: string | null): DiscoveryLayerPath;
|
|
105
118
|
/**
|
|
106
119
|
* `config.getDiscovery`(F-G1 / F-G2 / F-F6)。
|
|
107
120
|
*
|
|
@@ -141,7 +154,7 @@ export declare class DiscoveryService {
|
|
|
141
154
|
layer: DiscoveryLayerView;
|
|
142
155
|
removed: boolean;
|
|
143
156
|
}>;
|
|
144
|
-
/** 可写作用域 → 那一层;`workspace`
|
|
157
|
+
/** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
|
|
145
158
|
private writableMcpLayer;
|
|
146
159
|
private readMcpLayers;
|
|
147
160
|
/** 读一个 mcp.json:不存在 / 坏了 / 能读 三种结论分开(F-F6) */
|
|
@@ -2,9 +2,16 @@
|
|
|
2
2
|
* 能力发现服务:把「这个项目现在能用哪些 MCP / Skill / 工具 / 子 Agent」摊成分层清单。
|
|
3
3
|
*
|
|
4
4
|
* 对应能力编号:F-G1(MCP 5 层链,可编辑的只有 2 个作用域)、F-G2(Skill 4 层、工具与子 Agent
|
|
5
|
-
* 各 2 层,只发现真实存在的目录,离项目越近越优先)、F-F6
|
|
5
|
+
* 各 2 层,只发现真实存在的目录,离项目越近越优先)、F-F6(坏文件按层分两种处置,见下)、
|
|
6
6
|
* L9(MCP 的 5 层链)、R54(5 层链与各能力目录由本服务算好交给 ScopeRegistry)。
|
|
7
7
|
*
|
|
8
|
+
* **F-F6 里「坏文件」的那条口径不是一句话,按文件在哪分**:用户级那份
|
|
9
|
+
* (`<数据根>/mcp.json`)坏了由 SDK 恢复出厂并留残骸(PRD D32),本服务因此看到的是一个
|
|
10
|
+
* 好文件(除非文件是运行期才坏的,那时下一次启动才恢复);项目侧
|
|
11
|
+
* (`<cwd>/.mcp.json`、`<cwd>/.ai-zen/mcp.json`、`<cwd>/.agents/mcp.json`)与
|
|
12
|
+
* `~/.agents/mcp.json` 坏了仍然是看得见的 `broken` 状态、能修 —— 那些文件在用户的仓库里,
|
|
13
|
+
* 恢复出厂等于改人家的 git 内容。
|
|
14
|
+
*
|
|
8
15
|
* 三件事分开:
|
|
9
16
|
*
|
|
10
17
|
* 1. **路径**({@link DiscoveryService.scopeAbilityPaths} 等):只有「按 cwd 推出各能力应该去
|
|
@@ -12,7 +19,8 @@
|
|
|
12
19
|
* `createWorkspaceScopeFactory`)拿的就是这些路径。
|
|
13
20
|
* 2. **读**({@link DiscoveryService.discover}):把每一层的 `missing | ok | broken` 与条目
|
|
14
21
|
* 报出来。`missing` 与 `broken` 必须分开(F-F6):以前两者都当空配置,一保存就把用户的
|
|
15
|
-
*
|
|
22
|
+
* 文件覆盖掉了;`broken` 在项目侧与 `~/.agents` 各层是「看得见、能修」,用户级那层由
|
|
23
|
+
* SDK 恢复出厂(D32)。
|
|
16
24
|
* 3. **写**({@link DiscoveryService.writeMcpEntry} / {@link DiscoveryService.deleteMcpEntry}):
|
|
17
25
|
* 只写 MCP 的两个可编辑作用域,一次只动一条;写回时保留这一版不认识的字段与 `oauth`
|
|
18
26
|
* (F-F6),其中 `oauth.clientSecret` 省略就按磁盘原文保留(F-F2:读出去的是掩码,
|
|
@@ -109,9 +117,30 @@ export class DiscoveryService {
|
|
|
109
117
|
* `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
|
|
110
118
|
*/
|
|
111
119
|
mcpLayers(cwd) {
|
|
120
|
+
const editable = this.editableMcpLayers(cwd);
|
|
112
121
|
return withoutNulls([
|
|
113
122
|
projectLayer(cwd, 0, `.${FILE_MCP}`, "other"),
|
|
114
|
-
|
|
123
|
+
editable.workspace,
|
|
124
|
+
projectLayer(cwd, 2, join(PROJECT_AGENTS_DIR, FILE_MCP), "other"),
|
|
125
|
+
editable.user,
|
|
126
|
+
{
|
|
127
|
+
depth: 4,
|
|
128
|
+
path: join(this.homeDir, HOME_AGENTS_DIR, FILE_MCP),
|
|
129
|
+
editable: false,
|
|
130
|
+
scope: null,
|
|
131
|
+
owner: "other",
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
|
|
137
|
+
*
|
|
138
|
+
* 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
|
|
139
|
+
* 是同一份定义。
|
|
140
|
+
*/
|
|
141
|
+
editableMcpLayers(cwd) {
|
|
142
|
+
return {
|
|
143
|
+
workspace: cwd === null
|
|
115
144
|
? null
|
|
116
145
|
: {
|
|
117
146
|
depth: 1,
|
|
@@ -120,22 +149,14 @@ export class DiscoveryService {
|
|
|
120
149
|
scope: "workspace",
|
|
121
150
|
owner: "workspace",
|
|
122
151
|
},
|
|
123
|
-
|
|
124
|
-
{
|
|
152
|
+
user: {
|
|
125
153
|
depth: 3,
|
|
126
154
|
path: join(this.basePath, FILE_MCP),
|
|
127
155
|
editable: true,
|
|
128
156
|
scope: "user",
|
|
129
157
|
owner: "user",
|
|
130
158
|
},
|
|
131
|
-
|
|
132
|
-
depth: 4,
|
|
133
|
-
path: join(this.homeDir, HOME_AGENTS_DIR, FILE_MCP),
|
|
134
|
-
editable: false,
|
|
135
|
-
scope: null,
|
|
136
|
-
owner: "other",
|
|
137
|
-
},
|
|
138
|
-
]);
|
|
159
|
+
};
|
|
139
160
|
}
|
|
140
161
|
/** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
|
|
141
162
|
skillLayers(cwd) {
|
|
@@ -203,17 +224,6 @@ export class DiscoveryService {
|
|
|
203
224
|
mcpPaths(cwd) {
|
|
204
225
|
return this.mcpLayers(cwd).map((layer) => layer.path);
|
|
205
226
|
}
|
|
206
|
-
/** 按作用域找那一层的路径(写入时用) */
|
|
207
|
-
mcpLayer(scope, cwd) {
|
|
208
|
-
const layer = this.mcpLayers(cwd).find((candidate) => candidate.scope === scope);
|
|
209
|
-
if (!layer) {
|
|
210
|
-
throw new HostError("unsupported", {
|
|
211
|
-
params: { what: `the "${scope}" MCP scope` },
|
|
212
|
-
detail: { scope, reason: "no_such_scope" },
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return layer;
|
|
216
|
-
}
|
|
217
227
|
// -------------------------------------------------------------------------
|
|
218
228
|
// 读:一层的状态与条目
|
|
219
229
|
// -------------------------------------------------------------------------
|
|
@@ -307,15 +317,18 @@ export class DiscoveryService {
|
|
|
307
317
|
}
|
|
308
318
|
return { layer: await this.readMcpLayerView(layer), removed };
|
|
309
319
|
}
|
|
310
|
-
/** 可写作用域 → 那一层;`workspace`
|
|
320
|
+
/** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
|
|
311
321
|
writableMcpLayer(scope, cwd) {
|
|
312
|
-
|
|
322
|
+
const editable = this.editableMcpLayers(cwd);
|
|
323
|
+
if (scope === "user")
|
|
324
|
+
return editable.user;
|
|
325
|
+
if (editable.workspace === null || (cwd ?? "").trim() === "") {
|
|
313
326
|
throw new HostError("invalid_params", {
|
|
314
327
|
params: { field: "workspaceId" },
|
|
315
328
|
detail: { field: "workspaceId", reason: "workspace_scope_needs_workspace" },
|
|
316
329
|
});
|
|
317
330
|
}
|
|
318
|
-
return
|
|
331
|
+
return editable.workspace;
|
|
319
332
|
}
|
|
320
333
|
// -------------------------------------------------------------------------
|
|
321
334
|
// 内部:MCP
|
|
@@ -976,6 +989,8 @@ async function listSubdirectories(dir) {
|
|
|
976
989
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
977
990
|
}
|
|
978
991
|
catch {
|
|
992
|
+
// 走到这里只剩两种情形:调用前 `stat` 过了、目录刚被删掉,或没有读权限 ——
|
|
993
|
+
// 两种都按「这一层没有条目」报(层本身的 broken 已经由 `readDirectory` 报过)
|
|
979
994
|
return [];
|
|
980
995
|
}
|
|
981
996
|
}
|
|
@@ -985,6 +1000,7 @@ async function listFiles(dir) {
|
|
|
985
1000
|
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
986
1001
|
}
|
|
987
1002
|
catch {
|
|
1003
|
+
// 同 `listSubdirectories`:目录在 `stat` 之后刚被删、或没有读权限,按空报
|
|
988
1004
|
return [];
|
|
989
1005
|
}
|
|
990
1006
|
}
|
|
@@ -290,8 +290,8 @@ export class ModelService {
|
|
|
290
290
|
const input = params.imageModel;
|
|
291
291
|
const config = await this.readConfig();
|
|
292
292
|
const id = requireText(input?.id, "imageModel.id");
|
|
293
|
-
requireText(input?.name, "imageModel.name");
|
|
294
|
-
requireText(input?.modelName, "imageModel.modelName");
|
|
293
|
+
const name = requireText(input?.name, "imageModel.name");
|
|
294
|
+
const modelName = requireText(input?.modelName, "imageModel.modelName");
|
|
295
295
|
const endpointId = requireText(input?.endpointId, "imageModel.endpointId");
|
|
296
296
|
if (!findEndpoint(config, endpointId)) {
|
|
297
297
|
throw new HostError("invalid_params", {
|
|
@@ -309,9 +309,9 @@ export class ModelService {
|
|
|
309
309
|
defaultQuality: input.defaultQuality,
|
|
310
310
|
}),
|
|
311
311
|
id,
|
|
312
|
-
name
|
|
312
|
+
name,
|
|
313
313
|
endpointId,
|
|
314
|
-
modelName
|
|
314
|
+
modelName,
|
|
315
315
|
custom: true,
|
|
316
316
|
};
|
|
317
317
|
const next = {
|
|
@@ -111,6 +111,9 @@ export declare class QuestionService {
|
|
|
111
111
|
*
|
|
112
112
|
* 先落库再推事件:库里那一行是「多端看同一个」的真相(D22),推送只是提醒在线的那几个
|
|
113
113
|
* 窗口去拉(L5 断线即作废)。
|
|
114
|
+
*
|
|
115
|
+
* 入参不做形状校验:`kind` 与两个 `messageKey` 的类型本身就是闭集(`QuestionKind` /
|
|
116
|
+
* `MessageKey`),而这里只有 `askHandoff` 一个调用方,候选项是常量字面量。
|
|
114
117
|
*/
|
|
115
118
|
ask(input: AskQuestionInput, language?: Language): Promise<QuestionView>;
|
|
116
119
|
/**
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { DEFAULT_LANGUAGE, hasMessage, render } from "../i18n/index.js";
|
|
20
20
|
import { HostError } from "../wire/errors.js";
|
|
21
|
-
import { QUESTION_KINDS } from "../wire/states.js";
|
|
22
21
|
import { readUsage } from "./MessageService.js";
|
|
23
22
|
/** 交接问题的三个候选值(F-D2:自交接 / 独立摘要 / 不交接) */
|
|
24
23
|
export const HANDOFF_OPTION_SELF = "self";
|
|
@@ -154,42 +153,11 @@ export class QuestionService {
|
|
|
154
153
|
*
|
|
155
154
|
* 先落库再推事件:库里那一行是「多端看同一个」的真相(D22),推送只是提醒在线的那几个
|
|
156
155
|
* 窗口去拉(L5 断线即作废)。
|
|
156
|
+
*
|
|
157
|
+
* 入参不做形状校验:`kind` 与两个 `messageKey` 的类型本身就是闭集(`QuestionKind` /
|
|
158
|
+
* `MessageKey`),而这里只有 `askHandoff` 一个调用方,候选项是常量字面量。
|
|
157
159
|
*/
|
|
158
160
|
async ask(input, language = DEFAULT_LANGUAGE) {
|
|
159
|
-
if (!QUESTION_KINDS.includes(input.kind)) {
|
|
160
|
-
throw new HostError("invalid_params", {
|
|
161
|
-
params: { field: "kind" },
|
|
162
|
-
detail: { field: "kind", value: input.kind, kinds: QUESTION_KINDS },
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
if (!hasMessage(input.prompt.messageKey)) {
|
|
166
|
-
throw new HostError("invalid_params", {
|
|
167
|
-
params: { field: "prompt" },
|
|
168
|
-
detail: { field: "prompt", reason: "unknown_message_key" },
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
if (input.options.length === 0) {
|
|
172
|
-
throw new HostError("invalid_params", {
|
|
173
|
-
params: { field: "options" },
|
|
174
|
-
detail: { field: "options", reason: "empty" },
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const seen = new Set();
|
|
178
|
-
for (const option of input.options) {
|
|
179
|
-
if (option.value.trim().length === 0 || seen.has(option.value)) {
|
|
180
|
-
throw new HostError("invalid_params", {
|
|
181
|
-
params: { field: "options" },
|
|
182
|
-
detail: { field: "options", reason: "duplicate_or_empty_value", value: option.value },
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
seen.add(option.value);
|
|
186
|
-
if (!hasMessage(option.label.messageKey)) {
|
|
187
|
-
throw new HostError("invalid_params", {
|
|
188
|
-
params: { field: "options" },
|
|
189
|
-
detail: { field: "options", reason: "unknown_message_key", value: option.value },
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
161
|
const payload = {
|
|
194
162
|
code: input.code ?? input.prompt.messageKey,
|
|
195
163
|
prompt: { messageKey: input.prompt.messageKey, params: input.prompt.params ?? {} },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-zen/agents-host",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.3",
|
|
4
4
|
"description": "AI-Zen Agents Host — session/workspace service shared by CLI and Desktop",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@ai-zen/agents-core": "^4.6.0-alpha.0",
|
|
43
|
-
"@ai-zen/agents-sdk": "^1.2.0-alpha.
|
|
43
|
+
"@ai-zen/agents-sdk": "^1.2.0-alpha.2",
|
|
44
44
|
"ws": "^8.22.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|