@downcity/city 1.1.24 → 1.1.32

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 (192) hide show
  1. package/README.md +5 -11
  2. package/bin/cli/Index.d.ts +1 -1
  3. package/bin/cli/Index.js +4 -6
  4. package/bin/cli/Index.js.map +1 -1
  5. package/bin/cli/agent/AgentChat.js +1 -1
  6. package/bin/cli/agent/AgentChat.js.map +1 -1
  7. package/bin/cli/agent/AgentHistory.d.ts.map +1 -1
  8. package/bin/cli/agent/AgentHistory.js +2 -1
  9. package/bin/cli/agent/AgentHistory.js.map +1 -1
  10. package/bin/cli/agent/AgentManager.js +3 -3
  11. package/bin/cli/agent/AgentManager.js.map +1 -1
  12. package/bin/cli/agent/Init.js +6 -6
  13. package/bin/cli/agent/Init.js.map +1 -1
  14. package/bin/cli/agent/Restart.js +1 -1
  15. package/bin/cli/agent/Restart.js.map +1 -1
  16. package/bin/cli/agent/Run.d.ts.map +1 -1
  17. package/bin/cli/agent/Run.js +6 -7
  18. package/bin/cli/agent/Run.js.map +1 -1
  19. package/bin/cli/agent/Start.js +1 -1
  20. package/bin/cli/agent/Start.js.map +1 -1
  21. package/bin/cli/control-plane/ControlPlaneCommand.d.ts.map +1 -1
  22. package/bin/cli/control-plane/ControlPlaneCommand.js +19 -1
  23. package/bin/cli/control-plane/ControlPlaneCommand.js.map +1 -1
  24. package/bin/cli/control-plane/ControlPlanePublicManager.d.ts +17 -0
  25. package/bin/cli/control-plane/ControlPlanePublicManager.d.ts.map +1 -0
  26. package/bin/cli/control-plane/ControlPlanePublicManager.js +229 -0
  27. package/bin/cli/control-plane/ControlPlanePublicManager.js.map +1 -0
  28. package/bin/cli/control-plane/ControlPlanePublicMode.d.ts +53 -0
  29. package/bin/cli/control-plane/ControlPlanePublicMode.d.ts.map +1 -0
  30. package/bin/cli/control-plane/ControlPlanePublicMode.js +106 -0
  31. package/bin/cli/control-plane/ControlPlanePublicMode.js.map +1 -0
  32. package/bin/cli/control-plane/ControlPlaneRuntime.d.ts.map +1 -1
  33. package/bin/cli/control-plane/ControlPlaneRuntime.js +8 -6
  34. package/bin/cli/control-plane/ControlPlaneRuntime.js.map +1 -1
  35. package/bin/cli/control-plane/ControlPlaneStatus.d.ts.map +1 -1
  36. package/bin/cli/control-plane/ControlPlaneStatus.js +22 -8
  37. package/bin/cli/control-plane/ControlPlaneStatus.js.map +1 -1
  38. package/bin/cli/model/ModelManageCommand.d.ts.map +1 -1
  39. package/bin/cli/model/ModelManageCommand.js +22 -18
  40. package/bin/cli/model/ModelManageCommand.js.map +1 -1
  41. package/bin/cli/model/ModelManager.d.ts +1 -1
  42. package/bin/cli/model/ModelManager.d.ts.map +1 -1
  43. package/bin/cli/model/ModelManager.js +378 -76
  44. package/bin/cli/model/ModelManager.js.map +1 -1
  45. package/bin/cli/model/ModelManagerTypes.d.ts +97 -3
  46. package/bin/cli/model/ModelManagerTypes.d.ts.map +1 -1
  47. package/bin/cli/model/ModelSupport.d.ts +1 -1
  48. package/bin/cli/model/ModelSupport.d.ts.map +1 -1
  49. package/bin/cli/model/ModelSupport.js +1 -1
  50. package/bin/cli/model/ModelSupport.js.map +1 -1
  51. package/bin/cli/shared/Chat.d.ts +1 -1
  52. package/bin/cli/shared/Chat.js +2 -2
  53. package/bin/cli/shared/Chat.js.map +1 -1
  54. package/bin/cli/shared/ChatManager.d.ts +1 -1
  55. package/bin/cli/shared/ChatManager.js +27 -27
  56. package/bin/cli/shared/ChatManager.js.map +1 -1
  57. package/bin/cli/shared/Config.js +1 -1
  58. package/bin/cli/shared/Config.js.map +1 -1
  59. package/bin/cli/shared/ManagedPluginActionCommands.d.ts +14 -0
  60. package/bin/cli/shared/ManagedPluginActionCommands.d.ts.map +1 -0
  61. package/bin/cli/{service/ServiceActionCommands.js → shared/ManagedPluginActionCommands.js} +60 -60
  62. package/bin/cli/shared/ManagedPluginActionCommands.js.map +1 -0
  63. package/bin/cli/shared/ManagedPluginRemote.d.ts +30 -0
  64. package/bin/cli/shared/ManagedPluginRemote.d.ts.map +1 -0
  65. package/bin/cli/{service/ServiceCommandRemote.js → shared/ManagedPluginRemote.js} +34 -34
  66. package/bin/cli/shared/ManagedPluginRemote.js.map +1 -0
  67. package/bin/cli/shared/PluginScheduleCommand.d.ts +36 -0
  68. package/bin/cli/shared/PluginScheduleCommand.d.ts.map +1 -0
  69. package/bin/cli/{service/ServiceScheduleCommand.js → shared/PluginScheduleCommand.js} +41 -41
  70. package/bin/cli/shared/PluginScheduleCommand.js.map +1 -0
  71. package/bin/cli/{service/ServiceCommandSupport.d.ts → shared/PluginTargetSupport.d.ts} +12 -12
  72. package/bin/cli/shared/PluginTargetSupport.d.ts.map +1 -0
  73. package/bin/cli/{service/ServiceCommandSupport.js → shared/PluginTargetSupport.js} +14 -14
  74. package/bin/cli/shared/PluginTargetSupport.js.map +1 -0
  75. package/bin/cli/shared/Plugins.d.ts.map +1 -1
  76. package/bin/cli/shared/Plugins.js +159 -30
  77. package/bin/cli/shared/Plugins.js.map +1 -1
  78. package/bin/cli/shared/PortHints.js +1 -1
  79. package/bin/cli/shared/PortHints.js.map +1 -1
  80. package/bin/config/DowncitySchema.d.ts.map +1 -1
  81. package/bin/config/DowncitySchema.js +2 -8
  82. package/bin/config/DowncitySchema.js.map +1 -1
  83. package/bin/config/Paths.d.ts +12 -10
  84. package/bin/config/Paths.d.ts.map +1 -1
  85. package/bin/config/Paths.js +23 -17
  86. package/bin/config/Paths.js.map +1 -1
  87. package/bin/control/AgentStatusApiRoutes.js +30 -30
  88. package/bin/control/AgentStatusApiRoutes.js.map +1 -1
  89. package/bin/control/ChannelAccountApiRoutes.js +6 -6
  90. package/bin/control/ControlGateway.js +2 -2
  91. package/bin/control/ControlGateway.js.map +1 -1
  92. package/bin/control/ModelApiRoutes.d.ts +1 -1
  93. package/bin/control/ModelApiRoutes.d.ts.map +1 -1
  94. package/bin/control/ModelApiRoutes.js +1 -1
  95. package/bin/control/ModelApiRoutes.js.map +1 -1
  96. package/bin/control/ModelPoolService.d.ts +4 -121
  97. package/bin/control/ModelPoolService.d.ts.map +1 -1
  98. package/bin/control/ModelPoolService.js +4 -322
  99. package/bin/control/ModelPoolService.js.map +1 -1
  100. package/bin/control/PluginApiRoutes.js +5 -5
  101. package/bin/control/PluginApiRoutes.js.map +1 -1
  102. package/bin/control/gateway/AgentActions.d.ts.map +1 -1
  103. package/bin/control/gateway/AgentActions.js +18 -3
  104. package/bin/control/gateway/AgentActions.js.map +1 -1
  105. package/bin/control/gateway/AgentCatalog.js +2 -2
  106. package/bin/control/gateway/AgentCatalog.js.map +1 -1
  107. package/bin/control/instant/InstantApiRoutes.d.ts +3 -3
  108. package/bin/control/instant/InstantApiRoutes.d.ts.map +1 -1
  109. package/bin/control/instant/InstantApiRoutes.js +5 -5
  110. package/bin/control/instant/InstantApiRoutes.js.map +1 -1
  111. package/bin/control/instant/{InstantSessionService.d.ts → InstantSessionRunner.d.ts} +7 -7
  112. package/bin/control/instant/InstantSessionRunner.d.ts.map +1 -0
  113. package/bin/control/instant/{InstantSessionService.js → InstantSessionRunner.js} +4 -4
  114. package/bin/control/instant/InstantSessionRunner.js.map +1 -0
  115. package/bin/http/auth/RoutePolicy.js +7 -7
  116. package/bin/http/auth/RoutePolicy.js.map +1 -1
  117. package/bin/model/service/ModelPoolService.d.ts +145 -0
  118. package/bin/model/service/ModelPoolService.d.ts.map +1 -0
  119. package/bin/model/service/ModelPoolService.js +424 -0
  120. package/bin/model/service/ModelPoolService.js.map +1 -0
  121. package/bin/process/daemon/Api.d.ts +2 -2
  122. package/bin/process/daemon/Api.js +1 -1
  123. package/bin/process/registry/AgentHostRuntime.d.ts +1 -1
  124. package/bin/process/registry/AgentHostRuntime.d.ts.map +1 -1
  125. package/bin/process/registry/AgentHostRuntime.js +85 -4
  126. package/bin/process/registry/AgentHostRuntime.js.map +1 -1
  127. package/package.json +2 -2
  128. package/src/cli/Index.ts +4 -6
  129. package/src/cli/agent/AgentChat.ts +1 -1
  130. package/src/cli/agent/AgentHistory.ts +6 -1
  131. package/src/cli/agent/AgentManager.ts +3 -3
  132. package/src/cli/agent/Init.ts +6 -6
  133. package/src/cli/agent/Restart.ts +1 -1
  134. package/src/cli/agent/Run.ts +6 -7
  135. package/src/cli/agent/Start.ts +1 -1
  136. package/src/cli/control-plane/ControlPlaneCommand.ts +28 -1
  137. package/src/cli/control-plane/ControlPlanePublicManager.ts +280 -0
  138. package/src/cli/control-plane/ControlPlanePublicMode.ts +141 -0
  139. package/src/cli/control-plane/ControlPlaneRuntime.ts +8 -6
  140. package/src/cli/control-plane/ControlPlaneStatus.ts +22 -8
  141. package/src/cli/model/ModelManageCommand.ts +23 -19
  142. package/src/cli/model/ModelManager.ts +419 -76
  143. package/src/cli/model/ModelManagerTypes.ts +82 -0
  144. package/src/cli/model/ModelSupport.ts +1 -1
  145. package/src/cli/shared/Chat.ts +2 -2
  146. package/src/cli/shared/ChatManager.ts +27 -27
  147. package/src/cli/shared/Config.ts +1 -1
  148. package/src/cli/{service/ServiceActionCommands.ts → shared/ManagedPluginActionCommands.ts} +75 -75
  149. package/src/cli/{service/ServiceCommandRemote.ts → shared/ManagedPluginRemote.ts} +47 -47
  150. package/src/cli/{service/ServiceScheduleCommand.ts → shared/PluginScheduleCommand.ts} +49 -49
  151. package/src/cli/{service/ServiceCommandSupport.ts → shared/PluginTargetSupport.ts} +14 -14
  152. package/src/cli/shared/Plugins.ts +195 -35
  153. package/src/cli/shared/PortHints.ts +1 -1
  154. package/src/config/DowncitySchema.ts +2 -8
  155. package/src/config/Paths.ts +55 -14
  156. package/src/control/AgentStatusApiRoutes.ts +35 -35
  157. package/src/control/ChannelAccountApiRoutes.ts +6 -6
  158. package/src/control/ControlGateway.ts +2 -2
  159. package/src/control/ModelApiRoutes.ts +1 -1
  160. package/src/control/ModelPoolService.ts +4 -390
  161. package/src/control/PluginApiRoutes.ts +5 -5
  162. package/src/control/gateway/AgentActions.ts +20 -5
  163. package/src/control/gateway/AgentCatalog.ts +2 -2
  164. package/src/control/instant/InstantApiRoutes.ts +8 -8
  165. package/src/control/instant/{InstantSessionService.ts → InstantSessionRunner.ts} +7 -7
  166. package/src/http/auth/RoutePolicy.ts +7 -7
  167. package/src/model/service/ModelPoolService.ts +525 -0
  168. package/src/process/daemon/Api.ts +2 -2
  169. package/src/process/registry/AgentHostRuntime.ts +95 -5
  170. package/bin/cli/service/ServiceActionCommands.d.ts +0 -14
  171. package/bin/cli/service/ServiceActionCommands.d.ts.map +0 -1
  172. package/bin/cli/service/ServiceActionCommands.js.map +0 -1
  173. package/bin/cli/service/ServiceCommandRemote.d.ts +0 -30
  174. package/bin/cli/service/ServiceCommandRemote.d.ts.map +0 -1
  175. package/bin/cli/service/ServiceCommandRemote.js.map +0 -1
  176. package/bin/cli/service/ServiceCommandSupport.d.ts.map +0 -1
  177. package/bin/cli/service/ServiceCommandSupport.js.map +0 -1
  178. package/bin/cli/service/ServiceScheduleCommand.d.ts +0 -36
  179. package/bin/cli/service/ServiceScheduleCommand.d.ts.map +0 -1
  180. package/bin/cli/service/ServiceScheduleCommand.js.map +0 -1
  181. package/bin/cli/service/Services.d.ts +0 -14
  182. package/bin/cli/service/Services.d.ts.map +0 -1
  183. package/bin/cli/service/Services.js +0 -154
  184. package/bin/cli/service/Services.js.map +0 -1
  185. package/bin/control/instant/InstantSessionService.d.ts.map +0 -1
  186. package/bin/control/instant/InstantSessionService.js.map +0 -1
  187. package/bin/process/registry/PluginRuntime.d.ts +0 -24
  188. package/bin/process/registry/PluginRuntime.d.ts.map +0 -1
  189. package/bin/process/registry/PluginRuntime.js +0 -31
  190. package/bin/process/registry/PluginRuntime.js.map +0 -1
  191. package/src/cli/service/Services.ts +0 -189
  192. package/src/process/registry/PluginRuntime.ts +0 -42
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Console 模型池服务。
3
+ *
4
+ * 关键点(中文)
5
+ * - 统一封装 provider/model 的增删改查与测试逻辑,供 CLI 与 Console API 共同复用。
6
+ * - 删除保护、引用检查、输入校验都在这里收敛,避免多处实现漂移。
7
+ * - Provider 的密钥仅返回脱敏视图,避免在 UI 或 CLI 输出中泄露明文。
8
+ */
9
+
10
+ import fs from "fs-extra";
11
+ import { generateText } from "ai";
12
+ import type {
13
+ LlmProviderType,
14
+ StoredModel,
15
+ StoredModelProvider,
16
+ } from "@downcity/agent";
17
+ import { getDowncityJsonPath } from "@/config/Paths.js";
18
+ import { PlatformStore } from "@/platform/store/index.js";
19
+ import { listManagedAgentEntries } from "@/process/registry/CityRegistry.js";
20
+ import { discoverProviderModels } from "@/cli/model/ModelSupport.js";
21
+ import { createRuntimeModel } from "@/model/runtime/CreateRuntimeModel.js";
22
+
23
+ const SUPPORTED_PROVIDER_TYPES: readonly LlmProviderType[] = [
24
+ "anthropic",
25
+ "openai",
26
+ "deepseek",
27
+ "gemini",
28
+ "open-compatible",
29
+ "open-responses",
30
+ "moonshot-cn",
31
+ "moonshot-ai",
32
+ "kimi-code",
33
+ "xai",
34
+ "huggingface",
35
+ "openrouter",
36
+ ];
37
+
38
+ function maskSecret(value: string | undefined): string {
39
+ const raw = String(value || "").trim();
40
+ if (!raw) return "";
41
+ if (raw.length <= 8) return "***";
42
+ return `${raw.slice(0, 4)}***${raw.slice(-4)}`;
43
+ }
44
+
45
+ function assertProviderType(inputType: string): LlmProviderType {
46
+ const candidate = String(inputType || "").trim() as LlmProviderType;
47
+ if (!SUPPORTED_PROVIDER_TYPES.includes(candidate)) {
48
+ throw new Error(
49
+ `Unsupported provider type: ${inputType}. Supported: ${SUPPORTED_PROVIDER_TYPES.join(", ")}`,
50
+ );
51
+ }
52
+ return candidate;
53
+ }
54
+
55
+ function parseOptionalNumber(input: unknown): number | undefined {
56
+ if (input === undefined || input === null || input === "") return undefined;
57
+ const num = Number(input);
58
+ if (!Number.isFinite(num) || Number.isNaN(num)) {
59
+ throw new Error(`Invalid number: ${String(input)}`);
60
+ }
61
+ return num;
62
+ }
63
+
64
+ function parseOptionalPositiveInteger(input: unknown): number | undefined {
65
+ if (input === undefined || input === null || input === "") return undefined;
66
+ const num = Number.parseInt(String(input), 10);
67
+ if (!Number.isFinite(num) || Number.isNaN(num) || !Number.isInteger(num) || num <= 0) {
68
+ throw new Error(`Invalid positive integer: ${String(input)}`);
69
+ }
70
+ return num;
71
+ }
72
+
73
+ function formatModelIds(modelIds: string[]): string {
74
+ return modelIds.map((modelId) => `"${modelId}"`).join(", ");
75
+ }
76
+
77
+ function formatAgentReference(reference: {
78
+ projectRoot: string;
79
+ shipJsonPath: string;
80
+ agentName?: string;
81
+ }): string {
82
+ const agentName = String(reference.agentName || "").trim();
83
+ if (agentName) {
84
+ return `${agentName} (${reference.projectRoot})`;
85
+ }
86
+ return reference.projectRoot;
87
+ }
88
+
89
+ async function listModelReferences(modelId: string): Promise<Array<{
90
+ projectRoot: string;
91
+ shipJsonPath: string;
92
+ agentName?: string;
93
+ }>> {
94
+ const references: Array<{
95
+ projectRoot: string;
96
+ shipJsonPath: string;
97
+ agentName?: string;
98
+ }> = [];
99
+ const entries = await listManagedAgentEntries();
100
+ for (const entry of entries) {
101
+ const projectRoot = String(entry.projectRoot || "").trim();
102
+ if (!projectRoot) continue;
103
+ const shipJsonPath = getDowncityJsonPath(projectRoot);
104
+ if (!(await fs.pathExists(shipJsonPath))) continue;
105
+ try {
106
+ const raw = (await fs.readJson(shipJsonPath)) as {
107
+ name?: unknown;
108
+ execution?: {
109
+ type?: unknown;
110
+ modelId?: unknown;
111
+ };
112
+ };
113
+ const executionType = String(raw?.execution?.type || "").trim();
114
+ const candidateModelId = String(raw?.execution?.modelId || "").trim();
115
+ if (executionType !== "api" || candidateModelId !== modelId) continue;
116
+ const agentName = String(raw?.name || "").trim() || undefined;
117
+ references.push({
118
+ projectRoot,
119
+ shipJsonPath,
120
+ agentName,
121
+ });
122
+ } catch {
123
+ continue;
124
+ }
125
+ }
126
+ return references.sort((a, b) => a.projectRoot.localeCompare(b.projectRoot));
127
+ }
128
+
129
+ /**
130
+ * ModelPoolService:提供 CLI / UI 共用的模型池管理能力。
131
+ */
132
+ export class ModelPoolService {
133
+ /**
134
+ * 读取模型池快照。
135
+ */
136
+ async listPool(): Promise<{
137
+ providers: Array<{
138
+ id: string;
139
+ type: string;
140
+ baseUrl?: string;
141
+ hasApiKey: boolean;
142
+ apiKeyMasked?: string;
143
+ createdAt: string;
144
+ updatedAt: string;
145
+ }>;
146
+ models: Array<{
147
+ id: string;
148
+ providerId: string;
149
+ name: string;
150
+ temperature?: number;
151
+ maxTokens?: number;
152
+ topP?: number;
153
+ frequencyPenalty?: number;
154
+ presencePenalty?: number;
155
+ anthropicVersion?: string;
156
+ isPaused: boolean;
157
+ createdAt: string;
158
+ updatedAt: string;
159
+ }>;
160
+ providerIds: string[];
161
+ modelIds: string[];
162
+ }> {
163
+ const store = new PlatformStore();
164
+ try {
165
+ const providersRaw = await store.listProviders();
166
+ const models = store.listModels();
167
+ const providers = providersRaw.map((item) => ({
168
+ id: item.id,
169
+ type: item.type,
170
+ baseUrl: item.baseUrl,
171
+ hasApiKey: String(item.apiKey || "").trim().length > 0,
172
+ apiKeyMasked: maskSecret(item.apiKey),
173
+ createdAt: item.createdAt,
174
+ updatedAt: item.updatedAt,
175
+ }));
176
+ return {
177
+ providers,
178
+ models,
179
+ providerIds: providers.map((x) => x.id),
180
+ modelIds: models.map((x) => x.id),
181
+ };
182
+ } finally {
183
+ store.close();
184
+ }
185
+ }
186
+
187
+ /**
188
+ * 读取单个 provider 以及它绑定的模型。
189
+ */
190
+ async getProviderUsage(providerId: string): Promise<{
191
+ provider: StoredModelProvider;
192
+ models: StoredModel[];
193
+ }> {
194
+ const id = String(providerId || "").trim();
195
+ if (!id) throw new Error("providerId cannot be empty");
196
+ const store = new PlatformStore();
197
+ try {
198
+ const provider = await store.getProvider(id);
199
+ if (!provider) throw new Error(`Provider not found: ${id}`);
200
+ const models = store
201
+ .listModels()
202
+ .filter((model) => model.providerId === id)
203
+ .sort((a, b) => a.id.localeCompare(b.id));
204
+ return {
205
+ provider,
206
+ models,
207
+ };
208
+ } finally {
209
+ store.close();
210
+ }
211
+ }
212
+
213
+ /**
214
+ * 读取单个 model 以及它被哪些 agent 项目引用。
215
+ */
216
+ async getModelUsage(modelId: string): Promise<{
217
+ model: StoredModel;
218
+ references: Array<{
219
+ projectRoot: string;
220
+ shipJsonPath: string;
221
+ agentName?: string;
222
+ }>;
223
+ }> {
224
+ const id = String(modelId || "").trim();
225
+ if (!id) throw new Error("modelId cannot be empty");
226
+ const store = new PlatformStore();
227
+ try {
228
+ const model = store.getModel(id);
229
+ if (!model) throw new Error(`Model not found: ${id}`);
230
+ const references = await listModelReferences(id);
231
+ return {
232
+ model,
233
+ references,
234
+ };
235
+ } finally {
236
+ store.close();
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 新增或更新 provider。
242
+ */
243
+ async upsertProvider(input: {
244
+ id: string;
245
+ type: string;
246
+ baseUrl?: string;
247
+ apiKey?: string;
248
+ clearBaseUrl?: boolean;
249
+ clearApiKey?: boolean;
250
+ }): Promise<{
251
+ providerId: string;
252
+ provider: {
253
+ id: string;
254
+ type: string;
255
+ baseUrl?: string;
256
+ hasApiKey: boolean;
257
+ apiKeyMasked?: string;
258
+ };
259
+ }> {
260
+ const id = String(input.id || "").trim();
261
+ if (!id) throw new Error("providerId cannot be empty");
262
+ const type = assertProviderType(input.type);
263
+ if (input.baseUrl !== undefined && input.clearBaseUrl === true) {
264
+ throw new Error("baseUrl and clearBaseUrl cannot be used together");
265
+ }
266
+ if (input.apiKey !== undefined && input.clearApiKey === true) {
267
+ throw new Error("apiKey and clearApiKey cannot be used together");
268
+ }
269
+
270
+ const store = new PlatformStore();
271
+ try {
272
+ const current = await store.getProvider(id);
273
+ const nextBaseUrl = input.clearBaseUrl === true
274
+ ? undefined
275
+ : input.baseUrl !== undefined
276
+ ? String(input.baseUrl || "").trim() || undefined
277
+ : current?.baseUrl;
278
+ const nextApiKey = input.clearApiKey === true
279
+ ? undefined
280
+ : input.apiKey !== undefined
281
+ ? String(input.apiKey || "")
282
+ : current?.apiKey;
283
+
284
+ await store.upsertProvider({
285
+ id,
286
+ type,
287
+ baseUrl: nextBaseUrl,
288
+ apiKey: nextApiKey,
289
+ });
290
+
291
+ const saved = await store.getProvider(id);
292
+ return {
293
+ providerId: id,
294
+ provider: {
295
+ id,
296
+ type,
297
+ baseUrl: saved?.baseUrl,
298
+ hasApiKey: String(saved?.apiKey || "").trim().length > 0,
299
+ apiKeyMasked: maskSecret(saved?.apiKey),
300
+ },
301
+ };
302
+ } finally {
303
+ store.close();
304
+ }
305
+ }
306
+
307
+ /**
308
+ * 删除 provider。
309
+ */
310
+ async removeProvider(providerId: string): Promise<void> {
311
+ const usage = await this.getProviderUsage(providerId);
312
+ if (usage.models.length > 0) {
313
+ throw new Error(
314
+ `Provider "${usage.provider.id}" is still referenced by models: ${formatModelIds(usage.models.map((model) => model.id))}. Remove or rebind those models first.`,
315
+ );
316
+ }
317
+ const store = new PlatformStore();
318
+ try {
319
+ store.removeProvider(usage.provider.id);
320
+ } finally {
321
+ store.close();
322
+ }
323
+ }
324
+
325
+ /**
326
+ * 测试 provider 并返回发现结果。
327
+ */
328
+ async testProvider(providerId: string): Promise<{
329
+ providerId: string;
330
+ discoveredModels: string[];
331
+ modelCount: number;
332
+ status?: number;
333
+ }> {
334
+ const id = String(providerId || "").trim();
335
+ if (!id) throw new Error("providerId cannot be empty");
336
+ const store = new PlatformStore();
337
+ try {
338
+ const provider = await store.getProvider(id);
339
+ if (!provider) throw new Error(`Provider not found: ${id}`);
340
+ const discovery = await discoverProviderModels({
341
+ providerId: id,
342
+ providerType: provider.type,
343
+ baseUrl: provider.baseUrl,
344
+ apiKey: provider.apiKey,
345
+ });
346
+ if (!discovery.ok) {
347
+ throw new Error(discovery.error || `provider test failed (${discovery.status || "n/a"})`);
348
+ }
349
+ return {
350
+ providerId: id,
351
+ discoveredModels: discovery.models,
352
+ modelCount: discovery.models.length,
353
+ status: discovery.status,
354
+ };
355
+ } finally {
356
+ store.close();
357
+ }
358
+ }
359
+
360
+ /**
361
+ * 发现 provider 模型并可选自动写入模型池。
362
+ */
363
+ async discoverProvider(params: {
364
+ providerId: string;
365
+ autoAdd?: boolean;
366
+ prefix?: string;
367
+ }): Promise<{
368
+ providerId: string;
369
+ discoveredModels: string[];
370
+ modelCount: number;
371
+ autoAdded: Array<{ modelId: string; modelName: string }>;
372
+ }> {
373
+ const providerId = String(params.providerId || "").trim();
374
+ if (!providerId) throw new Error("providerId cannot be empty");
375
+ const store = new PlatformStore();
376
+ try {
377
+ const provider = await store.getProvider(providerId);
378
+ if (!provider) throw new Error(`Provider not found: ${providerId}`);
379
+ const discovery = await discoverProviderModels({
380
+ providerId,
381
+ providerType: provider.type,
382
+ baseUrl: provider.baseUrl,
383
+ apiKey: provider.apiKey,
384
+ });
385
+ if (!discovery.ok) {
386
+ throw new Error(
387
+ discovery.error || `provider discover failed (${discovery.status || "n/a"})`,
388
+ );
389
+ }
390
+
391
+ const autoAdded: Array<{ modelId: string; modelName: string }> = [];
392
+ if (params.autoAdd === true) {
393
+ const prefix = String(params.prefix || "").trim();
394
+ for (const remoteModelName of discovery.models) {
395
+ const modelName = String(remoteModelName || "").trim();
396
+ if (!modelName) continue;
397
+ const modelId = prefix ? `${prefix}${modelName}` : modelName;
398
+ if (store.getModel(modelId)) continue;
399
+ store.upsertModel({
400
+ id: modelId,
401
+ providerId,
402
+ name: modelName,
403
+ });
404
+ autoAdded.push({ modelId, modelName });
405
+ }
406
+ }
407
+
408
+ return {
409
+ providerId,
410
+ discoveredModels: discovery.models,
411
+ modelCount: discovery.models.length,
412
+ autoAdded,
413
+ };
414
+ } finally {
415
+ store.close();
416
+ }
417
+ }
418
+
419
+ /**
420
+ * 新增或更新 model。
421
+ */
422
+ async upsertModel(input: {
423
+ id: string;
424
+ providerId: string;
425
+ name: string;
426
+ temperature?: unknown;
427
+ maxTokens?: unknown;
428
+ topP?: unknown;
429
+ frequencyPenalty?: unknown;
430
+ presencePenalty?: unknown;
431
+ anthropicVersion?: string;
432
+ isPaused?: boolean;
433
+ }): Promise<{ modelId: string }> {
434
+ const id = String(input.id || "").trim();
435
+ if (!id) throw new Error("modelId cannot be empty");
436
+ const providerId = String(input.providerId || "").trim();
437
+ if (!providerId) throw new Error("providerId cannot be empty");
438
+ const name = String(input.name || "").trim();
439
+ if (!name) throw new Error("modelName cannot be empty");
440
+
441
+ const store = new PlatformStore();
442
+ try {
443
+ const provider = await store.getProvider(providerId);
444
+ if (!provider) throw new Error(`Provider not found: ${providerId}`);
445
+ store.upsertModel({
446
+ id,
447
+ providerId,
448
+ name,
449
+ temperature: parseOptionalNumber(input.temperature),
450
+ maxTokens: parseOptionalPositiveInteger(input.maxTokens),
451
+ topP: parseOptionalNumber(input.topP),
452
+ frequencyPenalty: parseOptionalNumber(input.frequencyPenalty),
453
+ presencePenalty: parseOptionalNumber(input.presencePenalty),
454
+ anthropicVersion: String(input.anthropicVersion || "").trim() || undefined,
455
+ isPaused: input.isPaused === true,
456
+ });
457
+ return { modelId: id };
458
+ } finally {
459
+ store.close();
460
+ }
461
+ }
462
+
463
+ /**
464
+ * 删除 model。
465
+ */
466
+ async removeModel(modelId: string): Promise<void> {
467
+ const usage = await this.getModelUsage(modelId);
468
+ if (usage.references.length > 0) {
469
+ throw new Error(
470
+ `Model "${usage.model.id}" is still used by agent projects: ${usage.references.map((reference) => formatAgentReference(reference)).join(", ")}. Switch those projects to another model first.`,
471
+ );
472
+ }
473
+ const store = new PlatformStore();
474
+ try {
475
+ store.removeModel(usage.model.id);
476
+ } finally {
477
+ store.close();
478
+ }
479
+ }
480
+
481
+ /**
482
+ * 设置 model pause 状态。
483
+ */
484
+ async setModelPaused(modelId: string, isPaused: boolean): Promise<void> {
485
+ const id = String(modelId || "").trim();
486
+ if (!id) throw new Error("modelId cannot be empty");
487
+ const store = new PlatformStore();
488
+ try {
489
+ const model = store.getModel(id);
490
+ if (!model) throw new Error(`Model not found: ${id}`);
491
+ store.setModelPaused(id, isPaused);
492
+ } finally {
493
+ store.close();
494
+ }
495
+ }
496
+
497
+ /**
498
+ * 测试 model 可调用性(真实推理调用)。
499
+ */
500
+ async testModel(modelId: string, prompt?: string): Promise<{
501
+ modelId: string;
502
+ prompt: string;
503
+ text: string;
504
+ }> {
505
+ const id = String(modelId || "").trim();
506
+ if (!id) throw new Error("modelId cannot be empty");
507
+ const actualPrompt = String(prompt || "").trim() || "Reply with exactly: OK";
508
+ const model = await createRuntimeModel({
509
+ config: {
510
+ name: "console-model-test",
511
+ version: "1.0.0",
512
+ execution: { type: "api", modelId: id },
513
+ },
514
+ });
515
+ const result = await generateText({
516
+ model,
517
+ prompt: actualPrompt,
518
+ });
519
+ return {
520
+ modelId: id,
521
+ prompt: actualPrompt,
522
+ text: result.text,
523
+ };
524
+ }
525
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 这里描述的是 CLI 与 agent daemon 之间的远程访问契约,不包含具体 HTTP 实现。
6
- * - service 等上层模块只依赖这些协议类型,不需要感知 city/daemon 的内部实现细节。
6
+ * - plugin runtime 等上层模块只依赖这些协议类型,不需要感知 city/daemon 的内部实现细节。
7
7
  * - 该文件属于 `main/city/daemon/*` 的协议边界,而不是通用 HTTP 类型目录。
8
8
  */
9
9
 
@@ -40,7 +40,7 @@ export type DaemonJsonApiCallParams = {
40
40
  *
41
41
  * 说明(中文)
42
42
  * - 仅本地 IPC transport 当前会消费该参数。
43
- * - 用于 `session execute`、`service command` 这类可能持续几十秒的请求,
43
+ * - 用于 `session execute`、`plugin command` 这类可能持续几十秒的请求,
44
44
  * 避免被默认短超时误判成“Agent server 未启动”。
45
45
  */
46
46
  timeoutMs?: number;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - `main/agent/*` 负责创建这些宿主能力对象,再注入到 AgentRuntime。
6
- * - services / session / plugins 只消费这些对象,不再直接 import `main/*`。
6
+ * - plugin runtimes / session / plugins 只消费这些对象,不再直接 import `main/*`。
7
7
  * - 当前由 city 在这里统一装配路径、plugin 配置持久化与平台能力三类宿主对象。
8
8
  */
9
9
  import {
@@ -23,6 +23,9 @@ import type {
23
23
  AgentPathRuntime,
24
24
  AgentPlatformRuntime,
25
25
  AgentPluginConfigRuntime,
26
+ ChatChannelAccountListItem,
27
+ StoredChannelAccount,
28
+ StoredChannelAccountChannel,
26
29
  } from "@downcity/agent";
27
30
  import type { DowncityConfig } from "@downcity/agent";
28
31
  import { PlatformStore } from "@/platform/store/index.js";
@@ -36,13 +39,71 @@ import {
36
39
  writeChatAuthorizationConfig,
37
40
  } from "@/platform/chatAuthorization/Store.js";
38
41
 
42
+ /**
43
+ * 脱敏显示密钥。
44
+ *
45
+ * 关键点(中文)
46
+ * - channel account 列表只返回安全视图,避免在 CLI/UI 展示链路泄露明文。
47
+ * - 保持和现有 model provider 脱敏规则一致,减少用户心智负担。
48
+ */
49
+ function maskSecret(value: string | undefined): string | undefined {
50
+ const raw = String(value || "").trim();
51
+ if (!raw) return undefined;
52
+ if (raw.length <= 8) return "***";
53
+ return `${raw.slice(0, 4)}***${raw.slice(-4)}`;
54
+ }
55
+
56
+ /**
57
+ * 将存储层 channel account 转成对外安全视图。
58
+ */
59
+ function toChannelAccountListItem(
60
+ account: StoredChannelAccount,
61
+ ): ChatChannelAccountListItem {
62
+ return {
63
+ id: account.id,
64
+ channel: account.channel,
65
+ name: account.name,
66
+ identity: account.identity,
67
+ owner: account.owner,
68
+ creator: account.creator,
69
+ domain: account.domain,
70
+ sandbox: account.sandbox === true,
71
+ hasBotToken: !!String(account.botToken || "").trim(),
72
+ hasAppId: !!String(account.appId || "").trim(),
73
+ hasAppSecret: !!String(account.appSecret || "").trim(),
74
+ botTokenMasked: maskSecret(account.botToken),
75
+ appIdMasked: maskSecret(account.appId),
76
+ appSecretMasked: maskSecret(account.appSecret),
77
+ createdAt: account.createdAt,
78
+ updatedAt: account.updatedAt,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * 归一化并校验 channel account 渠道名。
84
+ */
85
+ function normalizeChannelAccountChannel(
86
+ channelInput: string,
87
+ ): StoredChannelAccountChannel {
88
+ const channel = String(channelInput || "").trim().toLowerCase();
89
+ if (channel === "telegram" || channel === "feishu" || channel === "qq") {
90
+ return channel;
91
+ }
92
+ throw new Error(`Unsupported channel account channel: ${channelInput}`);
93
+ }
94
+
39
95
  /**
40
96
  * 创建当前项目的路径能力集合。
41
97
  */
42
- export function createAgentPathRuntime(projectRoot: string): AgentPathRuntime {
98
+ export function createAgentPathRuntime(
99
+ projectRoot: string,
100
+ agentIdInput: string,
101
+ ): AgentPathRuntime {
43
102
  const rootPath = String(projectRoot || "").trim();
103
+ const agentId = String(agentIdInput || "").trim();
44
104
  return {
45
105
  projectRoot: rootPath,
106
+ agentId,
46
107
  getDowncityDirPath: () => getDowncityDirPath(rootPath),
47
108
  getCacheDirPath: () => getCacheDirPath(rootPath),
48
109
  getDowncityChannelDirPath: () => getDowncityChannelDirPath(rootPath),
@@ -51,9 +112,10 @@ export function createAgentPathRuntime(projectRoot: string): AgentPathRuntime {
51
112
  getDowncityMemoryLongTermPath: () => getDowncityMemoryLongTermPath(rootPath),
52
113
  getDowncityMemoryDailyDirPath: () => getDowncityMemoryDailyDirPath(rootPath),
53
114
  getDowncityMemoryDailyPath: (date) => getDowncityMemoryDailyPath(rootPath, date),
54
- getDowncitySessionRootDirPath: () => getDowncitySessionRootDirPath(rootPath),
55
- getDowncitySessionDirPath: (sessionId) => getDowncitySessionDirPath(rootPath, sessionId),
56
- };
115
+ getDowncitySessionRootDirPath: () => getDowncitySessionRootDirPath(rootPath, agentId),
116
+ getDowncitySessionDirPath: (sessionId) =>
117
+ getDowncitySessionDirPath(rootPath, agentId, sessionId),
118
+ } as AgentPathRuntime;
57
119
  }
58
120
 
59
121
  /**
@@ -130,6 +192,34 @@ export function createAgentPlatformRuntime(): AgentPlatformRuntime {
130
192
  store.close();
131
193
  }
132
194
  },
195
+ listChannelAccounts: async () => {
196
+ const store = new PlatformStore();
197
+ try {
198
+ const accounts = await store.listChannelAccounts();
199
+ return accounts.map(toChannelAccountListItem);
200
+ } finally {
201
+ store.close();
202
+ }
203
+ },
204
+ updateChannelAccount: async (input) => {
205
+ const store = new PlatformStore();
206
+ try {
207
+ await store.upsertChannelAccount({
208
+ ...input,
209
+ channel: normalizeChannelAccountChannel(input.channel),
210
+ });
211
+ } finally {
212
+ store.close();
213
+ }
214
+ },
215
+ removeChannelAccount: async (channelAccountId) => {
216
+ const store = new PlatformStore();
217
+ try {
218
+ store.removeChannelAccount(channelAccountId);
219
+ } finally {
220
+ store.close();
221
+ }
222
+ },
133
223
  readChatAuthorizationConfig: (projectRoot) => readChatAuthorizationConfigSync(projectRoot),
134
224
  writeChatAuthorizationConfig,
135
225
  setChatAuthorizationUserRole,
@@ -1,14 +0,0 @@
1
- /**
2
- * Service action CLI 注册器。
3
- *
4
- * 关键点(中文)
5
- * - 负责把 service actions 挂到 commander(`city <service> <action>`)。
6
- * - 仅处理 CLI 参数映射与远程调用,不承载 service 状态机逻辑。
7
- * - service 注册表与调度时间解析统一复用 agent 包实现,避免 city 维护第二套事实源。
8
- */
9
- import type { Command } from "commander";
10
- /**
11
- * 注册所有 service actions 的 CLI 命令。
12
- */
13
- export declare function registerAllServicesForCli(program: Command): void;
14
- //# sourceMappingURL=ServiceActionCommands.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ServiceActionCommands.d.ts","sourceRoot":"","sources":["../../../src/cli/service/ServiceActionCommands.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgYzC;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkBhE"}