mu-agent-bridge 1.0.71 → 1.0.78

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 (195) hide show
  1. package/README.md +17 -10
  2. package/dist/src-ts/agent.js +35 -0
  3. package/dist/src-ts/channel-sessions.js +3 -2
  4. package/dist/src-ts/cli.js +67 -37
  5. package/dist/src-ts/control-server.js +12 -30
  6. package/dist/src-ts/core-updater.js +141 -33
  7. package/dist/src-ts/daemon-process.js +2 -1
  8. package/dist/src-ts/dsh-agent.js +31 -13
  9. package/dist/src-ts/http-agent.js +36 -0
  10. package/dist/src-ts/plugin-catalog.js +11 -2
  11. package/dist/src-ts/plugin-manager.js +351 -50
  12. package/dist/src-ts/plugins/abweb/index.js +1 -1
  13. package/dist/src-ts/plugins/abweb/public/app.js +73 -5
  14. package/dist/src-ts/plugins/abweb/public/index.html +15 -0
  15. package/dist/src-ts/plugins/abweb/public/style.css +4 -0
  16. package/dist/src-ts/plugins/aun/attachments.js +3 -21
  17. package/dist/src-ts/plugins/aun/index.js +155 -21
  18. package/dist/src-ts/plugins/aun/messages.js +4 -1
  19. package/dist/src-ts/plugins/aun/public/app.js +2 -1
  20. package/dist/src-ts/plugins/aun/public/index.html +1 -1
  21. package/dist/src-ts/plugins/cdp-draft.js +5 -2
  22. package/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  23. package/dist/src-ts/plugins/chatgpt/index.js +80 -19
  24. package/dist/src-ts/plugins/claude-cli/index.js +19 -2
  25. package/dist/src-ts/plugins/codex-cli/index.js +19 -2
  26. package/dist/src-ts/plugins/desktop-launch.js +44 -3
  27. package/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
  28. package/dist/src-ts/plugins/doubao/index.js +152 -8
  29. package/dist/src-ts/plugins/doubao/native-task.js +134 -0
  30. package/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  31. package/dist/src-ts/plugins/dsh/index.js +172 -21
  32. package/dist/src-ts/plugins/dsh/session-history.js +31 -3
  33. package/dist/src-ts/plugins/feishu/index.js +1 -1
  34. package/dist/src-ts/plugins/local-cli/agent.js +74 -9
  35. package/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  36. package/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  37. package/dist/src-ts/plugins/personal-assistant/index.js +263 -0
  38. package/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
  39. package/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  40. package/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  41. package/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  42. package/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
  43. package/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
  44. package/dist/src-ts/plugins/qwenwork/index.js +139 -24
  45. package/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
  46. package/dist/src-ts/plugins/workbuddy/index.js +124 -41
  47. package/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  48. package/dist/src-ts/plugins/zcode/index.js +97 -0
  49. package/dist/src-ts/session-management.js +132 -14
  50. package/dist/src-ts/version-manager.js +6 -3
  51. package/dist/tests-ts/abweb-cli.test.js +10 -2
  52. package/dist/tests-ts/abweb-model-config.test.js +73 -0
  53. package/dist/tests-ts/agent.test.js +27 -0
  54. package/dist/tests-ts/aun-attachments.test.js +19 -17
  55. package/dist/tests-ts/aun-default-owner-page.test.js +2 -1
  56. package/dist/tests-ts/aun-messages.test.js +20 -0
  57. package/dist/tests-ts/aun-plugin.test.js +292 -19
  58. package/dist/tests-ts/bundled-package.test.js +12 -2
  59. package/dist/tests-ts/cdp-draft-recovery.test.js +182 -3
  60. package/dist/tests-ts/channel-sessions.test.js +27 -0
  61. package/dist/tests-ts/chatgpt-plugin.test.js +118 -0
  62. package/dist/tests-ts/control-server.test.js +56 -0
  63. package/dist/tests-ts/core-updater.test.js +215 -46
  64. package/dist/tests-ts/daemon-process.test.js +126 -1
  65. package/dist/tests-ts/desktop-launch.test.js +73 -0
  66. package/dist/tests-ts/doubao-native-task.test.js +237 -0
  67. package/dist/tests-ts/doubao-plugin.test.js +773 -12
  68. package/dist/tests-ts/dsh-agent.test.js +157 -8
  69. package/dist/tests-ts/dsh-cdp.test.js +148 -0
  70. package/dist/tests-ts/dsh-history.test.js +37 -0
  71. package/dist/tests-ts/feishu-plugin.test.js +1 -1
  72. package/dist/tests-ts/http-agent.test.js +18 -0
  73. package/dist/tests-ts/installer-assets.test.js +1 -1
  74. package/dist/tests-ts/local-cli.test.js +146 -1
  75. package/dist/tests-ts/model-provider-cancellation.test.js +152 -0
  76. package/dist/tests-ts/native-session-management.test.js +8 -7
  77. package/dist/tests-ts/personal-assistant-coordination.test.js +92 -0
  78. package/dist/tests-ts/personal-assistant-extensions.test.js +35 -0
  79. package/dist/tests-ts/personal-assistant-memory.test.js +117 -0
  80. package/dist/tests-ts/personal-assistant-owner-context.test.js +107 -0
  81. package/dist/tests-ts/personal-assistant-remote-ability.test.js +123 -0
  82. package/dist/tests-ts/personal-assistant.test.js +663 -0
  83. package/dist/tests-ts/platform-integration.test.js +183 -1
  84. package/dist/tests-ts/plugin-catalog.test.js +12 -2
  85. package/dist/tests-ts/plugin-manager.test.js +356 -4
  86. package/dist/tests-ts/plugin-upgrade-deadline.test.js +354 -0
  87. package/dist/tests-ts/publish-packages.test.js +42 -0
  88. package/dist/tests-ts/qwenwork-plugin.test.js +135 -1
  89. package/dist/tests-ts/qwenwork-reconnect.test.js +195 -0
  90. package/dist/tests-ts/qwenwork-sse.test.js +33 -0
  91. package/dist/tests-ts/session-access-defaults.test.js +112 -0
  92. package/dist/tests-ts/session-management.test.js +159 -4
  93. package/dist/tests-ts/version-manager.test.js +40 -0
  94. package/dist/tests-ts/workbuddy-cdp-recovery.test.js +311 -0
  95. package/dist/tests-ts/workbuddy-plugin.test.js +499 -4
  96. package/dist/tests-ts/zcode-cdp.test.js +894 -0
  97. package/dist/tests-ts/zcode-plugin.test.js +396 -0
  98. package/package.json +1 -1
  99. package/plugins/abweb/dist/src-ts/agent.js +35 -0
  100. package/plugins/abweb/dist/src-ts/plugins/abweb/index.js +1 -1
  101. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +73 -5
  102. package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +57 -5
  103. package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +15 -0
  104. package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +4 -0
  105. package/plugins/abweb/package.json +1 -1
  106. package/plugins/abweb/plugin.json +1 -1
  107. package/plugins/aun/config.schema.json +1 -1
  108. package/plugins/aun/dist/src-ts/agent.js +35 -0
  109. package/plugins/aun/dist/src-ts/plugins/aun/attachments.js +3 -21
  110. package/plugins/aun/dist/src-ts/plugins/aun/index.js +155 -21
  111. package/plugins/aun/dist/src-ts/plugins/aun/messages.js +4 -1
  112. package/plugins/aun/dist/src-ts/plugins/aun/public/app.js +2 -1
  113. package/plugins/aun/dist/src-ts/plugins/aun/public/index.html +1 -1
  114. package/plugins/aun/package.json +1 -1
  115. package/plugins/aun/plugin.json +1 -1
  116. package/plugins/chatgpt/dist/src-ts/agent.js +35 -0
  117. package/plugins/chatgpt/dist/src-ts/plugins/cdp-draft.js +5 -2
  118. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +26 -12
  119. package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +80 -19
  120. package/plugins/chatgpt/dist/src-ts/plugins/desktop-launch.js +44 -3
  121. package/plugins/chatgpt/package.json +1 -1
  122. package/plugins/chatgpt/plugin.json +1 -1
  123. package/plugins/claude-cli/dist/src-ts/agent.js +35 -0
  124. package/plugins/claude-cli/dist/src-ts/plugins/claude-cli/index.js +19 -2
  125. package/plugins/claude-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  126. package/plugins/claude-cli/package.json +1 -1
  127. package/plugins/claude-cli/plugin.json +3 -2
  128. package/plugins/codex-cli/dist/src-ts/agent.js +35 -0
  129. package/plugins/codex-cli/dist/src-ts/plugins/codex-cli/index.js +19 -2
  130. package/plugins/codex-cli/dist/src-ts/plugins/local-cli/agent.js +74 -9
  131. package/plugins/codex-cli/package.json +1 -1
  132. package/plugins/codex-cli/plugin.json +3 -2
  133. package/plugins/doubao/config.schema.json +2 -1
  134. package/plugins/doubao/dist/src-ts/agent.js +35 -0
  135. package/plugins/doubao/dist/src-ts/plugins/desktop-launch.js +44 -3
  136. package/plugins/doubao/dist/src-ts/plugins/doubao/cdp-client.js +234 -45
  137. package/plugins/doubao/dist/src-ts/plugins/doubao/index.js +152 -8
  138. package/plugins/doubao/dist/src-ts/plugins/doubao/native-task.js +134 -0
  139. package/plugins/doubao/package.json +1 -1
  140. package/plugins/doubao/plugin.json +3 -2
  141. package/plugins/dsh/config.schema.json +5 -2
  142. package/plugins/dsh/dist/src-ts/agent.js +35 -0
  143. package/plugins/dsh/dist/src-ts/dsh-agent.js +31 -13
  144. package/plugins/dsh/dist/src-ts/plugins/desktop-launch.js +44 -3
  145. package/plugins/dsh/dist/src-ts/plugins/dsh/cdp-client.js +334 -0
  146. package/plugins/dsh/dist/src-ts/plugins/dsh/index.js +172 -21
  147. package/plugins/dsh/dist/src-ts/plugins/dsh/session-history.js +31 -3
  148. package/plugins/dsh/package.json +1 -1
  149. package/plugins/dsh/plugin.json +3 -2
  150. package/plugins/feishu/dist/src-ts/agent.js +35 -0
  151. package/plugins/feishu/dist/src-ts/plugins/feishu/index.js +1 -1
  152. package/plugins/feishu/package.json +1 -1
  153. package/plugins/feishu/plugin.json +1 -1
  154. package/plugins/personal-assistant/config.schema.json +23 -0
  155. package/plugins/personal-assistant/dist/src-ts/agent.js +69 -0
  156. package/plugins/personal-assistant/dist/src-ts/attachments.js +62 -0
  157. package/plugins/personal-assistant/dist/src-ts/plugin.js +13 -0
  158. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/claude-cli-model.js +30 -0
  159. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/coordination.js +57 -0
  160. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/index.js +263 -0
  161. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/model-client.js +181 -0
  162. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/owner-context.js +56 -0
  163. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/remote-ability.js +176 -0
  164. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/session-memory.js +124 -0
  165. package/plugins/personal-assistant/dist/src-ts/plugins/personal-assistant/tools.js +167 -0
  166. package/plugins/personal-assistant/package.json +39 -0
  167. package/plugins/personal-assistant/plugin.json +15 -0
  168. package/plugins/qwenwork/dist/src-ts/agent.js +35 -0
  169. package/plugins/qwenwork/dist/src-ts/plugins/cdp-draft.js +5 -2
  170. package/plugins/qwenwork/dist/src-ts/plugins/desktop-launch.js +44 -3
  171. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/cdp-client.js +90 -18
  172. package/plugins/qwenwork/dist/src-ts/plugins/qwenwork/index.js +139 -24
  173. package/plugins/qwenwork/package.json +1 -1
  174. package/plugins/qwenwork/plugin.json +3 -2
  175. package/plugins/workbuddy/config.schema.json +1 -1
  176. package/plugins/workbuddy/dist/src-ts/agent.js +35 -0
  177. package/plugins/workbuddy/dist/src-ts/plugins/desktop-launch.js +44 -3
  178. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/cdp-client.js +353 -82
  179. package/plugins/workbuddy/dist/src-ts/plugins/workbuddy/index.js +124 -41
  180. package/plugins/workbuddy/package.json +1 -1
  181. package/plugins/workbuddy/plugin.json +3 -2
  182. package/plugins/zcode/config.schema.json +13 -0
  183. package/plugins/zcode/dist/src-ts/agent.js +69 -0
  184. package/plugins/zcode/dist/src-ts/attachments.js +62 -0
  185. package/plugins/zcode/dist/src-ts/plugin.js +13 -0
  186. package/plugins/zcode/dist/src-ts/plugins/desktop-launch.js +238 -0
  187. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cdp-client.js +896 -0
  188. package/plugins/zcode/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  189. package/plugins/zcode/dist/src-ts/plugins/workbuddy/desktop-launch.js +10 -0
  190. package/plugins/zcode/dist/src-ts/plugins/workbuddy/index.js +567 -0
  191. package/plugins/zcode/dist/src-ts/plugins/workbuddy/session-history.js +121 -0
  192. package/plugins/zcode/dist/src-ts/plugins/zcode/cdp-client.js +500 -0
  193. package/plugins/zcode/dist/src-ts/plugins/zcode/index.js +97 -0
  194. package/plugins/zcode/package.json +16 -0
  195. package/plugins/zcode/plugin.json +11 -0
@@ -0,0 +1,396 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const strict_1 = __importDefault(require("node:assert/strict"));
40
+ const node_test_1 = __importStar(require("node:test"));
41
+ const childProcess = require("node:child_process");
42
+ const desktop = __importStar(require("../src-ts/plugins/desktop-launch"));
43
+ const cdp_client_1 = require("../src-ts/plugins/zcode/cdp-client");
44
+ const zcode_1 = require("../src-ts/plugins/zcode");
45
+ const agent_1 = require("../src-ts/agent");
46
+ function startupFixture(error = new TypeError("fetch failed", { cause: Object.assign(new Error("端口拒绝连接"), { code: "ECONNREFUSED" }) })) {
47
+ const platform = Object.getOwnPropertyDescriptor(process, "platform");
48
+ Object.defineProperty(process, "platform", { value: "win32" });
49
+ let online = false;
50
+ let connections = 0;
51
+ const launches = [];
52
+ const discovery = node_test_1.mock.method(desktop, "findWindowsExecutable", async () => "D:\\自定义安装\\ZCode\\ZCode.exe");
53
+ node_test_1.mock.method(cdp_client_1.ZcodeCdpClient.prototype, "connect", async () => { connections++; if (!online)
54
+ throw error; });
55
+ node_test_1.mock.method(cdp_client_1.ZcodeCdpClient.prototype, "connected", () => online);
56
+ node_test_1.mock.method(childProcess, "spawn", (file, args, options) => {
57
+ launches.push({ file, args, env: options.env });
58
+ online = true;
59
+ return { unref() { }, exitCode: null, signalCode: null };
60
+ });
61
+ return { launches, discovery, connections: () => connections, online(value) { online = value; }, restore() { node_test_1.mock.restoreAll(); Object.defineProperty(process, "platform", platform); } };
62
+ }
63
+ const startupContext = (config = {}) => ({ config, logger: { info() { }, warn() { }, error() { } } });
64
+ (0, node_test_1.default)("ZCode 后台激活自动发现程序并带 CDP 和附加参数启动,重载重新启动自有桌面", async () => {
65
+ const fixture = startupFixture();
66
+ const previous = process.env.ELECTRON_RUN_AS_NODE;
67
+ process.env.ELECTRON_RUN_AS_NODE = "1";
68
+ const plugin = new zcode_1.ZcodePlugin();
69
+ try {
70
+ const context = startupContext({ cdpPort: 19321, startupArgs: ["--lang=zh-CN", "--test-value=含 空格"] });
71
+ await plugin.activate(context);
72
+ strict_1.default.equal((await plugin.health()).status, "ready");
73
+ strict_1.default.equal(fixture.discovery.mock.calls[0].arguments[0], "ZCode.exe");
74
+ strict_1.default.equal(fixture.launches[0].file, "D:\\自定义安装\\ZCode\\ZCode.exe");
75
+ strict_1.default.deepEqual(fixture.launches[0].args, ["--lang=zh-CN", "--test-value=含 空格", "--remote-debugging-address=127.0.0.1", "--remote-debugging-port=19321"]);
76
+ strict_1.default.equal(fixture.launches[0].env.ELECTRON_RUN_AS_NODE, undefined);
77
+ await plugin.deactivate("reload");
78
+ fixture.online(false);
79
+ await plugin.activate(context);
80
+ strict_1.default.equal(fixture.launches.length, 2);
81
+ strict_1.default.ok(fixture.connections() >= 3);
82
+ }
83
+ finally {
84
+ await plugin.deactivate("stop");
85
+ fixture.restore();
86
+ if (previous === undefined)
87
+ delete process.env.ELECTRON_RUN_AS_NODE;
88
+ else
89
+ process.env.ELECTRON_RUN_AS_NODE = previous;
90
+ }
91
+ });
92
+ (0, node_test_1.default)("ZCode 已有 CDP、禁用自动启动和非端口错误均不拉起桌面", async () => {
93
+ for (const mode of ["connected", "disabled", "page"]) {
94
+ const fixture = startupFixture(mode === "page" ? new Error("未找到 ZCode 页面 target") : undefined);
95
+ const plugin = new zcode_1.ZcodePlugin();
96
+ try {
97
+ if (mode === "connected")
98
+ fixture.online(true);
99
+ const start = plugin.activate(startupContext({ autoStart: mode !== "disabled" }));
100
+ if (mode === "connected")
101
+ await start;
102
+ else
103
+ await strict_1.default.rejects(start, mode === "page" ? /未找到 ZCode 页面/ : /fetch failed/);
104
+ strict_1.default.equal(fixture.launches.length, 0);
105
+ strict_1.default.equal(fixture.discovery.mock.callCount(), 0);
106
+ }
107
+ finally {
108
+ await plugin.deactivate("stop");
109
+ fixture.restore();
110
+ }
111
+ }
112
+ });
113
+ (0, node_test_1.default)("ZCode 找不到安装记录时给出自身程序的配置提示", async () => {
114
+ const fixture = startupFixture();
115
+ fixture.discovery.mock.mockImplementation(async () => undefined);
116
+ const plugin = new zcode_1.ZcodePlugin();
117
+ try {
118
+ await strict_1.default.rejects(plugin.activate(startupContext()), /未找到 ZCode.exe.*executablePath/);
119
+ strict_1.default.equal(fixture.launches.length, 0);
120
+ }
121
+ finally {
122
+ await plugin.deactivate("stop");
123
+ fixture.restore();
124
+ }
125
+ });
126
+ (0, node_test_1.default)("ZCode 附加启动参数不能覆盖配置中的 CDP 地址或端口", async () => {
127
+ const fixture = startupFixture();
128
+ const plugin = new zcode_1.ZcodePlugin();
129
+ try {
130
+ for (const startupArgs of [["--remote-debugging-port=9999"], ["--remote-debugging-address", "0.0.0.0"], [123]]) {
131
+ await strict_1.default.rejects(plugin.activate(startupContext({ startupArgs })), /startupArgs/);
132
+ }
133
+ strict_1.default.equal(fixture.launches.length, 0);
134
+ }
135
+ finally {
136
+ await plugin.deactivate("stop");
137
+ fixture.restore();
138
+ }
139
+ });
140
+ (0, node_test_1.default)("ZCode 重载时清理自有进程,委托退出失败也执行清理", async () => {
141
+ for (const delegateFails of [false, true]) {
142
+ const fixture = startupFixture();
143
+ const plugin = new zcode_1.ZcodePlugin();
144
+ const child = { pid: 4321, exitCode: null, signalCode: null, unref() { } };
145
+ const cleanup = node_test_1.mock.method(desktop, "terminateOwnedProcess", async () => undefined);
146
+ try {
147
+ await plugin.activate(startupContext());
148
+ plugin.process = child;
149
+ if (delegateFails)
150
+ plugin.delegate.deactivate = async () => { throw new Error("委托退出失败"); };
151
+ if (delegateFails)
152
+ await strict_1.default.rejects(plugin.deactivate("reload"), /委托退出失败/);
153
+ else
154
+ await plugin.deactivate("reload");
155
+ strict_1.default.equal(cleanup.mock.callCount(), 1);
156
+ strict_1.default.equal(cleanup.mock.calls[0].arguments[0], child);
157
+ }
158
+ finally {
159
+ await plugin.deactivate("stop");
160
+ fixture.restore();
161
+ }
162
+ }
163
+ });
164
+ (0, node_test_1.default)("zcode CDP 客户端选择 zcode 页面", async () => {
165
+ const original = globalThis.WebSocket;
166
+ let url = "";
167
+ class TestWebSocket {
168
+ static OPEN = 1;
169
+ readyState = 0;
170
+ onopen;
171
+ onclose;
172
+ onerror;
173
+ onmessage;
174
+ constructor(target) { url = target; queueMicrotask(() => { this.readyState = 1; this.onopen?.(); }); }
175
+ send(message) { const { id } = JSON.parse(message); queueMicrotask(() => this.onmessage?.({ data: JSON.stringify({ id, result: { result: { value: true } } }) })); }
176
+ close() { this.readyState = 3; }
177
+ }
178
+ globalThis.WebSocket = TestWebSocket;
179
+ const client = new cdp_client_1.ZcodeCdpClient({
180
+ timeoutMs: 20,
181
+ fetchImpl: async () => new Response(JSON.stringify([
182
+ { type: "page", title: "WorkBuddy", url: "app://workbuddy", webSocketDebuggerUrl: "ws://127.0.0.1:9231/devtools/page/1" },
183
+ { type: "page", title: "ZCode", url: "app://zcode", webSocketDebuggerUrl: "ws://127.0.0.1:9231/devtools/page/2" },
184
+ ])),
185
+ });
186
+ try {
187
+ await client.connect();
188
+ strict_1.default.equal(url, "ws://127.0.0.1:9231/devtools/page/2");
189
+ }
190
+ finally {
191
+ client.close();
192
+ globalThis.WebSocket = original;
193
+ }
194
+ });
195
+ (0, node_test_1.default)("zcode CDP 客户端拒绝其他桌面页面", async () => {
196
+ const client = new cdp_client_1.ZcodeCdpClient({
197
+ fetchImpl: async () => new Response(JSON.stringify([
198
+ { type: "page", title: "WorkBuddy", url: "app://workbuddy", webSocketDebuggerUrl: "ws://127.0.0.1:9231/devtools/page/1" },
199
+ ])),
200
+ });
201
+ await strict_1.default.rejects(() => client.connect(), /未找到 ZCode 页面 target/);
202
+ });
203
+ (0, node_test_1.default)("ZCode 重载后仅对发现请求的旧连接重置重试一次,保留端口拒绝错误", async () => {
204
+ for (const mode of ["reset", "reset-refused", "refused"]) {
205
+ let attempts = 0;
206
+ const refused = new TypeError("fetch failed", { cause: Object.assign(new Error("端口拒绝连接"), { code: "ECONNREFUSED" }) });
207
+ const client = new cdp_client_1.ZcodeCdpClient({ fetchImpl: async () => {
208
+ attempts++;
209
+ if (mode !== "refused" && attempts === 1)
210
+ throw new TypeError("fetch failed", { cause: Object.assign(new Error("旧连接重置"), { code: "ECONNRESET" }) });
211
+ if (mode !== "reset")
212
+ throw refused;
213
+ return new Response("[]");
214
+ } });
215
+ await strict_1.default.rejects(client.connect(), mode === "reset" ? /未找到 ZCode 页面 target/ : error => error === refused);
216
+ strict_1.default.equal(attempts, mode === "refused" ? 1 : 2);
217
+ }
218
+ });
219
+ (0, node_test_1.default)("zcode 插件通过 CDP 客户端返回桌面回复", async () => {
220
+ const calls = [];
221
+ const plugin = new zcode_1.ZcodePlugin({ client: {
222
+ connect: async () => undefined,
223
+ close() { },
224
+ connected: () => true,
225
+ createTask: async () => undefined,
226
+ direct: async (text) => { calls.push(text); return "ZCODE_DESKTOP_REPLY"; },
227
+ } });
228
+ await plugin.activate({ config: { cdpPort: 9231 }, logger: { info() { }, warn() { }, error() { } } });
229
+ strict_1.default.deepEqual(await plugin.handle({ task: "请回复固定文本", sessionId: "zcode:test" }), { text: "ZCODE_DESKTOP_REPLY", sessionId: "zcode:test" });
230
+ strict_1.default.deepEqual(calls, ["请回复固定文本"]);
231
+ await plugin.deactivate("stop");
232
+ });
233
+ (0, node_test_1.default)("ZCode 将包含专家称呼的消息作为普通文本发送", async () => {
234
+ const plugin = new zcode_1.ZcodePlugin({ client: {
235
+ connect: async () => undefined, close() { }, connected: () => true,
236
+ direct: async (text, _timeout, expert) => { strict_1.default.equal(expert, undefined); return text; },
237
+ } });
238
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
239
+ try {
240
+ strict_1.default.equal((await plugin.handle({ task: "用数学专家解释这个题" })).text, "用数学专家解释这个题");
241
+ }
242
+ finally {
243
+ await plugin.deactivate("stop");
244
+ }
245
+ });
246
+ (0, node_test_1.default)("ZCode 复用会话层时保留原有等待配置", async () => {
247
+ const timeouts = [];
248
+ const plugin = new zcode_1.ZcodePlugin({ client: {
249
+ connect: async () => undefined, close() { }, connected: () => true,
250
+ direct: async (_text, timeout) => { timeouts.push(timeout); return "回复"; },
251
+ } });
252
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
253
+ try {
254
+ await plugin.handle({ task: "默认" });
255
+ await plugin.handle({ task: "短等待", timeoutMs: 1234 });
256
+ await plugin.handle({ task: "既有长等待", timeoutMs: 7_200_000 });
257
+ strict_1.default.deepEqual(timeouts, [3_600_000, 1234, 7_200_000]);
258
+ }
259
+ finally {
260
+ await plugin.deactivate("stop");
261
+ }
262
+ });
263
+ (0, node_test_1.default)("ZCode 失败后保留原任务绑定,续聊必须恢复原任务", async () => {
264
+ let desktop = "A";
265
+ let fail = false;
266
+ const sent = [];
267
+ const restored = [];
268
+ const plugin = new zcode_1.ZcodePlugin({ client: {
269
+ connect: async () => undefined, close() { }, connected: () => true,
270
+ createTask: async () => { desktop = "A"; },
271
+ conversationId: async () => desktop,
272
+ activateConversation: async (id) => { restored.push(id); desktop = id; },
273
+ direct: async () => { if (fail) {
274
+ desktop = "B";
275
+ throw new Error("桌面任务已切换");
276
+ } sent.push(desktop); return "回复"; },
277
+ } });
278
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
279
+ try {
280
+ await plugin.handle({ task: "首轮", sessionId: "peer-A" });
281
+ fail = true;
282
+ await strict_1.default.rejects(() => plugin.handle({ task: "切换", sessionId: "peer-A" }), /桌面任务已切换/);
283
+ fail = false;
284
+ await plugin.handle({ task: "续聊", sessionId: "peer-A" });
285
+ strict_1.default.deepEqual(restored, ["A"]);
286
+ strict_1.default.deepEqual(sent, ["A", "A"]);
287
+ }
288
+ finally {
289
+ await plugin.deactivate("stop");
290
+ }
291
+ });
292
+ (0, node_test_1.default)("ZCode 会话管理失败使用自身错误码和文案,不冒充 WorkBuddy", async () => {
293
+ let desktop = "session_original";
294
+ const plugin = new zcode_1.ZcodePlugin({ client: {
295
+ connect: async () => undefined, close() { }, connected: () => true,
296
+ createTask: async () => { desktop = "session_original"; },
297
+ conversationId: async () => desktop,
298
+ direct: async () => "回复",
299
+ } });
300
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
301
+ try {
302
+ await plugin.handle({ task: "首轮", sessionId: "peer-A" });
303
+ desktop = "session_other";
304
+ await strict_1.default.rejects(plugin.handle({ task: "续聊", sessionId: "peer-A" }), error => {
305
+ strict_1.default.ok(error instanceof agent_1.AgentOperationError);
306
+ strict_1.default.equal(error.code, "ZCODE_TASK_FAILED");
307
+ strict_1.default.equal(error.stage, "准备或恢复 ZCode 会话");
308
+ strict_1.default.doesNotMatch((0, agent_1.errorDetails)(error), /WorkBuddy|WORKBUDDY/);
309
+ strict_1.default.match((0, agent_1.errorDetails)(error), /ZCode 桌面任务已切换/);
310
+ return true;
311
+ });
312
+ }
313
+ finally {
314
+ await plugin.deactivate("stop");
315
+ }
316
+ });
317
+ (0, node_test_1.default)("ZCode 排队请求取消后不创建或切换桌面任务", async () => {
318
+ let created = 0;
319
+ const plugin = new zcode_1.ZcodePlugin({ client: {
320
+ connect: async () => undefined, close() { }, connected: () => true,
321
+ createTask: async () => { created++; }, direct: async () => "回复",
322
+ } });
323
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
324
+ const controller = new AbortController();
325
+ controller.abort(new Error("已取消"));
326
+ try {
327
+ await strict_1.default.rejects(() => plugin.handle({ task: "已取消请求", sessionId: "cancelled", signal: controller.signal }), /已取消/);
328
+ strict_1.default.equal(created, 0);
329
+ }
330
+ finally {
331
+ await plugin.deactivate("stop");
332
+ }
333
+ });
334
+ (0, node_test_1.default)("ZCode 委托将原生任务绑定传给发送端复核", async () => {
335
+ let desktop;
336
+ const expected = [];
337
+ const plugin = new zcode_1.ZcodePlugin({ client: {
338
+ connect: async () => undefined, close() { }, connected: () => true,
339
+ createTask: async () => { desktop = undefined; }, conversationId: async () => desktop,
340
+ direct: async () => "",
341
+ complete: async (_request, _expert, _newTask, expectedConversationId) => { expected.push(expectedConversationId); desktop = "A"; return { text: "回复" }; },
342
+ } });
343
+ await plugin.activate({ config: {}, logger: { info() { }, warn() { }, error() { } } });
344
+ try {
345
+ await plugin.handle({ task: "首轮", sessionId: "peer-A" });
346
+ await plugin.handle({ task: "续聊", sessionId: "peer-A" });
347
+ strict_1.default.deepEqual(expected, [null, "A"]);
348
+ }
349
+ finally {
350
+ await plugin.deactivate("stop");
351
+ }
352
+ });
353
+ (0, node_test_1.default)("ZCode CDP 协议错误、页面异常、超时和断线会拒绝请求并清理等待", async () => {
354
+ const original = globalThis.WebSocket;
355
+ let socket;
356
+ let reply = "error";
357
+ class TestWebSocket {
358
+ static OPEN = 1;
359
+ readyState = 0;
360
+ onopen;
361
+ onclose;
362
+ onerror;
363
+ onmessage;
364
+ constructor() { socket = this; queueMicrotask(() => { this.readyState = 1; this.onopen?.(); }); }
365
+ send(raw) {
366
+ const { id } = JSON.parse(raw);
367
+ if (reply === "silent")
368
+ return;
369
+ const response = reply === "error" ? { id, error: { code: -32000, message: "协议失败" } } : { id, result: { exceptionDetails: { text: "页面异常" } } };
370
+ queueMicrotask(() => this.onmessage?.({ data: JSON.stringify(response) }));
371
+ }
372
+ close() { this.readyState = 3; this.onclose?.(); }
373
+ }
374
+ globalThis.WebSocket = TestWebSocket;
375
+ const client = new cdp_client_1.ZcodeCdpClient({ timeoutMs: 20, fetchImpl: async () => new Response(JSON.stringify([{ type: "page", title: "ZCode", webSocketDebuggerUrl: "ws://127.0.0.1:9231/devtools/page/1" }])) });
376
+ const internal = client;
377
+ try {
378
+ await client.connect();
379
+ await strict_1.default.rejects(() => internal.cdp("Runtime.evaluate"), /协议失败/);
380
+ reply = "exception";
381
+ await strict_1.default.rejects(() => internal.evaluate("1"), /页面异常/);
382
+ reply = "silent";
383
+ await strict_1.default.rejects(() => internal.cdp("Runtime.evaluate"), /超时/);
384
+ strict_1.default.equal(internal.pending.size, 0);
385
+ const pending = internal.cdp("Runtime.evaluate");
386
+ const rejected = strict_1.default.rejects(pending, /已关闭|已断开/);
387
+ socket.close();
388
+ await rejected;
389
+ strict_1.default.equal(internal.pending.size, 0);
390
+ strict_1.default.equal(client.connected(), false);
391
+ }
392
+ finally {
393
+ client.close();
394
+ globalThis.WebSocket = original;
395
+ }
396
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.71",
3
+ "version": "1.0.78",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": {
@@ -1,7 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AgentOperationError = exports.AgentError = void 0;
4
+ exports.abortable = abortable;
4
5
  exports.errorDetails = errorDetails;
6
+ async function abortable(promise, signal) {
7
+ if (!signal)
8
+ return promise;
9
+ if (signal.aborted) {
10
+ // promise 已在调用方启动,先观察其拒绝,避免预取消时产生未处理拒绝。
11
+ void promise.catch(() => undefined);
12
+ signal.throwIfAborted();
13
+ }
14
+ return new Promise((resolve, reject) => {
15
+ let settled = false;
16
+ const cleanup = () => signal.removeEventListener("abort", abort);
17
+ const abort = () => {
18
+ if (settled)
19
+ return;
20
+ settled = true;
21
+ cleanup();
22
+ reject(signal.reason);
23
+ };
24
+ signal.addEventListener("abort", abort, { once: true });
25
+ promise.then((value) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ cleanup();
30
+ resolve(value);
31
+ }, (error) => {
32
+ if (settled)
33
+ return;
34
+ settled = true;
35
+ cleanup();
36
+ reject(error);
37
+ });
38
+ });
39
+ }
5
40
  class AgentError extends Error {
6
41
  constructor(message, options) {
7
42
  super(message, options);
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.11",
8
+ version: "1.0.15",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "1",
@@ -34,6 +34,7 @@
34
34
  let logHasNext = false;
35
35
  let loading = false;
36
36
  const installingPackages = new Set();
37
+ let assistantOriginal = {};
37
38
  function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
38
39
  function showResult(title, message, failed) {
39
40
  node("#operation-result-title").textContent = title;
@@ -76,7 +77,7 @@
76
77
  head.append(headRow);
77
78
  const body = document.createElement("tbody");
78
79
  for (const plugin of items) {
79
- const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
80
+ const installed = plugins.find((item) => item.id === (plugin.id ?? plugin.name.replace(/^agent-bridge-/, "")));
80
81
  const versionStatus = installed ? pluginVersions[installed.id] : undefined;
81
82
  const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
82
83
  const row = document.createElement("tr");
@@ -218,7 +219,8 @@
218
219
  versionsNode.replaceChildren(...entries.map((entry) => {
219
220
  const row = document.createElement("div");
220
221
  row.className = "version-row";
221
- row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${entry.state}${entry.error ? ` · ${entry.error}` : ""}`;
222
+ const state = entry.state === "unpublished" ? "未发布" : entry.state;
223
+ row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${state}${entry.error ? ` · ${entry.error}` : ""}`;
222
224
  return row;
223
225
  }));
224
226
  if (!entries.length)
@@ -309,7 +311,7 @@
309
311
  status.className = `status status-${plugin.status}`;
310
312
  status.textContent = plugin.status;
311
313
  if (plugin.type === "agent")
312
- type.textContent += ` · ${{ cli: "CLI", cdp: "CDP" }[plugin.manifest?.executionMode ?? ""] ?? "未声明"}`;
314
+ type.textContent += plugin.manifest?.executionMode === "cli" ? " · CLI" : " · 桌面";
313
315
  const dependencies = Object.entries(plugin.manifest?.dependencies ?? {});
314
316
  if (dependencies.length) {
315
317
  const detail = document.createElement("small");
@@ -344,6 +346,8 @@
344
346
  controls.className = "controls";
345
347
  if (plugin.manifest?.executionMode === "cli")
346
348
  controls.append(button("配置", `配置 ${plugin.id}`, () => configureCli(plugin)));
349
+ if (plugin.id === "personal-assistant")
350
+ controls.append(button("配置模型", "配置个人助理模型来源", () => configureAssistant(plugin)));
347
351
  if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
348
352
  controlsUpgrade(plugin.id);
349
353
  controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); }));
@@ -401,6 +405,70 @@
401
405
  detail.hidden = false;
402
406
  }).finally(() => { save.disabled = false; });
403
407
  };
408
+ node("#assistant-config-cancel").onclick = () => node("#assistant-config-dialog").close();
409
+ async function configureAssistant(plugin) {
410
+ const [config, sourceResult] = await Promise.all([
411
+ request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`),
412
+ request("/api/model-providers"),
413
+ ]);
414
+ assistantOriginal = config;
415
+ const select = node("#assistant-model-provider");
416
+ select.replaceChildren();
417
+ const add = (value, label, disabled = false) => { const option = document.createElement("option"); option.value = value; option.textContent = label; option.disabled = disabled; select.append(option); };
418
+ add("openai", "OpenAI 兼容接口");
419
+ if (config.modelProvider === "claude-cli")
420
+ add("claude-cli", "Claude CLI(原有配置,用户默认模型)");
421
+ for (const provider of sourceResult.providers ?? [])
422
+ add(`plugin:${provider.id}`, `${provider.displayName || provider.id}${provider.available ? "" : `(不可用:${provider.reason || provider.status})`}`, !provider.available);
423
+ if (config.modelProvider === "plugin" && config.modelPlugin && !(sourceResult.providers ?? []).some((item) => item.id === config.modelPlugin))
424
+ add(`plugin:${config.modelPlugin}`, `${config.modelPlugin}(未安装或未声明模型来源)`, true);
425
+ select.value = config.modelProvider === "plugin" && config.modelPlugin ? `plugin:${config.modelPlugin}` : config.modelProvider ?? "openai";
426
+ node("#assistant-model").value = config.model ?? "";
427
+ node("#assistant-endpoint").value = config.endpoint ?? "";
428
+ node("#assistant-api-key").value = "";
429
+ node("#assistant-api-key").placeholder = config.apiKey ? "已配置,留空保留" : "可选,本地免认证服务可留空";
430
+ node("#assistant-timeout").value = String(config.timeoutMs ?? 120000);
431
+ node("#assistant-allowed-peers").value = config.allowedPeers?.includes("*") ? "" : (config.allowedPeers ?? []).join("\n");
432
+ node("#assistant-allowed-agents").value = config.allowedAgents?.includes("*") ? "" : (config.allowedAgents ?? []).join("\n");
433
+ node("#assistant-config-error").hidden = true;
434
+ node("#assistant-config-dialog").showModal();
435
+ }
436
+ node("#assistant-config-form").onsubmit = event => {
437
+ event.preventDefault();
438
+ const save = node("#assistant-config-save");
439
+ save.disabled = true;
440
+ const source = node("#assistant-model-provider").value;
441
+ const provider = source.startsWith("plugin:") ? "plugin" : source;
442
+ const modelPlugin = source.startsWith("plugin:") ? source.slice("plugin:".length) : undefined;
443
+ const body = {};
444
+ if (provider !== (assistantOriginal.modelProvider ?? "openai"))
445
+ body.modelProvider = provider;
446
+ if (provider === "plugin" && modelPlugin !== assistantOriginal.modelPlugin)
447
+ body.modelPlugin = modelPlugin;
448
+ const model = node("#assistant-model").value.trim();
449
+ const endpoint = node("#assistant-endpoint").value.trim();
450
+ const apiKey = node("#assistant-api-key").value;
451
+ if (model && model !== String(assistantOriginal.model ?? ""))
452
+ body.model = model;
453
+ if (endpoint && endpoint !== String(assistantOriginal.endpoint ?? ""))
454
+ body.endpoint = endpoint;
455
+ if (apiKey.trim())
456
+ body.apiKey = apiKey.trim();
457
+ const timeoutMs = Number(node("#assistant-timeout").value);
458
+ if (timeoutMs !== Number(assistantOriginal.timeoutMs ?? 120000))
459
+ body.timeoutMs = timeoutMs;
460
+ for (const [field, selector] of [["allowedPeers", "#assistant-allowed-peers"], ["allowedAgents", "#assistant-allowed-agents"]]) {
461
+ const entries = [...new Set(node(selector).value.split(/[\s,,]+/).filter(Boolean))];
462
+ const values = entries.includes("*") ? [] : entries;
463
+ if (JSON.stringify(values) !== JSON.stringify(assistantOriginal[field]))
464
+ body[field] = values;
465
+ }
466
+ void request("/api/plugins/personal-assistant/config", "PATCH", body).then(result => {
467
+ node("#assistant-config-dialog").close();
468
+ showResult("个人助理配置已保存", result.restartRequired || result.action === "restart" ? "配置已保存,需要重启后台后生效。" : "模型来源配置已更新。", false);
469
+ return load(false);
470
+ }).catch(cause => { const detail = node("#assistant-config-error"); detail.textContent = String(cause); detail.hidden = false; }).finally(() => { save.disabled = false; });
471
+ };
404
472
  function renderMessages(messages) {
405
473
  messagesNode.replaceChildren();
406
474
  messagesEmpty.hidden = messages.length > 0;
@@ -453,8 +521,8 @@
453
521
  node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
454
522
  node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
455
523
  node("#save-plugin-trust").onclick = () => { const button = node("#save-plugin-trust"); button.disabled = true; trustStatusNode.textContent = "正在保存…"; void request("/api/plugin-trust", "PATCH", pluginTrustConfig).then(() => { pluginTrustDirty = false; trustStatusNode.textContent = "信任设置已保存"; return load(false); }).catch((cause) => { trustStatusNode.textContent = String(cause); }).finally(() => { button.disabled = false; }); };
456
- node("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?"))
457
- void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
524
+ node("#update-core").onclick = () => { if (confirm("确认检查后台更新?有更高版本时将升级并自动重启"))
525
+ void request("/api/update", "POST", {}).then(() => { error("后台正在检查更新,有更高版本时升级并重启;结果请查看运行日志"); }).catch((cause) => error(String(cause))); };
458
526
  node("#pair-button").onclick = () => {
459
527
  const code = pairCodeNode.value.trim();
460
528
  if (!/^\d{6}$/.test(code))