wwise-waapi-mcp 1.0.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 (38) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +221 -0
  3. package/README_ZH.md +221 -0
  4. package/config/domains.json +130 -0
  5. package/config/runtime.json +4 -0
  6. package/dist/src/core/server.js +80 -0
  7. package/dist/src/core/transport.js +92 -0
  8. package/dist/src/domains/audio/tools.js +193 -0
  9. package/dist/src/domains/catalog/tools.js +198 -0
  10. package/dist/src/domains/debug/tools.js +139 -0
  11. package/dist/src/domains/example/tools.js +56 -0
  12. package/dist/src/domains/log/tools.js +79 -0
  13. package/dist/src/domains/object/tools.js +499 -0
  14. package/dist/src/domains/plugin/tools.js +45 -0
  15. package/dist/src/domains/profiler/tools.js +266 -0
  16. package/dist/src/domains/project/tools.js +179 -0
  17. package/dist/src/domains/remote/tools.js +73 -0
  18. package/dist/src/domains/sound/tools.js +38 -0
  19. package/dist/src/domains/soundbank/tools.js +137 -0
  20. package/dist/src/domains/soundengine/tools.js +529 -0
  21. package/dist/src/domains/sourceControl/tools.js +191 -0
  22. package/dist/src/domains/switchContainer/tools.js +64 -0
  23. package/dist/src/domains/transport/tools.js +116 -0
  24. package/dist/src/domains/ui/tools.js +126 -0
  25. package/dist/src/domains/undo/tools.js +75 -0
  26. package/dist/src/index.js +95 -0
  27. package/dist/src/lib/errors.js +31 -0
  28. package/dist/src/lib/logger.js +43 -0
  29. package/dist/src/lib/referenceCatalog.js +167 -0
  30. package/dist/src/lib/response.js +88 -0
  31. package/dist/src/lib/runtimePaths.js +21 -0
  32. package/dist/src/lib/toolFactory.js +73 -0
  33. package/dist/src/lib/waapiClient.js +97 -0
  34. package/dist/src/lib/waapiSchemaResolver.js +120 -0
  35. package/dist/src/registry/toolRegistry.js +180 -0
  36. package/dist/src/registry/types.js +2 -0
  37. package/dist/tests/verify.js +119 -0
  38. package/package.json +56 -0
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.main = main;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const server_js_1 = require("./core/server.js");
9
+ const transport_js_1 = require("./core/transport.js");
10
+ const referenceCatalog_js_1 = require("./lib/referenceCatalog.js");
11
+ const runtimePaths_js_1 = require("./lib/runtimePaths.js");
12
+ const waapiSchemaResolver_js_1 = require("./lib/waapiSchemaResolver.js");
13
+ const toolRegistry_js_1 = require("./registry/toolRegistry.js");
14
+ const tools_js_1 = require("./domains/catalog/tools.js");
15
+ const tools_js_2 = require("./domains/example/tools.js");
16
+ const tools_js_3 = require("./domains/soundengine/tools.js");
17
+ const tools_js_4 = require("./domains/object/tools.js");
18
+ const tools_js_5 = require("./domains/audio/tools.js");
19
+ const tools_js_6 = require("./domains/soundbank/tools.js");
20
+ const tools_js_7 = require("./domains/transport/tools.js");
21
+ const tools_js_8 = require("./domains/profiler/tools.js");
22
+ const tools_js_9 = require("./domains/project/tools.js");
23
+ const tools_js_10 = require("./domains/remote/tools.js");
24
+ const tools_js_11 = require("./domains/ui/tools.js");
25
+ const tools_js_12 = require("./domains/debug/tools.js");
26
+ const tools_js_13 = require("./domains/switchContainer/tools.js");
27
+ const tools_js_14 = require("./domains/undo/tools.js");
28
+ const tools_js_15 = require("./domains/log/tools.js");
29
+ const tools_js_16 = require("./domains/plugin/tools.js");
30
+ const tools_js_17 = require("./domains/sourceControl/tools.js");
31
+ const tools_js_18 = require("./domains/sound/tools.js");
32
+ /** 从 config/domains.json 加载领域元数据列表。 */
33
+ function loadDomainsConfig() {
34
+ const document = JSON.parse(node_fs_1.default.readFileSync((0, runtimePaths_js_1.getConfigPath)("domains.json"), "utf8"));
35
+ return document.domains;
36
+ }
37
+ /**
38
+ * 服务器入口:加载配置和参考目录,创建并填充工具注册表,
39
+ * 根据环境变量应用访问控制策略,然后将 MCP 服务器连接到选定的传输层。
40
+ *
41
+ * 传输层选择(优先级从高到低):
42
+ * --http CLI 标志 → HTTP/SSE 模式
43
+ * MCP_TRANSPORT=http 环境变量 → HTTP/SSE 模式
44
+ * 默认 → stdio 模式
45
+ *
46
+ * HTTP/SSE 模式下端口配置(优先级从高到低):
47
+ * --port <n> CLI 标志
48
+ * PORT 环境变量
49
+ * 默认 3000
50
+ */
51
+ async function main() {
52
+ const schemaLocation = await (0, waapiSchemaResolver_js_1.resolveWaapiSchemaDirectory)();
53
+ const registry = new toolRegistry_js_1.ToolRegistry(loadDomainsConfig(), (0, referenceCatalog_js_1.loadReferenceCatalog)(schemaLocation.schemaDir));
54
+ console.error(`Using WAAPI schema directory: ${schemaLocation.schemaDir} (source: ${schemaLocation.source}).`);
55
+ registry.registerTools((0, tools_js_2.getExampleTools)());
56
+ registry.registerTools((0, tools_js_3.getSoundengineTools)());
57
+ registry.registerTools((0, tools_js_4.getObjectTools)());
58
+ registry.registerTools((0, tools_js_5.getAudioTools)());
59
+ registry.registerTools((0, tools_js_6.getSoundbankTools)());
60
+ registry.registerTools((0, tools_js_7.getTransportTools)());
61
+ registry.registerTools((0, tools_js_8.getProfilerTools)());
62
+ registry.registerTools((0, tools_js_9.getProjectTools)());
63
+ registry.registerTools((0, tools_js_10.getRemoteTools)());
64
+ registry.registerTools((0, tools_js_11.getUiTools)());
65
+ registry.registerTools((0, tools_js_12.getDebugTools)());
66
+ registry.registerTools((0, tools_js_13.getSwitchContainerTools)());
67
+ registry.registerTools((0, tools_js_14.getUndoTools)());
68
+ registry.registerTools((0, tools_js_15.getLogTools)());
69
+ registry.registerTools((0, tools_js_16.getPluginTools)());
70
+ registry.registerTools((0, tools_js_17.getSourceControlTools)());
71
+ registry.registerTools((0, tools_js_18.getSoundTools)());
72
+ registry.registerTools((0, tools_js_1.getCatalogTools)(registry));
73
+ const policy = toolRegistry_js_1.ToolRegistry.policyFromEnv(process.env);
74
+ // --- 传输层模式检测 ---
75
+ const args = process.argv.slice(2);
76
+ const useHttp = args.includes("--http") ||
77
+ (process.env["MCP_TRANSPORT"] ?? "").toLowerCase() === "http";
78
+ if (useHttp) {
79
+ const portFlag = args.indexOf("--port");
80
+ const portArg = portFlag !== -1 ? parseInt(args[portFlag + 1] ?? "", 10) : NaN;
81
+ const port = !isNaN(portArg) ? portArg :
82
+ process.env["PORT"] ? parseInt(process.env["PORT"], 10) : 3000;
83
+ await (0, transport_js_1.startHttpSseServer)(() => (0, server_js_1.createServer)(registry, policy), port);
84
+ }
85
+ else {
86
+ const server = (0, server_js_1.createServer)(registry, policy);
87
+ const transport = (0, transport_js_1.createStdioTransport)();
88
+ await server.connect(transport);
89
+ console.error("Wwise MCP server is running on stdio.");
90
+ }
91
+ }
92
+ void main().catch(error => {
93
+ console.error("Failed to start Wwise MCP server.", error);
94
+ process.exit(1);
95
+ });
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AppError = void 0;
4
+ exports.toFailureResponse = toFailureResponse;
5
+ const response_js_1 = require("./response.js");
6
+ /**
7
+ * 结构化应用错误,携带 WAAPI 错误码和可选的调试详情。
8
+ * 用于在工具处理流程中区分业务失败与运行时异常。
9
+ */
10
+ class AppError extends Error {
11
+ constructor(code, message, details) {
12
+ super(message);
13
+ this.name = "AppError";
14
+ this.code = code;
15
+ this.details = details;
16
+ }
17
+ }
18
+ exports.AppError = AppError;
19
+ /**
20
+ * 将任意捕获的异常统一转换为标准失败响应包。
21
+ * AppError 会保留其原有错误码;其他错误均归类为 internal_error。
22
+ */
23
+ function toFailureResponse(error) {
24
+ if (error instanceof AppError) {
25
+ return (0, response_js_1.fail)(error.code, error.message, error.details);
26
+ }
27
+ if (error instanceof Error) {
28
+ return (0, response_js_1.fail)("internal_error", error.message);
29
+ }
30
+ return (0, response_js_1.fail)("internal_error", "Unknown error", error);
31
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redact = redact;
4
+ exports.logToolCall = logToolCall;
5
+ /** 匹配敏感字段名的正则,命中则将值替换为 [REDACTED]。 */
6
+ const SENSITIVE_KEY_PATTERN = /token|password|secret/i;
7
+ /**
8
+ * 递归遍历对象/数组,将敏感字段值替换为 [REDACTED]。
9
+ * 确保工具参数在写入日志前不泄露凭据或密钥。
10
+ */
11
+ function redact(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map(item => redact(item));
14
+ }
15
+ if (value && typeof value === "object") {
16
+ const record = value;
17
+ const next = {};
18
+ for (const [key, nested] of Object.entries(record)) {
19
+ next[key] = SENSITIVE_KEY_PATTERN.test(key) ? "[REDACTED]" : redact(nested);
20
+ }
21
+ return next;
22
+ }
23
+ return value;
24
+ }
25
+ /**
26
+ * 将工具调用结果以结构化 JSON 形式写入 stderr。
27
+ * 调用方负责将 args 脱敏后再传入;此函数不会主动脱敏以避免重复处理。
28
+ */
29
+ function logToolCall(input) {
30
+ const payload = {
31
+ tool_name: input.toolName,
32
+ duration_ms: input.durationMs,
33
+ success: input.success,
34
+ error_code: input.errorCode ?? null,
35
+ args: redact(input.args)
36
+ };
37
+ const message = JSON.stringify(payload);
38
+ if (input.success) {
39
+ console.error(message);
40
+ return;
41
+ }
42
+ console.error(message);
43
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setReferenceDirectory = setReferenceDirectory;
7
+ exports.loadReferenceCatalog = loadReferenceCatalog;
8
+ exports.loadReferenceDocument = loadReferenceDocument;
9
+ exports.extractReferenceSummary = extractReferenceSummary;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ let activeReferenceDir;
13
+ /** 设置当前进程使用的 WAAPI schema 目录。 */
14
+ function setReferenceDirectory(referenceDir) {
15
+ activeReferenceDir = referenceDir;
16
+ }
17
+ /** 将字符串的首字母转大写。 */
18
+ function sentenceCase(value) {
19
+ if (!value) {
20
+ return value;
21
+ }
22
+ return value.charAt(0).toUpperCase() + value.slice(1);
23
+ }
24
+ /** 将 camelCase 字符串拆分为空格分隔的单词。 */
25
+ function splitCamelCase(value) {
26
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
27
+ }
28
+ /**
29
+ * 根据 WAAPI 函数全限定名(如 ak.soundengine.postEvent)推断所属领域和动作名。
30
+ * 顺序很重要:较具体的前缀应排在较通用前缀之前。
31
+ */
32
+ function domainFromToolName(name) {
33
+ if (name.startsWith("ak.soundengine.")) {
34
+ return { domain: "soundengine", action: name.slice("ak.soundengine.".length) };
35
+ }
36
+ if (name.startsWith("ak.wwise.core.object.")) {
37
+ return { domain: "object", action: name.slice("ak.wwise.core.object.".length) };
38
+ }
39
+ if (name.startsWith("ak.wwise.core.audioSourcePeaks.")) {
40
+ return { domain: "audio", action: name.slice("ak.wwise.core.audioSourcePeaks.".length) };
41
+ }
42
+ if (name.startsWith("ak.wwise.core.audio.")) {
43
+ return { domain: "audio", action: name.slice("ak.wwise.core.audio.".length) };
44
+ }
45
+ if (name.startsWith("ak.wwise.core.soundbank.")) {
46
+ return { domain: "soundbank", action: name.slice("ak.wwise.core.soundbank.".length) };
47
+ }
48
+ if (name.startsWith("ak.wwise.core.transport.")) {
49
+ return { domain: "transport", action: name.slice("ak.wwise.core.transport.".length) };
50
+ }
51
+ if (name.startsWith("ak.wwise.core.profiler.")) {
52
+ return { domain: "profiler", action: name.slice("ak.wwise.core.profiler.".length) };
53
+ }
54
+ if (name.startsWith("ak.wwise.core.switchContainer.")) {
55
+ return { domain: "switchContainer", action: name.slice("ak.wwise.core.switchContainer.".length) };
56
+ }
57
+ if (name.startsWith("ak.wwise.core.undo.")) {
58
+ return { domain: "undo", action: name.slice("ak.wwise.core.undo.".length) };
59
+ }
60
+ if (name.startsWith("ak.wwise.core.plugin.")) {
61
+ return { domain: "plugin", action: name.slice("ak.wwise.core.plugin.".length) };
62
+ }
63
+ if (name.startsWith("ak.wwise.core.sourceControl.")) {
64
+ return { domain: "sourceControl", action: name.slice("ak.wwise.core.sourceControl.".length) };
65
+ }
66
+ if (name.startsWith("ak.wwise.core.sound.")) {
67
+ return { domain: "sound", action: name.slice("ak.wwise.core.sound.".length) };
68
+ }
69
+ if (name === "ak.wwise.core.getInfo" || name === "ak.wwise.core.getProjectInfo") {
70
+ return { domain: "project", action: name.slice("ak.wwise.core.".length) };
71
+ }
72
+ if (name.startsWith("ak.wwise.console.project.")) {
73
+ return { domain: "project", action: name.slice("ak.wwise.console.project.".length) };
74
+ }
75
+ if (name.startsWith("ak.wwise.ui.project.")) {
76
+ return { domain: "project", action: name.slice("ak.wwise.ui.project.".length) };
77
+ }
78
+ if (name.startsWith("ak.wwise.core.project.")) {
79
+ return { domain: "project", action: name.slice("ak.wwise.core.project.".length) };
80
+ }
81
+ if (name.startsWith("ak.wwise.core.remote.")) {
82
+ return { domain: "remote", action: name.slice("ak.wwise.core.remote.".length) };
83
+ }
84
+ if (name.startsWith("ak.wwise.ui.")) {
85
+ return { domain: "ui", action: name.slice("ak.wwise.ui.".length) };
86
+ }
87
+ if (name.startsWith("ak.wwise.debug.")) {
88
+ return { domain: "debug", action: name.slice("ak.wwise.debug.".length) };
89
+ }
90
+ if (name.startsWith("ak.wwise.cli.")) {
91
+ return { domain: "debug", action: name.slice("ak.wwise.cli.".length) };
92
+ }
93
+ if (name.startsWith("ak.wwise.core.executeLuaScript")) {
94
+ return { domain: "debug", action: name.slice("ak.wwise.".length) };
95
+ }
96
+ if (name.startsWith("ak.wwise.core.log.")) {
97
+ return { domain: "log", action: name.slice("ak.wwise.core.log.".length) };
98
+ }
99
+ if (name.startsWith("ak.wwise.waapi.")) {
100
+ return { domain: "catalog", action: name.slice("ak.wwise.waapi.".length) };
101
+ }
102
+ return { domain: "catalog", action: name };
103
+ }
104
+ /** 将动作名(如 postEvent)转化为可读的摘要文字。 */
105
+ function summarizeAction(action) {
106
+ const normalized = action.split(".").map(splitCamelCase).join(" ");
107
+ return sentenceCase(normalized);
108
+ }
109
+ /**
110
+ * 扫描 WAAPI schema 目录下所有 JSON 文件,构建工具名 → 目录条目的映射表。
111
+ * 此过程不解析文件内容,仅根据文件名推断元数据,确保启动消耗最小。
112
+ */
113
+ function loadReferenceCatalog(referenceDir) {
114
+ setReferenceDirectory(referenceDir);
115
+ if (!node_fs_1.default.existsSync(referenceDir)) {
116
+ return new Map();
117
+ }
118
+ const files = node_fs_1.default.readdirSync(referenceDir);
119
+ const catalog = new Map();
120
+ for (const file of files) {
121
+ if (!file.endsWith(".json") || file === "waapi_definitions.json") {
122
+ continue;
123
+ }
124
+ const name = node_path_1.default.basename(file, ".json");
125
+ const parsed = domainFromToolName(name);
126
+ catalog.set(name, {
127
+ name,
128
+ domain: parsed.domain,
129
+ action: parsed.action,
130
+ referenceFile: file,
131
+ summary: summarizeAction(parsed.action)
132
+ });
133
+ }
134
+ return catalog;
135
+ }
136
+ /**
137
+ * 按需加载并解析单个 WAAPI 参考 JSON 文件。
138
+ * 若文件不存在则返回 undefined,调用方应处理该情况。
139
+ */
140
+ function loadReferenceDocument(toolName) {
141
+ if (!activeReferenceDir) {
142
+ return undefined;
143
+ }
144
+ const filePath = node_path_1.default.join(activeReferenceDir, `${toolName}.json`);
145
+ if (!node_fs_1.default.existsSync(filePath)) {
146
+ return undefined;
147
+ }
148
+ return JSON.parse(node_fs_1.default.readFileSync(filePath, "utf8"));
149
+ }
150
+ /**
151
+ * 从完整的 WAAPI 参考文档中提取第一个主函数的部分关键字段,用于工具详情展示。
152
+ * 仅提取 schema 类信息,不包含完整的实例和历史记录以控制响应体积。
153
+ */
154
+ function extractReferenceSummary(document) {
155
+ const functions = Array.isArray(document.functions) ? document.functions : [];
156
+ const topics = Array.isArray(document.topics) ? document.topics : [];
157
+ const primary = (functions[0] ?? topics[0] ?? {});
158
+ return {
159
+ id: primary.id,
160
+ description: primary.description,
161
+ restrict: primary.restrict,
162
+ seeAlso: primary.seeAlso,
163
+ argsSchema: primary.argsSchema,
164
+ resultSchema: primary.resultSchema,
165
+ optionsSchema: primary.optionsSchema
166
+ };
167
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.standardResponseJsonSchema = exports.standardResponseSchema = void 0;
4
+ exports.ok = ok;
5
+ exports.fail = fail;
6
+ exports.toMcpToolResult = toMcpToolResult;
7
+ const v4_1 = require("zod/v4");
8
+ /** Zod 运行时验证 schema,用于内部断言响应结构正确性。 */
9
+ exports.standardResponseSchema = v4_1.z.discriminatedUnion("ok", [
10
+ v4_1.z.object({
11
+ ok: v4_1.z.literal(true),
12
+ data: v4_1.z.unknown()
13
+ }),
14
+ v4_1.z.object({
15
+ ok: v4_1.z.literal(false),
16
+ error: v4_1.z.object({
17
+ code: v4_1.z.string(),
18
+ message: v4_1.z.string(),
19
+ details: v4_1.z.unknown().optional()
20
+ })
21
+ })
22
+ ]);
23
+ /**
24
+ * 对应 standardResponseSchema 的 JSON Schema 表示。
25
+ * 注意:不要将此 schema 传给 MCP SDK 的 registerTool outputSchema 参数,
26
+ * 该版本 SDK 对判别联合类型的运行时校验存在 bug。
27
+ */
28
+ exports.standardResponseJsonSchema = {
29
+ oneOf: [
30
+ {
31
+ type: "object",
32
+ properties: {
33
+ ok: { const: true },
34
+ data: {}
35
+ },
36
+ required: ["ok", "data"],
37
+ additionalProperties: false
38
+ },
39
+ {
40
+ type: "object",
41
+ properties: {
42
+ ok: { const: false },
43
+ error: {
44
+ type: "object",
45
+ properties: {
46
+ code: { type: "string" },
47
+ message: { type: "string" },
48
+ details: {}
49
+ },
50
+ required: ["code", "message"],
51
+ additionalProperties: false
52
+ }
53
+ },
54
+ required: ["ok", "error"],
55
+ additionalProperties: false
56
+ }
57
+ ]
58
+ };
59
+ /** 构造成功响应包。 */
60
+ function ok(data) {
61
+ return { ok: true, data };
62
+ }
63
+ /** 构造失败响应包,details 为可选的调试附加信息。 */
64
+ function fail(code, message, details) {
65
+ return {
66
+ ok: false,
67
+ error: {
68
+ code,
69
+ message,
70
+ ...(details === undefined ? {} : { details })
71
+ }
72
+ };
73
+ }
74
+ /**
75
+ * 将标准响应包序列化为 MCP SDK 要求的工具返回格式。
76
+ * 同时填充 structuredContent 以便支持 MCP 结构化输出的客户端直接解析。
77
+ */
78
+ function toMcpToolResult(response) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify(response, null, 2)
84
+ }
85
+ ],
86
+ structuredContent: response
87
+ };
88
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getProjectRoot = getProjectRoot;
7
+ exports.getConfigPath = getConfigPath;
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * 返回项目根目录的绝对路径。
11
+ * - 开发模式:__dirname 位于 dist/src/lib,向上三级即为仓库根目录。
12
+ * - 打包模式(pkg):使用可执行文件所在目录。
13
+ */
14
+ function getProjectRoot() {
15
+ const packagedProcess = process;
16
+ return packagedProcess.pkg ? node_path_1.default.dirname(process.execPath) : node_path_1.default.resolve(__dirname, "..", "..", "..");
17
+ }
18
+ /** 返回 config/ 目录下指定文件的绝对路径。 */
19
+ function getConfigPath(...segments) {
20
+ return node_path_1.default.join(getProjectRoot(), "config", ...segments);
21
+ }
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWaapiStubTool = void 0;
4
+ exports.createWaapiTool = createWaapiTool;
5
+ const v4_1 = require("zod/v4");
6
+ const response_js_1 = require("./response.js");
7
+ const waapiClient_js_1 = require("./waapiClient.js");
8
+ /**
9
+ * 在 Zod 属性规范中自动注入可选的 options 字段。
10
+ * 若传入的是原始 Zod 类型(透过 _zod 属性判定)则不作修改。
11
+ */
12
+ function withOptionalOptionsSchema(inputSchema) {
13
+ if (!inputSchema || Array.isArray(inputSchema) || typeof inputSchema !== "object" || "_zod" in inputSchema) {
14
+ return inputSchema;
15
+ }
16
+ const shape = inputSchema;
17
+ return {
18
+ ...shape,
19
+ options: shape.options ?? v4_1.z.unknown().optional()
20
+ };
21
+ }
22
+ /**
23
+ * 在 JSON Schema 中自动注入可选的 options 字段。
24
+ * 若 inputSchemaJson 不存在或不是 object 类型,返回最小化的兼容 schema。
25
+ */
26
+ function withOptionalOptionsJsonSchema(inputSchemaJson) {
27
+ if (!inputSchemaJson || inputSchemaJson.type !== "object") {
28
+ return {
29
+ type: "object",
30
+ properties: {
31
+ options: {}
32
+ },
33
+ additionalProperties: true
34
+ };
35
+ }
36
+ const properties = inputSchemaJson.properties && typeof inputSchemaJson.properties === "object"
37
+ ? { ...inputSchemaJson.properties }
38
+ : {};
39
+ if (!("options" in properties)) {
40
+ properties.options = {};
41
+ }
42
+ return {
43
+ ...inputSchemaJson,
44
+ properties
45
+ };
46
+ }
47
+ /**
48
+ * 创建一个真实执行 WAAPI RPC 调用的 ToolDefinition。
49
+ * 处理器会自动从入参中分离 options 并传入 WAAPI optionsSchema。
50
+ * 返回值使用标准包:{ ok: true, data: { tool, domain, result } }。
51
+ */
52
+ function createWaapiTool(input) {
53
+ return {
54
+ ...input,
55
+ inputSchema: withOptionalOptionsSchema(input.inputSchema),
56
+ inputSchemaJson: withOptionalOptionsJsonSchema(input.inputSchemaJson),
57
+ callable: true,
58
+ implementationStatus: "implemented",
59
+ outputSchemaJson: response_js_1.standardResponseJsonSchema,
60
+ handler: async (rawArgs) => {
61
+ const args = (rawArgs ?? {});
62
+ const { options, ...waapiArgs } = args;
63
+ const data = await (0, waapiClient_js_1.callWaapi)(input.name, waapiArgs, options);
64
+ return (0, response_js_1.ok)({
65
+ tool: input.name,
66
+ domain: input.domain,
67
+ result: data
68
+ });
69
+ }
70
+ };
71
+ }
72
+ /** createWaapiTool 的别名,保留以兼容旧引用。 */
73
+ exports.createWaapiStubTool = createWaapiTool;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getWaapiSession = getWaapiSession;
4
+ exports.callWaapi = callWaapi;
5
+ exports.disconnectWaapi = disconnectWaapi;
6
+ const waapi_client_1 = require("waapi-client");
7
+ const fs_1 = require("fs");
8
+ const errors_js_1 = require("./errors.js");
9
+ const runtimePaths_js_1 = require("./runtimePaths.js");
10
+ /** 从 runtime.json 读取 WAAPI URL 配置 */
11
+ function getWaapiUrl() {
12
+ try {
13
+ const configPath = (0, runtimePaths_js_1.getConfigPath)("runtime.json");
14
+ const config = JSON.parse((0, fs_1.readFileSync)(configPath, "utf-8"));
15
+ return config.waapiUrl?.trim() || "ws://127.0.0.1:8080/waapi";
16
+ }
17
+ catch {
18
+ // 如果读取配置失败,使用默认值
19
+ return "ws://127.0.0.1:8080/waapi";
20
+ }
21
+ }
22
+ /** 应用内共享的单例 WAAPI 会话对象。 */
23
+ let activeSession;
24
+ /** 连接过程中的 Promise,用于防止并发请求触发多次连接。 */
25
+ let connectPromise;
26
+ /**
27
+ * 尝试与 WAAPI 服务器建立新会话。
28
+ * 连接失败时抛出带 waapi_unavailable 错误码的 AppError。
29
+ */
30
+ async function openSession() {
31
+ const url = getWaapiUrl();
32
+ try {
33
+ return await (0, waapi_client_1.connect)(url);
34
+ }
35
+ catch (error) {
36
+ throw new errors_js_1.AppError("waapi_unavailable", `Unable to connect to WAAPI at ${url}.`, {
37
+ url,
38
+ cause: error instanceof Error ? error.message : error
39
+ });
40
+ }
41
+ }
42
+ /**
43
+ * 返回当前已活跃的 WAAPI 会话,必要时自动初始化连接。
44
+ * 并发调用时共享同一个连接 Promise,避免重复连接。
45
+ */
46
+ async function getWaapiSession() {
47
+ if (activeSession) {
48
+ return activeSession;
49
+ }
50
+ if (!connectPromise) {
51
+ connectPromise = openSession();
52
+ }
53
+ try {
54
+ activeSession = await connectPromise;
55
+ return activeSession;
56
+ }
57
+ finally {
58
+ connectPromise = undefined;
59
+ }
60
+ }
61
+ /**
62
+ * 对单个 WAAPI RPC 接口发起调用并返回原始结果。
63
+ * 调用失败时抛出带 waapi_call_failed 的 AppError,并重置内部会话以备下次重连。
64
+ * @param uri WAAPI 函数全限定名,如 ak.soundengine.postEvent
65
+ * @param args 函数入参(对应该接口的 argsSchema)
66
+ * @param options 函数选项(对应该接口的 optionsSchema)
67
+ */
68
+ async function callWaapi(uri, args, options) {
69
+ const session = await getWaapiSession();
70
+ try {
71
+ return await session.call(uri, args ?? {}, options ?? {});
72
+ }
73
+ catch (error) {
74
+ activeSession = undefined;
75
+ throw new errors_js_1.AppError("waapi_call_failed", `WAAPI call failed for ${uri}.`, {
76
+ uri,
77
+ cause: error instanceof Error ? error.message : error
78
+ });
79
+ }
80
+ }
81
+ /**
82
+ * 断开当前活跃的 WAAPI 会话。通常在进程退出前调用以保持连接干净。
83
+ * 断开错误会被忙略,不会影响清理流程。
84
+ */
85
+ async function disconnectWaapi() {
86
+ if (!activeSession) {
87
+ return;
88
+ }
89
+ const session = activeSession;
90
+ activeSession = undefined;
91
+ try {
92
+ await session.disconnect();
93
+ }
94
+ catch {
95
+ // Ignore disconnect errors during shutdown or reconnect.
96
+ }
97
+ }