dsh-multi-chat 0.4.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.
@@ -0,0 +1,27 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for
4
+ * `@deepseek-ai/dsh-client-ui-multi-wall`.
5
+ * @module @deepseek-ai/dsh-client-ui-multi-wall/invariant
6
+ */
7
+ const PACKAGE_NAME = "@deepseek-ai/dsh-client-ui-multi-wall";
8
+ /** Cordis companion plugin name. */
9
+ const name = "client-ui-multi-wall-invariant";
10
+ /** Service required before the companion can reserve package ownership. */
11
+ const inject = ["invariants"];
12
+ /**
13
+ * No runtime invariant: the wall contributes the conversation view-ring entry
14
+ * (the wall surface) and the sidebar footer shortcut, whose disposal is
15
+ * proven by the HMR-safety spec — the plugin owns one store handle used by
16
+ * the view entry, emits no cordis events, and holds no cross-plugin mutable
17
+ * state.
18
+ */
19
+ const install = () => {};
20
+ /**
21
+ * Register this package's invariant companion.
22
+ * @param ctx - Cordis context carrying the invariant service.
23
+ * @returns the installed registration's disposer after setup succeeds.
24
+ */
25
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
26
+ //#endregion
27
+ export { apply, inject, name };
@@ -0,0 +1,10 @@
1
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
+ /** Composed props: sidebar column state + locale. */
3
+ export type WallToggleProps = PropsRuntime<'sidebar.footer.action'> & PropsLocale<'multiWall'>;
4
+ /**
5
+ * Render the wall shortcut row (icon; label only in the wide column).
6
+ * @param props - composed slot props.
7
+ * @returns the shortcut row.
8
+ */
9
+ export declare function WallToggle({ wide, t }: WallToggleProps): import("react").JSX.Element;
10
+ //# sourceMappingURL=WallToggle.d.ts.map
@@ -0,0 +1,16 @@
1
+ import type { PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { HandleOf } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { ConvViewProps } from '@deepseek-ai/dsh-client-ui-conversation/client';
4
+ import type { createWallStore } from './store.ts';
5
+ import type { WallInjected } from './wall-injected.ts';
6
+ /** Composed props: the view-ring runtime share, the store, the probe face, and locale. */
7
+ export type WallViewProps = ConvViewProps & PropsStore<HandleOf<typeof createWallStore>> & WallInjected & PropsLocale<'multiWall'>;
8
+ /**
9
+ * Render the wall: toolbar plus the horizontally-filled pane grid. Discovery
10
+ * runs on mount and liveness polls every 5s; the store's persisted ports
11
+ * survive view switches and reloads.
12
+ * @param props - composed slot props.
13
+ * @returns the wall surface.
14
+ */
15
+ export declare function WallView({ useStore, actions, discover, probe, stop, create, link, t }: WallViewProps): import("react").JSX.Element;
16
+ //# sourceMappingURL=WallView.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Multi-window wall plugin, browser half: contributes the wall as a
3
+ * `conversation.view` ring entry (the official replace-the-chat-surface
4
+ * mechanism — the header grows a "多窗口墙" tab and the right panel swaps to
5
+ * the wall) plus a sidebar footer shortcut that jumps to that view. No
6
+ * existing slot is replaced and no original interaction logic changes; the
7
+ * wall is pure UI over the node half's /multi/api probe routes.
8
+ *
9
+ * Recursion guard: a wall pane embeds each instance as
10
+ * `http://127.0.0.1:<port>/?multi-wall=embed`; the embedded page sees the
11
+ * query flag and registers nothing, so a pane can never grow a wall of its
12
+ * own. The wall also never renders the port it is served on (SELF_PORT is
13
+ * filtered at render time and by the node half's discovery).
14
+ */
15
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
16
+ import { type MultiWallKey } from './locales.ts';
17
+ export { WallView } from './WallView.tsx';
18
+ export type { WallViewProps } from './WallView.tsx';
19
+ export { WallToggle } from './WallToggle.tsx';
20
+ export type { WallToggleProps } from './WallToggle.tsx';
21
+ export type { MultiWallKey } from './locales.ts';
22
+ export { createWallStore } from './store.ts';
23
+ export type { WallState, WallActions } from './store.ts';
24
+ export type { ProbeRow, StopRow, WallInjected } from './wall-injected.ts';
25
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
26
+ interface LocaleNamespaceMap {
27
+ /** Multi-window wall copy. */
28
+ multiWall: MultiWallKey;
29
+ }
30
+ }
31
+ /** Required services: slots for both registrations, locale for copy. */
32
+ export declare const inject: string[];
33
+ /**
34
+ * Client plugin body: register the dictionaries, the view-ring entry (the
35
+ * wall), and the sidebar footer shortcut. Embedded panes register nothing.
36
+ * @param ctx - client root context.
37
+ */
38
+ export declare function apply(ctx: ClientContext): void;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,94 @@
1
+ /** `multiWall` namespace dictionaries. */
2
+ /** Simplified Chinese dictionary (the key-set source of truth). */
3
+ export declare const zh: {
4
+ toggle: string;
5
+ 'toggle.aria': string;
6
+ 'view.multiWall': string;
7
+ 'overlay.title': string;
8
+ 'overlay.close': string;
9
+ scan: string;
10
+ 'scan.from': string;
11
+ 'scan.to': string;
12
+ create: string;
13
+ 'create.done': string;
14
+ 'create.failed': string;
15
+ 'create.unknown': string;
16
+ exit: string;
17
+ 'exit.aria': string;
18
+ link: string;
19
+ 'link.aria': string;
20
+ 'link.fetching': string;
21
+ 'link.none': string;
22
+ 'link.copy': string;
23
+ 'link.copied': string;
24
+ 'link.hint': string;
25
+ 'link.reachable': string;
26
+ 'link.unreachable': string;
27
+ columns: string;
28
+ 'columns.auto': string;
29
+ refresh: string;
30
+ openTab: string;
31
+ reload: string;
32
+ remove: string;
33
+ stop: string;
34
+ 'stop.confirm': string;
35
+ 'stop.self': string;
36
+ 'stop.done': string;
37
+ 'stop.failed': string;
38
+ zoom: string;
39
+ loading: string;
40
+ empty: string;
41
+ 'empty.hint': string;
42
+ 'status.scanning': string;
43
+ 'status.found': string;
44
+ 'status.none': string;
45
+ 'status.refreshed': string;
46
+ };
47
+ /** The multi-wall namespace key union. */
48
+ export type MultiWallKey = keyof typeof zh;
49
+ /** English dictionary, checked complete against the zh key set. */
50
+ export declare const en: {
51
+ toggle: string;
52
+ 'toggle.aria': string;
53
+ 'view.multiWall': string;
54
+ 'overlay.title': string;
55
+ 'overlay.close': string;
56
+ scan: string;
57
+ 'scan.from': string;
58
+ 'scan.to': string;
59
+ create: string;
60
+ 'create.done': string;
61
+ 'create.failed': string;
62
+ 'create.unknown': string;
63
+ exit: string;
64
+ 'exit.aria': string;
65
+ link: string;
66
+ 'link.aria': string;
67
+ 'link.fetching': string;
68
+ 'link.none': string;
69
+ 'link.copy': string;
70
+ 'link.copied': string;
71
+ 'link.hint': string;
72
+ 'link.reachable': string;
73
+ 'link.unreachable': string;
74
+ columns: string;
75
+ 'columns.auto': string;
76
+ refresh: string;
77
+ openTab: string;
78
+ reload: string;
79
+ remove: string;
80
+ stop: string;
81
+ 'stop.confirm': string;
82
+ 'stop.self': string;
83
+ 'stop.done': string;
84
+ 'stop.failed': string;
85
+ zoom: string;
86
+ loading: string;
87
+ empty: string;
88
+ 'empty.hint': string;
89
+ 'status.scanning': string;
90
+ 'status.found': string;
91
+ 'status.none': string;
92
+ 'status.refreshed': string;
93
+ };
94
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Wall store: which instances are shown and the grid columns. The ports list
3
+ * is the wall's whole business state — discovery writes it, removal filters
4
+ * it, and the grid renders from it. Open/closed is NOT stored here: the wall
5
+ * is a `conversation.view` ring entry, so the active view (chat store's
6
+ * `view` field) decides whether it renders.
7
+ */
8
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
9
+ /** Wall viewing state: displayed ports and grid columns. */
10
+ export type WallState = {
11
+ ports: number[];
12
+ columns: string;
13
+ };
14
+ /** The wall store's complete write set. */
15
+ export type WallActions = {
16
+ setPorts: (draft: WallState, ports: number[]) => void;
17
+ addPort: (draft: WallState, port: number) => void;
18
+ removePort: (draft: WallState, port: number) => void;
19
+ setColumns: (draft: WallState, columns: string) => void;
20
+ };
21
+ /**
22
+ * Create the wall store handle. Persisted under `dsh.multi-wall` so the port
23
+ * set and column choice survive view switches and reloads.
24
+ * @returns the store handle (spec + type + identity + factory in one).
25
+ */
26
+ export declare function createWallStore(): EngineStoreHandle<WallState, WallActions>;
27
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Injected probe face for the wall view: same-origin fetches to the node
3
+ * half's /multi/api routes. Callbacks only — components never see ctx.
4
+ */
5
+ /** One liveness row from /multi/api/status. */
6
+ export interface ProbeRow {
7
+ port: number;
8
+ alive: boolean;
9
+ status: number;
10
+ }
11
+ /** One stop result row from /multi/api/stop. */
12
+ export interface StopRow {
13
+ port: number;
14
+ ok: boolean;
15
+ error?: string;
16
+ }
17
+ /** One create result row from /multi/api/create. */
18
+ export interface CreateRow {
19
+ ok: boolean;
20
+ port?: number;
21
+ error?: string;
22
+ }
23
+ /** The phone-reachable link info from /multi/api/link. */
24
+ export interface LinkRow {
25
+ port: number;
26
+ host: string;
27
+ /** LAN URLs (`http://<lan-ip>:<port>/`), possibly empty. */
28
+ lan: string[];
29
+ /** Whether the instance is reachable off-loopback (binds 0.0.0.0). */
30
+ reachable: boolean;
31
+ /** Human hint when not reachable (restart / tunnel guidance). */
32
+ hint?: string;
33
+ }
34
+ /** The inject face delivered to the wall view registration. */
35
+ export interface WallInjected {
36
+ /** Discover live DSH instances (same-origin /multi/api/ports). */
37
+ discover: () => Promise<number[]>;
38
+ /** Probe liveness of specific ports (same-origin /multi/api/status). */
39
+ probe: (ports: number[]) => Promise<ProbeRow[]>;
40
+ /** Terminate the DSH instance on one port (same-origin /multi/api/stop). */
41
+ stop: (port: number) => Promise<StopRow>;
42
+ /** Start a new DSH instance and return its port (same-origin /multi/api/create). */
43
+ create: () => Promise<CreateRow>;
44
+ /** The phone-reachable link for this instance (same-origin /multi/api/link). */
45
+ link: () => Promise<LinkRow>;
46
+ }
47
+ /**
48
+ * Build the probe face bound to this origin's /multi/api routes.
49
+ * @param mount - the API base path ('' at the root, '/multi' when mounted).
50
+ * @returns the injected callbacks.
51
+ */
52
+ export declare function createWallInjected(mount?: string): WallInjected;
53
+ //# sourceMappingURL=wall-injected.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Multi-window wall plugin, node half: registers the `/multi/api/*` routes
3
+ * on the webserver. The browser half fetches these same-origin to discover
4
+ * which local ports are live DSH instances, to poll liveness, and to
5
+ * terminate a chosen instance (`/multi/api/stop`). The wall itself is pure
6
+ * UI — this half answers a few small JSON requests.
7
+ * @module @deepseek-ai/dsh-client-ui-multi-wall
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ import z from '@deepseek-ai/schemastery';
11
+ /** Stable Cordis plugin name. */
12
+ export declare const name = "client-ui-multi-wall";
13
+ /** Services required before the probe routes can be registered. */
14
+ export declare const inject: string[];
15
+ /** Plugin config: the wall's auto-discovery scan range. */
16
+ export interface MultiWallConfig {
17
+ /** First port of the auto-discovery range. */
18
+ scanFrom?: number;
19
+ /** Last port of the auto-discovery range. */
20
+ scanTo?: number;
21
+ /** Optional fixed port list; when set, discovery ignores the scan range. */
22
+ ports?: number[];
23
+ /**
24
+ * External base URL reported by `/multi/api/link` (e.g. the authenticated
25
+ * gateway in front of this loopback instance). When set, the link route
26
+ * answers `{ lan: [publicUrl + '/'], reachable: true }`.
27
+ */
28
+ publicUrl?: string;
29
+ }
30
+ /** Schema-validated config (the Loader resolves defaults for absent keys). */
31
+ export declare const Config: z<Schemastery.ObjectS<{
32
+ scanFrom: z<number, number>;
33
+ scanTo: z<number, number>;
34
+ ports: z<number[], number[]>;
35
+ publicUrl: z<string, string>;
36
+ }>, Schemastery.ObjectT<{
37
+ scanFrom: z<number, number>;
38
+ scanTo: z<number, number>;
39
+ ports: z<number[], number[]>;
40
+ publicUrl: z<string, string>;
41
+ }>>;
42
+ /** One stop result row from /multi/api/stop. */
43
+ export interface StopRow {
44
+ port: number;
45
+ ok: boolean;
46
+ /** Human-readable failure reason (absent on success). */
47
+ error?: string;
48
+ }
49
+ /**
50
+ * Terminate the DSH instance listening on one local port. The port serving
51
+ * this wall may also be terminated (the user may want to stop the instance
52
+ * they are viewing): the kill is deferred a beat so the HTTP response is
53
+ * written before the process dies, then the listener's PIDs are force-killed.
54
+ * @param port - the target port.
55
+ * @param selfPort - this instance's own listening port.
56
+ * @returns the stop result.
57
+ */
58
+ export declare function stopPort(port: number, selfPort: number): Promise<StopRow>;
59
+ /**
60
+ * Register the probe routes. Everything lives under `/multi/api` so the
61
+ * plugin is purely additive: exact `ports` (auto-discovery) and `status`
62
+ * (liveness of a specific port list).
63
+ * @param ctx - plugin context carrying the webServer service.
64
+ * @param config - validated {@link MultiWallConfig}.
65
+ */
66
+ export declare function apply(ctx: Context, config?: MultiWallConfig): void;
67
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Package-owned invariant companion for
3
+ * `@deepseek-ai/dsh-client-ui-multi-wall`.
4
+ * @module @deepseek-ai/dsh-client-ui-multi-wall/invariant
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ /** Cordis companion plugin name. */
8
+ export declare const name = "client-ui-multi-wall-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ export declare const inject: string[];
11
+ /**
12
+ * Register this package's invariant companion.
13
+ * @param ctx - Cordis context carrying the invariant service.
14
+ * @returns the installed registration's disposer after setup succeeds.
15
+ */
16
+ export declare const apply: (ctx: Context) => Promise<() => void>;
17
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-client-ui-multi-wall",
3
+ "description": "Multi-window wall surface: a sidebar footer action that opens a full-screen wall of every running DSH instance (one pane per port), inside the official web GUI. Additive slots only 鈥?no existing row or interaction logic is changed.",
4
+ "version": "0.2.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/client/ui-multi-wall"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./client": {
26
+ "types": "./lib/types/client/index.d.ts",
27
+ "default": "./lib/client.js"
28
+ },
29
+ "./src/*": "./src/*",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "dsh": {
33
+ "client": {
34
+ "inject": [
35
+ "@deepseek-ai/dsh-client-runtime",
36
+ "@deepseek-ai/dsh-client-locale",
37
+ "@deepseek-ai/dsh-client-ui-layout",
38
+ "@deepseek-ai/dsh-client-ui-sidebar",
39
+ "@deepseek-ai/dsh-client-ui-primitives"
40
+ ],
41
+ "platform": "web"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "bundle": "tsdown",
46
+ "watch": "tsdown --watch"
47
+ },
48
+ "license": "MIT",
49
+ "peerDependencies": {
50
+ "@deepseek-ai/dsh-client-locale": "workspace:^",
51
+ "@deepseek-ai/dsh-client-runtime": "workspace:^",
52
+ "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
53
+ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
54
+ "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
55
+ "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
56
+ "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
57
+ "@deepseek-ai/dsh-invariants": "workspace:^",
58
+ "@deepseek-ai/cordis": "workspace:^",
59
+ "@deepseek-ai/schemastery": "workspace:^",
60
+ "react": "^18.2.0"
61
+ },
62
+ "devDependencies": {
63
+ "@deepseek-ai/dsh-client-locale": "workspace:^",
64
+ "@deepseek-ai/dsh-client-runtime": "workspace:^",
65
+ "@deepseek-ai/dsh-client-test-runtime": "workspace:^",
66
+ "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
67
+ "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
68
+ "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^",
69
+ "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
70
+ "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
71
+ "@deepseek-ai/dsh-invariants": "workspace:^",
72
+ "@testing-library/react": "^16.1.0",
73
+ "@types/react": "~18.3.1",
74
+ "@deepseek-ai/cordis": "workspace:^",
75
+ "react": "^18.2.0",
76
+ "react-dom": "^18.2.0"
77
+ },
78
+ "files": [
79
+ "lib/index.js",
80
+ "lib/invariant.js",
81
+ "lib/client.js",
82
+ "lib/types/**/*.d.ts"
83
+ ]
84
+ }
@@ -0,0 +1,35 @@
1
+ ' gateway-hidden.vbs — launch the authenticated gateway with NO console window.
2
+ ' Uses WScript.Shell.Run(..., 0, false) so no conhost window ever flashes.
3
+ '
4
+ ' Usage:
5
+ ' wscript.exe "D:\2026AppDev\dsh-plugins-multi-task\scripts\gateway-hidden.vbs"
6
+ '
7
+ ' Or, to run with custom values, edit the constants below.
8
+ ' The node process is detached (second Run arg = false), so it outlives this
9
+ ' script. The hidden-window flag (0) keeps it fully invisible.
10
+
11
+ Option Explicit
12
+
13
+ ' ---- config ----
14
+ Dim port, target, token, name
15
+ port = 8443
16
+ target = "127.0.0.1:3070"
17
+ token = "dsh2026"
18
+ name = "DSH"
19
+ ' ----------------
20
+
21
+ Dim fso, shell, scriptAbs
22
+ Set fso = CreateObject("Scripting.FileSystemObject")
23
+ Set shell = CreateObject("WScript.Shell")
24
+
25
+ ' Absolute path to gateway.mjs, resolved relative to THIS script file.
26
+ scriptAbs = fso.GetParentFolderName(WScript.ScriptFullName) & "\gateway.mjs"
27
+
28
+ ' Quote the path defensively (spaces etc.).
29
+ scriptAbs = """" & scriptAbs & """"
30
+
31
+ Dim cmd
32
+ cmd = "node " & scriptAbs & " --target " & target & " --listen 0.0.0.0:" & CStr(port) & " --token " & token & " --name " & name
33
+
34
+ ' 0 = hidden window, false = don't wait for it to finish.
35
+ shell.Run cmd, 0, false
@@ -0,0 +1,21 @@
1
+ # gateway-start.ps1 — start the authenticated gateway with no console window.
2
+ # Wrapper around gateway-hidden.vbs (WScript.Shell window style 0 = invisible).
3
+ #
4
+ # Usage:
5
+ # .\scripts\gateway-start.ps1
6
+ #
7
+ # Values come from gateway-hidden.vbs (port 8443 -> 127.0.0.1:3070,
8
+ # token dsh2026). Edit that file to change them.
9
+
10
+ $ErrorActionPreference = 'Stop'
11
+ $root = Split-Path -Parent $PSScriptRoot
12
+ $vbs = Join-Path $root 'scripts\gateway-hidden.vbs'
13
+
14
+ if (-not (Test-Path $vbs)) { throw "未找到启动器:$vbs" }
15
+
16
+ # wscript runs the VBS with a hidden window (no console flash).
17
+ Start-Process -FilePath 'wscript.exe' -ArgumentList ('"' + $vbs + '"') -WindowStyle Hidden | Out-Null
18
+
19
+ Write-Host "网关已在后台静默启动(无窗口)。"
20
+ Write-Host "手机访问:http://<局域网IP>:8443 (口令 dsh2026)"
21
+ Write-Host "停止:.\scripts\gateway-stop.ps1"
@@ -0,0 +1,34 @@
1
+ # gateway-stop.ps1 — stop the authenticated gateway (finds the node process
2
+ # listening on the gateway port and terminates it).
3
+ #
4
+ # Usage:
5
+ # .\scripts\gateway-stop.ps1 # default port 8443
6
+ # .\scripts\gateway-stop.ps1 -Port 8443
7
+
8
+ param(
9
+ [int]$Port = 8443
10
+ )
11
+
12
+ $ErrorActionPreference = 'Stop'
13
+
14
+ $conns = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
15
+ if (-not $conns) {
16
+ Write-Host "端口 $Port 上没有正在监听的网关进程。"
17
+ exit 0
18
+ }
19
+
20
+ $pids = $conns | Select-Object -ExpandProperty OwningProcess -Unique
21
+ foreach ($pid in $pids) {
22
+ $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
23
+ if ($proc) {
24
+ Stop-Process -Id $pid -Force
25
+ Write-Host "已停止网关进程 (PID $pid, $($proc.ProcessName))。"
26
+ }
27
+ }
28
+
29
+ Start-Sleep -Milliseconds 300
30
+ if (Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) {
31
+ Write-Warning "端口 $Port 仍有监听,可能未完全停止。"
32
+ } else {
33
+ Write-Host "端口 $Port 已释放。"
34
+ }