@xyit/wellspring 0.0.0-stage → 1.1.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/README.md +853 -2
- package/dist/agent/pending-asks.d.ts +20 -0
- package/dist/agent/pending-asks.js +70 -0
- package/dist/agent/pending-asks.js.map +1 -0
- package/dist/agent/task-registry.d.ts +14 -0
- package/dist/agent/task-registry.js +80 -0
- package/dist/agent/task-registry.js.map +1 -0
- package/dist/agent/tool-context.d.ts +30 -0
- package/dist/agent/tool-context.js +10 -0
- package/dist/agent/tool-context.js.map +1 -0
- package/dist/agent/usage.d.ts +40 -0
- package/dist/agent/usage.js +122 -0
- package/dist/agent/usage.js.map +1 -0
- package/dist/api/host/agent.d.ts +165 -0
- package/dist/api/host/agent.js +5 -0
- package/dist/api/host/agent.js.map +1 -0
- package/dist/api/host/events.d.ts +2 -0
- package/dist/api/host/events.js +6 -0
- package/dist/api/host/events.js.map +1 -0
- package/dist/api/host/exec.d.ts +36 -0
- package/dist/api/host/exec.js +10 -0
- package/dist/api/host/exec.js.map +1 -0
- package/dist/api/host/fs.d.ts +24 -0
- package/dist/api/host/fs.js +7 -0
- package/dist/api/host/fs.js.map +1 -0
- package/dist/api/host/hooks.d.ts +210 -0
- package/dist/api/host/hooks.js +401 -0
- package/dist/api/host/hooks.js.map +1 -0
- package/dist/api/host/host.d.ts +36 -0
- package/dist/api/host/host.js +53 -0
- package/dist/api/host/host.js.map +1 -0
- package/dist/api/host/http.d.ts +58 -0
- package/dist/api/host/http.js +6 -0
- package/dist/api/host/http.js.map +1 -0
- package/dist/api/host/index.d.ts +15 -0
- package/dist/api/host/index.js +20 -0
- package/dist/api/host/index.js.map +1 -0
- package/dist/api/host/models.d.ts +65 -0
- package/dist/api/host/models.js +8 -0
- package/dist/api/host/models.js.map +1 -0
- package/dist/api/host/network.d.ts +22 -0
- package/dist/api/host/network.js +7 -0
- package/dist/api/host/network.js.map +1 -0
- package/dist/api/host/plugin.d.ts +138 -0
- package/dist/api/host/plugin.js +6 -0
- package/dist/api/host/plugin.js.map +1 -0
- package/dist/api/host/plugins.d.ts +49 -0
- package/dist/api/host/plugins.js +6 -0
- package/dist/api/host/plugins.js.map +1 -0
- package/dist/api/host/pty.d.ts +55 -0
- package/dist/api/host/pty.js +21 -0
- package/dist/api/host/pty.js.map +1 -0
- package/dist/api/host/session.d.ts +5 -0
- package/dist/api/host/session.js +52 -0
- package/dist/api/host/session.js.map +1 -0
- package/dist/api/host/version.d.ts +2 -0
- package/dist/api/host/version.js +3 -0
- package/dist/api/host/version.js.map +1 -0
- package/dist/api/host/workspace.d.ts +38 -0
- package/dist/api/host/workspace.js +9 -0
- package/dist/api/host/workspace.js.map +1 -0
- package/dist/api/plugin/http-client.d.ts +42 -0
- package/dist/api/plugin/http-client.js +364 -0
- package/dist/api/plugin/http-client.js.map +1 -0
- package/dist/api/plugin/http-protocol.d.ts +152 -0
- package/dist/api/plugin/http-protocol.js +49 -0
- package/dist/api/plugin/http-protocol.js.map +1 -0
- package/dist/api/result/result.d.ts +16 -0
- package/dist/api/result/result.js +43 -0
- package/dist/api/result/result.js.map +1 -0
- package/dist/commands/agent-turn.d.ts +63 -0
- package/dist/commands/agent-turn.js +687 -0
- package/dist/commands/agent-turn.js.map +1 -0
- package/dist/commands/ask-prompt.d.ts +6 -0
- package/dist/commands/ask-prompt.js +49 -0
- package/dist/commands/ask-prompt.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +283 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/conversation.d.ts +51 -0
- package/dist/commands/conversation.js +630 -0
- package/dist/commands/conversation.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +77 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/exec-approval.d.ts +3 -0
- package/dist/commands/exec-approval.js +80 -0
- package/dist/commands/exec-approval.js.map +1 -0
- package/dist/commands/help.d.ts +2 -0
- package/dist/commands/help.js +35 -0
- package/dist/commands/help.js.map +1 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +226 -0
- package/dist/commands/key.js.map +1 -0
- package/dist/commands/plugin.d.ts +2 -0
- package/dist/commands/plugin.js +190 -0
- package/dist/commands/plugin.js.map +1 -0
- package/dist/commands/session.d.ts +2 -0
- package/dist/commands/session.js +167 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/shared.d.ts +35 -0
- package/dist/commands/shared.js +66 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +75 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/stop.d.ts +2 -0
- package/dist/commands/stop.js +24 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +33 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/config/codes.d.ts +13 -0
- package/dist/config/codes.js +15 -0
- package/dist/config/codes.js.map +1 -0
- package/dist/config/credentials.d.ts +67 -0
- package/dist/config/credentials.js +214 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/config/defaults.d.ts +5 -0
- package/dist/config/defaults.js +31 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/index.js +10 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.js +68 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/providers.d.ts +30 -0
- package/dist/config/providers.js +97 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/types.d.ts +75 -0
- package/dist/config/types.js +4 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validate.d.ts +4 -0
- package/dist/config/validate.js +391 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/display/markdown.d.ts +7 -0
- package/dist/display/markdown.js +116 -0
- package/dist/display/markdown.js.map +1 -0
- package/dist/display/tool-label.d.ts +6 -0
- package/dist/display/tool-label.js +81 -0
- package/dist/display/tool-label.js.map +1 -0
- package/dist/event/event.d.ts +27 -0
- package/dist/event/event.js +152 -0
- package/dist/event/event.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +308 -0
- package/dist/index.js.map +1 -0
- package/dist/input/codes.d.ts +15 -0
- package/dist/input/codes.js +20 -0
- package/dist/input/codes.js.map +1 -0
- package/dist/input/commands.d.ts +10 -0
- package/dist/input/commands.js +65 -0
- package/dist/input/commands.js.map +1 -0
- package/dist/input/help.d.ts +8 -0
- package/dist/input/help.js +51 -0
- package/dist/input/help.js.map +1 -0
- package/dist/input/index.d.ts +6 -0
- package/dist/input/index.js +10 -0
- package/dist/input/index.js.map +1 -0
- package/dist/input/parse-options.d.ts +11 -0
- package/dist/input/parse-options.js +51 -0
- package/dist/input/parse-options.js.map +1 -0
- package/dist/input/parse.d.ts +3 -0
- package/dist/input/parse.js +119 -0
- package/dist/input/parse.js.map +1 -0
- package/dist/input/types.d.ts +17 -0
- package/dist/input/types.js +2 -0
- package/dist/input/types.js.map +1 -0
- package/dist/logger/elog4js.d.ts +65 -0
- package/dist/logger/elog4js.js +305 -0
- package/dist/logger/elog4js.js.map +1 -0
- package/dist/logger/logger.d.ts +37 -0
- package/dist/logger/logger.js +196 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/network/index.d.ts +2 -0
- package/dist/network/index.js +6 -0
- package/dist/network/index.js.map +1 -0
- package/dist/network/openai.d.ts +117 -0
- package/dist/network/openai.js +443 -0
- package/dist/network/openai.js.map +1 -0
- package/dist/network/proxy.d.ts +9 -0
- package/dist/network/proxy.js +72 -0
- package/dist/network/proxy.js.map +1 -0
- package/dist/network/web.d.ts +34 -0
- package/dist/network/web.js +152 -0
- package/dist/network/web.js.map +1 -0
- package/dist/platform/clipboard.d.ts +15 -0
- package/dist/platform/clipboard.js +82 -0
- package/dist/platform/clipboard.js.map +1 -0
- package/dist/platform/exec.d.ts +28 -0
- package/dist/platform/exec.js +95 -0
- package/dist/platform/exec.js.map +1 -0
- package/dist/platform/file.d.ts +38 -0
- package/dist/platform/file.js +197 -0
- package/dist/platform/file.js.map +1 -0
- package/dist/platform/index.d.ts +7 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/notification.d.ts +14 -0
- package/dist/platform/notification.js +82 -0
- package/dist/platform/notification.js.map +1 -0
- package/dist/platform/open-external.d.ts +10 -0
- package/dist/platform/open-external.js +105 -0
- package/dist/platform/open-external.js.map +1 -0
- package/dist/platform/os.d.ts +20 -0
- package/dist/platform/os.js +86 -0
- package/dist/platform/os.js.map +1 -0
- package/dist/platform/path.d.ts +32 -0
- package/dist/platform/path.js +154 -0
- package/dist/platform/path.js.map +1 -0
- package/dist/plugin/access-guard.d.ts +1 -0
- package/dist/plugin/access-guard.js +40 -0
- package/dist/plugin/access-guard.js.map +1 -0
- package/dist/plugin/agent-service.d.ts +25 -0
- package/dist/plugin/agent-service.js +809 -0
- package/dist/plugin/agent-service.js.map +1 -0
- package/dist/plugin/agent-tools.d.ts +12 -0
- package/dist/plugin/agent-tools.js +31 -0
- package/dist/plugin/agent-tools.js.map +1 -0
- package/dist/plugin/capabilities.d.ts +26 -0
- package/dist/plugin/capabilities.js +192 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/console-guard.d.ts +2 -0
- package/dist/plugin/console-guard.js +52 -0
- package/dist/plugin/console-guard.js.map +1 -0
- package/dist/plugin/data-access.d.ts +15 -0
- package/dist/plugin/data-access.js +57 -0
- package/dist/plugin/data-access.js.map +1 -0
- package/dist/plugin/fs.d.ts +3 -0
- package/dist/plugin/fs.js +93 -0
- package/dist/plugin/fs.js.map +1 -0
- package/dist/plugin/host-alias.d.ts +2 -0
- package/dist/plugin/host-alias.js +57 -0
- package/dist/plugin/host-alias.js.map +1 -0
- package/dist/plugin/index.d.ts +7 -0
- package/dist/plugin/index.js +11 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/install.d.ts +17 -0
- package/dist/plugin/install.js +163 -0
- package/dist/plugin/install.js.map +1 -0
- package/dist/plugin/loader.d.ts +45 -0
- package/dist/plugin/loader.js +739 -0
- package/dist/plugin/loader.js.map +1 -0
- package/dist/plugin/manage.d.ts +31 -0
- package/dist/plugin/manage.js +271 -0
- package/dist/plugin/manage.js.map +1 -0
- package/dist/plugin/models-service.d.ts +15 -0
- package/dist/plugin/models-service.js +317 -0
- package/dist/plugin/models-service.js.map +1 -0
- package/dist/plugin/network-service.d.ts +10 -0
- package/dist/plugin/network-service.js +87 -0
- package/dist/plugin/network-service.js.map +1 -0
- package/dist/plugin/package-manager.d.ts +46 -0
- package/dist/plugin/package-manager.js +148 -0
- package/dist/plugin/package-manager.js.map +1 -0
- package/dist/plugin/paths.d.ts +6 -0
- package/dist/plugin/paths.js +15 -0
- package/dist/plugin/paths.js.map +1 -0
- package/dist/plugin/registry.d.ts +17 -0
- package/dist/plugin/registry.js +82 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/runtime.d.ts +9 -0
- package/dist/plugin/runtime.js +43 -0
- package/dist/plugin/runtime.js.map +1 -0
- package/dist/plugin/scaffold.d.ts +13 -0
- package/dist/plugin/scaffold.js +132 -0
- package/dist/plugin/scaffold.js.map +1 -0
- package/dist/plugin/state.d.ts +6 -0
- package/dist/plugin/state.js +38 -0
- package/dist/plugin/state.js.map +1 -0
- package/dist/plugin/zip.d.ts +7 -0
- package/dist/plugin/zip.js +138 -0
- package/dist/plugin/zip.js.map +1 -0
- package/dist/sandbox/audit.d.ts +12 -0
- package/dist/sandbox/audit.js +20 -0
- package/dist/sandbox/audit.js.map +1 -0
- package/dist/sandbox/backends/bwrap.d.ts +28 -0
- package/dist/sandbox/backends/bwrap.js +197 -0
- package/dist/sandbox/backends/bwrap.js.map +1 -0
- package/dist/sandbox/backends/direct.d.ts +7 -0
- package/dist/sandbox/backends/direct.js +32 -0
- package/dist/sandbox/backends/direct.js.map +1 -0
- package/dist/sandbox/bridge-state.d.ts +13 -0
- package/dist/sandbox/bridge-state.js +12 -0
- package/dist/sandbox/bridge-state.js.map +1 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +6 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +8 -0
- package/dist/sandbox/manager.js +325 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/memory-limit.d.ts +44 -0
- package/dist/sandbox/memory-limit.js +193 -0
- package/dist/sandbox/memory-limit.js.map +1 -0
- package/dist/sandbox/plugin-client.d.ts +80 -0
- package/dist/sandbox/plugin-client.js +1601 -0
- package/dist/sandbox/plugin-client.js.map +1 -0
- package/dist/sandbox/plugin-host.d.ts +1 -0
- package/dist/sandbox/plugin-host.js +1001 -0
- package/dist/sandbox/plugin-host.js.map +1 -0
- package/dist/sandbox/protocol.d.ts +43 -0
- package/dist/sandbox/protocol.js +96 -0
- package/dist/sandbox/protocol.js.map +1 -0
- package/dist/sandbox/rpc-timeouts.d.ts +28 -0
- package/dist/sandbox/rpc-timeouts.js +15 -0
- package/dist/sandbox/rpc-timeouts.js.map +1 -0
- package/dist/sandbox/supervisor.d.ts +31 -0
- package/dist/sandbox/supervisor.js +114 -0
- package/dist/sandbox/supervisor.js.map +1 -0
- package/dist/sandbox/types.d.ts +124 -0
- package/dist/sandbox/types.js +17 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/service/control.d.ts +32 -0
- package/dist/service/control.js +117 -0
- package/dist/service/control.js.map +1 -0
- package/dist/session/compress.d.ts +3 -0
- package/dist/session/compress.js +42 -0
- package/dist/session/compress.js.map +1 -0
- package/dist/session/index.d.ts +4 -0
- package/dist/session/index.js +8 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/registry.d.ts +4 -0
- package/dist/session/registry.js +12 -0
- package/dist/session/registry.js.map +1 -0
- package/dist/session/replay.d.ts +10 -0
- package/dist/session/replay.js +109 -0
- package/dist/session/replay.js.map +1 -0
- package/dist/session/session.d.ts +40 -0
- package/dist/session/session.js +108 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/store.d.ts +58 -0
- package/dist/session/store.js +382 -0
- package/dist/session/store.js.map +1 -0
- package/dist/session/types.d.ts +56 -0
- package/dist/session/types.js +3 -0
- package/dist/session/types.js.map +1 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +17 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/registry.d.ts +25 -0
- package/dist/workspace/registry.js +293 -0
- package/dist/workspace/registry.js.map +1 -0
- package/package.json +73 -5
- package/scripts/build-node-pty.mjs +55 -0
- package/scripts/rewrite-dts.mjs +36 -0
- package/scripts/run-tests.mjs +41 -0
- package/scripts/verify-sandbox.ts +98 -0
- package/src/agent/pending-asks.ts +88 -0
- package/src/agent/task-registry.ts +108 -0
- package/src/agent/tool-context.ts +47 -0
- package/src/agent/usage.ts +170 -0
- package/src/api/host/agent.ts +162 -0
- package/src/api/host/events.ts +7 -0
- package/src/api/host/exec.ts +47 -0
- package/src/api/host/fs.ts +33 -0
- package/src/api/host/hooks.ts +635 -0
- package/src/api/host/host.ts +99 -0
- package/src/api/host/http.ts +79 -0
- package/src/api/host/index.ts +20 -0
- package/src/api/host/models.ts +81 -0
- package/src/api/host/network.ts +32 -0
- package/src/api/host/plugin.ts +158 -0
- package/src/api/host/plugins.ts +59 -0
- package/src/api/host/pty.ts +73 -0
- package/src/api/host/session.ts +61 -0
- package/src/api/host/version.ts +2 -0
- package/src/api/host/workspace.ts +51 -0
- package/src/api/plugin/http-client.ts +492 -0
- package/src/api/plugin/http-protocol.ts +231 -0
- package/src/api/result/result.ts +52 -0
- package/src/commands/agent-turn.ts +817 -0
- package/src/commands/ask-prompt.ts +52 -0
- package/src/commands/config.ts +306 -0
- package/src/commands/conversation.ts +777 -0
- package/src/commands/doctor.ts +92 -0
- package/src/commands/exec-approval.ts +91 -0
- package/src/commands/help.ts +41 -0
- package/src/commands/key.ts +281 -0
- package/src/commands/plugin.ts +222 -0
- package/src/commands/session.ts +179 -0
- package/src/commands/shared.ts +103 -0
- package/src/commands/start.ts +88 -0
- package/src/commands/stop.ts +24 -0
- package/src/commands/version.ts +40 -0
- package/src/config/codes.ts +17 -0
- package/src/config/credentials.ts +302 -0
- package/src/config/defaults.ts +32 -0
- package/src/config/index.ts +10 -0
- package/src/config/loader.ts +80 -0
- package/src/config/providers.ts +133 -0
- package/src/config/types.ts +88 -0
- package/src/config/validate.ts +458 -0
- package/src/display/markdown.ts +137 -0
- package/src/display/tool-label.ts +87 -0
- package/src/event/event.ts +171 -0
- package/src/index.ts +344 -0
- package/src/input/codes.ts +21 -0
- package/src/input/commands.ts +76 -0
- package/src/input/help.ts +60 -0
- package/src/input/index.ts +10 -0
- package/src/input/parse-options.ts +66 -0
- package/src/input/parse.ts +138 -0
- package/src/input/types.ts +19 -0
- package/src/logger/elog4js.ts +366 -0
- package/src/logger/logger.ts +251 -0
- package/src/network/index.ts +6 -0
- package/src/network/openai.ts +571 -0
- package/src/network/proxy.ts +80 -0
- package/src/network/web.ts +206 -0
- package/src/platform/clipboard.ts +107 -0
- package/src/platform/exec.ts +128 -0
- package/src/platform/file.ts +235 -0
- package/src/platform/index.ts +12 -0
- package/src/platform/notification.ts +108 -0
- package/src/platform/open-external.ts +132 -0
- package/src/platform/os.ts +101 -0
- package/src/platform/path.ts +214 -0
- package/src/plugin/access-guard.ts +46 -0
- package/src/plugin/agent-service.ts +969 -0
- package/src/plugin/agent-tools.ts +43 -0
- package/src/plugin/capabilities.ts +240 -0
- package/src/plugin/console-guard.ts +66 -0
- package/src/plugin/data-access.ts +70 -0
- package/src/plugin/fs.ts +109 -0
- package/src/plugin/host-alias.ts +61 -0
- package/src/plugin/index.ts +11 -0
- package/src/plugin/install.ts +213 -0
- package/src/plugin/loader.ts +952 -0
- package/src/plugin/manage.ts +338 -0
- package/src/plugin/models-service.ts +383 -0
- package/src/plugin/network-service.ts +102 -0
- package/src/plugin/package-manager.ts +214 -0
- package/src/plugin/paths.ts +19 -0
- package/src/plugin/registry.ts +103 -0
- package/src/plugin/runtime.ts +57 -0
- package/src/plugin/scaffold.ts +158 -0
- package/src/plugin/state.ts +52 -0
- package/src/plugin/zip.ts +156 -0
- package/src/sandbox/audit.ts +38 -0
- package/src/sandbox/backends/bwrap.ts +251 -0
- package/src/sandbox/backends/direct.ts +43 -0
- package/src/sandbox/bridge-state.ts +29 -0
- package/src/sandbox/index.ts +5 -0
- package/src/sandbox/manager.ts +406 -0
- package/src/sandbox/memory-limit.ts +245 -0
- package/src/sandbox/plugin-client.ts +1881 -0
- package/src/sandbox/plugin-host.ts +1282 -0
- package/src/sandbox/protocol.ts +142 -0
- package/src/sandbox/pty-bridge.py +154 -0
- package/src/sandbox/rpc-timeouts.ts +43 -0
- package/src/sandbox/supervisor.ts +172 -0
- package/src/sandbox/types.ts +149 -0
- package/src/service/control.ts +155 -0
- package/src/session/compress.ts +47 -0
- package/src/session/index.ts +8 -0
- package/src/session/registry.ts +17 -0
- package/src/session/replay.ts +132 -0
- package/src/session/session.ts +134 -0
- package/src/session/store.ts +468 -0
- package/src/session/types.ts +61 -0
- package/src/workspace/index.ts +21 -0
- package/src/workspace/registry.ts +356 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DeepSeek 网页搜索与抓取 API
|
|
3
|
+
* Search POST /v1/tools/search
|
|
4
|
+
* Fetch POST /v1/tools/fetch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Result } from "../api/result/result.ts"
|
|
8
|
+
import { readEnv } from "../platform/os.ts"
|
|
9
|
+
|
|
10
|
+
export const WEB_CODES = {
|
|
11
|
+
missingApiKey: "web.missing-api-key",
|
|
12
|
+
invalidUrl: "web.invalid-url",
|
|
13
|
+
requestFailed: "web.request-failed",
|
|
14
|
+
searchFailed: "web.search-failed",
|
|
15
|
+
fetchFailed: "web.fetch-failed",
|
|
16
|
+
unsupported: "web.unsupported-provider",
|
|
17
|
+
} as const
|
|
18
|
+
|
|
19
|
+
export interface WebSearchResult {
|
|
20
|
+
readonly title: string
|
|
21
|
+
readonly url: string
|
|
22
|
+
readonly snippet: string
|
|
23
|
+
readonly summary: string
|
|
24
|
+
readonly content: string
|
|
25
|
+
readonly siteName: string | null
|
|
26
|
+
readonly date: string | null
|
|
27
|
+
readonly authority: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WebFetchResult {
|
|
31
|
+
readonly title: string
|
|
32
|
+
readonly content: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WebClient {
|
|
36
|
+
search(query: string, count?: number): Promise<Result<readonly WebSearchResult[]>>
|
|
37
|
+
fetch(url: string): Promise<Result<WebFetchResult>>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WebClientOptions {
|
|
41
|
+
readonly baseUrl?: string | null
|
|
42
|
+
readonly apiKey?: string
|
|
43
|
+
readonly fetch?: typeof fetch
|
|
44
|
+
readonly timeoutMs?: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function endpoint(baseUrl: string | null | undefined, path: string): string | null {
|
|
52
|
+
const base = (baseUrl ?? "https://api.deepseek.com").trim().replace(/\/+$/, "")
|
|
53
|
+
if (base === "") {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
const normalized = base.endsWith("/v1") ? base.slice(0, -3) : base
|
|
57
|
+
try {
|
|
58
|
+
return new URL(`${normalized}/v1/tools/${path}`).toString()
|
|
59
|
+
} catch {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504])
|
|
65
|
+
|
|
66
|
+
function sleep(ms: number): Promise<void> {
|
|
67
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function postJson(
|
|
71
|
+
url: string,
|
|
72
|
+
apiKey: string,
|
|
73
|
+
body: unknown,
|
|
74
|
+
fetchImpl: typeof fetch,
|
|
75
|
+
timeoutMs: number,
|
|
76
|
+
): Promise<Result<unknown>> {
|
|
77
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetchImpl(url, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
Authorization: `Bearer ${apiKey}`,
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
87
|
+
})
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
const code = `web.http-${response.status}`
|
|
90
|
+
if (RETRY_STATUSES.has(response.status) && attempt === 0) {
|
|
91
|
+
await sleep(300)
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
return Result.error<unknown>(code)
|
|
95
|
+
}
|
|
96
|
+
return Result.successWithData<unknown>(await response.json())
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
99
|
+
const code = `${WEB_CODES.requestFailed}:${message}`
|
|
100
|
+
if (attempt === 0) {
|
|
101
|
+
await sleep(300)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
return Result.error<unknown>(code)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Result.error<unknown>(WEB_CODES.requestFailed)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Markdown 转义清理:DeepSeek Fetch 会把所有 ASCII 标点转义成 `\.` / `\(` 之类
|
|
111
|
+
const MARKDOWN_ESCAPES = /\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g
|
|
112
|
+
|
|
113
|
+
function unescapeMarkdown(text: string): string {
|
|
114
|
+
return text.replace(MARKDOWN_ESCAPES, "$1")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseSearchResult(value: unknown): WebSearchResult | null {
|
|
118
|
+
if (!isObject(value) || typeof value["url"] !== "string") {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
title: unescapeMarkdown(typeof value["title"] === "string" ? value["title"] : ""),
|
|
123
|
+
url: value["url"],
|
|
124
|
+
snippet: unescapeMarkdown(typeof value["snippet"] === "string" ? value["snippet"] : ""),
|
|
125
|
+
summary: unescapeMarkdown(typeof value["summary"] === "string" ? value["summary"] : ""),
|
|
126
|
+
content: unescapeMarkdown(typeof value["content"] === "string" ? value["content"] : ""),
|
|
127
|
+
siteName: typeof value["site_name"] === "string" ? value["site_name"] : null,
|
|
128
|
+
date: typeof value["date"] === "string" ? value["date"] : null,
|
|
129
|
+
authority: typeof value["authority"] === "number" ? value["authority"] : -1,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// HTTP/HTTPS 校验
|
|
134
|
+
function validUrl(value: string): boolean {
|
|
135
|
+
try {
|
|
136
|
+
const url = new URL(value)
|
|
137
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.host !== ""
|
|
138
|
+
} catch {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createDeepSeekWebClient(options: WebClientOptions = {}): WebClient {
|
|
144
|
+
const apiKey = options.apiKey ?? readEnv("OPENAI_API_KEY") ?? ""
|
|
145
|
+
const fetchImpl = options.fetch ?? fetch
|
|
146
|
+
const timeoutMs = options.timeoutMs ?? 60_000
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
async search(query, count = 5) {
|
|
150
|
+
if (apiKey === "") {
|
|
151
|
+
return Result.error<readonly WebSearchResult[]>(WEB_CODES.missingApiKey)
|
|
152
|
+
}
|
|
153
|
+
const url = endpoint(options.baseUrl, "search")
|
|
154
|
+
if (url === null) {
|
|
155
|
+
return Result.error<readonly WebSearchResult[]>(WEB_CODES.unsupported)
|
|
156
|
+
}
|
|
157
|
+
const response = await postJson(
|
|
158
|
+
url,
|
|
159
|
+
apiKey,
|
|
160
|
+
{ query, count: Math.max(1, Math.min(50, count)), need_content: false },
|
|
161
|
+
fetchImpl,
|
|
162
|
+
timeoutMs,
|
|
163
|
+
)
|
|
164
|
+
if (!response.isSuccess()) {
|
|
165
|
+
return Result.error<readonly WebSearchResult[]>(
|
|
166
|
+
response.getCode() ?? WEB_CODES.searchFailed,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
const data = response.getData()
|
|
170
|
+
const results = isObject(data) ? data["results"] : undefined
|
|
171
|
+
if (!Array.isArray(results)) {
|
|
172
|
+
return Result.error<readonly WebSearchResult[]>(WEB_CODES.searchFailed)
|
|
173
|
+
}
|
|
174
|
+
return Result.successWithData<readonly WebSearchResult[]>(
|
|
175
|
+
results
|
|
176
|
+
.map(parseSearchResult)
|
|
177
|
+
.filter((item): item is WebSearchResult => item !== null),
|
|
178
|
+
)
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async fetch(targetUrl) {
|
|
182
|
+
if (apiKey === "") {
|
|
183
|
+
return Result.error<WebFetchResult>(WEB_CODES.missingApiKey)
|
|
184
|
+
}
|
|
185
|
+
if (!validUrl(targetUrl)) {
|
|
186
|
+
return Result.error<WebFetchResult>(WEB_CODES.invalidUrl)
|
|
187
|
+
}
|
|
188
|
+
const url = endpoint(options.baseUrl, "fetch")
|
|
189
|
+
if (url === null) {
|
|
190
|
+
return Result.error<WebFetchResult>(WEB_CODES.unsupported)
|
|
191
|
+
}
|
|
192
|
+
const response = await postJson(url, apiKey, { url: targetUrl }, fetchImpl, timeoutMs)
|
|
193
|
+
if (!response.isSuccess()) {
|
|
194
|
+
return Result.error<WebFetchResult>(response.getCode() ?? WEB_CODES.fetchFailed)
|
|
195
|
+
}
|
|
196
|
+
const data = response.getData()
|
|
197
|
+
if (!isObject(data) || typeof data["content"] !== "string") {
|
|
198
|
+
return Result.error<WebFetchResult>(WEB_CODES.fetchFailed)
|
|
199
|
+
}
|
|
200
|
+
return Result.successWithData<WebFetchResult>({
|
|
201
|
+
title: unescapeMarkdown(typeof data["title"] === "string" ? data["title"] : ""),
|
|
202
|
+
content: unescapeMarkdown(data["content"]),
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 剪贴板读写。
|
|
3
|
+
* 各平台通过系统命令实现;命令缺失或不可用时按候选顺序回退。
|
|
4
|
+
* Windows 的文本按 UTF-8 字节经 stdin/stdout 收发,避开控制台代码页。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describeError, powershellArgs, runCommand, type RunCommandResult } from "./exec.ts"
|
|
8
|
+
import { isTermux, platform, type PlatformName } from "./os.ts"
|
|
9
|
+
|
|
10
|
+
export interface ClipboardCommand {
|
|
11
|
+
readonly command: string
|
|
12
|
+
readonly args: readonly string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ClipboardCommands {
|
|
16
|
+
readonly write: readonly ClipboardCommand[]
|
|
17
|
+
readonly read: readonly ClipboardCommand[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const POWERSHELL_WRITE = powershellArgs(
|
|
21
|
+
[
|
|
22
|
+
"$input = [Console]::OpenStandardInput()",
|
|
23
|
+
"$buffer = New-Object byte[] 8192",
|
|
24
|
+
"$memory = New-Object System.IO.MemoryStream",
|
|
25
|
+
"while (($read = $input.Read($buffer, 0, $buffer.Length)) -gt 0) { $memory.Write($buffer, 0, $read) }",
|
|
26
|
+
"Set-Clipboard -Value ([System.Text.Encoding]::UTF8.GetString($memory.ToArray()))",
|
|
27
|
+
].join("\n"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const POWERSHELL_READ = powershellArgs(
|
|
31
|
+
[
|
|
32
|
+
"$text = Get-Clipboard -Raw",
|
|
33
|
+
"if ($null -eq $text) { $text = '' }",
|
|
34
|
+
"$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)",
|
|
35
|
+
"$output = [Console]::OpenStandardOutput()",
|
|
36
|
+
"$output.Write($bytes, 0, $bytes.Length)",
|
|
37
|
+
"$output.Flush()",
|
|
38
|
+
].join("\n"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
/** 生成候选命令,按优先级排列。单独导出以便在任意平台测试 */
|
|
42
|
+
export function buildClipboardCommands(
|
|
43
|
+
targetPlatform: PlatformName = platform,
|
|
44
|
+
inTermux: boolean = isTermux,
|
|
45
|
+
): ClipboardCommands {
|
|
46
|
+
switch (targetPlatform) {
|
|
47
|
+
case "windows":
|
|
48
|
+
return {
|
|
49
|
+
write: [{ command: "powershell", args: POWERSHELL_WRITE }],
|
|
50
|
+
read: [{ command: "powershell", args: POWERSHELL_READ }],
|
|
51
|
+
}
|
|
52
|
+
case "macos":
|
|
53
|
+
return {
|
|
54
|
+
write: [{ command: "pbcopy", args: [] }],
|
|
55
|
+
read: [{ command: "pbpaste", args: [] }],
|
|
56
|
+
}
|
|
57
|
+
case "android":
|
|
58
|
+
// Android 应用沙箱内只有 Termux:API 提供的命令
|
|
59
|
+
if (!inTermux) {
|
|
60
|
+
return { write: [], read: [] }
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
write: [{ command: "termux-clipboard-set", args: [] }],
|
|
64
|
+
read: [{ command: "termux-clipboard-get", args: [] }],
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return {
|
|
68
|
+
write: [
|
|
69
|
+
{ command: "wl-copy", args: [] },
|
|
70
|
+
{ command: "xclip", args: ["-selection", "clipboard"] },
|
|
71
|
+
{ command: "xsel", args: ["--clipboard", "--input"] },
|
|
72
|
+
],
|
|
73
|
+
read: [
|
|
74
|
+
{ command: "wl-paste", args: [] },
|
|
75
|
+
{ command: "xclip", args: ["-selection", "clipboard", "-o"] },
|
|
76
|
+
{ command: "xsel", args: ["--clipboard", "--output"] },
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function runCandidates(
|
|
83
|
+
commands: readonly ClipboardCommand[],
|
|
84
|
+
input?: string,
|
|
85
|
+
): Promise<RunCommandResult> {
|
|
86
|
+
const failures: string[] = []
|
|
87
|
+
for (const candidate of commands) {
|
|
88
|
+
try {
|
|
89
|
+
return await runCommand(candidate.command, candidate.args, { input })
|
|
90
|
+
} catch (error) {
|
|
91
|
+
failures.push(`${candidate.command}: ${describeError(error)}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const detail = failures.length === 0 ? "当前平台没有可用命令" : failures.join(";")
|
|
95
|
+
throw new Error(`剪贴板不可用:${detail}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 写入剪贴板 */
|
|
99
|
+
export async function writeClipboard(text: string): Promise<void> {
|
|
100
|
+
await runCandidates(buildClipboardCommands().write, text)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 读取剪贴板。返回命令的原始输出,不做换行裁剪 */
|
|
104
|
+
export async function readClipboard(): Promise<string> {
|
|
105
|
+
const result = await runCandidates(buildClipboardCommands().read)
|
|
106
|
+
return result.stdout
|
|
107
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 命令执行。
|
|
3
|
+
* 命令缺失统一抛 CommandNotFoundError,供候选命令回退;stderr 有内容但退出码为 0 不算失败。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from "node:child_process"
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 15_000
|
|
9
|
+
|
|
10
|
+
/** 命令不存在(ENOENT) */
|
|
11
|
+
export class CommandNotFoundError extends Error {
|
|
12
|
+
readonly command: string
|
|
13
|
+
|
|
14
|
+
constructor(command: string) {
|
|
15
|
+
super(`命令不存在:${command}`)
|
|
16
|
+
this.name = "CommandNotFoundError"
|
|
17
|
+
this.command = command
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 命令存在但退出码非 0 */
|
|
22
|
+
export class CommandFailedError extends Error {
|
|
23
|
+
readonly command: string
|
|
24
|
+
readonly exitCode: number | null
|
|
25
|
+
readonly stderr: string
|
|
26
|
+
|
|
27
|
+
constructor(command: string, exitCode: number | null, stderr: string) {
|
|
28
|
+
const detail = stderr.trim()
|
|
29
|
+
super(
|
|
30
|
+
`命令执行失败:${command}(退出码 ${exitCode ?? "signal"})${detail === "" ? "" : `:${detail}`}`,
|
|
31
|
+
)
|
|
32
|
+
this.name = "CommandFailedError"
|
|
33
|
+
this.command = command
|
|
34
|
+
this.exitCode = exitCode
|
|
35
|
+
this.stderr = stderr
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RunCommandOptions {
|
|
40
|
+
/** 写入 stdin 的文本 */
|
|
41
|
+
readonly input?: string
|
|
42
|
+
readonly env?: NodeJS.ProcessEnv
|
|
43
|
+
readonly timeoutMs?: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RunCommandResult {
|
|
47
|
+
readonly stdout: string
|
|
48
|
+
readonly stderr: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 运行命令并收集输出 */
|
|
52
|
+
export function runCommand(
|
|
53
|
+
command: string,
|
|
54
|
+
args: readonly string[] = [],
|
|
55
|
+
options: RunCommandOptions = {},
|
|
56
|
+
): Promise<RunCommandResult> {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
let settled = false
|
|
59
|
+
let timer: NodeJS.Timeout | undefined
|
|
60
|
+
|
|
61
|
+
const finish = (action: () => void): void => {
|
|
62
|
+
if (settled) {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
settled = true
|
|
66
|
+
if (timer !== undefined) {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
}
|
|
69
|
+
action()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const child = spawn(command, [...args], {
|
|
73
|
+
env: options.env,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
let stdout = ""
|
|
78
|
+
let stderr = ""
|
|
79
|
+
child.stdout.setEncoding("utf8")
|
|
80
|
+
child.stderr.setEncoding("utf8")
|
|
81
|
+
child.stdout.on("data", (chunk: string) => {
|
|
82
|
+
stdout += chunk
|
|
83
|
+
})
|
|
84
|
+
child.stderr.on("data", (chunk: string) => {
|
|
85
|
+
stderr += chunk
|
|
86
|
+
})
|
|
87
|
+
// 命令未读取 stdin 就退出时写入会触发 EPIPE
|
|
88
|
+
child.stdin.on("error", () => undefined)
|
|
89
|
+
|
|
90
|
+
timer = setTimeout(() => {
|
|
91
|
+
child.kill("SIGKILL")
|
|
92
|
+
finish(() => reject(new Error(`命令执行超时:${command}`)))
|
|
93
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
94
|
+
|
|
95
|
+
child.once("error", (error) => {
|
|
96
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
97
|
+
finish(() => reject(new CommandNotFoundError(command)))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
finish(() => reject(error))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
child.once("close", (code) => {
|
|
104
|
+
if (code === 0) {
|
|
105
|
+
finish(() => resolve({ stdout, stderr }))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
finish(() => reject(new CommandFailedError(command, code, stderr)))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
child.stdin.end(options.input)
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** PowerShell 脚本编码为 -EncodedCommand 参数,绕开 cmd 的引号与编码问题 */
|
|
116
|
+
export function powershellArgs(script: string): string[] {
|
|
117
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64")
|
|
118
|
+
return ["-NoProfile", "-NonInteractive", "-EncodedCommand", encoded]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 合并 message 与 cause 链,用于候选命令全失败时的报错 */
|
|
122
|
+
export function describeError(error: unknown): string {
|
|
123
|
+
if (!(error instanceof Error)) {
|
|
124
|
+
return String(error)
|
|
125
|
+
}
|
|
126
|
+
const cause = error.cause
|
|
127
|
+
return cause === undefined ? error.message : `${error.message}(${describeError(cause)})`
|
|
128
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 文件落盘。
|
|
3
|
+
* 流程:临时文件 -> fsync -> rename -> chmod -> fsync 目录。
|
|
4
|
+
* Windows 不执行 chmod 与目录 fsync;rename 遇文件占用时短暂重试。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto"
|
|
8
|
+
import { closeSync, mkdirSync, openSync, type Stats } from "node:fs"
|
|
9
|
+
import { chmod, mkdir, open, readFile, rename, rm, stat, unlink } from "node:fs/promises"
|
|
10
|
+
import { basename, dirname, join } from "node:path"
|
|
11
|
+
import { setTimeout as sleep } from "node:timers/promises"
|
|
12
|
+
|
|
13
|
+
import { isWindows } from "./os.ts"
|
|
14
|
+
import type { PathRoots } from "./path.ts"
|
|
15
|
+
|
|
16
|
+
const FILE_MODE = 0o600
|
|
17
|
+
const DIR_MODE = 0o700
|
|
18
|
+
|
|
19
|
+
/** 是否为文件不存在(ENOENT) */
|
|
20
|
+
export function isNotFound(error: unknown): boolean {
|
|
21
|
+
return (
|
|
22
|
+
typeof error === "object" &&
|
|
23
|
+
error !== null &&
|
|
24
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 创建目录,默认权限 0700 */
|
|
29
|
+
export async function ensureDir(dir: string, mode = DIR_MODE): Promise<void> {
|
|
30
|
+
await mkdir(dir, { recursive: true, mode })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 同步验证文件可写:创建父目录并打开目标,失败时抛出。用于需要提前确认的调用方 */
|
|
34
|
+
export function assertWritableFile(file: string, mode = FILE_MODE): void {
|
|
35
|
+
mkdirSync(dirname(file), { recursive: true, mode: DIR_MODE })
|
|
36
|
+
const fd = openSync(file, "a", mode)
|
|
37
|
+
closeSync(fd)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 读取文本文件,不存在时返回 null */
|
|
41
|
+
export async function readTextIfExists(file: string): Promise<string | null> {
|
|
42
|
+
try {
|
|
43
|
+
return await readFile(file, "utf8")
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (isNotFound(error)) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
throw error
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 读取二进制文件,不存在时返回 null */
|
|
53
|
+
export async function readBytesIfExists(file: string): Promise<Buffer | null> {
|
|
54
|
+
try {
|
|
55
|
+
return await readFile(file)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (isNotFound(error)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** stat,文件不存在时返回 null */
|
|
65
|
+
export async function statIfExists(file: string): Promise<Stats | null> {
|
|
66
|
+
try {
|
|
67
|
+
return await stat(file)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (isNotFound(error)) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 删除文件或目录,不存在时忽略 */
|
|
77
|
+
export async function removePath(file: string): Promise<void> {
|
|
78
|
+
await rm(file, { recursive: true, force: true })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 读取 JSON 文件,不存在时返回 null。解析失败向上抛出 */
|
|
82
|
+
export async function readJsonIfExists<T>(file: string): Promise<T | null> {
|
|
83
|
+
const text = await readTextIfExists(file)
|
|
84
|
+
return text === null ? null : (JSON.parse(text) as T)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WriteFileOptions {
|
|
88
|
+
/** 文件权限,默认 0600 */
|
|
89
|
+
readonly mode?: number
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 原子写。临时文件必须与目标文件同目录,跨分区 rename 不再具有原子性。
|
|
94
|
+
*/
|
|
95
|
+
export async function writeTextAtomic(
|
|
96
|
+
file: string,
|
|
97
|
+
text: string,
|
|
98
|
+
options: WriteFileOptions = {},
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
await writeBytesAtomic(file, Buffer.from(text, "utf8"), options)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 原子写二进制(会话的 zstd 段用) */
|
|
104
|
+
export async function writeBytesAtomic(
|
|
105
|
+
file: string,
|
|
106
|
+
data: Uint8Array,
|
|
107
|
+
options: WriteFileOptions = {},
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const dir = dirname(file)
|
|
110
|
+
const mode = options.mode ?? FILE_MODE
|
|
111
|
+
await ensureDir(dir)
|
|
112
|
+
|
|
113
|
+
const tmp = join(dir, `.${basename(file)}.tmp-${process.pid}-${randomUUID().slice(0, 8)}`)
|
|
114
|
+
const handle = await open(tmp, "w", mode)
|
|
115
|
+
try {
|
|
116
|
+
await handle.writeFile(data)
|
|
117
|
+
// 内容先落盘
|
|
118
|
+
await handle.sync()
|
|
119
|
+
} catch (error) {
|
|
120
|
+
await handle.close().catch(() => undefined)
|
|
121
|
+
await unlink(tmp).catch(() => undefined)
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
await handle.close()
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await renameWithRetry(tmp, file)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await unlink(tmp).catch(() => undefined)
|
|
130
|
+
throw error
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isWindows) {
|
|
134
|
+
// 部分文件系统 rename 后权限跟随临时文件,这里显式重设
|
|
135
|
+
await chmodQuiet(file, mode)
|
|
136
|
+
}
|
|
137
|
+
await syncDir(dir)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** 原子写 JSON,固定 4 空格缩进并保留末尾换行 */
|
|
141
|
+
export async function writeJsonAtomic(
|
|
142
|
+
file: string,
|
|
143
|
+
value: unknown,
|
|
144
|
+
options: WriteFileOptions & { readonly indent?: number } = {},
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const text = `${JSON.stringify(value, null, options.indent ?? 4)}\n`
|
|
147
|
+
await writeTextAtomic(file, text, options)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 将损坏文件移到一边,返回移动后的位置 */
|
|
151
|
+
export async function moveAside(file: string, suffix: string): Promise<string> {
|
|
152
|
+
const target = `${file}.${suffix}`
|
|
153
|
+
await rename(file, target)
|
|
154
|
+
return target
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 备份与损坏文件用的时间戳,形如 20260919-013045 */
|
|
158
|
+
export function timeStamp(date = new Date()): string {
|
|
159
|
+
const pad = (n: number): string => String(n).padStart(2, "0")
|
|
160
|
+
return (
|
|
161
|
+
`${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` +
|
|
162
|
+
`-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 创建四个根目录。POSIX 下将已存在目录权限收紧至 0700 */
|
|
167
|
+
export async function ensureDirs(roots: PathRoots, mode = DIR_MODE): Promise<void> {
|
|
168
|
+
for (const dir of [roots.configDir, roots.dataDir, roots.cacheDir, roots.logDir]) {
|
|
169
|
+
await ensureDir(dir, mode)
|
|
170
|
+
if (isWindows) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
const info = await stat(dir)
|
|
174
|
+
if ((info.mode & 0o777) !== mode) {
|
|
175
|
+
await chmodQuiet(dir, mode)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Android 外部存储(FAT/exFAT)与部分挂载点不支持 chmod,权限收紧失败时继续 */
|
|
181
|
+
const CHMOD_IGNORED_CODES = new Set(["EPERM", "EACCES", "EINVAL", "ENOTSUP", "ENOSYS"])
|
|
182
|
+
|
|
183
|
+
const RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"])
|
|
184
|
+
const RENAME_ATTEMPTS = 3
|
|
185
|
+
|
|
186
|
+
function isRetryableRenameError(error: unknown): boolean {
|
|
187
|
+
if (typeof error !== "object" || error === null) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
const code = (error as { code?: unknown }).code
|
|
191
|
+
return typeof code === "string" && RENAME_RETRY_CODES.has(code)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Windows 上杀毒软件与索引服务会短暂占用目标文件,rename 失败时重试 */
|
|
195
|
+
async function renameWithRetry(from: string, to: string): Promise<void> {
|
|
196
|
+
const attempts = isWindows ? RENAME_ATTEMPTS : 1
|
|
197
|
+
for (let attempt = 1; ; attempt++) {
|
|
198
|
+
try {
|
|
199
|
+
await rename(from, to)
|
|
200
|
+
return
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (attempt >= attempts || !isRetryableRenameError(error)) {
|
|
203
|
+
throw error
|
|
204
|
+
}
|
|
205
|
+
await sleep(40 * attempt)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function chmodQuiet(file: string, mode: number): Promise<void> {
|
|
211
|
+
try {
|
|
212
|
+
await chmod(file, mode)
|
|
213
|
+
} catch (error) {
|
|
214
|
+
const code = (error as { code?: unknown }).code
|
|
215
|
+
if (typeof code !== "string" || !CHMOD_IGNORED_CODES.has(code)) {
|
|
216
|
+
throw error
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function syncDir(dir: string): Promise<void> {
|
|
222
|
+
if (isWindows) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const handle = await open(dir, "r")
|
|
227
|
+
try {
|
|
228
|
+
await handle.sync()
|
|
229
|
+
} finally {
|
|
230
|
+
await handle.close()
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// 目录 fsync 失败时忽略
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 平台层入口。
|
|
3
|
+
* 平台识别、目录解析与文件落盘;上层不直接读取 process.platform 或拼接系统目录。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from "./clipboard.ts"
|
|
7
|
+
export * from "./exec.ts"
|
|
8
|
+
export * from "./file.ts"
|
|
9
|
+
export * from "./notification.ts"
|
|
10
|
+
export * from "./open-external.ts"
|
|
11
|
+
export * from "./os.ts"
|
|
12
|
+
export * from "./path.ts"
|