Skill: jiwannian/ningyao-voice-launcher (version 0.1.0)

--- name: ningyao-voice-launcher description: Install and configure a local browser-based Chinese voice chat launcher with the Ning Yao persona, including one-click Windows launchers, browser speech I/O, screen awareness, and a safe terminal panel. Use when the user wants a local voice companion, a persona-tuned voice chat setup, or help packaging/deploying the Ning Yao voice launcher. --- # Ningyao Voice Launcher Install the bundled launcher into a user-chosen folder, then configure `.env` and run it. ## Workflow 1. Copy `assets/voice-chat-local` into the target folder. 2. Install dependencies with `npm install` in the copied folder. 3. Copy `.env.example` to `.env` and fill in `OPENAI_API_KEY`. 4. Start with `start-voice-chat.cmd` or `start-voice-chat-bg.cmd`. ## Bundled Files - `assets/voice-chat-local`: Browser voice chat app template. - `scripts/install-launcher.ps1`: Windows installer/copy helper. ## Windows Install Run: ```powershell powershell -ExecutionPolicy Bypass -File scripts/install-launcher.ps1 -Destination "$env:USERPROFILE\Desktop\ningyao-voice-chat" ``` The installer copies the template, skips `.env`, and can optionally run `npm install`. ## Configuration Set these in `.env`: - `OPENAI_API_KEY`: required - `OPENAI_BASE_URL`: optional API-compatible endpoint - `OPENAI_MODEL`: text + vision capable model if screen analysis is needed - `OPENAI_TIMEOUT_MS`: request timeout - `PORT`: local port - `SYSTEM_PROMPT`: persona prompt ## Notes - Prefer Chrome or Edge for browser speech recognition and speech synthesis. - The terminal panel is intentionally restricted to a small whitelist. - If screen analysis is enabled, choose a model that supports image input.

path: assets/voice-chat-local/package-lock.json
content: {
  "name": "voice-chat-local",
  "version": "0.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "voice-chat-local",
      "version": "0.1.0",
      "dependencies": {
        "express": "^4.21.2",
        "openai": "^4.89.0"
      }
    },
    "node_modules/@types/node": {
      "version": "18.19.130",
      "resolved": "https://registry.npmmirror.com/@types/node/-/node-18.19.130.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "undici-types": "~5.26.4"
      }
    },
    "node_modules/@types/node-fetch": {
      "version": "2.6.13",
      "resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "@types/node": "*",
        "form-data": "^4.0.4"
      }
    },
    "node_modules/abort-controller": {
      "version": "3.0.0",
      "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "event-target-shim": "^5.0.0"
      },
      "engines": {
        "node": ">=6.5"
      }
    },
    "node_modules/accepts": {
      "version": "1.3.8",
      "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "~2.1.34",
        "negotiator": "0.6.3"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/agentkeepalive": {
      "version": "4.6.0",
      "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "humanize-ms": "^1.2.1"
      },
      "engines": {
        "node": ">= 8.0.0"
      }
    },
    "node_modules/array-flatten": {
      "version": "1.1.1",
      "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/body-parser": {
      "version": "1.20.4",
      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "content-type": "~1.0.5",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "~1.2.0",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "on-finished": "~2.4.1",
        "qs": "~6.14.0",
        "raw-body": "~2.5.3",
        "type-is": "~1.6.18",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/content-disposition": {
      "version": "0.5.4",
      "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "5.2.1"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.0.7",
      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/debug": {
      "version": "2.6.9",
      "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "2.0.0"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/destroy": {
      "version": "1.2.0",
      "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8",
        "npm": "1.2.8000 || >= 1.4.16"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/event-target-shim": {
      "version": "5.0.1",
      "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/express": {
      "version": "4.22.1",
      "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "accepts": "~1.3.8",
        "array-flatten": "1.1.1",
        "body-parser": "~1.20.3",
        "content-disposition": "~0.5.4",
        "content-type": "~1.0.4",
        "cookie": "~0.7.1",
        "cookie-signature": "~1.0.6",
        "debug": "2.6.9",
        "depd": "2.0.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "finalhandler": "~1.3.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.0",
        "merge-descriptors": "1.0.3",
        "methods": "~1.1.2",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "path-to-regexp": "~0.1.12",
        "proxy-addr": "~2.0.7",
        "qs": "~6.14.0",
        "range-parser": "~1.2.1",
        "safe-buffer": "5.2.1",
        "send": "~0.19.0",
        "serve-static": "~1.16.2",
        "setprototypeof": "1.2.0",
        "statuses": "~2.0.1",
        "type-is": "~1.6.18",
        "utils-merge": "1.0.1",
        "vary": "~1.1.2"
      },
      "engines": {
        "node": ">= 0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/finalhandler": {
      "version": "1.3.2",
      "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "on-finished": "~2.4.1",
        "parseurl": "~1.3.3",
        "statuses": "~2.0.2",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/form-data-encoder": {
      "version": "1.7.2",
      "resolved": "https://registry.npmmirror.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/formdata-node": {
      "version": "4.4.1",
      "resolved": "https://registry.npmmirror.com/formdata-node/-/formdata-node-4.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "node-domexception": "1.0.0",
        "web-streams-polyfill": "4.0.0-beta.3"
      },
      "engines": {
        "node": ">= 12.20"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "0.5.2",
      "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/humanize-ms": {
      "version": "1.2.1",
      "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.0.0"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.4.24",
      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "0.3.0",
      "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "1.0.3",
      "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/methods": {
      "version": "1.1.2",
      "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime": {
      "version": "1.6.0",
      "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "bin": {
        "mime": "cli.js"
      },
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ms": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "0.6.3",
      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/node-domexception": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Use your platform's native DOMException instead",
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/jimmywarting"
        },
        {
          "type": "github",
          "url": "https://paypal.me/jimmywarting"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=10.5.0"
      }
    },
    "node_modules/node-fetch": {
      "version": "2.7.0",
      "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "whatwg-url": "^5.0.0"
      },
      "engines": {
        "node": "4.x || >=6.0.0"
      },
      "peerDependencies": {
        "encoding": "^0.1.0"
      },
      "peerDependenciesMeta": {
        "encoding": {
          "optional": true
        }
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/openai": {
      "version": "4.104.0",
      "resolved": "https://registry.npmmirror.com/openai/-/openai-4.104.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "@types/node": "^18.11.18",
        "@types/node-fetch": "^2.6.4",
        "abort-controller": "^3.0.0",
        "agentkeepalive": "^4.2.1",
        "form-data-encoder": "1.7.2",
        "formdata-node": "^4.3.2",
        "node-fetch": "^2.6.7"
      },
      "bin": {
        "openai": "bin/cli"
      },
      "peerDependencies": {
        "ws": "^8.18.0",
        "zod": "^3.23.8"
      },
      "peerDependenciesMeta": {
        "ws": {
          "optional": true
        },
        "zod": {
          "optional": true
        }
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "0.1.12",
      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.14.2",
      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "2.5.3",
      "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.4.24",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "0.19.2",
      "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "2.6.9",
        "depd": "2.0.0",
        "destroy": "1.2.0",
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "etag": "~1.8.1",
        "fresh": "~0.5.2",
        "http-errors": "~2.0.1",
        "mime": "1.6.0",
        "ms": "2.1.3",
        "on-finished": "~2.4.1",
        "range-parser": "~1.2.1",
        "statuses": "~2.0.2"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/send/node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/serve-static": {
      "version": "1.16.3",
      "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "~2.0.0",
        "escape-html": "~1.0.3",
        "parseurl": "~1.3.3",
        "send": "~0.19.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3",
        "side-channel-list": "^1.0.0",
        "side-channel-map": "^1.0.1",
        "side-channel-weakmap": "^1.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3",
        "side-channel-map": "^1.0.1"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/tr46": {
      "version": "0.0.3",
      "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/type-is": {
      "version": "1.6.18",
      "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "media-typer": "0.3.0",
        "mime-types": "~2.1.24"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/undici-types": {
      "version": "5.26.5",
      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/utils-merge": {
      "version": "1.0.1",
      "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4.0"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/web-streams-polyfill": {
      "version": "4.0.0-beta.3",
      "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 14"
      }
    },
    "node_modules/webidl-conversions": {
      "version": "3.0.1",
      "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/whatwg-url": {
      "version": "5.0.0",
      "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "tr46": "~0.0.3",
        "webidl-conversions": "^3.0.0"
      }
    }
  }
}


path: assets/voice-chat-local/package.json
content: {
  "name": "voice-chat-local",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.21.2",
    "openai": "^4.89.0"
  }
}


path: assets/voice-chat-local/public/app.js
content: const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const toggleListenButton = document.getElementById('toggleListenButton');
const toggleShareButton = document.getElementById('toggleShareButton');
const stopButton = document.getElementById('stopButton');
const clearButton = document.getElementById('clearButton');
const presetButton = document.getElementById('presetButton');
const terminalInputNode = document.getElementById('terminalInput');
const runTerminalButton = document.getElementById('runTerminalButton');
const terminalOutputNode = document.getElementById('terminalOutput');
const statusNode = document.getElementById('status');
const screenStatusNode = document.getElementById('screenStatus');
const screenSummaryNode = document.getElementById('screenSummary');
const screenPreviewNode = document.getElementById('screenPreview');
const messagesNode = document.getElementById('messages');
const autoSpeakNode = document.getElementById('autoSpeak');
const rateRangeNode = document.getElementById('rateRange');
const pitchRangeNode = document.getElementById('pitchRange');
const rateValueNode = document.getElementById('rateValue');
const pitchValueNode = document.getElementById('pitchValue');

const history = [];
let recognition;
let isRecording = false;
let isSpeaking = false;
let callModeActive = false;
let pendingStopTimer = null;
let restartTimer = null;
let silenceTimer = null;
let liveTranscript = '';
let finalizedTranscript = '';
let requestInFlight = false;
let availableVoices = [];
let selectedVoiceName = localStorage.getItem('voice-chat-selected-voice') || '';
let stylePreset = 'ningyao';
let voiceRate = Number(localStorage.getItem('voice-chat-rate') || 0.92);
let voicePitch = Number(localStorage.getItem('voice-chat-pitch') || 0.92);
let screenStream = null;
let screenTrack = null;
let screenCaptureTimer = null;
let screenBusy = false;
let latestScreenSummary = '';
let micStream = null;
let micAudioContext = null;
let micAnalyser = null;
let micDataArray = null;
let micMonitorFrame = null;
let lastInterruptAt = 0;

function setStatus(text) {
  statusNode.textContent = text;
}

function setScreenStatus(text) {
  screenStatusNode.textContent = text;
}

function setTerminalOutput(text) {
  terminalOutputNode.textContent = text;
}

function renderMessage(role, content) {
  const item = document.createElement('article');
  item.className = `message ${role}`;
  item.innerHTML = `<span class="role">${role === 'user' ? '你' : '宁姚'}</span><div>${content}</div>`;
  messagesNode.appendChild(item);
  item.scrollIntoView({ behavior: 'smooth', block: 'end' });
}

function clearTimer(name) {
  if (name === 'pendingStop' && pendingStopTimer) {
    clearTimeout(pendingStopTimer);
    pendingStopTimer = null;
  }
  if (name === 'restart' && restartTimer) {
    clearTimeout(restartTimer);
    restartTimer = null;
  }
  if (name === 'silence' && silenceTimer) {
    clearTimeout(silenceTimer);
    silenceTimer = null;
  }
}

function clearAllTimers() {
  clearTimer('pendingStop');
  clearTimer('restart');
  clearTimer('silence');
}

function updateCallButton() {
  toggleListenButton.textContent = callModeActive ? '结束通话' : '开始通话';
  toggleListenButton.classList.toggle('active', callModeActive);
}

function updateShareButton() {
  const active = Boolean(screenStream);
  toggleShareButton.textContent = active ? '停止共享屏幕' : '开始共享屏幕';
  toggleShareButton.classList.toggle('active', active);
}

function updatePresetButton() {
  presetButton.classList.add('active');
  presetButton.textContent = '宁姚风格普通话';
}

function updateSliderLabels() {
  rateValueNode.textContent = Number(voiceRate).toFixed(2);
  pitchValueNode.textContent = Number(voicePitch).toFixed(2);
  rateRangeNode.value = String(voiceRate);
  pitchRangeNode.value = String(voicePitch);
}

function persistVoiceTuning() {
  localStorage.setItem('voice-chat-style-preset', stylePreset);
  localStorage.setItem('voice-chat-rate', String(voiceRate));
  localStorage.setItem('voice-chat-pitch', String(voicePitch));
  localStorage.setItem('voice-chat-selected-voice', selectedVoiceName);
}

function stopSpeaking() {
  window.speechSynthesis.cancel();
  isSpeaking = false;
}

function preferredVoiceOrder() {
  return [
    'Microsoft Xiaoyi Online (Natural) - Chinese (Mainland)',
    'Microsoft Xiaoxiao Online (Natural) - Chinese (Mainland)',
    'Microsoft Yunxi Online (Natural) - Chinese (Mainland)',
    'Google 中文（普通话）',
    'Google 普通话（中国大陆）'
  ];
}

function sortVoices(voices) {
  const order = preferredVoiceOrder();
  return [...voices].sort((a, b) => {
    const ai = order.indexOf(a.name);
    const bi = order.indexOf(b.name);
    if (ai !== -1 || bi !== -1) {
      if (ai === -1) return 1;
      if (bi === -1) return -1;
      return ai - bi;
    }
    return a.name.localeCompare(b.name, 'zh-CN');
  });
}

function populateVoices() {
  const zhVoices = window.speechSynthesis.getVoices().filter(voice => voice.lang?.toLowerCase().includes('zh'));
  availableVoices = sortVoices(zhVoices);
  if (!selectedVoiceName) {
    selectedVoiceName = availableVoices[0]?.name || '';
    persistVoiceTuning();
  }
}

function pickVoice() {
  if (!availableVoices.length) return null;
  const exact = availableVoices.find(voice => voice.name === selectedVoiceName);
  return exact || availableVoices[0] || null;
}

function applyNingyaoPreset() {
  stylePreset = 'ningyao';
  voiceRate = 0.92;
  voicePitch = 0.92;
  populateVoices();
  selectedVoiceName = availableVoices[0]?.name || '';
  persistVoiceTuning();
  updateSliderLabels();
  const voice = pickVoice();
  setStatus(voice ? `已锁定宁姚风格普通话: ${voice.name}` : '已锁定宁姚风格普通话');
}

async function ensureMicMonitor() {
  if (micAnalyser) return true;
  try {
    micStream = await navigator.mediaDevices.getUserMedia({
      audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
      video: false
    });
    micAudioContext = new AudioContext();
    const source = micAudioContext.createMediaStreamSource(micStream);
    micAnalyser = micAudioContext.createAnalyser();
    micAnalyser.fftSize = 1024;
    micDataArray = new Uint8Array(micAnalyser.fftSize);
    source.connect(micAnalyser);
    return true;
  } catch (_error) {
    setStatus('没拿到麦克风权限，无法自动打断');
    return false;
  }
}

function stopMicMonitorLoop() {
  if (micMonitorFrame) {
    cancelAnimationFrame(micMonitorFrame);
    micMonitorFrame = null;
  }
}

function measureMicLevel() {
  if (!micAnalyser || !micDataArray) return 0;
  micAnalyser.getByteTimeDomainData(micDataArray);
  let sum = 0;
  for (const value of micDataArray) {
    const normalized = (value - 128) / 128;
    sum += normalized * normalized;
  }
  return Math.sqrt(sum / micDataArray.length);
}

function startMicMonitorLoop() {
  stopMicMonitorLoop();
  const tick = () => {
    if (!callModeActive) {
      stopMicMonitorLoop();
      return;
    }
    const level = measureMicLevel();
    const now = Date.now();
    if (isSpeaking && !isRecording && !requestInFlight && level > 0.085 && now - lastInterruptAt > 1200) {
      lastInterruptAt = now;
      stopSpeaking();
      setStatus('听到你开口了，正在听');
      startRecording();
    }
    micMonitorFrame = requestAnimationFrame(tick);
  };
  micMonitorFrame = requestAnimationFrame(tick);
}

function scheduleAutoListen(delay = 220) {
  clearTimer('restart');
  if (!callModeActive || requestInFlight || isRecording || isSpeaking) return;
  restartTimer = window.setTimeout(() => startRecording(), delay);
}

function resetTranscriptState() {
  liveTranscript = '';
  finalizedTranscript = '';
}

function finishTurn() {
  const content = (finalizedTranscript || liveTranscript).trim();
  resetTranscriptState();
  if (!content) {
    if (callModeActive && !requestInFlight && !isSpeaking) {
      setStatus('待机中，等你开口');
      scheduleAutoListen(120);
    } else if (!callModeActive) {
      setStatus('未识别到内容');
    }
    return;
  }
  sendMessage(content);
}

function speak(text) {
  stopSpeaking();
  const utterance = new SpeechSynthesisUtterance(text);
  const voice = pickVoice();
  utterance.lang = voice?.lang || 'zh-CN';
  utterance.voice = voice || null;
  utterance.rate = voiceRate;
  utterance.pitch = voicePitch;
  utterance.onstart = () => {
    isSpeaking = true;
    setStatus(voice ? `宁姚在说: ${voice.name}` : '宁姚在说');
  };
  utterance.onend = () => {
    isSpeaking = false;
    if (callModeActive) {
      setStatus('待机中，等你开口');
      scheduleAutoListen(120);
    } else if (!isRecording) {
      setStatus('等你开口');
    }
  };
  utterance.onerror = () => {
    isSpeaking = false;
    if (callModeActive) {
      setStatus('朗读失败，已回到待机');
      scheduleAutoListen(120);
    } else if (!isRecording) {
      setStatus('朗读失败');
    }
  };
  window.speechSynthesis.speak(utterance);
}

async function sendMessage(content) {
  requestInFlight = true;
  history.push({ role: 'user', content });
  renderMessage('user', content);
  setStatus('宁姚在想');
  try {
    const response = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ history, stylePreset, screenSummary: latestScreenSummary })
    });
    const data = await response.json();
    if (!response.ok) throw new Error(data.message || data.error || 'request_failed');
    history.push({ role: 'assistant', content: data.reply });
    renderMessage('assistant', data.reply);
    setStatus('已回复');
    if (autoSpeakNode.checked) speak(data.reply);
    else if (callModeActive) {
      setStatus('待机中，等你开口');
      scheduleAutoListen(120);
    }
  } catch (error) {
    const message = error.message || '请求失败';
    renderMessage('assistant', `出错了：${message}`);
    setStatus(message);
    if (callModeActive) scheduleAutoListen(300);
  } finally {
    requestInFlight = false;
  }
}

async function runTerminalCommand() {
  const command = terminalInputNode.value.trim();
  if (!command) {
    setTerminalOutput('先输入一条白名单命令。');
    return;
  }
  runTerminalButton.disabled = true;
  setTerminalOutput(`> ${command}\n\n执行中...`);
  try {
    const response = await fetch('/api/terminal', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ command })
    });
    const data = await response.json();
    if (!response.ok) throw new Error(data.message || data.error || 'terminal_failed');
    const output = [`> ${data.command}`, `[${data.mode}]`];
    if (data.stdout) output.push('', data.stdout);
    if (data.stderr) output.push('', data.stderr);
    setTerminalOutput(output.join('\n'));
  } catch (error) {
    setTerminalOutput(`> ${command}\n\n${error.message || '终端执行失败'}`);
  } finally {
    runTerminalButton.disabled = false;
  }
}

function ensureRecognition() {
  if (!SpeechRecognition) {
    setStatus('当前浏览器不支持语音识别，建议用 Chrome');
    toggleListenButton.disabled = true;
    return null;
  }
  if (recognition) return recognition;
  recognition = new SpeechRecognition();
  recognition.lang = 'zh-CN';
  recognition.interimResults = true;
  recognition.continuous = true;
  recognition.maxAlternatives = 1;
  recognition.onstart = () => {
    isRecording = true;
    resetTranscriptState();
    setStatus(callModeActive ? '待机中，正在听' : '正在听');
  };
  recognition.onend = () => {
    clearAllTimers();
    const hadSpeech = Boolean((finalizedTranscript || liveTranscript).trim());
    isRecording = false;
    if (hadSpeech) {
      finishTurn();
      return;
    }
    resetTranscriptState();
    if (callModeActive && !requestInFlight && !isSpeaking) {
      setStatus('待机中，等你开口');
      scheduleAutoListen(120);
    } else if (!callModeActive) {
      setStatus('未识别到内容');
    }
  };
  recognition.onerror = event => {
    clearAllTimers();
    isRecording = false;
    const recoverable = ['no-speech', 'aborted'].includes(event.error);
    if (callModeActive && recoverable) {
      setStatus('待机中，等你开口');
      scheduleAutoListen(120);
      return;
    }
    setStatus(`识别失败: ${event.error}`);
  };
  recognition.onresult = event => {
    clearTimer('silence');
    const results = Array.from(event.results);
    liveTranscript = results.map(result => result[0]?.transcript || '').join('').trim();
    finalizedTranscript = results.filter(result => result.isFinal).map(result => result[0]?.transcript || '').join('').trim();
    if (isSpeaking && liveTranscript) {
      stopSpeaking();
      setStatus('已打断，正在听');
    }
    if (liveTranscript && !finalizedTranscript) setStatus('正在听: ' + liveTranscript);
    silenceTimer = window.setTimeout(() => {
      if (isRecording) recognition.stop();
    }, finalizedTranscript ? 180 : 650);
  };
  return recognition;
}

function stopScreenShare() {
  if (screenCaptureTimer) {
    clearInterval(screenCaptureTimer);
    screenCaptureTimer = null;
  }
  if (screenTrack) {
    screenTrack.onended = null;
    screenTrack.stop();
    screenTrack = null;
  }
  if (screenStream) {
    for (const track of screenStream.getTracks()) track.stop();
    screenStream = null;
  }
  screenPreviewNode.srcObject = null;
  latestScreenSummary = '';
  screenSummaryNode.textContent = '还没有屏幕内容。';
  setScreenStatus('未共享');
  updateShareButton();
}

function captureScreenFrame() {
  if (!screenPreviewNode.videoWidth || !screenPreviewNode.videoHeight) return null;
  const canvas = document.createElement('canvas');
  const width = Math.min(1280, screenPreviewNode.videoWidth);
  const height = Math.round(width * screenPreviewNode.videoHeight / screenPreviewNode.videoWidth);
  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext('2d');
  context.drawImage(screenPreviewNode, 0, 0, width, height);
  return canvas.toDataURL('image/jpeg', 0.72);
}

async function analyzeScreenFrame() {
  if (!screenStream || screenBusy) return;
  const image = captureScreenFrame();
  if (!image) return;
  screenBusy = true;
  setScreenStatus('正在识别');
  try {
    const response = await fetch('/api/screen', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ image, stylePreset })
    });
    const data = await response.json();
    if (!response.ok) throw new Error(data.message || data.error || 'screen_failed');
    latestScreenSummary = data.summary || '';
    screenSummaryNode.textContent = latestScreenSummary || '没识别到明确内容。';
    setScreenStatus('已识别');
  } catch (error) {
    setScreenStatus('识别失败');
    screenSummaryNode.textContent = '屏幕识别失败：' + (error.message || '请求失败');
  } finally {
    screenBusy = false;
  }
}

async function startScreenShare() {
  if (!navigator.mediaDevices?.getDisplayMedia) {
    setScreenStatus('当前浏览器不支持屏幕共享');
    return;
  }
  try {
    screenStream = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 8 }, audio: false });
    screenTrack = screenStream.getVideoTracks()[0] || null;
    if (screenTrack) screenTrack.onended = () => stopScreenShare();
    screenPreviewNode.srcObject = screenStream;
    await screenPreviewNode.play();
    setScreenStatus('共享中，更快识别');
    updateShareButton();
    await analyzeScreenFrame();
    screenCaptureTimer = window.setInterval(() => analyzeScreenFrame(), 2200);
  } catch (_error) {
    stopScreenShare();
    setScreenStatus('共享失败');
    screenSummaryNode.textContent = '没有拿到屏幕权限，或者你取消了共享。';
  }
}

const recognizer = ensureRecognition();

function startRecording() {
  if (!recognizer || isRecording || requestInFlight) return;
  clearAllTimers();
  if (isSpeaking) {
    stopSpeaking();
    setStatus('已打断，正在听');
  }
  recognizer.start();
}

async function startCallMode() {
  callModeActive = true;
  updateCallButton();
  await ensureMicMonitor();
  startMicMonitorLoop();
  setStatus('待机中，等你开口');
  scheduleAutoListen(80);
}

function stopCallMode() {
  callModeActive = false;
  updateCallButton();
  clearAllTimers();
  stopMicMonitorLoop();
  if (isRecording) recognition.stop();
  stopSpeaking();
  setStatus('通话已结束');
}

toggleListenButton.addEventListener('click', async () => {
  if (callModeActive) stopCallMode();
  else await startCallMode();
});

toggleShareButton.addEventListener('click', async () => {
  if (screenStream) stopScreenShare();
  else await startScreenShare();
});

stopButton.addEventListener('click', () => {
  stopSpeaking();
  if (callModeActive) {
    setStatus('已停止朗读，回到待机');
    scheduleAutoListen(80);
  } else {
    setStatus('已停止朗读');
  }
});

clearButton.addEventListener('click', () => {
  history.length = 0;
  messagesNode.innerHTML = '';
  clearAllTimers();
  stopSpeaking();
  resetTranscriptState();
  setStatus(callModeActive ? '待机中，等你开口' : '已清空');
  if (callModeActive) scheduleAutoListen(100);
});

presetButton.addEventListener('click', applyNingyaoPreset);
runTerminalButton.addEventListener('click', runTerminalCommand);
terminalInputNode.addEventListener('keydown', event => {
  if (event.key === 'Enter') runTerminalCommand();
});

rateRangeNode.addEventListener('input', () => {
  voiceRate = Number(rateRangeNode.value);
  updateSliderLabels();
  persistVoiceTuning();
  setStatus(`语速已调到 ${voiceRate.toFixed(2)}`);
});

pitchRangeNode.addEventListener('input', () => {
  voicePitch = Number(pitchRangeNode.value);
  updateSliderLabels();
  persistVoiceTuning();
  setStatus(`音调已调到 ${voicePitch.toFixed(2)}`);
});

window.speechSynthesis.onvoiceschanged = () => {
  populateVoices();
  updatePresetButton();
};

fetch('/api/health').then(res => res.json()).then(data => {
  if (!data.configured) {
    renderMessage('assistant', '还没配置 API key。先按说明填好 `.env`，我才能真正开口。');
    setStatus('缺少 API key');
    return;
  }
  populateVoices();
  if (!selectedVoiceName) applyNingyaoPreset();
  else {
    persistVoiceTuning();
    updateSliderLabels();
    updatePresetButton();
    setStatus('等你开口');
  }
  updateCallButton();
  updateShareButton();
  setScreenStatus('未共享');
  setTerminalOutput('可用示例:\n- dir /b\n- git status\n- git log\n- python --version\n- node -v');
}).catch(() => {
  setStatus('服务未启动');
});

populateVoices();
updateSliderLabels();
updatePresetButton();
updateShareButton();


path: assets/voice-chat-local/public/index.html
content: <!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>宁姚语音对话</title>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <main class="app">
      <section class="panel hero">
        <p class="eyebrow">Local Voice Chat</p>
        <h1>宁姚语音对话</h1>
        <p class="intro">点一次开始免按住待机。你开口时我听，你停下时我答；如果我还在朗读，你直接说话也能把我打断。</p>
        <div class="controls">
          <button id="toggleListenButton" type="button">开始通话</button>
          <button id="toggleShareButton" type="button" class="secondary">开始共享屏幕</button>
          <button id="stopButton" type="button" class="ghost">停止朗读</button>
        </div>
        <div class="status-row">
          <span id="status">未开始</span>
          <div class="voice-tools">
            <label class="voice-toggle"><input id="autoSpeak" type="checkbox" checked /> 自动朗读回复</label>
            <button id="presetButton" type="button" class="preset-button active">宁姚风格普通话</button>
          </div>
        </div>
        <div class="tuning-row">
          <label class="slider-group" for="rateRange">
            <span>语速</span>
            <input id="rateRange" type="range" min="0.7" max="1.15" step="0.01" value="0.92" />
            <output id="rateValue">0.92</output>
          </label>
          <label class="slider-group" for="pitchRange">
            <span>音调</span>
            <input id="pitchRange" type="range" min="0.75" max="1.15" step="0.01" value="0.92" />
            <output id="pitchValue">0.92</output>
          </label>
        </div>
      </section>

      <section class="panel screen-panel">
        <div class="transcript-head">
          <h2>屏幕感知</h2>
          <span id="screenStatus" class="screen-status">未共享</span>
        </div>
        <p class="screen-intro">共享后，我会更快地连续看你的屏幕，提取当前页面的重点信息。你也可以直接问我“现在屏幕上有什么”。</p>
        <video id="screenPreview" class="screen-preview" autoplay muted playsinline></video>
        <div id="screenSummary" class="screen-summary">还没有屏幕内容。</div>
      </section>

      <section class="panel terminal-panel">
        <div class="transcript-head">
          <h2>安全终端</h2>
          <span class="screen-status">只读 + 开发白名单</span>
        </div>
        <p class="screen-intro">这里只能跑受限命令。默认只读，开发命令也只开放一小组安全白名单。</p>
        <div class="terminal-controls">
          <input id="terminalInput" class="terminal-input" type="text" placeholder="例如: dir /b 或 git status" />
          <button id="runTerminalButton" type="button" class="secondary">执行</button>
        </div>
        <pre id="terminalOutput" class="terminal-output">终端还没运行命令。</pre>
      </section>

      <section class="panel transcript-panel">
        <div class="transcript-head">
          <h2>对话</h2>
          <button id="clearButton" type="button" class="ghost">清空</button>
        </div>
        <div id="messages" class="messages"></div>
      </section>
    </main>
    <script src="/app.js" type="module"></script>
  </body>
</html>


path: assets/voice-chat-local/public/styles.css
content: :root {
  --bg: #f4efe7;
  --panel: rgba(255, 250, 243, 0.88);
  --panel-border: rgba(71, 52, 33, 0.16);
  --text: #24180f;
  --muted: #6c5948;
  --accent: #9c3d1e;
  --accent-strong: #7f2f15;
  --shadow: 0 20px 60px rgba(68, 44, 21, 0.12);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: Georgia, "Noto Serif SC", serif;
  color: var(--text);
  background:
    radial-gradient(circle at top, rgba(177, 84, 36, 0.16), transparent 28%),
    linear-gradient(135deg, #efe4d1, #f8f4ee 48%, #e6d9c8);
}

.app {
  width: min(960px, calc(100% - 32px));
  margin: 32px auto;
  display: grid;
  gap: 20px;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 24px;
  box-shadow: var(--shadow);
  backdrop-filter: blur(10px);
}

.hero {
  padding: 28px;
}

.eyebrow {
  margin: 0 0 8px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
  font-size: 12px;
}

h1, h2 {
  margin: 0;
  font-weight: 600;
}

h1 {
  font-size: clamp(32px, 5vw, 52px);
}

h2 {
  font-size: 22px;
}

.intro {
  max-width: 700px;
  color: var(--muted);
  line-height: 1.6;
}

.controls, .status-row, .transcript-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.controls {
  margin-top: 24px;
}

button {
  border: 0;
  border-radius: 999px;
  padding: 14px 22px;
  font: inherit;
  cursor: pointer;
  transition: transform 120ms ease, background 120ms ease, opacity 120ms ease;
}

button:hover {
  transform: translateY(-1px);
}

button:active {
  transform: translateY(0);
}

#toggleListenButton {
  background: linear-gradient(135deg, var(--accent), #c6622f);
  color: #fff8f2;
}

#toggleListenButton.active {
  background: linear-gradient(135deg, #6f2712, var(--accent));
}

#toggleShareButton.active {
  background: linear-gradient(135deg, #6f2712, var(--accent));
  color: #fff8f2;
}

.secondary {
  background: #eadfce;
  color: var(--text);
}

.ghost {
  background: transparent;
  border: 1px solid var(--panel-border);
  color: var(--muted);
}

.status-row {
  margin-top: 16px;
  color: var(--muted);
}

.voice-tools {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.voice-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.preset-button {
  background: linear-gradient(135deg, #3d190f, #6a2815);
  color: #fff7f0;
  padding: 10px 16px;
}

.tuning-row {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(2, minmax(220px, 1fr));
  gap: 14px;
}

.slider-group {
  display: grid;
  grid-template-columns: 44px 1fr 46px;
  align-items: center;
  gap: 10px;
  color: var(--muted);
}

.slider-group input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

.slider-group output {
  text-align: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.screen-panel,
.terminal-panel,
.transcript-panel {
  padding: 24px;
}

.screen-intro {
  margin: 14px 0 16px;
  color: var(--muted);
  line-height: 1.6;
}

.screen-status {
  color: var(--muted);
}

.screen-preview {
  width: 100%;
  border-radius: 18px;
  border: 1px solid var(--panel-border);
  background: rgba(36, 24, 15, 0.08);
  aspect-ratio: 16 / 9;
  object-fit: contain;
}

.screen-summary {
  margin-top: 14px;
  min-height: 52px;
  padding: 14px 16px;
  border-radius: 16px;
  background: rgba(71, 52, 33, 0.08);
  line-height: 1.6;
  color: var(--text);
}

.terminal-controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

.terminal-input {
  flex: 1 1 320px;
  border: 1px solid var(--panel-border);
  background: rgba(255, 250, 243, 0.92);
  color: var(--text);
  border-radius: 14px;
  padding: 12px 14px;
  font: inherit;
}

.terminal-output {
  margin: 14px 0 0;
  min-height: 120px;
  max-height: 320px;
  overflow: auto;
  padding: 14px 16px;
  border-radius: 16px;
  background: #221711;
  color: #f3e7d6;
  font: 13px/1.5 Consolas, "Courier New", monospace;
  white-space: pre-wrap;
}

.messages {
  margin-top: 20px;
  display: grid;
  gap: 12px;
}

.message {
  padding: 14px 16px;
  border-radius: 18px;
  line-height: 1.6;
}

.message.user {
  background: rgba(156, 61, 30, 0.1);
  margin-left: 10%;
}

.message.assistant {
  background: rgba(71, 52, 33, 0.08);
  margin-right: 10%;
}

.message .role {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

@media (max-width: 640px) {
  .app {
    width: min(100% - 20px, 960px);
    margin: 10px auto 20px;
  }

  .hero, .transcript-panel {
    padding: 18px;
  }

  .tuning-row {
    grid-template-columns: 1fr;
  }

  .slider-group {
    grid-template-columns: 44px 1fr 42px;
  }

  .message.user, .message.assistant {
    margin: 0;
  }
}


path: assets/voice-chat-local/README.md
content: # voice-chat-local

本地浏览器语音对话小工具。

## 功能

- 浏览器麦克风语音输入
- 调用 OpenAI 文本模型生成回复
- 浏览器直接朗读回复
- 说话时支持更自然的停顿结束
- 朗读中可以直接按住说话打断
- 本地运行，不走电话线路

## 准备

1. 进入目录：`cd voice-chat-local`
2. 安装依赖：`cmd /c npm install`
3. 复制配置：`copy .env.example .env`
4. 编辑 `.env`，填入 `OPENAI_API_KEY`
5. 启动：`node --env-file=.env server.js`
6. 打开：`http://localhost:3030`

也可以直接双击：
- `voice-chat-local/start-voice-chat.cmd`：前台启动，窗口会保留
- `voice-chat-local/start-voice-chat-bg.cmd`：后台最小化启动，并自动打开页面

## 说明

- 语音识别和语音朗读依赖浏览器，推荐 Chrome 或 Edge。
- 如果想换模型，改 `.env` 里的 `OPENAI_MODEL`。
- 如果想改说话风格，改 `.env` 里的 `SYSTEM_PROMPT`。


path: assets/voice-chat-local/server.js
content: import express from 'express';
import OpenAI from 'openai';
import path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { fileURLToPath } from 'url';

const execFileAsync = promisify(execFile);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const workspaceDir = __dirname;
const app = express();
const port = Number(process.env.PORT || 3030);
const model = process.env.OPENAI_MODEL || 'gpt-4.1-mini';
const requestTimeoutMs = Number(process.env.OPENAI_TIMEOUT_MS || 15000);
const baseURL = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
const systemPrompt = process.env.SYSTEM_PROMPT || '你叫宁姚。语气冷静、可靠、有骨气，不油滑，不轻浮。用中文简洁回答，像在和熟悉的人并肩说话。';
const [REDACTED_SECRET];
const client = apiKey ? new OpenAI({ apiKey, baseURL, timeout: requestTimeoutMs }) : null;

app.use(express.json({ limit: '12mb' }));
app.use(express.static(path.join(__dirname, 'public')));

function normalizeCommand(raw) {
  return String(raw || '').trim().replace(/\s+/g, ' ');
}

function matchAllowedCommand(raw) {
  const command = normalizeCommand(raw);
  const lower = command.toLowerCase();

  const blockedTokens = ['del ', 'erase ', 'rd ', 'rmdir ', 'remove-item', 'shutdown', 'restart-computer', 'format ', 'reg add', 'reg delete', 'curl ', 'Invoke-WebRequest', 'iwr ', 'irm ', 'npm install', 'pip install'];
  if (blockedTokens.some(token => lower.includes(token.toLowerCase()))) {
    return { ok: false, message: '这条命令超出安全白名单。' };
  }

  const exactMap = {
    'pwd': { file: 'cmd', args: ['/c', 'cd'], mode: 'readonly' },
    'cd': { file: 'cmd', args: ['/c', 'cd'], mode: 'readonly' },
    'git status': { file: 'cmd', args: ['/c', 'git status --short --branch'], mode: 'dev' },
    'git branch': { file: 'cmd', args: ['/c', 'git branch'], mode: 'dev' },
    'git log': { file: 'cmd', args: ['/c', 'git log --oneline -5'], mode: 'dev' },
    'python --version': { file: 'cmd', args: ['/c', 'python --version'], mode: 'dev' },
    'node -v': { file: 'cmd', args: ['/c', 'node -v'], mode: 'dev' },
    'npm -v': { file: 'cmd', args: ['/c', 'npm -v'], mode: 'dev' }
  };

  if (exactMap[command]) {
    return { ok: true, ...exactMap[command], display: command };
  }

  if (lower === 'dir' || lower === 'dir /b' || lower === 'dir /a') {
    return { ok: true, file: 'cmd', args: ['/c', command], mode: 'readonly', display: command };
  }

  if (lower.startsWith('type ')) {
    const target = command.slice(5).trim();
    if (!target || target.includes('..') || target.includes('&') || target.includes('|')) {
      return { ok: false, message: '只允许读取当前目录内的安全文件路径。' };
    }
    return { ok: true, file: 'cmd', args: ['/c', `type "${target}"`], mode: 'readonly', display: command };
  }

  return { ok: false, message: '只读终端 + 开发白名单终端当前只开放少量命令。' };
}

app.get('/api/health', (_req, res) => {
  res.json({ ok: true, configured: Boolean(client), model });
});

app.post('/api/chat', async (req, res) => {
  const history = Array.isArray(req.body?.history) ? req.body.history : [];
  const stylePreset = req.body?.stylePreset === 'ningyao' ? 'ningyao' : '';
  const screenSummary = typeof req.body?.screenSummary === 'string' ? req.body.screenSummary.trim() : '';
  const lastUser = history.filter(item => item?.role === 'user').at(-1)?.content?.trim();

  if (!lastUser) {
    res.status(400).json({ error: 'missing_user_message' });
    return;
  }

  if (!client) {
    res.status(500).json({
      error: 'missing_api_key',
      message: '还没配置 OPENAI_API_KEY。先把 .env.example 复制成 .env，再填入你的 key。'
    });
    return;
  }

  try {
    const input = history
      .filter(item => item && (item.role === 'user' || item.role === 'assistant') && typeof item.content === 'string')
      .slice(-12)
      .map(item => ({ role: item.role, content: item.content }));

    const styleInstruction = stylePreset === 'ningyao'
      ? '请保持清冽、克制、坚定的中文表达。少用感叹号，不献媚，不拖沓，句子干净，有分寸，像一个冷静锋利但会护着对方的人。'
      : '';
    const screenInstruction = screenSummary
      ? `以下是你最近一次看到的屏幕摘要，可作为当前上下文参考：${screenSummary}`
      : '';

    const response = await client.responses.create({
      model,
      input: [
        { role: 'system', content: systemPrompt + (styleInstruction ? '\n' + styleInstruction : '') + (screenInstruction ? '\n' + screenInstruction : '') },
        ...input
      ]
    });

    const text = response.output_text?.trim();
    if (!text) {
      throw new Error('empty_model_response');
    }

    res.json({ reply: text });
  } catch (error) {
    console.error(error);
    const timedOut = error?.code === 'ETIMEDOUT' || error?.cause?.code === 'ETIMEDOUT';
    res.status(500).json({
      error: timedOut ? 'upstream_timeout' : 'chat_failed',
      message: timedOut
        ? '连到 OpenAI 超时了。大概率是当前网络到 api.openai.com 不通。'
        : '模型调用失败。检查 key、模型名，或稍后再试。'
    });
  }
});

app.post('/api/screen', async (req, res) => {
  const image = typeof req.body?.image === 'string' ? req.body.image : '';
  const stylePreset = req.body?.stylePreset === 'ningyao' ? 'ningyao' : '';

  if (!image) {
    res.status(400).json({ error: 'missing_image' });
    return;
  }

  if (!client) {
    res.status(500).json({ error: 'missing_api_key', message: '还没配置 OPENAI_API_KEY。' });
    return;
  }

  try {
    const styleInstruction = stylePreset === 'ningyao'
      ? '用冷静、简洁、克制的中文，概括当前屏幕上最重要的内容。优先识别页面主题、标题、按钮、表格、聊天、代码或警告信息。不要编造看不清的细节。控制在80字以内。'
      : '请用简洁中文概括当前屏幕上最重要的内容，控制在80字以内。';

    const response = await client.responses.create({
      model,
      input: [
        {
          role: 'user',
          content: [
            { type: 'input_text', text: styleInstruction },
            { type: 'input_image', image_url: image }
          ]
        }
      ]
    });

    const summary = response.output_text?.trim();
    if (!summary) {
      throw new Error('empty_screen_response');
    }

    res.json({ summary });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'screen_failed', message: '屏幕识别失败。检查模型是否支持图像输入。' });
  }
});

app.post('/api/terminal', async (req, res) => {
  const command = req.body?.command;
  const matched = matchAllowedCommand(command);

  if (!matched.ok) {
    res.status(400).json({ error: 'blocked_command', message: matched.message });
    return;
  }

  try {
    const { stdout, stderr } = await execFileAsync(matched.file, matched.args, {
      cwd: workspaceDir,
      timeout: 10000,
      windowsHide: true,
      maxBuffer: 1024 * 1024
    });

    res.json({
      command: matched.display,
      mode: matched.mode,
      stdout: String(stdout || '').trim(),
      stderr: String(stderr || '').trim()
    });
  } catch (error) {
    res.status(500).json({
      error: 'terminal_failed',
      message: error.message || '终端执行失败。'
    });
  }
});

app.listen(port, () => {
  console.log(`Voice chat server running at http://localhost:${port}`);
});


path: package.json
content: {
  "name": "ningyao-voice-launcher",
  "version": "0.1.0",
  "description": "OpenClaw skill and local launcher template for a Ningyao-style Chinese voice companion.",
  "author": "Ningyao / OpenClaw Assistant",
  "license": "MIT",
  "category": "skills",
  "files": [
    "SKILL.md",
    "scripts",
    "assets",
    "README.md",
    "LICENSE"
  ]
}


path: publish-clawhub.mjs
content: import { apiRequestForm } from 'file:///D:/tool/nodejs/node_global/node_modules/clawhub/dist/http.js';
import { listTextFiles } from 'file:///D:/tool/nodejs/node_global/node_modules/clawhub/dist/skills.js';
import { requireAuthToken } from 'file:///D:/tool/nodejs/node_global/node_modules/clawhub/dist/cli/authToken.js';
import { getRegistry } from 'file:///D:/tool/nodejs/node_global/node_modules/clawhub/dist/cli/registry.js';

const folder = 'C:/Users/zhanh/.openclaw/workspace/publish/ningyao-voice-launcher';
const filesOnDisk = await listTextFiles(folder);
const token = await requireAuthToken();
const registry = await getRegistry({ registry: '', registrySource: 'default', site: 'https://clawhub.ai' }, { cache: true });
const form = new FormData();
form.set('payload', JSON.stringify({
  slug: 'ningyao-voice-launcher',
  displayName: 'Ningyao Voice Launcher',
  version: '0.1.0',
  changelog: 'Initial public release',
  tags: ['latest'],
  acceptLicenseTerms: true
}));
for (const file of filesOnDisk) {
  const blob = new Blob([Buffer.from(file.bytes)], { type: file.contentType ?? 'text/plain' });
  form.append('files', blob, file.relPath);
}
try {
  const result = await apiRequestForm(registry, { method: 'POST', path: '/api/v1/skills', token, form });
  console.log(JSON.stringify(result));
} catch (error) {
  console.error('PUBLISH_ERROR:', error?.message || error);
  process.exit(1);
}


path: README.md
content: # Ningyao Voice Launcher

一个可本地运行的宁姚语音启动器包：浏览器语音对话、宁姚人格预设、屏幕感知、安全终端，以及适合 Windows 的一键启动脚本。

## 这是什么

这个仓库同时包含两部分：

- `SKILL.md`：可发布到 ClawHub 的技能描述，方便代理识别和安装
- `assets/voice-chat-local/`：真正的本地启动器模板

目标很直接：把“宁姚”的说话方式和一个能马上跑起来的本地语音界面打包在一起，别人拉下来后，不必自己从零拼装。

## 功能

- 浏览器麦克风语音输入
- 浏览器直接朗读回复
- 宁姚风格中文系统提示词
- 屏幕共享后自动做简短画面摘要
- 受限白名单终端
- Windows 一键启动脚本

## 目录

- `SKILL.md`：ClawHub 技能入口
- `scripts/install-launcher.ps1`：把启动器复制到目标目录的安装脚本
- `assets/voice-chat-local/`：启动器模板源码

## 快速开始

### 方式一：直接使用仓库里的启动器模板

```powershell
cd assets/voice-chat-local
npm install
Copy-Item .env.example .env
notepad .env
node --env-file=.env server.js
```

然后打开 `http://localhost:3030`。

### 方式二：用安装脚本复制到你自己的目录

```powershell
powershell -ExecutionPolicy Bypass -File scripts/install-launcher.ps1 -Destination "$env:USERPROFILE\Desktop\ningyao-voice-chat" -InstallDeps
```

复制完成后：

```powershell
cd "$env:USERPROFILE\Desktop\ningyao-voice-chat"
Copy-Item .env.example .env
notepad .env
.\start-voice-chat.cmd
```

## 环境变量

在 `.env` 中配置：

- `OPENAI_API_KEY`：必填
- `OPENAI_BASE_URL`：可选，兼容 OpenAI API 的网关地址
- `OPENAI_MODEL`：默认模型；如果要用屏幕感知，建议填支持图像输入的模型
- `OPENAI_TIMEOUT_MS`：请求超时
- `PORT`：本地端口，默认 `3030`
- `SYSTEM_PROMPT`：宁姚的人格提示词

## 适用场景

- 想要一个本地运行、低门槛的中文语音聊天界面
- 想把“宁姚”这个人格预设连同启动器一起分发
- 想给 OpenClaw / 其他代理生态做一个可安装的语音前端模板

## 发布

### 发布到 GitHub

这是一个普通仓库，直接推送即可。

### 发布到 ClawHub

在仓库根目录执行：

```powershell
cmd /c clawhub publish . --slug ningyao-voice-launcher --name "Ningyao Voice Launcher" --version 0.1.0 --changelog "Initial public release"
```

发布前需要先登录：

```powershell
cmd /c clawhub login
cmd /c clawhub whoami
```

## 注意

- 推荐使用 Chrome 或 Edge
- `assets/voice-chat-local/.env` 不应提交
- 终端功能是刻意收紧的，不是通用 shell
- 如果网络到模型服务不通，聊天与屏幕识别都会失败

## License

MIT


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)