create-windy 0.3.0 → 0.3.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.
- package/README.md +26 -3
- package/dist/cli.js +8077 -3852
- package/package.json +3 -3
- package/template/.windy-template.json +2 -2
- package/template/README.md +25 -3
- package/template/_gitignore +4 -0
- package/template/apps/agent-server/tests/__init__.py +1 -0
- package/template/apps/agent-server/tests/openai_provider_test_support.py +131 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +7 -123
- package/template/apps/server/src/auth/routes.test-support.ts +94 -0
- package/template/apps/server/src/auth/routes.test.ts +20 -88
- package/template/apps/server/src/index.ts +17 -34
- package/template/apps/server/src/mcp/adapter.test.ts +3 -2
- package/template/apps/server/src/mcp/adapter.ts +9 -9
- package/template/apps/server/src/mcp/routes.ts +4 -3
- package/template/apps/server/src/platform-http-runtime.ts +87 -0
- package/template/apps/server/src/system/drizzle-system-configs.ts +319 -0
- package/template/apps/server/src/system/drizzle-system.ts +35 -315
- package/template/apps/server/src/system/resource-routes.ts +270 -0
- package/template/apps/server/src/system/routes.ts +3 -269
- package/template/apps/web/src/pages/system/components/SystemResourceLoadError.vue +21 -0
- package/template/apps/web/src/pages/system/components/SystemResourceLoadError.webtest.ts +17 -0
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +5 -10
- package/template/docs/architecture/mcp-business-capabilities.md +107 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +5 -5
- package/template/docs/platform/agent-tools.md +4 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/composition-fixtures.test-support.ts +164 -0
- package/template/packages/modules/src/composition.test.ts +5 -158
- package/template/packages/modules/src/system-api-operational.ts +234 -0
- package/template/packages/modules/src/system-api-permissions.ts +8 -229
- package/template/packages/modules/src/system-module-tasks.ts +86 -0
- package/template/packages/modules/src/system-modules-access.test.ts +60 -0
- package/template/packages/modules/src/system-modules.test.ts +0 -53
- package/template/packages/modules/src/system-modules.ts +2 -84
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/mcp.test.ts +12 -0
- package/template/packages/shared/src/mcp.ts +7 -0
|
@@ -1,49 +1,25 @@
|
|
|
1
1
|
import type { FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
2
|
-
import { recordMutationAudit } from "../audit/policy.js";
|
|
3
2
|
import { registerFeatureFlagRoutes } from "../feature/routes.js";
|
|
4
3
|
import { findSystemFeature } from "../route-guards.js";
|
|
5
4
|
import type { createServerRuntime } from "../runtime.js";
|
|
6
|
-
import type { ManagedEntity } from "./entities.js";
|
|
7
5
|
import { registerMenuRoutes } from "./menu-routes.js";
|
|
8
6
|
import {
|
|
9
7
|
registerDepartmentQuickCreateRoute,
|
|
10
8
|
registerDepartmentTransitionRoute,
|
|
11
9
|
} from "./department-routes.js";
|
|
12
|
-
import {
|
|
13
|
-
asPartial,
|
|
14
|
-
notFound,
|
|
15
|
-
parseImportPayload,
|
|
16
|
-
parseListQuery,
|
|
17
|
-
requiredId,
|
|
18
|
-
resourceEvent,
|
|
19
|
-
routeContext,
|
|
20
|
-
} from "./route-helpers.js";
|
|
21
|
-
import type { ResourceRouteConfig, RouteApp } from "./route-types.js";
|
|
10
|
+
import type { RouteApp } from "./route-types.js";
|
|
22
11
|
import type { SystemRepositories } from "./seed.js";
|
|
23
|
-
import { resource
|
|
12
|
+
import { resource } from "./resource-route-support.js";
|
|
24
13
|
import type { AuthRepository } from "../auth/repository.js";
|
|
25
14
|
import { identityRouteConfigs } from "./identity-route-config.js";
|
|
26
15
|
// @windy-module system.audit begin
|
|
27
16
|
import { registerAuditRoutes } from "./audit-routes.js";
|
|
28
17
|
// @windy-module system.audit end
|
|
29
|
-
import {
|
|
30
|
-
accessCall,
|
|
31
|
-
importNotSupported,
|
|
32
|
-
readAccessible,
|
|
33
|
-
resourceConflict,
|
|
34
|
-
} from "./resource-access-response.js";
|
|
35
|
-
import { runResourceMutation } from "./mutation-runner.js";
|
|
36
|
-
import { registerResourceExportRoute } from "./resource-export-route.js";
|
|
37
|
-
import {
|
|
38
|
-
projectEntity,
|
|
39
|
-
projectEntities,
|
|
40
|
-
projectImportResult,
|
|
41
|
-
} from "./response-projection.js";
|
|
42
18
|
// @windy-module system.data-governance begin
|
|
43
19
|
import { registerUserPlaintextRoutes } from "../data-governance/user-plaintext-routes.js";
|
|
44
20
|
// @windy-module system.data-governance end
|
|
45
|
-
import { searchAuditEvent } from "../audit/search-summary.js";
|
|
46
21
|
import { DepartmentOrganizationPolicy } from "./department-organization.js";
|
|
22
|
+
import { registerResourceRoutes } from "./resource-routes.js";
|
|
47
23
|
|
|
48
24
|
export type { RouteApp } from "./route-types.js";
|
|
49
25
|
|
|
@@ -144,245 +120,3 @@ export function registerSystemRoutes(
|
|
|
144
120
|
|
|
145
121
|
return app;
|
|
146
122
|
}
|
|
147
|
-
|
|
148
|
-
function registerResourceRoutes<T extends ManagedEntity>(
|
|
149
|
-
app: RouteApp,
|
|
150
|
-
config: ResourceRouteConfig<T>,
|
|
151
|
-
runtime: Parameters<typeof registerSystemRoutes>[2],
|
|
152
|
-
) {
|
|
153
|
-
app.get(`/system/${config.path}`, (rawContext) => {
|
|
154
|
-
const { guardContext, query } = routeContext(rawContext);
|
|
155
|
-
return withPermission(
|
|
156
|
-
runtime,
|
|
157
|
-
guardContext,
|
|
158
|
-
"GET",
|
|
159
|
-
config.permissionResource,
|
|
160
|
-
"read",
|
|
161
|
-
config.feature,
|
|
162
|
-
async () => {
|
|
163
|
-
const listQuery = parseListQuery(query);
|
|
164
|
-
if (listQuery instanceof Response) return listQuery;
|
|
165
|
-
const result = await accessCall(() =>
|
|
166
|
-
config.dataAccess.mode === "request"
|
|
167
|
-
? config.dataAccess.access.list(
|
|
168
|
-
listQuery,
|
|
169
|
-
config.searchFields,
|
|
170
|
-
guardContext.actor,
|
|
171
|
-
)
|
|
172
|
-
: config.repository.list(listQuery, config.searchFields),
|
|
173
|
-
);
|
|
174
|
-
if (result instanceof Response) return result;
|
|
175
|
-
const search = listQuery.q?.trim();
|
|
176
|
-
if (search) {
|
|
177
|
-
runtime.auditSink.recordEvent(
|
|
178
|
-
searchAuditEvent(guardContext, config.path, search, result.total),
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
return { ...result, items: projectEntities(config, result.items) };
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (config.backupExport !== false) {
|
|
187
|
-
registerResourceExportRoute(app, config, runtime);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
app.get(`/system/${config.path}/:id`, (rawContext) => {
|
|
191
|
-
const { guardContext, params } = routeContext(rawContext);
|
|
192
|
-
return withPermission(
|
|
193
|
-
runtime,
|
|
194
|
-
guardContext,
|
|
195
|
-
"GET",
|
|
196
|
-
config.permissionResource,
|
|
197
|
-
"read",
|
|
198
|
-
config.feature,
|
|
199
|
-
async () => {
|
|
200
|
-
const item = await readAccessible(
|
|
201
|
-
config,
|
|
202
|
-
requiredId(params),
|
|
203
|
-
guardContext.actor,
|
|
204
|
-
);
|
|
205
|
-
if (item instanceof Response) return item;
|
|
206
|
-
return item
|
|
207
|
-
? projectEntity(config, item)
|
|
208
|
-
: notFound(config.path, requiredId(params));
|
|
209
|
-
},
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
if (config.readonly) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
app.post(`/system/${config.path}/import`, (rawContext) => {
|
|
218
|
-
const { guardContext, body } = routeContext(rawContext);
|
|
219
|
-
return withPermission(
|
|
220
|
-
runtime,
|
|
221
|
-
guardContext,
|
|
222
|
-
"POST",
|
|
223
|
-
config.permissionResource,
|
|
224
|
-
"manage",
|
|
225
|
-
config.feature,
|
|
226
|
-
async () => {
|
|
227
|
-
if (config.importable === false) {
|
|
228
|
-
return importNotSupported();
|
|
229
|
-
}
|
|
230
|
-
const payload = parseImportPayload<T>(body);
|
|
231
|
-
const result = await accessCall(() =>
|
|
232
|
-
config.dataAccess.mode === "request"
|
|
233
|
-
? config.dataAccess.access.importMany(
|
|
234
|
-
payload.items,
|
|
235
|
-
guardContext.actor,
|
|
236
|
-
payload.mode,
|
|
237
|
-
)
|
|
238
|
-
: config.repository.importMany(
|
|
239
|
-
payload.items,
|
|
240
|
-
guardContext.actor,
|
|
241
|
-
payload.mode,
|
|
242
|
-
),
|
|
243
|
-
);
|
|
244
|
-
if (result instanceof Response) return result;
|
|
245
|
-
await recordMutationAudit(
|
|
246
|
-
runtime,
|
|
247
|
-
config.path,
|
|
248
|
-
resourceEvent(guardContext, "data.import", config.path, undefined, {
|
|
249
|
-
created: result.created,
|
|
250
|
-
updated: result.updated,
|
|
251
|
-
skipped: result.skipped,
|
|
252
|
-
total: payload.items.length,
|
|
253
|
-
}),
|
|
254
|
-
);
|
|
255
|
-
return {
|
|
256
|
-
...projectImportResult(config, result),
|
|
257
|
-
total: payload.items.length,
|
|
258
|
-
};
|
|
259
|
-
},
|
|
260
|
-
);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
app.post(`/system/${config.path}`, (rawContext) => {
|
|
264
|
-
const { guardContext, body } = routeContext(rawContext);
|
|
265
|
-
return withPermission(
|
|
266
|
-
runtime,
|
|
267
|
-
guardContext,
|
|
268
|
-
"POST",
|
|
269
|
-
config.permissionResource,
|
|
270
|
-
"manage",
|
|
271
|
-
config.feature,
|
|
272
|
-
async () =>
|
|
273
|
-
runResourceMutation(config, async () => {
|
|
274
|
-
const input = asPartial<T>(body);
|
|
275
|
-
const validationError = await config.validateInput?.(input);
|
|
276
|
-
if (validationError) return invalidResource(validationError);
|
|
277
|
-
const policyError = await config.beforeCreate?.(input);
|
|
278
|
-
if (policyError) return resourceConflict(policyError);
|
|
279
|
-
const item = await accessCall(() =>
|
|
280
|
-
config.dataAccess.mode === "request"
|
|
281
|
-
? config.dataAccess.access.create(input, guardContext.actor)
|
|
282
|
-
: config.repository.create(input, guardContext.actor),
|
|
283
|
-
);
|
|
284
|
-
if (item instanceof Response) return item;
|
|
285
|
-
await recordMutationAudit(
|
|
286
|
-
runtime,
|
|
287
|
-
config.path,
|
|
288
|
-
resourceEvent(guardContext, "crud.create", config.path, item.id),
|
|
289
|
-
);
|
|
290
|
-
return projectEntity(config, item);
|
|
291
|
-
}),
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
app.put(`/system/${config.path}/:id`, (rawContext) => {
|
|
296
|
-
const { guardContext, params, body } = routeContext(rawContext);
|
|
297
|
-
return withPermission(
|
|
298
|
-
runtime,
|
|
299
|
-
guardContext,
|
|
300
|
-
"PUT",
|
|
301
|
-
config.permissionResource,
|
|
302
|
-
"manage",
|
|
303
|
-
config.feature,
|
|
304
|
-
async () =>
|
|
305
|
-
runResourceMutation(config, async () => {
|
|
306
|
-
const id = requiredId(params);
|
|
307
|
-
const input = asPartial<T>(body);
|
|
308
|
-
const current = await readAccessible(config, id, guardContext.actor);
|
|
309
|
-
if (current instanceof Response) return current;
|
|
310
|
-
if (!current) return notFound(config.path, id);
|
|
311
|
-
const validationError = await config.validateInput?.(input, current);
|
|
312
|
-
if (validationError) return invalidResource(validationError);
|
|
313
|
-
const policyError = await config.beforeUpdate?.(current, input);
|
|
314
|
-
if (policyError) return resourceConflict(policyError);
|
|
315
|
-
const item = await accessCall(() =>
|
|
316
|
-
config.dataAccess.mode === "request"
|
|
317
|
-
? config.dataAccess.access.update(id, input, guardContext.actor)
|
|
318
|
-
: config.repository.update(id, input, guardContext.actor),
|
|
319
|
-
);
|
|
320
|
-
if (item instanceof Response) return item;
|
|
321
|
-
if (!item) {
|
|
322
|
-
return notFound(config.path, id);
|
|
323
|
-
}
|
|
324
|
-
await config.afterUpdate?.(current, item, guardContext.actor);
|
|
325
|
-
await recordMutationAudit(
|
|
326
|
-
runtime,
|
|
327
|
-
config.path,
|
|
328
|
-
resourceEvent(guardContext, "crud.update", config.path, id),
|
|
329
|
-
);
|
|
330
|
-
return projectEntity(config, item);
|
|
331
|
-
}),
|
|
332
|
-
);
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
app.delete(`/system/${config.path}/:id`, (rawContext) => {
|
|
336
|
-
const { guardContext, params } = routeContext(rawContext);
|
|
337
|
-
return withPermission(
|
|
338
|
-
runtime,
|
|
339
|
-
guardContext,
|
|
340
|
-
"DELETE",
|
|
341
|
-
config.permissionResource,
|
|
342
|
-
"manage",
|
|
343
|
-
config.feature,
|
|
344
|
-
async () =>
|
|
345
|
-
runResourceMutation(config, async () => {
|
|
346
|
-
const id = requiredId(params);
|
|
347
|
-
const current = await readAccessible(config, id, guardContext.actor);
|
|
348
|
-
if (current instanceof Response) return current;
|
|
349
|
-
if (!current) return notFound(config.path, id);
|
|
350
|
-
const policyError = await config.beforeDelete?.(current);
|
|
351
|
-
if (policyError) return resourceConflict(policyError);
|
|
352
|
-
const item = await accessCall(() =>
|
|
353
|
-
config.dataAccess.mode === "request"
|
|
354
|
-
? config.dataAccess.access.softDelete(id, guardContext.actor)
|
|
355
|
-
: config.repository.softDelete(id, guardContext.actor),
|
|
356
|
-
);
|
|
357
|
-
if (item instanceof Response) return item;
|
|
358
|
-
if (!item) {
|
|
359
|
-
return notFound(config.path, id);
|
|
360
|
-
}
|
|
361
|
-
await config.afterDelete?.(item, guardContext.actor);
|
|
362
|
-
await recordMutationAudit(
|
|
363
|
-
runtime,
|
|
364
|
-
config.path,
|
|
365
|
-
resourceEvent(guardContext, "crud.delete", config.path, id),
|
|
366
|
-
);
|
|
367
|
-
return projectEntity(config, item);
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function invalidResource(
|
|
374
|
-
error: NonNullable<
|
|
375
|
-
Awaited<
|
|
376
|
-
ReturnType<
|
|
377
|
-
NonNullable<ResourceRouteConfig<ManagedEntity>["validateInput"]>
|
|
378
|
-
>
|
|
379
|
-
>
|
|
380
|
-
>,
|
|
381
|
-
): Response {
|
|
382
|
-
return Response.json(
|
|
383
|
-
typeof error === "string"
|
|
384
|
-
? { error: "INVALID_RESOURCE", message: error }
|
|
385
|
-
: error,
|
|
386
|
-
{ status: 400 },
|
|
387
|
-
);
|
|
388
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
error: string;
|
|
6
|
+
}>();
|
|
7
|
+
|
|
8
|
+
const emit = defineEmits<{
|
|
9
|
+
retry: [];
|
|
10
|
+
}>();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div
|
|
15
|
+
class="flex flex-col gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm"
|
|
16
|
+
>
|
|
17
|
+
<p class="font-medium text-destructive">加载失败</p>
|
|
18
|
+
<p class="text-muted-foreground">{{ error }}</p>
|
|
19
|
+
<Button variant="outline" class="w-fit" @click="emit('retry')">重试</Button>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { mount } from "@vue/test-utils";
|
|
3
|
+
import SystemResourceLoadError from "./SystemResourceLoadError.vue";
|
|
4
|
+
|
|
5
|
+
describe("SystemResourceLoadError", () => {
|
|
6
|
+
test("展示加载错误并通过显式事件请求重试", async () => {
|
|
7
|
+
const wrapper = mount(SystemResourceLoadError, {
|
|
8
|
+
props: { error: "服务暂不可用" },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(wrapper.text()).toContain("加载失败");
|
|
12
|
+
expect(wrapper.text()).toContain("服务暂不可用");
|
|
13
|
+
await wrapper.get("button").trigger("click");
|
|
14
|
+
|
|
15
|
+
expect(wrapper.emitted("retry")).toHaveLength(1);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -3,7 +3,6 @@ import { computed, onMounted, shallowRef } from "vue";
|
|
|
3
3
|
import type { PermissionDefinition } from "@southwind-ai/shared";
|
|
4
4
|
import ConfirmDialog from "@/components/common/ConfirmDialog.vue";
|
|
5
5
|
import DataPagination from "@/components/common/DataPagination.vue";
|
|
6
|
-
import { Button } from "@/components/ui/button";
|
|
7
6
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
7
|
import type { SystemRecord } from "@/services/system-api";
|
|
9
8
|
import { useSystemResource } from "../composables/useSystemResource";
|
|
@@ -21,6 +20,7 @@ import DepartmentTransitionDialog from "./department/DepartmentTransitionDialog.
|
|
|
21
20
|
import { useDepartmentTransitions } from "../composables/useDepartmentTransitions";
|
|
22
21
|
import SystemResourceActions from "./SystemResourceActions.vue";
|
|
23
22
|
import SystemResourceEditor from "./SystemResourceEditor.vue";
|
|
23
|
+
import SystemResourceLoadError from "./SystemResourceLoadError.vue";
|
|
24
24
|
// @windy-module system.audit begin
|
|
25
25
|
import AuditLogDetailDialog from "./AuditLogDetailDialog.vue";
|
|
26
26
|
// @windy-module system.audit end
|
|
@@ -248,16 +248,11 @@ async function importData(file: File) {
|
|
|
248
248
|
/>
|
|
249
249
|
</CardHeader>
|
|
250
250
|
<CardContent class="space-y-4">
|
|
251
|
-
<
|
|
251
|
+
<SystemResourceLoadError
|
|
252
252
|
v-if="resource.error.value"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<p class="text-muted-foreground">{{ resource.error.value }}</p>
|
|
257
|
-
<Button variant="outline" class="w-fit" @click="resource.reload"
|
|
258
|
-
>重试</Button
|
|
259
|
-
>
|
|
260
|
-
</div>
|
|
253
|
+
:error="resource.error.value"
|
|
254
|
+
@retry="resource.reload"
|
|
255
|
+
/>
|
|
261
256
|
<SystemResourceTable
|
|
262
257
|
v-else
|
|
263
258
|
:columns="config.columns"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# MCP 业务能力暴露
|
|
2
|
+
|
|
3
|
+
## 决策
|
|
4
|
+
|
|
5
|
+
Windy 以 Module Manifest、Server SDK Handler 和 `GovernedToolRegistry` 组成业务能力深
|
|
6
|
+
模块。MCP、原生 Agent Tool Route 和 Agent Runtime Tool Host 都是该 Interface 的
|
|
7
|
+
Adapter,不拥有业务能力、权限或审计定义。
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
业务 Module
|
|
11
|
+
├── Manifest:Schema / Tool / Feature / Permission / Audit
|
|
12
|
+
└── Handler:类型化 JSON 实现
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
GovernedToolRegistry
|
|
16
|
+
├── Schema 与大小限制
|
|
17
|
+
├── License / Feature / RBAC / Data Scope
|
|
18
|
+
├── Timeout / Audit
|
|
19
|
+
└── 结构化结果
|
|
20
|
+
│
|
|
21
|
+
┌───┴───────────┐
|
|
22
|
+
▼ ▼
|
|
23
|
+
Agent Tool Route MCP Adapter
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
删除 Registry 后,输入输出校验、Guard、Scope、超时和审计复杂度会重新散落到多个调用面;
|
|
27
|
+
因此该模块承担了真实的平台复杂度,而不是协议转发层。
|
|
28
|
+
|
|
29
|
+
## 为什么不直接包装 OpenAPI
|
|
30
|
+
|
|
31
|
+
HTTP Route 面向确定性客户端,参数经常包含分页、传输和页面交互细节。MCP Tool 面向模型
|
|
32
|
+
选择和结构化调用,需要更小、语义明确、风险可治理的 Interface。自动包装所有 OpenAPI
|
|
33
|
+
会导致:
|
|
34
|
+
|
|
35
|
+
- 模型看到大量浅层 CRUD;
|
|
36
|
+
- 页面传输参数成为模型契约;
|
|
37
|
+
- 写操作缺少幂等和审批语义;
|
|
38
|
+
- Route、Tool、权限和审计产生多套漂移定义。
|
|
39
|
+
|
|
40
|
+
因此业务模块显式选择哪些能力成为 Tool;OpenAPI 和 MCP 可以调用同一业务 Service,
|
|
41
|
+
但不会相互自动推导。
|
|
42
|
+
|
|
43
|
+
## MCP Transport
|
|
44
|
+
|
|
45
|
+
远程接入已采用官方 MCP TypeScript SDK `1.29.0` 和无状态 Streamable HTTP,Endpoint
|
|
46
|
+
为 `/api/mcp`。v2 在当前实施时仍为预发布,不进入生产依赖。无状态模式适合当前只读
|
|
47
|
+
请求,也避免把授权快照保存为长期 Session 状态。
|
|
48
|
+
|
|
49
|
+
MCP Endpoint 必须位于现有 Server 身份解析之后:
|
|
50
|
+
|
|
51
|
+
- Bearer/Cookie 只用于解析当前 Server Actor,入口另需通过 `system.agent` Feature 与
|
|
52
|
+
`system.agent.read` Permission;
|
|
53
|
+
- `tools/list` 只返回当前 Actor 获准的只读 Tool;
|
|
54
|
+
- `tools/call` 每次重新经过 Registry,不复用列表阶段的授权结论;
|
|
55
|
+
- 客户端参数不能提供 Permission、Feature、License、Actor 或 Scope;
|
|
56
|
+
- MCP 协议错误与业务 Guard 错误使用稳定、脱敏的错误信息。
|
|
57
|
+
|
|
58
|
+
## Tool、Resource 与 Skill
|
|
59
|
+
|
|
60
|
+
当前只提供 Tool:
|
|
61
|
+
|
|
62
|
+
- Tool 对应可执行的类型化业务查询;
|
|
63
|
+
- Resource 等出现明确的只读文档或静态能力目录消费场景后再增加;
|
|
64
|
+
- Prompt 不作为业务规则来源。
|
|
65
|
+
|
|
66
|
+
Skill 解决开发流程,不属于运行时授权 Interface。仓库内开发 Skill 指导 Agent:
|
|
67
|
+
|
|
68
|
+
1. 创建业务模块和 Manifest;
|
|
69
|
+
2. 注册 Feature、Permission、Audit 与 JSON Schema;
|
|
70
|
+
3. 实现 Repository、Service 和 Tool Handler;
|
|
71
|
+
4. 增加 Composition、Guard、Data Scope 与 Schema 测试;
|
|
72
|
+
5. 使用脚手架和真实生成回归验证。
|
|
73
|
+
|
|
74
|
+
Skill 内容可以从稳定契约和模板派生,但不能独立声明 Tool Key 或授权事实。
|
|
75
|
+
`.agents/skills/windy-business-module/SKILL.md` 随 Starter 一起分发,并指向 Manifest、
|
|
76
|
+
Module Host 和 Tool Handler 的代码事实源。生成组合未包含 `system.agent` 时,Skill
|
|
77
|
+
要求明确物理边界,不在业务层另建 MCP 或授权实现。
|
|
78
|
+
|
|
79
|
+
## 写操作门槛
|
|
80
|
+
|
|
81
|
+
MCP Adapter 只暴露 `operation=query`。开放 `command/background` 前必须完成:
|
|
82
|
+
|
|
83
|
+
- 强制幂等键及结果重放语义;
|
|
84
|
+
- 风险分级和人工审批;
|
|
85
|
+
- Durable Job/结果状态引用;
|
|
86
|
+
- Actor、请求、审批和业务副作用的完整关联;
|
|
87
|
+
- 超时后真实副作用是否仍继续的明确语义;
|
|
88
|
+
- 重试、取消和补偿回归。
|
|
89
|
+
|
|
90
|
+
在这些条件满足前,Manifest 可以预先描述写能力,但 MCP Adapter 必须过滤它们。
|
|
91
|
+
|
|
92
|
+
## 运维与验证
|
|
93
|
+
|
|
94
|
+
- Server 启动前校验声明、Schema 和 Handler 一一对应。
|
|
95
|
+
- MCP 不可用不能影响普通业务 Route 和非 AI 能力启动。
|
|
96
|
+
- 审计只记录 Tool Key、Actor、Request ID、输入输出字节数和结果,不保存原始内容。
|
|
97
|
+
- 回归至少覆盖未授权不可发现、直接调用重新授权、Schema 拒绝、数据范围不可扩大和
|
|
98
|
+
Handler 输出漂移。
|
|
99
|
+
- MCP API Path、Server 身份、Route 与 Manifest Binding 使用
|
|
100
|
+
`packages/shared/src/mcp.ts` 的共享 Contract;生成 README 通过跨层测试校对该路径。
|
|
101
|
+
- Tool 目录从当前 Module Manifest 组合,不在 README 或 Skill 固化。新模板通过发布时
|
|
102
|
+
`template:sync` 更新;既有客户项目只通过显式 `windy update` 合并,不远程静默改写。
|
|
103
|
+
|
|
104
|
+
官方协议与 SDK 资料:
|
|
105
|
+
|
|
106
|
+
- [MCP TypeScript SDK v1](https://ts.sdk.modelcontextprotocol.io/)
|
|
107
|
+
- [MCP Server 与 Streamable HTTP](https://ts.sdk.modelcontextprotocol.io/server)
|
|
@@ -78,9 +78,9 @@ composable。
|
|
|
78
78
|
先提交当前业务改动并保持 Git 工作区干净,然后执行:
|
|
79
79
|
|
|
80
80
|
```bash
|
|
81
|
-
bunx --bun create-windy@0.3.
|
|
82
|
-
bunx --bun create-windy@0.3.
|
|
83
|
-
bunx --bun create-windy@0.3.
|
|
81
|
+
bunx --bun create-windy@0.3.1 update --check --cwd .
|
|
82
|
+
bunx --bun create-windy@0.3.1 update --dry-run --cwd .
|
|
83
|
+
bunx --bun create-windy@0.3.1 update --cwd .
|
|
84
84
|
bun install
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -212,14 +212,14 @@ SSE 事件包括 `output.delta`、`tool.requested`、`tool.completed`、usage,
|
|
|
212
212
|
`run.completed`、`run.failed` 或 `run.cancelled` 终态。断线后使用已确认 sequence 作为
|
|
213
213
|
cursor 恢复;不要重新创建 Run 或重复执行 Tool。
|
|
214
214
|
|
|
215
|
-
## 从 create-windy 0.2.34 升级到 0.3.
|
|
215
|
+
## 从 create-windy 0.2.34 升级到 0.3.1
|
|
216
216
|
|
|
217
|
-
`0.2.34 → 0.3.0`
|
|
218
|
-
|
|
217
|
+
目标 CLI 会一次解析 `0.2.34 → 0.3.0 → 0.3.1` 连续 recipe,不需要逐个安装中间
|
|
218
|
+
CLI。确认 npm registry 已能解析精确的 `0.3.1` artifact 后,先检查计划,再应用:
|
|
219
219
|
|
|
220
220
|
```bash
|
|
221
|
-
bunx --bun create-windy@0.3.
|
|
222
|
-
bunx --bun create-windy@0.3.
|
|
221
|
+
bunx --bun create-windy@0.3.1 update --check
|
|
222
|
+
bunx --bun create-windy@0.3.1 update
|
|
223
223
|
bun install
|
|
224
224
|
```
|
|
225
225
|
|
|
@@ -94,6 +94,10 @@ Transport 返回协议允许的状态。
|
|
|
94
94
|
http://localhost:9747/api/mcp
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
普通 Server 已提供该 Endpoint,不需要启动 `dev:agent`。非浏览器 MCP 客户端先通过
|
|
98
|
+
`POST /api/auth/api/login` 获取 Bearer Token,再调用 `tools/list` 获取当前账号实时可见
|
|
99
|
+
的 Tool 与 JSON Schema;客户端和文档都不应固化 Tool 清单。
|
|
100
|
+
|
|
97
101
|
MCP 使用无状态模式,不保存授权快照或长期 Session。Feature、角色或数据范围变化会在下一次
|
|
98
102
|
调用立即生效。
|
|
99
103
|
|