@ai-zen/agents-host 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +5 -3
- package/dist/config.js +16 -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 +8 -3
- package/dist/services/DiscoveryService.js +33 -25
- 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,9 @@ export interface HostSettings {
|
|
|
90
91
|
/**
|
|
91
92
|
* 读 `<dataRoot>/host.json`。
|
|
92
93
|
*
|
|
93
|
-
* 三种情况分开:文件不在 →
|
|
94
|
-
*
|
|
94
|
+
* 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
|
|
95
|
+
* 就是坏文件,恢复出厂:当空设置用(默认端口、默认语言)、写回一份合法的 `{}`、回调一次
|
|
96
|
+
* (D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件);`port` 在但不是可用端口 →
|
|
95
97
|
* `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
|
|
96
98
|
*/
|
|
97
|
-
export declare function readHostSettings(paths: HostPaths): Promise<HostSettings>;
|
|
99
|
+
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 { readFile, 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,12 @@ function invalidPort(field, value, source) {
|
|
|
119
119
|
/**
|
|
120
120
|
* 读 `<dataRoot>/host.json`。
|
|
121
121
|
*
|
|
122
|
-
* 三种情况分开:文件不在 →
|
|
123
|
-
*
|
|
122
|
+
* 三种情况分开:文件不在 → 空设置(这一档没说话,静默);读不出来 / 不是 JSON / 不是对象 →
|
|
123
|
+
* 就是坏文件,恢复出厂:当空设置用(默认端口、默认语言)、写回一份合法的 `{}`、回调一次
|
|
124
|
+
* (D32:坏了就恢复出厂,下次启动不再遇到同一个坏文件);`port` 在但不是可用端口 →
|
|
124
125
|
* `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
|
|
125
126
|
*/
|
|
126
|
-
export async function readHostSettings(paths) {
|
|
127
|
+
export async function readHostSettings(paths, onRecovered) {
|
|
127
128
|
let raw;
|
|
128
129
|
try {
|
|
129
130
|
raw = await readFile(paths.hostConfigFile, "utf8");
|
|
@@ -132,29 +133,17 @@ export async function readHostSettings(paths) {
|
|
|
132
133
|
// 文件不在是正常情况(绝大多数据根上就没有这份文件)
|
|
133
134
|
if (isMissingFile(error))
|
|
134
135
|
return {};
|
|
135
|
-
|
|
136
|
-
messageKey: "config.fileBroken",
|
|
137
|
-
params: { path: paths.hostConfigFile, reason: "unreadable" },
|
|
138
|
-
detail: { path: paths.hostConfigFile, reason: "unreadable" },
|
|
139
|
-
});
|
|
136
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
140
137
|
}
|
|
141
138
|
let parsed;
|
|
142
139
|
try {
|
|
143
140
|
parsed = JSON.parse(raw);
|
|
144
141
|
}
|
|
145
142
|
catch {
|
|
146
|
-
|
|
147
|
-
messageKey: "config.fileBroken",
|
|
148
|
-
params: { path: paths.hostConfigFile, reason: "not_json" },
|
|
149
|
-
detail: { path: paths.hostConfigFile, reason: "not_json" },
|
|
150
|
-
});
|
|
143
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
151
144
|
}
|
|
152
145
|
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
|
-
});
|
|
146
|
+
return await recoverHostSettings(paths, onRecovered);
|
|
158
147
|
}
|
|
159
148
|
const record = parsed;
|
|
160
149
|
if (!Object.prototype.hasOwnProperty.call(record, "port") || record.port === null)
|
|
@@ -164,6 +153,14 @@ export async function readHostSettings(paths) {
|
|
|
164
153
|
throw invalidPort("port", port, "host.json");
|
|
165
154
|
return { port };
|
|
166
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* 坏掉之后恢复出厂:写回一份合法的 `{}`(=默认端口、默认语言),再回调一次。
|
|
158
|
+
*/
|
|
159
|
+
async function recoverHostSettings(paths, onRecovered) {
|
|
160
|
+
await writeFile(paths.hostConfigFile, "{}\n", "utf8");
|
|
161
|
+
await onRecovered?.(paths.hostConfigFile);
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
167
164
|
function isMissingFile(error) {
|
|
168
165
|
return (typeof error === "object" &&
|
|
169
166
|
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 {
|
|
@@ -85,6 +85,13 @@ export declare class DiscoveryService {
|
|
|
85
85
|
* `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
|
|
86
86
|
*/
|
|
87
87
|
mcpLayers(cwd: string | null): DiscoveryLayerPath[];
|
|
88
|
+
/**
|
|
89
|
+
* 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
|
|
90
|
+
*
|
|
91
|
+
* 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
|
|
92
|
+
* 是同一份定义。
|
|
93
|
+
*/
|
|
94
|
+
private editableMcpLayers;
|
|
88
95
|
/** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
|
|
89
96
|
skillLayers(cwd: string | null): DiscoveryLayerPath[];
|
|
90
97
|
/** 用户工具的 2 层目录:`<ws>/.agents/tools` → `<aiZenDir>/tools` */
|
|
@@ -100,8 +107,6 @@ export declare class DiscoveryService {
|
|
|
100
107
|
scopeAbilityPaths(cwd: string): ScopeAbilityPaths;
|
|
101
108
|
/** 只取 MCP 的 5 层路径(喂给 SDK 的 `discoverMcpServers` / `McpScopePlugin`) */
|
|
102
109
|
mcpPaths(cwd: string): string[];
|
|
103
|
-
/** 按作用域找那一层的路径(写入时用) */
|
|
104
|
-
mcpLayer(scope: McpEditableScope, cwd: string | null): DiscoveryLayerPath;
|
|
105
110
|
/**
|
|
106
111
|
* `config.getDiscovery`(F-G1 / F-G2 / F-F6)。
|
|
107
112
|
*
|
|
@@ -141,7 +146,7 @@ export declare class DiscoveryService {
|
|
|
141
146
|
layer: DiscoveryLayerView;
|
|
142
147
|
removed: boolean;
|
|
143
148
|
}>;
|
|
144
|
-
/** 可写作用域 → 那一层;`workspace`
|
|
149
|
+
/** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
|
|
145
150
|
private writableMcpLayer;
|
|
146
151
|
private readMcpLayers;
|
|
147
152
|
/** 读一个 mcp.json:不存在 / 坏了 / 能读 三种结论分开(F-F6) */
|
|
@@ -109,9 +109,30 @@ export class DiscoveryService {
|
|
|
109
109
|
* `cwd` 为 `null`(还没打开项目)时只给后两层:项目侧那三层的路径得先有项目才算得出来。
|
|
110
110
|
*/
|
|
111
111
|
mcpLayers(cwd) {
|
|
112
|
+
const editable = this.editableMcpLayers(cwd);
|
|
112
113
|
return withoutNulls([
|
|
113
114
|
projectLayer(cwd, 0, `.${FILE_MCP}`, "other"),
|
|
114
|
-
|
|
115
|
+
editable.workspace,
|
|
116
|
+
projectLayer(cwd, 2, join(PROJECT_AGENTS_DIR, FILE_MCP), "other"),
|
|
117
|
+
editable.user,
|
|
118
|
+
{
|
|
119
|
+
depth: 4,
|
|
120
|
+
path: join(this.homeDir, HOME_AGENTS_DIR, FILE_MCP),
|
|
121
|
+
editable: false,
|
|
122
|
+
scope: null,
|
|
123
|
+
owner: "other",
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 5 层链里那两个可写的层(第 1 层与第 4 层);没有项目时 `workspace` 那层是 `null`。
|
|
129
|
+
*
|
|
130
|
+
* 位置、能不能写、归谁只在这里定一次:读(`mcpLayers`)与写(`writableMcpLayer`)拿的
|
|
131
|
+
* 是同一份定义。
|
|
132
|
+
*/
|
|
133
|
+
editableMcpLayers(cwd) {
|
|
134
|
+
return {
|
|
135
|
+
workspace: cwd === null
|
|
115
136
|
? null
|
|
116
137
|
: {
|
|
117
138
|
depth: 1,
|
|
@@ -120,22 +141,14 @@ export class DiscoveryService {
|
|
|
120
141
|
scope: "workspace",
|
|
121
142
|
owner: "workspace",
|
|
122
143
|
},
|
|
123
|
-
|
|
124
|
-
{
|
|
144
|
+
user: {
|
|
125
145
|
depth: 3,
|
|
126
146
|
path: join(this.basePath, FILE_MCP),
|
|
127
147
|
editable: true,
|
|
128
148
|
scope: "user",
|
|
129
149
|
owner: "user",
|
|
130
150
|
},
|
|
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
|
-
]);
|
|
151
|
+
};
|
|
139
152
|
}
|
|
140
153
|
/** Skill 的 4 层目录:`<ws>/.ai-zen/skills` → `<ws>/.agents/skills` → `<aiZenDir>/skills` → `~/.agents/skills` */
|
|
141
154
|
skillLayers(cwd) {
|
|
@@ -203,17 +216,6 @@ export class DiscoveryService {
|
|
|
203
216
|
mcpPaths(cwd) {
|
|
204
217
|
return this.mcpLayers(cwd).map((layer) => layer.path);
|
|
205
218
|
}
|
|
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
219
|
// -------------------------------------------------------------------------
|
|
218
220
|
// 读:一层的状态与条目
|
|
219
221
|
// -------------------------------------------------------------------------
|
|
@@ -307,15 +309,18 @@ export class DiscoveryService {
|
|
|
307
309
|
}
|
|
308
310
|
return { layer: await this.readMcpLayerView(layer), removed };
|
|
309
311
|
}
|
|
310
|
-
/** 可写作用域 → 那一层;`workspace`
|
|
312
|
+
/** 可写作用域 → 那一层;`workspace` 作用域必须能落到某个项目里(空串也算没给) */
|
|
311
313
|
writableMcpLayer(scope, cwd) {
|
|
312
|
-
|
|
314
|
+
const editable = this.editableMcpLayers(cwd);
|
|
315
|
+
if (scope === "user")
|
|
316
|
+
return editable.user;
|
|
317
|
+
if (editable.workspace === null || (cwd ?? "").trim() === "") {
|
|
313
318
|
throw new HostError("invalid_params", {
|
|
314
319
|
params: { field: "workspaceId" },
|
|
315
320
|
detail: { field: "workspaceId", reason: "workspace_scope_needs_workspace" },
|
|
316
321
|
});
|
|
317
322
|
}
|
|
318
|
-
return
|
|
323
|
+
return editable.workspace;
|
|
319
324
|
}
|
|
320
325
|
// -------------------------------------------------------------------------
|
|
321
326
|
// 内部:MCP
|
|
@@ -976,6 +981,8 @@ async function listSubdirectories(dir) {
|
|
|
976
981
|
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
977
982
|
}
|
|
978
983
|
catch {
|
|
984
|
+
// 走到这里只剩两种情形:调用前 `stat` 过了、目录刚被删掉,或没有读权限 ——
|
|
985
|
+
// 两种都按「这一层没有条目」报(层本身的 broken 已经由 `readDirectory` 报过)
|
|
979
986
|
return [];
|
|
980
987
|
}
|
|
981
988
|
}
|
|
@@ -985,6 +992,7 @@ async function listFiles(dir) {
|
|
|
985
992
|
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
986
993
|
}
|
|
987
994
|
catch {
|
|
995
|
+
// 同 `listSubdirectories`:目录在 `stat` 之后刚被删、或没有读权限,按空报
|
|
988
996
|
return [];
|
|
989
997
|
}
|
|
990
998
|
}
|
|
@@ -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.2",
|
|
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.1",
|
|
44
44
|
"ws": "^8.22.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|