tabwright 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.
- package/LICENSE +21 -0
- package/bin.js +4 -0
- package/dist/a11y-client.js +204 -0
- package/dist/agent-skills/tabwright/SKILL.md +119 -0
- package/dist/aria-snapshot.d.ts +262 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +1256 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/aria-snapshot.test.d.ts +2 -0
- package/dist/aria-snapshot.test.d.ts.map +1 -0
- package/dist/aria-snapshot.test.js +270 -0
- package/dist/aria-snapshot.test.js.map +1 -0
- package/dist/aria-snapshot.unit.test.d.ts +2 -0
- package/dist/aria-snapshot.unit.test.d.ts.map +1 -0
- package/dist/aria-snapshot.unit.test.js +542 -0
- package/dist/aria-snapshot.unit.test.js.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +894 -0
- package/dist/browser-config.d.ts +13 -0
- package/dist/browser-config.d.ts.map +1 -0
- package/dist/browser-config.js +126 -0
- package/dist/browser-config.js.map +1 -0
- package/dist/browser-install.d.ts +16 -0
- package/dist/browser-install.d.ts.map +1 -0
- package/dist/browser-install.js +238 -0
- package/dist/browser-install.js.map +1 -0
- package/dist/browser-launch.d.ts +17 -0
- package/dist/browser-launch.d.ts.map +1 -0
- package/dist/browser-launch.js +44 -0
- package/dist/browser-launch.js.map +1 -0
- package/dist/builtin-capabilities.d.ts +28 -0
- package/dist/builtin-capabilities.d.ts.map +1 -0
- package/dist/builtin-capabilities.js +2673 -0
- package/dist/builtin-capabilities.js.map +1 -0
- package/dist/capability-agent-skill.d.ts +60 -0
- package/dist/capability-agent-skill.d.ts.map +1 -0
- package/dist/capability-agent-skill.js +225 -0
- package/dist/capability-agent-skill.js.map +1 -0
- package/dist/capability-auth-state.d.ts +34 -0
- package/dist/capability-auth-state.d.ts.map +1 -0
- package/dist/capability-auth-state.js +157 -0
- package/dist/capability-auth-state.js.map +1 -0
- package/dist/capability-auth.d.ts +36 -0
- package/dist/capability-auth.d.ts.map +1 -0
- package/dist/capability-auth.js +198 -0
- package/dist/capability-auth.js.map +1 -0
- package/dist/capability-cli-confirmation.test.d.ts +2 -0
- package/dist/capability-cli-confirmation.test.d.ts.map +1 -0
- package/dist/capability-cli-confirmation.test.js +79 -0
- package/dist/capability-cli-confirmation.test.js.map +1 -0
- package/dist/capability-options.d.ts +20 -0
- package/dist/capability-options.d.ts.map +1 -0
- package/dist/capability-options.js +31 -0
- package/dist/capability-options.js.map +1 -0
- package/dist/capability-options.test.d.ts +2 -0
- package/dist/capability-options.test.d.ts.map +1 -0
- package/dist/capability-options.test.js +81 -0
- package/dist/capability-options.test.js.map +1 -0
- package/dist/capability-package.d.ts +27 -0
- package/dist/capability-package.d.ts.map +1 -0
- package/dist/capability-package.js +444 -0
- package/dist/capability-package.js.map +1 -0
- package/dist/capability-package.test.d.ts +2 -0
- package/dist/capability-package.test.d.ts.map +1 -0
- package/dist/capability-package.test.js +179 -0
- package/dist/capability-package.test.js.map +1 -0
- package/dist/capability-registry.d.ts +262 -0
- package/dist/capability-registry.d.ts.map +1 -0
- package/dist/capability-registry.js +931 -0
- package/dist/capability-registry.js.map +1 -0
- package/dist/capability-registry.test.d.ts +2 -0
- package/dist/capability-registry.test.d.ts.map +1 -0
- package/dist/capability-registry.test.js +1683 -0
- package/dist/capability-registry.test.js.map +1 -0
- package/dist/capability-runner.d.ts +99 -0
- package/dist/capability-runner.d.ts.map +1 -0
- package/dist/capability-runner.js +608 -0
- package/dist/capability-runner.js.map +1 -0
- package/dist/capability-search-relevance.test.d.ts +2 -0
- package/dist/capability-search-relevance.test.d.ts.map +1 -0
- package/dist/capability-search-relevance.test.js +67 -0
- package/dist/capability-search-relevance.test.js.map +1 -0
- package/dist/capability-studio.d.ts +11 -0
- package/dist/capability-studio.d.ts.map +1 -0
- package/dist/capability-studio.js +593 -0
- package/dist/capability-studio.js.map +1 -0
- package/dist/capability-studio.test.d.ts +2 -0
- package/dist/capability-studio.test.d.ts.map +1 -0
- package/dist/capability-studio.test.js +41 -0
- package/dist/capability-studio.test.js.map +1 -0
- package/dist/cdp-log.d.ts +19 -0
- package/dist/cdp-log.d.ts.map +1 -0
- package/dist/cdp-log.js +83 -0
- package/dist/cdp-log.js.map +1 -0
- package/dist/cdp-log.test.d.ts +2 -0
- package/dist/cdp-log.test.d.ts.map +1 -0
- package/dist/cdp-log.test.js +109 -0
- package/dist/cdp-log.test.js.map +1 -0
- package/dist/cdp-relay.d.ts +19 -0
- package/dist/cdp-relay.d.ts.map +1 -0
- package/dist/cdp-relay.js +2375 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/cdp-session.d.ts +37 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +36 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +65 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/channel-owner-inspect.test.d.ts +2 -0
- package/dist/channel-owner-inspect.test.d.ts.map +1 -0
- package/dist/channel-owner-inspect.test.js +75 -0
- package/dist/channel-owner-inspect.test.js.map +1 -0
- package/dist/chrome-discovery.d.ts +65 -0
- package/dist/chrome-discovery.d.ts.map +1 -0
- package/dist/chrome-discovery.js +173 -0
- package/dist/chrome-discovery.js.map +1 -0
- package/dist/chrome-discovery.test.d.ts +2 -0
- package/dist/chrome-discovery.test.d.ts.map +1 -0
- package/dist/chrome-discovery.test.js +41 -0
- package/dist/chrome-discovery.test.js.map +1 -0
- package/dist/clean-html.d.ts +11 -0
- package/dist/clean-html.d.ts.map +1 -0
- package/dist/clean-html.js +106 -0
- package/dist/clean-html.js.map +1 -0
- package/dist/cli-help.test.d.ts +2 -0
- package/dist/cli-help.test.d.ts.map +1 -0
- package/dist/cli-help.test.js +98 -0
- package/dist/cli-help.test.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2709 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud-client.d.ts +56 -0
- package/dist/cloud-client.d.ts.map +1 -0
- package/dist/cloud-client.js +127 -0
- package/dist/cloud-client.js.map +1 -0
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +27 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/debugger-api.md +458 -0
- package/dist/debugger-examples-types.d.ts +24 -0
- package/dist/debugger-examples-types.d.ts.map +1 -0
- package/dist/debugger-examples-types.js +2 -0
- package/dist/debugger-examples-types.js.map +1 -0
- package/dist/debugger-examples.d.ts +6 -0
- package/dist/debugger-examples.d.ts.map +1 -0
- package/dist/debugger-examples.js +53 -0
- package/dist/debugger-examples.js.map +1 -0
- package/dist/debugger.d.ts +381 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +630 -0
- package/dist/debugger.js.map +1 -0
- package/dist/diff-utils.d.ts +20 -0
- package/dist/diff-utils.d.ts.map +1 -0
- package/dist/diff-utils.js +50 -0
- package/dist/diff-utils.js.map +1 -0
- package/dist/diff-utils.test.d.ts +2 -0
- package/dist/diff-utils.test.d.ts.map +1 -0
- package/dist/diff-utils.test.js +137 -0
- package/dist/diff-utils.test.js.map +1 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +246 -0
- package/dist/doctor.js.map +1 -0
- package/dist/doctor.test.d.ts +2 -0
- package/dist/doctor.test.d.ts.map +1 -0
- package/dist/doctor.test.js +208 -0
- package/dist/doctor.test.js.map +1 -0
- package/dist/editor-api.md +374 -0
- package/dist/editor-examples.d.ts +11 -0
- package/dist/editor-examples.d.ts.map +1 -0
- package/dist/editor-examples.js +124 -0
- package/dist/editor-examples.js.map +1 -0
- package/dist/editor.d.ts +203 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +363 -0
- package/dist/editor.js.map +1 -0
- package/dist/env-compat.d.ts +2 -0
- package/dist/env-compat.d.ts.map +1 -0
- package/dist/env-compat.js +27 -0
- package/dist/env-compat.js.map +1 -0
- package/dist/executor.d.ts +229 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +1465 -0
- package/dist/executor.js.map +1 -0
- package/dist/executor.unit.test.d.ts +2 -0
- package/dist/executor.unit.test.d.ts.map +1 -0
- package/dist/executor.unit.test.js +143 -0
- package/dist/executor.unit.test.js.map +1 -0
- package/dist/extension/_locales/en/messages.json +278 -0
- package/dist/extension/_locales/zh_CN/messages.json +256 -0
- package/dist/extension/assets/options-CYH96Lcx.css +1349 -0
- package/dist/extension/background.js +3994 -0
- package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
- package/dist/extension/icons/icon-black-128.png +0 -0
- package/dist/extension/icons/icon-black-16.png +0 -0
- package/dist/extension/icons/icon-black-32.png +0 -0
- package/dist/extension/icons/icon-black-48.png +0 -0
- package/dist/extension/icons/icon-gray-128.png +0 -0
- package/dist/extension/icons/icon-gray-16.png +0 -0
- package/dist/extension/icons/icon-gray-32.png +0 -0
- package/dist/extension/icons/icon-gray-48.png +0 -0
- package/dist/extension/icons/icon-green-128.png +0 -0
- package/dist/extension/icons/icon-green-16.png +0 -0
- package/dist/extension/icons/icon-green-32.png +0 -0
- package/dist/extension/icons/icon-green-48.png +0 -0
- package/dist/extension/icons/tabwright-icon-black.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.png +0 -0
- package/dist/extension/icons/tabwright-icon-gray-disabled.svg +1 -0
- package/dist/extension/manifest.json +54 -0
- package/dist/extension/options.js +16145 -0
- package/dist/extension/rrweb-recorder.js +12952 -0
- package/dist/extension/src/options.html +165 -0
- package/dist/extension/src/prism-bash.min.js +1 -0
- package/dist/extension/src/prism.min.js +1 -0
- package/dist/extension/src/welcome.html +396 -0
- package/dist/extension-connection.test.d.ts +2 -0
- package/dist/extension-connection.test.d.ts.map +1 -0
- package/dist/extension-connection.test.js +708 -0
- package/dist/extension-connection.test.js.map +1 -0
- package/dist/extension-icon-warning.test.d.ts +2 -0
- package/dist/extension-icon-warning.test.d.ts.map +1 -0
- package/dist/extension-icon-warning.test.js +33 -0
- package/dist/extension-icon-warning.test.js.map +1 -0
- package/dist/ghost-browser.d.ts +62 -0
- package/dist/ghost-browser.d.ts.map +1 -0
- package/dist/ghost-browser.js +107 -0
- package/dist/ghost-browser.js.map +1 -0
- package/dist/ghost-cursor-client.js +374 -0
- package/dist/ghost-cursor-controller.d.ts +46 -0
- package/dist/ghost-cursor-controller.d.ts.map +1 -0
- package/dist/ghost-cursor-controller.js +98 -0
- package/dist/ghost-cursor-controller.js.map +1 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +79 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts +8 -0
- package/dist/htmlrewrite.d.ts.map +1 -0
- package/dist/htmlrewrite.js +339 -0
- package/dist/htmlrewrite.js.map +1 -0
- package/dist/htmlrewrite.test.d.ts +2 -0
- package/dist/htmlrewrite.test.d.ts.map +1 -0
- package/dist/htmlrewrite.test.js +7975 -0
- package/dist/htmlrewrite.test.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/kill-port.d.ts +29 -0
- package/dist/kill-port.d.ts.map +1 -0
- package/dist/kill-port.js +190 -0
- package/dist/kill-port.js.map +1 -0
- package/dist/kitty-graphics.d.ts +22 -0
- package/dist/kitty-graphics.d.ts.map +1 -0
- package/dist/kitty-graphics.js +71 -0
- package/dist/kitty-graphics.js.map +1 -0
- package/dist/kitty-graphics.test.d.ts +2 -0
- package/dist/kitty-graphics.test.d.ts.map +1 -0
- package/dist/kitty-graphics.test.js +125 -0
- package/dist/kitty-graphics.test.js.map +1 -0
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +98 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.d.ts +20 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +56 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp.d.ts +6 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +511 -0
- package/dist/mcp.js.map +1 -0
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +180 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/options-page.test.d.ts +2 -0
- package/dist/options-page.test.d.ts.map +1 -0
- package/dist/options-page.test.js +773 -0
- package/dist/options-page.test.js.map +1 -0
- package/dist/package-paths.d.ts +4 -0
- package/dist/package-paths.d.ts.map +1 -0
- package/dist/package-paths.js +30 -0
- package/dist/package-paths.js.map +1 -0
- package/dist/page-markdown.d.ts +40 -0
- package/dist/page-markdown.d.ts.map +1 -0
- package/dist/page-markdown.js +181 -0
- package/dist/page-markdown.js.map +1 -0
- package/dist/performance-examples.d.ts +5 -0
- package/dist/performance-examples.d.ts.map +1 -0
- package/dist/performance-examples.js +112 -0
- package/dist/performance-examples.js.map +1 -0
- package/dist/performance-profiling.md +417 -0
- package/dist/playwright-import.d.ts +19 -0
- package/dist/playwright-import.d.ts.map +1 -0
- package/dist/playwright-import.js +40 -0
- package/dist/playwright-import.js.map +1 -0
- package/dist/popup-relocation.test.d.ts +7 -0
- package/dist/popup-relocation.test.d.ts.map +1 -0
- package/dist/popup-relocation.test.js +139 -0
- package/dist/popup-relocation.test.js.map +1 -0
- package/dist/product-paths.d.ts +9 -0
- package/dist/product-paths.d.ts.map +1 -0
- package/dist/product-paths.js +24 -0
- package/dist/product-paths.js.map +1 -0
- package/dist/product-paths.test.d.ts +2 -0
- package/dist/product-paths.test.d.ts.map +1 -0
- package/dist/product-paths.test.js +39 -0
- package/dist/product-paths.test.js.map +1 -0
- package/dist/prompt.md +1004 -0
- package/dist/protocol.d.ts +248 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +59 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocol.test.d.ts +2 -0
- package/dist/protocol.test.d.ts.map +1 -0
- package/dist/protocol.test.js +31 -0
- package/dist/protocol.test.js.map +1 -0
- package/dist/react-source.d.ts +57 -0
- package/dist/react-source.d.ts.map +1 -0
- package/dist/react-source.js +254 -0
- package/dist/react-source.js.map +1 -0
- package/dist/readability.js +1674 -0
- package/dist/relay-client.d.ts +104 -0
- package/dist/relay-client.d.ts.map +1 -0
- package/dist/relay-client.js +426 -0
- package/dist/relay-client.js.map +1 -0
- package/dist/relay-client.test.d.ts +2 -0
- package/dist/relay-client.test.d.ts.map +1 -0
- package/dist/relay-client.test.js +305 -0
- package/dist/relay-client.test.js.map +1 -0
- package/dist/relay-core.test.d.ts +2 -0
- package/dist/relay-core.test.d.ts.map +1 -0
- package/dist/relay-core.test.js +1427 -0
- package/dist/relay-core.test.js.map +1 -0
- package/dist/relay-navigation.test.d.ts +2 -0
- package/dist/relay-navigation.test.d.ts.map +1 -0
- package/dist/relay-navigation.test.js +661 -0
- package/dist/relay-navigation.test.js.map +1 -0
- package/dist/relay-session.test.d.ts +2 -0
- package/dist/relay-session.test.d.ts.map +1 -0
- package/dist/relay-session.test.js +1121 -0
- package/dist/relay-session.test.js.map +1 -0
- package/dist/relay-state.d.ts +178 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +346 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +594 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/replay-ai-index.d.ts +99 -0
- package/dist/replay-ai-index.d.ts.map +1 -0
- package/dist/replay-ai-index.js +677 -0
- package/dist/replay-ai-index.js.map +1 -0
- package/dist/replay-ai-index.test.d.ts +2 -0
- package/dist/replay-ai-index.test.d.ts.map +1 -0
- package/dist/replay-ai-index.test.js +255 -0
- package/dist/replay-ai-index.test.js.map +1 -0
- package/dist/replay-cli.test.d.ts +2 -0
- package/dist/replay-cli.test.d.ts.map +1 -0
- package/dist/replay-cli.test.js +275 -0
- package/dist/replay-cli.test.js.map +1 -0
- package/dist/replay-eval.d.ts +44 -0
- package/dist/replay-eval.d.ts.map +1 -0
- package/dist/replay-eval.js +902 -0
- package/dist/replay-eval.js.map +1 -0
- package/dist/replay-handoff.d.ts +29 -0
- package/dist/replay-handoff.d.ts.map +1 -0
- package/dist/replay-handoff.js +83 -0
- package/dist/replay-handoff.js.map +1 -0
- package/dist/replay-handoff.test.d.ts +2 -0
- package/dist/replay-handoff.test.d.ts.map +1 -0
- package/dist/replay-handoff.test.js +123 -0
- package/dist/replay-handoff.test.js.map +1 -0
- package/dist/replay-workflow-compiler.d.ts +44 -0
- package/dist/replay-workflow-compiler.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.js +601 -0
- package/dist/replay-workflow-compiler.js.map +1 -0
- package/dist/replay-workflow-compiler.test.d.ts +2 -0
- package/dist/replay-workflow-compiler.test.d.ts.map +1 -0
- package/dist/replay-workflow-compiler.test.js +223 -0
- package/dist/replay-workflow-compiler.test.js.map +1 -0
- package/dist/rrweb-recording-relay.d.ts +56 -0
- package/dist/rrweb-recording-relay.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.js +299 -0
- package/dist/rrweb-recording-relay.js.map +1 -0
- package/dist/rrweb-recording-relay.test.d.ts +2 -0
- package/dist/rrweb-recording-relay.test.d.ts.map +1 -0
- package/dist/rrweb-recording-relay.test.js +85 -0
- package/dist/rrweb-recording-relay.test.js.map +1 -0
- package/dist/rrweb-recording.d.ts +104 -0
- package/dist/rrweb-recording.d.ts.map +1 -0
- package/dist/rrweb-recording.js +180 -0
- package/dist/rrweb-recording.js.map +1 -0
- package/dist/scoped-fs.d.ts +95 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +358 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/scoped-fs.test.d.ts +5 -0
- package/dist/scoped-fs.test.d.ts.map +1 -0
- package/dist/scoped-fs.test.js +73 -0
- package/dist/scoped-fs.test.js.map +1 -0
- package/dist/selector-generator.js +7378 -0
- package/dist/skill-stub.test.d.ts +2 -0
- package/dist/skill-stub.test.d.ts.map +1 -0
- package/dist/skill-stub.test.js +22 -0
- package/dist/skill-stub.test.js.map +1 -0
- package/dist/snapshot-tools.test.d.ts +2 -0
- package/dist/snapshot-tools.test.d.ts.map +1 -0
- package/dist/snapshot-tools.test.js +856 -0
- package/dist/snapshot-tools.test.js.map +1 -0
- package/dist/start-relay-server.d.ts +6 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +57 -0
- package/dist/start-relay-server.js.map +1 -0
- package/dist/styles-api.md +124 -0
- package/dist/styles-examples.d.ts +8 -0
- package/dist/styles-examples.d.ts.map +1 -0
- package/dist/styles-examples.js +64 -0
- package/dist/styles-examples.js.map +1 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +231 -0
- package/dist/styles.js.map +1 -0
- package/dist/tabwright-agent-skill.d.ts +30 -0
- package/dist/tabwright-agent-skill.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.js +74 -0
- package/dist/tabwright-agent-skill.js.map +1 -0
- package/dist/tabwright-agent-skill.test.d.ts +2 -0
- package/dist/tabwright-agent-skill.test.d.ts.map +1 -0
- package/dist/tabwright-agent-skill.test.js +51 -0
- package/dist/tabwright-agent-skill.test.js.map +1 -0
- package/dist/test-declarations.d.ts +13 -0
- package/dist/test-declarations.d.ts.map +1 -0
- package/dist/test-declarations.js +2 -0
- package/dist/test-declarations.js.map +1 -0
- package/dist/test-utils.d.ts +69 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +448 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/test-utils.test.d.ts +2 -0
- package/dist/test-utils.test.d.ts.map +1 -0
- package/dist/test-utils.test.js +55 -0
- package/dist/test-utils.test.js.map +1 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +60 -0
- package/dist/utils.js.map +1 -0
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +146 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/dist/workflow-capability.d.ts +72 -0
- package/dist/workflow-capability.d.ts.map +1 -0
- package/dist/workflow-capability.js +316 -0
- package/dist/workflow-capability.js.map +1 -0
- package/dist/workflow-script-flow.test.d.ts +2 -0
- package/dist/workflow-script-flow.test.d.ts.map +1 -0
- package/dist/workflow-script-flow.test.js +285 -0
- package/dist/workflow-script-flow.test.js.map +1 -0
- package/package.json +80 -0
- package/src/__snapshots__/x.com.processed.html +1003 -0
- package/src/__snapshots__/x.com.processed.withStyles.html +2720 -0
- package/src/a11y-client.ts +241 -0
- package/src/agent-skills/conan-config/SKILL.md +141 -0
- package/src/agent-skills/conan-config/agents/openai.yaml +4 -0
- package/src/aria-snapshot.test.ts +311 -0
- package/src/aria-snapshot.ts +1681 -0
- package/src/aria-snapshot.unit.test.ts +615 -0
- package/src/aria-snapshots/github-interactive.txt +193 -0
- package/src/aria-snapshots/github-raw.txt +309 -0
- package/src/aria-snapshots/hackernews-interactive.txt +477 -0
- package/src/aria-snapshots/hackernews-raw.txt +428 -0
- package/src/aria-snapshots/prosemirror-interactive.txt +66 -0
- package/src/aria-snapshots/prosemirror-raw.txt +145 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/assets/x.com.html +32946 -0
- package/src/browser-config.ts +179 -0
- package/src/browser-install.ts +284 -0
- package/src/browser-launch.ts +75 -0
- package/src/builtin-capabilities.ts +2765 -0
- package/src/capability-agent-skill.ts +305 -0
- package/src/capability-auth-state.ts +197 -0
- package/src/capability-auth.ts +249 -0
- package/src/capability-cli-confirmation.test.ts +90 -0
- package/src/capability-options.test.ts +89 -0
- package/src/capability-options.ts +57 -0
- package/src/capability-package.test.ts +204 -0
- package/src/capability-package.ts +555 -0
- package/src/capability-registry.test.ts +1905 -0
- package/src/capability-registry.ts +1234 -0
- package/src/capability-runner.ts +801 -0
- package/src/capability-search-relevance.test.ts +73 -0
- package/src/capability-studio.test.ts +45 -0
- package/src/capability-studio.ts +641 -0
- package/src/cdp-log.test.ts +131 -0
- package/src/cdp-log.ts +112 -0
- package/src/cdp-relay.ts +2944 -0
- package/src/cdp-session.ts +77 -0
- package/src/cdp-types.ts +158 -0
- package/src/channel-owner-inspect.test.ts +94 -0
- package/src/chrome-discovery.test.ts +50 -0
- package/src/chrome-discovery.ts +220 -0
- package/src/clean-html.ts +141 -0
- package/src/cli-help.test.ts +123 -0
- package/src/cli.ts +3181 -0
- package/src/cloud-client.ts +179 -0
- package/src/create-logger.ts +38 -0
- package/src/debugger-examples-types.ts +16 -0
- package/src/debugger-examples.ts +66 -0
- package/src/debugger.ts +708 -0
- package/src/diff-utils.test.ts +152 -0
- package/src/diff-utils.ts +70 -0
- package/src/doctor.test.ts +244 -0
- package/src/doctor.ts +321 -0
- package/src/editor-examples.ts +158 -0
- package/src/editor.ts +426 -0
- package/src/env-compat.ts +28 -0
- package/src/executor.ts +1824 -0
- package/src/executor.unit.test.ts +168 -0
- package/src/extension-connection.test.ts +855 -0
- package/src/extension-icon-warning.test.ts +45 -0
- package/src/ghost-browser.ts +149 -0
- package/src/ghost-cursor-client.ts +494 -0
- package/src/ghost-cursor-controller.ts +129 -0
- package/src/ghost-cursor.ts +123 -0
- package/src/htmlrewrite.test.ts +8008 -0
- package/src/htmlrewrite.ts +390 -0
- package/src/index.ts +53 -0
- package/src/kill-port.ts +219 -0
- package/src/kitty-graphics.test.ts +139 -0
- package/src/kitty-graphics.ts +79 -0
- package/src/locator-selector.test.ts +119 -0
- package/src/mcp-client.ts +78 -0
- package/src/mcp.ts +629 -0
- package/src/on-mouse-action.test.ts +226 -0
- package/src/options-page.test.ts +839 -0
- package/src/package-paths.ts +38 -0
- package/src/page-markdown.ts +240 -0
- package/src/performance-examples.ts +186 -0
- package/src/playwright-import.ts +59 -0
- package/src/popup-relocation.test.ts +163 -0
- package/src/product-paths.test.ts +47 -0
- package/src/product-paths.ts +28 -0
- package/src/protocol.test.ts +52 -0
- package/src/protocol.ts +351 -0
- package/src/react-source.ts +379 -0
- package/src/relay-client.test.ts +375 -0
- package/src/relay-client.ts +572 -0
- package/src/relay-core.test.ts +1603 -0
- package/src/relay-navigation.test.ts +790 -0
- package/src/relay-session.test.ts +1375 -0
- package/src/relay-state.test.ts +729 -0
- package/src/relay-state.ts +561 -0
- package/src/replay-ai-index.test.ts +278 -0
- package/src/replay-ai-index.ts +872 -0
- package/src/replay-cli.test.ts +325 -0
- package/src/replay-eval.ts +1039 -0
- package/src/replay-handoff.test.ts +163 -0
- package/src/replay-handoff.ts +109 -0
- package/src/replay-workflow-compiler.test.ts +250 -0
- package/src/replay-workflow-compiler.ts +700 -0
- package/src/resource.md +409 -0
- package/src/rrweb-recording-relay.test.ts +99 -0
- package/src/rrweb-recording-relay.ts +400 -0
- package/src/rrweb-recording.ts +322 -0
- package/src/scoped-fs.test.ts +80 -0
- package/src/scoped-fs.ts +420 -0
- package/src/skill-stub.test.ts +24 -0
- package/src/skill.md +1413 -0
- package/src/snapshot-tools.test.ts +1017 -0
- package/src/snapshots/hacker-news-accessibility-full.md +81 -0
- package/src/snapshots/hacker-news-accessibility-interactive.md +43 -0
- package/src/snapshots/page-markdown-output.txt +16 -0
- package/src/snapshots/shadcn-ui-accessibility-full.md +201 -0
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +109 -0
- package/src/start-relay-server.ts +69 -0
- package/src/styles-examples.ts +84 -0
- package/src/styles.ts +343 -0
- package/src/tabwright-agent-skill.test.ts +61 -0
- package/src/tabwright-agent-skill.ts +105 -0
- package/src/test-declarations.ts +13 -0
- package/src/test-utils.test.ts +70 -0
- package/src/test-utils.ts +566 -0
- package/src/utils.ts +78 -0
- package/src/wait-for-page-load.ts +198 -0
- package/src/workflow-capability.ts +417 -0
- package/src/workflow-script-flow.test.ts +388 -0
|
@@ -0,0 +1,2765 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as nodeUrl from 'node:url'
|
|
5
|
+
import {
|
|
6
|
+
createCapability,
|
|
7
|
+
getCapabilityDir,
|
|
8
|
+
readCapabilityScript,
|
|
9
|
+
updateCapabilityManifest,
|
|
10
|
+
updateCapabilityScript,
|
|
11
|
+
type CapabilityLocation,
|
|
12
|
+
type CapabilityRecord,
|
|
13
|
+
} from './capability-registry.js'
|
|
14
|
+
import { removeCapabilityAuthFiles } from './capability-auth-state.js'
|
|
15
|
+
|
|
16
|
+
export interface InstallBuiltinCapabilitySuiteOptions {
|
|
17
|
+
suite: string
|
|
18
|
+
cwd?: string
|
|
19
|
+
location?: CapabilityLocation
|
|
20
|
+
overwrite?: boolean
|
|
21
|
+
trust?: boolean
|
|
22
|
+
installAgentSkills?: boolean
|
|
23
|
+
codexHome?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InstalledBuiltinCapabilitySuite {
|
|
27
|
+
suite: string
|
|
28
|
+
capabilities: CapabilityRecord[]
|
|
29
|
+
agentSkills: InstalledBuiltinAgentSkill[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isBuiltinCapabilitySuite(suite: string): suite is 'conan-config' {
|
|
33
|
+
return suite === 'conan-config'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BuiltinCapabilityDefinition {
|
|
37
|
+
id: string
|
|
38
|
+
title: string
|
|
39
|
+
description: string
|
|
40
|
+
script: string
|
|
41
|
+
manifest: Parameters<typeof updateCapabilityManifest>[0]['patch']
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BuiltinAgentSkillFileDefinition {
|
|
45
|
+
relativePath: string
|
|
46
|
+
bundledPath: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BuiltinAgentSkillDefinition {
|
|
50
|
+
target: 'codex'
|
|
51
|
+
name: string
|
|
52
|
+
files: BuiltinAgentSkillFileDefinition[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface InstalledBuiltinAgentSkillFile {
|
|
56
|
+
path: string
|
|
57
|
+
status: 'created' | 'updated' | 'unchanged'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InstalledBuiltinAgentSkill {
|
|
61
|
+
target: 'codex'
|
|
62
|
+
name: string
|
|
63
|
+
dir: string
|
|
64
|
+
files: InstalledBuiltinAgentSkillFile[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const conanConfigSearchScript = String.raw`
|
|
68
|
+
const CONFIG_ENVIRONMENTS = {
|
|
69
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
70
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
71
|
+
};
|
|
72
|
+
const DEFAULT_MODES = ["config", "configValue", "grouping"];
|
|
73
|
+
|
|
74
|
+
function resolveEnvironment(rawInput) {
|
|
75
|
+
const nextInput = rawInput || {};
|
|
76
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
77
|
+
if (explicit) {
|
|
78
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) {
|
|
79
|
+
throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
80
|
+
}
|
|
81
|
+
return explicit;
|
|
82
|
+
}
|
|
83
|
+
const urlText = nextInput.url || nextInput.configUrl || "";
|
|
84
|
+
if (!urlText) return "cn-prod";
|
|
85
|
+
const hostname = new URL(urlText).hostname;
|
|
86
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
87
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
88
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const environment = resolveEnvironment(input);
|
|
92
|
+
const CONFIG_API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
93
|
+
|
|
94
|
+
function normalizeInput(rawInput) {
|
|
95
|
+
const nextInput = rawInput || {};
|
|
96
|
+
const query = String(nextInput.query || nextInput.keyword || nextInput.search || "").trim();
|
|
97
|
+
if (!query) {
|
|
98
|
+
throw new Error("Missing search query. Provide input.query, input.keyword, or input.search.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const requestedMode = String(nextInput.mode || "").trim();
|
|
102
|
+
const modes = requestedMode
|
|
103
|
+
? requestedMode
|
|
104
|
+
.split(",")
|
|
105
|
+
.map((item) => item.trim())
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
: DEFAULT_MODES;
|
|
108
|
+
const unsupported = modes.filter((mode) => !DEFAULT_MODES.includes(mode));
|
|
109
|
+
if (unsupported.length) {
|
|
110
|
+
throw new Error("Unsupported search mode(s): " + unsupported.join(", ") + ". Use config, configValue, grouping.");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
query,
|
|
115
|
+
modes,
|
|
116
|
+
limit: Number.isInteger(nextInput.limit) && nextInput.limit > 0 ? nextInput.limit : 100,
|
|
117
|
+
includeRawTree: nextInput.includeRawTree === true,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function requestJson(path) {
|
|
122
|
+
const cookieHeader = secrets.cookieHeader;
|
|
123
|
+
if (!cookieHeader) {
|
|
124
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const response = await fetch(CONFIG_API_ORIGIN + path, {
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "application/json",
|
|
130
|
+
Cookie: cookieHeader,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
|
|
135
|
+
if (response.status === 401 || response.status === 403) {
|
|
136
|
+
throw new Error("Request failed " + response.status + ": login expired. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
137
|
+
}
|
|
138
|
+
if (response.status < 200 || response.status >= 300) {
|
|
139
|
+
throw new Error("Request failed " + response.status + ": " + JSON.stringify(body));
|
|
140
|
+
}
|
|
141
|
+
if (body && body.code !== undefined && body.code !== 0) {
|
|
142
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
143
|
+
}
|
|
144
|
+
return body;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function nodeLabel(node) {
|
|
148
|
+
return node.name || node.desc || node.groupingKey || node.key || "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractResultsFromTree(tree, mode) {
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
function visit(node, ancestors = []) {
|
|
155
|
+
const label = nodeLabel(node);
|
|
156
|
+
const nextAncestors = label ? ancestors.concat(label) : ancestors;
|
|
157
|
+
|
|
158
|
+
if (node.key) {
|
|
159
|
+
const root = ancestors[0] || "";
|
|
160
|
+
const groupingPath = ancestors.slice(1).join(" / ");
|
|
161
|
+
results.push({
|
|
162
|
+
mode,
|
|
163
|
+
id: node.id,
|
|
164
|
+
name: node.name || "",
|
|
165
|
+
desc: node.desc || "",
|
|
166
|
+
key: node.key,
|
|
167
|
+
schemaId: node.schemaId,
|
|
168
|
+
rootGroupingKey: node.rootGroupingKey || node.rootGroupingId || "",
|
|
169
|
+
groupingId: node.groupingId,
|
|
170
|
+
root,
|
|
171
|
+
groupingPath,
|
|
172
|
+
path: nextAncestors.join(" / "),
|
|
173
|
+
createdLdap: node.createdLdap || node.ldap || "",
|
|
174
|
+
updatedLdap: node.updatedLdap || "",
|
|
175
|
+
createdTime: node.createdTime || node.dbctime,
|
|
176
|
+
updatedTime: node.updatedTime || node.dbutime,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const child of node.children || []) {
|
|
181
|
+
visit(child, nextAncestors);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const node of tree || []) {
|
|
186
|
+
visit(node, []);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return results;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function dedupeResults(results) {
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const deduped = [];
|
|
195
|
+
for (const result of results) {
|
|
196
|
+
const key = result.mode + ":" + result.key + ":" + (result.id || "");
|
|
197
|
+
if (seen.has(key)) continue;
|
|
198
|
+
seen.add(key);
|
|
199
|
+
deduped.push(result);
|
|
200
|
+
}
|
|
201
|
+
return deduped;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function markdownTableCell(value) {
|
|
205
|
+
if (value === undefined || value === null) return "";
|
|
206
|
+
return String(value).replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildMarkdown(output) {
|
|
210
|
+
const lines = [
|
|
211
|
+
"# 配置查询:" + output.query,
|
|
212
|
+
"",
|
|
213
|
+
"- 查询接口: /conan-config/api/enhancedConfigs/searchTree",
|
|
214
|
+
"- 查询维度: " + output.modes.join(", "),
|
|
215
|
+
"- 命中配置: " + output.resultCount + " 个",
|
|
216
|
+
"",
|
|
217
|
+
"## 分维度结果",
|
|
218
|
+
"",
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
for (const modeResult of output.resultsByMode) {
|
|
222
|
+
lines.push("### " + modeResult.mode + ": " + modeResult.query);
|
|
223
|
+
lines.push("");
|
|
224
|
+
if (!modeResult.results.length) {
|
|
225
|
+
lines.push("- 无匹配配置项", "");
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
lines.push("| 配置名称 | key | desc | path | schemaId |");
|
|
229
|
+
lines.push("| --- | --- | --- | --- | ---: |");
|
|
230
|
+
for (const result of modeResult.results) {
|
|
231
|
+
lines.push(
|
|
232
|
+
"| " +
|
|
233
|
+
markdownTableCell(result.name) +
|
|
234
|
+
" | " +
|
|
235
|
+
markdownTableCell(result.key) +
|
|
236
|
+
" | " +
|
|
237
|
+
markdownTableCell(result.desc) +
|
|
238
|
+
" | " +
|
|
239
|
+
markdownTableCell(result.path) +
|
|
240
|
+
" | " +
|
|
241
|
+
markdownTableCell(result.schemaId) +
|
|
242
|
+
" |",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
lines.push("");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push("## 全部命中");
|
|
249
|
+
lines.push("");
|
|
250
|
+
if (!output.results.length) {
|
|
251
|
+
lines.push("- 无匹配配置项");
|
|
252
|
+
} else {
|
|
253
|
+
lines.push("| mode | 配置名称 | key | path |");
|
|
254
|
+
lines.push("| --- | --- | --- | --- |");
|
|
255
|
+
for (const result of output.results) {
|
|
256
|
+
lines.push("| " + result.mode + " | " + markdownTableCell(result.name) + " | " + markdownTableCell(result.key) + " | " + markdownTableCell(result.path) + " |");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return lines.join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function searchMode(query, mode) {
|
|
264
|
+
const params = new URLSearchParams({ [mode]: query });
|
|
265
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/searchTree?" + params.toString());
|
|
266
|
+
const tree = body.data?.configBases || [];
|
|
267
|
+
const results = extractResultsFromTree(tree, mode);
|
|
268
|
+
return {
|
|
269
|
+
mode,
|
|
270
|
+
query,
|
|
271
|
+
rootCount: tree.length,
|
|
272
|
+
resultCount: results.length,
|
|
273
|
+
results,
|
|
274
|
+
rawTree: tree,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const normalized = normalizeInput(input);
|
|
279
|
+
const modeResults = [];
|
|
280
|
+
for (const mode of normalized.modes) {
|
|
281
|
+
modeResults.push(await searchMode(normalized.query, mode));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const combined = dedupeResults(modeResults.flatMap((item) => item.results)).slice(0, normalized.limit);
|
|
285
|
+
const output = {
|
|
286
|
+
action: "search",
|
|
287
|
+
environment,
|
|
288
|
+
query: normalized.query,
|
|
289
|
+
modes: normalized.modes,
|
|
290
|
+
suggestions: normalized.modes.map((mode) => ({ mode, text: mode + ": " + normalized.query })),
|
|
291
|
+
resultCount: combined.length,
|
|
292
|
+
results: combined,
|
|
293
|
+
resultsByMode: modeResults.map((item) => ({
|
|
294
|
+
mode: item.mode,
|
|
295
|
+
query: item.query,
|
|
296
|
+
rootCount: item.rootCount,
|
|
297
|
+
resultCount: item.resultCount,
|
|
298
|
+
results: item.results.slice(0, normalized.limit),
|
|
299
|
+
rawTree: normalized.includeRawTree ? item.rawTree : undefined,
|
|
300
|
+
})),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
...output,
|
|
305
|
+
view: {
|
|
306
|
+
markdown: buildMarkdown(output),
|
|
307
|
+
table: combined,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
`
|
|
311
|
+
|
|
312
|
+
const conanConfigQueryScript = String.raw`
|
|
313
|
+
const CONFIG_ENVIRONMENTS = {
|
|
314
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
315
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
function resolveEnvironment(rawInput) {
|
|
319
|
+
const nextInput = rawInput || {};
|
|
320
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
321
|
+
if (explicit) {
|
|
322
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) {
|
|
323
|
+
throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
324
|
+
}
|
|
325
|
+
return explicit;
|
|
326
|
+
}
|
|
327
|
+
const urlText = nextInput.url || nextInput.configUrl || "";
|
|
328
|
+
if (!urlText) return "cn-prod";
|
|
329
|
+
const hostname = new URL(urlText).hostname;
|
|
330
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
331
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
332
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const environment = resolveEnvironment(input);
|
|
336
|
+
const CONFIG_API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
337
|
+
|
|
338
|
+
function parseConfigUrl(urlText) {
|
|
339
|
+
if (!urlText || typeof urlText !== "string") return {};
|
|
340
|
+
try {
|
|
341
|
+
const url = new URL(urlText);
|
|
342
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
343
|
+
const queryIndex = hash.indexOf("?");
|
|
344
|
+
const hashParams = new URLSearchParams(queryIndex >= 0 ? hash.slice(queryIndex + 1) : "");
|
|
345
|
+
return {
|
|
346
|
+
key: hashParams.get("key") || url.searchParams.get("key") || "",
|
|
347
|
+
namespace:
|
|
348
|
+
hashParams.get("rootGroupingKey") ||
|
|
349
|
+
hashParams.get("namespace") ||
|
|
350
|
+
url.searchParams.get("rootGroupingKey") ||
|
|
351
|
+
url.searchParams.get("namespace") ||
|
|
352
|
+
"",
|
|
353
|
+
};
|
|
354
|
+
} catch {
|
|
355
|
+
return {};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeInput(rawInput) {
|
|
360
|
+
const nextInput = rawInput || {};
|
|
361
|
+
const parsedUrl = parseConfigUrl(nextInput.url || nextInput.configUrl || "");
|
|
362
|
+
const namespace =
|
|
363
|
+
nextInput.namespace || nextInput.rootGroupingKey || nextInput.grouping || parsedUrl.namespace || "Space_Pedia";
|
|
364
|
+
const key = nextInput.key || parsedUrl.key || "";
|
|
365
|
+
if (!key) {
|
|
366
|
+
throw new Error("Missing config key. Provide input.key or a config admin URL containing ?key=...");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
namespace,
|
|
371
|
+
key,
|
|
372
|
+
includeRawValue: nextInput.includeRawValue === true,
|
|
373
|
+
saveArtifacts: nextInput.saveArtifacts !== false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseValue(valueText) {
|
|
378
|
+
if (!valueText || valueText === "{}") return {};
|
|
379
|
+
return JSON.parse(valueText);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function orderedSchemaChildren(schema) {
|
|
383
|
+
return Object.values(schema?.properties || {}).sort((left, right) => {
|
|
384
|
+
return (left?.["x-index"] ?? 0) - (right?.["x-index"] ?? 0);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isValueSchemaField(schema) {
|
|
389
|
+
return !!(schema && schema.name && schema.type !== "void");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isArraySchema(schema) {
|
|
393
|
+
return String(schema?.type || "").toLowerCase().startsWith("array");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function schemaFieldLabel(schema, fallback = "") {
|
|
397
|
+
return String(schema?.title || schema?.["x-component-props"]?.title || schema?.name || fallback || "").trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function summarizeSchemaField(definition) {
|
|
401
|
+
return {
|
|
402
|
+
name: definition.name || "",
|
|
403
|
+
label: schemaFieldLabel(definition, definition.name || ""),
|
|
404
|
+
title: definition.title || "",
|
|
405
|
+
type: definition.type || "any",
|
|
406
|
+
component: definition["x-component"] || "",
|
|
407
|
+
required: definition.required === true,
|
|
408
|
+
description: definition.description || "",
|
|
409
|
+
enum: Array.isArray(definition.enum)
|
|
410
|
+
? definition.enum.map((item) => ({ label: item.label, value: item.value }))
|
|
411
|
+
: undefined,
|
|
412
|
+
default: definition.default,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function collectSchemaFields(schema, path = [], acc = []) {
|
|
417
|
+
if (!schema || typeof schema !== "object") return acc;
|
|
418
|
+
|
|
419
|
+
for (const child of orderedSchemaChildren(schema)) {
|
|
420
|
+
if (isValueSchemaField(child)) {
|
|
421
|
+
const fieldPath = path.concat(child.name);
|
|
422
|
+
acc.push([fieldPath.join("."), summarizeSchemaField(child)]);
|
|
423
|
+
|
|
424
|
+
const nestedPath = isArraySchema(child) ? path.concat(child.name + "[]") : fieldPath;
|
|
425
|
+
if (child.items) collectSchemaFields(child.items, nestedPath, acc);
|
|
426
|
+
else collectSchemaFields(child, nestedPath, acc);
|
|
427
|
+
} else {
|
|
428
|
+
collectSchemaFields(child, path, acc);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return acc;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function summarizeSchema(schema) {
|
|
436
|
+
return Object.fromEntries(collectSchemaFields(schema));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function findNamedSchemaProperty(schema, key) {
|
|
440
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
441
|
+
|
|
442
|
+
const children = orderedSchemaChildren(schema);
|
|
443
|
+
const direct = children.find((child) => isValueSchemaField(child) && child.name === key);
|
|
444
|
+
if (direct) return direct;
|
|
445
|
+
|
|
446
|
+
for (const child of children) {
|
|
447
|
+
if (!isValueSchemaField(child)) {
|
|
448
|
+
const nested = findNamedSchemaProperty(child, key);
|
|
449
|
+
if (nested) return nested;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (schema.items) return findNamedSchemaProperty(schema.items, key);
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function schemaDisplayKey(key, schema) {
|
|
458
|
+
const label = schemaFieldLabel(schema, key);
|
|
459
|
+
return label && label !== key ? label + " (" + key + ")" : key;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildLabeledValue(value, schema) {
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
const itemSchema = schema?.items || schema;
|
|
465
|
+
return value.map((item) => buildLabeledValue(item, itemSchema));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!value || typeof value !== "object") return value;
|
|
469
|
+
|
|
470
|
+
return Object.fromEntries(
|
|
471
|
+
Object.entries(value).map(([key, item]) => {
|
|
472
|
+
const childSchema = findNamedSchemaProperty(schema, key);
|
|
473
|
+
return [schemaDisplayKey(key, childSchema), buildLabeledValue(item, childSchema)];
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function summarizeValue(value) {
|
|
479
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
480
|
+
return {
|
|
481
|
+
type: Array.isArray(value) ? "array" : typeof value,
|
|
482
|
+
topLevelKeys: [],
|
|
483
|
+
arrays: [],
|
|
484
|
+
objects: [],
|
|
485
|
+
primitives: [],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const entries = Object.entries(value);
|
|
490
|
+
return {
|
|
491
|
+
type: "object",
|
|
492
|
+
topLevelKeys: entries.map(([key]) => key),
|
|
493
|
+
arrays: entries
|
|
494
|
+
.filter(([, item]) => Array.isArray(item))
|
|
495
|
+
.map(([key, items]) => ({
|
|
496
|
+
key,
|
|
497
|
+
count: items.length,
|
|
498
|
+
sampleKeys:
|
|
499
|
+
items.length > 0 && items[0] && typeof items[0] === "object" && !Array.isArray(items[0])
|
|
500
|
+
? Object.keys(items[0])
|
|
501
|
+
: [],
|
|
502
|
+
})),
|
|
503
|
+
objects: entries
|
|
504
|
+
.filter(([, item]) => item && typeof item === "object" && !Array.isArray(item))
|
|
505
|
+
.map(([key, item]) => ({ key, keys: Object.keys(item) })),
|
|
506
|
+
primitives: entries
|
|
507
|
+
.filter(([, item]) => !item || typeof item !== "object")
|
|
508
|
+
.map(([key, item]) => ({ key, value: item })),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function markdownTableCell(value) {
|
|
513
|
+
if (value === undefined || value === null) return "";
|
|
514
|
+
return String(value).replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function schemaFieldsMarkdown(schemaSummary) {
|
|
518
|
+
const rows = Object.entries(schemaSummary);
|
|
519
|
+
if (!rows.length) return "- 无 schema 字段信息";
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
"| path | schema label | type | required | component |",
|
|
523
|
+
"| --- | --- | --- | --- | --- |",
|
|
524
|
+
...rows.map(([path, definition]) => {
|
|
525
|
+
return "| " + markdownTableCell(path) + " | " + markdownTableCell(definition.label || definition.title || definition.name || "") + " | " + markdownTableCell(definition.type || "") + " | " + (definition.required ? "是" : "否") + " | " + markdownTableCell(definition.component || "") + " |";
|
|
526
|
+
}),
|
|
527
|
+
].join("\n");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function fullValueSectionsMarkdown(labeledValue) {
|
|
531
|
+
if (!labeledValue || typeof labeledValue !== "object" || Array.isArray(labeledValue)) {
|
|
532
|
+
return ["~~~json", JSON.stringify(labeledValue, null, 2), "~~~"].join("\n");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return Object.entries(labeledValue)
|
|
536
|
+
.flatMap(([key, item]) => [
|
|
537
|
+
"### " + key,
|
|
538
|
+
"",
|
|
539
|
+
"~~~json",
|
|
540
|
+
JSON.stringify(item, null, 2),
|
|
541
|
+
"~~~",
|
|
542
|
+
"",
|
|
543
|
+
])
|
|
544
|
+
.join("\n");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildArrayTables(value, schema) {
|
|
548
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
|
|
549
|
+
|
|
550
|
+
return Object.entries(value)
|
|
551
|
+
.filter(([, item]) => Array.isArray(item))
|
|
552
|
+
.map(([key, rows]) => {
|
|
553
|
+
const arraySchema = findNamedSchemaProperty(schema, key);
|
|
554
|
+
return {
|
|
555
|
+
name: schemaDisplayKey(key, arraySchema),
|
|
556
|
+
rows: rows.map((row) => buildLabeledValue(row, arraySchema?.items || arraySchema)),
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildMarkdown(meta, schemaSummary, labeledValue, valueSummary) {
|
|
562
|
+
const summaryLines = [
|
|
563
|
+
"- namespace: " + meta.namespace,
|
|
564
|
+
"- key: " + meta.key,
|
|
565
|
+
"- configId: " + meta.id,
|
|
566
|
+
"- schemaId: " + (meta.schemaId || "无"),
|
|
567
|
+
"- 草稿: " + (meta.hasDraft ? "有" : "无"),
|
|
568
|
+
"- 描述: " + (meta.desc || meta.name || "无"),
|
|
569
|
+
"- 顶层字段: " + (valueSummary.topLevelKeys.join(", ") || "无"),
|
|
570
|
+
...valueSummary.arrays.map((item) => "- " + item.key + ": 数组 " + item.count + " 条;示例字段 " + (item.sampleKeys.join(", ") || "无")),
|
|
571
|
+
...valueSummary.objects.map((item) => "- " + item.key + ": 对象;字段 " + item.keys.join(", ")),
|
|
572
|
+
...valueSummary.primitives.map((item) => "- " + item.key + ": " + item.value),
|
|
573
|
+
"- 完整字段和值见下方 JSON;字段名优先使用 schema label,括号内保留原始 key",
|
|
574
|
+
];
|
|
575
|
+
|
|
576
|
+
return [
|
|
577
|
+
"# " + (meta.desc || meta.name || meta.key),
|
|
578
|
+
"",
|
|
579
|
+
"## 总览",
|
|
580
|
+
"",
|
|
581
|
+
...summaryLines,
|
|
582
|
+
"",
|
|
583
|
+
"## Schema 字段",
|
|
584
|
+
"",
|
|
585
|
+
schemaFieldsMarkdown(schemaSummary),
|
|
586
|
+
"",
|
|
587
|
+
"## 完整配置 JSON(按顶层字段拆分,schema label / 原始 key)",
|
|
588
|
+
"",
|
|
589
|
+
fullValueSectionsMarkdown(labeledValue),
|
|
590
|
+
].join("\n");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function sanitizeArtifactPart(value) {
|
|
594
|
+
const sanitized = String(value || "")
|
|
595
|
+
.trim()
|
|
596
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
597
|
+
.replace(/^-+|-+$/g, "");
|
|
598
|
+
return sanitized || "unknown";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function artifactTimestamp(value) {
|
|
602
|
+
const date = typeof value === "number" ? new Date(value) : new Date();
|
|
603
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function saveConfigArtifacts(output) {
|
|
607
|
+
if (typeof artifacts === "undefined" || !artifacts.writeJson || !artifacts.writeText || !artifacts.path) {
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const baseDir =
|
|
612
|
+
"conan-config/" +
|
|
613
|
+
(output.environment === "cn-prod" ? "" : sanitizeArtifactPart(output.environment) + "/") +
|
|
614
|
+
sanitizeArtifactPart(output.namespace) +
|
|
615
|
+
"/" +
|
|
616
|
+
sanitizeArtifactPart(output.key);
|
|
617
|
+
const stamp = artifactTimestamp(output.updatedTime);
|
|
618
|
+
const artifactPaths = {
|
|
619
|
+
root: artifacts.root,
|
|
620
|
+
latestFullJson: artifacts.path({ filename: baseDir + "/latest.full.json" }),
|
|
621
|
+
latestValueJson: artifacts.path({ filename: baseDir + "/latest.value.json" }),
|
|
622
|
+
latestLabeledValueJson: artifacts.path({ filename: baseDir + "/latest.labeled-value.json" }),
|
|
623
|
+
latestSummaryJson: artifacts.path({ filename: baseDir + "/latest.summary.json" }),
|
|
624
|
+
latestSummaryMarkdown: artifacts.path({ filename: baseDir + "/latest.summary.md" }),
|
|
625
|
+
snapshotFullJson: artifacts.path({ filename: baseDir + "/" + stamp + ".full.json" }),
|
|
626
|
+
};
|
|
627
|
+
const summary = {
|
|
628
|
+
action: output.action,
|
|
629
|
+
environment: output.environment,
|
|
630
|
+
namespace: output.namespace,
|
|
631
|
+
key: output.key,
|
|
632
|
+
configId: output.configId,
|
|
633
|
+
schemaId: output.schemaId,
|
|
634
|
+
metadata: output.metadata,
|
|
635
|
+
updatedTime: output.updatedTime,
|
|
636
|
+
hasDraft: output.hasDraft,
|
|
637
|
+
view: {
|
|
638
|
+
kind: output.view.kind,
|
|
639
|
+
summary: output.view.summary,
|
|
640
|
+
schemaFields: output.view.schemaFields,
|
|
641
|
+
tables: output.view.tables,
|
|
642
|
+
},
|
|
643
|
+
artifacts: artifactPaths,
|
|
644
|
+
};
|
|
645
|
+
const outputWithArtifacts = {
|
|
646
|
+
...output,
|
|
647
|
+
artifacts: artifactPaths,
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
artifacts.writeJson({ filename: baseDir + "/latest.value.json", value: output.value });
|
|
651
|
+
artifacts.writeJson({ filename: baseDir + "/latest.labeled-value.json", value: output.view.labeledValue });
|
|
652
|
+
artifacts.writeJson({ filename: baseDir + "/latest.summary.json", value: summary });
|
|
653
|
+
artifacts.writeText({ filename: baseDir + "/latest.summary.md", text: output.view.markdown + "\n" });
|
|
654
|
+
artifacts.writeJson({ filename: baseDir + "/latest.full.json", value: outputWithArtifacts });
|
|
655
|
+
artifacts.writeJson({ filename: baseDir + "/" + stamp + ".full.json", value: outputWithArtifacts });
|
|
656
|
+
|
|
657
|
+
return artifactPaths;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function requestJson(path, options = {}) {
|
|
661
|
+
const cookieHeader = secrets.cookieHeader;
|
|
662
|
+
if (!cookieHeader) {
|
|
663
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const url = path.startsWith("http") ? path : CONFIG_API_ORIGIN + path;
|
|
667
|
+
const response = await fetch(url, {
|
|
668
|
+
...options,
|
|
669
|
+
headers: {
|
|
670
|
+
"Content-Type": "application/json",
|
|
671
|
+
Cookie: cookieHeader,
|
|
672
|
+
...(options.headers || {}),
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const text = await response.text();
|
|
677
|
+
let body;
|
|
678
|
+
try {
|
|
679
|
+
body = JSON.parse(text);
|
|
680
|
+
} catch {
|
|
681
|
+
body = text;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (response.status === 401 || response.status === 403) {
|
|
685
|
+
throw new Error("Request failed " + response.status + ": login expired. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
686
|
+
}
|
|
687
|
+
if (response.status < 200 || response.status >= 300) {
|
|
688
|
+
throw new Error("Request failed " + response.status + ": " + (typeof body === "string" ? body : JSON.stringify(body)));
|
|
689
|
+
}
|
|
690
|
+
if (body && typeof body === "object" && body.code !== undefined && body.code !== 0) {
|
|
691
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
692
|
+
}
|
|
693
|
+
return body;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function requestJsonOptional(path) {
|
|
697
|
+
try {
|
|
698
|
+
return await requestJson(path);
|
|
699
|
+
} catch {
|
|
700
|
+
return undefined;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function loadConfig(normalized) {
|
|
705
|
+
const namespace = encodeURIComponent(normalized.namespace);
|
|
706
|
+
const key = encodeURIComponent(normalized.key);
|
|
707
|
+
|
|
708
|
+
const newConfig = await requestJson("/conan-config/api/newConfigs/" + namespace + "/" + key);
|
|
709
|
+
const enhancedDetail = await requestJsonOptional("/conan-config/api/enhancedConfigs/detail/" + namespace + "/" + key);
|
|
710
|
+
|
|
711
|
+
const config = enhancedDetail?.data?.config || {};
|
|
712
|
+
const draft = enhancedDetail?.data?.configDraft;
|
|
713
|
+
const schemaId = config.schemaId;
|
|
714
|
+
let schema = {};
|
|
715
|
+
let schemaSummary = {};
|
|
716
|
+
if (schemaId) {
|
|
717
|
+
const schemaRows = await requestJson("/conan-config/api/schema?ids=" + encodeURIComponent(schemaId));
|
|
718
|
+
const schemaRow = Array.isArray(schemaRows) ? schemaRows[0] : undefined;
|
|
719
|
+
if (schemaRow?.schemaJson) {
|
|
720
|
+
schema = JSON.parse(schemaRow.schemaJson).schema || {};
|
|
721
|
+
schemaSummary = summarizeSchema(schema);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const rawValue = newConfig.value ?? config.value ?? "{}";
|
|
726
|
+
const value = typeof rawValue === "string" ? parseValue(rawValue) : rawValue;
|
|
727
|
+
const labeledValue = buildLabeledValue(value, schema);
|
|
728
|
+
const valueSummary = summarizeValue(value);
|
|
729
|
+
const meta = {
|
|
730
|
+
id: newConfig.id || config.id,
|
|
731
|
+
namespace: newConfig.namespace || normalized.namespace,
|
|
732
|
+
key: newConfig.key || normalized.key,
|
|
733
|
+
desc: newConfig.desc || config.desc || "",
|
|
734
|
+
name: config.name || "",
|
|
735
|
+
ldapId: newConfig.ldapId || "",
|
|
736
|
+
createdTime: newConfig.createdTime || config.createdTime,
|
|
737
|
+
updatedTime: newConfig.updatedTime || config.updatedTime,
|
|
738
|
+
schemaId,
|
|
739
|
+
hasDraft: !!draft,
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const output = {
|
|
743
|
+
action: "get",
|
|
744
|
+
environment,
|
|
745
|
+
namespace: meta.namespace,
|
|
746
|
+
key: meta.key,
|
|
747
|
+
configId: meta.id,
|
|
748
|
+
schemaId: schemaId || null,
|
|
749
|
+
metadata: meta,
|
|
750
|
+
value,
|
|
751
|
+
rawValue: normalized.includeRawValue ? rawValue : undefined,
|
|
752
|
+
schema: schemaSummary,
|
|
753
|
+
view: {
|
|
754
|
+
kind: "conan_config_query",
|
|
755
|
+
summary: {
|
|
756
|
+
topLevelKeys: valueSummary.topLevelKeys,
|
|
757
|
+
arrays: valueSummary.arrays,
|
|
758
|
+
objects: valueSummary.objects,
|
|
759
|
+
schemaFieldCount: Object.keys(schemaSummary).length,
|
|
760
|
+
hasDraft: !!draft,
|
|
761
|
+
},
|
|
762
|
+
schemaFields: schemaSummary,
|
|
763
|
+
labeledValue,
|
|
764
|
+
tables: buildArrayTables(value, schema),
|
|
765
|
+
markdown: buildMarkdown(meta, schemaSummary, labeledValue, valueSummary),
|
|
766
|
+
},
|
|
767
|
+
updatedTime: meta.updatedTime,
|
|
768
|
+
hasDraft: !!draft,
|
|
769
|
+
};
|
|
770
|
+
if (!normalized.saveArtifacts) {
|
|
771
|
+
return output;
|
|
772
|
+
}
|
|
773
|
+
const artifactPaths = saveConfigArtifacts(output);
|
|
774
|
+
if (!artifactPaths) {
|
|
775
|
+
return output;
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
...output,
|
|
779
|
+
artifacts: artifactPaths,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const normalized = normalizeInput(input);
|
|
784
|
+
return await loadConfig(normalized);
|
|
785
|
+
`
|
|
786
|
+
|
|
787
|
+
const conanConfigPrepareChangeScript = String.raw`
|
|
788
|
+
const CONFIG_ENVIRONMENTS = {
|
|
789
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
790
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
function resolveEnvironment(rawInput) {
|
|
794
|
+
const nextInput = rawInput || {};
|
|
795
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
796
|
+
if (explicit) {
|
|
797
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
798
|
+
return explicit;
|
|
799
|
+
}
|
|
800
|
+
if (!nextInput.url) return "cn-prod";
|
|
801
|
+
const hostname = new URL(nextInput.url).hostname;
|
|
802
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
803
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
804
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const environment = resolveEnvironment(input);
|
|
808
|
+
const API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
809
|
+
|
|
810
|
+
function parseTarget(rawInput) {
|
|
811
|
+
const nextInput = rawInput || {};
|
|
812
|
+
const parsed = (() => {
|
|
813
|
+
if (!nextInput.url) return {};
|
|
814
|
+
const url = new URL(nextInput.url);
|
|
815
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
816
|
+
const query = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : "";
|
|
817
|
+
const params = new URLSearchParams(query);
|
|
818
|
+
return {
|
|
819
|
+
key: params.get("key") || url.searchParams.get("key") || "",
|
|
820
|
+
namespace:
|
|
821
|
+
params.get("rootGroupingKey") ||
|
|
822
|
+
params.get("namespace") ||
|
|
823
|
+
url.searchParams.get("rootGroupingKey") ||
|
|
824
|
+
url.searchParams.get("namespace") ||
|
|
825
|
+
"",
|
|
826
|
+
};
|
|
827
|
+
})();
|
|
828
|
+
const key = nextInput.key || parsed.key || "";
|
|
829
|
+
const namespace = nextInput.namespace || nextInput.rootGroupingKey || parsed.namespace || "";
|
|
830
|
+
if (!key || !namespace) throw new Error("Provide url or namespace/key for the target config.");
|
|
831
|
+
return { key, namespace };
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function requestJson(path) {
|
|
835
|
+
if (!secrets.cookieHeader) {
|
|
836
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
837
|
+
}
|
|
838
|
+
const response = await fetch(API_ORIGIN + path, {
|
|
839
|
+
headers: { "Content-Type": "application/json", Cookie: secrets.cookieHeader },
|
|
840
|
+
});
|
|
841
|
+
const text = await response.text();
|
|
842
|
+
const body = (() => {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(text);
|
|
845
|
+
} catch {
|
|
846
|
+
return text;
|
|
847
|
+
}
|
|
848
|
+
})();
|
|
849
|
+
if (response.status === 401 || response.status === 403) throw new Error("Login expired: " + response.status);
|
|
850
|
+
if (!response.ok) throw new Error("Request failed " + response.status + ": " + (typeof body === "string" ? body : JSON.stringify(body)));
|
|
851
|
+
if (body && typeof body === "object" && body.code !== undefined && body.code !== 0) {
|
|
852
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
853
|
+
}
|
|
854
|
+
return body;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function canonicalJson(value) {
|
|
858
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
|
|
859
|
+
if (value && typeof value === "object") {
|
|
860
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key])).join(",") + "}";
|
|
861
|
+
}
|
|
862
|
+
return JSON.stringify(value);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function sha256(value) {
|
|
866
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
867
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
868
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function collectDiff(before, after, path = "$") {
|
|
872
|
+
if (canonicalJson(before) === canonicalJson(after)) return [];
|
|
873
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
874
|
+
return Array.from({ length: Math.max(before.length, after.length) }).flatMap((_, index) => {
|
|
875
|
+
return collectDiff(before[index], after[index], path + "[" + index + "]");
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
if (before && after && typeof before === "object" && typeof after === "object") {
|
|
879
|
+
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
|
|
880
|
+
return keys.flatMap((key) => {
|
|
881
|
+
return collectDiff(before[key], after[key], path + "." + key);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return [{ path, before, after }];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const target = parseTarget(input);
|
|
888
|
+
const namespace = encodeURIComponent(target.namespace);
|
|
889
|
+
const key = encodeURIComponent(target.key);
|
|
890
|
+
const detailBody = await requestJson("/conan-config/api/enhancedConfigs/detail/" + namespace + "/" + key);
|
|
891
|
+
const config = detailBody?.data?.config;
|
|
892
|
+
if (!config) throw new Error("Config detail does not exist.");
|
|
893
|
+
if (!config.schemaId) throw new Error("Config has no schemaId and cannot enter AI validation.");
|
|
894
|
+
const schemaRows = await requestJson("/conan-config/api/schema?ids=" + encodeURIComponent(config.schemaId));
|
|
895
|
+
const schemaRow = Array.isArray(schemaRows) ? schemaRows[0] : undefined;
|
|
896
|
+
if (!schemaRow?.schemaJson) throw new Error("Schema API did not return schemaJson.");
|
|
897
|
+
const parsedSchema = JSON.parse(schemaRow.schemaJson);
|
|
898
|
+
const currentValue = JSON.parse(config.value || "{}");
|
|
899
|
+
const sourceSha256 = await sha256(currentValue);
|
|
900
|
+
const hasTargetValue = input.value && typeof input.value === "object" && !Array.isArray(input.value);
|
|
901
|
+
const targetSha256 = hasTargetValue ? await sha256(input.value) : undefined;
|
|
902
|
+
const diff = hasTargetValue ? collectDiff(currentValue, input.value) : [];
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
action: "prepare-change",
|
|
906
|
+
environment,
|
|
907
|
+
...target,
|
|
908
|
+
configId: config.id,
|
|
909
|
+
schemaId: config.schemaId,
|
|
910
|
+
updatedTime: config.updatedTime,
|
|
911
|
+
sourceSha256,
|
|
912
|
+
targetSha256,
|
|
913
|
+
hasDraft: !!detailBody?.data?.configDraft,
|
|
914
|
+
currentValue,
|
|
915
|
+
targetValue: hasTargetValue ? input.value : undefined,
|
|
916
|
+
changeSummary: input.changeSummary || "",
|
|
917
|
+
diff,
|
|
918
|
+
schema: parsedSchema.schema || {},
|
|
919
|
+
form: parsedSchema.form || {},
|
|
920
|
+
instruction:
|
|
921
|
+
"AI must validate the complete targetValue against schema, x-validator, x-reactions, component semantics, and the structured diff. Return validationReport with passed, errors, warnings, warningsAccepted, environment, schemaId, updatedTime, sourceSha256, targetSha256, and summary. Show the semantic diff and target environment, then obtain explicit user confirmation before apply-change.",
|
|
922
|
+
};
|
|
923
|
+
`
|
|
924
|
+
|
|
925
|
+
const conanConfigApplyChangeScript = String.raw`
|
|
926
|
+
const CONFIG_ENVIRONMENTS = {
|
|
927
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
928
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
function resolveEnvironment(rawInput) {
|
|
932
|
+
const nextInput = rawInput || {};
|
|
933
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
934
|
+
if (explicit) {
|
|
935
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
936
|
+
return explicit;
|
|
937
|
+
}
|
|
938
|
+
if (!nextInput.url) return "cn-prod";
|
|
939
|
+
const hostname = new URL(nextInput.url).hostname;
|
|
940
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
941
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
942
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const environment = resolveEnvironment(input);
|
|
946
|
+
const API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
947
|
+
|
|
948
|
+
function parseTarget(rawInput) {
|
|
949
|
+
const nextInput = rawInput || {};
|
|
950
|
+
const parsed = (() => {
|
|
951
|
+
if (!nextInput.url) return {};
|
|
952
|
+
const url = new URL(nextInput.url);
|
|
953
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
954
|
+
const query = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : "";
|
|
955
|
+
const params = new URLSearchParams(query);
|
|
956
|
+
return {
|
|
957
|
+
key: params.get("key") || url.searchParams.get("key") || "",
|
|
958
|
+
namespace:
|
|
959
|
+
params.get("rootGroupingKey") ||
|
|
960
|
+
params.get("namespace") ||
|
|
961
|
+
url.searchParams.get("rootGroupingKey") ||
|
|
962
|
+
url.searchParams.get("namespace") ||
|
|
963
|
+
"",
|
|
964
|
+
};
|
|
965
|
+
})();
|
|
966
|
+
const key = nextInput.key || parsed.key || "";
|
|
967
|
+
const namespace = nextInput.namespace || nextInput.rootGroupingKey || parsed.namespace || "";
|
|
968
|
+
if (!key || !namespace) throw new Error("Provide url or namespace/key for the target config.");
|
|
969
|
+
return { key, namespace };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function requestJson(path, options = {}) {
|
|
973
|
+
if (!secrets.cookieHeader) {
|
|
974
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
975
|
+
}
|
|
976
|
+
const response = await fetch(API_ORIGIN + path, {
|
|
977
|
+
...options,
|
|
978
|
+
headers: { "Content-Type": "application/json", Cookie: secrets.cookieHeader, ...(options.headers || {}) },
|
|
979
|
+
});
|
|
980
|
+
const text = await response.text();
|
|
981
|
+
const body = (() => {
|
|
982
|
+
try {
|
|
983
|
+
return JSON.parse(text);
|
|
984
|
+
} catch {
|
|
985
|
+
return text;
|
|
986
|
+
}
|
|
987
|
+
})();
|
|
988
|
+
if (response.status === 401 || response.status === 403) throw new Error("Login expired: " + response.status);
|
|
989
|
+
if (!response.ok) throw new Error("Request failed " + response.status + ": " + (typeof body === "string" ? body : JSON.stringify(body)));
|
|
990
|
+
if (body && typeof body === "object" && body.code !== undefined && body.code !== 0) {
|
|
991
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
992
|
+
}
|
|
993
|
+
return body;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function canonicalJson(value) {
|
|
997
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
|
|
998
|
+
if (value && typeof value === "object") {
|
|
999
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key])).join(",") + "}";
|
|
1000
|
+
}
|
|
1001
|
+
return JSON.stringify(value);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async function sha256(value) {
|
|
1005
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
1006
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1007
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const target = parseTarget(input);
|
|
1011
|
+
const report = input.validationReport;
|
|
1012
|
+
if (!report?.passed || (report.errors || []).length) {
|
|
1013
|
+
return { status: "ai_validation_failed", environment, ...target, validationErrors: report?.errors || ["AI validation did not pass."], submitted: false };
|
|
1014
|
+
}
|
|
1015
|
+
if ((report.warnings || []).length && report.warningsAccepted !== true) {
|
|
1016
|
+
return { status: "warnings_not_accepted", environment, ...target, validationErrors: report.warnings, submitted: false };
|
|
1017
|
+
}
|
|
1018
|
+
if (report.environment !== environment) {
|
|
1019
|
+
return { status: "environment_changed_after_validation", environment, ...target, validationErrors: ["Target environment changed after validation."], submitted: false };
|
|
1020
|
+
}
|
|
1021
|
+
const targetSha256 = await sha256(input.value);
|
|
1022
|
+
if (targetSha256 !== report.targetSha256) {
|
|
1023
|
+
return { status: "target_changed_after_validation", environment, ...target, validationErrors: ["Target JSON changed after validation."], submitted: false };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const namespace = encodeURIComponent(target.namespace);
|
|
1027
|
+
const key = encodeURIComponent(target.key);
|
|
1028
|
+
const detailBody = await requestJson("/conan-config/api/enhancedConfigs/detail/" + namespace + "/" + key);
|
|
1029
|
+
const config = detailBody?.data?.config;
|
|
1030
|
+
if (!config) throw new Error("Config detail does not exist.");
|
|
1031
|
+
if (detailBody?.data?.configDraft) {
|
|
1032
|
+
return { status: "draft_exists", environment, ...target, validationErrors: ["An existing draft must not be overwritten."], submitted: false };
|
|
1033
|
+
}
|
|
1034
|
+
const currentValue = JSON.parse(config.value || "{}");
|
|
1035
|
+
const sourceSha256 = await sha256(currentValue);
|
|
1036
|
+
if (
|
|
1037
|
+
config.schemaId !== report.schemaId ||
|
|
1038
|
+
config.updatedTime !== report.updatedTime ||
|
|
1039
|
+
sourceSha256 !== report.sourceSha256
|
|
1040
|
+
) {
|
|
1041
|
+
return {
|
|
1042
|
+
status: "source_changed_after_validation",
|
|
1043
|
+
environment,
|
|
1044
|
+
...target,
|
|
1045
|
+
schemaId: config.schemaId || null,
|
|
1046
|
+
updatedTime: config.updatedTime,
|
|
1047
|
+
sourceSha256,
|
|
1048
|
+
validationErrors: ["schemaId, updatedTime, or source value changed. Prepare and validate again."],
|
|
1049
|
+
submitted: false,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const updatePayload = {
|
|
1054
|
+
desc: config.desc,
|
|
1055
|
+
groupingId: config.groupingId,
|
|
1056
|
+
id: config.id,
|
|
1057
|
+
name: config.name,
|
|
1058
|
+
schemaId: config.schemaId,
|
|
1059
|
+
updatedTime: config.updatedTime,
|
|
1060
|
+
value: JSON.stringify(input.value),
|
|
1061
|
+
operationType: 3,
|
|
1062
|
+
};
|
|
1063
|
+
const updateBody = await requestJson("/conan-config/api/enhancedConfigs/update", {
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
body: JSON.stringify(updatePayload),
|
|
1066
|
+
});
|
|
1067
|
+
const configDraftId = updateBody?.data;
|
|
1068
|
+
if (!configDraftId) throw new Error("Update API did not return configDraftId; config was not published.");
|
|
1069
|
+
const publishBody = await requestJson("/conan-config/api/enhancedConfigs/publish", {
|
|
1070
|
+
method: "POST",
|
|
1071
|
+
body: JSON.stringify({ configId: config.id, configDraftId }),
|
|
1072
|
+
});
|
|
1073
|
+
if (!publishBody?.data) throw new Error("Publish API did not return success.");
|
|
1074
|
+
|
|
1075
|
+
const verifiedConfig = await requestJson("/conan-config/api/newConfigs/" + namespace + "/" + key);
|
|
1076
|
+
const verifiedValue = typeof verifiedConfig.value === "string" ? JSON.parse(verifiedConfig.value || "{}") : verifiedConfig.value;
|
|
1077
|
+
const verifiedSha256 = await sha256(verifiedValue);
|
|
1078
|
+
if (verifiedSha256 !== targetSha256) throw new Error("Published config verification failed: target hash mismatch.");
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
status: "published",
|
|
1082
|
+
environment,
|
|
1083
|
+
...target,
|
|
1084
|
+
configId: config.id,
|
|
1085
|
+
schemaId: config.schemaId,
|
|
1086
|
+
configDraftId,
|
|
1087
|
+
sourceSha256,
|
|
1088
|
+
targetSha256,
|
|
1089
|
+
verifiedSha256,
|
|
1090
|
+
validationErrors: [],
|
|
1091
|
+
validationWarnings: report.warnings || [],
|
|
1092
|
+
submitted: true,
|
|
1093
|
+
verified: true,
|
|
1094
|
+
changeSummary: input.changeSummary,
|
|
1095
|
+
};
|
|
1096
|
+
`
|
|
1097
|
+
|
|
1098
|
+
// Mirrors the B-side workflow in conan-oversea-designer: update creates a draft, publish consumes
|
|
1099
|
+
// that draft id, unPublish discards it, updatedTime provides optimistic concurrency, rollback uses
|
|
1100
|
+
// operationType 4, and copy creates a new definition that reuses the source schema without content.
|
|
1101
|
+
const conanConfigLifecycleReadScript = String.raw`
|
|
1102
|
+
const CONFIG_ENVIRONMENTS = {
|
|
1103
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
1104
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
function resolveEnvironment(rawInput) {
|
|
1108
|
+
const nextInput = rawInput || {};
|
|
1109
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
1110
|
+
if (explicit) {
|
|
1111
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
1112
|
+
return explicit;
|
|
1113
|
+
}
|
|
1114
|
+
const urlText = nextInput.url || nextInput.configUrl || nextInput.sourceUrl || "";
|
|
1115
|
+
if (!urlText) return "cn-prod";
|
|
1116
|
+
const hostname = new URL(urlText).hostname;
|
|
1117
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
1118
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
1119
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function parseTarget(rawInput, prefix = "") {
|
|
1123
|
+
const nextInput = rawInput || {};
|
|
1124
|
+
const urlText = nextInput[prefix + "Url"] || (prefix ? "" : nextInput.url || nextInput.configUrl || "");
|
|
1125
|
+
const parsed = (() => {
|
|
1126
|
+
if (!urlText) return {};
|
|
1127
|
+
const url = new URL(urlText);
|
|
1128
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
1129
|
+
const query = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : "";
|
|
1130
|
+
const params = new URLSearchParams(query);
|
|
1131
|
+
return {
|
|
1132
|
+
key: params.get("key") || url.searchParams.get("key") || "",
|
|
1133
|
+
namespace:
|
|
1134
|
+
params.get("rootGroupingKey") ||
|
|
1135
|
+
params.get("namespace") ||
|
|
1136
|
+
url.searchParams.get("rootGroupingKey") ||
|
|
1137
|
+
url.searchParams.get("namespace") ||
|
|
1138
|
+
"",
|
|
1139
|
+
};
|
|
1140
|
+
})();
|
|
1141
|
+
const key = nextInput[prefix + "Key"] || (prefix ? "" : nextInput.key) || parsed.key || "";
|
|
1142
|
+
const namespace =
|
|
1143
|
+
nextInput[prefix + "Namespace"] ||
|
|
1144
|
+
(prefix ? "" : nextInput.namespace || nextInput.rootGroupingKey) ||
|
|
1145
|
+
parsed.namespace ||
|
|
1146
|
+
"";
|
|
1147
|
+
if (!key || !namespace) throw new Error("Provide " + (prefix || "target") + "Url or namespace/key.");
|
|
1148
|
+
return { key, namespace };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function parseJsonValue(value) {
|
|
1152
|
+
if (value === undefined || value === null || value === "") return {};
|
|
1153
|
+
return typeof value === "string" ? JSON.parse(value || "{}") : value;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function canonicalJson(value) {
|
|
1157
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
|
|
1158
|
+
if (value && typeof value === "object") {
|
|
1159
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key])).join(",") + "}";
|
|
1160
|
+
}
|
|
1161
|
+
return JSON.stringify(value);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
async function sha256(value) {
|
|
1165
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
1166
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1167
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function collectDiff(before, after, path = "$") {
|
|
1171
|
+
if (canonicalJson(before) === canonicalJson(after)) return [];
|
|
1172
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
1173
|
+
return Array.from({ length: Math.max(before.length, after.length) }).flatMap((_, index) => {
|
|
1174
|
+
return collectDiff(before[index], after[index], path + "[" + index + "]");
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (before && after && typeof before === "object" && typeof after === "object") {
|
|
1178
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])].sort().flatMap((key) => {
|
|
1179
|
+
return collectDiff(before[key], after[key], path + "." + key);
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
return [{ path, before, after }];
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const environment = resolveEnvironment(input);
|
|
1186
|
+
const API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
1187
|
+
|
|
1188
|
+
async function requestJson(path, options = {}) {
|
|
1189
|
+
if (!secrets.cookieHeader) {
|
|
1190
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
1191
|
+
}
|
|
1192
|
+
const response = await fetch(API_ORIGIN + path, {
|
|
1193
|
+
...options,
|
|
1194
|
+
headers: { "Content-Type": "application/json", Cookie: secrets.cookieHeader, ...(options.headers || {}) },
|
|
1195
|
+
});
|
|
1196
|
+
const text = await response.text();
|
|
1197
|
+
const body = (() => {
|
|
1198
|
+
try {
|
|
1199
|
+
return JSON.parse(text);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return text;
|
|
1202
|
+
}
|
|
1203
|
+
})();
|
|
1204
|
+
if (response.status === 401 || response.status === 403) {
|
|
1205
|
+
throw new Error("Request failed " + response.status + ": login expired. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
1206
|
+
}
|
|
1207
|
+
if (!response.ok) throw new Error("Request failed " + response.status + ": " + (typeof body === "string" ? body : JSON.stringify(body)));
|
|
1208
|
+
if (body && typeof body === "object" && body.code !== undefined && body.code !== 0) {
|
|
1209
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
1210
|
+
}
|
|
1211
|
+
return body;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function loadDetail(target) {
|
|
1215
|
+
const namespace = encodeURIComponent(target.namespace);
|
|
1216
|
+
const key = encodeURIComponent(target.key);
|
|
1217
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/detail/" + namespace + "/" + key);
|
|
1218
|
+
if (!body?.data?.config) throw new Error("Config detail does not exist.");
|
|
1219
|
+
return body.data;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function loadSchema(schemaId) {
|
|
1223
|
+
if (!schemaId) return { row: null, schema: {}, form: {} };
|
|
1224
|
+
const rows = await requestJson("/conan-config/api/schema?ids=" + encodeURIComponent(schemaId));
|
|
1225
|
+
const row = Array.isArray(rows) ? rows[0] : undefined;
|
|
1226
|
+
if (!row?.schemaJson) throw new Error("Schema API did not return schemaJson for schemaId " + schemaId + ".");
|
|
1227
|
+
const parsed = JSON.parse(row.schemaJson);
|
|
1228
|
+
return { row, schema: parsed.schema || {}, form: parsed.form || {} };
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function flattenTree(nodes, rootGroupingKey = "") {
|
|
1232
|
+
return (nodes || []).flatMap((node) => {
|
|
1233
|
+
const root = rootGroupingKey || node.rootGroupingKey || node.groupingKey || "";
|
|
1234
|
+
return [{ node, rootGroupingKey: root }, ...flattenTree(node.children || [], root)];
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
async function loadTree() {
|
|
1239
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/searchTree");
|
|
1240
|
+
return body?.data?.configBases || [];
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function validateDefinition(rawInput, grouping) {
|
|
1244
|
+
const definition = {
|
|
1245
|
+
groupingId: Number(rawInput.groupingId),
|
|
1246
|
+
key: String(rawInput.targetKey || rawInput.key || "").trim(),
|
|
1247
|
+
name: String(rawInput.targetName || rawInput.name || "").trim(),
|
|
1248
|
+
desc: String(rawInput.targetDesc || rawInput.desc || "").trim(),
|
|
1249
|
+
schemaId: Number(rawInput.schemaId),
|
|
1250
|
+
};
|
|
1251
|
+
const errors = [
|
|
1252
|
+
Number.isInteger(definition.groupingId) && definition.groupingId > 0 ? "" : "groupingId must be a positive integer.",
|
|
1253
|
+
/^[a-zA-Z0-9_]{5,50}$/.test(definition.key) ? "" : "key must be 5-50 letters, digits, or underscores.",
|
|
1254
|
+
definition.name && definition.name.length <= 20 ? "" : "name is required and must be at most 20 characters.",
|
|
1255
|
+
definition.desc && definition.desc.length <= 45 ? "" : "desc is required and must be at most 45 characters.",
|
|
1256
|
+
Number.isInteger(definition.schemaId) && definition.schemaId >= 0 ? "" : "schemaId must be a non-negative integer.",
|
|
1257
|
+
grouping ? "" : "groupingId does not exist.",
|
|
1258
|
+
].filter(Boolean);
|
|
1259
|
+
if (errors.length) throw new Error(errors.join(" "));
|
|
1260
|
+
return { ...definition, rootGroupingKey: grouping.rootGroupingKey };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async function prepareDefinition(rawInput, overrides = {}) {
|
|
1264
|
+
const tree = await loadTree();
|
|
1265
|
+
const flatTree = flattenTree(tree);
|
|
1266
|
+
const groupingId = Number(rawInput.groupingId);
|
|
1267
|
+
const grouping = flatTree.find((item) => item.node.id === groupingId && item.node.type === 2);
|
|
1268
|
+
const definition = validateDefinition({ ...rawInput, ...overrides }, grouping);
|
|
1269
|
+
const duplicate = flatTree.find((item) => {
|
|
1270
|
+
return item.node.key === definition.key && item.rootGroupingKey === definition.rootGroupingKey;
|
|
1271
|
+
});
|
|
1272
|
+
if (duplicate) throw new Error("Config key already exists in target root grouping: " + definition.rootGroupingKey + "/" + definition.key);
|
|
1273
|
+
const schema = await loadSchema(definition.schemaId);
|
|
1274
|
+
return {
|
|
1275
|
+
definition,
|
|
1276
|
+
schema,
|
|
1277
|
+
groupingUpdatedTime: grouping.node.updatedTime || null,
|
|
1278
|
+
targetSha256: await sha256(definition),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (input.action === "list-spaces") {
|
|
1283
|
+
const body = await requestJson("/conan-config/api/enhancedConfigGroupings/rootGrouping");
|
|
1284
|
+
const spaces = body?.data || [];
|
|
1285
|
+
return { action: "list-spaces", environment, count: spaces.length, spaces };
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (input.action === "history") {
|
|
1289
|
+
const target = parseTarget(input);
|
|
1290
|
+
const detail = await loadDetail(target);
|
|
1291
|
+
const histories = detail.configHistories || [];
|
|
1292
|
+
const requestedIds = Array.isArray(input.historyIds) ? input.historyIds.map(Number).filter(Number.isInteger) : [];
|
|
1293
|
+
const details = requestedIds.length
|
|
1294
|
+
? (await requestJson("/conan-config/api/enhancedConfigs/detailHistory?ids=" + requestedIds.join(",")))?.data || []
|
|
1295
|
+
: [];
|
|
1296
|
+
return {
|
|
1297
|
+
action: "history",
|
|
1298
|
+
environment,
|
|
1299
|
+
...target,
|
|
1300
|
+
configId: detail.config.id,
|
|
1301
|
+
historyCount: histories.length,
|
|
1302
|
+
histories,
|
|
1303
|
+
details,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (input.action === "get-schema") {
|
|
1308
|
+
const schemaId = Number(input.schemaId) || (await loadDetail(parseTarget(input))).config.schemaId;
|
|
1309
|
+
const loaded = await loadSchema(schemaId);
|
|
1310
|
+
return {
|
|
1311
|
+
action: "get-schema",
|
|
1312
|
+
environment,
|
|
1313
|
+
schemaId,
|
|
1314
|
+
metadata: loaded.row,
|
|
1315
|
+
schema: loaded.schema,
|
|
1316
|
+
form: loaded.form,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (input.action === "prepare-publish-draft") {
|
|
1321
|
+
const target = parseTarget(input);
|
|
1322
|
+
const detail = await loadDetail(target);
|
|
1323
|
+
const config = detail.config;
|
|
1324
|
+
const draft = detail.configDraft;
|
|
1325
|
+
if (!draft?.id || !draft.config) throw new Error("Config has no draft to publish.");
|
|
1326
|
+
const currentValue = parseJsonValue(config.value);
|
|
1327
|
+
const draftValue = parseJsonValue(draft.config.value);
|
|
1328
|
+
const schemaId = draft.config.schemaId || config.schemaId;
|
|
1329
|
+
const loaded = await loadSchema(schemaId);
|
|
1330
|
+
return {
|
|
1331
|
+
action: "prepare-publish-draft",
|
|
1332
|
+
environment,
|
|
1333
|
+
...target,
|
|
1334
|
+
configId: config.id,
|
|
1335
|
+
configDraftId: draft.id,
|
|
1336
|
+
draftOperator: draft.opLdap || "",
|
|
1337
|
+
schemaId,
|
|
1338
|
+
updatedTime: config.updatedTime,
|
|
1339
|
+
sourceSha256: await sha256(currentValue),
|
|
1340
|
+
draftSha256: await sha256(draftValue),
|
|
1341
|
+
currentValue,
|
|
1342
|
+
draftValue,
|
|
1343
|
+
diff: collectDiff(currentValue, draftValue),
|
|
1344
|
+
schema: loaded.schema,
|
|
1345
|
+
form: loaded.form,
|
|
1346
|
+
instruction: "Validate the complete draftValue, show the semantic diff and target environment, then obtain confirmation before publish-draft.",
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (input.action === "prepare-create") {
|
|
1351
|
+
const prepared = await prepareDefinition(input);
|
|
1352
|
+
return {
|
|
1353
|
+
action: "prepare-create",
|
|
1354
|
+
environment,
|
|
1355
|
+
definition: prepared.definition,
|
|
1356
|
+
groupingUpdatedTime: prepared.groupingUpdatedTime,
|
|
1357
|
+
targetSha256: prepared.targetSha256,
|
|
1358
|
+
schema: prepared.schema.schema,
|
|
1359
|
+
form: prepared.schema.form,
|
|
1360
|
+
warnings: prepared.definition.schemaId === 0 ? ["The new config has no Formily schema."] : [],
|
|
1361
|
+
instruction: "Show the complete definition and environment, then obtain confirmation before create.",
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
if (input.action === "prepare-copy") {
|
|
1366
|
+
const source = parseTarget(input, "source");
|
|
1367
|
+
const sourceDetail = await loadDetail(source);
|
|
1368
|
+
const sourceConfig = sourceDetail.config;
|
|
1369
|
+
const prepared = await prepareDefinition(input, {
|
|
1370
|
+
schemaId: sourceConfig.schemaId || 0,
|
|
1371
|
+
targetName: input.targetName || sourceConfig.name,
|
|
1372
|
+
targetDesc: input.targetDesc || sourceConfig.desc,
|
|
1373
|
+
});
|
|
1374
|
+
return {
|
|
1375
|
+
action: "prepare-copy",
|
|
1376
|
+
environment,
|
|
1377
|
+
source: { ...source, configId: sourceConfig.id, schemaId: sourceConfig.schemaId },
|
|
1378
|
+
target: prepared.definition,
|
|
1379
|
+
sourceUpdatedTime: sourceConfig.updatedTime,
|
|
1380
|
+
sourceDefinitionSha256: await sha256({
|
|
1381
|
+
id: sourceConfig.id,
|
|
1382
|
+
name: sourceConfig.name,
|
|
1383
|
+
desc: sourceConfig.desc,
|
|
1384
|
+
schemaId: sourceConfig.schemaId,
|
|
1385
|
+
updatedTime: sourceConfig.updatedTime,
|
|
1386
|
+
}),
|
|
1387
|
+
groupingUpdatedTime: prepared.groupingUpdatedTime,
|
|
1388
|
+
targetSha256: prepared.targetSha256,
|
|
1389
|
+
schema: prepared.schema.schema,
|
|
1390
|
+
form: prepared.schema.form,
|
|
1391
|
+
contentCopied: false,
|
|
1392
|
+
instruction: "B-side copy duplicates config metadata and schema only, not value content. Show source, target and environment, then obtain confirmation before copy.",
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (input.action === "prepare-rollback") {
|
|
1397
|
+
const target = parseTarget(input);
|
|
1398
|
+
const historyId = Number(input.historyId);
|
|
1399
|
+
if (!Number.isInteger(historyId) || historyId <= 0) throw new Error("historyId must be a positive integer.");
|
|
1400
|
+
const detail = await loadDetail(target);
|
|
1401
|
+
const historyRows = (await requestJson("/conan-config/api/enhancedConfigs/detailHistory?ids=" + historyId))?.data || [];
|
|
1402
|
+
const historyRow = historyRows[0];
|
|
1403
|
+
if (!historyRow?.config) throw new Error("History record does not exist: " + historyId);
|
|
1404
|
+
const currentConfig = detail.config;
|
|
1405
|
+
if (historyRow.configId && historyRow.configId !== currentConfig.id) throw new Error("History record belongs to a different config.");
|
|
1406
|
+
const currentValue = parseJsonValue(currentConfig.value);
|
|
1407
|
+
const historicalValue = parseJsonValue(historyRow.config.value);
|
|
1408
|
+
const schemaId = historyRow.config.schemaId;
|
|
1409
|
+
const loaded = await loadSchema(schemaId);
|
|
1410
|
+
return {
|
|
1411
|
+
action: "prepare-rollback",
|
|
1412
|
+
environment,
|
|
1413
|
+
...target,
|
|
1414
|
+
configId: currentConfig.id,
|
|
1415
|
+
historyId,
|
|
1416
|
+
hasDraft: !!detail.configDraft,
|
|
1417
|
+
schemaId,
|
|
1418
|
+
updatedTime: currentConfig.updatedTime,
|
|
1419
|
+
sourceSha256: await sha256(currentValue),
|
|
1420
|
+
historicalSha256: await sha256(historicalValue),
|
|
1421
|
+
currentValue,
|
|
1422
|
+
historicalValue,
|
|
1423
|
+
diff: collectDiff(currentValue, historicalValue),
|
|
1424
|
+
schema: loaded.schema,
|
|
1425
|
+
form: loaded.form,
|
|
1426
|
+
instruction: "Validate the complete historicalValue against its schema, show the semantic diff and environment, then obtain confirmation before rollback.",
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
throw new Error("Unsupported Conan config read action: " + input.action);
|
|
1431
|
+
`
|
|
1432
|
+
|
|
1433
|
+
const conanConfigLifecycleWriteScript = String.raw`
|
|
1434
|
+
const CONFIG_ENVIRONMENTS = {
|
|
1435
|
+
"cn-prod": "https://conan.zhenguanyu.com",
|
|
1436
|
+
"cn-test": "https://ytkconan.zhenguanyu.com",
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
function resolveEnvironment(rawInput) {
|
|
1440
|
+
const nextInput = rawInput || {};
|
|
1441
|
+
const explicit = String(nextInput.environment || "").trim();
|
|
1442
|
+
if (explicit) {
|
|
1443
|
+
if (!CONFIG_ENVIRONMENTS[explicit]) throw new Error("Unsupported environment: " + explicit + ". Use cn-prod or cn-test.");
|
|
1444
|
+
return explicit;
|
|
1445
|
+
}
|
|
1446
|
+
const urlText = nextInput.url || nextInput.configUrl || nextInput.sourceUrl || "";
|
|
1447
|
+
if (!urlText) return "cn-prod";
|
|
1448
|
+
const hostname = new URL(urlText).hostname;
|
|
1449
|
+
if (hostname === "ytkconan.zhenguanyu.com" || hostname === "buff-test.zhenguanyu.com") return "cn-test";
|
|
1450
|
+
if (hostname === "conan.zhenguanyu.com" || hostname === "buff.zhenguanyu.com") return "cn-prod";
|
|
1451
|
+
throw new Error("Unsupported Conan config host: " + hostname);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function parseTarget(rawInput, prefix = "") {
|
|
1455
|
+
const nextInput = rawInput || {};
|
|
1456
|
+
const urlText = nextInput[prefix + "Url"] || (prefix ? "" : nextInput.url || nextInput.configUrl || "");
|
|
1457
|
+
const parsed = (() => {
|
|
1458
|
+
if (!urlText) return {};
|
|
1459
|
+
const url = new URL(urlText);
|
|
1460
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
1461
|
+
const query = hash.includes("?") ? hash.slice(hash.indexOf("?") + 1) : "";
|
|
1462
|
+
const params = new URLSearchParams(query);
|
|
1463
|
+
return {
|
|
1464
|
+
key: params.get("key") || url.searchParams.get("key") || "",
|
|
1465
|
+
namespace:
|
|
1466
|
+
params.get("rootGroupingKey") ||
|
|
1467
|
+
params.get("namespace") ||
|
|
1468
|
+
url.searchParams.get("rootGroupingKey") ||
|
|
1469
|
+
url.searchParams.get("namespace") ||
|
|
1470
|
+
"",
|
|
1471
|
+
};
|
|
1472
|
+
})();
|
|
1473
|
+
const key = nextInput[prefix + "Key"] || (prefix ? "" : nextInput.key) || parsed.key || "";
|
|
1474
|
+
const namespace =
|
|
1475
|
+
nextInput[prefix + "Namespace"] ||
|
|
1476
|
+
(prefix ? "" : nextInput.namespace || nextInput.rootGroupingKey) ||
|
|
1477
|
+
parsed.namespace ||
|
|
1478
|
+
"";
|
|
1479
|
+
if (!key || !namespace) throw new Error("Provide " + (prefix || "target") + "Url or namespace/key.");
|
|
1480
|
+
return { key, namespace };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function parseJsonValue(value) {
|
|
1484
|
+
if (value === undefined || value === null || value === "") return {};
|
|
1485
|
+
return typeof value === "string" ? JSON.parse(value || "{}") : value;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function canonicalJson(value) {
|
|
1489
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalJson).join(",") + "]";
|
|
1490
|
+
if (value && typeof value === "object") {
|
|
1491
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key])).join(",") + "}";
|
|
1492
|
+
}
|
|
1493
|
+
return JSON.stringify(value);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async function sha256(value) {
|
|
1497
|
+
const bytes = new TextEncoder().encode(canonicalJson(value));
|
|
1498
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1499
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const environment = resolveEnvironment(input);
|
|
1503
|
+
const API_ORIGIN = CONFIG_ENVIRONMENTS[environment];
|
|
1504
|
+
|
|
1505
|
+
async function requestJson(path, options = {}) {
|
|
1506
|
+
if (!secrets.cookieHeader) {
|
|
1507
|
+
throw new Error("Missing saved cookie auth. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
1508
|
+
}
|
|
1509
|
+
const response = await fetch(API_ORIGIN + path, {
|
|
1510
|
+
...options,
|
|
1511
|
+
headers: { "Content-Type": "application/json", Cookie: secrets.cookieHeader, ...(options.headers || {}) },
|
|
1512
|
+
});
|
|
1513
|
+
const text = await response.text();
|
|
1514
|
+
const body = (() => {
|
|
1515
|
+
try {
|
|
1516
|
+
return JSON.parse(text);
|
|
1517
|
+
} catch {
|
|
1518
|
+
return text;
|
|
1519
|
+
}
|
|
1520
|
+
})();
|
|
1521
|
+
if (response.status === 401 || response.status === 403) {
|
|
1522
|
+
throw new Error("Request failed " + response.status + ": login expired. Run: tabwright capability refresh-auth conan-config --browser user --json");
|
|
1523
|
+
}
|
|
1524
|
+
if (!response.ok) throw new Error("Request failed " + response.status + ": " + (typeof body === "string" ? body : JSON.stringify(body)));
|
|
1525
|
+
if (body && typeof body === "object" && body.code !== undefined && body.code !== 0) {
|
|
1526
|
+
throw new Error("API failed: " + (body.msg || JSON.stringify(body)));
|
|
1527
|
+
}
|
|
1528
|
+
return body;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
async function loadDetail(target) {
|
|
1532
|
+
const namespace = encodeURIComponent(target.namespace);
|
|
1533
|
+
const key = encodeURIComponent(target.key);
|
|
1534
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/detail/" + namespace + "/" + key);
|
|
1535
|
+
if (!body?.data?.config) throw new Error("Config detail does not exist.");
|
|
1536
|
+
return body.data;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function flattenTree(nodes, rootGroupingKey = "") {
|
|
1540
|
+
return (nodes || []).flatMap((node) => {
|
|
1541
|
+
const root = rootGroupingKey || node.rootGroupingKey || node.groupingKey || "";
|
|
1542
|
+
return [{ node, rootGroupingKey: root }, ...flattenTree(node.children || [], root)];
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function loadTree() {
|
|
1547
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/searchTree");
|
|
1548
|
+
return body?.data?.configBases || [];
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function validateDefinition(rawInput, grouping) {
|
|
1552
|
+
const definition = {
|
|
1553
|
+
groupingId: Number(rawInput.groupingId),
|
|
1554
|
+
key: String(rawInput.targetKey || rawInput.key || "").trim(),
|
|
1555
|
+
name: String(rawInput.targetName || rawInput.name || "").trim(),
|
|
1556
|
+
desc: String(rawInput.targetDesc || rawInput.desc || "").trim(),
|
|
1557
|
+
schemaId: Number(rawInput.schemaId),
|
|
1558
|
+
};
|
|
1559
|
+
const errors = [
|
|
1560
|
+
Number.isInteger(definition.groupingId) && definition.groupingId > 0 ? "" : "groupingId must be a positive integer.",
|
|
1561
|
+
/^[a-zA-Z0-9_]{5,50}$/.test(definition.key) ? "" : "key must be 5-50 letters, digits, or underscores.",
|
|
1562
|
+
definition.name && definition.name.length <= 20 ? "" : "name is required and must be at most 20 characters.",
|
|
1563
|
+
definition.desc && definition.desc.length <= 45 ? "" : "desc is required and must be at most 45 characters.",
|
|
1564
|
+
Number.isInteger(definition.schemaId) && definition.schemaId >= 0 ? "" : "schemaId must be a non-negative integer.",
|
|
1565
|
+
grouping ? "" : "groupingId does not exist.",
|
|
1566
|
+
].filter(Boolean);
|
|
1567
|
+
if (errors.length) throw new Error(errors.join(" "));
|
|
1568
|
+
return { ...definition, rootGroupingKey: grouping.rootGroupingKey };
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
async function prepareDefinition(rawInput, overrides = {}) {
|
|
1572
|
+
const tree = await loadTree();
|
|
1573
|
+
const flatTree = flattenTree(tree);
|
|
1574
|
+
const groupingId = Number(rawInput.groupingId);
|
|
1575
|
+
const grouping = flatTree.find((item) => item.node.id === groupingId && item.node.type === 2);
|
|
1576
|
+
const definition = validateDefinition({ ...rawInput, ...overrides }, grouping);
|
|
1577
|
+
const duplicate = flatTree.find((item) => {
|
|
1578
|
+
return item.node.key === definition.key && item.rootGroupingKey === definition.rootGroupingKey;
|
|
1579
|
+
});
|
|
1580
|
+
if (duplicate) throw new Error("Config key already exists in target root grouping: " + definition.rootGroupingKey + "/" + definition.key);
|
|
1581
|
+
return {
|
|
1582
|
+
definition,
|
|
1583
|
+
groupingUpdatedTime: grouping.node.updatedTime || null,
|
|
1584
|
+
targetSha256: await sha256(definition),
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
function failure(status, target, validationErrors) {
|
|
1589
|
+
return {
|
|
1590
|
+
status,
|
|
1591
|
+
environment,
|
|
1592
|
+
...target,
|
|
1593
|
+
validationErrors,
|
|
1594
|
+
submitted: false,
|
|
1595
|
+
published: false,
|
|
1596
|
+
verified: false,
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function reportErrors(report, expected) {
|
|
1601
|
+
if (!report?.passed || (report.errors || []).length) return report?.errors || ["AI validation did not pass."];
|
|
1602
|
+
if ((report.warnings || []).length && report.warningsAccepted !== true) return report.warnings;
|
|
1603
|
+
return Object.entries(expected).flatMap(([key, value]) => {
|
|
1604
|
+
return report[key] === value ? [] : [key + " changed after preparation."];
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function updatePayload(config, value, operationType, schemaId = config.schemaId) {
|
|
1609
|
+
return {
|
|
1610
|
+
desc: config.desc,
|
|
1611
|
+
groupingId: config.groupingId,
|
|
1612
|
+
id: config.id,
|
|
1613
|
+
name: config.name,
|
|
1614
|
+
schemaId,
|
|
1615
|
+
updatedTime: config.updatedTime,
|
|
1616
|
+
value: JSON.stringify(value),
|
|
1617
|
+
operationType,
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
async function createDraft(config, value, operationType, schemaId = config.schemaId) {
|
|
1622
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/update", {
|
|
1623
|
+
method: "POST",
|
|
1624
|
+
body: JSON.stringify(updatePayload(config, value, operationType, schemaId)),
|
|
1625
|
+
});
|
|
1626
|
+
if (!body?.data) throw new Error("Update API did not return configDraftId.");
|
|
1627
|
+
return body.data;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
async function publishDraft(configId, configDraftId) {
|
|
1631
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/publish", {
|
|
1632
|
+
method: "POST",
|
|
1633
|
+
body: JSON.stringify({ configId, configDraftId }),
|
|
1634
|
+
});
|
|
1635
|
+
if (!body?.data) throw new Error("Publish API did not return success.");
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (input.action === "save-draft") {
|
|
1639
|
+
const target = parseTarget(input);
|
|
1640
|
+
const detail = await loadDetail(target);
|
|
1641
|
+
const config = detail.config;
|
|
1642
|
+
if (detail.configDraft) return failure("draft_exists", target, ["An existing draft must be published or discarded first."]);
|
|
1643
|
+
const currentValue = parseJsonValue(config.value);
|
|
1644
|
+
const targetValue = input.value;
|
|
1645
|
+
const expected = {
|
|
1646
|
+
environment,
|
|
1647
|
+
schemaId: config.schemaId,
|
|
1648
|
+
updatedTime: config.updatedTime,
|
|
1649
|
+
sourceSha256: await sha256(currentValue),
|
|
1650
|
+
targetSha256: await sha256(targetValue),
|
|
1651
|
+
};
|
|
1652
|
+
const errors = reportErrors(input.validationReport, expected);
|
|
1653
|
+
if (errors.length) return failure("validation_failed", target, errors);
|
|
1654
|
+
const configDraftId = await createDraft(config, targetValue, 3);
|
|
1655
|
+
const verifiedDetail = await loadDetail(target);
|
|
1656
|
+
const verifiedDraft = verifiedDetail.configDraft;
|
|
1657
|
+
const verifiedDraftSha256 = verifiedDraft?.config ? await sha256(parseJsonValue(verifiedDraft.config.value)) : "";
|
|
1658
|
+
const verified = verifiedDraft?.id === configDraftId && verifiedDraftSha256 === expected.targetSha256;
|
|
1659
|
+
if (!verified) throw new Error("Draft verification failed.");
|
|
1660
|
+
return {
|
|
1661
|
+
status: "draft_saved",
|
|
1662
|
+
environment,
|
|
1663
|
+
...target,
|
|
1664
|
+
configId: config.id,
|
|
1665
|
+
configDraftId,
|
|
1666
|
+
targetSha256: expected.targetSha256,
|
|
1667
|
+
validationErrors: [],
|
|
1668
|
+
submitted: true,
|
|
1669
|
+
published: false,
|
|
1670
|
+
verified,
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (input.action === "publish-draft") {
|
|
1675
|
+
const target = parseTarget(input);
|
|
1676
|
+
const detail = await loadDetail(target);
|
|
1677
|
+
const config = detail.config;
|
|
1678
|
+
const draft = detail.configDraft;
|
|
1679
|
+
if (!draft?.id || !draft.config) return failure("draft_missing", target, ["Config has no draft to publish."]);
|
|
1680
|
+
const expected = {
|
|
1681
|
+
environment,
|
|
1682
|
+
configDraftId: draft.id,
|
|
1683
|
+
schemaId: draft.config.schemaId || config.schemaId,
|
|
1684
|
+
updatedTime: config.updatedTime,
|
|
1685
|
+
sourceSha256: await sha256(parseJsonValue(config.value)),
|
|
1686
|
+
draftSha256: await sha256(parseJsonValue(draft.config.value)),
|
|
1687
|
+
};
|
|
1688
|
+
const errors = reportErrors(input.validationReport, expected);
|
|
1689
|
+
if (errors.length) return failure("validation_failed", target, errors);
|
|
1690
|
+
await publishDraft(config.id, draft.id);
|
|
1691
|
+
const namespace = encodeURIComponent(target.namespace);
|
|
1692
|
+
const key = encodeURIComponent(target.key);
|
|
1693
|
+
const verifiedConfig = await requestJson("/conan-config/api/newConfigs/" + namespace + "/" + key);
|
|
1694
|
+
const verifiedSha256 = await sha256(parseJsonValue(verifiedConfig.value));
|
|
1695
|
+
const verifiedDetail = await loadDetail(target);
|
|
1696
|
+
const verified = verifiedSha256 === expected.draftSha256 && !verifiedDetail.configDraft;
|
|
1697
|
+
if (!verified) throw new Error("Published draft verification failed.");
|
|
1698
|
+
return {
|
|
1699
|
+
status: "published",
|
|
1700
|
+
environment,
|
|
1701
|
+
...target,
|
|
1702
|
+
configId: config.id,
|
|
1703
|
+
configDraftId: draft.id,
|
|
1704
|
+
targetSha256: expected.draftSha256,
|
|
1705
|
+
verifiedSha256,
|
|
1706
|
+
validationErrors: [],
|
|
1707
|
+
submitted: true,
|
|
1708
|
+
published: true,
|
|
1709
|
+
verified,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
if (input.action === "discard-draft") {
|
|
1714
|
+
const target = parseTarget(input);
|
|
1715
|
+
const detail = await loadDetail(target);
|
|
1716
|
+
const config = detail.config;
|
|
1717
|
+
const draft = detail.configDraft;
|
|
1718
|
+
if (!draft?.id || !draft.config) return failure("draft_missing", target, ["Config has no draft to discard."]);
|
|
1719
|
+
const draftSha256 = await sha256(parseJsonValue(draft.config.value));
|
|
1720
|
+
if (draft.id !== Number(input.configDraftId) || draftSha256 !== input.draftSha256) {
|
|
1721
|
+
return failure("draft_changed", target, ["Draft id or content changed after inspection."]);
|
|
1722
|
+
}
|
|
1723
|
+
await requestJson("/conan-config/api/enhancedConfigs/unPublish", {
|
|
1724
|
+
method: "POST",
|
|
1725
|
+
body: JSON.stringify({ configId: config.id, configDraftId: draft.id }),
|
|
1726
|
+
});
|
|
1727
|
+
const verifiedDetail = await loadDetail(target);
|
|
1728
|
+
const verified = !verifiedDetail.configDraft;
|
|
1729
|
+
if (!verified) throw new Error("Discarded draft verification failed.");
|
|
1730
|
+
return {
|
|
1731
|
+
status: "draft_discarded",
|
|
1732
|
+
environment,
|
|
1733
|
+
...target,
|
|
1734
|
+
configId: config.id,
|
|
1735
|
+
configDraftId: draft.id,
|
|
1736
|
+
validationErrors: [],
|
|
1737
|
+
submitted: true,
|
|
1738
|
+
published: false,
|
|
1739
|
+
verified,
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
if (input.action === "create") {
|
|
1744
|
+
const prepared = await prepareDefinition(input);
|
|
1745
|
+
const report = input.preparationReport || {};
|
|
1746
|
+
const errors = [
|
|
1747
|
+
report.environment === environment ? "" : "environment changed after preparation.",
|
|
1748
|
+
report.targetSha256 === prepared.targetSha256 ? "" : "target definition changed after preparation.",
|
|
1749
|
+
report.groupingUpdatedTime === prepared.groupingUpdatedTime ? "" : "target grouping changed after preparation.",
|
|
1750
|
+
].filter(Boolean);
|
|
1751
|
+
if (errors.length) return failure("preparation_changed", { namespace: prepared.definition.rootGroupingKey, key: prepared.definition.key }, errors);
|
|
1752
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/add", {
|
|
1753
|
+
method: "POST",
|
|
1754
|
+
body: JSON.stringify({
|
|
1755
|
+
groupingId: prepared.definition.groupingId,
|
|
1756
|
+
key: prepared.definition.key,
|
|
1757
|
+
name: prepared.definition.name,
|
|
1758
|
+
desc: prepared.definition.desc,
|
|
1759
|
+
schemaId: prepared.definition.schemaId,
|
|
1760
|
+
}),
|
|
1761
|
+
});
|
|
1762
|
+
const created = body?.data;
|
|
1763
|
+
const configId = Number(created?.id || created);
|
|
1764
|
+
const target = { namespace: created?.rootGroupingKey || prepared.definition.rootGroupingKey, key: created?.key || prepared.definition.key };
|
|
1765
|
+
const verifiedDetail = Number.isInteger(configId) && configId > 0
|
|
1766
|
+
? (await requestJson("/conan-config/api/enhancedConfigs/detail?id=" + configId))?.data
|
|
1767
|
+
: await loadDetail(target);
|
|
1768
|
+
const verifiedConfig = verifiedDetail?.config;
|
|
1769
|
+
const verified =
|
|
1770
|
+
!!verifiedConfig &&
|
|
1771
|
+
verifiedConfig.key === target.key &&
|
|
1772
|
+
verifiedConfig.groupingId === prepared.definition.groupingId &&
|
|
1773
|
+
verifiedConfig.schemaId === prepared.definition.schemaId;
|
|
1774
|
+
if (!verified) throw new Error("Created config verification failed.");
|
|
1775
|
+
return {
|
|
1776
|
+
status: "created",
|
|
1777
|
+
environment,
|
|
1778
|
+
...target,
|
|
1779
|
+
configId: verifiedConfig.id,
|
|
1780
|
+
definition: prepared.definition,
|
|
1781
|
+
validationErrors: [],
|
|
1782
|
+
submitted: true,
|
|
1783
|
+
published: true,
|
|
1784
|
+
verified,
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (input.action === "copy") {
|
|
1789
|
+
const source = parseTarget(input, "source");
|
|
1790
|
+
const sourceDetail = await loadDetail(source);
|
|
1791
|
+
const sourceConfig = sourceDetail.config;
|
|
1792
|
+
const prepared = await prepareDefinition(input, {
|
|
1793
|
+
schemaId: sourceConfig.schemaId || 0,
|
|
1794
|
+
targetName: input.targetName || sourceConfig.name,
|
|
1795
|
+
targetDesc: input.targetDesc || sourceConfig.desc,
|
|
1796
|
+
});
|
|
1797
|
+
const sourceDefinitionSha256 = await sha256({
|
|
1798
|
+
id: sourceConfig.id,
|
|
1799
|
+
name: sourceConfig.name,
|
|
1800
|
+
desc: sourceConfig.desc,
|
|
1801
|
+
schemaId: sourceConfig.schemaId,
|
|
1802
|
+
updatedTime: sourceConfig.updatedTime,
|
|
1803
|
+
});
|
|
1804
|
+
const report = input.preparationReport || {};
|
|
1805
|
+
const errors = [
|
|
1806
|
+
report.environment === environment ? "" : "environment changed after preparation.",
|
|
1807
|
+
report.targetSha256 === prepared.targetSha256 ? "" : "target definition changed after preparation.",
|
|
1808
|
+
report.groupingUpdatedTime === prepared.groupingUpdatedTime ? "" : "target grouping changed after preparation.",
|
|
1809
|
+
report.sourceDefinitionSha256 === sourceDefinitionSha256 ? "" : "source definition changed after preparation.",
|
|
1810
|
+
].filter(Boolean);
|
|
1811
|
+
if (errors.length) return failure("preparation_changed", { namespace: prepared.definition.rootGroupingKey, key: prepared.definition.key }, errors);
|
|
1812
|
+
const body = await requestJson("/conan-config/api/enhancedConfigs/add", {
|
|
1813
|
+
method: "POST",
|
|
1814
|
+
body: JSON.stringify({
|
|
1815
|
+
groupingId: prepared.definition.groupingId,
|
|
1816
|
+
key: prepared.definition.key,
|
|
1817
|
+
name: prepared.definition.name,
|
|
1818
|
+
desc: prepared.definition.desc,
|
|
1819
|
+
schemaId: sourceConfig.schemaId || 0,
|
|
1820
|
+
}),
|
|
1821
|
+
});
|
|
1822
|
+
const created = body?.data;
|
|
1823
|
+
const configId = Number(created?.id || created);
|
|
1824
|
+
const target = { namespace: created?.rootGroupingKey || prepared.definition.rootGroupingKey, key: created?.key || prepared.definition.key };
|
|
1825
|
+
const verifiedDetail = Number.isInteger(configId) && configId > 0
|
|
1826
|
+
? (await requestJson("/conan-config/api/enhancedConfigs/detail?id=" + configId))?.data
|
|
1827
|
+
: await loadDetail(target);
|
|
1828
|
+
const verifiedConfig = verifiedDetail?.config;
|
|
1829
|
+
const verified = !!verifiedConfig && verifiedConfig.key === target.key && verifiedConfig.schemaId === sourceConfig.schemaId;
|
|
1830
|
+
if (!verified) throw new Error("Copied config verification failed.");
|
|
1831
|
+
return {
|
|
1832
|
+
status: "copied",
|
|
1833
|
+
environment,
|
|
1834
|
+
source: { ...source, configId: sourceConfig.id },
|
|
1835
|
+
target: { ...target, configId: verifiedConfig.id },
|
|
1836
|
+
contentCopied: false,
|
|
1837
|
+
validationErrors: [],
|
|
1838
|
+
submitted: true,
|
|
1839
|
+
published: true,
|
|
1840
|
+
verified,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
if (input.action === "rollback") {
|
|
1845
|
+
const target = parseTarget(input);
|
|
1846
|
+
const historyId = Number(input.historyId);
|
|
1847
|
+
const detail = await loadDetail(target);
|
|
1848
|
+
if (detail.configDraft) return failure("draft_exists", target, ["Publish or discard the current draft before rollback."]);
|
|
1849
|
+
const historyRows = (await requestJson("/conan-config/api/enhancedConfigs/detailHistory?ids=" + historyId))?.data || [];
|
|
1850
|
+
const historyRow = historyRows[0];
|
|
1851
|
+
if (!historyRow?.config) return failure("history_missing", target, ["History record does not exist."]);
|
|
1852
|
+
const config = detail.config;
|
|
1853
|
+
if (historyRow.configId && historyRow.configId !== config.id) return failure("history_mismatch", target, ["History record belongs to a different config."]);
|
|
1854
|
+
const currentValue = parseJsonValue(config.value);
|
|
1855
|
+
const historicalValue = parseJsonValue(historyRow.config.value);
|
|
1856
|
+
const expected = {
|
|
1857
|
+
environment,
|
|
1858
|
+
historyId,
|
|
1859
|
+
schemaId: historyRow.config.schemaId,
|
|
1860
|
+
updatedTime: config.updatedTime,
|
|
1861
|
+
sourceSha256: await sha256(currentValue),
|
|
1862
|
+
historicalSha256: await sha256(historicalValue),
|
|
1863
|
+
};
|
|
1864
|
+
const errors = reportErrors(input.validationReport, expected);
|
|
1865
|
+
if (errors.length) return failure("validation_failed", target, errors);
|
|
1866
|
+
const configDraftId = await createDraft(config, historicalValue, 4, historyRow.config.schemaId);
|
|
1867
|
+
await publishDraft(config.id, configDraftId);
|
|
1868
|
+
const verifiedDetail = await loadDetail(target);
|
|
1869
|
+
const verifiedConfig = verifiedDetail.config;
|
|
1870
|
+
const verifiedSha256 = await sha256(parseJsonValue(verifiedConfig.value));
|
|
1871
|
+
const verified = verifiedSha256 === expected.historicalSha256 && verifiedConfig.schemaId === expected.schemaId && !verifiedDetail.configDraft;
|
|
1872
|
+
if (!verified) throw new Error("Rollback verification failed.");
|
|
1873
|
+
return {
|
|
1874
|
+
status: "rolled_back",
|
|
1875
|
+
environment,
|
|
1876
|
+
...target,
|
|
1877
|
+
configId: config.id,
|
|
1878
|
+
configDraftId,
|
|
1879
|
+
historyId,
|
|
1880
|
+
targetSha256: expected.historicalSha256,
|
|
1881
|
+
verifiedSha256,
|
|
1882
|
+
validationErrors: [],
|
|
1883
|
+
submitted: true,
|
|
1884
|
+
published: true,
|
|
1885
|
+
verified,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
throw new Error("Unsupported Conan config write action: " + input.action);
|
|
1890
|
+
`
|
|
1891
|
+
|
|
1892
|
+
const conanConfigScript = [
|
|
1893
|
+
'if (input.action === "search") { return await (async () => {',
|
|
1894
|
+
conanConfigSearchScript,
|
|
1895
|
+
'})() }',
|
|
1896
|
+
'if (input.action === "get") { return await (async () => {',
|
|
1897
|
+
conanConfigQueryScript,
|
|
1898
|
+
'})() }',
|
|
1899
|
+
'if (input.action === "prepare-change") { return await (async () => {',
|
|
1900
|
+
conanConfigPrepareChangeScript,
|
|
1901
|
+
'})() }',
|
|
1902
|
+
'if (input.action === "apply-change") { return await (async () => {',
|
|
1903
|
+
conanConfigApplyChangeScript,
|
|
1904
|
+
'})() }',
|
|
1905
|
+
'if (["list-spaces", "history", "get-schema", "prepare-publish-draft", "prepare-create", "prepare-copy", "prepare-rollback"].includes(input.action)) { return await (async () => {',
|
|
1906
|
+
conanConfigLifecycleReadScript,
|
|
1907
|
+
'})() }',
|
|
1908
|
+
'if (["save-draft", "publish-draft", "discard-draft", "create", "copy", "rollback"].includes(input.action)) { return await (async () => {',
|
|
1909
|
+
conanConfigLifecycleWriteScript,
|
|
1910
|
+
'})() }',
|
|
1911
|
+
'throw new Error("Unsupported Conan config action: " + input.action)',
|
|
1912
|
+
'',
|
|
1913
|
+
].join('\n')
|
|
1914
|
+
|
|
1915
|
+
const conanConfigEnvironmentProperty = {
|
|
1916
|
+
type: 'string',
|
|
1917
|
+
enum: ['cn-prod', 'cn-test'],
|
|
1918
|
+
description: '国内正式或国内测试环境;配置后台 URL 可自动推断。',
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
const conanConfigTargetInputProperties = {
|
|
1922
|
+
environment: conanConfigEnvironmentProperty,
|
|
1923
|
+
url: { type: 'string' },
|
|
1924
|
+
configUrl: { type: 'string' },
|
|
1925
|
+
namespace: { type: 'string' },
|
|
1926
|
+
rootGroupingKey: { type: 'string' },
|
|
1927
|
+
key: { type: 'string' },
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const conanConfigDefinitionInputProperties = {
|
|
1931
|
+
environment: conanConfigEnvironmentProperty,
|
|
1932
|
+
groupingId: { type: 'number' },
|
|
1933
|
+
key: { type: 'string' },
|
|
1934
|
+
targetKey: { type: 'string' },
|
|
1935
|
+
name: { type: 'string' },
|
|
1936
|
+
targetName: { type: 'string' },
|
|
1937
|
+
desc: { type: 'string' },
|
|
1938
|
+
targetDesc: { type: 'string' },
|
|
1939
|
+
schemaId: { type: 'number' },
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
const conanConfigWriteOutputSchema = {
|
|
1943
|
+
type: 'object',
|
|
1944
|
+
properties: {
|
|
1945
|
+
status: { type: 'string' },
|
|
1946
|
+
environment: { type: 'string' },
|
|
1947
|
+
validationErrors: { type: 'array' },
|
|
1948
|
+
submitted: { type: 'boolean' },
|
|
1949
|
+
published: { type: 'boolean' },
|
|
1950
|
+
verified: { type: 'boolean' },
|
|
1951
|
+
},
|
|
1952
|
+
required: ['status', 'environment', 'validationErrors', 'submitted', 'published', 'verified'],
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const conanConfigCapabilityDefinitions: BuiltinCapabilityDefinition[] = [
|
|
1956
|
+
{
|
|
1957
|
+
id: 'conan-config',
|
|
1958
|
+
title: 'Conan 文案配置',
|
|
1959
|
+
description:
|
|
1960
|
+
'搜索、读取和管理国内正式/测试环境的 Conan/Buff Space_Enhanced_Config 配置生命周期;只读操作可自动执行,草稿、发布、新建、复制、丢弃草稿和回滚必须人工确认。',
|
|
1961
|
+
script: conanConfigScript,
|
|
1962
|
+
manifest: {
|
|
1963
|
+
runtime: 'node',
|
|
1964
|
+
status: 'trusted',
|
|
1965
|
+
permissions: ['network:https://conan.zhenguanyu.com/*', 'network:https://ytkconan.zhenguanyu.com/*'],
|
|
1966
|
+
sideEffect: 'write',
|
|
1967
|
+
requiresConfirmation: true,
|
|
1968
|
+
whenToUse: [
|
|
1969
|
+
'搜索 Conan/Buff 文案配置目录或配置值',
|
|
1970
|
+
'通过 Space_Enhanced_Config 后台链接或 namespace/key 读取完整配置',
|
|
1971
|
+
'查看空间、Schema、草稿和历史版本',
|
|
1972
|
+
'修改配置前读取当前值和完整 Formily schema,生成语义 Diff 与 AI 校验报告',
|
|
1973
|
+
'用户确认结构化 Diff 后保存草稿、发布、丢弃草稿或回滚,并回读验证',
|
|
1974
|
+
'经用户确认后新建配置或复制已有配置定义与 Schema',
|
|
1975
|
+
],
|
|
1976
|
+
whenNotToUse: [
|
|
1977
|
+
'删除配置、管理分组、创建或编辑 Schema、置顶收藏配置',
|
|
1978
|
+
'操作海外环境;当前仅支持 cn-prod 和 cn-test',
|
|
1979
|
+
'缺少完整目标值、AI 校验报告或用户写入确认时发布配置',
|
|
1980
|
+
'覆盖已有草稿或在源配置变化后继续使用旧校验报告',
|
|
1981
|
+
],
|
|
1982
|
+
tags: [
|
|
1983
|
+
'conan-config',
|
|
1984
|
+
'copywriting',
|
|
1985
|
+
'search',
|
|
1986
|
+
'query',
|
|
1987
|
+
'draft',
|
|
1988
|
+
'history',
|
|
1989
|
+
'rollback',
|
|
1990
|
+
'schema-validation',
|
|
1991
|
+
'publish',
|
|
1992
|
+
'node-runtime',
|
|
1993
|
+
],
|
|
1994
|
+
auth: {
|
|
1995
|
+
type: 'cookie',
|
|
1996
|
+
refresh: 'from-browser',
|
|
1997
|
+
secretKey: 'cookieHeader',
|
|
1998
|
+
browserUrls: [
|
|
1999
|
+
'https://conan.zhenguanyu.com/',
|
|
2000
|
+
'https://ytkconan.zhenguanyu.com/',
|
|
2001
|
+
'https://buff.zhenguanyu.com/',
|
|
2002
|
+
'https://buff-test.zhenguanyu.com/',
|
|
2003
|
+
],
|
|
2004
|
+
requiredCookieNames: [],
|
|
2005
|
+
failureSignals: [
|
|
2006
|
+
'Full authentication is required',
|
|
2007
|
+
'Request failed 401',
|
|
2008
|
+
'Request failed 403',
|
|
2009
|
+
'Missing saved cookie auth',
|
|
2010
|
+
],
|
|
2011
|
+
},
|
|
2012
|
+
inputSchema: {
|
|
2013
|
+
type: 'object',
|
|
2014
|
+
properties: {
|
|
2015
|
+
action: { type: 'string' },
|
|
2016
|
+
},
|
|
2017
|
+
required: ['action'],
|
|
2018
|
+
},
|
|
2019
|
+
outputSchema: {
|
|
2020
|
+
type: 'object',
|
|
2021
|
+
properties: {},
|
|
2022
|
+
},
|
|
2023
|
+
operations: {
|
|
2024
|
+
search: {
|
|
2025
|
+
title: '搜索配置',
|
|
2026
|
+
description: '通过 searchTree API 按 config、configValue、grouping 搜索配置目录。',
|
|
2027
|
+
match: [],
|
|
2028
|
+
routingHint: 'search-first',
|
|
2029
|
+
sideEffect: 'read',
|
|
2030
|
+
requiresConfirmation: false,
|
|
2031
|
+
inputSchema: {
|
|
2032
|
+
type: 'object',
|
|
2033
|
+
properties: {
|
|
2034
|
+
action: { type: 'string' },
|
|
2035
|
+
environment: conanConfigEnvironmentProperty,
|
|
2036
|
+
query: { type: 'string' },
|
|
2037
|
+
keyword: { type: 'string' },
|
|
2038
|
+
search: { type: 'string' },
|
|
2039
|
+
mode: { type: 'string' },
|
|
2040
|
+
limit: { type: 'number' },
|
|
2041
|
+
includeRawTree: { type: 'boolean' },
|
|
2042
|
+
},
|
|
2043
|
+
required: ['action', 'query'],
|
|
2044
|
+
},
|
|
2045
|
+
outputSchema: {
|
|
2046
|
+
type: 'object',
|
|
2047
|
+
properties: {
|
|
2048
|
+
action: { type: 'string' },
|
|
2049
|
+
environment: { type: 'string' },
|
|
2050
|
+
query: { type: 'string' },
|
|
2051
|
+
resultCount: { type: 'number' },
|
|
2052
|
+
results: { type: 'array' },
|
|
2053
|
+
view: { type: 'object' },
|
|
2054
|
+
},
|
|
2055
|
+
required: ['action', 'environment', 'query', 'resultCount', 'results', 'view'],
|
|
2056
|
+
},
|
|
2057
|
+
},
|
|
2058
|
+
get: {
|
|
2059
|
+
title: '读取配置',
|
|
2060
|
+
description: '通过精确后台链接或 namespace/key 读取当前值、Schema 摘要和本地产物。',
|
|
2061
|
+
match: [
|
|
2062
|
+
'https://buff.zhenguanyu.com/*Space_Enhanced_Config*key=*rootGroupingKey=*',
|
|
2063
|
+
'https://conan.zhenguanyu.com/*Space_Enhanced_Config*key=*rootGroupingKey=*',
|
|
2064
|
+
'https://buff-test.zhenguanyu.com/*Space_Enhanced_Config*key=*rootGroupingKey=*',
|
|
2065
|
+
'https://ytkconan.zhenguanyu.com/*Space_Enhanced_Config*key=*rootGroupingKey=*',
|
|
2066
|
+
'Space_Enhanced_Config key rootGroupingKey namespace',
|
|
2067
|
+
],
|
|
2068
|
+
routingHint: 'exact-match-direct-run',
|
|
2069
|
+
sideEffect: 'read',
|
|
2070
|
+
requiresConfirmation: false,
|
|
2071
|
+
inputSchema: {
|
|
2072
|
+
type: 'object',
|
|
2073
|
+
properties: {
|
|
2074
|
+
action: { type: 'string' },
|
|
2075
|
+
environment: conanConfigEnvironmentProperty,
|
|
2076
|
+
url: { type: 'string' },
|
|
2077
|
+
configUrl: { type: 'string' },
|
|
2078
|
+
namespace: { type: 'string' },
|
|
2079
|
+
rootGroupingKey: { type: 'string' },
|
|
2080
|
+
grouping: { type: 'string' },
|
|
2081
|
+
key: { type: 'string' },
|
|
2082
|
+
includeRawValue: { type: 'boolean' },
|
|
2083
|
+
saveArtifacts: { type: 'boolean' },
|
|
2084
|
+
},
|
|
2085
|
+
required: ['action'],
|
|
2086
|
+
},
|
|
2087
|
+
outputSchema: {
|
|
2088
|
+
type: 'object',
|
|
2089
|
+
properties: {
|
|
2090
|
+
action: { type: 'string' },
|
|
2091
|
+
environment: { type: 'string' },
|
|
2092
|
+
namespace: { type: 'string' },
|
|
2093
|
+
key: { type: 'string' },
|
|
2094
|
+
configId: { type: 'number' },
|
|
2095
|
+
metadata: { type: 'object' },
|
|
2096
|
+
value: { type: 'object' },
|
|
2097
|
+
schema: { type: 'object' },
|
|
2098
|
+
view: { type: 'object' },
|
|
2099
|
+
artifacts: { type: 'object' },
|
|
2100
|
+
},
|
|
2101
|
+
required: ['action', 'environment', 'namespace', 'key', 'configId', 'metadata', 'value', 'schema', 'view'],
|
|
2102
|
+
},
|
|
2103
|
+
},
|
|
2104
|
+
'list-spaces': {
|
|
2105
|
+
title: '列出配置空间',
|
|
2106
|
+
description: '读取国内正式或测试环境的根配置空间及权限信息。',
|
|
2107
|
+
match: [],
|
|
2108
|
+
routingHint: 'search-first',
|
|
2109
|
+
sideEffect: 'read',
|
|
2110
|
+
requiresConfirmation: false,
|
|
2111
|
+
inputSchema: {
|
|
2112
|
+
type: 'object',
|
|
2113
|
+
properties: {
|
|
2114
|
+
action: { type: 'string' },
|
|
2115
|
+
environment: conanConfigEnvironmentProperty,
|
|
2116
|
+
},
|
|
2117
|
+
required: ['action'],
|
|
2118
|
+
},
|
|
2119
|
+
outputSchema: {
|
|
2120
|
+
type: 'object',
|
|
2121
|
+
properties: {
|
|
2122
|
+
action: { type: 'string' },
|
|
2123
|
+
environment: { type: 'string' },
|
|
2124
|
+
count: { type: 'number' },
|
|
2125
|
+
spaces: { type: 'array' },
|
|
2126
|
+
},
|
|
2127
|
+
required: ['action', 'environment', 'count', 'spaces'],
|
|
2128
|
+
},
|
|
2129
|
+
},
|
|
2130
|
+
history: {
|
|
2131
|
+
title: '查看配置历史',
|
|
2132
|
+
description: '读取版本记录,并可按 historyIds 拉取历史版本的完整配置。',
|
|
2133
|
+
match: [],
|
|
2134
|
+
routingHint: 'search-first',
|
|
2135
|
+
sideEffect: 'read',
|
|
2136
|
+
requiresConfirmation: false,
|
|
2137
|
+
inputSchema: {
|
|
2138
|
+
type: 'object',
|
|
2139
|
+
properties: {
|
|
2140
|
+
action: { type: 'string' },
|
|
2141
|
+
...conanConfigTargetInputProperties,
|
|
2142
|
+
historyIds: { type: 'array', items: { type: 'number' } },
|
|
2143
|
+
},
|
|
2144
|
+
required: ['action'],
|
|
2145
|
+
},
|
|
2146
|
+
outputSchema: {
|
|
2147
|
+
type: 'object',
|
|
2148
|
+
properties: {
|
|
2149
|
+
action: { type: 'string' },
|
|
2150
|
+
environment: { type: 'string' },
|
|
2151
|
+
namespace: { type: 'string' },
|
|
2152
|
+
key: { type: 'string' },
|
|
2153
|
+
configId: { type: 'number' },
|
|
2154
|
+
historyCount: { type: 'number' },
|
|
2155
|
+
histories: { type: 'array' },
|
|
2156
|
+
details: { type: 'array' },
|
|
2157
|
+
},
|
|
2158
|
+
required: ['action', 'environment', 'namespace', 'key', 'configId', 'historyCount', 'histories', 'details'],
|
|
2159
|
+
},
|
|
2160
|
+
},
|
|
2161
|
+
'get-schema': {
|
|
2162
|
+
title: '读取配置 Schema',
|
|
2163
|
+
description: '按 schemaId 或配置目标读取完整 Formily schema、form 和元数据。',
|
|
2164
|
+
match: [],
|
|
2165
|
+
routingHint: 'search-first',
|
|
2166
|
+
sideEffect: 'read',
|
|
2167
|
+
requiresConfirmation: false,
|
|
2168
|
+
inputSchema: {
|
|
2169
|
+
type: 'object',
|
|
2170
|
+
properties: {
|
|
2171
|
+
action: { type: 'string' },
|
|
2172
|
+
...conanConfigTargetInputProperties,
|
|
2173
|
+
schemaId: { type: 'number' },
|
|
2174
|
+
},
|
|
2175
|
+
required: ['action'],
|
|
2176
|
+
},
|
|
2177
|
+
outputSchema: {
|
|
2178
|
+
type: 'object',
|
|
2179
|
+
properties: {
|
|
2180
|
+
action: { type: 'string' },
|
|
2181
|
+
environment: { type: 'string' },
|
|
2182
|
+
schemaId: { type: 'number' },
|
|
2183
|
+
metadata: { type: ['object', 'null'] },
|
|
2184
|
+
schema: { type: 'object' },
|
|
2185
|
+
form: { type: 'object' },
|
|
2186
|
+
},
|
|
2187
|
+
required: ['action', 'environment', 'schemaId', 'metadata', 'schema', 'form'],
|
|
2188
|
+
},
|
|
2189
|
+
},
|
|
2190
|
+
'prepare-publish-draft': {
|
|
2191
|
+
title: '准备发布草稿',
|
|
2192
|
+
description: '读取当前线上值和草稿,计算哈希与语义 Diff,供 AI 校验后确认发布。',
|
|
2193
|
+
match: [],
|
|
2194
|
+
routingHint: 'search-first',
|
|
2195
|
+
sideEffect: 'read',
|
|
2196
|
+
requiresConfirmation: false,
|
|
2197
|
+
inputSchema: {
|
|
2198
|
+
type: 'object',
|
|
2199
|
+
properties: {
|
|
2200
|
+
action: { type: 'string' },
|
|
2201
|
+
...conanConfigTargetInputProperties,
|
|
2202
|
+
},
|
|
2203
|
+
required: ['action'],
|
|
2204
|
+
},
|
|
2205
|
+
outputSchema: {
|
|
2206
|
+
type: 'object',
|
|
2207
|
+
properties: {
|
|
2208
|
+
action: { type: 'string' },
|
|
2209
|
+
environment: { type: 'string' },
|
|
2210
|
+
namespace: { type: 'string' },
|
|
2211
|
+
key: { type: 'string' },
|
|
2212
|
+
configId: { type: 'number' },
|
|
2213
|
+
configDraftId: { type: 'number' },
|
|
2214
|
+
schemaId: { type: 'number' },
|
|
2215
|
+
updatedTime: { type: 'number' },
|
|
2216
|
+
sourceSha256: { type: 'string' },
|
|
2217
|
+
draftSha256: { type: 'string' },
|
|
2218
|
+
currentValue: { type: 'object' },
|
|
2219
|
+
draftValue: { type: 'object' },
|
|
2220
|
+
diff: { type: 'array' },
|
|
2221
|
+
schema: { type: 'object' },
|
|
2222
|
+
form: { type: 'object' },
|
|
2223
|
+
},
|
|
2224
|
+
required: [
|
|
2225
|
+
'action',
|
|
2226
|
+
'environment',
|
|
2227
|
+
'namespace',
|
|
2228
|
+
'key',
|
|
2229
|
+
'configId',
|
|
2230
|
+
'configDraftId',
|
|
2231
|
+
'schemaId',
|
|
2232
|
+
'updatedTime',
|
|
2233
|
+
'sourceSha256',
|
|
2234
|
+
'draftSha256',
|
|
2235
|
+
'currentValue',
|
|
2236
|
+
'draftValue',
|
|
2237
|
+
'diff',
|
|
2238
|
+
'schema',
|
|
2239
|
+
'form',
|
|
2240
|
+
],
|
|
2241
|
+
},
|
|
2242
|
+
},
|
|
2243
|
+
'prepare-create': {
|
|
2244
|
+
title: '准备新建配置',
|
|
2245
|
+
description: '校验目标分组、key、名称、描述和 Schema,生成不可变的新建预览。',
|
|
2246
|
+
match: [],
|
|
2247
|
+
routingHint: 'search-first',
|
|
2248
|
+
sideEffect: 'read',
|
|
2249
|
+
requiresConfirmation: false,
|
|
2250
|
+
inputSchema: {
|
|
2251
|
+
type: 'object',
|
|
2252
|
+
properties: {
|
|
2253
|
+
action: { type: 'string' },
|
|
2254
|
+
...conanConfigDefinitionInputProperties,
|
|
2255
|
+
},
|
|
2256
|
+
required: ['action', 'groupingId', 'key', 'name', 'desc', 'schemaId'],
|
|
2257
|
+
},
|
|
2258
|
+
outputSchema: {
|
|
2259
|
+
type: 'object',
|
|
2260
|
+
properties: {
|
|
2261
|
+
action: { type: 'string' },
|
|
2262
|
+
environment: { type: 'string' },
|
|
2263
|
+
definition: { type: 'object' },
|
|
2264
|
+
groupingUpdatedTime: { type: ['number', 'null'] },
|
|
2265
|
+
targetSha256: { type: 'string' },
|
|
2266
|
+
schema: { type: 'object' },
|
|
2267
|
+
form: { type: 'object' },
|
|
2268
|
+
warnings: { type: 'array' },
|
|
2269
|
+
},
|
|
2270
|
+
required: [
|
|
2271
|
+
'action',
|
|
2272
|
+
'environment',
|
|
2273
|
+
'definition',
|
|
2274
|
+
'groupingUpdatedTime',
|
|
2275
|
+
'targetSha256',
|
|
2276
|
+
'schema',
|
|
2277
|
+
'form',
|
|
2278
|
+
'warnings',
|
|
2279
|
+
],
|
|
2280
|
+
},
|
|
2281
|
+
},
|
|
2282
|
+
'prepare-copy': {
|
|
2283
|
+
title: '准备复制配置定义',
|
|
2284
|
+
description: '读取源配置并校验目标分组和 key;与 B 端一致,仅复制元数据和 Schema,不复制内容值。',
|
|
2285
|
+
match: [],
|
|
2286
|
+
routingHint: 'search-first',
|
|
2287
|
+
sideEffect: 'read',
|
|
2288
|
+
requiresConfirmation: false,
|
|
2289
|
+
inputSchema: {
|
|
2290
|
+
type: 'object',
|
|
2291
|
+
properties: {
|
|
2292
|
+
action: { type: 'string' },
|
|
2293
|
+
environment: conanConfigEnvironmentProperty,
|
|
2294
|
+
sourceUrl: { type: 'string' },
|
|
2295
|
+
sourceNamespace: { type: 'string' },
|
|
2296
|
+
sourceKey: { type: 'string' },
|
|
2297
|
+
groupingId: { type: 'number' },
|
|
2298
|
+
targetKey: { type: 'string' },
|
|
2299
|
+
targetName: { type: 'string' },
|
|
2300
|
+
targetDesc: { type: 'string' },
|
|
2301
|
+
},
|
|
2302
|
+
required: ['action', 'groupingId', 'targetKey'],
|
|
2303
|
+
},
|
|
2304
|
+
outputSchema: {
|
|
2305
|
+
type: 'object',
|
|
2306
|
+
properties: {
|
|
2307
|
+
action: { type: 'string' },
|
|
2308
|
+
environment: { type: 'string' },
|
|
2309
|
+
source: { type: 'object' },
|
|
2310
|
+
target: { type: 'object' },
|
|
2311
|
+
sourceUpdatedTime: { type: 'number' },
|
|
2312
|
+
sourceDefinitionSha256: { type: 'string' },
|
|
2313
|
+
groupingUpdatedTime: { type: ['number', 'null'] },
|
|
2314
|
+
targetSha256: { type: 'string' },
|
|
2315
|
+
schema: { type: 'object' },
|
|
2316
|
+
form: { type: 'object' },
|
|
2317
|
+
contentCopied: { type: 'boolean' },
|
|
2318
|
+
},
|
|
2319
|
+
required: [
|
|
2320
|
+
'action',
|
|
2321
|
+
'environment',
|
|
2322
|
+
'source',
|
|
2323
|
+
'target',
|
|
2324
|
+
'sourceUpdatedTime',
|
|
2325
|
+
'sourceDefinitionSha256',
|
|
2326
|
+
'groupingUpdatedTime',
|
|
2327
|
+
'targetSha256',
|
|
2328
|
+
'schema',
|
|
2329
|
+
'form',
|
|
2330
|
+
'contentCopied',
|
|
2331
|
+
],
|
|
2332
|
+
},
|
|
2333
|
+
},
|
|
2334
|
+
'prepare-rollback': {
|
|
2335
|
+
title: '准备回滚历史版本',
|
|
2336
|
+
description: '读取指定历史版本,计算当前值与历史值的哈希和语义 Diff,供 AI 校验。',
|
|
2337
|
+
match: [],
|
|
2338
|
+
routingHint: 'search-first',
|
|
2339
|
+
sideEffect: 'read',
|
|
2340
|
+
requiresConfirmation: false,
|
|
2341
|
+
inputSchema: {
|
|
2342
|
+
type: 'object',
|
|
2343
|
+
properties: {
|
|
2344
|
+
action: { type: 'string' },
|
|
2345
|
+
...conanConfigTargetInputProperties,
|
|
2346
|
+
historyId: { type: 'number' },
|
|
2347
|
+
},
|
|
2348
|
+
required: ['action', 'historyId'],
|
|
2349
|
+
},
|
|
2350
|
+
outputSchema: {
|
|
2351
|
+
type: 'object',
|
|
2352
|
+
properties: {
|
|
2353
|
+
action: { type: 'string' },
|
|
2354
|
+
environment: { type: 'string' },
|
|
2355
|
+
namespace: { type: 'string' },
|
|
2356
|
+
key: { type: 'string' },
|
|
2357
|
+
configId: { type: 'number' },
|
|
2358
|
+
historyId: { type: 'number' },
|
|
2359
|
+
hasDraft: { type: 'boolean' },
|
|
2360
|
+
schemaId: { type: 'number' },
|
|
2361
|
+
updatedTime: { type: 'number' },
|
|
2362
|
+
sourceSha256: { type: 'string' },
|
|
2363
|
+
historicalSha256: { type: 'string' },
|
|
2364
|
+
currentValue: { type: 'object' },
|
|
2365
|
+
historicalValue: { type: 'object' },
|
|
2366
|
+
diff: { type: 'array' },
|
|
2367
|
+
schema: { type: 'object' },
|
|
2368
|
+
form: { type: 'object' },
|
|
2369
|
+
},
|
|
2370
|
+
required: [
|
|
2371
|
+
'action',
|
|
2372
|
+
'environment',
|
|
2373
|
+
'namespace',
|
|
2374
|
+
'key',
|
|
2375
|
+
'configId',
|
|
2376
|
+
'historyId',
|
|
2377
|
+
'hasDraft',
|
|
2378
|
+
'schemaId',
|
|
2379
|
+
'updatedTime',
|
|
2380
|
+
'sourceSha256',
|
|
2381
|
+
'historicalSha256',
|
|
2382
|
+
'currentValue',
|
|
2383
|
+
'historicalValue',
|
|
2384
|
+
'diff',
|
|
2385
|
+
'schema',
|
|
2386
|
+
'form',
|
|
2387
|
+
],
|
|
2388
|
+
},
|
|
2389
|
+
},
|
|
2390
|
+
'prepare-change': {
|
|
2391
|
+
title: '准备并校验修改',
|
|
2392
|
+
description: '读取当前值和完整 Formily schema,计算源值/目标值哈希与语义 Diff,供 AI 生成校验报告。',
|
|
2393
|
+
match: [],
|
|
2394
|
+
routingHint: 'search-first',
|
|
2395
|
+
sideEffect: 'read',
|
|
2396
|
+
requiresConfirmation: false,
|
|
2397
|
+
inputSchema: {
|
|
2398
|
+
type: 'object',
|
|
2399
|
+
properties: {
|
|
2400
|
+
action: { type: 'string' },
|
|
2401
|
+
environment: conanConfigEnvironmentProperty,
|
|
2402
|
+
url: { type: 'string' },
|
|
2403
|
+
namespace: { type: 'string' },
|
|
2404
|
+
rootGroupingKey: { type: 'string' },
|
|
2405
|
+
key: { type: 'string' },
|
|
2406
|
+
value: { type: 'object' },
|
|
2407
|
+
changeSummary: { type: 'string' },
|
|
2408
|
+
},
|
|
2409
|
+
required: ['action'],
|
|
2410
|
+
},
|
|
2411
|
+
outputSchema: {
|
|
2412
|
+
type: 'object',
|
|
2413
|
+
properties: {
|
|
2414
|
+
action: { type: 'string' },
|
|
2415
|
+
environment: { type: 'string' },
|
|
2416
|
+
namespace: { type: 'string' },
|
|
2417
|
+
key: { type: 'string' },
|
|
2418
|
+
schemaId: { type: 'number' },
|
|
2419
|
+
updatedTime: { type: 'number' },
|
|
2420
|
+
sourceSha256: { type: 'string' },
|
|
2421
|
+
currentValue: { type: 'object' },
|
|
2422
|
+
schema: { type: 'object' },
|
|
2423
|
+
diff: { type: 'array' },
|
|
2424
|
+
},
|
|
2425
|
+
required: [
|
|
2426
|
+
'action',
|
|
2427
|
+
'environment',
|
|
2428
|
+
'namespace',
|
|
2429
|
+
'key',
|
|
2430
|
+
'schemaId',
|
|
2431
|
+
'updatedTime',
|
|
2432
|
+
'sourceSha256',
|
|
2433
|
+
'currentValue',
|
|
2434
|
+
'schema',
|
|
2435
|
+
'diff',
|
|
2436
|
+
],
|
|
2437
|
+
},
|
|
2438
|
+
},
|
|
2439
|
+
'apply-change': {
|
|
2440
|
+
title: '发布修改',
|
|
2441
|
+
description: '校验 AI 报告、目标哈希、源值和版本后更新发布,并回读验证。',
|
|
2442
|
+
match: [],
|
|
2443
|
+
routingHint: 'search-first',
|
|
2444
|
+
sideEffect: 'write',
|
|
2445
|
+
requiresConfirmation: true,
|
|
2446
|
+
inputSchema: {
|
|
2447
|
+
type: 'object',
|
|
2448
|
+
properties: {
|
|
2449
|
+
action: { type: 'string' },
|
|
2450
|
+
environment: conanConfigEnvironmentProperty,
|
|
2451
|
+
url: { type: 'string' },
|
|
2452
|
+
namespace: { type: 'string' },
|
|
2453
|
+
rootGroupingKey: { type: 'string' },
|
|
2454
|
+
key: { type: 'string' },
|
|
2455
|
+
value: { type: 'object' },
|
|
2456
|
+
changeSummary: { type: 'string' },
|
|
2457
|
+
validationReport: { type: 'object' },
|
|
2458
|
+
},
|
|
2459
|
+
required: ['action', 'value', 'changeSummary', 'validationReport'],
|
|
2460
|
+
},
|
|
2461
|
+
outputSchema: {
|
|
2462
|
+
type: 'object',
|
|
2463
|
+
properties: {
|
|
2464
|
+
status: { type: 'string' },
|
|
2465
|
+
environment: { type: 'string' },
|
|
2466
|
+
validationErrors: { type: 'array' },
|
|
2467
|
+
submitted: { type: 'boolean' },
|
|
2468
|
+
},
|
|
2469
|
+
required: ['status', 'environment', 'validationErrors', 'submitted'],
|
|
2470
|
+
},
|
|
2471
|
+
},
|
|
2472
|
+
'save-draft': {
|
|
2473
|
+
title: '保存配置草稿',
|
|
2474
|
+
description: '校验目标值、源值哈希和版本后保存草稿,不发布;已有草稿时拒绝覆盖。',
|
|
2475
|
+
match: [],
|
|
2476
|
+
routingHint: 'search-first',
|
|
2477
|
+
sideEffect: 'write',
|
|
2478
|
+
requiresConfirmation: true,
|
|
2479
|
+
inputSchema: {
|
|
2480
|
+
type: 'object',
|
|
2481
|
+
properties: {
|
|
2482
|
+
action: { type: 'string' },
|
|
2483
|
+
...conanConfigTargetInputProperties,
|
|
2484
|
+
value: { type: 'object' },
|
|
2485
|
+
changeSummary: { type: 'string' },
|
|
2486
|
+
validationReport: { type: 'object' },
|
|
2487
|
+
},
|
|
2488
|
+
required: ['action', 'value', 'changeSummary', 'validationReport'],
|
|
2489
|
+
},
|
|
2490
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2491
|
+
},
|
|
2492
|
+
'publish-draft': {
|
|
2493
|
+
title: '发布已有草稿',
|
|
2494
|
+
description: '校验草稿 ID、内容哈希、线上源值和 Schema 后发布,并回读验证。',
|
|
2495
|
+
match: [],
|
|
2496
|
+
routingHint: 'search-first',
|
|
2497
|
+
sideEffect: 'write',
|
|
2498
|
+
requiresConfirmation: true,
|
|
2499
|
+
inputSchema: {
|
|
2500
|
+
type: 'object',
|
|
2501
|
+
properties: {
|
|
2502
|
+
action: { type: 'string' },
|
|
2503
|
+
...conanConfigTargetInputProperties,
|
|
2504
|
+
validationReport: { type: 'object' },
|
|
2505
|
+
},
|
|
2506
|
+
required: ['action', 'validationReport'],
|
|
2507
|
+
},
|
|
2508
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2509
|
+
},
|
|
2510
|
+
'discard-draft': {
|
|
2511
|
+
title: '丢弃已有草稿',
|
|
2512
|
+
description: '核对草稿 ID 和内容哈希后丢弃草稿,并确认草稿已消失。',
|
|
2513
|
+
match: [],
|
|
2514
|
+
routingHint: 'search-first',
|
|
2515
|
+
sideEffect: 'write',
|
|
2516
|
+
requiresConfirmation: true,
|
|
2517
|
+
inputSchema: {
|
|
2518
|
+
type: 'object',
|
|
2519
|
+
properties: {
|
|
2520
|
+
action: { type: 'string' },
|
|
2521
|
+
...conanConfigTargetInputProperties,
|
|
2522
|
+
configDraftId: { type: 'number' },
|
|
2523
|
+
draftSha256: { type: 'string' },
|
|
2524
|
+
},
|
|
2525
|
+
required: ['action', 'configDraftId', 'draftSha256'],
|
|
2526
|
+
},
|
|
2527
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2528
|
+
},
|
|
2529
|
+
create: {
|
|
2530
|
+
title: '新建配置',
|
|
2531
|
+
description: '复核 prepare-create 的目标定义和分组版本后新建配置,并回读验证。',
|
|
2532
|
+
match: [],
|
|
2533
|
+
routingHint: 'search-first',
|
|
2534
|
+
sideEffect: 'write',
|
|
2535
|
+
requiresConfirmation: true,
|
|
2536
|
+
inputSchema: {
|
|
2537
|
+
type: 'object',
|
|
2538
|
+
properties: {
|
|
2539
|
+
action: { type: 'string' },
|
|
2540
|
+
...conanConfigDefinitionInputProperties,
|
|
2541
|
+
preparationReport: { type: 'object' },
|
|
2542
|
+
},
|
|
2543
|
+
required: ['action', 'groupingId', 'key', 'name', 'desc', 'schemaId', 'preparationReport'],
|
|
2544
|
+
},
|
|
2545
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2546
|
+
},
|
|
2547
|
+
copy: {
|
|
2548
|
+
title: '复制配置定义',
|
|
2549
|
+
description: '复核源配置和目标定义后新建配置,沿用源 Schema;不复制内容值。',
|
|
2550
|
+
match: [],
|
|
2551
|
+
routingHint: 'search-first',
|
|
2552
|
+
sideEffect: 'write',
|
|
2553
|
+
requiresConfirmation: true,
|
|
2554
|
+
inputSchema: {
|
|
2555
|
+
type: 'object',
|
|
2556
|
+
properties: {
|
|
2557
|
+
action: { type: 'string' },
|
|
2558
|
+
environment: conanConfigEnvironmentProperty,
|
|
2559
|
+
sourceUrl: { type: 'string' },
|
|
2560
|
+
sourceNamespace: { type: 'string' },
|
|
2561
|
+
sourceKey: { type: 'string' },
|
|
2562
|
+
groupingId: { type: 'number' },
|
|
2563
|
+
targetKey: { type: 'string' },
|
|
2564
|
+
targetName: { type: 'string' },
|
|
2565
|
+
targetDesc: { type: 'string' },
|
|
2566
|
+
preparationReport: { type: 'object' },
|
|
2567
|
+
},
|
|
2568
|
+
required: ['action', 'groupingId', 'targetKey', 'preparationReport'],
|
|
2569
|
+
},
|
|
2570
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2571
|
+
},
|
|
2572
|
+
rollback: {
|
|
2573
|
+
title: '回滚历史版本',
|
|
2574
|
+
description: '复核历史版本、当前源值、Schema 和版本后创建回滚草稿、发布并验证。',
|
|
2575
|
+
match: [],
|
|
2576
|
+
routingHint: 'search-first',
|
|
2577
|
+
sideEffect: 'write',
|
|
2578
|
+
requiresConfirmation: true,
|
|
2579
|
+
inputSchema: {
|
|
2580
|
+
type: 'object',
|
|
2581
|
+
properties: {
|
|
2582
|
+
action: { type: 'string' },
|
|
2583
|
+
...conanConfigTargetInputProperties,
|
|
2584
|
+
historyId: { type: 'number' },
|
|
2585
|
+
validationReport: { type: 'object' },
|
|
2586
|
+
},
|
|
2587
|
+
required: ['action', 'historyId', 'validationReport'],
|
|
2588
|
+
},
|
|
2589
|
+
outputSchema: conanConfigWriteOutputSchema,
|
|
2590
|
+
},
|
|
2591
|
+
},
|
|
2592
|
+
examples: [
|
|
2593
|
+
{
|
|
2594
|
+
description: '用户给 Space_Enhanced_Config 后台链接时,直接解析 URL 并查询配置。',
|
|
2595
|
+
input: {
|
|
2596
|
+
action: 'get',
|
|
2597
|
+
url: 'https://buff.zhenguanyu.com/buff-army/#/buff-oversea-designer/Space_Enhanced_Config?key=wareLandingPageSendCouponConfig&rootGroupingKey=Space_Pedia',
|
|
2598
|
+
},
|
|
2599
|
+
},
|
|
2600
|
+
{
|
|
2601
|
+
description: '搜索会员订单相关配置',
|
|
2602
|
+
input: { action: 'search', query: '会员订单' },
|
|
2603
|
+
},
|
|
2604
|
+
],
|
|
2605
|
+
},
|
|
2606
|
+
},
|
|
2607
|
+
]
|
|
2608
|
+
|
|
2609
|
+
const conanConfigAgentSkillDefinitions: BuiltinAgentSkillDefinition[] = [
|
|
2610
|
+
{
|
|
2611
|
+
target: 'codex',
|
|
2612
|
+
name: 'conan-config',
|
|
2613
|
+
files: [
|
|
2614
|
+
{
|
|
2615
|
+
relativePath: 'SKILL.md',
|
|
2616
|
+
bundledPath: 'agent-skills/conan-config/SKILL.md',
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
relativePath: 'agents/openai.yaml',
|
|
2620
|
+
bundledPath: 'agent-skills/conan-config/agents/openai.yaml',
|
|
2621
|
+
},
|
|
2622
|
+
],
|
|
2623
|
+
},
|
|
2624
|
+
]
|
|
2625
|
+
|
|
2626
|
+
export function installBuiltinCapabilitySuite(
|
|
2627
|
+
options: InstallBuiltinCapabilitySuiteOptions,
|
|
2628
|
+
): InstalledBuiltinCapabilitySuite {
|
|
2629
|
+
if (!isBuiltinCapabilitySuite(options.suite)) {
|
|
2630
|
+
throw new Error(`Unknown builtin capability suite: ${options.suite}`)
|
|
2631
|
+
}
|
|
2632
|
+
const location = options.location || 'user'
|
|
2633
|
+
const capabilities = conanConfigCapabilityDefinitions.map((definition) => {
|
|
2634
|
+
if (options.overwrite) {
|
|
2635
|
+
removeCapabilityAuthFiles({
|
|
2636
|
+
capabilityDir: getCapabilityDir({ id: definition.id, location, cwd: options.cwd }),
|
|
2637
|
+
})
|
|
2638
|
+
}
|
|
2639
|
+
createCapability({
|
|
2640
|
+
id: definition.id,
|
|
2641
|
+
title: definition.title,
|
|
2642
|
+
description: definition.description,
|
|
2643
|
+
location,
|
|
2644
|
+
cwd: options.cwd,
|
|
2645
|
+
overwrite: options.overwrite,
|
|
2646
|
+
createdBy: 'ai',
|
|
2647
|
+
runtime: 'node',
|
|
2648
|
+
})
|
|
2649
|
+
updateCapabilityScript({
|
|
2650
|
+
id: definition.id,
|
|
2651
|
+
cwd: options.cwd,
|
|
2652
|
+
source: definition.script.trimStart(),
|
|
2653
|
+
})
|
|
2654
|
+
const capability = updateCapabilityManifest({
|
|
2655
|
+
id: definition.id,
|
|
2656
|
+
cwd: options.cwd,
|
|
2657
|
+
allowUnvalidatedTrust: options.trust !== false,
|
|
2658
|
+
patch: {
|
|
2659
|
+
...definition.manifest,
|
|
2660
|
+
status: options.trust === false ? 'draft' : 'trusted',
|
|
2661
|
+
},
|
|
2662
|
+
})
|
|
2663
|
+
const script = readCapabilityScript({ id: definition.id, cwd: options.cwd })
|
|
2664
|
+
if (!script.includes('CONFIG_API_ORIGIN')) {
|
|
2665
|
+
throw new Error(`Failed to install builtin capability script: ${definition.id}`)
|
|
2666
|
+
}
|
|
2667
|
+
return capability
|
|
2668
|
+
})
|
|
2669
|
+
const agentSkills =
|
|
2670
|
+
options.installAgentSkills === false
|
|
2671
|
+
? []
|
|
2672
|
+
: installBuiltinAgentSkills({
|
|
2673
|
+
definitions: conanConfigAgentSkillDefinitions,
|
|
2674
|
+
overwrite: options.overwrite,
|
|
2675
|
+
codexHome: options.codexHome,
|
|
2676
|
+
})
|
|
2677
|
+
return { suite: options.suite, capabilities, agentSkills }
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function installBuiltinAgentSkills(options: {
|
|
2681
|
+
definitions: BuiltinAgentSkillDefinition[]
|
|
2682
|
+
overwrite?: boolean
|
|
2683
|
+
codexHome?: string
|
|
2684
|
+
}): InstalledBuiltinAgentSkill[] {
|
|
2685
|
+
return options.definitions.map((definition) => {
|
|
2686
|
+
if (definition.target !== 'codex') {
|
|
2687
|
+
throw new Error(`Unsupported builtin agent skill target: ${definition.target}`)
|
|
2688
|
+
}
|
|
2689
|
+
return installCodexAgentSkill({
|
|
2690
|
+
definition,
|
|
2691
|
+
overwrite: options.overwrite,
|
|
2692
|
+
codexHome: options.codexHome,
|
|
2693
|
+
})
|
|
2694
|
+
})
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function installCodexAgentSkill(options: {
|
|
2698
|
+
definition: BuiltinAgentSkillDefinition
|
|
2699
|
+
overwrite?: boolean
|
|
2700
|
+
codexHome?: string
|
|
2701
|
+
}): InstalledBuiltinAgentSkill {
|
|
2702
|
+
const codexHome = options.codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
|
|
2703
|
+
const dir = path.join(codexHome, 'skills', options.definition.name)
|
|
2704
|
+
const files = options.definition.files.map((file) => {
|
|
2705
|
+
return installCodexAgentSkillFile({
|
|
2706
|
+
skillDir: dir,
|
|
2707
|
+
file,
|
|
2708
|
+
overwrite: options.overwrite,
|
|
2709
|
+
})
|
|
2710
|
+
})
|
|
2711
|
+
return {
|
|
2712
|
+
target: 'codex',
|
|
2713
|
+
name: options.definition.name,
|
|
2714
|
+
dir,
|
|
2715
|
+
files,
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function installCodexAgentSkillFile(options: {
|
|
2720
|
+
skillDir: string
|
|
2721
|
+
file: BuiltinAgentSkillFileDefinition
|
|
2722
|
+
overwrite?: boolean
|
|
2723
|
+
}): InstalledBuiltinAgentSkillFile {
|
|
2724
|
+
const targetPath = resolveInsideDirectory({
|
|
2725
|
+
root: options.skillDir,
|
|
2726
|
+
relativePath: options.file.relativePath,
|
|
2727
|
+
})
|
|
2728
|
+
const content = readBundledAgentSkillFile(options.file.bundledPath)
|
|
2729
|
+
if (fs.existsSync(targetPath)) {
|
|
2730
|
+
const existing = fs.readFileSync(targetPath, 'utf-8')
|
|
2731
|
+
if (existing === content) {
|
|
2732
|
+
return { path: targetPath, status: 'unchanged' }
|
|
2733
|
+
}
|
|
2734
|
+
if (!options.overwrite) {
|
|
2735
|
+
throw new Error(
|
|
2736
|
+
`Agent skill file already exists with different content: ${targetPath}. Use --force to overwrite.`,
|
|
2737
|
+
)
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true })
|
|
2741
|
+
const status = fs.existsSync(targetPath) ? 'updated' : 'created'
|
|
2742
|
+
fs.writeFileSync(targetPath, content)
|
|
2743
|
+
return { path: targetPath, status }
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function readBundledAgentSkillFile(relativePath: string): string {
|
|
2747
|
+
const moduleDir = path.dirname(nodeUrl.fileURLToPath(import.meta.url))
|
|
2748
|
+
const candidates = [path.join(moduleDir, relativePath), path.join(moduleDir, '..', 'src', relativePath)]
|
|
2749
|
+
const sourcePath = candidates.find((candidate) => {
|
|
2750
|
+
return fs.existsSync(candidate)
|
|
2751
|
+
})
|
|
2752
|
+
if (!sourcePath) {
|
|
2753
|
+
throw new Error(`Bundled agent skill file not found: ${relativePath}`)
|
|
2754
|
+
}
|
|
2755
|
+
return fs.readFileSync(sourcePath, 'utf-8')
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
function resolveInsideDirectory(options: { root: string; relativePath: string }): string {
|
|
2759
|
+
const root = path.resolve(options.root)
|
|
2760
|
+
const targetPath = path.resolve(root, ...options.relativePath.split('/'))
|
|
2761
|
+
if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`)) {
|
|
2762
|
+
throw new Error(`Agent skill file must stay inside skill directory: ${options.relativePath}`)
|
|
2763
|
+
}
|
|
2764
|
+
return targetPath
|
|
2765
|
+
}
|