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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +21 -0
  7. package/dist/cli.js +353 -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
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 自动命名(PRD D2 / F-C4)。
3
+ *
4
+ * 触发条件三件事同时成立才起名,缺一不动:
5
+ *
6
+ * 1. **第一轮结束之后**(本文件按「这段对话里只有一条用户消息」判定:第一轮刚结束就是这个
7
+ * 形状,第二轮之后就不再满足);
8
+ * 2. **用户没改过名字**(`conversations.nameIsAuto`,R35 的那一列;不由名字内容反推);
9
+ * 3. **开关开着**(`AppConfig.autoTitle`,缺省视为开 —— 老配置里没有这个字段,用户的意图
10
+ * 显然是「按出厂的样子跑」)。
11
+ *
12
+ * 用哪一个模型:「取名模型」`AppConfig.namingModel`,没配就回落 `defaultModel`;两个都拿不到
13
+ * 就**保持原名**(原名 = 创建时间,D2)。起不出名(调用失败 / 模型给了个空串)同样保持原名,
14
+ * 不报错、不重试 —— 改名是锦上添花,不该在界面上留一条失败提示。
15
+ *
16
+ * 写回走 `applyAutoName`(而不是用户改名那个方法):它把 `nameIsAuto` 置 1,于是「这个名字是
17
+ * 自动起的」这个事实在库里继续成立(R35)。
18
+ */
19
+ import type { ConfigManager } from "@ai-zen/agents-sdk";
20
+ import type { Language } from "../i18n/index.js";
21
+ import type { ChatEventEmitter } from "../runtime/ChatRunner.js";
22
+ import type { ConversationRepository } from "../storage/ConversationRepository.js";
23
+ import type { MessageRepository } from "../storage/MessageRepository.js";
24
+ import type { ProblemFact } from "../wire/index.js";
25
+ import type { NoticeService } from "./NoticeService.js";
26
+ /** 一次「取名」调用的形状(用例注入替身,不必连模型) */
27
+ export type TitleCompleter = (input: {
28
+ system: string;
29
+ user: string;
30
+ modelId: string;
31
+ }) => Promise<string>;
32
+ /** 为什么这次没起名(内部结果,用于测试与日志;不出网) */
33
+ export type AutoNameSkip = "conversation_missing" | "config_unreadable" | "disabled" | "renamed_by_user" | "not_first_turn" | "already_named" | "no_model" | "empty_title" | "call_failed";
34
+ /** 一次自动命名的结果 */
35
+ export interface AutoNameOutcome {
36
+ /** 这次有没有真的改名 */
37
+ named: boolean;
38
+ /** 结果名字(没改名时是原来的名字) */
39
+ name: string;
40
+ /** 没改名时是原因;改了就为 `null` */
41
+ skipped: AutoNameSkip | null;
42
+ /** 起不出名的事实(失败时给结构化原因,便于日志与测试断言) */
43
+ problem: ProblemFact | null;
44
+ }
45
+ export interface AutoNamingDeps {
46
+ conversations: ConversationRepository;
47
+ messages: MessageRepository;
48
+ configManager: ConfigManager;
49
+ /** 取名调用;缺省走 SDK 的 `completeOnce`(与交接摘要共用同一份请求体形状) */
50
+ complete?: TitleCompleter;
51
+ /** 通知通道(起好名字后那条「给它起名叫…」的提示,F-I8) */
52
+ notices?: NoticeService;
53
+ emit?: ChatEventEmitter;
54
+ now?: () => number;
55
+ }
56
+ export declare class AutoNaming {
57
+ private readonly conversations;
58
+ private readonly messages;
59
+ private readonly configManager;
60
+ private readonly complete;
61
+ private readonly notices?;
62
+ private readonly emit?;
63
+ private readonly now;
64
+ /**
65
+ * 本进程里已经起过名的对话。
66
+ *
67
+ * 库里的 `nameIsAuto` 区分不了「没起过」与「自动起过」(R35 两条都置 1),而「第一轮」
68
+ * 这条判定在同一个轮次里连续调两次时会重复成立。这一份只防「同一进程内重复起名」,
69
+ * 重启后若仍是第一轮会再起一次 —— 那次起名同样只在第一轮里发生。
70
+ */
71
+ private readonly named;
72
+ constructor(deps: AutoNamingDeps);
73
+ /**
74
+ * 一轮结束之后调一次(接线层在 `conversation.send` 结算后调用)。
75
+ *
76
+ * 这个方法是幂等且可反复调用的:条件不成立时什么都不做(直接返回原因),所以接在每一轮
77
+ * 之后都不会多做一次请求 —— 第二轮之后 `not_first_turn` 会把后面的调用全部挡住。
78
+ */
79
+ afterTurn(conversationId: string, language?: Language): Promise<AutoNameOutcome>;
80
+ /** 这段对话当前该不该起名(接线层在调用前想做别的判断时用;不改任何状态) */
81
+ shouldName(conversationId: string): Promise<{
82
+ should: boolean;
83
+ reason: AutoNameSkip | null;
84
+ }>;
85
+ /**
86
+ * 缺省的取名调用:用 SDK 的 `completeOnce`(与交接摘要共用同一份请求体形状)。
87
+ *
88
+ * 客户端按配置里的模型现场装配(`createModelFromConfig`)—— 起名是一次几十 token 的小
89
+ * 请求,为它拉一个 Scope(会连带连 MCP)代价与收益不成比例,所以这里不借 Scope。
90
+ */
91
+ private completeOnce;
92
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * 自动命名(PRD D2 / F-C4)。
3
+ *
4
+ * 触发条件三件事同时成立才起名,缺一不动:
5
+ *
6
+ * 1. **第一轮结束之后**(本文件按「这段对话里只有一条用户消息」判定:第一轮刚结束就是这个
7
+ * 形状,第二轮之后就不再满足);
8
+ * 2. **用户没改过名字**(`conversations.nameIsAuto`,R35 的那一列;不由名字内容反推);
9
+ * 3. **开关开着**(`AppConfig.autoTitle`,缺省视为开 —— 老配置里没有这个字段,用户的意图
10
+ * 显然是「按出厂的样子跑」)。
11
+ *
12
+ * 用哪一个模型:「取名模型」`AppConfig.namingModel`,没配就回落 `defaultModel`;两个都拿不到
13
+ * 就**保持原名**(原名 = 创建时间,D2)。起不出名(调用失败 / 模型给了个空串)同样保持原名,
14
+ * 不报错、不重试 —— 改名是锦上添花,不该在界面上留一条失败提示。
15
+ *
16
+ * 写回走 `applyAutoName`(而不是用户改名那个方法):它把 `nameIsAuto` 置 1,于是「这个名字是
17
+ * 自动起的」这个事实在库里继续成立(R35)。
18
+ */
19
+ import { AgentNS } from "@ai-zen/agents-core";
20
+ import { completeOnce, createModelFromConfig } from "@ai-zen/agents-sdk";
21
+ import { DEFAULT_LANGUAGE, render } from "../i18n/index.js";
22
+ import { toAgentMessage } from "./MessageService.js";
23
+ /** 起名时喂给模型的正文上限(够看出在聊什么,又不至于把整段历史发一遍) */
24
+ const PROMPT_CHARS = 2000;
25
+ /** 起出来的名字最长多少个字符(侧栏一行的容量) */
26
+ const MAX_TITLE_CHARS = 60;
27
+ /**
28
+ * 起名的指令。
29
+ *
30
+ * 这是**给模型看**的提示,不是界面文案,所以不走 i18n 键空间(那套是给人看的句子)。
31
+ */
32
+ const TITLE_INSTRUCTION = [
33
+ "You name conversations. Read the exchange below and give it a short title.",
34
+ "Rules: at most 6 words, no quotes, no trailing punctuation, no preamble like \"Title:\".",
35
+ "Name what the task actually is, in the language the user wrote in.",
36
+ "Answer with the title only.",
37
+ ].join("\n");
38
+ function iso(ms) {
39
+ return new Date(ms).toISOString();
40
+ }
41
+ /** 一段消息里的纯文本(分段内容只取文本段) */
42
+ function textOf(content) {
43
+ if (typeof content === "string")
44
+ return content;
45
+ if (Array.isArray(content)) {
46
+ return content
47
+ .map((section) => ("text" in section && typeof section.text === "string" ? section.text : ""))
48
+ .filter((text) => text.length > 0)
49
+ .join("\n");
50
+ }
51
+ return "";
52
+ }
53
+ /** 截断到上限(模型看到的正文不必比这更长) */
54
+ function clip(text, limit) {
55
+ const trimmed = text.trim();
56
+ return trimmed.length > limit ? `${trimmed.slice(0, limit)}…` : trimmed;
57
+ }
58
+ /**
59
+ * 把模型那句「标题」收拾成能放进侧栏的一行。
60
+ *
61
+ * 模型常见的越界只有三种:加引号、写「Title: xxx」、换行写解释。三种都在这里收掉;
62
+ * 收完是空串就当作「起不出名」。
63
+ */
64
+ function sanitizeTitle(raw) {
65
+ let text = raw.trim();
66
+ if (text.length === 0)
67
+ return "";
68
+ text = text.split(/\r?\n/).find((line) => line.trim().length > 0) ?? "";
69
+ text = text.replace(/^\s*(title|name|标题)\s*[::]\s*/i, "").trim();
70
+ text = text.replace(/^["'“”‘’「」『』]+|["'“”‘’「」『』]+$/g, "").trim();
71
+ text = text.replace(/\s+/g, " ").trim();
72
+ text = text.replace(/[。..,,;;::!!??]+$/g, "").trim();
73
+ if (text.length === 0)
74
+ return "";
75
+ return text.length > MAX_TITLE_CHARS ? text.slice(0, MAX_TITLE_CHARS).trim() : text;
76
+ }
77
+ export class AutoNaming {
78
+ conversations;
79
+ messages;
80
+ configManager;
81
+ complete;
82
+ notices;
83
+ emit;
84
+ now;
85
+ /**
86
+ * 本进程里已经起过名的对话。
87
+ *
88
+ * 库里的 `nameIsAuto` 区分不了「没起过」与「自动起过」(R35 两条都置 1),而「第一轮」
89
+ * 这条判定在同一个轮次里连续调两次时会重复成立。这一份只防「同一进程内重复起名」,
90
+ * 重启后若仍是第一轮会再起一次 —— 那次起名同样只在第一轮里发生。
91
+ */
92
+ named = new Set();
93
+ constructor(deps) {
94
+ this.conversations = deps.conversations;
95
+ this.messages = deps.messages;
96
+ this.configManager = deps.configManager;
97
+ this.complete =
98
+ deps.complete ??
99
+ (async (input) => await this.completeOnce(input.system, input.user, input.modelId));
100
+ this.notices = deps.notices;
101
+ this.emit = deps.emit;
102
+ this.now = deps.now ?? Date.now;
103
+ }
104
+ /**
105
+ * 一轮结束之后调一次(接线层在 `conversation.send` 结算后调用)。
106
+ *
107
+ * 这个方法是幂等且可反复调用的:条件不成立时什么都不做(直接返回原因),所以接在每一轮
108
+ * 之后都不会多做一次请求 —— 第二轮之后 `not_first_turn` 会把后面的调用全部挡住。
109
+ */
110
+ async afterTurn(conversationId, language = DEFAULT_LANGUAGE) {
111
+ const conversation = await this.conversations.get(conversationId);
112
+ if (!conversation) {
113
+ return skip("conversation_missing", "", null);
114
+ }
115
+ let config;
116
+ try {
117
+ config = await this.configManager.read();
118
+ }
119
+ catch (error) {
120
+ const reason = error instanceof Error ? error.message : String(error);
121
+ return skip("config_unreadable", conversation.name, failure(reason, language));
122
+ }
123
+ if (config.autoTitle === false)
124
+ return skip("disabled", conversation.name, null);
125
+ if (!conversation.nameIsAuto)
126
+ return skip("renamed_by_user", conversation.name, null);
127
+ if (this.named.has(conversation.id))
128
+ return skip("already_named", conversation.name, null);
129
+ const rows = await this.messages.listByConversation(conversation.id);
130
+ const messages = rows.map((row) => toAgentMessage(row));
131
+ const userMessages = messages.filter((message) => message.role === AgentNS.Role.User);
132
+ if (userMessages.length !== 1)
133
+ return skip("not_first_turn", conversation.name, null);
134
+ // 取名模型:配了就用它,否则回落兜底模型;配置里找不到这个模型也等于「没有取名模型」
135
+ const namingModelId = config.namingModel ?? config.defaultModel;
136
+ if (!namingModelId)
137
+ return skip("no_model", conversation.name, null);
138
+ const namingModel = config.models.find((model) => model.id === namingModelId);
139
+ if (!namingModel)
140
+ return skip("no_model", conversation.name, null);
141
+ const user = buildPrompt(userMessages, messages);
142
+ if (user.length === 0)
143
+ return skip("empty_title", conversation.name, null);
144
+ let title;
145
+ try {
146
+ title = await this.complete({ system: TITLE_INSTRUCTION, user, modelId: namingModel.id });
147
+ }
148
+ catch (error) {
149
+ const reason = error instanceof Error ? error.message : String(error);
150
+ return skip("call_failed", conversation.name, failure(reason, language));
151
+ }
152
+ const name = sanitizeTitle(title);
153
+ if (name.length === 0)
154
+ return skip("empty_title", conversation.name, null);
155
+ const updatedAt = this.now();
156
+ await this.conversations.applyAutoName(conversation.id, name, updatedAt);
157
+ this.named.add(conversation.id);
158
+ this.emit?.("conversation.changed", {
159
+ conversationId: conversation.id,
160
+ workspaceId: conversation.workspaceId,
161
+ change: "renamed",
162
+ messageCount: messages.length,
163
+ at: iso(updatedAt),
164
+ });
165
+ if (this.notices) {
166
+ await this.notices.create({
167
+ level: "info",
168
+ code: "notice.autoNamed",
169
+ params: { name },
170
+ duration: 5,
171
+ conversationId: conversation.id,
172
+ }, language);
173
+ }
174
+ return { named: true, name, skipped: null, problem: null };
175
+ }
176
+ /** 这段对话当前该不该起名(接线层在调用前想做别的判断时用;不改任何状态) */
177
+ async shouldName(conversationId) {
178
+ const conversation = await this.conversations.get(conversationId);
179
+ if (!conversation)
180
+ return { should: false, reason: "conversation_missing" };
181
+ const config = await this.configManager.read();
182
+ if (config.autoTitle === false)
183
+ return { should: false, reason: "disabled" };
184
+ if (!conversation.nameIsAuto)
185
+ return { should: false, reason: "renamed_by_user" };
186
+ const rows = await this.messages.listByConversation(conversationId);
187
+ const userCount = rows.filter((row) => row.role === AgentNS.Role.User).length;
188
+ if (userCount !== 1)
189
+ return { should: false, reason: "not_first_turn" };
190
+ if (!config.namingModel && !config.defaultModel)
191
+ return { should: false, reason: "no_model" };
192
+ return { should: true, reason: null };
193
+ }
194
+ /**
195
+ * 缺省的取名调用:用 SDK 的 `completeOnce`(与交接摘要共用同一份请求体形状)。
196
+ *
197
+ * 客户端按配置里的模型现场装配(`createModelFromConfig`)—— 起名是一次几十 token 的小
198
+ * 请求,为它拉一个 Scope(会连带连 MCP)代价与收益不成比例,所以这里不借 Scope。
199
+ */
200
+ async completeOnce(system, user, modelId) {
201
+ const config = await this.configManager.read();
202
+ const { client, model, modelConfig } = createModelFromConfig(config, modelId);
203
+ return await completeOnce({ client, model, modelConfig, system, user });
204
+ }
205
+ }
206
+ /** 喂给模型的正文:第一条用户消息 + 最后一条助手回复 */
207
+ function buildPrompt(userMessages, messages) {
208
+ const first = userMessages[0];
209
+ const userText = clip(first ? textOf(first.content) : "", PROMPT_CHARS);
210
+ const lastAssistant = [...messages]
211
+ .reverse()
212
+ .find((message) => message.role === AgentNS.Role.Assistant);
213
+ const replyText = clip(lastAssistant ? textOf(lastAssistant.content) : "", PROMPT_CHARS);
214
+ return [
215
+ userText.length > 0 ? `You: ${userText}` : "",
216
+ replyText.length > 0 ? `Assistant: ${replyText}` : "",
217
+ ]
218
+ .filter((line) => line.length > 0)
219
+ .join("\n\n");
220
+ }
221
+ /** 没改名:带上原因 */
222
+ function skip(reason, name, problem) {
223
+ return { named: false, name, skipped: reason, problem };
224
+ }
225
+ /** 起不出名的事实(`autoName.failed`:不报错,只是记下为什么保持原名) */
226
+ function failure(reason, language) {
227
+ const params = { reason };
228
+ return {
229
+ code: "autoName.failed",
230
+ messageKey: "autoName.failed",
231
+ params,
232
+ message: render(language, "autoName.failed", params),
233
+ detail: { reason },
234
+ };
235
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `@` 补全的数据来源:一个项目的文件清单(F-H5 / D7)。
3
+ *
4
+ * 为什么在宿主:远程前端看不到目录(PRD §3 尺子 ①),`@` 菜单列的是磁盘上的真实文件。
5
+ *
6
+ * 口径(D7 逐条):
7
+ * - **按需扫描**:第一次问才扫,扫完缓存一下;
8
+ * - **git 仓库优先 `git ls-files`**:只列 git 认的文件(含未跟踪但没被 ignore 的),天然排除了
9
+ * `node_modules`、构建产物这些不该出现在补全里的东西;
10
+ * - **非 git 用遍历**:跳过 `node_modules` / `.git` / `dist` 三个目录;
11
+ * - **20000 条上限**:到了上限就停,并如实说「被截断了」;
12
+ * - **5s 超时**:超时返回已经读到的那些(可能不完整),并说明它不完整;
13
+ * - **15s 缓存**:15 秒内重复问不重新扫;
14
+ * - **未就绪要说话**:还在扫就回 `scanning`,扫不出来回 `unavailable` + 原因。
15
+ * 假装空目录比慢更糟 —— 界面会以为这个项目里没有文件。
16
+ *
17
+ * 计时与「跑 git」都是注入点:用例用假时钟与假执行器验证超时 / 缓存 / 上限,
18
+ * 不需要真的等 5 秒,也不需要真的有 git。
19
+ */
20
+ import type { CompletionFilesParams, CompletionFilesResult, FileEntryView } from "../wire/index.js";
21
+ import type { WorkspaceCwdLookup } from "./DiscoveryService.js";
22
+ /** 清单条数上限(D7:20000 条) */
23
+ export declare const DEFAULT_MAX_ENTRIES = 20000;
24
+ /** 扫描超时(D7:5s) */
25
+ export declare const DEFAULT_SCAN_TIMEOUT_MS = 5000;
26
+ /** 缓存有效期(D7:15s) */
27
+ export declare const DEFAULT_CACHE_TTL_MS = 15000;
28
+ /** 一次补全默认回多少条 */
29
+ export declare const DEFAULT_LIMIT = 50;
30
+ /** 一次补全最多回多少条 */
31
+ export declare const MAX_LIMIT = 500;
32
+ /** 计时入口(与 `runtime/ScopeRegistry` 的 `Clock` 同形状:`setTimeout` 返回取消函数) */
33
+ export interface CompletionClock {
34
+ now(): number;
35
+ setTimeout(handler: () => void, ms: number): () => void;
36
+ }
37
+ /** 真实时钟 */
38
+ export declare const systemCompletionClock: CompletionClock;
39
+ /** 一次 `git ls-files` 的结果 */
40
+ export interface GitListing {
41
+ ok: boolean;
42
+ /** NUL 分隔输出的那些路径(已经切成一条条) */
43
+ files: string[];
44
+ /** 失败原因(`ok === false` 时给) */
45
+ reason?: string;
46
+ }
47
+ /** 跑 git 的方式(测试注入假的,生产走高定制的 `defaultGitLister`) */
48
+ export type GitLister = (cwd: string, timeoutMs: number) => Promise<GitListing>;
49
+ /** 补全服务的依赖 */
50
+ export interface CompletionServiceOptions {
51
+ /** 按 workspaceId 取 cwd(项目不存在时回 `undefined`) */
52
+ findWorkspace?: WorkspaceCwdLookup;
53
+ /** 跑 git 的方式;缺省用子进程跑 `git ls-files` */
54
+ runGit?: GitLister;
55
+ /** 计时入口;缺省真实时钟 */
56
+ clock?: CompletionClock;
57
+ maxEntries?: number;
58
+ scanTimeoutMs?: number;
59
+ cacheTtlMs?: number;
60
+ }
61
+ export declare class CompletionService {
62
+ private readonly findWorkspace?;
63
+ private readonly runGit;
64
+ private readonly clock;
65
+ private readonly maxEntries;
66
+ private readonly scanTimeoutMs;
67
+ private readonly cacheTtlMs;
68
+ /** workspaceId → 扫描状态(cwd 创建即定死,PRD D9,所以按项目缓存是安全的) */
69
+ private readonly states;
70
+ constructor(options?: CompletionServiceOptions);
71
+ /**
72
+ * `completion.files`(F-H5 / D7)。
73
+ *
74
+ * 三条出口,全部带 `state`:
75
+ * - 有现成清单 → `ready`(可能带「不完整 / 被截断」的说明);
76
+ * - 正在扫 → `scanning`(客户端继续打字时会再来问一次);
77
+ * - 扫不出来 → `unavailable` + 原因。
78
+ */
79
+ files(params: CompletionFilesParams): Promise<CompletionFilesResult>;
80
+ /**
81
+ * 等一次扫描落地(预热与用例用)。
82
+ *
83
+ * 接线层可以在项目被打开时预取一次:这样用户第一次按 `@` 已经是 `ready`。
84
+ */
85
+ prefetch(workspaceId: string): Promise<void>;
86
+ /** 丢掉一个项目的缓存(项目被删时用;下次问会重新扫) */
87
+ forget(workspaceId: string): void;
88
+ /**
89
+ * 扫一次并写状态。
90
+ *
91
+ * 超时不是「失败」:已经读到的那些仍然是真话,所以照常给出去,只是把
92
+ * 「不完整」这件事写在 `problem` 里(D7:未就绪要说话,而不是假装齐了)。
93
+ */
94
+ private scan;
95
+ /**
96
+ * 收集文件路径:git 仓库走 `git ls-files`,否则自己遍历。
97
+ *
98
+ * 失败时把「为什么拿不到」作为 `failure` 回出去(`null` 表示正常拿到)。
99
+ */
100
+ private collect;
101
+ /** 遍历目录(跳过 {@link EXCLUDED_DIRS};符号链接不跟,避免环) */
102
+ private walk;
103
+ /** `<cwd>/.git` 在(目录或 worktree 的 `.git` 文件)就当它是 git 仓库 */
104
+ private isGitRepo;
105
+ /** 给 git 的超时:比整次扫描的预算略小 */
106
+ private gitTimeout;
107
+ /** 跑 `work`,到点就放弃等它(返回 `"timeout"`) */
108
+ private withTimeout;
109
+ /** 状态 → 出参(只切出这次要的那一页) */
110
+ private respond;
111
+ private stateOf;
112
+ private resolveCwd;
113
+ }
114
+ /** 用子进程跑 `git ls-files`;不在仓库里或者没有 git 时回 `ok: false`(调用方退回遍历) */
115
+ export declare const defaultGitLister: GitLister;
116
+ /**
117
+ * 把清单按已输入的前缀切成「目录 + 文件」两类条目。
118
+ *
119
+ * 前缀可以是路径形式(`src/co`):目录部分当过滤基准,最后一段当「还差什么」;
120
+ * 没有斜杠时(`@READ`)同时按路径开头与文件名开头匹配,这样根目录里的文件也找得到。
121
+ */
122
+ export declare function selectEntries(paths: string[], prefix: string, limit: number): FileEntryView[];