@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.1

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.
Files changed (146) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +759 -2
  4. package/dist/app.d.ts +384 -0
  5. package/dist/app.js +1148 -0
  6. package/dist/cli.d.ts +22 -0
  7. package/dist/cli.js +354 -0
  8. package/dist/config.d.ts +97 -0
  9. package/dist/config.js +171 -0
  10. package/dist/handlers/agentHandlers.d.ts +18 -0
  11. package/dist/handlers/agentHandlers.js +20 -0
  12. package/dist/handlers/assetHandlers.d.ts +29 -0
  13. package/dist/handlers/assetHandlers.js +35 -0
  14. package/dist/handlers/capabilities.d.ts +26 -0
  15. package/dist/handlers/capabilities.js +138 -0
  16. package/dist/handlers/completionHandlers.d.ts +18 -0
  17. package/dist/handlers/completionHandlers.js +13 -0
  18. package/dist/handlers/configHandlers.d.ts +31 -0
  19. package/dist/handlers/configHandlers.js +59 -0
  20. package/dist/handlers/conversationHandlers.d.ts +118 -0
  21. package/dist/handlers/conversationHandlers.js +314 -0
  22. package/dist/handlers/handoffHandlers.d.ts +17 -0
  23. package/dist/handlers/handoffHandlers.js +14 -0
  24. package/dist/handlers/hostHandlers.d.ts +39 -0
  25. package/dist/handlers/hostHandlers.js +56 -0
  26. package/dist/handlers/index.d.ts +128 -0
  27. package/dist/handlers/index.js +92 -0
  28. package/dist/handlers/messageHandlers.d.ts +11 -0
  29. package/dist/handlers/messageHandlers.js +10 -0
  30. package/dist/handlers/modelHandlers.d.ts +17 -0
  31. package/dist/handlers/modelHandlers.js +19 -0
  32. package/dist/handlers/noticeHandlers.d.ts +24 -0
  33. package/dist/handlers/noticeHandlers.js +24 -0
  34. package/dist/handlers/questionHandlers.d.ts +16 -0
  35. package/dist/handlers/questionHandlers.js +16 -0
  36. package/dist/handlers/searchHandlers.d.ts +12 -0
  37. package/dist/handlers/searchHandlers.js +12 -0
  38. package/dist/handlers/workspaceHandlers.d.ts +29 -0
  39. package/dist/handlers/workspaceHandlers.js +47 -0
  40. package/dist/i18n/en.d.ts +243 -0
  41. package/dist/i18n/en.js +270 -0
  42. package/dist/i18n/index.d.ts +43 -0
  43. package/dist/i18n/index.js +72 -0
  44. package/dist/i18n/zh.d.ts +240 -0
  45. package/dist/i18n/zh.js +257 -0
  46. package/dist/index.d.ts +17 -0
  47. package/dist/index.js +15 -0
  48. package/dist/net/UploadReceiver.d.ts +186 -0
  49. package/dist/net/UploadReceiver.js +494 -0
  50. package/dist/net/WsHub.d.ts +231 -0
  51. package/dist/net/WsHub.js +689 -0
  52. package/dist/net/auth.d.ts +73 -0
  53. package/dist/net/auth.js +74 -0
  54. package/dist/net/discovery.d.ts +72 -0
  55. package/dist/net/discovery.js +171 -0
  56. package/dist/net/index.d.ts +14 -0
  57. package/dist/net/index.js +10 -0
  58. package/dist/runtime/ChatRunner.d.ts +374 -0
  59. package/dist/runtime/ChatRunner.js +1204 -0
  60. package/dist/runtime/ConversationDrafts.d.ts +43 -0
  61. package/dist/runtime/ConversationDrafts.js +47 -0
  62. package/dist/runtime/RunRegistry.d.ts +79 -0
  63. package/dist/runtime/RunRegistry.js +108 -0
  64. package/dist/runtime/ScopeRegistry.d.ts +153 -0
  65. package/dist/runtime/ScopeRegistry.js +259 -0
  66. package/dist/runtime/TransientRunner.d.ts +90 -0
  67. package/dist/runtime/TransientRunner.js +179 -0
  68. package/dist/runtime/index.d.ts +14 -0
  69. package/dist/runtime/index.js +10 -0
  70. package/dist/services/AgentService.d.ts +150 -0
  71. package/dist/services/AgentService.js +718 -0
  72. package/dist/services/AssetService.d.ts +338 -0
  73. package/dist/services/AssetService.js +739 -0
  74. package/dist/services/AutoNaming.d.ts +92 -0
  75. package/dist/services/AutoNaming.js +235 -0
  76. package/dist/services/CompletionService.d.ts +122 -0
  77. package/dist/services/CompletionService.js +442 -0
  78. package/dist/services/ConfigService.d.ts +171 -0
  79. package/dist/services/ConfigService.js +695 -0
  80. package/dist/services/ConversationService.d.ts +274 -0
  81. package/dist/services/ConversationService.js +902 -0
  82. package/dist/services/CredentialService.d.ts +33 -0
  83. package/dist/services/CredentialService.js +86 -0
  84. package/dist/services/DiscoveryService.d.ts +182 -0
  85. package/dist/services/DiscoveryService.js +998 -0
  86. package/dist/services/HandoffService.d.ts +189 -0
  87. package/dist/services/HandoffService.js +414 -0
  88. package/dist/services/Logger.d.ts +130 -0
  89. package/dist/services/Logger.js +256 -0
  90. package/dist/services/MessageService.d.ts +88 -0
  91. package/dist/services/MessageService.js +280 -0
  92. package/dist/services/ModelService.d.ts +89 -0
  93. package/dist/services/ModelService.js +489 -0
  94. package/dist/services/NoticeService.d.ts +93 -0
  95. package/dist/services/NoticeService.js +217 -0
  96. package/dist/services/QuestionService.d.ts +184 -0
  97. package/dist/services/QuestionService.js +457 -0
  98. package/dist/services/SearchService.d.ts +54 -0
  99. package/dist/services/SearchService.js +117 -0
  100. package/dist/services/WorkspaceService.d.ts +96 -0
  101. package/dist/services/WorkspaceService.js +213 -0
  102. package/dist/services/imageSize.d.ts +41 -0
  103. package/dist/services/imageSize.js +146 -0
  104. package/dist/services/index.d.ts +20 -0
  105. package/dist/services/index.js +15 -0
  106. package/dist/services/secretMask.d.ts +24 -0
  107. package/dist/services/secretMask.js +34 -0
  108. package/dist/storage/AssetRepository.d.ts +87 -0
  109. package/dist/storage/AssetRepository.js +180 -0
  110. package/dist/storage/ConversationRepository.d.ts +55 -0
  111. package/dist/storage/ConversationRepository.js +128 -0
  112. package/dist/storage/CredentialRepository.d.ts +28 -0
  113. package/dist/storage/CredentialRepository.js +47 -0
  114. package/dist/storage/Database.d.ts +95 -0
  115. package/dist/storage/Database.js +180 -0
  116. package/dist/storage/MessageRepository.d.ts +104 -0
  117. package/dist/storage/MessageRepository.js +323 -0
  118. package/dist/storage/MetaRepository.d.ts +21 -0
  119. package/dist/storage/MetaRepository.js +33 -0
  120. package/dist/storage/NoticeRepository.d.ts +31 -0
  121. package/dist/storage/NoticeRepository.js +89 -0
  122. package/dist/storage/QuestionRepository.d.ts +34 -0
  123. package/dist/storage/QuestionRepository.js +96 -0
  124. package/dist/storage/WorkspaceRepository.d.ts +25 -0
  125. package/dist/storage/WorkspaceRepository.js +44 -0
  126. package/dist/storage/dbWorker.d.ts +10 -0
  127. package/dist/storage/dbWorker.js +132 -0
  128. package/dist/storage/index.d.ts +20 -0
  129. package/dist/storage/index.js +16 -0
  130. package/dist/storage/migrations.d.ts +26 -0
  131. package/dist/storage/migrations.js +207 -0
  132. package/dist/storage/types.d.ts +298 -0
  133. package/dist/storage/types.js +14 -0
  134. package/dist/wire/errors.d.ts +105 -0
  135. package/dist/wire/errors.js +143 -0
  136. package/dist/wire/events.d.ts +102 -0
  137. package/dist/wire/events.js +13 -0
  138. package/dist/wire/frames.d.ts +106 -0
  139. package/dist/wire/frames.js +156 -0
  140. package/dist/wire/index.d.ts +31 -0
  141. package/dist/wire/index.js +31 -0
  142. package/dist/wire/methods.d.ts +1896 -0
  143. package/dist/wire/methods.js +99 -0
  144. package/dist/wire/states.d.ts +487 -0
  145. package/dist/wire/states.js +61 -0
  146. package/package.json +63 -3
package/dist/cli.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 命令行入口(`bin.agents-host` → `dist/cli.js`)。
4
+ *
5
+ * 三组动作:
6
+ * - `agents-host [serve]`:前台起服务。缺省是本地实例(只监听回环,凭证自动生成并写在
7
+ * `<数据根>/run/token`);`--public` 起对外实例,`--host` 指定监听地址;
8
+ * - `agents-host token create|list|revoke`:**不开服务**,直接开库操作凭证(PRD L11 / F-E4)。
9
+ * 明文只在 `create` 的那一次输出里出现,此后库里只有摘要;
10
+ * - `agents-host version`:版本与协议版本(升级判定要比这两个,PRD D4 / F-E3)。
11
+ *
12
+ * 参数解析是手写的:这个包不新增依赖,而命令形状就这么几种。解析失败给一句人话 + 退出码 2
13
+ * (「参数用法不对」与「运行失败」分开,脚本才好分支);运行失败走 1,成功走 0。
14
+ *
15
+ * 错误一律走 i18n 渲染出的人话,不甩栈:命令行是给人看的,栈只在日志里有意义。
16
+ */
17
+ /**
18
+ * 入口:返回退出码。
19
+ *
20
+ * 用法错误走 2、运行失败走 1、成功走 0 —— 脚本据此分支,不去解析句子。
21
+ */
22
+ export declare function main(argv: readonly string[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,354 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 命令行入口(`bin.agents-host` → `dist/cli.js`)。
4
+ *
5
+ * 三组动作:
6
+ * - `agents-host [serve]`:前台起服务。缺省是本地实例(只监听回环,凭证自动生成并写在
7
+ * `<数据根>/run/token`);`--public` 起对外实例,`--host` 指定监听地址;
8
+ * - `agents-host token create|list|revoke`:**不开服务**,直接开库操作凭证(PRD L11 / F-E4)。
9
+ * 明文只在 `create` 的那一次输出里出现,此后库里只有摘要;
10
+ * - `agents-host version`:版本与协议版本(升级判定要比这两个,PRD D4 / F-E3)。
11
+ *
12
+ * 参数解析是手写的:这个包不新增依赖,而命令形状就这么几种。解析失败给一句人话 + 退出码 2
13
+ * (「参数用法不对」与「运行失败」分开,脚本才好分支);运行失败走 1,成功走 0。
14
+ *
15
+ * 错误一律走 i18n 渲染出的人话,不甩栈:命令行是给人看的,栈只在日志里有意义。
16
+ */
17
+ import { mkdir } from "node:fs/promises";
18
+ import { createHost, resolveHostVersion } from "./app.js";
19
+ import { readHostSettings, resolvePaths } from "./config.js";
20
+ import { CredentialService } from "./services/index.js";
21
+ import { CredentialRepository, Database, migrate } from "./storage/index.js";
22
+ import { PROTOCOL_VERSION, HostError, toErrorData } from "./wire/index.js";
23
+ /** 用法不对时的退出码(与「跑起来但失败了」区分开) */
24
+ const EXIT_USAGE = 2;
25
+ /** 运行期失败的退出码 */
26
+ const EXIT_FAILURE = 1;
27
+ /**
28
+ * 命令行的语言。
29
+ *
30
+ * 宿主的产品文案是「按连接的语言」渲染的(PRD D8),而命令行没有连接、也就没有协商入口,
31
+ * 所以它与自己的说明文字用同一种语言。这一点已在 `docs/plan-questions.md` 记过。
32
+ */
33
+ const CLI_LANGUAGE = "zh";
34
+ const USAGE = `用法:
35
+ agents-host [serve] [--port <端口>] [--host <地址>] [--local | --public] [--ai-zen-dir <目录>]
36
+ 前台启动服务。缺省是本地实例(只监听 127.0.0.1,凭证自动生成并写在 <数据根>/run/token)。
37
+ 端口的三档来源:--port > <数据根>/host.json 的 port > 默认 8790。
38
+ --port 0 让系统分配一个空闲端口(端口被占用时的退路,也是测试与冒烟用的写法)。
39
+ 日志写在 <数据根>/logs/host.log(按大小轮转)。
40
+
41
+ agents-host token create [--note <备注>] [--ai-zen-dir <目录>]
42
+ 新建一串凭证。明文只显示这一次。
43
+ agents-host token list [--ai-zen-dir <目录>]
44
+ 列出全部凭证(含已吊销的)。
45
+ agents-host token revoke <凭证 id> [--ai-zen-dir <目录>]
46
+ 单独吊销一串凭证。
47
+
48
+ agents-host version
49
+ 打印宿主版本与协议版本。
50
+
51
+ --help / -h 打印这段说明。`;
52
+ /** 参数用法错误:带着一句给人看的话,由入口翻成退出码 2 */
53
+ class UsageError extends Error {
54
+ }
55
+ /**
56
+ * 解析命令行。
57
+ *
58
+ * 顺序不敏感(`--port` 可以写在子命令前后):脚本里两种写法都有人用,为此出错不值当。
59
+ */
60
+ function parseArgs(argv) {
61
+ const parsed = { command: "serve", local: true };
62
+ const words = [];
63
+ /** `--local` / `--public` 有没有被人明确写出来(用来发现与 `--host` 的矛盾) */
64
+ let localGiven = false;
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const arg = argv[index] ?? "";
67
+ switch (arg) {
68
+ case "-h":
69
+ case "--help":
70
+ parsed.command = "help";
71
+ return parsed;
72
+ case "--local":
73
+ parsed.local = true;
74
+ localGiven = true;
75
+ break;
76
+ case "--public":
77
+ parsed.local = false;
78
+ localGiven = true;
79
+ break;
80
+ case "--port": {
81
+ const value = argv[index + 1];
82
+ if (value === undefined)
83
+ throw new UsageError("--port 后面要跟一个端口号");
84
+ parsed.port = parsePort(value);
85
+ index += 1;
86
+ break;
87
+ }
88
+ case "--host": {
89
+ const value = argv[index + 1];
90
+ if (value === undefined || value.trim() === "") {
91
+ throw new UsageError("--host 后面要跟一个监听地址");
92
+ }
93
+ parsed.host = value.trim();
94
+ index += 1;
95
+ break;
96
+ }
97
+ case "--note": {
98
+ const value = argv[index + 1];
99
+ if (value === undefined)
100
+ throw new UsageError("--note 后面要跟一句备注");
101
+ parsed.note = value;
102
+ index += 1;
103
+ break;
104
+ }
105
+ case "--ai-zen-dir": {
106
+ const value = argv[index + 1];
107
+ if (value === undefined || value.trim() === "") {
108
+ throw new UsageError("--ai-zen-dir 后面要跟一个目录");
109
+ }
110
+ parsed.aiZenDir = value;
111
+ index += 1;
112
+ break;
113
+ }
114
+ default:
115
+ if (arg.startsWith("-"))
116
+ throw new UsageError(`不认识的参数:${arg}`);
117
+ words.push(arg);
118
+ }
119
+ }
120
+ const [head, ...rest] = words;
121
+ if (head !== undefined) {
122
+ if (head === "serve") {
123
+ if (rest.length > 0)
124
+ throw new UsageError(`serve 不接受多余的位置参数:${rest.join(" ")}`);
125
+ parsed.command = "serve";
126
+ }
127
+ else if (head === "version") {
128
+ if (rest.length > 0)
129
+ throw new UsageError(`version 不接受位置参数:${rest.join(" ")}`);
130
+ parsed.command = "version";
131
+ }
132
+ else if (head === "token") {
133
+ applyTokenArgs(parsed, rest);
134
+ }
135
+ else {
136
+ throw new UsageError(`不认识的命令:${head}`);
137
+ }
138
+ }
139
+ // 监听地址决定了这是哪种实例(PRD A6:本地实例只监听回环)
140
+ if (parsed.host !== undefined) {
141
+ if (isLoopbackHost(parsed.host)) {
142
+ parsed.local = true;
143
+ }
144
+ else if (localGiven && parsed.local) {
145
+ throw new UsageError(`--local 与对外地址 ${parsed.host} 互相矛盾:本地实例只监听回环`);
146
+ }
147
+ else {
148
+ parsed.local = false;
149
+ }
150
+ }
151
+ return parsed;
152
+ }
153
+ function applyTokenArgs(parsed, rest) {
154
+ const [action, ...tail] = rest;
155
+ if (action === "create" || action === "list") {
156
+ if (tail.length > 0)
157
+ throw new UsageError(`token ${action} 不接受位置参数:${tail.join(" ")}`);
158
+ parsed.command = "token";
159
+ parsed.action = action;
160
+ return;
161
+ }
162
+ if (action === "revoke") {
163
+ const id = tail[0];
164
+ if (id === undefined)
165
+ throw new UsageError("token revoke 后面要跟一个凭证 id");
166
+ if (tail.length > 1)
167
+ throw new UsageError(`token revoke 只接受一个 id:${tail.join(" ")}`);
168
+ parsed.command = "token";
169
+ parsed.action = "revoke";
170
+ parsed.credentialId = id;
171
+ return;
172
+ }
173
+ throw new UsageError(`不认识的 token 动作:${action ?? "(空)"}(可用:create / list / revoke)`);
174
+ }
175
+ /**
176
+ * 端口:0..65535 的整数,`0` 表示让系统分配。
177
+ *
178
+ * 写错就当场说,不静默回落到默认端口(R12 的口径:静默回落会造成「我配了 9000 却在 8790 上」)。
179
+ */
180
+ function parsePort(value) {
181
+ const port = Number(value);
182
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
183
+ throw new UsageError(`端口要写成 0..65535 的整数(0 表示让系统分配),收到的是:${value}`);
184
+ }
185
+ return port;
186
+ }
187
+ /** 这个地址是不是回环(本地实例只监听回环,PRD A6) */
188
+ function isLoopbackHost(host) {
189
+ return host === "localhost" || host === "::1" || host.startsWith("127.");
190
+ }
191
+ /**
192
+ * 开库做一件凭证相关的事(不启服务)。
193
+ *
194
+ * 与 `HostApp` 用同一个库文件与同一套迁移:命令行建出来的凭证,起服务后立刻能用。
195
+ */
196
+ async function withCredentials(paths, work) {
197
+ await mkdir(paths.dataRoot, { recursive: true });
198
+ const db = new Database(paths.dbFile);
199
+ try {
200
+ await migrate(db);
201
+ return await work(new CredentialService({ credentials: new CredentialRepository(db) }));
202
+ }
203
+ finally {
204
+ await db.close();
205
+ }
206
+ }
207
+ /** `token create`:明文只在这里出现一次 */
208
+ async function runTokenCreate(paths, note) {
209
+ const result = await withCredentials(paths, (credentials) => credentials.create({ note: note ?? "" }));
210
+ process.stdout.write(`凭证已创建(id: ${result.credential.id})\n`);
211
+ if (result.credential.note !== "")
212
+ process.stdout.write(`备注:${result.credential.note}\n`);
213
+ process.stdout.write(`\n${result.token}\n\n`);
214
+ process.stdout.write("上面这串凭证只显示这一次:宿主此后只保存它的摘要,丢了只能重新创建。\n");
215
+ process.stdout.write("把它分发给要连接这台宿主的设备,对方在握手时带上它即可。\n");
216
+ }
217
+ /** `token list`:连已吊销的一起列,吊销状态看中间那列 */
218
+ async function runTokenList(paths) {
219
+ const result = await withCredentials(paths, (credentials) => credentials.list());
220
+ if (result.credentials.length === 0) {
221
+ process.stdout.write("还没有任何凭证。用 `agents-host token create --note <备注>` 创建一串。\n");
222
+ return;
223
+ }
224
+ for (const credential of result.credentials) {
225
+ const state = credential.revokedAt === null ? "有效" : `已吊销(${credential.revokedAt})`;
226
+ process.stdout.write(`${credential.id} ${state} 创建于 ${credential.createdAt} ${credential.note}\n`);
227
+ }
228
+ }
229
+ /** `token revoke`:`revoked: false` 表示这串本来就已经吊销了(不是失败,R42) */
230
+ async function runTokenRevoke(paths, credentialId) {
231
+ const result = await withCredentials(paths, (credentials) => credentials.revoke({ credentialId }));
232
+ process.stdout.write(result.revoked ? `已吊销:${credentialId}\n` : `这串凭证本来就已吊销:${credentialId}\n`);
233
+ }
234
+ /**
235
+ * `serve`:前台起服务。
236
+ *
237
+ * 启动后打印四样东西:实际监听地址(端口可能是系统分配的)、版本、发现文件、本地凭证交付文件
238
+ * —— 客户端要连的就是前两个,本机免输凭证靠的是第三个(PRD A5 / architecture §2)。
239
+ * 日志落在哪儿也报一行(PRD F-J2:界面被占满时那是唯一的去处)。
240
+ *
241
+ * 端口的三档来源(D25 / 裁定 R67):`--port` > `<数据根>/host.json` 的 `port` > 默认 8790。
242
+ * 配置文件那一档在这里读:读坏了(不是 JSON、端口不合法)就当场失败,不静默回落。
243
+ *
244
+ * 同一个数据根上已经有人在服务时(单实例),本进程只是把那个地址报出来就退出:
245
+ * 两个进程各写一份 `host.json` 会让客户端连谁都不对。
246
+ */
247
+ async function runServe(parsed) {
248
+ const paths = resolvePaths(parsed.aiZenDir);
249
+ // 参数给了端口就不读配置文件:那一档这时轮不到它说话
250
+ const configPort = parsed.port === undefined ? (await readHostSettings(paths)).port : undefined;
251
+ const app = await createHost({
252
+ aiZenDir: parsed.aiZenDir,
253
+ port: parsed.port,
254
+ configPort,
255
+ host: parsed.host,
256
+ local: parsed.local,
257
+ });
258
+ const target = await app.start();
259
+ if (app.attached) {
260
+ process.stdout.write(`已有宿主在运行:ws://${target.host}:${target.port}\n`);
261
+ process.stdout.write(`发现文件:${app.paths.hostFile}\n`);
262
+ process.stdout.write("本进程不重复监听,直接退出。\n");
263
+ await app.stop("requested");
264
+ return;
265
+ }
266
+ process.stdout.write(`宿主已启动:ws://${target.host}:${target.port}\n`);
267
+ process.stdout.write(`版本:${app.version}(协议 ${PROTOCOL_VERSION})\n`);
268
+ process.stdout.write(`发现文件:${app.paths.hostFile}\n`);
269
+ process.stdout.write(`日志:${app.paths.logsDir}\n`);
270
+ if (app.local) {
271
+ process.stdout.write(`本地凭证交付文件:${app.paths.tokenFile}\n`);
272
+ }
273
+ else {
274
+ process.stdout.write("对外实例:用 `agents-host token create --note <备注>` 创建凭证,再分发给要连接的设备。\n");
275
+ }
276
+ // 优雅退出:等收尾做完(摘发现文件、断连接、回收 Scope、关库)再让进程走
277
+ const stop = () => {
278
+ void app.shutdown("requested");
279
+ };
280
+ process.once("SIGINT", stop);
281
+ process.once("SIGTERM", stop);
282
+ }
283
+ /** 版本:脚本里比对升级(PRD D4 / F-E3) */
284
+ function runVersion() {
285
+ process.stdout.write(`@ai-zen/agents-host ${resolveHostVersion()}(协议 ${PROTOCOL_VERSION})\n`);
286
+ }
287
+ /**
288
+ * 失败原因(人话)。
289
+ *
290
+ * `HostError` 带文案键与参数,按命令行的语言渲染一句;其余错误只说它说了什么 —— 栈不进
291
+ * 终端,它只对改代码的人有用。
292
+ */
293
+ function describeFailure(error) {
294
+ if (HostError.is(error)) {
295
+ const data = toErrorData(error, CLI_LANGUAGE);
296
+ const detail = error.detail;
297
+ const hint = typeof detail === "object" && detail !== null && typeof detail.path === "string"
298
+ ? `(${detail.path})`
299
+ : "";
300
+ return `${data.message}${hint}`;
301
+ }
302
+ return error instanceof Error ? error.message : String(error);
303
+ }
304
+ /**
305
+ * 入口:返回退出码。
306
+ *
307
+ * 用法错误走 2、运行失败走 1、成功走 0 —— 脚本据此分支,不去解析句子。
308
+ */
309
+ export async function main(argv) {
310
+ let parsed;
311
+ try {
312
+ parsed = parseArgs(argv);
313
+ }
314
+ catch (error) {
315
+ if (error instanceof UsageError) {
316
+ process.stderr.write(`${error.message}\n\n${USAGE}\n`);
317
+ return EXIT_USAGE;
318
+ }
319
+ throw error;
320
+ }
321
+ if (parsed.command === "help") {
322
+ process.stdout.write(`${USAGE}\n`);
323
+ return 0;
324
+ }
325
+ try {
326
+ if (parsed.command === "version") {
327
+ runVersion();
328
+ return 0;
329
+ }
330
+ const paths = resolvePaths(parsed.aiZenDir);
331
+ if (parsed.command === "token") {
332
+ if (parsed.action === "create")
333
+ await runTokenCreate(paths, parsed.note);
334
+ else if (parsed.action === "list")
335
+ await runTokenList(paths);
336
+ else
337
+ await runTokenRevoke(paths, parsed.credentialId ?? "");
338
+ return 0;
339
+ }
340
+ await runServe(parsed);
341
+ // 服务在后台跑(监听、推送),主流程不阻塞在某个 Promise 上
342
+ return 0;
343
+ }
344
+ catch (error) {
345
+ process.stderr.write(`${describeFailure(error)}\n`);
346
+ return EXIT_FAILURE;
347
+ }
348
+ }
349
+ // 本文件就是进程入口(`bin.agents-host` → `dist/cli.js`),不做「被 import 时不执行」的分支:
350
+ // 库里没有任何地方 import 它,带了那个分支只会在符号链接安装(`node_modules/.bin`)时静默失效。
351
+ void main(process.argv.slice(2)).then((code) => {
352
+ if (code !== 0)
353
+ process.exitCode = code;
354
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 路径与监听的来源(plan §1 的 `config.ts` 契约)。
3
+ *
4
+ * 这一层只回答两个问题:**文件放哪**、**听哪个地址**。它不建目录、不读配置文件内容、
5
+ * 不认识业务对象 —— 目录的创建归启动流程,配置内容归 SDK 的 `ConfigManager`。
6
+ *
7
+ * `resolvePaths` 不从进程的工作目录推导任何东西:宿主是常驻后台进程,它的工作目录
8
+ * 由启动方式决定(可能是个空目录、可能是用户后来删掉的目录),拿它当数据根会让「同一台机器上的
9
+ * 数据在哪」随启动方式漂移。数据根只有一个来源:显式参数 → `AI_ZEN_DIR` → 用户目录下的 `~/.ai-zen`。
10
+ */
11
+ /** 数据根下的路径(`architecture.md` §2 的目录图) */
12
+ export interface HostPaths {
13
+ /** 数据根(`AI_ZEN_DIR`,缺省 `~/.ai-zen`) */
14
+ dataRoot: string;
15
+ /** 库文件:`<dataRoot>/ai-zen.db` */
16
+ dbFile: string;
17
+ /** 附件目录:`<dataRoot>/assets` */
18
+ assetsDir: string;
19
+ /** 运行期目录:`<dataRoot>/run` */
20
+ runDir: string;
21
+ /** 发现文件:`<dataRoot>/run/host.json`(这台机器上谁在服务的唯一真相) */
22
+ hostFile: string;
23
+ /** 本地凭证交付文件:`<dataRoot>/run/token` */
24
+ tokenFile: string;
25
+ /** SDK 的配置文件:`<dataRoot>/config.json`(配置唯一真相,裁定 R13) */
26
+ configFile: string;
27
+ /** 宿主自己的设置文件:`<dataRoot>/host.json`(D25 的「配置文件」那一档,裁定 R67) */
28
+ hostConfigFile: string;
29
+ /** 日志目录:`<dataRoot>/logs`(PRD F-J2:界面被占用时日志另有去处) */
30
+ logsDir: string;
31
+ }
32
+ /** 数据根环境变量(PRD F-J4:数据根可由它改,测试与多实例都靠它) */
33
+ export declare const AI_ZEN_DIR_ENV = "AI_ZEN_DIR";
34
+ /** 默认端口(PRD D25) */
35
+ export declare const DEFAULT_PORT = 8790;
36
+ /** 本地实例监听的地址(PRD A6:本地实例只监听回环) */
37
+ export declare const LOOPBACK_HOST = "127.0.0.1";
38
+ /** 对外实例监听的地址(创建者自己起、自己配凭证,PRD A6) */
39
+ export declare const PUBLIC_HOST = "0.0.0.0";
40
+ /**
41
+ * 数据根:显式参数 → 环境变量 → `~/.ai-zen`。
42
+ *
43
+ * 空字符串按「没给」处理:`AI_ZEN_DIR=` 这种写法在脚本里很常见,让它变成「当前目录」会
44
+ * 把数据散落到不可预期的位置。
45
+ */
46
+ export declare function resolveDataRoot(aiZenDir?: string): string;
47
+ /** 按数据根推出全部路径(库、附件、运行期目录、发现文件、凭证文件、两份配置文件、日志目录) */
48
+ export declare function resolvePaths(aiZenDir?: string): HostPaths;
49
+ /** 监听目标:地址 + 端口 */
50
+ export interface ListenTarget {
51
+ host: string;
52
+ port: number;
53
+ }
54
+ /** `resolveListenTarget` 的入参 */
55
+ export interface ListenTargetInput {
56
+ /** 启动参数给的端口(优先级最高,PRD D25) */
57
+ port?: number;
58
+ /** 配置文件里的端口(第二优先) */
59
+ configPort?: number;
60
+ /** 是不是本地实例:`true` 只监听回环,`false` 监听对外地址(PRD A6) */
61
+ local: boolean;
62
+ }
63
+ /**
64
+ * 决定监听哪个地址、哪个端口。
65
+ *
66
+ * 端口优先级:参数 > 配置文件 > 默认(PRD D25)。**显式给出的值不合法就报错**(裁定 R67):
67
+ * 静默回落会造成「我配了 9000 却在 8790 上」这种查不出来的偏差,所以只有「完全没给」才落到
68
+ * 下一顺位。
69
+ *
70
+ * 两个来源的合法范围不同:启动参数允许 `0`(让系统分配,用例与冒烟都用它),配置文件里的
71
+ * 端口必须是真端口 —— 写 `0` 的意图只能是「每次启动换一个」,而这件事在配置文件里说不清。
72
+ */
73
+ export declare function resolveListenTarget(input: ListenTargetInput): ListenTarget;
74
+ /** 端口的三档优先级与两套取值范围(分出来是为了让调用方与用例都能单独验这一跳) */
75
+ export declare function resolvePort(port: number | undefined, configPort: number | undefined): number;
76
+ /** 端口是否是 1..65535 的整数 */
77
+ export declare function isValidPort(value: unknown): value is number;
78
+ /** 启动参数里的端口:`0` 也认(交给系统分配),其余同 {@link isValidPort} */
79
+ export declare function isPortOrAuto(value: unknown): value is number;
80
+ /**
81
+ * 宿主设置文件的形状。
82
+ *
83
+ * 只放「宿主自己的启动参数」这类东西,`config.json` 那一份归 SDK 管(R13)。眼下只有一个
84
+ * `port`:D25 的中间那一档需要一个真实来源,而不是让 `configPort` 只能由宿主程序注入。
85
+ */
86
+ export interface HostSettings {
87
+ /** 端口;缺省表示「这一档没说话」,落到默认端口 */
88
+ port?: number;
89
+ }
90
+ /**
91
+ * 读 `<dataRoot>/host.json`。
92
+ *
93
+ * 三种情况分开:文件不在 → 空设置(这一档没说话);读不出来 / 不是 JSON 对象 → `config_invalid`
94
+ * (写坏了要让人看见,不能当没有,否则用户改完没有任何反馈);`port` 在但不是可用端口 →
95
+ * `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
96
+ */
97
+ export declare function readHostSettings(paths: HostPaths): Promise<HostSettings>;
package/dist/config.js ADDED
@@ -0,0 +1,171 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { HostError } from "./wire/errors.js";
5
+ /** 数据根目录名(缺省落在用户目录下) */
6
+ const DEFAULT_DIR_NAME = ".ai-zen";
7
+ /** 库文件名 */
8
+ const DB_FILE_NAME = "ai-zen.db";
9
+ /** 运行期子目录名 */
10
+ const RUN_DIR_NAME = "run";
11
+ /** 发现文件名 */
12
+ const HOST_FILE_NAME = "host.json";
13
+ /** 本地凭证交付文件名 */
14
+ const TOKEN_FILE_NAME = "token";
15
+ /** 附件子目录名 */
16
+ const ASSETS_DIR_NAME = "assets";
17
+ /** SDK 配置文件名(`<dataRoot>/config.json`,裁定 R13) */
18
+ const CONFIG_FILE_NAME = "config.json";
19
+ /** 宿主设置文件名(`<dataRoot>/host.json`,裁定 R67) */
20
+ const HOST_CONFIG_FILE_NAME = "host.json";
21
+ /** 日志子目录名(PRD F-J2) */
22
+ const LOGS_DIR_NAME = "logs";
23
+ /** 数据根环境变量(PRD F-J4:数据根可由它改,测试与多实例都靠它) */
24
+ export const AI_ZEN_DIR_ENV = "AI_ZEN_DIR";
25
+ /** 默认端口(PRD D25) */
26
+ export const DEFAULT_PORT = 8790;
27
+ /** 本地实例监听的地址(PRD A6:本地实例只监听回环) */
28
+ export const LOOPBACK_HOST = "127.0.0.1";
29
+ /** 对外实例监听的地址(创建者自己起、自己配凭证,PRD A6) */
30
+ export const PUBLIC_HOST = "0.0.0.0";
31
+ /**
32
+ * 数据根:显式参数 → 环境变量 → `~/.ai-zen`。
33
+ *
34
+ * 空字符串按「没给」处理:`AI_ZEN_DIR=` 这种写法在脚本里很常见,让它变成「当前目录」会
35
+ * 把数据散落到不可预期的位置。
36
+ */
37
+ export function resolveDataRoot(aiZenDir) {
38
+ const explicit = normalizeDir(aiZenDir);
39
+ if (explicit)
40
+ return explicit;
41
+ const fromEnv = normalizeDir(process.env[AI_ZEN_DIR_ENV]);
42
+ if (fromEnv)
43
+ return fromEnv;
44
+ return join(homedir(), DEFAULT_DIR_NAME);
45
+ }
46
+ /** 按数据根推出全部路径(库、附件、运行期目录、发现文件、凭证文件、两份配置文件、日志目录) */
47
+ export function resolvePaths(aiZenDir) {
48
+ const dataRoot = resolveDataRoot(aiZenDir);
49
+ const runDir = join(dataRoot, RUN_DIR_NAME);
50
+ return {
51
+ dataRoot,
52
+ dbFile: join(dataRoot, DB_FILE_NAME),
53
+ assetsDir: join(dataRoot, ASSETS_DIR_NAME),
54
+ runDir,
55
+ hostFile: join(runDir, HOST_FILE_NAME),
56
+ tokenFile: join(runDir, TOKEN_FILE_NAME),
57
+ configFile: join(dataRoot, CONFIG_FILE_NAME),
58
+ hostConfigFile: join(dataRoot, HOST_CONFIG_FILE_NAME),
59
+ logsDir: join(dataRoot, LOGS_DIR_NAME),
60
+ };
61
+ }
62
+ function normalizeDir(value) {
63
+ if (typeof value !== "string")
64
+ return undefined;
65
+ const trimmed = value.trim();
66
+ return trimmed === "" ? undefined : trimmed;
67
+ }
68
+ /**
69
+ * 决定监听哪个地址、哪个端口。
70
+ *
71
+ * 端口优先级:参数 > 配置文件 > 默认(PRD D25)。**显式给出的值不合法就报错**(裁定 R67):
72
+ * 静默回落会造成「我配了 9000 却在 8790 上」这种查不出来的偏差,所以只有「完全没给」才落到
73
+ * 下一顺位。
74
+ *
75
+ * 两个来源的合法范围不同:启动参数允许 `0`(让系统分配,用例与冒烟都用它),配置文件里的
76
+ * 端口必须是真端口 —— 写 `0` 的意图只能是「每次启动换一个」,而这件事在配置文件里说不清。
77
+ */
78
+ export function resolveListenTarget(input) {
79
+ return {
80
+ host: input.local ? LOOPBACK_HOST : PUBLIC_HOST,
81
+ port: resolvePort(input.port, input.configPort),
82
+ };
83
+ }
84
+ /** 端口的三档优先级与两套取值范围(分出来是为了让调用方与用例都能单独验这一跳) */
85
+ export function resolvePort(port, configPort) {
86
+ if (port !== undefined) {
87
+ if (!isPortOrAuto(port))
88
+ throw invalidPort("port", port, "launch_argument");
89
+ return port;
90
+ }
91
+ if (configPort !== undefined) {
92
+ if (!isValidPort(configPort))
93
+ throw invalidPort("configPort", configPort, "host.json");
94
+ return configPort;
95
+ }
96
+ return DEFAULT_PORT;
97
+ }
98
+ /** 端口是否是 1..65535 的整数 */
99
+ export function isValidPort(value) {
100
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
101
+ }
102
+ /** 启动参数里的端口:`0` 也认(交给系统分配),其余同 {@link isValidPort} */
103
+ export function isPortOrAuto(value) {
104
+ return value === 0 || isValidPort(value);
105
+ }
106
+ /**
107
+ * 端口写得不对。
108
+ *
109
+ * 码是 `config_invalid`(PRD F-A8:配置类问题要能当场修),`detail` 说明哪个来源给的什么值,
110
+ * 界面据此把光标放到那一栏(`host.json` 的 `port`,或启动参数)。
111
+ */
112
+ function invalidPort(field, value, source) {
113
+ return new HostError("config_invalid", {
114
+ messageKey: "error.portInvalid",
115
+ params: { value: String(value), source },
116
+ detail: { field, value, source, reason: "not_a_usable_port", allowed: "1..65535, 0 (launch argument only)" },
117
+ });
118
+ }
119
+ /**
120
+ * 读 `<dataRoot>/host.json`。
121
+ *
122
+ * 三种情况分开:文件不在 → 空设置(这一档没说话);读不出来 / 不是 JSON 对象 → `config_invalid`
123
+ * (写坏了要让人看见,不能当没有,否则用户改完没有任何反馈);`port` 在但不是可用端口 →
124
+ * `config_invalid`(裁定 R67:显式给出的非法值报错,不静默回落)。
125
+ */
126
+ export async function readHostSettings(paths) {
127
+ let raw;
128
+ try {
129
+ raw = await readFile(paths.hostConfigFile, "utf8");
130
+ }
131
+ catch (error) {
132
+ // 文件不在是正常情况(绝大多数据根上就没有这份文件)
133
+ if (isMissingFile(error))
134
+ return {};
135
+ throw new HostError("config_invalid", {
136
+ messageKey: "config.fileBroken",
137
+ params: { path: paths.hostConfigFile, reason: "unreadable" },
138
+ detail: { path: paths.hostConfigFile, reason: "unreadable" },
139
+ });
140
+ }
141
+ let parsed;
142
+ try {
143
+ parsed = JSON.parse(raw);
144
+ }
145
+ catch {
146
+ throw new HostError("config_invalid", {
147
+ messageKey: "config.fileBroken",
148
+ params: { path: paths.hostConfigFile, reason: "not_json" },
149
+ detail: { path: paths.hostConfigFile, reason: "not_json" },
150
+ });
151
+ }
152
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
153
+ throw new HostError("config_invalid", {
154
+ messageKey: "config.fileBroken",
155
+ params: { path: paths.hostConfigFile, reason: "not_an_object" },
156
+ detail: { path: paths.hostConfigFile, reason: "not_an_object" },
157
+ });
158
+ }
159
+ const record = parsed;
160
+ if (!Object.prototype.hasOwnProperty.call(record, "port") || record.port === null)
161
+ return {};
162
+ const port = record.port;
163
+ if (!isValidPort(port))
164
+ throw invalidPort("port", port, "host.json");
165
+ return { port };
166
+ }
167
+ function isMissingFile(error) {
168
+ return (typeof error === "object" &&
169
+ error !== null &&
170
+ error.code === "ENOENT");
171
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `agent.*` 的接线壳(F-B1…F-B7 / F-F5 / F-G3)。
3
+ *
4
+ * 与 `config.*` 同一套口径:入参直接交给服务,出参里的句子按这条连接的语言重渲染
5
+ * (D8)。业务判断在 `services/AgentService.ts`。
6
+ */
7
+ import type { HandlerTable } from "../net/WsHub.js";
8
+ import type { AgentService } from "../services/AgentService.js";
9
+ /** 本文件负责的方法名 */
10
+ export type AgentHandlerName = "agent.list" | "agent.get" | "agent.create" | "agent.update" | "agent.delete" | "agent.setModel" | "agent.setThinking" | "agent.restoreFactory";
11
+ /** 本文件产出的 handler 表片段 */
12
+ export type AgentHandlers = Pick<HandlerTable, AgentHandlerName>;
13
+ /** 接线壳的依赖 */
14
+ export interface AgentHandlerDeps {
15
+ agents: AgentService;
16
+ }
17
+ /** 造出 `agent.*` 的 handler 表 */
18
+ export declare function createAgentHandlers(deps: AgentHandlerDeps): AgentHandlers;